mirror of
https://github.com/forcedotcom/afv-library.git
synced 2026-08-07 16:04:41 +08:00
ci: update package details and add GitHub workflows for skills validation and release
This commit is contained in:
parent
59f7832fa0
commit
aaefaa24e5
70
.github/workflows/release-skills.yml
vendored
Normal file
70
.github/workflows/release-skills.yml
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
name: release-skills
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['skills/**']
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skipped: ${{ steps.changelog.outputs.skipped }}
|
||||
version: ${{ steps.changelog.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Conventional Changelog Action
|
||||
id: changelog
|
||||
uses: TriPSs/conventional-changelog-action@v5
|
||||
with:
|
||||
git-user-name: svc-idee-bot
|
||||
git-user-email: svc_idee_bot@salesforce.com
|
||||
github-token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
tag-prefix: ""
|
||||
release-count: "0"
|
||||
skip-on-empty: ${{ github.event_name == 'push' }}
|
||||
version-file: "package.json"
|
||||
output-file: "CHANGELOG.md"
|
||||
|
||||
- name: Create Github Release
|
||||
id: release
|
||||
uses: ncipollo/release-action@v2
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' }}
|
||||
with:
|
||||
name: "${{ steps.changelog.outputs.version }}"
|
||||
tag: "${{ steps.changelog.outputs.version }}"
|
||||
commit: ${{ github.sha }}
|
||||
body: |
|
||||
## Changes in @salesforce/afv-skills
|
||||
|
||||
${{ steps.changelog.outputs.clean_changelog }}
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
skipIfReleaseExists: true
|
||||
|
||||
- name: Publish to npm
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' && steps.release.outputs.id != '' }}
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc
|
||||
npm publish --access public
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Notify cline-fork
|
||||
if: ${{ steps.changelog.outputs.skipped == 'false' }}
|
||||
uses: peter-evans/repository-dispatch@v3
|
||||
with:
|
||||
token: ${{ secrets.IDEE_GH_TOKEN }}
|
||||
repository: forcedotcom/cline-fork
|
||||
event-type: skills-released
|
||||
client-payload: '{"version": "${{ steps.changelog.outputs.version }}"}'
|
||||
10
.github/workflows/validate-pr.yml
vendored
Normal file
10
.github/workflows/validate-pr.yml
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
name: pr-validation
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, edited]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
pr-validation:
|
||||
uses: salesforcecli/github-workflows/.github/workflows/validatePR.yml@main
|
||||
16
.github/workflows/validate-skills.yml
vendored
Normal file
16
.github/workflows/validate-skills.yml
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
name: Validate Skills
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths: ['skills/**']
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: npm ci
|
||||
- run: node scripts/validate-skills.js
|
||||
12
package.json
12
package.json
@ -1,8 +1,14 @@
|
||||
{
|
||||
"name": "afv-library",
|
||||
"description": "AI prompts and rules library for Agentforce Vibes development",
|
||||
"private": true,
|
||||
"name": "@salesforce/afv-skills",
|
||||
"version": "1.0.0",
|
||||
"description": "Salesforce skills for Agentforce Vibes",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"skills/*/"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.0.0",
|
||||
"@commitlint/config-conventional": "^19.0.0",
|
||||
"@salesforce/webapp-template-app-react-sample-b2e-experimental": "*",
|
||||
"@salesforce/webapp-template-app-react-sample-b2x-experimental": "*"
|
||||
},
|
||||
|
||||
104
scripts/validate-skills.js
Normal file
104
scripts/validate-skills.js
Normal file
@ -0,0 +1,104 @@
|
||||
#!/usr/bin/env node
|
||||
// Validates the skills/ directory structure and SKILL.md format.
|
||||
// Exits with code 1 if any violations are found.
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const SKILLS_DIR = path.join(__dirname, "..", "skills")
|
||||
|
||||
let errors = []
|
||||
let checked = 0
|
||||
|
||||
function parseFrontmatter(content) {
|
||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
||||
if (!match) return null
|
||||
const raw = match[1]
|
||||
const result = {}
|
||||
for (const line of raw.split(/\r?\n/)) {
|
||||
const colonIdx = line.indexOf(":")
|
||||
if (colonIdx === -1) continue
|
||||
const key = line.slice(0, colonIdx).trim()
|
||||
const value = line.slice(colonIdx + 1).trim()
|
||||
result[key] = value
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getFrontmatterEnd(content) {
|
||||
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/)
|
||||
if (!match) return -1
|
||||
return match[0].length
|
||||
}
|
||||
|
||||
const topLevelEntries = fs.readdirSync(SKILLS_DIR)
|
||||
|
||||
for (const entry of topLevelEntries) {
|
||||
const entryPath = path.join(SKILLS_DIR, entry)
|
||||
const stat = fs.statSync(entryPath)
|
||||
|
||||
if (!stat.isDirectory()) {
|
||||
errors.push(`Loose file in skills/: ${entry} (expected only directories)`)
|
||||
continue
|
||||
}
|
||||
|
||||
const skillMdPath = path.join(entryPath, "SKILL.md")
|
||||
if (!fs.existsSync(skillMdPath)) {
|
||||
errors.push(`Missing SKILL.md in skills/${entry}/`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for nested subdirectories that also contain SKILL.md
|
||||
const subEntries = fs.readdirSync(entryPath)
|
||||
for (const sub of subEntries) {
|
||||
const subPath = path.join(entryPath, sub)
|
||||
if (fs.statSync(subPath).isDirectory()) {
|
||||
const nestedSkillMd = path.join(subPath, "SKILL.md")
|
||||
if (fs.existsSync(nestedSkillMd)) {
|
||||
errors.push(
|
||||
`Nested skill detected: skills/${entry}/${sub}/SKILL.md — skill directories must be exactly one level deep under skills/`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate SKILL.md frontmatter and body
|
||||
const content = fs.readFileSync(skillMdPath, "utf8")
|
||||
|
||||
const frontmatter = parseFrontmatter(content)
|
||||
if (!frontmatter) {
|
||||
errors.push(`skills/${entry}/SKILL.md: missing or malformed YAML frontmatter (expected --- ... --- block at top)`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (!frontmatter.name) {
|
||||
errors.push(`skills/${entry}/SKILL.md: missing "name" field in frontmatter`)
|
||||
} else if (frontmatter.name !== entry) {
|
||||
errors.push(
|
||||
`skills/${entry}/SKILL.md: "name" field ("${frontmatter.name}") does not match directory name ("${entry}")`
|
||||
)
|
||||
}
|
||||
|
||||
if (!frontmatter.description || frontmatter.description.trim() === "") {
|
||||
errors.push(`skills/${entry}/SKILL.md: missing or empty "description" field in frontmatter`)
|
||||
}
|
||||
|
||||
const frontmatterEnd = getFrontmatterEnd(content)
|
||||
const body = frontmatterEnd !== -1 ? content.slice(frontmatterEnd).trim() : ""
|
||||
if (!body) {
|
||||
errors.push(`skills/${entry}/SKILL.md: body (instructions after frontmatter) is empty`)
|
||||
}
|
||||
|
||||
checked++
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`\nSkill validation failed with ${errors.length} error(s):\n`)
|
||||
for (const err of errors) {
|
||||
console.error(` ✗ ${err}`)
|
||||
}
|
||||
console.error("")
|
||||
process.exit(1)
|
||||
} else {
|
||||
console.log(`Skill validation passed: ${checked} skill(s) checked.`)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user