From 30c26c7bd33ecef79abeeede8a55d2d416c1af84 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 25 Sep 2026 23:06:27 -0700 Subject: [PATCH 1/4] Distinguish ./ From $/ Resolution in the Pin Rule's Prose A $/ reference resolves at the containing workflow file's commit, while a ./ action reference resolves against whatever the job checked out, which inside a cross-repository reusable workflow is the caller's checkout. State the difference in scripts/README.md's sha-pin bullet and in WORKFLOW.md's test-methodology trace paragraph, and regenerate the skill copies that include the latter. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- .../fleet-skills/.source-digests/workflow-ci-contract | 2 +- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- WORKFLOW.md | 2 +- scripts/README.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.agents/skills/workflow-ci-contract/references/test-methodology.md b/.agents/skills/workflow-ci-contract/references/test-methodology.md index f7690a0aa..5aa1ca4ae 100644 --- a/.agents/skills/workflow-ci-contract/references/test-methodology.md +++ b/.agents/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A local (`./`) or self-repository (`$/`) call carries no pin of its own and runs at the workflow commit, so it is traced at whatever SHA the outermost pinning caller fixed. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 71f6542ba..5274d3f57 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -4fd3a739a2fc09e2 +7029473a4c46866a diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md index f7690a0aa..5aa1ca4ae 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A local (`./`) or self-repository (`$/`) call carries no pin of its own and runs at the workflow commit, so it is traced at whatever SHA the outermost pinning caller fixed. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.github/skills/workflow-ci-contract/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md index f7690a0aa..5aa1ca4ae 100644 --- a/.github/skills/workflow-ci-contract/references/test-methodology.md +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A local (`./`) or self-repository (`$/`) call carries no pin of its own and runs at the workflow commit, so it is traced at whatever SHA the outermost pinning caller fixed. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/WORKFLOW.md b/WORKFLOW.md index 211ba45ee..3fe24fcd6 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -226,7 +226,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A local (`./`) or self-repository (`$/`) call carries no pin of its own and runs at the workflow commit, so it is traced at whatever SHA the outermost pinning caller fixed. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/scripts/README.md b/scripts/README.md index 4ffc84c0d..b228dcf02 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -111,7 +111,7 @@ Every rule in the default set is clean tree-wide except `comment-added`, which r Three deterministic checks: -- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references run at the workflow commit, so they need no separate pin. +- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference resolves at the containing workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout. - `eol`: every path pinned LF in [`.gitattributes`][gitattributes] has the matching [`.editorconfig`][editorconfig] override the line-ending rule requires, with EditorConfig brace syntax expanded. One direction only: an `.editorconfig` LF glob with no git pin is legitimate, since `.editorconfig` governs what the editor writes where git enforces a class it must not guess at. - `eol-coverage`: the same pins read against the tree instead. A tracked file opening `#!` that git does not resolve to `eol=lf` is an interpreter line a CRLF checkout breaks, and a pin matching no tracked file is dead unless its block is marked `forward-declared`. From da78261a8d85b93cde8e489b2b313dbc00f1b1f0 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 25 Sep 2026 23:08:55 -0700 Subject: [PATCH 2/4] Cover Job-Level ./ Calls and the Checkout Condition in the Pin Prose A job-level ./ reusable-workflow call resolves at the calling workflow file's commit, like $/, and a ./ action reference names the caller's tree only where the reusable workflow checks out its caller. Align check_sha_pin's docstring with the same distinction. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- .../fleet-skills/.source-digests/workflow-ci-contract | 2 +- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- .github/actions/repo-gate/repo_gate.py | 2 +- .../skills/workflow-ci-contract/references/test-methodology.md | 2 +- WORKFLOW.md | 2 +- scripts/README.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.agents/skills/workflow-ci-contract/references/test-methodology.md b/.agents/skills/workflow-ci-contract/references/test-methodology.md index 5aa1ca4ae..2c3808447 100644 --- a/.agents/skills/workflow-ci-contract/references/test-methodology.md +++ b/.agents/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 5274d3f57..2f5c07cf1 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -7029473a4c46866a +5630c1e6833a43af diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md index 5aa1ca4ae..2c3808447 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.github/actions/repo-gate/repo_gate.py b/.github/actions/repo-gate/repo_gate.py index 2f0ab8be0..5c33c4e1d 100755 --- a/.github/actions/repo-gate/repo_gate.py +++ b/.github/actions/repo-gate/repo_gate.py @@ -218,7 +218,7 @@ def resolved_eol(root: Path, paths: list[str]) -> dict[str, str] | None: def check_sha_pin(root: Path, files: list[str]) -> list[str]: """Every external `uses:` is a 40-hex SHA, and one under this owner resolves. - A local or self-repository ref names the running commit and is skipped. References under + A local or self-repository ref names no ref to pin and is skipped. References under another owner are shape-checked but not resolved. Resolution is scoped to the scanned repository's own owner, because that is where the fleet's diff --git a/.github/skills/workflow-ci-contract/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md index 5aa1ca4ae..2c3808447 100644 --- a/.github/skills/workflow-ci-contract/references/test-methodology.md +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/WORKFLOW.md b/WORKFLOW.md index 3fe24fcd6..fa3a990dd 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -226,7 +226,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call carries no pin of its own and resolves at the containing workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout, so it is traced at that checkout's commit. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/scripts/README.md b/scripts/README.md index b228dcf02..eeb816a17 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -111,7 +111,7 @@ Every rule in the default set is clean tree-wide except `comment-added`, which r Three deterministic checks: -- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference resolves at the containing workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which inside a reusable workflow called from another repository is the caller's checkout. +- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference and a job-level `./` reusable-workflow call resolve at the calling workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which is the caller's tree where a reusable workflow called from another repository checks out its caller. - `eol`: every path pinned LF in [`.gitattributes`][gitattributes] has the matching [`.editorconfig`][editorconfig] override the line-ending rule requires, with EditorConfig brace syntax expanded. One direction only: an `.editorconfig` LF glob with no git pin is legitimate, since `.editorconfig` governs what the editor writes where git enforces a class it must not guess at. - `eol-coverage`: the same pins read against the tree instead. A tracked file opening `#!` that git does not resolve to `eol=lf` is an interpreter line a CRLF checkout breaks, and a pin matching no tracked file is dead unless its block is marked `forward-declared`. From e0f1c3558f65e0e0392e4cf9f4ce29a13b817a54 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 26 Sep 2026 06:30:56 -0700 Subject: [PATCH 3/4] Name the Bare .github/ Skip in the sha-pin Prose check_sha_pin also skips a ref starting with a bare .github/ path, so its docstring and the scripts/README.md sha-pin bullet list that form beside ./ and $/. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/actions/repo-gate/repo_gate.py | 4 ++-- scripts/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/repo-gate/repo_gate.py b/.github/actions/repo-gate/repo_gate.py index 5c33c4e1d..8452f894c 100755 --- a/.github/actions/repo-gate/repo_gate.py +++ b/.github/actions/repo-gate/repo_gate.py @@ -218,8 +218,8 @@ def resolved_eol(root: Path, paths: list[str]) -> dict[str, str] | None: def check_sha_pin(root: Path, files: list[str]) -> list[str]: """Every external `uses:` is a 40-hex SHA, and one under this owner resolves. - A local or self-repository ref names no ref to pin and is skipped. References under - another owner are shape-checked but not resolved. + A local ref (`./`, or a bare `.github/` path) or a self-repository ref (`$/`) names no ref to + pin and is skipped. References under another owner are shape-checked but not resolved. Resolution is scoped to the scanned repository's own owner, because that is where the fleet's own actions live and where the decay this catches comes from: a squash merge deletes the diff --git a/scripts/README.md b/scripts/README.md index eeb816a17..da19af4fd 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -111,7 +111,7 @@ Every rule in the default set is clean tree-wide except `comment-added`, which r Three deterministic checks: -- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference and a job-level `./` reusable-workflow call resolve at the calling workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which is the caller's tree where a reusable workflow called from another repository checks out its caller. +- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`, or a bare `.github/` path) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference and a job-level `./` reusable-workflow call resolve at the calling workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which is the caller's tree where a reusable workflow called from another repository checks out its caller. - `eol`: every path pinned LF in [`.gitattributes`][gitattributes] has the matching [`.editorconfig`][editorconfig] override the line-ending rule requires, with EditorConfig brace syntax expanded. One direction only: an `.editorconfig` LF glob with no git pin is legitimate, since `.editorconfig` governs what the editor writes where git enforces a class it must not guess at. - `eol-coverage`: the same pins read against the tree instead. A tracked file opening `#!` that git does not resolve to `eol=lf` is an interpreter line a CRLF checkout breaks, and a pin matching no tracked file is dead unless its block is marked `forward-declared`. From 063e6151ce301b697f0e208c2409aee8a0b86b1a Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 26 Sep 2026 06:32:54 -0700 Subject: [PATCH 4/4] State the Bare .github/ Skip as Unvalidated and Trace at the Calling File's Commit A bare .github/ ref is not a local reference GitHub accepts, so the sha-pin prose names it as a form the check skips without validating rather than as a local reference (#1889 tracks the code). The tracing sentence now traces a $/ or job-level ./ callee at its calling file's own commit rather than at the outermost pin, which it contradicted. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../workflow-ci-contract/references/test-methodology.md | 2 +- .../fleet-skills/.source-digests/workflow-ci-contract | 2 +- .../workflow-ci-contract/references/test-methodology.md | 2 +- .github/actions/repo-gate/repo_gate.py | 5 +++-- .../workflow-ci-contract/references/test-methodology.md | 2 +- WORKFLOW.md | 2 +- scripts/README.md | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.agents/skills/workflow-ci-contract/references/test-methodology.md b/.agents/skills/workflow-ci-contract/references/test-methodology.md index 2c3808447..36f359f2b 100644 --- a/.agents/skills/workflow-ci-contract/references/test-methodology.md +++ b/.agents/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at that file's own commit, which is the SHA fixed by the pin that reached that file, or the audited commit where that file is one of the audited repository's own workflows. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract index 2f5c07cf1..a40930e99 100644 --- a/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract +++ b/.claude-plugin/fleet-skills/.source-digests/workflow-ci-contract @@ -1 +1 @@ -5630c1e6833a43af +ac509cee1085405f diff --git a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md index 2c3808447..36f359f2b 100644 --- a/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md +++ b/.claude-plugin/fleet-skills/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at that file's own commit, which is the SHA fixed by the pin that reached that file, or the audited commit where that file is one of the audited repository's own workflows. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/.github/actions/repo-gate/repo_gate.py b/.github/actions/repo-gate/repo_gate.py index 8452f894c..2d43d651d 100755 --- a/.github/actions/repo-gate/repo_gate.py +++ b/.github/actions/repo-gate/repo_gate.py @@ -218,8 +218,9 @@ def resolved_eol(root: Path, paths: list[str]) -> dict[str, str] | None: def check_sha_pin(root: Path, files: list[str]) -> list[str]: """Every external `uses:` is a 40-hex SHA, and one under this owner resolves. - A local ref (`./`, or a bare `.github/` path) or a self-repository ref (`$/`) names no ref to - pin and is skipped. References under another owner are shape-checked but not resolved. + A local (`./`) or self-repository (`$/`) ref names no ref to pin and is skipped, and so is + one starting with a bare `.github/`, unvalidated. References under another owner are + shape-checked but not resolved. Resolution is scoped to the scanned repository's own owner, because that is where the fleet's own actions live and where the decay this catches comes from: a squash merge deletes the diff --git a/.github/skills/workflow-ci-contract/references/test-methodology.md b/.github/skills/workflow-ci-contract/references/test-methodology.md index 2c3808447..36f359f2b 100644 --- a/.github/skills/workflow-ci-contract/references/test-methodology.md +++ b/.github/skills/workflow-ci-contract/references/test-methodology.md @@ -18,7 +18,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at that file's own commit, which is the SHA fixed by the pin that reached that file, or the audited commit where that file is one of the audited repository's own workflows. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/WORKFLOW.md b/WORKFLOW.md index fa3a990dd..0fb02dcff 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -226,7 +226,7 @@ Cite what each verdict rests on. That is `file:line` for a file in the audited r ### 5B. End-to-End Trace Scenarios (No Execution, Deterministic from the YAML) -For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at whatever SHA the outermost pinning caller fixed. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: +For each *applicable* scenario, evaluate every job's `if:`/`needs:` against the inputs and emit the predicted **run/skip + version + release + artifact-end-state** table, then compare to the expected. A scenario governing a construct the repo does not contain is N/A, per `WORKFLOW.md` section 1, and an absent trigger is such a construct. Each scenario's trigger belongs to one workflow, so read that workflow's own `on:` block rather than the repo's type: S1 to S4 the pull request workflow's, S5 to S10 the publisher's, S11 the upstream tracker's, and S12 and S13 the deploy workflow's. A publisher carrying only `workflow_dispatch` therefore records S5, S6 and S9 N/A, their push and schedule paths never firing there, and a repo with no publisher at all records S5 to S10 N/A together. Where a scenario's path runs through a workflow or composite action the repo only **calls**, trace that callee as the repo reaches it, read at the SHA the caller pins rather than at the callee's current default branch, which is the same evidence rule 5A states. Predicting from the callee's `main` predicts a table for YAML the audited repo never runs. A self-repository (`$/`) call or a job-level local (`./`) reusable-workflow call carries no pin of its own and resolves at the calling workflow file's commit, so it is traced at that file's own commit, which is the SHA fixed by the pin that reached that file, or the audited commit where that file is one of the audited repository's own workflows. A local (`./`) action reference carries no pin either, but it resolves against whatever the job checked out, so it is traced at that checkout's commit, which is the caller's where a reusable workflow called from another repository checks out its caller. Minimum set: | # | Input | Expected output | Exercises | | --- | --- | --- | --- | diff --git a/scripts/README.md b/scripts/README.md index da19af4fd..5e3792e86 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -111,7 +111,7 @@ Every rule in the default set is clean tree-wide except `comment-added`, which r Three deterministic checks: -- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`, or a bare `.github/` path) and self-repository (`$/`) references name no ref, so they take no pin. They resolve differently, though. A `$/` reference and a job-level `./` reusable-workflow call resolve at the calling workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which is the caller's tree where a reusable workflow called from another repository checks out its caller. +- `sha-pin`: every external action or reusable-workflow `uses:` reference is a 40-hex commit SHA, with the documented `dotnet/nbgv@master` exception. References under the scanned repository's owner are also resolved through GitHub. References under another owner are shape-checked only. Local (`./`) and self-repository (`$/`) references name no ref, so they take no pin, and the check also skips a reference starting with a bare `.github/` without validating it. They resolve differently, though. A `$/` reference and a job-level `./` reusable-workflow call resolve at the calling workflow file's commit, while a `./` action reference resolves against whatever the job checked out, which is the caller's tree where a reusable workflow called from another repository checks out its caller. - `eol`: every path pinned LF in [`.gitattributes`][gitattributes] has the matching [`.editorconfig`][editorconfig] override the line-ending rule requires, with EditorConfig brace syntax expanded. One direction only: an `.editorconfig` LF glob with no git pin is legitimate, since `.editorconfig` governs what the editor writes where git enforces a class it must not guess at. - `eol-coverage`: the same pins read against the tree instead. A tracked file opening `#!` that git does not resolve to `eol=lf` is an interpreter line a CRLF checkout breaks, and a pin matching no tracked file is dead unless its block is marked `forward-declared`.