From b2072fef2f181385db570b7976558f2884dfb7d7 Mon Sep 17 00:00:00 2001 From: Shinrai Date: Thu, 20 Aug 2026 15:43:59 -0700 Subject: [PATCH 1/3] fix(ci): scope coverage PR-body injection to release PRs, derive badge filename per branch The coverage-pr-comment job in workflow-ci.yml gated only on github.event_name == 'pull_request', so every feature PR into next got whole-repo coverage injected into its description alongside the two persistent release PRs. Key on github.head_ref instead, which is only 'next'/'hotfixes' for those release PRs. Also derive badge_filename per branch (coverage.json for the default branch, coverage-.json otherwise) so a future widening of the coverage-badge trigger to next/hotfixes can't clobber master's badge JSON with theirs. A caller-supplied non-default filename still wins. Fixes #238 --- .github/workflows/workflow-ci.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/workflow-ci.yml b/.github/workflows/workflow-ci.yml index c7b55a3..487b473 100644 --- a/.github/workflows/workflow-ci.yml +++ b/.github/workflows/workflow-ci.yml @@ -419,7 +419,18 @@ jobs: skip_type_check: ${{ inputs.skip_type_check }} coverage_summary_path: ${{ inputs.coverage_summary_path }} badges_branch: ${{ inputs.badges_branch }} - badge_filename: ${{ inputs.badge_filename }} + # This job currently only fires on push to `inputs.default_branch` + # (see the `if:` above), so `github.ref_name` is always that + # branch today. Deriving per-branch here anyway means the + # filename is already correct if this job's trigger is ever + # widened to publish coverage for `next`/`hotfixes` too — a + # shared `coverage.json` would otherwise let whichever branch + # last published clobber the others' badge. A caller-supplied + # non-default `badge_filename` always wins (explicit override). + badge_filename: ${{ inputs.badge_filename != 'coverage.json' && + inputs.badge_filename || (github.ref_name == + inputs.default_branch && 'coverage.json' || + format('coverage-{0}.json', github.ref_name)) }} upload_coverage_artifact: ${{ inputs.upload_coverage_artifact }} debug: ${{ inputs.debug }} secrets: @@ -438,13 +449,22 @@ jobs: # `!cancelled()` guard: same cascade-skip reason as coverage-badge — # this job's chain runs through `ci` → `sync-gate`, and `sync-gate` # skips on this job's own trigger (pull_request), not just push. + # + # `github.head_ref` is scoped to the two persistent release PRs + # (`next` → master, `hotfixes` → master), not the base branch — every + # PR in the flow already targets one of {next, master, hotfixes} as + # its BASE (feature branches → next), so a base-branch filter can't + # tell a release PR apart from a feature PR. Keying on head_ref + # excludes feature/fix/etc PRs, which would otherwise get whole-repo + # coverage injected into their description on every run. if: > !cancelled() && needs.paths-gate.outputs.docs_only != 'true' && needs.commit-gate.outputs.should_skip != 'true' && needs.ci.result == 'success' && (inputs.enable_coverage_pr_comment == true || inputs.enable_coverage_pr_comment == 'true') && - github.event_name == 'pull_request' + github.event_name == 'pull_request' && (github.head_ref == 'next' + || github.head_ref == 'hotfixes') uses: CLDMV/.github/.github/workflows/reusable-coverage-pr-comment.yml@v4 with: runs_on: ${{ inputs.runs_on }} From 1879c8e790d1eaf798f0babdb2907362fd6104fa Mon Sep 17 00:00:00 2001 From: Shinrai Date: Thu, 20 Aug 2026 16:00:18 -0700 Subject: [PATCH 2/3] fix(ci): guard coverage PR-comment against fork head-ref spoof, allowlist badge filenames Addresses Copilot review on PR #239: - coverage-pr-comment gated only on head_ref == 'next'/'hotfixes', which a fork PR could spoof by naming its own head branch 'next'/'hotfixes' and running the job (pull-requests: write + bot secrets) against untrusted code. Add a same-repo check. - badge_filename derived via format('coverage-{0}.json', github.ref_name) against an arbitrary ref; GitHub Actions expressions have no string-replace function, so a branch name containing '/' would flow straight into the filename and break push-badge (no intermediate directory creation). Replace the open-ended format() with an explicit allowlist of the two known integration branch names (both guaranteed slash-free), falling back to the existing coverage.json otherwise. --- .github/workflows/workflow-ci.yml | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/.github/workflows/workflow-ci.yml b/.github/workflows/workflow-ci.yml index 487b473..8355a25 100644 --- a/.github/workflows/workflow-ci.yml +++ b/.github/workflows/workflow-ci.yml @@ -427,10 +427,22 @@ jobs: # shared `coverage.json` would otherwise let whichever branch # last published clobber the others' badge. A caller-supplied # non-default `badge_filename` always wins (explicit override). + # + # Deliberately an explicit allowlist of the two known integration + # branch names, NOT `format('coverage-{0}.json', github.ref_name)` + # against an arbitrary ref — GitHub Actions expressions have no + # string-replace function, so a slash in a branch name (e.g. a + # future `feature/*` trigger) would flow straight into the + # filename and break `push-badge`, which doesn't create + # intermediate directories. `next`/`hotfixes` are guaranteed + # slash-free by this repo's own branch-naming convention; any + # other ref (including today's only reachable case, + # `inputs.default_branch`) falls back to the existing single + # `coverage.json`. badge_filename: ${{ inputs.badge_filename != 'coverage.json' && - inputs.badge_filename || (github.ref_name == - inputs.default_branch && 'coverage.json' || - format('coverage-{0}.json', github.ref_name)) }} + inputs.badge_filename || (github.ref_name == 'next' && + 'coverage-next.json') || (github.ref_name == 'hotfixes' && + 'coverage-hotfixes.json') || 'coverage.json' }} upload_coverage_artifact: ${{ inputs.upload_coverage_artifact }} debug: ${{ inputs.debug }} secrets: @@ -457,6 +469,13 @@ jobs: # tell a release PR apart from a feature PR. Keying on head_ref # excludes feature/fix/etc PRs, which would otherwise get whole-repo # coverage injected into their description on every run. + # + # The same-repo check guards against a FORK PR whose head branch + # happens to be literally named `next`/`hotfixes` — head_ref alone + # can't tell that apart from the real release PR, and matching it + # would run this job (with pull-requests: write + bot secrets) against + # untrusted fork code for no benefit, since only same-repo pushes can + # ever produce the real release PRs. if: > !cancelled() && needs.paths-gate.outputs.docs_only != 'true' && @@ -464,7 +483,9 @@ jobs: == 'success' && (inputs.enable_coverage_pr_comment == true || inputs.enable_coverage_pr_comment == 'true') && github.event_name == 'pull_request' && (github.head_ref == 'next' - || github.head_ref == 'hotfixes') + || github.head_ref == 'hotfixes') && + github.event.pull_request.head.repo.full_name == + github.repository uses: CLDMV/.github/.github/workflows/reusable-coverage-pr-comment.yml@v4 with: runs_on: ${{ inputs.runs_on }} From b380cac9f882e6603cf643e323777c6d96b60307 Mon Sep 17 00:00:00 2001 From: "cldmv-bot[bot]" <230771808+cldmv-bot[bot]@users.noreply.github.com> Date: Thu, 20 Aug 2026 23:10:37 +0000 Subject: [PATCH 3/3] chore: bump version to 4.22.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7a66dbd..a71f070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cldmv/.github", - "version": "4.21.1", + "version": "4.22.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cldmv/.github", - "version": "4.21.1", + "version": "4.22.1", "license": "Apache-2.0", "devDependencies": { "@cldmv/eslint-plugin-jsonv": "^1.0.3", diff --git a/package.json b/package.json index 5ade995..aa876e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cldmv/.github", - "version": "4.22.0", + "version": "4.22.1", "description": "Shared GitHub Actions, reusable workflows, and org-wide tooling for the CLDMV organization.", "author": { "name": "Shinrai",