Skip to content

Commit c4a994a

Browse files
committed
Initial skill migration with CI and gated release workflow
1 parent a320fbe commit c4a994a

22 files changed

Lines changed: 830 additions & 0 deletions

File tree

.github/workflows/skills-ci.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: skills-ci
2+
3+
on:
4+
push:
5+
paths:
6+
- 'skills/**'
7+
- 'scripts/**'
8+
- '.github/workflows/skills-*.yml'
9+
pull_request:
10+
paths:
11+
- 'skills/**'
12+
- 'scripts/**'
13+
- '.github/workflows/skills-*.yml'
14+
15+
jobs:
16+
validate:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.11'
23+
- uses: actions/setup-node@v4
24+
with:
25+
node-version: '20'
26+
- name: Validate skills
27+
run: python scripts/validate_skills.py
28+
- name: Smoke discovery
29+
run: npx --yes skills add . --list
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
name: skills-release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
paths:
7+
- 'skills/**'
8+
- 'scripts/**'
9+
- '.release-policy.yml'
10+
- '.github/workflows/skills-release.yml'
11+
workflow_dispatch:
12+
inputs:
13+
version_override:
14+
description: 'Optional explicit version tag (vX.Y.Z)'
15+
required: false
16+
type: string
17+
bump_type:
18+
description: 'Semver bump type when no version_override is set'
19+
required: false
20+
default: 'patch'
21+
type: choice
22+
options:
23+
- patch
24+
- minor
25+
- major
26+
- auto
27+
28+
permissions:
29+
contents: write
30+
31+
jobs:
32+
release:
33+
runs-on: ubuntu-latest
34+
steps:
35+
- uses: actions/checkout@v4
36+
with:
37+
fetch-depth: 0
38+
- uses: actions/setup-python@v5
39+
with:
40+
python-version: '3.11'
41+
42+
- name: Validate skills
43+
run: python scripts/validate_skills.py
44+
45+
- name: Read release policy
46+
id: policy
47+
run: |
48+
python - << 'PY'
49+
from pathlib import Path
50+
txt = Path('.release-policy.yml').read_text(encoding='utf-8')
51+
auto = 'auto_release: true' in txt
52+
with open(Path.cwd() / '.policy_out', 'w', encoding='utf-8') as f:
53+
f.write(f"auto_release={'true' if auto else 'false'}\n")
54+
PY
55+
cat .policy_out >> "$GITHUB_OUTPUT"
56+
57+
- name: Decide whether release is allowed
58+
id: gate
59+
run: |
60+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
61+
echo "should_release=true" >> "$GITHUB_OUTPUT"
62+
elif [ "${{ steps.policy.outputs.auto_release }}" = "true" ]; then
63+
echo "should_release=true" >> "$GITHUB_OUTPUT"
64+
else
65+
echo "should_release=false" >> "$GITHUB_OUTPUT"
66+
fi
67+
68+
- name: Stop (policy gate)
69+
if: steps.gate.outputs.should_release != 'true'
70+
run: echo "Release policy disabled for push events; exiting successfully."
71+
72+
- name: Determine bump from PR labels
73+
if: steps.gate.outputs.should_release == 'true' && github.event_name == 'push'
74+
id: prlabels
75+
uses: actions/github-script@v7
76+
with:
77+
script: |
78+
const {owner, repo} = context.repo;
79+
let bump = 'patch';
80+
try {
81+
const prs = await github.rest.repos.listPullRequestsAssociatedWithCommit({
82+
owner,
83+
repo,
84+
commit_sha: context.sha,
85+
});
86+
if (prs.data && prs.data.length > 0) {
87+
const labels = (prs.data[0].labels || []).map(l => l.name);
88+
if (labels.includes('release:major')) bump = 'major';
89+
else if (labels.includes('release:minor')) bump = 'minor';
90+
}
91+
} catch (e) {
92+
core.warning(`Could not infer PR labels: ${e.message}`);
93+
}
94+
core.setOutput('bump', bump);
95+
96+
- name: Compute version
97+
if: steps.gate.outputs.should_release == 'true'
98+
id: version
99+
env:
100+
INPUT_BUMP: ${{ github.event.inputs.bump_type }}
101+
INPUT_VERSION: ${{ github.event.inputs.version_override }}
102+
PUSH_BUMP: ${{ steps.prlabels.outputs.bump }}
103+
run: |
104+
if [ -n "${INPUT_VERSION:-}" ]; then
105+
VERSION="$INPUT_VERSION"
106+
else
107+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
108+
BUMP="${INPUT_BUMP:-patch}"
109+
else
110+
BUMP="${PUSH_BUMP:-patch}"
111+
fi
112+
VERSION=$(python scripts/compute_next_version.py --bump "$BUMP")
113+
fi
114+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
115+
116+
- name: Check tag does not already exist
117+
if: steps.gate.outputs.should_release == 'true'
118+
run: |
119+
if git rev-parse "${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
120+
echo "Tag ${{ steps.version.outputs.version }} already exists" >&2
121+
exit 1
122+
fi
123+
124+
- name: Determine release range
125+
if: steps.gate.outputs.should_release == 'true'
126+
id: range
127+
run: |
128+
PREV=$(git tag --list 'v*' --sort=-v:refname | head -n 1 || true)
129+
if [ -n "$PREV" ]; then
130+
FROM="$PREV"
131+
else
132+
FROM=$(git rev-list --max-parents=0 HEAD)
133+
fi
134+
echo "from_ref=$FROM" >> "$GITHUB_OUTPUT"
135+
echo "to_ref=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
136+
137+
- name: Build release notes
138+
if: steps.gate.outputs.should_release == 'true'
139+
run: |
140+
python scripts/build_release_notes.py \
141+
--from-ref "${{ steps.range.outputs.from_ref }}" \
142+
--to-ref "${{ steps.range.outputs.to_ref }}" \
143+
--version "${{ steps.version.outputs.version }}" \
144+
--repo "${{ github.repository }}" \
145+
--output RELEASE_NOTES.md
146+
147+
- name: Create and push tag
148+
if: steps.gate.outputs.should_release == 'true'
149+
run: |
150+
git config user.name "github-actions[bot]"
151+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
152+
git tag "${{ steps.version.outputs.version }}" "${GITHUB_SHA}"
153+
git push origin "${{ steps.version.outputs.version }}"
154+
155+
- name: Create GitHub release
156+
if: steps.gate.outputs.should_release == 'true'
157+
uses: softprops/action-gh-release@v2
158+
with:
159+
tag_name: ${{ steps.version.outputs.version }}
160+
name: ${{ steps.version.outputs.version }}
161+
body_path: RELEASE_NOTES.md

