Skip to content

Commit 22b307e

Browse files
feat(ci): add GitHub Actions workflow for manual repository maintenance
Add skills-manage workflow and action template for non-interactive maintenance operations from GitHub UI. The new workflow supports add, remove, and sync operations with configurable parameters. - Add skills-manage.yml workflow template with workflow_dispatch inputs for operation, skill-name, source, allow-missing-sources, and run-tests - Add skillsbase-manage composite action template with Node setup, dependency installation, and CLI command execution - Refactor skills-sync.yml to use the new skillsbase-sync composite action for consistency - Add GithubActionTemplateTarget interface to templates.ts for type safety - Update writeGithubActions function to generate both sync and manage workflow/action pairs - Update maintainer-workflow.md documentation to describe the new GitHub Maintenance Path - Add test assertions for new manage workflow and action files - Add test for kind-specific generation to verify workflow and action pairing Co-Authored-By: Hagicode <noreply@hagicode.com> Signed-off-by: newbe36524 <newbe36524@qq.com>
1 parent 4f10d19 commit 22b307e

6 files changed

Lines changed: 252 additions & 33 deletions

File tree

src/lib/templates.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ interface WriteGithubActionsOptions extends WriteManagedFileOptions {
2626
kind?: GithubActionKind;
2727
}
2828

