ci: give main a run-unique concurrency group (#113) #223
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 🧪 CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| # On main each run is that commit's verification record, so it gets a run-unique | |
| # group and nothing there queues behind or evicts anything else. Disabling | |
| # cancellation alone would not be enough: a concurrency group holds one running | |
| # plus one pending run, and a third push cancels the pending one, so a second | |
| # push's checks would still be lost. A cancelled check is not a failed check, so | |
| # main would report green over verification that never completed. | |
| # | |
| # Pull requests keep the ref-keyed group, so superseded runs there still cancel. | |
| group: "${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.run_id || github.ref }}" | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| permissions: {} | |
| jobs: | |
| validate-manifests: | |
| name: Validate manifests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: 📄 Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: ✅ Validate manifests | |
| # Single source of truth in scripts/validate-manifests.sh (documented in | |
| # AGENTS.md, self-tested in the lint-scripts job) — no inline/doc drift. | |
| run: ./scripts/validate-manifests.sh | |
| lint-scripts: | |
| name: Lint scripts | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: 📄 Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: 🐚 ShellCheck scripts | |
| run: shellcheck scripts/*.sh | |
| - name: 🧪 Self-test the version-bump guard | |
| # Proves check-plugin-version-bump.sh PASSES what it must not block (a bumped | |
| # plugin, a new plugin, a deleted plugin, an unrelated change) and FAILS every | |
| # drift it exists to catch — including a second unbumped plugin in the same | |
| # change, which an early-exit refactor would silently hide. | |
| run: ./scripts/check-plugin-version-bump.test.sh | |
| - name: 🧪 Self-test the version-bump helper | |
| # Proves bump-plugin-version.sh moves the version in ALL FOUR manifests that must | |
| # agree, that --changed-since bumps exactly what moved and is idempotent on a | |
| # re-run, and that it fails closed on a bad plugin/level/non-semver version. | |
| run: ./scripts/bump-plugin-version.test.sh | |
| - name: 🧪 Self-test the manifest guard | |
| # Proves validate-manifests.sh PASSES a consistent fixture and FAILS each | |
| # drift scenario it exists to catch (malformed/desynced manifests, every | |
| # plugin.json completeness rule, every manifest↔plugins lockstep rule), so | |
| # a refactor that silently weakens a check is caught here — not by a broken | |
| # plugin reaching consumers. Self-contained: throwaway fixtures, no network. | |
| run: ./scripts/validate-manifests.test.sh | |
| - name: 🧪 Self-test skill helper scripts | |
| # Runs every hermetic *.test.sh bundled alongside a skill's helper scripts | |
| # (plugins/**/skills/**/scripts/), so a regression in a script that ships | |
| # to users is caught here rather than mid-audit. New script tests are picked | |
| # up automatically — no workflow edit needed. Self-contained: each test | |
| # stubs its external tools (no network, no cluster). | |
| run: | | |
| shopt -s nullglob | |
| tests=(plugins/*/skills/*/scripts/*.test.sh) | |
| if [ ${#tests[@]} -eq 0 ]; then | |
| echo "No skill helper-script self-tests found." | |
| exit 0 | |
| fi | |
| for t in "${tests[@]}"; do | |
| echo "::group::$t" | |
| bash "$t" | |
| echo "::endgroup::" | |
| done | |
| check-version-bump: | |
| name: Check version bump | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: 📄 Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| # The guard diffs against the base commit, so it needs real history — a | |
| # shallow checkout would make it fail closed on every run. | |
| fetch-depth: 0 | |
| - name: 🔢 Verify changed plugins moved their version | |
| # Consumers cache plugins by <marketplace>/<plugin>/<version>, so shipping a | |
| # content change on an unchanged version is unreachable for anyone who already | |
| # installed it. This is a PR gate: on a push to main the change has already | |
| # merged, so an absent base is reported and passed rather than failing the branch. | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} | |
| run: | | |
| if [ -z "$BASE_SHA" ] || ! git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then | |
| echo "No usable base commit for this event — skipping (PR gate only)." | |
| exit 0 | |
| fi | |
| ./scripts/check-plugin-version-bump.sh "$BASE_SHA" HEAD | |
| discover-skills: | |
| name: Discover skills | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| skills: ${{ steps.list.outputs.skills }} | |
| steps: | |
| - name: 📄 Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: 📂 List skill directories | |
| id: list | |
| run: | | |
| skills=$(find plugins -mindepth 4 -maxdepth 4 -name SKILL.md -printf '%h\n' \ | |
| | sed 's|^\./||' \ | |
| | sort \ | |
| | jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| if ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$skills"; then | |
| echo "::error::Skill discovery produced invalid output." | |
| exit 1 | |
| fi | |
| echo "skills=$skills" >> "$GITHUB_OUTPUT" | |
| echo "Discovered: $skills" | |
| validate-spec: | |
| name: Validate spec (${{ matrix.skill }}) | |
| needs: discover-skills | |
| if: needs.discover-skills.outputs.skills != '[]' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| skill: ${{ fromJson(needs.discover-skills.outputs.skills) }} | |
| steps: | |
| - name: 📄 Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: 🐍 Set up Python | |
| uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - name: 📦 Install skills-ref | |
| env: | |
| AGENTSKILLS_REF: 8d8fcbc69e0c42e05922c2ffc287a3bbdef7b0a3 | |
| run: | | |
| python -m pip install --disable-pip-version-check \ | |
| "skills-ref @ git+https://github.com/agentskills/agentskills.git@${AGENTSKILLS_REF}#subdirectory=skills-ref" | |
| - name: ✅ Validate ${{ matrix.skill }} against agentskills.io spec | |
| env: | |
| SKILL: ${{ matrix.skill }} | |
| run: skills-ref validate "$SKILL" | |
| ci-required-checks: | |
| name: CI - Required Checks | |
| runs-on: ubuntu-latest | |
| permissions: | |
| checks: read | |
| statuses: read | |
| pull-requests: read | |
| timeout-minutes: 5 | |
| needs: [validate-manifests, lint-scripts, check-version-bump, discover-skills, validate-spec] | |
| if: ${{ always() }} | |
| steps: | |
| - uses: devantler-tech/actions/require-checks-in-pr@1f66c91d45d374ceac9fe830a783444ebc9be958 # v3.2.0 | |
| with: | |
| job-results: >- | |
| ${{ needs.validate-manifests.result }} | |
| ${{ needs.lint-scripts.result }} | |
| ${{ needs.check-version-bump.result }} | |
| ${{ needs.discover-skills.result }} | |
| ${{ needs.validate-spec.result }} |