From 85d43badd49f2973a544bb1e5c77c87ae2e7193b Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sat, 16 May 2026 16:39:02 -0500 Subject: [PATCH 1/7] fix(compliance): track per-workflow version tags in stub checker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The centralized-stub check used a hardcoded @v1 for all workflows, but pr-review-mention-reusable was already bumped to v2 and its standard template updated accordingly. This caused the checker to flag every valid v2 stub as non-compliant. Changes: - Extend the centralized array format from wf:reusable to wf:reusable:version so each workflow carries its own canonical tag - Mark pr-review-mention as v2; all other stubs remain at v1 - Update ci-standards.md compliance note for §10 to reference @v2 --- scripts/compliance-audit.sh | 43 ++++++++++++++++++++----------------- standards/ci-standards.md | 2 +- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 578b3df69..2e37105af 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -677,17 +677,20 @@ check_ci_concurrency() { } # --------------------------------------------------------------------------- -# Check: Tier 1 centralized workflows must be thin caller stubs pinned to @v1 +# Check: Tier 1 centralized workflows must be thin caller stubs pinned to the +# canonical version tag for their reusable. # # For each workflow that the org has centralized into a reusable workflow, # verify the downstream repo's copy is a stub that delegates via: -# uses: petry-projects/.github/.github/workflows/.yml@v1 +# uses: petry-projects/.github/.github/workflows/.yml@ # # This prevents drift: a repo that copies the inline pre-centralization -# version (or pins to @main, or pins to an older tag) is flagged so it -# can be re-synced from the standard. The central .github repo itself is +# version (or pins to @main, or pins to a non-canonical tag) is flagged so +# it can be re-synced from the standard. The central .github repo itself is # exempt because it owns the reusables and may legitimately reference # its own workflows by @main during release prep. +# +# Array format: "workflow-filename:expected-reusable-basename:version-tag" # --------------------------------------------------------------------------- check_centralized_workflow_stubs() { local repo="$1" @@ -696,16 +699,16 @@ check_centralized_workflow_stubs() { # own reusables by @main; skip the stub check for it. [ "$repo" = ".github" ] && return - # workflow-filename:expected-reusable-basename + # workflow-filename:expected-reusable-basename:version-tag local centralized=( - "dev-lead.yml:dev-lead-reusable" - "auto-rebase.yml:auto-rebase-reusable" - "dependency-audit.yml:dependency-audit-reusable" - "dependabot-automerge.yml:dependabot-automerge-reusable" - "dependabot-rebase.yml:dependabot-rebase-reusable" - "agent-shield.yml:agent-shield-reusable" - "feature-ideation.yml:feature-ideation-reusable" - "pr-review-mention.yml:pr-review-mention-reusable" + "dev-lead.yml:dev-lead-reusable:v1" + "auto-rebase.yml:auto-rebase-reusable:v1" + "dependency-audit.yml:dependency-audit-reusable:v1" + "dependabot-automerge.yml:dependabot-automerge-reusable:v1" + "dependabot-rebase.yml:dependabot-rebase-reusable:v1" + "agent-shield.yml:agent-shield-reusable:v1" + "feature-ideation.yml:feature-ideation-reusable:v1" + "pr-review-mention.yml:pr-review-mention-reusable:v2" ) # List the repo's workflow directory once instead of probing each file. @@ -714,9 +717,9 @@ check_centralized_workflow_stubs() { workflow_list=$(gh_api "repos/$ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null || echo "") [ -z "$workflow_list" ] && return - local entry wf reusable + local entry wf reusable version for entry in "${centralized[@]}"; do - IFS=':' read -r wf reusable <<< "$entry" + IFS=':' read -r wf reusable version <<< "$entry" # Skip workflows that don't exist in this repo. Required workflows are # checked separately by check_required_workflows; conditional ones @@ -734,19 +737,19 @@ check_centralized_workflow_stubs() { [ -z "$decoded" ] && continue # Required pattern: a non-comment line whose `uses:` value is exactly - # petry-projects/.github/.github/workflows/.yml@v1 + # petry-projects/.github/.github/workflows/.yml@ # Anchor to start-of-line + optional indent so a `# uses: ...` comment # cannot satisfy the check. - local expected="petry-projects/\\.github/\\.github/workflows/${reusable}\\.yml@v1" + local expected="petry-projects/\\.github/\\.github/workflows/${reusable}\\.yml@${version}" if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*${expected}([[:space:]]|$)"; then - continue # stub is correctly pinned to @v1 — compliant + continue # stub is correctly pinned to the canonical version — compliant fi # Determine why it's non-compliant for a more actionable message. local why if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*petry-projects/\\.github/\\.github/workflows/${reusable}\\.yml@"; then - why="references the reusable but is not pinned to \`@v1\` (org standard)" + why="references the reusable but is not pinned to \`@${version}\` (org standard)" elif echo "$decoded" | grep -qF "petry-projects/.github/.github/workflows/${reusable}"; then why="references the reusable but the \`uses:\` line does not match the canonical stub" else @@ -754,7 +757,7 @@ check_centralized_workflow_stubs() { fi add_finding "$repo" "ci-workflows" "non-stub-$wf" "error" \ - "Centralized workflow \`$wf\` $why. Replace with the canonical stub from \`standards/workflows/${wf}\` which delegates to \`petry-projects/.github/.github/workflows/${reusable}.yml@v1\`." \ + "Centralized workflow \`$wf\` $why. Replace with the canonical stub from \`standards/workflows/${wf}\` which delegates to \`petry-projects/.github/.github/workflows/${reusable}.yml@${version}\`." \ "standards/ci-standards.md#centralization-tiers" done } diff --git a/standards/ci-standards.md b/standards/ci-standards.md index 5640fdb29..4dbba4396 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -556,7 +556,7 @@ present as an org-level secret; no per-repo setup needed. **Compliance:** The compliance audit (`check_centralized_workflow_stubs`) verifies that repos have `pr-review-mention.yml` as a thin caller stub delegating to -`petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@v1`. +`petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@v2`. --- From 4b8204929611dddc5641d4b7af2b977e5264ad5b Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sat, 16 May 2026 16:56:29 -0500 Subject: [PATCH 2/7] fix(compliance): guard against missing version tag in centralized array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If an entry is added without the third colon-delimited field, `version` is empty and the expected regex becomes `...reusable.yml@` — malformed and silently matching nothing. Fail fast with a clear error instead. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- scripts/compliance-audit.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 2e37105af..120e8c734 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -720,6 +720,7 @@ check_centralized_workflow_stubs() { local entry wf reusable version for entry in "${centralized[@]}"; do IFS=':' read -r wf reusable version <<< "$entry" + [ -z "$version" ] && { echo "::error::centralized entry '$entry' missing version tag — expected format 'wf:reusable:version'" >&2; exit 1; } # Skip workflows that don't exist in this repo. Required workflows are # checked separately by check_required_workflows; conditional ones From 3645418a2ac0185d3c94b5d05ea8af5d666b6fe8 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sat, 16 May 2026 17:54:54 -0500 Subject: [PATCH 3/7] docs(ci-standards): update tier table and pin rationale for per-workflow versions The Tier 1 table and "Why pin to @v1?" blurb both implied all stubs use @v1 universally. Now that pr-review-mention is @v2, update both to reflect that each reusable carries its own canonical tag. Addresses Codex review comment on PR #302. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- standards/ci-standards.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/standards/ci-standards.md b/standards/ci-standards.md index 4dbba4396..95fae60ca 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -22,7 +22,7 @@ where to send a fix when behavior needs to change. | Tier | Examples | What lives in `standards/workflows/` | Where logic lives | Edits allowed in adopting repo | |---|---|---|---|---| -| **1. Stub** | `dev-lead.yml`, `dependency-audit.yml`, `dependabot-automerge.yml`, `dependabot-rebase.yml`, `agent-shield.yml`, `feature-ideation.yml` | A thin caller stub that delegates via `uses: petry-projects/.github/.github/workflows/-reusable.yml@v1` | The matching `*-reusable.yml` in this repo (single source of truth) | **None** in normal use. May tune `with:` inputs where the reusable exposes them (e.g. `agent-shield` accepts `min-severity`, `required-files`; `feature-ideation` requires `project_context`). To change behavior, open a PR against the reusable in this repo — repos on `@v1` pick it up after the `v1` tag is bumped; repos on `@main` pick it up on their next run. | +| **1. Stub** | `dev-lead.yml`, `dependency-audit.yml`, `dependabot-automerge.yml`, `dependabot-rebase.yml`, `agent-shield.yml`, `feature-ideation.yml`, `pr-review-mention.yml` | A thin caller stub that delegates via `uses: petry-projects/.github/.github/workflows/-reusable.yml@`, where `` is the canonical tag for that reusable (see `check_centralized_workflow_stubs` in `scripts/compliance-audit.sh`; most are `@v1`, `pr-review-mention` is `@v2`) | The matching `*-reusable.yml` in this repo (single source of truth) | **None** in normal use. May tune `with:` inputs where the reusable exposes them (e.g. `agent-shield` accepts `min-severity`, `required-files`; `feature-ideation` requires `project_context`). To change behavior, open a PR against the reusable in this repo — the canonical tag is bumped deliberately when a release is ready. | | **2. Per-repo template** | `ci.yml`, `sonarcloud.yml` | _(no template — see the patterns documented below)_ | In each repo, because the workflow is tech-stack-specific (language matrix, build tool, test framework) | **Limited.** Each adopting repo carries its own copy. Stay within the patterns in this document; do not change action SHAs, permission scopes, trigger events, or job names without raising a standards PR first. | | **GitHub-managed** | CodeQL default setup | _(no workflow file — managed via repo Settings → Code security)_ | GitHub | None. Configured via `apply-repo-settings.sh`; per-repo `codeql.yml` files are treated as drift by the compliance audit. See [§2 CodeQL Analysis](#2-codeql-analysis-github-managed-default-setup). | | **3. Free per-repo** | `release.yml`, project-specific automation | _(out of scope for this standard)_ | Per-repo | Free, but must still comply with the [Action Pinning Policy](#action-pinning-policy) and the [Required Workflows](#required-workflows) constraints. | @@ -33,11 +33,12 @@ file with that header, **stop and read the header first** — if the change isn't allowed by the contract, the right move is a PR against the central reusable, not a local edit. -> **Why pin to `@v1`?** Stubs reference reusables by tag, not `@main`, so a +> **Why pin to a canonical tag?** Stubs reference reusables by tag, not `@main`, so a > bad commit on the central repo's `main` branch cannot break every -> downstream repo simultaneously. The `v1` tag is bumped deliberately when -> a backward-compatible release is ready; breaking changes will publish a -> `v2` tag that downstream repos opt into explicitly. +> downstream repo simultaneously. Each reusable has its own canonical tag +> (most are `@v1`; `pr-review-mention` is `@v2`). A tag is bumped deliberately +> when a backward-compatible release is ready; breaking changes publish a new +> tag that downstream repos opt into explicitly. ### Available templates From fce58c3f3bc2db26ee7cc1f876fb29d9b03501ed Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 17 May 2026 11:31:01 -0500 Subject: [PATCH 4/7] chore(compliance): use safe regex escaping for reusable version tags --- scripts/compliance-audit.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 94b3b1ab8..f1daf5692 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -100,6 +100,11 @@ log_end() { echo "::endgroup::" >&2; } info() { echo "[INFO] $*" >&2; } warn() { echo "::warning::$*" >&2; } +# escape_ere escapes ERE metacharacters in a string for literal matching in grep -E +escape_ere() { + printf '%s' "$1" | sed 's/[][\.^$*+?(){}|\\/]/\\&/g' +} + # Retry wrapper for gh api calls (handles rate limits) gh_api() { local retries=3 @@ -761,7 +766,10 @@ check_centralized_workflow_stubs() { # petry-projects/.github/.github/workflows/.yml@ # Anchor to start-of-line + optional indent so a `# uses: ...` comment # cannot satisfy the check. - local expected="petry-projects/\\.github/\\.github/workflows/${reusable}\\.yml@${version}" + local esc_reusable esc_version + esc_reusable=$(escape_ere "$reusable") + esc_version=$(escape_ere "$version") + local expected="petry-projects/\\.github/\\.github/workflows/${esc_reusable}\\.yml@${esc_version}" if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*${expected}([[:space:]]|$)"; then continue # stub is correctly pinned to the canonical version — compliant @@ -769,7 +777,7 @@ check_centralized_workflow_stubs() { # Determine why it's non-compliant for a more actionable message. local why - if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*petry-projects/\\.github/\\.github/workflows/${reusable}\\.yml@"; then + if echo "$decoded" | grep -qE "^[[:space:]]*uses:[[:space:]]*petry-projects/\\.github/\\.github/workflows/${esc_reusable}\\.yml@"; then why="references the reusable but is not pinned to \`@${version}\` (org standard)" elif echo "$decoded" | grep -qF "petry-projects/.github/.github/workflows/${reusable}"; then why="references the reusable but the \`uses:\` line does not match the canonical stub" From 01eb68ca1d32f2414f4f9930173dd94fbeb6de7f Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 17 May 2026 11:41:05 -0500 Subject: [PATCH 5/7] chore(compliance): include braces in regex escaping --- scripts/compliance-audit.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index f1daf5692..d1ef08ee1 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -102,7 +102,7 @@ warn() { echo "::warning::$*" >&2; } # escape_ere escapes ERE metacharacters in a string for literal matching in grep -E escape_ere() { - printf '%s' "$1" | sed 's/[][\.^$*+?(){}|\\/]/\\&/g' + printf '%s' "$1" | sed 's/[][\.^$*+?(){}|\\/{}]/\\&/g' } # Retry wrapper for gh api calls (handles rate limits) From ca4c4f74d95b103226161ae2c2278f07fd2fb84f Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 17 May 2026 12:04:23 -0500 Subject: [PATCH 6/7] docs(ci): clarify internal reusable workflow tag exceptions for v2 --- standards/ci-standards.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/standards/ci-standards.md b/standards/ci-standards.md index 95fae60ca..2166beaa6 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -856,26 +856,26 @@ incorrect pinned one. ### Exception: Internal Reusable Workflow References Calls to `petry-projects/.github` reusable workflows use tag references -(`@v1`, `@main`) — **not SHA pins** — and are exempt from this policy. +(`@v1`, `@v2`, or `@main`) — **not SHA pins** — and are exempt from this policy. ```yaml # CORRECT — tag ref for internal reusable workflow -uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@v1 +uses: petry-projects/.github/.github/workflows/dev-lead-reusable.yml@v1 +uses: petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@v2 # WRONG — do not SHA-pin internal reusable workflow refs -uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@ee22b427cbce9ecadcf2b436acb57c3adf0cb63d +uses: petry-projects/.github/.github/workflows/dev-lead-reusable.yml@ee22b427cbce9ecadcf2b436acb57c3adf0cb63d ``` **Why:** Pinning the `uses:` line in a Tier 1 caller stub creates a diff from -the default branch. Anthropic's OIDC token endpoint validates that -`.github/workflows/claude.yml` on a PR branch is identical to the default -branch — any diff causes `401 Workflow validation failed` and Claude Code -cannot run on that PR. - -The `@v1` tag on `petry-projects/.github` is managed deliberately (bumped only -on backward-compatible releases) and is not subject to tag-force-push risk -because the org controls the tag. **Do not open compliance PRs to pin these -references.** +the default branch. Anthropic's OIDC token endpoint validates that the +workflow file on a PR branch is identical to the default branch — any diff +causes `401 Workflow validation failed` and the agent cannot run on that PR. + +The canonical tags (e.g. `@v1`, `@v2`) on `petry-projects/.github` are managed +deliberately (bumped only on backward-compatible releases) and are not subject +to tag-force-push risk because the org controls the tag. **Do not open +compliance PRs to pin these references.** --- From 6b627fe524d033e57a0fda7d5c471339bbb0064b Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Sun, 17 May 2026 12:07:59 -0500 Subject: [PATCH 7/7] chore(compliance): document regex escaping invariant --- scripts/compliance-audit.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index d1ef08ee1..57187ba8f 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -100,7 +100,9 @@ log_end() { echo "::endgroup::" >&2; } info() { echo "[INFO] $*" >&2; } warn() { echo "::warning::$*" >&2; } -# escape_ere escapes ERE metacharacters in a string for literal matching in grep -E +# escape_ere escapes ERE metacharacters in a string for literal matching in grep -E. +# This ensures that version tags (e.g. v2.1) and reusable basenames are treated +# as literal strings even if they contain regex metacharacters. escape_ere() { printf '%s' "$1" | sed 's/[][\.^$*+?(){}|\\/{}]/\\&/g' }