Skip to content

feat(create-gh-pages-site): support Spectator specs #83

feat(create-gh-pages-site): support Spectator specs

feat(create-gh-pages-site): support Spectator specs #83

Workflow file for this run

name: Skill Lint
on:
pull_request:
# Anchored so the push trigger below reuses one list. Actions supports YAML
# anchors and aliases; only merge keys (`<<:`) remain unsupported.
paths: &skill_lint_paths
- "skills/**/SKILL.md"
- "skills/**/package.json"
- "skills/**/test/**"
- "skills/**/*.yaml"
- "skills/**/*.yml"
- "skills/**/eval.yaml"
# Reference guidance and shipped scripts are part of the skill's
# behaviour, so a change to either must be linted. Without these, a PR
# that edits only references/ receives no CI at all: the skill still
# lints clean by luck because nothing in the filter changed, and a broken
# cross-link or a deleted reference reaches main unchecked.
- "skills/**/references/**"
- "skills/**/scripts/**"
- "docs/specs/git-tidy/**"
- "site/src/content/skills/**"
- "site/public/images/**"
- "site/package.json"
- "site/package-lock.json"
- ".github/tools/vally/**"
- "README.md"
- "marketplace.json"
- "plugin.json"
- ".agents/plugins/**"
- ".codex-plugin/**"
- ".claude-plugin/**"
- ".cursor-plugin/**"
- "gemini-extension.json"
- "test/**"
- "mcp.json"
- ".mcp.json"
- ".lsp.json"
- "settings.json"
- "hooks/**"
- "agents/**"
- "commands/**"
- "workflows/**"
- "output-styles/**"
- "themes/**"
- "monitors/**"
- "bin/**"
- "com.github.copilot/**"
# Workflow policy tests inspect every workflow and must run whenever any
# workflow changes.
- ".github/workflows/**"
push:
branches: [main]
paths: *skill_lint_paths
workflow_dispatch:
permissions:
contents: read
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
# Node 24, not 22. Vally declares `npm>=11.11.1`, and Node 22 still
# bundles npm 10.x, which makes every install log an EBADENGINE warning.
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm
cache-dependency-path: "**/package-lock.json"
# Use the locked CI toolchain instead of a floating global install.
- name: Install Vally CLI
run: |
npm ci --prefix .github/tools/vally --ignore-scripts
echo "$PWD/.github/tools/vally/node_modules/.bin" >> "$GITHUB_PATH"
- name: Validate distribution manifests
run: node --test test/*.test.mjs
- name: Determine skills to lint
id: skills
env:
# Routed through env for the same reason the steps below do it: no
# workflow context value is interpolated straight into a shell script.
BASE_SHA: ${{ github.event.pull_request.base.sha }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
if [ "$EVENT_NAME" = "pull_request" ]; then
# Lint only skills with changes in this PR. Keep this list in step
# with the `paths` filter above: a path that triggers the workflow
# but is missing here selects no skill, so the job runs and lints
# nothing while still reporting success.
CHANGED=$(git diff --name-only "$BASE_SHA" HEAD)
if printf '%s\n' "$CHANGED" \
| grep -Eq '^(README\.md|marketplace\.json|plugin\.json)$'; then
DIRS=$(find skills -maxdepth 2 -name "SKILL.md" -printf '%h\n' | sort -u)
else
DIRS=$(printf '%s\n' "$CHANGED" \
| grep -E '^skills/[^/]+/(SKILL\.md|package\.json|.*\.ya?ml|evals/|test/|references/|scripts/)' \
| sed 's|^\(skills/[^/]*\)/.*|\1|' | sort -u || true)
if printf '%s\n' "$CHANGED" | grep -Eq '^docs/specs/git-tidy/'; then
DIRS=$(printf '%s\n%s\n' "$DIRS" "skills/git-tidy" \
| sed '/^$/d' | sort -u)
fi
fi
else
# Push to main or manual dispatch: lint all skills
DIRS=$(find skills -maxdepth 2 -name "SKILL.md" -printf '%h\n' | sort -u)
fi
while IFS= read -r dir; do
[ -z "$dir" ] && continue
if [[ ! "$dir" =~ ^skills/[a-z0-9]+(-[a-z0-9]+)*$ ]]; then
echo "::error::Refusing non-canonical skill directory."
exit 1
fi
done <<< "$DIRS"
if [ -z "$DIRS" ]; then
printf 'dirs=\n' >> "$GITHUB_OUTPUT"
echo "No skill directories to lint"
else
{
echo "dirs<<EOF"
echo "$DIRS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
echo "Will lint: $DIRS"
fi
- name: Lint skills (spec-compliance)
if: steps.skills.outputs.dirs != ''
env:
SKILL_DIRS: ${{ steps.skills.outputs.dirs }}
run: |
FAILED=0
while IFS= read -r dir; do
echo "::group::Linting $dir"
if ! vally lint "$dir"; then
FAILED=1
fi
echo "::endgroup::"
done <<< "$SKILL_DIRS"
exit $FAILED
- name: Validate eval specs
if: steps.skills.outputs.dirs != ''
env:
SKILL_DIRS: ${{ steps.skills.outputs.dirs }}
run: |
while IFS= read -r dir; do
while IFS= read -r -d '' spec; do
echo "Validating $spec"
vally lint --eval-spec "$spec" --strict
done < <(
find "$dir" -name "eval.yaml" -path "*/evals/*" -print0 2>/dev/null
)
done <<< "$SKILL_DIRS"
- name: Run deterministic skill tests
if: steps.skills.outputs.dirs != ''
env:
SKILL_DIRS: ${{ steps.skills.outputs.dirs }}
run: |
while IFS= read -r dir; do
if [ -f "$dir/package.json" ] && npm pkg get scripts.test --prefix "$dir" | grep -qv '{}'; then
if [ "$(npm pkg get dependencies --prefix "$dir")" != '{}' ]; then
npm ci --prefix "$dir" --omit=dev --ignore-scripts --no-audit --no-fund
fi
npm test --prefix "$dir"
fi
done <<< "$SKILL_DIRS"
# Deliberately ungated. The catalog is a registration surface for every
# skill, so a SKILL.md or marketplace edit can break the site even when
# nothing under site/ changed. Building on every run is the cheapest way
# to keep catalog integrity a hard gate rather than a nightly surprise.
- name: Build catalog site
run: |
npm ci --prefix site --ignore-scripts
npm run build --prefix site
# The dns-doctor cache engine resolves platform cache paths and refuses
# redirected paths, and both behave differently per operating system. macOS
# in particular puts os.tmpdir() under /var, which is itself a link to
# /private/var, so a guard that compares a path to its own resolved form
# fails there and nowhere else. That regression is only observable on this
# matrix, so the suite runs on all three.
dns-doctor-tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm
cache-dependency-path: skills/dns-doctor/package-lock.json
- name: Install dependencies
working-directory: skills/dns-doctor
run: npm ci --ignore-scripts
- name: Run tests and enforce coverage
working-directory: skills/dns-doctor
run: npm run test:coverage
git-tidy-tests:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "24"
cache: npm
cache-dependency-path: skills/git-tidy/package-lock.json
- name: Install dependencies
working-directory: skills/git-tidy
run: npm ci --ignore-scripts
- name: Run tests and enforce coverage
working-directory: skills/git-tidy
run: npm run test:coverage