.release-policy.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
auto_release: false

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Gecode
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Gecode Skills
2+
3+
Canonical skill repository for Gecode-focused AI agent skills.
4+
5+
Install with:
6+
7+
```bash
8+
npx skills add Gecode/gecode-skills
9+
```
10+
11+
List available skills:
12+
13+
```bash
14+
npx skills add Gecode/gecode-skills --list
15+
```
16+
17+
Install a single skill:
18+
19+
```bash
20+
npx skills add Gecode/gecode-skills --skill gecode-modeling
21+
```
22+
23+
## Available Skills
24+
25+
- `gecode-general-knowledge`
26+
- `gecode-modeling`
27+
- `gecode-propagator-implementation`
28+
- `gecode-brancher-implementation`
29+
- `gecode-memory-handling`
30+
- `gecode-search-engines`
31+
- `gecode-search-engine-implementation`
32+
33+
## Contributing
34+
35+
### Skill structure
36+
37+
Each skill must be under:
38+
39+
- `skills/<skill-name>/SKILL.md`
40+
41+
Optional metadata for UIs can be added at:
42+
43+
- `skills/<skill-name>/agents/openai.yaml`
44+
45+
### Required frontmatter
46+
47+
Each `SKILL.md` must include YAML frontmatter with:
48+
49+
- `name`
50+
- `description`
51+
52+
The `name` must match the directory name (`<skill-name>`).
53+
54+
### Release bump labels
55+
56+
Auto-release determines semver bump from PR labels:
57+
58+
- `release:major` -> major bump
59+
- `release:minor` -> minor bump
60+
- no label -> patch bump
61+
62+
## Release policy
63+
64+
Releases are controlled by `.release-policy.yml`.
65+
66+
- Initially: `auto_release: false`
67+
- This means pushes to `main` do **not** auto-release.
68+
- Manual release via workflow dispatch is enabled.
69+
70+
To enable auto-release later, set:
71+
72+
```yaml
73+
auto_release: true
74+
```
75+
76+
and merge that change with maintainer review.

scripts/build_release_notes.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import subprocess
6+
from pathlib import Path
7+
8+
9+
def changed_skills(diff_range: str) -> list[str]:
10+
out = subprocess.check_output(["git", "diff", "--name-only", diff_range], text=True)
11+
names: set[str] = set()
12+
for line in out.splitlines():
13+
parts = line.split("/")
14+
if len(parts) >= 3 and parts[0] == "skills" and parts[1].startswith("gecode-"):
15+
names.add(parts[1])
16+
return sorted(names)
17+
18+
19+
def main() -> int:
20+
ap = argparse.ArgumentParser()
21+
ap.add_argument("--from-ref", required=True)
22+
ap.add_argument("--to-ref", required=True)
23+
ap.add_argument("--version", required=True)
24+
ap.add_argument("--repo", required=True, help="owner/repo")
25+
ap.add_argument("--output", required=True)
26+
args = ap.parse_args()
27+
28+
diff_range = f"{args.from_ref}..{args.to_ref}"
29+
skills = changed_skills(diff_range)
30+
31+
lines = []
32+
lines.append(f"# {args.version}")
33+
lines.append("")
34+
if skills:
35+
lines.append("## Changed skills")
36+
lines.append("")
37+
for s in skills:
38+
lines.append(f"- `{s}`")
39+
lines.append("")
40+
else:
41+
lines.append("No skill directory changes detected in this release range.")
42+
lines.append("")
43+
44+
lines.append("## Install")
45+
lines.append("")
46+
lines.append(f"```bash\nnpx skills add {args.repo}\n```")
47+
lines.append("")
48+
lines.append("List available skills:")
49+
lines.append("")
50+
lines.append(f"```bash\nnpx skills add {args.repo} --list\n```")
51+
52+
Path(args.output).write_text("\n".join(lines) + "\n", encoding="utf-8")
53+
return 0
54+
55+
56+
if __name__ == "__main__":
57+
raise SystemExit(main())

0 commit comments

Comments
 (0)