From 0d04734ec13c194c41a234cac31d4a64d9388330 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:30:17 +0000 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20implement=20issue=20#549=20?= =?UTF-8?q?=E2=80=94=20feat(ci-standards):=20adopt=20inline-NOSONAR=20S763?= =?UTF-8?q?7=20exemption=20in=20caller-stub=20templates=20(org-wide;=20sup?= =?UTF-8?q?ersedes=20per-file=20approach)=20=E2=80=94=20spike-validated?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/compliance-audit.sh | 99 ++++++++++++++++- standards/ci-standards.md | 104 +++++++++++------- standards/workflows/add-to-project.yml | 2 +- standards/workflows/agent-shield.yml | 2 +- standards/workflows/auto-rebase.yml | 2 +- standards/workflows/dependabot-automerge.yml | 2 +- standards/workflows/dependabot-rebase.yml | 2 +- standards/workflows/dependency-audit.yml | 2 +- standards/workflows/dev-lead.yml | 13 ++- standards/workflows/idea-enhancer.yml | 2 +- standards/workflows/idea-triage.yml | 2 +- standards/workflows/initiative-planner.yml | 2 +- standards/workflows/pr-auto-review.yml | 2 +- standards/workflows/pr-review-mention.yml | 2 +- .../sonar-s7637-exemption.bats | 73 ++++++++++++ 15 files changed, 250 insertions(+), 61 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index a35b1a722..9cbaca2c7 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -829,16 +829,107 @@ classify_sonar_s7637_exemption() { fi } +# --------------------------------------------------------------------------- +# Inline NOSONAR S7637 marker on first-party caller stubs (#549) +# +# Canonical mechanism (supersedes the per-file sonar-project.properties approach): +# the inline `# NOSONAR(githubactions:S7637)` marker on the channel-pinned +# first-party reusable-ref `uses:` line travels WITH the caller-stub file, so +# adopters inherit the exemption on copy with zero per-repo properties entries. +# +# classify_inline_s7637_marker echoes exactly one verdict for a single workflow +# file's decoded content: +# present — every channel-pinned first-party reusable-ref `uses:` line carries +# the inline `NOSONAR(githubactions:S7637)` marker. +# missing — at least one channel-pinned first-party reusable-ref line lacks the +# marker; SonarCloud's githubactions:S7637 would fire on it. +# n/a — the file has no channel-pinned first-party reusable ref (it is +# SHA-pinned — which already satisfies S7637 — or carries no +# first-party reusable ref at all), so no inline marker is required. +# --------------------------------------------------------------------------- +classify_inline_s7637_marker() { + local content="$1" + + # First-party reusable-workflow `uses:` lines (channel- or SHA-pinned). + local uses_lines + uses_lines=$(printf '%s\n' "$content" \ + | grep -E '^[[:space:]]*uses:[[:space:]]*petry-projects/\.github(-private)?/\.github/workflows/[^@[:space:]]+@' || true) + + if [ -z "$uses_lines" ]; then + echo "n/a" + return + fi + + local line ref channel_seen=0 missing=0 + while IFS= read -r line; do + [ -z "$line" ] && continue + # The pinned ref is the token after the first '@', up to whitespace/comment. + ref=$(printf '%s\n' "$line" | sed -E 's/^[^@]*@([^[:space:]#]+).*/\1/') + # A 40-char hex SHA pin already satisfies S7637 — no inline marker needed. + if printf '%s' "$ref" | grep -qE '^[0-9a-f]{40}$'; then + continue + fi + channel_seen=1 + if ! printf '%s' "$line" | grep -qF 'NOSONAR(githubactions:S7637)'; then + missing=1 + fi + done <<< "$uses_lines" + + if [ "$channel_seen" -eq 0 ]; then + echo "n/a" + elif [ "$missing" -eq 1 ]; then + echo "missing" + else + echo "present" + fi +} + check_sonar_s7637_exemption() { local repo="$1" # Only relevant when the repo actually runs SonarCloud analysis. gh_api "repos/$ORG/$repo/contents/.github/workflows/sonarcloud.yml" --jq '.name' > /dev/null 2>&1 || return + # Canonical (#549): scan the repo's caller-stub workflows for the inline + # `# NOSONAR(githubactions:S7637)` marker. If every channel-pinned first-party + # reusable-ref stub carries it, the repo is exempt with no per-file + # sonar-project.properties entries needed. + local workflows wf wf_content wf_decoded inline_verdict + local inline_seen=0 inline_missing=0 + workflows=$(gh_api "repos/$ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null || echo "") + for wf in $workflows; do + case "$wf" in *.yml | *.yaml) ;; *) continue ;; esac + wf_content=$(gh_api "repos/$ORG/$repo/contents/.github/workflows/$wf" --jq '.content' 2>/dev/null || echo "") + [ -z "$wf_content" ] && continue + wf_decoded=$(echo "$wf_content" | base64 -d 2>/dev/null || echo "") + [ -z "$wf_decoded" ] && continue + inline_verdict=$(classify_inline_s7637_marker "$wf_decoded") + case "$inline_verdict" in + present) inline_seen=1 ;; + missing) inline_seen=1; inline_missing=1 ;; + esac + done + + # Every channel-pinned first-party stub carries the inline marker → exempt. + if [ "$inline_seen" -eq 1 ] && [ "$inline_missing" -eq 0 ]; then + return + fi + + # Legacy per-file mechanism — accepted during the transition (#549). A repo + # whose stubs are not yet inline-marked is still compliant if its + # sonar-project.properties carries the narrow per-stub exemption. local content decoded content=$(gh_api "repos/$ORG/$repo/contents/sonar-project.properties" --jq '.content' 2>/dev/null || echo "") - # A missing properties file is reported separately by check_sonarcloud. - [ -z "$content" ] && return + if [ -z "$content" ]; then + # No properties file. Flag only when a stub needs but lacks the inline marker + # (a missing properties file itself is reported by check_sonarcloud). + if [ "$inline_missing" -eq 1 ]; then + add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \ + "First-party caller stub(s) carry a channel-pinned reusable ref (\`@/stable\`, \`@v1\`/\`@v2\`) without the inline \`# NOSONAR(githubactions:S7637)\` marker, and there is no legacy \`sonar-project.properties\` exemption. SonarCloud will flag them as unpinned actions even though the org exempts them from SHA-pinning. Add the inline marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` entry." \ + "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" + fi + return + fi decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") [ -z "$decoded" ] && return @@ -848,12 +939,12 @@ check_sonar_s7637_exemption() { case "$verdict" in missing) add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \ - "\`sonar-project.properties\` has no \`sonar.issue.ignore\` for \`githubactions:S7637\`. SonarCloud will flag first-party reusable-ref caller stubs (\`@/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the canonical per-stub exemption." \ + "No \`githubactions:S7637\` exemption found. SonarCloud will flag first-party reusable-ref caller stubs (\`@/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the inline \`# NOSONAR(githubactions:S7637)\` marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` exemption in \`sonar-project.properties\`." \ "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" ;; too-broad) add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-too-broad" "error" \ - "\`sonar-project.properties\` suppresses \`githubactions:S7637\` with a blanket workflow-path \`resourceKey\` (e.g. \`workflows/*.yml\` or \`**\`). This also exempts third-party actions in \`ci.yml\`/\`sonarcloud.yml\` from SHA-pin enforcement. Scope the exemption to the individual caller-stub files instead." \ + "\`sonar-project.properties\` suppresses \`githubactions:S7637\` with a blanket workflow-path \`resourceKey\` (e.g. \`workflows/*.yml\` or \`**\`). This also exempts third-party actions in \`ci.yml\`/\`sonarcloud.yml\` from SHA-pin enforcement. Scope the exemption to the individual caller-stub files instead, or migrate to the inline \`# NOSONAR(githubactions:S7637)\` marker." \ "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" ;; esac diff --git a/standards/ci-standards.md b/standards/ci-standards.md index e12f38b9e..1ba279b9e 100644 --- a/standards/ci-standards.md +++ b/standards/ci-standards.md @@ -431,37 +431,63 @@ The org compliance audit already exempts them via `check_action_pinning`, but SonarCloud is an **external** gate that doesn't know about that exemption, so it flags them as high-severity findings and can fail the quality gate. -**Standard:** every SonarCloud-gated repo's `sonar-project.properties` MUST -suppress `githubactions:S7637` **only on the individual first-party caller-stub -workflow files** — each stub gets its own `sonar.issue.ignore.multicriteria` -criterion keyed to that file path. A blanket `resourceKey` such as -`**/.github/workflows/*.yml`, `*.yml`, or `**` is **forbidden**: it would also -drop S7637 on `ci.yml` / `sonarcloud.yml`, leaving third-party actions -un-enforced. Third-party actions must always stay SHA-pinned. - -> SonarCloud's `sonar.issue.ignore` matches by file path + rule, not by `uses:` -> ref content — so the exemption is scoped per caller-stub file rather than by -> the ref string itself. - -Caller stubs that carry only a first-party reusable ref (and therefore need the -exemption): `agent-shield.yml`, `pr-review-mention.yml`, `pr-auto-review.yml`, -`auto-rebase.yml`, `dependabot-rebase.yml`, `dependabot-automerge.yml`, -`dependency-audit.yml`, `feature-ideation.yml`, `add-to-project.yml`, -`dev-lead.yml`. Copy the canonical block verbatim from this repo's -[`sonar-project.properties`](https://github.com/petry-projects/.github/blob/main/sonar-project.properties); -omit criteria for stubs a given repo does not have. +**Standard (canonical — inline NOSONAR in the stub):** the first-party +channel-pinned `uses:` line in every caller stub MUST carry an inline +`# NOSONAR(githubactions:S7637)` marker, e.g.: + +```yaml +jobs: + dev-lead: + # First-party channel tag — do NOT SHA-pin (AGENTS.md mutable-ref exception). + uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable # NOSONAR(githubactions:S7637) first-party channel ref +``` + +SonarCloud honors the inline comment and suppresses S7637 on exactly that line, +so the exemption travels **with the stub file** — adopters who copy the template +verbatim inherit it with **zero** per-repo `sonar-project.properties` entries and +**no file rename**. Spike-validated on `bmad-bgreat-suite#334` (`3cbfa005`): +Quality Gate ERROR → OK, `new_security_rating` C → A, required `SonarCloud` +check fail → pass. + +The marker goes **only on first-party reusable-ref lines** pinned to a moving +channel/tag (`@/stable`, `@v1`/`@v2`). A SHA-pinned ref (e.g. +`feature-ideation.yml`'s `@ # v1`) already satisfies S7637 and gets **no** +marker. Third-party actions stay SHA-pinned and S7637-enforced; never add a +blanket glob or a file-wide disable. + +Caller-stub templates carrying a channel-pinned first-party reusable ref (and +therefore the inline marker): `dev-lead.yml`, `agent-shield.yml`, +`pr-review-mention.yml`, `pr-auto-review.yml`, `auto-rebase.yml`, +`dependabot-rebase.yml`, `dependabot-automerge.yml`, `dependency-audit.yml`, +`add-to-project.yml`, `idea-triage.yml`, `idea-enhancer.yml`, +`initiative-planner.yml`. Copy each template verbatim from +[`standards/workflows/`](https://github.com/petry-projects/.github/tree/main/standards/workflows) +and the marker comes with it. + +**Legacy (per-file `sonar-project.properties` — optional, transitional):** the +older mechanism suppressed S7637 **only on the individual caller-stub workflow +files** via one `sonar.issue.ignore.multicriteria` criterion per file path. It +still works and is **accepted during the transition**, but is no longer required +once the stubs carry the inline marker — the inline marker is the canonical +mechanism and auto-covers any newly added channel-pinned stub without a per-repo +edit. A blanket `resourceKey` such as `**/.github/workflows/*.yml`, `*.yml`, or +`**` remains **forbidden**: it would also drop S7637 on `ci.yml` / +`sonarcloud.yml`, leaving third-party actions un-enforced. ```properties +# Legacy / optional — only needed for stubs not yet carrying the inline marker. sonar.issue.ignore.multicriteria=s7637_agentshield,… sonar.issue.ignore.multicriteria.s7637_agentshield.ruleKey=githubactions:S7637 sonar.issue.ignore.multicriteria.s7637_agentshield.resourceKey=**/agent-shield.yml # …one ruleKey/resourceKey pair per caller-stub file ``` -The compliance audit's `check_sonar_s7637_exemption` enforces this org-wide: it -files `sonar-s7637-exemption-missing` (warning) when a SonarCloud-gated repo's -properties carry no S7637 exemption, and `sonar-s7637-exemption-too-broad` -(error) when the exemption uses a blanket workflow-path `resourceKey`. +The compliance audit's `check_sonar_s7637_exemption` enforces this org-wide: a +repo is compliant when every channel-pinned first-party caller stub carries the +inline marker **or** (during transition) the legacy per-file properties +exemption is present. It files `sonar-s7637-exemption-missing` (warning) when a +SonarCloud-gated repo has neither, and `sonar-s7637-exemption-too-broad` (error) +when the legacy exemption uses a blanket workflow-path `resourceKey`. ### 4. Secret Scanning (`ci.yml` — gitleaks job) @@ -1610,26 +1636,24 @@ The cron's three `repository_dispatch` retry types — `dev-lead-ci-failure`, 3. Set `GH_PAT_WORKFLOWS` — a PAT with read access to `petry-projects/.github-private` — as an org or repo secret (required for cross-repo script access). 4. Optionally set `vars.DEV_LEAD_ENGINE` to `claude` (default), `gemini`, or `copilot`. 5. Optionally set `vars.DEV_LEAD_DRY_RUN=true` during the initial rollout period. -6. **If the repo is SonarCloud-gated, add the `s7637_devlead` exemption to - `sonar-project.properties`** (required). `dev-lead.yml` is a first-party +6. **If the repo is SonarCloud-gated**, the S7637 exemption is already baked + into the stub: `dev-lead.yml`'s `uses:` line carries the inline + `# NOSONAR(githubactions:S7637)` marker, so copying the template verbatim + (step 1) is all that is required. `dev-lead.yml` is a first-party reusable-ref caller stub pinned to a moving channel tag — `@dev-lead/stable`, or a canary-ring tag (`@dev-lead/ring0` | `@dev-lead/ring1` | `@dev-lead/next`) under the [canary-rings rollout](#reusable-workflow-versioning--the-stable-channel). - That mutable ref is intentionally **not** SHA-pinned, so SonarCloud's - `githubactions:S7637` flags it at HIGH severity, drives `new_security_rating` - to **C**, and fails the Quality Gate — blocking the required `SonarCloud` - check. Add the per-file criterion (keyed to `**/dev-lead.yml`, **never** a - blanket `resourceKey`) exactly as for every other caller stub: - - ```properties - sonar.issue.ignore.multicriteria.s7637_devlead.ruleKey=githubactions:S7637 - sonar.issue.ignore.multicriteria.s7637_devlead.resourceKey=**/dev-lead.yml - ``` - - and add `s7637_devlead` to the `sonar.issue.ignore.multicriteria` list. See - [SonarCloud Exemption: First-Party Reusable-Ref S7637](#sonarcloud-exemption-first-party-reusable-ref-s7637) - for the canonical block; `check_sonar_s7637_exemption` files - `sonar-s7637-exemption-missing` against repos that omit it. + That mutable ref is intentionally **not** SHA-pinned, so without the marker + SonarCloud's `githubactions:S7637` would flag it at HIGH severity, drive + `new_security_rating` to **C**, and fail the Quality Gate — blocking the + required `SonarCloud` check. The inline marker suppresses S7637 on exactly + that line and travels with the file. **No `sonar-project.properties` entry is + needed.** (The legacy per-file `s7637_devlead` criterion keyed to + `**/dev-lead.yml` still works and is accepted during the transition, but is + optional once the stub carries the marker.) See + [SonarCloud Exemption: First-Party Reusable-Ref S7637](#sonarcloud-exemption-first-party-reusable-ref-s7637); + `check_sonar_s7637_exemption` files `sonar-s7637-exemption-missing` only when + a repo has neither the inline marker nor the legacy entry. ### Required secrets diff --git a/standards/workflows/add-to-project.yml b/standards/workflows/add-to-project.yml index 393e18cb5..877bb17c2 100644 --- a/standards/workflows/add-to-project.yml +++ b/standards/workflows/add-to-project.yml @@ -51,7 +51,7 @@ jobs: add-to-project: permissions: contents: read - uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/stable + uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/stable # NOSONAR(githubactions:S7637) first-party channel ref with: project_id: PVT_kwDOD2inqs4BZq3- project_url: https://github.com/orgs/petry-projects/projects/1 diff --git a/standards/workflows/agent-shield.yml b/standards/workflows/agent-shield.yml index 4cf7bb7dd..fb8840f5c 100644 --- a/standards/workflows/agent-shield.yml +++ b/standards/workflows/agent-shield.yml @@ -30,4 +30,4 @@ permissions: jobs: agent-shield: - uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@agent-shield/stable + uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@agent-shield/stable # NOSONAR(githubactions:S7637) first-party channel ref diff --git a/standards/workflows/auto-rebase.yml b/standards/workflows/auto-rebase.yml index 6e74015b5..8a40c33b8 100644 --- a/standards/workflows/auto-rebase.yml +++ b/standards/workflows/auto-rebase.yml @@ -50,5 +50,5 @@ jobs: permissions: contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/) pull-requests: write # post comments on PRs - uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/stable + uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/standards/workflows/dependabot-automerge.yml b/standards/workflows/dependabot-automerge.yml index c1c5ef2fa..a8e31f8d7 100644 --- a/standards/workflows/dependabot-automerge.yml +++ b/standards/workflows/dependabot-automerge.yml @@ -35,5 +35,5 @@ jobs: permissions: contents: read pull-requests: read - uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/stable + uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/standards/workflows/dependabot-rebase.yml b/standards/workflows/dependabot-rebase.yml index e918c3ba5..cd8e9e822 100644 --- a/standards/workflows/dependabot-rebase.yml +++ b/standards/workflows/dependabot-rebase.yml @@ -50,7 +50,7 @@ jobs: permissions: contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/) pull-requests: write # re-approve PRs after branch update - uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@dependabot-rebase/stable + uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@dependabot-rebase/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: APP_ID: ${{ secrets.APP_ID }} APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/standards/workflows/dependency-audit.yml b/standards/workflows/dependency-audit.yml index c1af742eb..bfad3f422 100644 --- a/standards/workflows/dependency-audit.yml +++ b/standards/workflows/dependency-audit.yml @@ -30,4 +30,4 @@ permissions: jobs: dependency-audit: - uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@dependency-audit/stable + uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@dependency-audit/stable # NOSONAR(githubactions:S7637) first-party channel ref diff --git a/standards/workflows/dev-lead.yml b/standards/workflows/dev-lead.yml index b36c3af5b..cce253d36 100644 --- a/standards/workflows/dev-lead.yml +++ b/standards/workflows/dev-lead.yml @@ -8,11 +8,12 @@ # 2. Ensure CLAUDE_CODE_OAUTH_TOKEN is set as an org or repo secret. # 3. Optionally set GH_PAT_WORKFLOWS (required if Claude pushes workflow files). # 4. Optionally set vars.DEV_LEAD_ENGINE = "claude" | "gemini" | "copilot". -# 5. SonarCloud-gated repo? Add the s7637_devlead exemption to -# sonar-project.properties (resourceKey **/dev-lead.yml, NEVER a blanket -# glob). The `uses:` ref below is a first-party reusable pinned to a moving -# channel/ring tag, so SonarCloud's githubactions:S7637 would otherwise -# fail the Quality Gate. See ../ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637. +# 5. SonarCloud-gated repo? Nothing to do — the `uses:` line below already +# carries the inline `# NOSONAR(githubactions:S7637)` marker. The ref is a +# first-party reusable pinned to a moving channel/ring tag, so without the +# marker SonarCloud's githubactions:S7637 would fail the Quality Gate. The +# marker travels with this file on copy; no sonar-project.properties entry +# is needed. See ../ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637. # # UNLIKE claude.yml, this file has NO OIDC byte-for-byte constraint and may be # freely modified on PR branches to adjust triggers for repo-specific needs. @@ -53,7 +54,7 @@ jobs: # dependency). Promotion is done by moving the dev-lead/stable tag centrally; this # caller is never edited on release. agent_ref threads the same channel into # dev-lead's own scripts/prompts checkout. See ../ci-standards.md#dev-lead-agent. - uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable + uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable # NOSONAR(githubactions:S7637) first-party channel ref with: agent_ref: dev-lead/stable secrets: inherit diff --git a/standards/workflows/idea-enhancer.yml b/standards/workflows/idea-enhancer.yml index a7cb51b7d..80af11956 100644 --- a/standards/workflows/idea-enhancer.yml +++ b/standards/workflows/idea-enhancer.yml @@ -45,5 +45,5 @@ permissions: {} jobs: idea-enhancer: - uses: petry-projects/.github/.github/workflows/idea-enhancer-reusable.yml@idea-enhancer/stable + uses: petry-projects/.github/.github/workflows/idea-enhancer-reusable.yml@idea-enhancer/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/standards/workflows/idea-triage.yml b/standards/workflows/idea-triage.yml index bdfac82b5..43e121f82 100644 --- a/standards/workflows/idea-triage.yml +++ b/standards/workflows/idea-triage.yml @@ -42,5 +42,5 @@ permissions: {} jobs: idea-triage: - uses: petry-projects/.github/.github/workflows/idea-triage-reusable.yml@idea-triage/stable + uses: petry-projects/.github/.github/workflows/idea-triage-reusable.yml@idea-triage/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/standards/workflows/initiative-planner.yml b/standards/workflows/initiative-planner.yml index d8f8f3740..d8d8f488a 100644 --- a/standards/workflows/initiative-planner.yml +++ b/standards/workflows/initiative-planner.yml @@ -44,5 +44,5 @@ permissions: {} jobs: initiative-planner: - uses: petry-projects/.github/.github/workflows/initiative-planner-reusable.yml@initiative-planner/stable + uses: petry-projects/.github/.github/workflows/initiative-planner-reusable.yml@initiative-planner/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/standards/workflows/pr-auto-review.yml b/standards/workflows/pr-auto-review.yml index 7409476ed..be91ffa77 100644 --- a/standards/workflows/pr-auto-review.yml +++ b/standards/workflows/pr-auto-review.yml @@ -50,6 +50,6 @@ jobs: pull-requests: read checks: read actions: read - uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 + uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2 # NOSONAR(githubactions:S7637) first-party channel ref secrets: GH_PAT_WORKFLOWS: ${{ secrets.GH_PAT_WORKFLOWS }} diff --git a/standards/workflows/pr-review-mention.yml b/standards/workflows/pr-review-mention.yml index d9a27b285..1898aeb5e 100644 --- a/standards/workflows/pr-review-mention.yml +++ b/standards/workflows/pr-review-mention.yml @@ -37,5 +37,5 @@ jobs: pr-review-mention: permissions: pull-requests: write - uses: petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@pr-review-mention/stable + uses: petry-projects/.github/.github/workflows/pr-review-mention-reusable.yml@pr-review-mention/stable # NOSONAR(githubactions:S7637) first-party channel ref secrets: inherit diff --git a/test/scripts/compliance-audit/sonar-s7637-exemption.bats b/test/scripts/compliance-audit/sonar-s7637-exemption.bats index 9682baa52..d01ebab45 100644 --- a/test/scripts/compliance-audit/sonar-s7637-exemption.bats +++ b/test/scripts/compliance-audit/sonar-s7637-exemption.bats @@ -105,3 +105,76 @@ PROPS [ "$status" -eq 0 ] [ "$output" = "present" ] } + +# --------------------------------------------------------------------------- +# classify_inline_s7637_marker() — the canonical mechanism (#549). Verifies the +# inline `# NOSONAR(githubactions:S7637)` marker travels on each channel-pinned +# first-party reusable-ref `uses:` line in a caller-stub workflow file. +# present — every channel-pinned first-party reusable-ref line carries the marker +# missing — at least one channel-pinned first-party reusable-ref line lacks it +# n/a — no channel-pinned first-party reusable ref (SHA-pinned or none) +# --------------------------------------------------------------------------- + +# Run the real inline classifier against the given workflow-file content. +classify_inline() { + run bash -c 'source "$1" >/dev/null 2>&1; classify_inline_s7637_marker "$2"' \ + _ "$SCRIPT" "$1" +} + +@test "inline: channel-pinned first-party ref with the marker is present" { + classify_inline "$(printf 'jobs:\n dev-lead:\n uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable # NOSONAR(githubactions:S7637) first-party channel ref\n')" + [ "$status" -eq 0 ] + [ "$output" = "present" ] +} + +@test "inline: channel-pinned first-party ref without the marker is missing" { + classify_inline "$(printf 'jobs:\n dev-lead:\n uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable\n')" + [ "$status" -eq 0 ] + [ "$output" = "missing" ] +} + +@test "inline: a @v2 moving-major channel ref needs the marker too" { + classify_inline "$(printf 'jobs:\n pr-auto-review:\n uses: petry-projects/.github/.github/workflows/pr-auto-review-reusable.yml@v2\n')" + [ "$status" -eq 0 ] + [ "$output" = "missing" ] +} + +@test "inline: a SHA-pinned first-party ref needs no marker (n/a)" { + classify_inline "$(printf 'jobs:\n feature-ideation:\n uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@e20a8fac1b6a10bd6ad0999258e88a423f890ba6 # v1\n')" + [ "$status" -eq 0 ] + [ "$output" = "n/a" ] +} + +@test "inline: a workflow with no first-party reusable ref is n/a" { + classify_inline "$(printf 'jobs:\n ci:\n steps:\n - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683\n')" + [ "$status" -eq 0 ] + [ "$output" = "n/a" ] +} + +@test "inline: mixed — one marked channel ref + one SHA-pinned ref is present" { + body=$(cat <<'WF' +jobs: + a: + uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@agent-shield/stable # NOSONAR(githubactions:S7637) first-party channel ref + b: + uses: petry-projects/.github/.github/workflows/feature-ideation-reusable.yml@e20a8fac1b6a10bd6ad0999258e88a423f890ba6 # v1 +WF +) + classify_inline "$body" + [ "$status" -eq 0 ] + [ "$output" = "present" ] +} + +@test "inline: every shipped channel-pinned caller-stub template is present" { + root="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" + for tmpl in dev-lead agent-shield pr-review-mention pr-auto-review auto-rebase \ + dependabot-rebase dependabot-automerge dependency-audit \ + add-to-project idea-triage idea-enhancer initiative-planner; do + classify_inline "$(cat "$root/standards/workflows/$tmpl.yml")" + [ "$status" -eq 0 ] + [ "$output" = "present" ] || { + echo "template $tmpl.yml classified as '$output', expected present" >&2 + return 1 + } + done +} From d743cbc5282bf113e45046d5b20e8ff8386631df Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:35:03 +0000 Subject: [PATCH 2/5] chore: apply manual instructions [skip ci-relay] --- scripts/deploy-standard-workflows.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy-standard-workflows.sh b/scripts/deploy-standard-workflows.sh index 93914efc7..130287845 100755 --- a/scripts/deploy-standard-workflows.sh +++ b/scripts/deploy-standard-workflows.sh @@ -164,7 +164,7 @@ fetch_existing() { is_already_compliant() { local existing_content="$1" template="$2" repo="$3" local expected_uses - expected_uses=$(grep -E '^[[:space:]]*uses:' "$template" | head -1 | sed 's/^[[:space:]]*uses:[[:space:]]*//' | tr -d '\r') + expected_uses=$(grep -E '^[[:space:]]*uses:' "$template" | head -1 | sed 's/^[[:space:]]*uses:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '\r') [[ -z "$expected_uses" ]] && return 1 local prefix="${expected_uses%@*}" ref_after="${expected_uses##*@}" base From cee36747dc18e41a01cc92a65b53ee0b7a6884a1 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:39:37 +0000 Subject: [PATCH 3/5] chore: apply manual instructions [skip ci-relay] --- scripts/compliance-audit.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 9cbaca2c7..f0c16e084 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -857,7 +857,7 @@ classify_inline_s7637_marker() { if [ -z "$uses_lines" ]; then echo "n/a" - return + return 0 fi local line ref channel_seen=0 missing=0 @@ -912,7 +912,7 @@ check_sonar_s7637_exemption() { # Every channel-pinned first-party stub carries the inline marker → exempt. if [ "$inline_seen" -eq 1 ] && [ "$inline_missing" -eq 0 ]; then - return + return 0 fi # Legacy per-file mechanism — accepted during the transition (#549). A repo @@ -928,7 +928,7 @@ check_sonar_s7637_exemption() { "First-party caller stub(s) carry a channel-pinned reusable ref (\`@/stable\`, \`@v1\`/\`@v2\`) without the inline \`# NOSONAR(githubactions:S7637)\` marker, and there is no legacy \`sonar-project.properties\` exemption. SonarCloud will flag them as unpinned actions even though the org exempts them from SHA-pinning. Add the inline marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` entry." \ "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" fi - return + return 0 fi decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") [ -z "$decoded" ] && return @@ -938,9 +938,11 @@ check_sonar_s7637_exemption() { case "$verdict" in missing) - add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \ - "No \`githubactions:S7637\` exemption found. SonarCloud will flag first-party reusable-ref caller stubs (\`@/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the inline \`# NOSONAR(githubactions:S7637)\` marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` exemption in \`sonar-project.properties\`." \ - "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" + if [ "$inline_seen" -eq 1 ]; then + add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \ + "No \`githubactions:S7637\` exemption found. SonarCloud will flag first-party reusable-ref caller stubs (\`@/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the inline \`# NOSONAR(githubactions:S7637)\` marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` exemption in \`sonar-project.properties\`." \ + "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" + fi ;; too-broad) add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-too-broad" "error" \ From c5b28cd7429c41b5021babeecc4ba36086f8bf41 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:42:54 +0000 Subject: [PATCH 4/5] fix(bot): address bot feedback [skip ci-relay] --- scripts/compliance-audit.sh | 4 +++- .../compliance-audit/sonar-s7637-exemption.bats | 11 +++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index f0c16e084..1766947a3 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -938,7 +938,9 @@ check_sonar_s7637_exemption() { case "$verdict" in missing) - if [ "$inline_seen" -eq 1 ]; then + # Only a real gap when a channel-pinned first-party stub actually lacks the + # inline marker; with no such stub there is nothing for S7637 to fire on. + if [ "$inline_missing" -eq 1 ]; then add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \ "No \`githubactions:S7637\` exemption found. SonarCloud will flag first-party reusable-ref caller stubs (\`@/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the inline \`# NOSONAR(githubactions:S7637)\` marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` exemption in \`sonar-project.properties\`." \ "standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637" diff --git a/test/scripts/compliance-audit/sonar-s7637-exemption.bats b/test/scripts/compliance-audit/sonar-s7637-exemption.bats index d01ebab45..e6f75d3fd 100644 --- a/test/scripts/compliance-audit/sonar-s7637-exemption.bats +++ b/test/scripts/compliance-audit/sonar-s7637-exemption.bats @@ -167,13 +167,12 @@ WF @test "inline: every shipped channel-pinned caller-stub template is present" { root="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)" - for tmpl in dev-lead agent-shield pr-review-mention pr-auto-review auto-rebase \ - dependabot-rebase dependabot-automerge dependency-audit \ - add-to-project idea-triage idea-enhancer initiative-planner; do - classify_inline "$(cat "$root/standards/workflows/$tmpl.yml")" + for tmpl in "$root"/standards/workflows/*.yml; do + tmpl_name=$(basename "$tmpl") + classify_inline "$(cat "$tmpl")" [ "$status" -eq 0 ] - [ "$output" = "present" ] || { - echo "template $tmpl.yml classified as '$output', expected present" >&2 + [ "$output" != "missing" ] || { + echo "template $tmpl_name classified as missing, but should be present or n/a" >&2 return 1 } done From a68bc087ed3ffa12992ecb53a002cda9107262b7 Mon Sep 17 00:00:00 2001 From: donpetry-bot <281750570+donpetry-bot@users.noreply.github.com> Date: Sat, 27 Jun 2026 17:45:35 +0000 Subject: [PATCH 5/5] fix(compliance-audit): use explicit return 0 for robust exit code handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all bare `return` statements in compliance-audit.sh with explicit `return 0` to ensure consistent and clear exit code handling across functions. This is particularly important for check_sonar_s7637_exemption's early exit when sonarcloud.yml is not present — now it correctly returns success (0) instead of propagating the gh_api failure code. Co-Authored-By: Claude Haiku 4.5 --- scripts/compliance-audit.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scripts/compliance-audit.sh b/scripts/compliance-audit.sh index 1766947a3..4a18ca3ad 100755 --- a/scripts/compliance-audit.sh +++ b/scripts/compliance-audit.sh @@ -283,7 +283,7 @@ check_dependabot_config() { add_finding "$repo" "dependabot" "missing-config" "error" \ "Missing \`.github/dependabot.yml\` configuration file" \ "standards/dependabot-policy.md" - return + return 0 fi local decoded @@ -688,7 +688,7 @@ check_codeowners() { add_finding "$repo" "settings" "missing-codeowners" "error" \ "No \`CODEOWNERS\` file found — required for code owner review enforcement (pr-quality ruleset)" \ "standards/codeowners-standard.md" - return + return 0 fi # Extract non-comment, non-blank owner lines for accurate matching. @@ -704,7 +704,7 @@ check_codeowners() { add_finding "$repo" "settings" "codeowners-empty" "error" \ "CODEOWNERS file has no owner lines (only comments/blank)" \ "standards/codeowners-standard.md" - return + return 0 fi # Rule 1: @petry-projects/org-leads MUST be the first owner on every line @@ -803,7 +803,7 @@ classify_sonar_s7637_exemption() { if [ -z "$keys" ]; then echo "missing" - return + return 0 fi local key resourcekey base broad=0 @@ -888,7 +888,7 @@ check_sonar_s7637_exemption() { local repo="$1" # Only relevant when the repo actually runs SonarCloud analysis. - gh_api "repos/$ORG/$repo/contents/.github/workflows/sonarcloud.yml" --jq '.name' > /dev/null 2>&1 || return + gh_api "repos/$ORG/$repo/contents/.github/workflows/sonarcloud.yml" --jq '.name' > /dev/null 2>&1 || return 0 # Canonical (#549): scan the repo's caller-stub workflows for the inline # `# NOSONAR(githubactions:S7637)` marker. If every channel-pinned first-party @@ -931,7 +931,7 @@ check_sonar_s7637_exemption() { return 0 fi decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "") - [ -z "$decoded" ] && return + [ -z "$decoded" ] && return 0 local verdict verdict=$(classify_sonar_s7637_exemption "$decoded")