docs: promote stranded conventions to path-scoped rules and nested AGENTS.md #8876
Workflow file for this run
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: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| # The PR number scopes cancellation to exactly one PR (head_ref would collide | |
| # across same-named fork branches); push runs fall back to the unique run_id, | |
| # so default-branch runs are never cancelled. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| # Quality lanes run the ci-workflows composite actions / reusable workflows | |
| # (referenced by SHA, bumped via Dependabot — never copied) against this repo's | |
| # exact standards materializations. Tool configs live at their tool-native | |
| # root paths; comment-hygiene's default policy is bundled with its action. The | |
| # local ci-status gateway aggregates every lane into the | |
| # single required check the org ci-gate ruleset keys on. | |
| jobs: | |
| # Single resolution of the docs-only diff scope. Every gated lane reads | |
| # `needs.scope.outputs.run_full` instead of re-running the detector, so the | |
| # detector runs once per workflow rather than once per lane and there is one | |
| # place to reason about what the answer means. | |
| # | |
| # THE OUTPUT IS POSITIVE ON PURPOSE. A consumer writes | |
| # `run_full == 'true'` to do work and `run_full == 'false'` to report the | |
| # work not applicable. Both are plain equality against a value that | |
| # `${{ <expr> != 'true' }}` can only ever render as the string `true` or the | |
| # string `false`, so the two forms are exact complements and a new lane has | |
| # no negation to spell wrong. The earlier shape asked each consumer to write | |
| # `docs_only != 'true'` and relied on a prose comment to keep that polarity | |
| # straight across every site. | |
| # | |
| # FAIL-CLOSED TOWARD RUNNING THE SUITE. `scripts/check-docs-only.sh` already | |
| # emits `docs_only=false` on every condition it cannot resolve. This job | |
| # covers the cases below the script: a detector that cannot complete — a | |
| # non-zero exit, or an output file it could not write — leaves | |
| # `steps.detect.outputs.docs_only` unset, which the `run_full` expression | |
| # below resolves to `true`, and the full suite runs. The same unset-output | |
| # path covers a push event, where the detect step does not run at all. | |
| # | |
| # Note precisely what each half contributes, because the two are easy to | |
| # conflate. `continue-on-error` does NOT clear a step's outputs: a step that | |
| # writes to $GITHUB_OUTPUT and then fails keeps what it wrote. What it does is | |
| # keep a failed detector from failing THIS job and skipping every consumer. | |
| # The output being unset on a failed run is a property of | |
| # `scripts/check-docs-only.sh`, which follows every emit with an immediate | |
| # `exit 0` and never emits `true` on a path that can subsequently fail. That | |
| # invariant is what the consumers ultimately rest on; it is stated in that | |
| # script's header and exercised by its suite. | |
| # | |
| # `scripts/check-docs-only-gate.sh` asserts the workflow-side properties | |
| # against this file, so they are checked rather than asserted in prose. | |
| # | |
| # The detector self-test runs first and is NOT `continue-on-error`, so a | |
| # broken detector turns this job red rather than resolving a scope nobody | |
| # verified. | |
| scope: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| outputs: | |
| run_full: ${{ steps.detect.outputs.docs_only != 'true' }} | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the docs-only detector | |
| run: bash scripts/check-docs-only.test.sh | |
| - name: Detect a docs-only diff | |
| id: detect | |
| if: github.event_name == 'pull_request' | |
| continue-on-error: true | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-docs-only.sh "origin/$BASE_REF" | |
| # Public repository: every lane runs on GitHub-hosted ubuntu-24.04 (free for | |
| # public repositories); the local-runner selector is not permitted here. The | |
| # short hygiene checks still share one checkout to avoid per-check setup | |
| # churn. | |
| # Docs-linting lane. Markdownlint / typos / editorconfig / gitleaks / | |
| # eol-renormalize / comment-hygiene / machine-specific-paths read the changed | |
| # docs themselves and run on every diff. actionlint, the four | |
| # check-jsonschema steps, and the manifest duplicate-key detector (which | |
| # reads the exact same manifest files as the schema steps, just via | |
| # object_pairs_hook instead of a schema — see #1498) read fixed, path-scoped | |
| # inputs (.github/workflows/** and the named manifests) that a diff confined | |
| # to the docs-only allowlist (scripts/docs-only-paths.txt) cannot touch, so | |
| # on such a diff they report an honest evaluated-and-not-applicable success. | |
| # ShellCheck, exec-bit, hook-wiring-liveness, and purged-em-dashes are NOT | |
| # gated: ShellCheck lints every tracked *.sh/*.bash, exec-bit flags every | |
| # tracked shebang file recorded 100644, hook-wiring-liveness greps | |
| # .claude/hooks/*.sh against settings.json, and purged-em-dashes reads the | |
| # markdown prose a docs-only diff is made of — a shell/shebang/hook file added | |
| # under an otherwise docs-only prefix like docs/topics/ is real input they | |
| # must still catch, and so is a docs-only diff that reintroduces an em dash on | |
| # a surface already declared purged. The job NEVER skips — only | |
| # the path-scoped steps are gated, on `needs.scope.outputs.run_full` from the | |
| # `scope` lane, which resolves the diff once for the whole workflow and | |
| # carries the self-test and fail-closed guarantees (see that job). The gated | |
| # steps' intentional docs-only skip is mapped to `success` in the | |
| # CHECK_RESULTS feed below (where the step provably did not run), never by | |
| # weakening the aggregator — it still fails closed on any real non-`success` | |
| # outcome. | |
| hygiene: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Lint markdown | |
| id: markdown | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/markdown@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| config: .markdownlint-cli2.jsonc | |
| - name: Spell-check | |
| id: typos | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/typos@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| config: _typos.toml | |
| - name: Scan for secrets | |
| id: gitleaks | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/gitleaks@f2d5e06757201f2fce187096a2c6fa805836c3d2 | |
| with: | |
| config: .gitleaks.toml | |
| scan-mode: git | |
| redact: true | |
| - name: Check editorconfig conformance | |
| id: editorconfig | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/editorconfig@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| config: .editorconfig-checker.json | |
| - name: Lint shell scripts | |
| id: shellcheck | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/shellcheck@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| rcfile: .shellcheckrc | |
| - name: Lint workflows | |
| id: actionlint | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/actionlint@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| - name: Validate marketplace manifest | |
| id: marketplace_schema | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| schemafile: https://json.schemastore.org/claude-code-marketplace.json | |
| files: .claude-plugin/marketplace.json | |
| - name: Validate plugin manifests | |
| id: plugin_schema | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| schemafile: https://json.schemastore.org/claude-code-plugin-manifest.json | |
| files: plugins/*/.claude-plugin/plugin.json | |
| # JSON Schema validates the ALREADY-PARSED document, so it structurally | |
| # cannot see a duplicate object key -- by the time a schema runs, the | |
| # earlier member is already gone (every JSON parser resolves a repeated | |
| # key last-wins). #1492 shipped plugins/skill-quality/.claude-plugin/ | |
| # plugin.json with two "version" members through this exact schema step | |
| # green. scripts/check-manifest-duplicate-keys.py reads the raw | |
| # key/value pairs via object_pairs_hook, BEFORE that collapse, so it | |
| # catches what the schema step next to it cannot. See #1498. | |
| - name: Test the manifest duplicate-key detector | |
| run: bash scripts/check-manifest-duplicate-keys.test.sh | |
| - name: Detect duplicate keys in plugin/marketplace manifests | |
| id: duplicate_keys | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| run: python3 scripts/check-manifest-duplicate-keys.py | |
| - name: Validate dependabot.yml | |
| id: dependabot_schema | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| builtin-schema: vendor.dependabot | |
| files: .github/dependabot.yml | |
| - name: Validate workflows | |
| id: workflow_schema | |
| if: needs.scope.outputs.run_full == 'true' | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| builtin-schema: vendor.github-workflows | |
| files: >- | |
| .github/workflows/ci.yml | |
| .github/workflows/claude-review.yml | |
| .github/workflows/claude-security-review.yml | |
| .github/workflows/dependabot-miro-bundle.yml | |
| .github/workflows/do-not-merge.yml | |
| .github/workflows/link-check.yml | |
| .github/workflows/pr-issue-linkage.yml | |
| .github/workflows/pr-title.yml | |
| .github/workflows/silent-revert-canary.yml | |
| - name: Verify shebang files are executable | |
| id: exec_bit | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/exec-bit@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| - name: Check for machine-specific paths | |
| id: machine_paths | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/machine-specific-paths@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| # The guardrails plugin bundles a path-detection pattern lib whose | |
| # regex bodies self-match this lane's own detector. The autonomy | |
| # security-binding fixtures carry ADVERSARIAL host paths on purpose | |
| # (invented /home users the checker must reject) — sanitizing them | |
| # would gut the regressions they pin. Its graded-fixture manifest is | |
| # the same corpus: it pins each fixture's trusted --credential-roots | |
| # (a real runner home) and expected not-under-root rejections, so it | |
| # necessarily carries those same host paths. | |
| # audit-permission-state is the third case of the same kind: its | |
| # C6-winPath check EXISTS to find Windows paths in permission rules, | |
| # so its detector message quotes one, its criteria file quotes the | |
| # settings page quoting one, and its fixtures must carry them or the | |
| # check has nothing to detect. The oracle fixtures reproduce Claude | |
| # Code's own [DEBUG] narration, which prints native Windows settings | |
| # paths — rewriting those would make the fixture stop matching what | |
| # the parser has to survive. The paths are invented (`alice`, `x`), | |
| # not any operator's. source-control's same-drive unit tests need | |
| # literal `C:/` / `D:/` drive fixtures to exercise the gate (#2764). | |
| exclude: >- | |
| :(exclude)plugins/guardrails/lib/path-detection/** | |
| :(exclude)plugins/autonomy/skills/setup/evals/fixtures/security-binding/** | |
| :(exclude)plugins/autonomy/skills/setup/scripts/check-security-binding.fixtures.test.manifest.json | |
| :(exclude)plugins/claude-config/skills/audit-permission-state/reference/criteria.md | |
| :(exclude)plugins/claude-config/skills/audit-permission-state/scripts/permission-plane-lint.sh | |
| :(exclude)plugins/claude-config/skills/audit-permission-state/scripts/permission-plane-lint.test.sh | |
| :(exclude)plugins/claude-config/skills/audit-permission-state/scripts/automode-entry-diff.test.sh | |
| :(exclude)plugins/source-control/scripts/worktree-create.test.sh | |
| - name: Check index-level EOL drift | |
| id: eol | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/eol-renormalize@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| - name: Scan comment hygiene | |
| id: comment_hygiene | |
| continue-on-error: true | |
| uses: melodic-software/ci-workflows/.github/actions/comment-hygiene@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| # The audit-comment-residue skill IS a code-comment linter: its detector | |
| # fixtures and shape library necessarily contain the markers this policy | |
| # bans (TODO/FIXME, PR/issue refs) as test corpus and as the documented | |
| # TODO(#issue) sanctioned-exemption. Exempt that skill's scripts + evals | |
| # (its SKILL.md is markdown, already outside the scan's extension set). | |
| exclude: >- | |
| :(exclude)plugins/code-tidying/skills/audit-comment-residue/scripts/** | |
| :(exclude)plugins/code-tidying/skills/audit-comment-residue/evals/** | |
| - name: Test the hook-wiring-liveness gate | |
| run: bash scripts/check-hook-wiring-liveness.test.sh | |
| - name: Check repo-local hooks are wired in settings.json | |
| id: hook_wiring | |
| continue-on-error: true | |
| run: scripts/check-hook-wiring-liveness.sh | |
| # Deliberately UNCONDITIONAL, like the other whole-repo scanners. This | |
| # gate reads markdown prose, which is exactly what a docs-only diff | |
| # changes, so gating it on run_full would disable it on precisely the pull | |
| # requests most able to trip it. A regression would then reach main under | |
| # a green check rather than an honest not-applicable. The self-test runs | |
| # first and ungated so a broken gate cannot mask a regression. | |
| - name: Test the purged-em-dash gate | |
| run: bash scripts/check-purged-em-dashes.test.sh | |
| - name: Check purged surfaces stayed free of em dashes | |
| id: purged_em_dashes | |
| continue-on-error: true | |
| run: scripts/check-purged-em-dashes.sh | |
| - name: Report docs-irrelevant checks not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the path-scoped linters (actionlint, check-jsonschema x4, the manifest duplicate-key detector) cannot be affected — reporting success for them. ShellCheck, exec-bit, hook-wiring-liveness, and purged-em-dashes scan the whole repo and stay unconditional." | |
| - name: Test hygiene result aggregation | |
| run: scripts/aggregate-hygiene-results.sh --self-test | |
| - name: Aggregate hygiene results | |
| if: always() | |
| env: | |
| # Display names (left) are annotations; step ids (right) use underscores. | |
| # The path-scoped linters (actionlint, the four check-jsonschema steps, | |
| # and the manifest duplicate-key detector) carry | |
| # `if: run_full == 'true'`, so on a docs-only diff they are skipped | |
| # (never run) — their intentional skip is mapped to `success` here | |
| # BECAUSE the step provably did not run, so no real outcome is masked. | |
| # The aggregator stays fail-closed on any real non-`success` | |
| # (including `skipped`); it is never told to pass on `skipped`. The | |
| # condition here (`run_full == 'false'`) tests the same single output | |
| # against its only other value, so the two can never disagree. | |
| # `scripts/check-docs-only-gate.sh` pins both halves: that this | |
| # condition is the sanctioned complement of the gates, and that every | |
| # step named on the right of an override is one that carries the gate | |
| # — an override on an UNGATED step would mask a real failure as | |
| # success, and that is the direction that fails open. ShellCheck, | |
| # exec-bit, and hook-wiring-liveness are unconditional (whole-repo | |
| # scanners), so they feed their raw outcome. | |
| CHECK_RESULTS: | | |
| markdown=${{ steps.markdown.outcome }} | |
| typos=${{ steps.typos.outcome }} | |
| gitleaks=${{ steps.gitleaks.outcome }} | |
| editorconfig=${{ steps.editorconfig.outcome }} | |
| shellcheck=${{ steps.shellcheck.outcome }} | |
| actionlint=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.actionlint.outcome }} | |
| marketplace-schema=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.marketplace_schema.outcome }} | |
| plugin-schema=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.plugin_schema.outcome }} | |
| manifest-duplicate-keys=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.duplicate_keys.outcome }} | |
| dependabot-schema=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.dependabot_schema.outcome }} | |
| workflow-schema=${{ needs.scope.outputs.run_full == 'false' && 'success' || steps.workflow_schema.outcome }} | |
| exec-bit=${{ steps.exec_bit.outcome }} | |
| machine-specific-paths=${{ steps.machine_paths.outcome }} | |
| eol-renormalize=${{ steps.eol.outcome }} | |
| comment-hygiene=${{ steps.comment_hygiene.outcome }} | |
| hook-wiring-liveness=${{ steps.hook_wiring.outcome }} | |
| purged-em-dashes=${{ steps.purged_em_dashes.outcome }} | |
| run: scripts/aggregate-hygiene-results.sh | |
| zizmor: | |
| permissions: | |
| contents: read | |
| # Actions security lint; any regular-persona finding blocks the merge gate. | |
| uses: melodic-software/ci-workflows/.github/workflows/zizmor.yml@31a5b76c4a0b663023dc1c944e2bcfc01d6f6c46 # 31a5b76 2026-07-21 | |
| with: | |
| runner: ubuntu-24.04 | |
| paths: . | |
| fail-on-severity: low | |
| # The ONLY job on a Windows runner, and deliberately so. Every other suite in | |
| # this repository is platform-agnostic bash string logic that a Linux runner | |
| # exercises identically — `lib/powershell/ps-command.sh`, for instance, carries | |
| # zero `OSTYPE`/`cygpath`/`uname` branches despite classifying PowerShell | |
| # commands, so Linux tests it faithfully. | |
| # | |
| # `lib/hook-utils.sh` is the exception: it carries OSTYPE-gated branches that a | |
| # Linux runner NEVER EXECUTES — case-insensitive path folding for the Windows | |
| # filesystem, and `cygpath` short-name (8.3) resolution. Those branches were | |
| # previously verified only by whatever a maintainer happened to run on a dev | |
| # box. This job is what makes them a gate — but only because it is also listed | |
| # in the `ci-status` needs graph below. `ci-status` is the single required merge | |
| # check, so a lane missing from that list is informational no matter how loudly | |
| # a comment here calls it a gate: the aggregate would report success while this | |
| # lane was red. | |
| # | |
| # Free: GitHub Actions is free for public repositories on standard | |
| # GitHub-hosted runners, Windows included; only larger runners are charged. | |
| # https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions | |
| # | |
| # Keep this job SMALL. Adding a platform-agnostic suite here buys no coverage | |
| # and pays Windows' process-creation cost (~140ms/spawn vs ~3ms on Linux), | |
| # which is the whole reason the full suite is unusable on a Windows dev box. | |
| # Pinned, not `windows-latest`: the runner policy forbids the floating | |
| # `*-latest` labels outright (policy.json forbiddenHostedRunnerLabels), for the | |
| # same reason the Linux lanes pin ubuntu-24.04 — an image roll must be a | |
| # reviewed commit, not a silent Tuesday. | |
| hook-utils-windows: | |
| runs-on: windows-2025 | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Run shared lib tests on Windows | |
| run: bash lib/hook-utils.test.sh | |
| # #2774: the disk-hygiene read-only Bash allowlist is NT-specific (MSYS path | |
| # mapping, .exe basename stripping, ProgramFiles trust roots). Linux CI cannot | |
| # exercise those branches — Path.is_absolute() is True for /usr/bin/ls on | |
| # POSIX — which is how an inert Windows allowlist shipped green. Keep this | |
| # job SMALL: GuardTests only, not the full hygiene suite. | |
| # | |
| # #2871: a dedicated fast signal for the full module | |
| # (`python -m unittest -v test_hygiene`) lives on `disk-hygiene-tests` | |
| # (Linux). It is not invoked here because two tests fail on windows-2025 | |
| # (315/317, 9.96s on run 32477560257): | |
| # - HygieneTests.test_preview_allows_root_children_os_managed_snapshot | |
| # expects ready-for-explicit-approval; Windows returns blocked | |
| # - HandoffVerifyTests.test_stash_must_exist_in_an_independent_checkout | |
| # compares path strings; 8.3 `RUNNER~1` != long `runneradmin` | |
| # Those two are already green on Linux. Do not silently omit a class here | |
| # without this comment; the other nine classes gate on disk-hygiene-tests | |
| # (dedicated fast signal; plugin-gate also runs the module on | |
| # non-docs-only diffs via hygiene.test.sh). | |
| disk-hygiene-guard-windows: | |
| runs-on: windows-2025 | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.14' | |
| - name: Run disk-hygiene GuardTests on Windows | |
| working-directory: plugins/disk-hygiene/skills/clean/scripts | |
| run: python -m unittest -v test_hygiene.GuardTests | |
| # #2871: dedicated required signal for the full test_hygiene module. | |
| # plugin-gate already runs hygiene.test.sh on non-docs-only diffs (this | |
| # PR's own plugin-gate did). That is the heavy lane (Node, ShellCheck, | |
| # every plugins/**/*.test.sh). This job exists so the ten TestCase | |
| # classes have a fast, always-on check that does not wait on that | |
| # matrix and cannot take hygiene.test.sh's SKIP-when-Python-absent | |
| # path. The Windows job above stays GuardTests-only (NT allowlist + | |
| # two Windows failures named there). Suite is not host-destructive | |
| # (tempdir fixtures; no live drive-root scans). Measured 317 OK. | |
| disk-hygiene-tests: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.14' | |
| - name: Run disk-hygiene tests | |
| working-directory: plugins/disk-hygiene/skills/clean/scripts | |
| run: python -m unittest -v test_hygiene | |
| # #2834: an MSYS path literal (`/d/...`) handed to a Windows-native consumer | |
| # re-anchors to the CURRENT drive's root, so the consumer creates a phantom | |
| # `<drive>:\<letter>\` tree and writes there. Nothing errors — the run just | |
| # measures something other than what it claims. docs/conventions/ | |
| # windows-path-emit/ owns the rule; this lane holds its two artifacts to | |
| # their contracts. | |
| # | |
| # What is REQUIRED here is the detectors' own unit contract — deterministic, | |
| # fixture-scoped, and including the assertion that the drive-root scan is a | |
| # reported no-op on a non-Windows host. Pointing the live scan at a runner's | |
| # drive roots is deliberately NOT wired: it would put an unquantified | |
| # false-positive tail on the required aggregate, which docs/adr/0003 rules | |
| # out until a guard has measured precision. Self-test first, so a broken | |
| # detector cannot mask a regression. | |
| windows-path-emit-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the MSYS-to-Windows emit helper | |
| run: bash scripts/emit-windows-path.test.sh | |
| - name: Test the drive-root litter detector | |
| run: bash scripts/check-drive-root-litter.test.sh | |
| - name: Assert the drive-root scan is a no-op on a non-Windows host | |
| run: scripts/check-drive-root-litter.sh | |
| # The emit helper's whole reason to exist is its NT branch: cygpath's answer | |
| # cannot be faked on Linux, so on a Linux runner those cases report NOT | |
| # EXERCISED and the suite is green having proved nothing about them. That is | |
| # exactly how an inert Windows-only surface shipped green in #2774. This job | |
| # runs the same suites where the branch is real. Keep it SMALL: these two | |
| # suites only, and never the live drive-root scan. | |
| windows-path-emit-windows: | |
| runs-on: windows-2025 | |
| timeout-minutes: 20 | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the MSYS-to-Windows emit helper on Windows | |
| run: bash scripts/emit-windows-path.test.sh | |
| - name: Test the drive-root litter detector on Windows | |
| run: bash scripts/check-drive-root-litter.test.sh | |
| hook-utils-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify plugin copies match the shared lib | |
| run: scripts/sync-hook-utils.sh --check | |
| - name: Run shared lib tests | |
| run: bash lib/hook-utils.test.sh | |
| - name: Verify carrying plugins bumped when the lib changed | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-hook-utils.sh --check-bump "origin/$BASE_REF" | |
| parse-concern-value-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify plugin copies match the shared parser | |
| run: scripts/sync-parse-concern-value.sh --check | |
| - name: Run shared parser tests | |
| run: bash lib/parse-concern-value.test.sh | |
| - name: Verify consuming plugins bumped when the lib changed | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-parse-concern-value.sh --check-bump "origin/$BASE_REF" | |
| managed-scope-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify managed-scope cluster matches canonical | |
| run: scripts/sync-managed-scope.sh --check | |
| - name: Run managed-scope tests | |
| run: bash plugins/claude-config/lib/managed-scope.test.sh | |
| - name: Verify carrying plugins bumped when canonical changed | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-managed-scope.sh --check-bump "origin/$BASE_REF" | |
| state-key-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify state-key cluster matches canonical | |
| run: scripts/sync-state-key.sh --check | |
| - name: Run state-key tests | |
| run: bash plugins/claude-config/lib/state-key.test.sh | |
| - name: Verify carrying plugins bumped when canonical changed | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-state-key.sh --check-bump "origin/$BASE_REF" | |
| resolve-convention-pattern-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify plugin copies match the shared resolver | |
| run: scripts/sync-resolve-convention-pattern.sh --check | |
| - name: Run shared resolver tests | |
| run: bash lib/resolve-convention-pattern.test.sh | |
| - name: Verify consuming plugins bumped when the lib changed | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-resolve-convention-pattern.sh --check-bump "origin/$BASE_REF" | |
| standards-contract-sync: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Verify plugin binding copies match the canonical contract | |
| run: scripts/sync-standards-contract.sh --check | |
| - name: Run sync-standards-contract tests | |
| run: bash scripts/sync-standards-contract.test.sh | |
| - name: Verify contract, changelog, and carrying plugins bumped together | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/sync-standards-contract.sh --check-bump "origin/$BASE_REF" | |
| cross-plugin-source-drift: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Check for unregistered or drifted cross-plugin source clusters | |
| run: scripts/check-cross-plugin-source-drift.sh --check | |
| - name: Run cross-plugin-source-drift tests | |
| run: bash scripts/check-cross-plugin-source-drift.test.sh | |
| detector-findings-crosswalk-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Self-test first, so a broken detector cannot mask a regression. | |
| - name: Run detector-findings-crosswalk tests | |
| run: bash scripts/check-detector-findings-crosswalk.test.sh | |
| - name: Check every crosswalk row argues its disposition from a stated test | |
| run: scripts/check-detector-findings-crosswalk.sh --check | |
| skill-leaf-name-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # Self-test first, so a broken detector cannot mask a regression. | |
| - name: Run skill-leaf-name tests | |
| run: bash scripts/check-skill-leaf-names.test.sh | |
| - name: Check for unregistered cross-plugin skill leaf-name collisions | |
| run: scripts/check-skill-leaf-names.sh --check | |
| # A plugin's prose counts its own skills by hand ("bundling fourteen skills", | |
| # "the other thirteen skills are zero-config") and nothing recomputes those | |
| # numbers when a skill lands, so adding one silently falsifies every sentence | |
| # that counted the old set. Measured: PR #3011 went stale on six such counts, | |
| # each caught by a different reviewer rather than by anything mechanical, and | |
| # session-flow's CHANGELOG records an "other eleven skills" line that was | |
| # already off by one before that PR. Self-test first so a broken detector | |
| # cannot mask a regression. | |
| skill-count-claim-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Run skill-count-claim tests | |
| run: bash scripts/check-skill-count-claims.test.sh | |
| - name: Check every skill-count claim against the tree | |
| run: scripts/check-skill-count-claims.sh --check | |
| # A ${user_config.*} token in a plugin hook config silently drops the whole | |
| # hook entry whenever the key is unset (the declared userConfig `default` is | |
| # unimplemented upstream — anthropics/claude-code#46477), so the hook never | |
| # fires on a default install. The channel decision matrix | |
| # (docs/conventions/hook-config-delivery/) rules argv out for hooks; this | |
| # gate pins the exact regression #1242 fixed. Self-test first so a broken | |
| # detector cannot mask a regression. | |
| userconfig-argv-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the userconfig-argv gate | |
| run: bash scripts/check-hook-userconfig-argv.test.sh | |
| - name: Check plugin hook configs for bare userConfig argv tokens | |
| run: scripts/check-hook-userconfig-argv.sh | |
| # An exec-form hook (a hook object carrying `args`) resolves `command` as an | |
| # executable through PATH, so a bare name is machine-dependent: on Windows | |
| # `bash` resolves to the WSL relay System32\bash.exe and `python3` to a | |
| # zero-length WindowsApps alias stub, and the hook never launches. A failed | |
| # launch is a NON-BLOCKING error, so a PreToolUse guard wired this way | |
| # silently enforces nothing. This class has shipped three times in | |
| # disk-hygiene alone (#1006 fixed it, #1504 reintroduced it, #2570 fixed it | |
| # again) while the claude-config audit checklist carried it as an `error` row | |
| # the whole time — a checklist a human reads is not a gate. Repo-wide and | |
| # static, covering both declaration surfaces (hook config JSON and skill/agent | |
| # frontmatter). Self-test first so a broken detector cannot mask a regression. | |
| hook-exec-form-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # PyYAML reads the frontmatter surface (YAML has no jq). Pinned and | |
| # hash-locked in the same file every other Python pin lives in; the gate | |
| # fails closed if it is missing rather than clearing files it never | |
| # parsed, so this step is load-bearing, not convenience. | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.14' | |
| cache: pip | |
| cache-dependency-path: .github/requirements-ci.txt | |
| - name: Install the pinned YAML reader | |
| run: | | |
| python -m pip install --only-binary=:all: --require-hashes \ | |
| --requirement .github/requirements-ci.txt | |
| - name: Test the hook-exec-form gate | |
| run: bash scripts/check-hook-exec-form.test.sh | |
| - name: Check hook declarations for exec-form bare command names | |
| run: scripts/check-hook-exec-form.sh | |
| # Three separate readers judge `metadata.summary`, and they used to disagree: | |
| # a malformed value reached CI twice in #3162, failing in opposite directions, | |
| # each time after clearing the local check meant to catch it. The local bash | |
| # reader enforced only a length cap, the cheat-sheet generator's guard | |
| # enforced YAML-scalar safety, and a real YAML parser enforced neither but | |
| # rejected the document outright. Making them agree is not sufficient on its | |
| # own, because agreement on a wrong rule is still wrong, so this gate pins | |
| # every verdict to a real YAML parse rather than to the other readers. It | |
| # sweeps a shared case table (the rule set) and every SKILL.md in the tree | |
| # (the tree), and it fails closed without PyYAML rather than clearing on the | |
| # two regex readers alone. | |
| summary-reader-parity-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.14' | |
| cache: pip | |
| cache-dependency-path: .github/requirements-ci.txt | |
| - name: Install the pinned YAML reader | |
| run: | | |
| python -m pip install --only-binary=:all: --require-hashes \ | |
| --requirement .github/requirements-ci.txt | |
| - name: Check that every summary reader returns the same verdict | |
| run: bash scripts/check-summary-reader-parity.test.sh | |
| # Every option a plugin declares must be documented in that plugin's README. | |
| # The block is generated from the manifest, so the failure this catches is an | |
| # option added to plugin.json whose README block was never regenerated -- an | |
| # undocumented knob is indistinguishable from one that does not exist, and the | |
| # only place a user can discover it is the manifest itself. | |
| plugin-options-docs-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Check plugin option docs are in sync with each manifest | |
| run: python3 scripts/sync-plugin-options-docs.py --check | |
| silent-skip-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Check hook entry scripts for silent prerequisite skips | |
| run: scripts/check-silent-skips.sh | |
| - name: Run silent-skip-gate tests | |
| run: bash scripts/check-silent-skips.test.sh | |
| # #1547: a routine `git merge origin/main` silently dropped | |
| # plugins/guardrails/.claude-plugin/plugin.json for ~14 minutes on a branch | |
| # while .claude-plugin/marketplace.json still catalogued "guardrails" at that | |
| # directory — no conflict markers, nothing to alert the author; an AI | |
| # reviewer reading the diff caught it, not CI. check-jsonschema (the | |
| # marketplace_schema/plugin_schema steps in `hygiene`) validates the SHAPE of | |
| # a manifest that exists; it cannot see one that is simply absent. This gate | |
| # asserts every catalog entry resolves to a present, name-matching plugin | |
| # manifest, and (paired, lower severity) that every plugins/*/ directory is | |
| # itself catalogued. Repo-wide and static, so it always runs (no docs-only or | |
| # PR-diff scoping — a manifest going missing is not confined to a diff). The | |
| # self-test runs first so a broken detector cannot mask a regression behind a | |
| # green gate. | |
| plugin-manifest-presence-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the plugin-manifest-presence gate | |
| run: bash scripts/check-plugin-manifest-presence.test.sh | |
| - name: Check every catalog entry resolves to a present, name-matching plugin manifest | |
| run: scripts/check-plugin-manifest-presence.sh | |
| # docs/CLOUD-SESSIONS.md promises `enabledPlugins` "turns on the whole | |
| # catalog, so this repo dogfoods everything it publishes"; nothing enforced | |
| # it, and ai-slop (#2892), context-budget (#2932) and improvement (#2985) | |
| # each reached main catalogued but never enabled. The failure is silent by | |
| # construction: .claude/cloud-bootstrap.sh computes its install set from that | |
| # same enabledPlugins map, so a session comes up green with the plugin's | |
| # skills simply absent and nothing naming what is missing. The consumer-side | |
| # detector cannot cover this — check-plugin-drift.sh resolves marketplaces | |
| # through `source.repo` and SKIPs one that declares none, which is exactly | |
| # this repo's relative `directory` source. Repo-wide and static, so it always | |
| # runs. The self-test runs first so a broken detector cannot mask a | |
| # regression behind a green gate. | |
| plugin-catalog-enablement-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the plugin-catalog-enablement gate | |
| run: bash scripts/check-plugin-catalog-enablement.test.sh | |
| - name: Check every catalogued plugin carries an enabledPlugins key | |
| run: scripts/check-plugin-catalog-enablement.sh | |
| # Every file under a skill's evals/fixtures/ must be consumed by a grader — an | |
| # eval files[] entry or a test assertion — so an ungraded suite cannot sit in | |
| # the tree reading as tested. The check is repo-wide and static; existing debt | |
| # is grandfathered in scripts/orphaned-fixtures-baseline.txt with a stale | |
| # guard, so the gate holds the line without red-lining fixtures a member issue | |
| # already owns. The self-test runs unconditionally so a broken detector cannot | |
| # mask a regression behind a green gate. | |
| orphaned-fixture-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the orphaned-fixture detector | |
| run: bash scripts/check-orphaned-fixtures.test.sh | |
| - name: Check for orphaned eval fixtures | |
| run: scripts/check-orphaned-fixtures.sh --check | |
| # A test suite that BUILDS a git fixture must clear the inherited git | |
| # environment, in shell and in Python alike. `git -C <fixture>` is a | |
| # readability guard, not an isolation guarantee: an exported ABSOLUTE GIT_DIR | |
| # overrides repository DISCOVERY, so the fixture identity lands in the | |
| # CALLER's .git/config — shared by every worktree of the clone — and the | |
| # caller's next commit is silently re-authored. That failed | |
| # required_signatures with `no_user` and cost #2827 -> #2830 (#2840). The | |
| # check is repo-wide and static. The grandfathered baseline was drained | |
| # in #2872; a new fixture-building suite must isolate itself. The | |
| # self-test runs unconditionally so a broken detector cannot mask a | |
| # regression behind a green gate. | |
| fixture-git-isolation-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the fixture-git-isolation detector | |
| run: bash scripts/check-fixture-git-isolation.test.sh | |
| - name: Test that the shared git test harness isolates its fixtures | |
| run: bash scripts/test-git-helpers.test.sh | |
| - name: Check fixture git isolation | |
| run: scripts/check-fixture-git-isolation.sh --check | |
| # Every finding kind audit-fleet.sh emits must have a Finding: <kind> | |
| # collector-output assertion in audit-fleet.test.sh (or be grandfathered). | |
| # Lives outside the suite so a stale-base squash that rewrites the test file | |
| # alongside the collector cannot delete its own defender (#2656). Existing | |
| # debt is grandfathered in scripts/fleet-finding-test-coverage-baseline.txt | |
| # with a stale guard. | |
| fleet-finding-test-coverage-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the fleet finding-kind test-coverage detector | |
| run: bash scripts/check-fleet-finding-test-coverage.test.sh | |
| - name: "Check every emitted fleet finding kind has a Finding: assertion" | |
| run: scripts/check-fleet-finding-test-coverage.sh --check | |
| # The audit skill's documented argument grammar must match what audit-fleet.sh | |
| # actually accepts. Lives outside both SKILL.md and the collector so a | |
| # docs-only silent revert (#2646) cannot delete its own defender (#2713). | |
| # Probes the script (never pattern-matches the parser) under a sealed env. | |
| fleet-audit-doc-grammar-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the fleet audit doc-grammar detector | |
| run: bash scripts/check-fleet-audit-doc-grammar.test.sh | |
| - name: Check audit skill grammar matches the parser | |
| run: scripts/check-fleet-audit-doc-grammar.sh --check | |
| # Stale-base squash merges can silently revert recently-landed fixes when a PR | |
| # is cut from a base that has since moved, and the squash deletes the tests | |
| # that would have caught the drop (claude-code-plugins#2691). This gate fails | |
| # the PR while its merge-base is behind on overlapping paths. It covers the | |
| # stale-BASE class only: a branch stale in content but current in history | |
| # passes here, and that class belongs to the post-merge silent-revert canary | |
| # (scripts/check-silent-revert.sh, silent-revert-canary.yml). The two are | |
| # disjoint; neither subsumes the other. | |
| # The job itself never skips (a skipped required lane's result is not | |
| # `success`, which the ci-status aggregate rejects) — only the PR overlap | |
| # steps are event-gated. The detector self-test runs on every event so a | |
| # broken detector cannot mask a regression behind a push-to-main skip. | |
| stale-base-overlap-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| # PR head, not the synthetic merge commit: overlap is computed against | |
| # the branch tip the author will squash, so merge-base math must match | |
| # that tip rather than an already-merged-with-base tree. Push falls | |
| # back to github.sha so `ref` is never empty on main. | |
| ref: ${{ github.event.pull_request.head.sha || github.sha }} | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the stale-base overlap detector | |
| run: bash scripts/check-stale-base-overlap.test.sh | |
| - name: Fail when behind base on overlapping paths | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-stale-base-overlap.sh --check "origin/$BASE_REF" | |
| # CHANGELOG parity: a versioned plugin must keep a CHANGELOG.md whose newest | |
| # version heading does not outrun the manifest (static --check), a PR that | |
| # changes a plugin's manifest version must update that plugin's CHANGELOG.md in | |
| # the same diff (--check-bump, PR-only), and no PR may DROP a version heading a | |
| # changelog already carried (--check-preserved, PR-only). Existing | |
| # "versioned but changelog-less" debt is grandfathered by name in | |
| # scripts/changelog-parity-baseline.txt with a stale guard; the bump gate is | |
| # never relaxed by the baseline. The self-test runs unconditionally so a broken | |
| # gate cannot mask a regression, and the PR-diff step is event-gated. | |
| changelog-parity-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the changelog-parity gate | |
| run: bash scripts/check-changelog-parity.test.sh | |
| - name: Verify every versioned plugin keeps a CHANGELOG.md at or below its manifest version | |
| run: scripts/check-changelog-parity.sh --check | |
| - name: Verify a version bump updates the plugin's CHANGELOG.md | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-changelog-parity.sh --check-bump "origin/$BASE_REF" | |
| # The other half of the same PR discipline: the bump gate polices what a | |
| # change set ADDS and never what it REMOVES, so a merge-forward that writes | |
| # the new release under the PREVIOUS release's heading deletes a released | |
| # section with no conflict marker and no gate to catch it (#2264). | |
| - name: Verify no changed changelog drops a version heading it carried at the fork point | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-changelog-parity.sh --check-preserved "origin/$BASE_REF" | |
| # Whole-file, not per-diff: the other two modes reason about one version at | |
| # a time, so neither can see that two branches staged the same number or | |
| # that a stale-based entry landed below one already on main. | |
| - name: Verify every changelog reads newest-first with no duplicate versions | |
| run: scripts/check-changelog-parity.sh --check-order | |
| # Contract-slice prune: docs/topics/<slug>/ is Contract tier per | |
| # docs/conventions/topic-docs/README.md — committed on a task branch only, | |
| # pruned before merge. The convention specified this required check but it was | |
| # never wired, so 17 slices reached main (#1417). The gate keys on where a path | |
| # LANDS, so the prune commit itself and a history-preserving graduation out of | |
| # docs/topics/ both pass; only an add, edit, or rename-into is red-lined. The | |
| # pre-existing 17 are grandfathered by slug in | |
| # scripts/contract-slice-baseline.txt with a stale guard, and burn down under | |
| # #1419. The self-test runs unconditionally so a broken gate cannot mask a | |
| # regression, and the PR-diff step is event-gated. | |
| contract-slice-prune-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the contract-slice prune gate | |
| run: bash scripts/check-contract-slice-prune.test.sh | |
| - name: Verify no baseline entry outlives its slice | |
| run: scripts/check-contract-slice-prune.sh --check | |
| - name: Verify the change set prunes its contract slice | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-contract-slice-prune.sh --check-diff "origin/$BASE_REF" | |
| # The review-disposition-and-resolution contract (D4.6 grounding, D4.6 | |
| # provenance, D7.5 thread eligibility, and who may authorize a resolution | |
| # that ships no fix) is canonical in ONE file and restated across half a | |
| # dozen others; nothing checked that the copies agreed. #1633 shipped seven | |
| # such defects across five review rounds — each an individually well-formed | |
| # surface instructing a lane to do what another surface forbids — and the | |
| # HAND-RUN sweep that finally converged then produced a false "no remaining | |
| # gaps" of its own, because it asked whether a qualifier appeared ANYWHERE | |
| # in a file rather than inside the passage that restates the rule. This gate | |
| # is span-scoped for exactly that reason: a restatement declares itself with | |
| # a `contract-restatement` marker and is held to the canonical's qualifiers | |
| # within its own bounds, and text that restates a clause outside every span | |
| # is itself reported (tag it, or reduce it to a pointer — the pointer is the | |
| # stronger fix). The in-scope surface set is DERIVED from the clause | |
| # vocabulary, never hardcoded, so a new restatement anywhere in the tree is | |
| # in scope the moment it is written. Repo-wide and static, so it always runs. | |
| # Self-test first, so a broken detector cannot mask a real gap behind a green | |
| # gate. See #1659. | |
| contract-clause-coverage-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the contract-clause coverage gate | |
| run: bash scripts/check-contract-clause-coverage.test.sh | |
| - name: Check every restatement carries the canonical clause's qualifiers | |
| run: python3 scripts/check-contract-clause-coverage.py | |
| # The docs-only short-circuit is a fail-open shape by construction: getting it | |
| # wrong makes a heavy lane report success without running. Its safety | |
| # properties — one resolution, an output whose unset value means "run the full | |
| # suite", a detector failure absorbed rather than propagated, an unweakened | |
| # self-test, and exactly one spelling for a consumer to copy — used to live in | |
| # a prose comment repeated next to every gated step. This lane checks them | |
| # against the workflow instead. Self-test first, so a broken gate cannot mask | |
| # a regression. | |
| docs-only-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the docs-only scope-contract gate | |
| run: bash scripts/check-docs-only-gate.test.sh | |
| - name: Check the docs-only scope contract holds in ci.yml | |
| run: scripts/check-docs-only-gate.sh --check | |
| # `ci-status` below is the single check the org ci-gate ruleset keys on, and | |
| # its own comment calls its `needs` list "the single source of truth for the | |
| # lane list" — but nothing enforced the other direction. A job defined in THIS | |
| # file and absent from that list still runs and still turns red in the run | |
| # list, while `ci-status` reports success and the merge proceeds: | |
| # informational, not a gate. `hook-utils-windows` states exactly that in prose | |
| # above, and prose is not a gate — `managed-scope-sync` and `state-key-sync` | |
| # shipped outside the aggregate for their whole lifetime while | |
| # scripts/cross-plugin-source-registry.txt advertised each as a dedicated | |
| # check (#2856). Same false-green class as `liveness-assertion`, on the merge | |
| # gate itself. This lane closes the loop mechanically: every job here is in | |
| # `ci-status.needs`, or carries a `# lane-coverage-ok: <reason>` annotation | |
| # recording why it stays out. Static, one file, no diff and no base ref, and | |
| # unrecognized YAML shape exits 2 rather than reporting coverage it did not | |
| # parse. Self-test first so a broken detector cannot mask a regression. | |
| lane-coverage-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Test the lane-coverage gate | |
| run: bash scripts/check-lane-coverage.test.sh | |
| - name: Check every ci.yml job is reachable from the ci-status aggregate | |
| run: scripts/check-lane-coverage.sh --check | |
| # Deep plugin-contract lane and the heaviest suite (Node + Python installs, | |
| # every plugins/**/*.test.sh, manifest + catalog validation). A PR whose diff | |
| # is confined to the docs-only allowlist (scripts/docs-only-paths.txt) cannot | |
| # affect any of it, so those runs report an honest evaluated-and-not-applicable | |
| # success. The job NEVER skips — a skipped required lane's result is not | |
| # `success` (the ci-status aggregate rejects it) and a workflow skipped by a | |
| # path filter leaves its required check Pending | |
| # (troubleshooting-required-status-checks). Only the inner install/test steps | |
| # are gated, on `needs.scope.outputs.run_full` from the `scope` lane, which | |
| # carries the self-test and fail-closed guarantees for every lane that reads | |
| # it. | |
| plugin-gate: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Set up Node | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| cache-dependency-path: package-lock.json | |
| - name: Set up Python | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.14' | |
| cache: pip | |
| cache-dependency-path: .github/requirements-ci.txt | |
| - name: Install and verify ShellCheck toolchain | |
| if: needs.scope.outputs.run_full == 'true' | |
| # This canonical action also makes the exact ShellCheck version | |
| # available to the later bash-format contract tests in this same job. | |
| uses: melodic-software/ci-workflows/.github/actions/shellcheck@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| paths: plugins/bash-format | |
| rcfile: .shellcheckrc | |
| # Contract tests run first and gate independently of the CLI validate step | |
| # below: a validate-infra problem (e.g. the CLI needing auth) can never mask | |
| # a real contract-test failure. shfmt is optional in both environments, so | |
| # shfmt-gated cases skip rather than fail. Node, Python, ShellCheck, Biome, | |
| # and Ruff are declared here so hosted and local runs exercise the same | |
| # contract suites instead of inheriting different image tool inventories. | |
| - name: Install locked plugin test toolchains | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: | | |
| npm ci | |
| python -m pip install --user --only-binary=:all: --require-hashes \ | |
| --requirement .github/requirements-ci.txt | |
| echo "$GITHUB_WORKSPACE/node_modules/.bin" >> "$GITHUB_PATH" | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: Run plugin contract tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: scripts/run-plugin-tests.sh | |
| # Explicit step: run-plugin-tests.sh discovers only plugins/**/*.test.sh, | |
| # so the cheat-sheet generator suite under scripts/ never runs without it. | |
| - name: Run cheat-sheet generator tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/generate-cheatsheet.test.sh | |
| # Same reason as the step above — run-plugin-tests.sh never reaches | |
| # scripts/. This suite is wired into CI rather than left to local runs | |
| # because its load-bearing cases assert against the LIVE repository: that | |
| # the shared-lib copy set affected-tests.sh derives still equals what each | |
| # scripts/sync-*.sh manifest declares today. That equality is what stops | |
| # the selector silently under-selecting after a manifest changes shape or | |
| # a new carrying plugin appears, and a check that only ever runs when | |
| # someone remembers to run it is exactly the rot it guards against. | |
| - name: Run affected-suite selector tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/affected-tests.test.sh | |
| # Same reason again — run-plugin-tests.sh never reaches scripts/. This | |
| # suite covers scripts/lib/changed-files.sh, the base-ref and changed-file | |
| # resolver the checker gates share. Its NUL-safety cases are the invariant | |
| # four of those gates had diverged on (#2914), and no caller suite | |
| # exercises a C-quotable pathname, so this is the only place a regression | |
| # there turns anything red. | |
| - name: Run shared changed-file resolver tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/lib/changed-files.test.sh | |
| # Same reason again — run-plugin-tests.sh never reaches scripts/. This | |
| # suite covers scripts/lib/read-list.sh, the list-file reader the gates | |
| # share. Its two comment modes are semantically different on purpose | |
| # (#3161) and each is asserted against the other's answer, so a change | |
| # that quietly collapses them turns this red rather than silently | |
| # truncating a token pattern. | |
| - name: Run shared list-file reader tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/lib/read-list.test.sh | |
| # Same reason again — run-plugin-tests.sh never reaches scripts/. This | |
| # suite covers scripts/lib/test-harness.sh, the ok/fail/report contract | |
| # the scripts/*.test.sh suites share. A recorded failure that still | |
| # exits 0 is the false-green this library exists to refuse (#3160). | |
| - name: Run shared test-harness tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/lib/test-harness.test.sh | |
| # Same reason again — run-plugin-tests.sh never reaches scripts/. The | |
| # self-test runs first so a broken validator cannot mask a regression | |
| # behind a green gate: no shipping plugin violates the check-only | |
| # carve-out assertions, so the shipping tree alone proves nothing about | |
| # them and only these fixtures show the gate still goes red (#3137). | |
| - name: Run plugin-contract validator tests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash scripts/validate-plugin-contracts.test.sh | |
| - name: Validate plugin and catalog manifests | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: scripts/validate-plugins.sh | |
| - name: Report not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the plugin contract suite cannot be affected — reporting success." | |
| # The miro plugin ships a bundled Node MCP server — the marketplace's first. | |
| # Its TypeScript source is the source of truth; dist/index.min.js is committed | |
| # generated output. This lane rebuilds from source with the pinned toolchain | |
| # and fails on any drift, then runs the bundle over stdio so a build that | |
| # compiles but cannot serve MCP is caught here, not on a consumer's machine. | |
| # Every step reads only plugins/miro/**, so a docs-only diff cannot affect it; | |
| # it reads the same `scope` lane output as plugin-gate and never skips as a | |
| # job. (Scoping this lane to plugins/miro/** specifically — skipping it | |
| # on any non-miro diff — is a broader, separately-tracked optimization.) | |
| miro-plugin: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Set up Node | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| cache-dependency-path: plugins/miro/package-lock.json | |
| - name: Install dependencies | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: npm ci | |
| working-directory: plugins/miro | |
| - name: Typecheck | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: npm run typecheck | |
| working-directory: plugins/miro | |
| - name: Lint | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: npm run lint | |
| working-directory: plugins/miro | |
| - name: Test | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: npm test | |
| working-directory: plugins/miro | |
| - name: Verify the committed bundle matches source | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: npm run verify-bundle | |
| working-directory: plugins/miro | |
| - name: Smoke-test the bundled MCP server over stdio | |
| if: needs.scope.outputs.run_full == 'true' | |
| working-directory: plugins/miro | |
| run: | | |
| printf '%s\n%s\n' \ | |
| '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0.0.0"}}}' \ | |
| '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \ | |
| | MIRO_API_TOKEN=ci-smoke-token timeout 10 node dist/index.min.js > smoke-out.json | |
| grep -q '"miro_create_board"' smoke-out.json | |
| rm -f smoke-out.json | |
| - name: Report not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the miro plugin build cannot be affected — reporting success." | |
| # The knowledge plugin's video-digest extraction pipeline is a Node package | |
| # with a typecheck and vitest suite no other lane installs or runs. Its | |
| # committed lockfile pins the shared vendor/ packages as packed installs | |
| # (install-links via the package's .npmrc), so manifest/lockfile drift breaks | |
| # `npm ci` only on a clean install — without this lane that breakage stays | |
| # latent until a consumer's machine hits it. Reads the same `scope` lane | |
| # output as plugin-gate and miro-plugin. (Scoping this | |
| # lane to its own paths — skipping it on unrelated diffs — is the same | |
| # separately-tracked optimization noted on miro-plugin.) | |
| video-extraction: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Set up Node | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| # Lock-file path for cache keying only (KIND-2 data reference under the | |
| # encapsulation contract); logic goes through the skill's scripts/ facade below. | |
| cache-dependency-path: plugins/knowledge/skills/video-digest/extraction/package-lock.json | |
| - name: Install dependencies | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/video-digest/scripts/run-tests.sh install | |
| - name: Typecheck | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/video-digest/scripts/run-tests.sh build | |
| - name: Test | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/video-digest/scripts/run-tests.sh test | |
| - name: Report not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the video extraction suite cannot be affected — reporting success." | |
| # ai-briefing's build/render pipeline is a Node package (native `node --test` | |
| # suite) no other lane installs or runs — including test/url-policy.test.js, | |
| # which asserts the SSRF gate (lib/url-policy.js) that decides which URLs the | |
| # briefing validator is allowed to dereference: loopback/private/link-local/ | |
| # CGN/benchmarking/documentation/multicast rejection, DNS-gate-time A/AAAA | |
| # resolution, and the IPv6 2000::/3 allowlist inversion. Without this lane | |
| # that suite runs locally only, so a future refactor could silently break the | |
| # predicate with nothing red in CI (#1488). Reads the same `scope` lane | |
| # output as plugin-gate, miro-plugin and | |
| # video-extraction. (Scoping this lane to its own paths — skipping it on | |
| # unrelated diffs — is the same separately-tracked optimization noted on | |
| # miro-plugin.) | |
| ai-briefing-build: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Set up Node | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| # Lock-file path for cache keying only (KIND-2 data reference under the | |
| # encapsulation contract); logic goes through the skill's scripts/ facade below. | |
| cache-dependency-path: plugins/ai-briefing/skills/generate/output/build/package-lock.json | |
| - name: Install dependencies | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/ai-briefing/skills/generate/scripts/run-tests.sh install | |
| - name: Test | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/ai-briefing/skills/generate/scripts/run-tests.sh test | |
| - name: Report not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the ai-briefing build suite cannot be affected — reporting success." | |
| # The knowledge plugin's course-digest extraction pipeline is a Node package | |
| # (typecheck + vitest suite) no other lane installs or runs — same shape as | |
| # video-extraction, including the shared vendor/ packages pulled in as | |
| # file: dependencies via the package's .npmrc install-links=true. Without | |
| # this lane the suite (utils, the adapter contract, the dometrain/teachable | |
| # adapters, clerk/teachable-sso auth, config, and the hotmart/mux players) | |
| # runs locally only, so a future refactor could silently break it with | |
| # nothing red in CI (#1507). Reads the same `scope` lane output as | |
| # plugin-gate, miro-plugin, video-extraction and | |
| # ai-briefing-build. (Scoping this lane to its own paths — skipping it on | |
| # unrelated diffs — is the same separately-tracked optimization noted on | |
| # miro-plugin.) | |
| course-digest-extraction: | |
| needs: [scope] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Set up Node | |
| if: needs.scope.outputs.run_full == 'true' | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| # Lock-file path for cache keying only (KIND-2 data reference under the | |
| # encapsulation contract); logic goes through the skill's scripts/ facade below. | |
| cache-dependency-path: plugins/knowledge/skills/course-digest/extraction/package-lock.json | |
| - name: Install dependencies | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/course-digest/scripts/run-tests.sh install | |
| - name: Typecheck | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/course-digest/scripts/run-tests.sh build | |
| - name: Test | |
| if: needs.scope.outputs.run_full == 'true' | |
| run: bash plugins/knowledge/skills/course-digest/scripts/run-tests.sh test | |
| - name: Report not applicable to a docs-only diff | |
| if: needs.scope.outputs.run_full == 'false' | |
| run: echo "Diff is within the docs-only allowlist (scripts/docs-only-paths.txt); the course-digest extraction suite cannot be affected — reporting success." | |
| # Skill-regression net: the only lane that invokes the skill-quality checker. | |
| # On a PR it runs the static contract gate (trigger-keyword preservation vs | |
| # the base ref, frontmatter, line caps, broken refs, evals presence) over each | |
| # changed skill under plugins/*/skills/**, replacing the work lane's manual | |
| # high-blast-radius skill-diff read with a deterministic check. Eval sets are | |
| # schema-validated AND quality-linted (check-evals-quality.sh) on every | |
| # event. The job itself never skips (a skipped required job reports success | |
| # to branch protection) — only the PR-diff step is event-gated, and the | |
| # self-test and eval steps give push a passing path. | |
| skill-quality-gate: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| # Self-test first so a broken orchestrator can never mask a real skill | |
| # regression behind a green gate. | |
| - name: Test the changed-skill gate | |
| run: bash scripts/check-changed-skills.test.sh | |
| - name: Gate changed skills on the static skill-quality contract | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-changed-skills.sh "origin/$BASE_REF" | |
| - name: Validate every eval set against the bundled schema | |
| uses: melodic-software/ci-workflows/.github/actions/check-jsonschema@c2654182bc2d78f7909795df78304d482aa69226 # c265418 2026-07-13 | |
| with: | |
| schemafile: plugins/skill-quality/reference/evals.schema.json | |
| files: plugins/*/skills/*/evals/evals.json | |
| # Self-test first so a broken lint cannot mask a regression behind a | |
| # green step. | |
| - name: Test the eval-quality lint | |
| run: bash plugins/skill-quality/scripts/check-evals-quality.test.sh | |
| # Beyond-schema quality lint (FAIL tier: duplicate case ids/names, empty | |
| # criterion items, unresolvable files fixtures; WARN tier stays advisory | |
| # and never fails the step). Whole-repo and static, so it runs on every | |
| # event like the schema step above. | |
| - name: Lint every eval set for quality | |
| run: bash plugins/skill-quality/scripts/check-evals-quality.sh plugins/*/skills/*/evals/evals.json | |
| - name: Test the precompute-compose gate | |
| run: bash scripts/check-skill-precompute-compose.test.sh | |
| - name: Report precompute git+multi-line violations (warn-only) | |
| run: scripts/check-skill-precompute-compose.sh --all | |
| - name: Test the shared listing-budget reporter | |
| run: bash plugins/skill-quality/scripts/check-listing-budget.test.sh | |
| # Report-only (always exit 0 — see the script's own header): pools every | |
| # plugin's skills root into ONE shared aggregate, the shape a consumer who | |
| # installs the whole marketplace actually experiences. Runs on every | |
| # event, unlike the PR-diff-gated step above, since the aggregate is a | |
| # whole-repo property, not a per-change one. | |
| - name: Report the shared skill-listing budget across every plugin | |
| run: bash plugins/skill-quality/scripts/check-listing-budget.sh plugins/*/skills | |
| # Portability lint: skills declared ecosystem/forge/tracker-agnostic must not | |
| # ship bare hardcoded stack/forge/branch/tracker defaults — the coupling class | |
| # the external reviewer re-caught PR after PR. On a PR it scans the skill files | |
| # the change touches (not the whole corpus) against the coupling-token list, so | |
| # enabling a token class prevents NEW coupling without red-lining existing | |
| # violations that member issues own. The job itself never skips (a skipped | |
| # required job reports success to branch protection) — only the PR-diff step is | |
| # event-gated, and the self-test gives push a passing path and stops a broken | |
| # detector masking a real violation behind a green gate. | |
| portability-lint: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the portability gate | |
| run: bash scripts/check-skill-portability.test.sh | |
| - name: Gate changed skill files on the portability contract | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-skill-portability.sh "origin/$BASE_REF" | |
| # Shell-portability lint (#1491 / #2704): flags GNU-only constructs in | |
| # changed **/*.sh files and skill markdown under plugins/*/skills/ — a class | |
| # neither shellcheck (syntax/style) nor portability-lint (skill-coupling | |
| # tokens, skill files only) covers, and no runner here uses BSD userland | |
| # (macOS system grep/sed/date/stat/mktemp/sort — a Windows runner's Git Bash | |
| # still ships GNU grep/sed, so it would not help either). Same shape as | |
| # portability-lint: changed-file scoped so enabling a class never red-lines | |
| # main, self-test-first so a broken detector cannot mask a regression behind | |
| # a green gate, job never skips. Skill-md backlog is grandfathered in | |
| # scripts/shell-portability-skill-md-baseline.txt. | |
| shell-portability-lint: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Fetch base | |
| uses: ./.github/actions/checkout-with-base | |
| - name: Test the shell-portability gate | |
| run: bash scripts/check-shell-portability.test.sh | |
| - name: Gate changed shell and skill-md files for GNU-only constructs | |
| if: github.event_name == 'pull_request' | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: scripts/check-shell-portability.sh "origin/$BASE_REF" | |
| runner-policy: | |
| name: Runner policy | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Check out | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up Node | |
| uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version-file: .node-version | |
| cache: npm | |
| cache-dependency-path: .github/standards/runner-policy/package-lock.json | |
| - name: Install pinned runner-policy dependencies | |
| run: npm ci --prefix .github/standards/runner-policy | |
| - name: Enforce runner policy | |
| run: node .github/standards/runner-policy/runner-policy.mjs --root . | |
| env: | |
| CI_REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }} | |
| ci-status: | |
| needs: | |
| - scope | |
| - hygiene | |
| - hook-utils-sync | |
| - hook-utils-windows | |
| - disk-hygiene-guard-windows | |
| - disk-hygiene-tests | |
| - windows-path-emit-gate | |
| - windows-path-emit-windows | |
| - parse-concern-value-sync | |
| - managed-scope-sync | |
| - state-key-sync | |
| - resolve-convention-pattern-sync | |
| - standards-contract-sync | |
| - cross-plugin-source-drift | |
| - detector-findings-crosswalk-gate | |
| - skill-leaf-name-gate | |
| - skill-count-claim-gate | |
| - userconfig-argv-gate | |
| - hook-exec-form-gate | |
| - summary-reader-parity-gate | |
| - plugin-options-docs-gate | |
| - silent-skip-gate | |
| - stale-base-overlap-gate | |
| - plugin-manifest-presence-gate | |
| - plugin-catalog-enablement-gate | |
| - orphaned-fixture-gate | |
| - fixture-git-isolation-gate | |
| - fleet-finding-test-coverage-gate | |
| - fleet-audit-doc-grammar-gate | |
| - changelog-parity-gate | |
| - contract-slice-prune-gate | |
| - contract-clause-coverage-gate | |
| - docs-only-gate | |
| - lane-coverage-gate | |
| - plugin-gate | |
| - miro-plugin | |
| - video-extraction | |
| - ai-briefing-build | |
| - course-digest-extraction | |
| - runner-policy | |
| - skill-quality-gate | |
| - portability-lint | |
| - shell-portability-lint | |
| - zizmor | |
| # Fail-closed through execution: !cancelled() (never a success-guard) so a | |
| # lane failure still runs this required aggregate and the result join below | |
| # turns it red. A success-guard would skip the job, and a skipped required | |
| # check reports success to branch protection. | |
| if: ${{ !cancelled() }} | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Aggregate lane results | |
| # Derived from the needs graph above — the single source of truth for the | |
| # lane list. Adding a lane to needs automatically extends this check. | |
| env: | |
| RESULTS: ${{ join(needs.*.result, ' ') }} | |
| run: | | |
| for r in $RESULTS; do | |
| case "$r" in | |
| success) ;; | |
| *) echo "A required lane did not pass (result: $r)."; exit 1 ;; | |
| esac | |
| done | |
| echo "All required lanes passed." |