Skip to content

ci: give main a run-unique concurrency group (#113) #223

ci: give main a run-unique concurrency group (#113)

ci: give main a run-unique concurrency group (#113) #223

Workflow file for this run

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 }}