29+
interface GithubActionTemplateTarget {
30+
relativePath: string;
31+
template: string;
32+
}
33+
2934
async function resolveTemplateRoot(): Promise<string> {
3035
for (const candidate of templateRootCandidates) {
3136
if (await pathExists(candidate)) {
@@ -98,19 +103,31 @@ export async function writeGithubActions(
98103
NODE_VERSION: DEFAULT_NODE_VERSION,
99104
};
100105

101-
const targets: Array<{ relativePath: string; template: string }> = [];
106+
const targets: GithubActionTemplateTarget[] = [];
102107
if (kind === "workflow" || kind === "all") {
103-
targets.push({
104-
relativePath: path.join(".github", "workflows", "skills-sync.yml"),
105-
template: path.join("workflows", "skills-sync.yml"),
106-
});
108+
targets.push(
109+
{
110+
relativePath: path.join(".github", "workflows", "skills-sync.yml"),
111+
template: path.join("workflows", "skills-sync.yml"),
112+
},
113+
{
114+
relativePath: path.join(".github", "workflows", "skills-manage.yml"),
115+
template: path.join("workflows", "skills-manage.yml"),
116+
},
117+
);
107118
}
108119

109-
if (kind === "action" || kind === "all") {
110-
targets.push({
111-
relativePath: path.join(".github", "actions", "skillsbase-sync", "action.yml"),
112-
template: path.join("actions", "skillsbase-sync", "action.yml"),
113-
});
120+
if (kind === "workflow" || kind === "action" || kind === "all") {
121+
targets.push(
122+
{
123+
relativePath: path.join(".github", "actions", "skillsbase-sync", "action.yml"),
124+
template: path.join("actions", "skillsbase-sync", "action.yml"),
125+
},
126+
{
127+
relativePath: path.join(".github", "actions", "skillsbase-manage", "action.yml"),
128+
template: path.join("actions", "skillsbase-manage", "action.yml"),
129+
},
130+
);
114131
}
115132

116133
if (targets.length === 0) {
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Managed by skillsbase CLI.
2+
3+
name: skillsbase-manage
4+
description: Run a managed add, remove, or sync operation for a skillsbase repository
5+
6+
inputs:
7+
node-version:
8+
description: Node.js version used for maintenance
9+
required: false
10+
default: "{{NODE_VERSION}}"
11+
operation:
12+
description: Maintenance operation to run (`add`, `remove`, or `sync`)
13+
required: true
14+
skill-name:
15+
description: Skill name for `add` or `remove`
16+
required: false
17+
default: ""
18+
source:
19+
description: Optional source key for `add` or `remove`
20+
required: false
21+
default: ""
22+
allow-missing-sources:
23+
description: Skip missing local roots during the operation
24+
required: false
25+
default: "false"
26+
run-tests:
27+
description: Whether to run npm test after the maintenance command
28+
required: false
29+
default: "true"
30+
31+
runs:
32+
using: composite
33+
steps:
34+
- name: Setup Node.js
35+
uses: actions/setup-node@v4
36+
with:
37+
node-version: ${{ inputs.node-version }}
38+
cache: npm
39+
40+
- name: Install dependencies
41+
shell: bash
42+
run: npm ci
43+
44+
- name: Upgrade npm
45+
shell: bash
46+
run: npm install --global npm@10.9.2
47+
48+
- name: Install skillsbase
49+
shell: bash
50+
run: npm install --global @hagicode/skillsbase
51+
52+
- name: Run maintenance command
53+
shell: bash
54+
env:
55+
OPERATION: ${{ inputs.operation }}
56+
SKILL_NAME: ${{ inputs.skill-name }}
57+
SOURCE: ${{ inputs.source }}
58+
ALLOW_MISSING_SOURCES: ${{ inputs.allow-missing-sources }}
59+
run: |
60+
set -euo pipefail
61+
62+
base_args=(--repo .)
63+
if [[ "${ALLOW_MISSING_SOURCES}" == "true" ]]; then
64+
base_args+=(--allow-missing-sources)
65+
fi
66+
67+
case "${OPERATION}" in
68+
add|remove)
69+
if [[ -z "${SKILL_NAME}" ]]; then
70+
echo "::error::skill-name is required when operation=${OPERATION}."
71+
exit 1
72+
fi
73+
74+
command=(skillsbase "${OPERATION}" "${SKILL_NAME}" "${base_args[@]}")
75+
if [[ -n "${SOURCE}" ]]; then
76+
command+=(--source "${SOURCE}")
77+
fi
78+
;;
79+
sync)
80+
command=(skillsbase sync "${base_args[@]}")
81+
;;
82+
*)
83+
echo "::error::Unsupported operation: ${OPERATION}."
84+
exit 1
85+
;;
86+
esac
87+
88+
printf 'Running:'
89+
printf ' %q' "${command[@]}"
90+
printf '\n'
91+
"${command[@]}"
92+
93+
- name: Run tests
94+
if: ${{ inputs.run-tests == 'true' }}
95+
shell: bash
96+
run: npm test

templates/docs/maintainer-workflow.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,31 @@
22

33
# Maintainer Workflow
44

5-
The maintainer flow is `init -> add -> sync -> github_action`.
5+
The maintainer flow is `init -> add/remove -> sync -> github_action`.
66

77
## Lifecycle
88

99
1. `skillsbase init`
10-
2. `skillsbase add <skill-name>`
10+
2. `skillsbase add <skill-name>` or `skillsbase remove <skill-name>`
1111
3. `skillsbase sync`
1212
4. `skillsbase github_action --kind all`
1313

14+
## GitHub Maintenance Path
15+
16+
Use `.github/workflows/skills-manage.yml` only for explicit non-interactive maintenance from GitHub UI.
17+
18+
- `operation` chooses `add`, `remove`, or `sync`
19+
- `skill-name` is required for `add` and `remove`
20+
- `source` is optional and maps to `--source`
21+
- `allow-missing-sources` maps to `--allow-missing-sources`
22+
- `run-tests` controls the post-operation `npm test`
23+
- The workflow does not commit, push, or open pull requests
24+
1425
## Notes
1526

1627
- `sources.yaml` is the single source of truth.
1728
- `skills/` stores managed output only.
1829
- `.skill-source.json` records source and conversion metadata.
1930
- `skillsbase sync --check` validates drift without writing files.
2031
- If a source root is unavailable, use `skillsbase sync --allow-missing-sources` to skip it.
32+
- Local CLI commands remain the primary maintainer path.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Managed by skillsbase CLI.
2+
3+
name: Skills Manage
4+
5+
on:
6+
workflow_dispatch:
7+
inputs:
8+
operation:
9+
description: Operation to run (`add`, `remove`, or `sync`)
10+
required: true
11+
type: choice
12+
default: sync
13+
options:
14+
- add
15+
- remove
16+
- sync
17+
skill-name:
18+
description: Skill name for `add` or `remove`
19+
required: false
20+
type: string
21+
source:
22+
description: Optional source key for `add` or `remove`
23+
required: false
24+
type: string
25+
allow-missing-sources:
26+
description: Skip missing local roots during the operation
27+
required: false
28+
type: boolean
29+
default: false
30+
run-tests:
31+
description: Run `npm test` after the maintenance command
32+
required: false
33+
type: boolean
34+
default: true
35+
36+
permissions:
37+
contents: read
38+
39+
jobs:
40+
manage:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- name: Checkout repository
44+
uses: actions/checkout@v4
45+
46+
- name: Run skillsbase maintenance
47+
uses: ./.github/actions/skillsbase-manage
48+
with:
49+
node-version: {{NODE_VERSION}}
50+
operation: ${{ inputs.operation }}
51+
skill-name: ${{ inputs.skill-name }}
52+
source: ${{ inputs.source }}
53+
allow-missing-sources: ${{ inputs.allow-missing-sources }}
54+
run-tests: ${{ inputs.run-tests }}

templates/workflows/skills-sync.yml

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,8 @@ jobs:
2323
- name: Checkout repository
2424
uses: actions/checkout@v4
2525

26-
- name: Setup Node.js
27-
uses: actions/setup-node@v4
26+
- name: Run skillsbase validation
27+
uses: ./.github/actions/skillsbase-sync
2828
with:
2929
node-version: {{NODE_VERSION}}
30-
cache: npm
31-
32-
- name: Install dependencies
33-
run: npm ci
34-
35-
- name: Upgrade npm
36-
run: npm install --global npm@10.9.2
37-
38-
- name: Install skillsbase
39-
run: npm install --global @hagicode/skillsbase
40-
41-
- name: Run tests
42-
run: npm test
43-
44-
- name: Validate managed repository state
45-
run: skillsbase sync --check --repo .
30+
run-tests: "true"

tests/skillsbase.test.ts

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,9 @@ test("init creates the managed repository baseline", async () => {
201201
await fs.access(path.join(repoPath, "skills", "README.md"));
202202
await fs.access(path.join(repoPath, "docs", "maintainer-workflow.md"));
203203
await fs.access(path.join(repoPath, ".github", "workflows", "skills-sync.yml"));
204+
await fs.access(path.join(repoPath, ".github", "workflows", "skills-manage.yml"));
204205
await fs.access(path.join(repoPath, ".github", "actions", "skillsbase-sync", "action.yml"));
206+
await fs.access(path.join(repoPath, ".github", "actions", "skillsbase-manage", "action.yml"));
205207
assert.match(await read("sources.yaml", repoPath), /remoteRepository: example\/skillsbase/);
206208
});
207209

@@ -500,15 +502,68 @@ test("github_action writes managed workflow and action assets", async () => {
500502
});
501503

502504
assert.equal(result.exitCode, 0);
503-
assert.match(await read(".github/workflows/skills-sync.yml", repoPath), /Managed by skillsbase CLI/);
505+
const [syncWorkflow, manageWorkflow, syncAction, manageAction] = await Promise.all([
506+
read(".github/workflows/skills-sync.yml", repoPath),
507+
read(".github/workflows/skills-manage.yml", repoPath),
508+
read(".github/actions/skillsbase-sync/action.yml", repoPath),
509+
read(".github/actions/skillsbase-manage/action.yml", repoPath),
510+
]);
511+
512+
assert.match(syncWorkflow, /Managed by skillsbase CLI/);
513+
assert.match(syncWorkflow, /uses: \.\/\.github\/actions\/skillsbase-sync/);
514+
assert.match(syncWorkflow, /run-tests: "true"/);
515+
516+
assert.match(manageWorkflow, /workflow_dispatch:/);
517+
assert.match(manageWorkflow, /operation:/);
518+
assert.match(manageWorkflow, /skill-name:/);
519+
assert.match(manageWorkflow, /allow-missing-sources:/);
520+
assert.match(manageWorkflow, /uses: \.\/\.github\/actions\/skillsbase-manage/);
521+
504522
assert.match(
505-
await read(".github/actions/skillsbase-sync/action.yml", repoPath),
523+
syncAction,
506524
/npm install --global @hagicode\/skillsbase/,
507525
);
508526
assert.match(
509-
await read(".github/actions/skillsbase-sync/action.yml", repoPath),
527+
syncAction,
510528
/skillsbase sync --check --repo \./,
511529
);
530+
assert.match(manageAction, /operation:\n description:/);
531+
assert.match(manageAction, /skill-name:\n description:/);
532+
assert.match(manageAction, /allow-missing-sources:/);
533+
assert.match(manageAction, /case "\$\{OPERATION\}" in/);
534+
assert.match(manageAction, /command=\(skillsbase "\$\{OPERATION\}"/);
535+
assert.match(manageAction, /command=\(skillsbase sync "\$\{base_args\[@\]\}"\)/);
536+
});
537+
538+
test("github_action kind-specific generation keeps workflow and action pairs together", async () => {
539+
const tempRoot = await createTempDir();
540+
const workflowRepoPath = path.join(tempRoot, "workflow-repo");
541+
await fs.mkdir(workflowRepoPath, { recursive: true });
542+
543+
const workflowResult = await runCommand({
544+
cwd: workflowRepoPath,
545+
args: ["github_action", "--repo", workflowRepoPath, "--kind", "workflow"],
546+
});
547+
548+
assert.equal(workflowResult.exitCode, 0);
549+
assert.equal(await exists(path.join(workflowRepoPath, ".github", "workflows", "skills-sync.yml")), true);
550+
assert.equal(await exists(path.join(workflowRepoPath, ".github", "workflows", "skills-manage.yml")), true);
551+
assert.equal(await exists(path.join(workflowRepoPath, ".github", "actions", "skillsbase-sync", "action.yml")), true);
552+
assert.equal(await exists(path.join(workflowRepoPath, ".github", "actions", "skillsbase-manage", "action.yml")), true);
553+
554+
const actionRepoPath = path.join(tempRoot, "action-repo");
555+
await fs.mkdir(actionRepoPath, { recursive: true });
556+
557+
const actionResult = await runCommand({
558+
cwd: actionRepoPath,
559+
args: ["github_action", "--repo", actionRepoPath, "--kind", "action"],
560+
});
561+
562+
assert.equal(actionResult.exitCode, 0);
563+
assert.equal(await exists(path.join(actionRepoPath, ".github", "actions", "skillsbase-sync", "action.yml")), true);
564+
assert.equal(await exists(path.join(actionRepoPath, ".github", "actions", "skillsbase-manage", "action.yml")), true);
565+
assert.equal(await exists(path.join(actionRepoPath, ".github", "workflows", "skills-sync.yml")), false);
566+
assert.equal(await exists(path.join(actionRepoPath, ".github", "workflows", "skills-manage.yml")), false);
512567
});
513568

514569
test("sync fails with actionable diagnostics when the manifest is missing", async () => {

0 commit comments

Comments
 (0)