From 7c32d20648a46d1a778c4c2411fe705dc8dc33e7 Mon Sep 17 00:00:00 2001 From: seonghobae Date: Sat, 5 Sep 2026 14:40:24 +0000 Subject: [PATCH 1/7] fix(strix): coalesce push scans per protected branch instead of one group per run id The workflow-level concurrency key fell back to github.run_id for every non-PR event, so each main push was its own group and no newer main head ever retired an older, superseded scan. Measured 2026-09-05T14:27Z in .github: nine push/main Strix runs outstanding at once (five running, one past two hours; four queued) against a 10-30 minute normal scan, each holding a slot under the shared 60-job ceiling. Scope push events as push- with the existing cancel-in-progress: true, so a newer head of the same protected branch supersedes the older scan exactly as a newer PR head does. A push scan covers the whole tree and publishes no strix commit status, so the newest head subsumes every older one; schedule and PR-less repository_dispatch keep a unique run id and the pr_number admission output is unchanged. Contract: queue-contract docstring records the measurement and a new assertion pins the push-{0} clause; the quick gate asserts it too; the 2026-09-04 doctoring record carries a dated amendment. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .github/workflows/strix.yml | 14 +++++++++++++- ...rtup-failure-and-strix-concurrency-20260904.md | 14 ++++++++++++++ scripts/ci/test_strix_quick_gate.sh | 1 + tests/test_required_workflow_queue_contract.py | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 58ed3dab8d..d8d8c2b35b 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -77,12 +77,24 @@ on: concurrency: # Workflow-level admission is required: job-level groups are never evaluated # while the whole run is queued behind the organization job ceiling. + # Push scans coalesce per protected branch: a newer head of the same branch + # supersedes the older scan exactly as a newer PR head does. A push scan + # covers the whole tree (STRIX_TARGET_PATH is './' outside PR scope) and + # publishes no 'strix' commit status, so the newest head subsumes every + # older one. With the run-id fallback every main push was its own group and + # nothing ever retired a superseded main scan: on 2026-09-05 nine push/main + # runs were outstanding at once in this repository (five running for up to + # two hours, four queued) against a 10-30 minute normal scan, each holding a + # slot under the shared 60-job ceiling. schedule and repository_dispatch + # without a PR number keep a unique run id. group: >- strix-security-scan-${{ github.event.pull_request.base.repo.full_name || github.event.client_payload.target_repository || github.repository }}-${{ github.event.pull_request.number || - github.event.client_payload.pr_number || github.run_id }} + github.event.client_payload.pr_number || + (github.event_name == 'push' && format('push-{0}', github.ref_name)) || + github.run_id }} cancel-in-progress: true # Scorecard Token-Permissions (alert #43): keep the workflow-level token diff --git a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md index e710eb5d1f..a905213e04 100644 --- a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md +++ b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md @@ -43,6 +43,20 @@ another. Workflow-level concurrency was deliberately not used because GitHub applies it before any live-head admission job can run and does not guarantee concurrency ordering. +**Amendment (2026-09-05).** "nor one another" no longer holds for `push` +events on the same branch. Measured at 14:27Z in `.github`: nine `push`/`main` +Strix runs were outstanding at once (five running, jobs started 12:31-14:25Z, +one already past two hours; four queued), each holding one slot under the +shared 60-job ceiling, against a 10-30 minute normal scan. The run-id fallback +in the workflow-level group made every main push its own group, so no newer +main head ever retired an older scan. The workflow-level group now scopes +`push` events as `push-`: a newer head of the same protected branch +supersedes the older scan exactly as a newer PR head does. This loses nothing +the gate consumes: a push scan covers the whole tree (`STRIX_TARGET_PATH` is +`./` outside PR scope) and publishes no `strix` commit status. `schedule` and +PR-less `repository_dispatch` runs still receive a unique run id. The +`pr_number=${GITHUB_RUN_ID}` admission output is unchanged. + ## Verification - `python -m pytest -q tests/test_pr_review_merge_scheduler.py -k 'startup_failures or startup_failure'` diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index b9b1c43de3..6ea00c099f 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -203,6 +203,7 @@ assert_strix_workflow_pr_trigger_hardened() { assert_file_contains "$workflow_file" "needs: [changed-scope, admit-current-head]" "strix provider queue waits for live-head admission" assert_file_contains "$workflow_file" 'strix-security-scan-${{' "strix workflow coalesces by repository and PR before job admission" assert_file_not_contains "$workflow_file" 'strix-security-scan-${{ needs.admit-current-head.outputs.target_repository }}-${{' "strix concurrency is not delayed until job admission" + assert_file_contains "$workflow_file" "format('push-{0}', github.ref_name)" "strix push scans coalesce per protected branch instead of one group per run id" assert_file_contains "$workflow_file" "cancel-superseded-pr-runs:" "strix workflow runs superseded-head cleanup outside the provider scan queue" assert_file_not_contains "$workflow_file" "format('closed-pr-{0}-{1}'" "strix cleanup does not need a second concurrency queue" assert_file_contains "$workflow_file" 'echo "pr_number=${GITHUB_RUN_ID}"' "strix workflow preserves independent push and schedule evidence" diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 803d43ab59..0c05ca6f6f 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -382,6 +382,17 @@ def test_strix_serializes_provider_evidence_per_repository_and_pr() -> None: superseded runs before runner admission, including runs still blocked by the organization-wide job ceiling. Native and dispatched evidence share one group; non-PR events use a unique run id. + + 2026-09-05: push events are scoped per protected branch (``push-``) + instead of a unique run id. Measured that morning in this repository: + nine ``push``/``main`` Strix runs were outstanding at once (five running + for up to two hours, four queued) against a 10-30 minute normal scan, + because the run-id fallback made every main push its own group and + nothing ever retired a superseded main scan. A push scan covers the whole + tree (``STRIX_TARGET_PATH`` is ``./`` outside PR scope) and publishes no + ``strix`` commit status, so the newest branch head subsumes every older + one. ``schedule`` and ``repository_dispatch`` without a PR number keep a + unique run id. """ workflow = workflow_text("strix.yml") concurrency_contract = workflow.split("concurrency:", 1)[1].split( @@ -398,6 +409,10 @@ def test_strix_serializes_provider_evidence_per_repository_and_pr() -> None: assert "github.event.pull_request.number" in concurrency_contract assert "github.event.client_payload.pr_number" in concurrency_contract assert "github.run_id" in concurrency_contract + assert ( + "(github.event_name == 'push' && format('push-{0}', github.ref_name)) ||" + in concurrency_contract + ) assert "github.event.pull_request.head.sha" not in concurrency_contract assert "github.event.client_payload.pr_head_sha" not in concurrency_contract assert "cancel-in-progress: true" in concurrency_contract From 1ed074c37e6e13fd3092fed21a677d04f5905bc0 Mon Sep 17 00:00:00 2001 From: seonghobae Date: Sat, 5 Sep 2026 16:19:28 +0000 Subject: [PATCH 2/7] docs(strix): state the merge-burst tradeoff of per-branch push coalescing Requested in review on #1938: during a merge burst each new main head cancels the previous scan, so the latest main is scanned once merging pauses for one scan duration; the weekly schedule scan is the floor. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .../startup-failure-and-strix-concurrency-20260904.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md index a905213e04..7c380ce7b5 100644 --- a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md +++ b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md @@ -57,6 +57,14 @@ the gate consumes: a push scan covers the whole tree (`STRIX_TARGET_PATH` is PR-less `repository_dispatch` runs still receive a unique run id. The `pr_number=${GITHUB_RUN_ID}` admission output is unchanged. +Tradeoff, stated so a later reader of the security dashboard is not +surprised: with `main` moving roughly every 30 minutes against a 10-30 minute +scan, "main is scanned after every merge" becomes "the latest `main` is +scanned once merging pauses for at least one scan duration". During a merge +burst each new head cancels the previous scan; the burst's final head is +scanned, and the weekly full-tree `schedule` scan (unique run id, never +cancelled) is the floor under a sustained burst. + ## Verification - `python -m pytest -q tests/test_pr_review_merge_scheduler.py -k 'startup_failures or startup_failure'` From 0dc71046a3117b706c39c855352388f0f788a882 Mon Sep 17 00:00:00 2001 From: seonghobae Date: Sat, 5 Sep 2026 16:20:38 +0000 Subject: [PATCH 3/7] docs(strix): record the coverage half of the push-coalescing trade in the workflow comment Peer measurement on #1938: 50 main pushes in 24 h, median gap 8.4 min, mean 26.5 min, so only the final head of each merge burst completes a scan. Stated next to the group key so a later coverage measurement does not read the gaps as a defect. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .github/workflows/strix.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index d8d8c2b35b..b301e2318a 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -87,6 +87,12 @@ concurrency: # two hours, four queued) against a 10-30 minute normal scan, each holding a # slot under the shared 60-job ceiling. schedule and repository_dispatch # without a PR number keep a unique run id. + # Coverage trade, measured the same day: main moved 50 times in 24 hours + # (median gap 8.4 min, mean 26.5 min, bursty), so with cancel-in-progress + # only the final head of each merge burst completes a scan -- one completed + # main scan per quiet window, not one per push. That is the intended + # exchange: a cancelled push scan loses no evidence a later whole-tree scan + # does not re-cover, and the weekly schedule scan is the floor. group: >- strix-security-scan-${{ github.event.pull_request.base.repo.full_name || From aca3b05146721365a2ae9b7fcfb04789ae7a789c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 08:24:07 +0000 Subject: [PATCH 4/7] docs(strix): separate occupied slots from queued runs, and scan scope from per-commit evidence retention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review corrections on ea0a429a6: the 14:27Z measurement supports five runs holding runner slots and four waiting in the queue, not nine slots; and a newer push scan covers the current tree in full without being a record of every earlier commit's findings — code that entered and left between two heads, or findings a retired run never uploaded, are absent, and a per-commit retention guarantee would need its own contract. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- ...-failure-and-strix-concurrency-20260904.md | 21 ++++++++++++------- .../test_required_workflow_queue_contract.py | 5 +++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md index 7c380ce7b5..5ba354947a 100644 --- a/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md +++ b/docs/doctoring/startup-failure-and-strix-concurrency-20260904.md @@ -45,16 +45,23 @@ concurrency ordering. **Amendment (2026-09-05).** "nor one another" no longer holds for `push` events on the same branch. Measured at 14:27Z in `.github`: nine `push`/`main` -Strix runs were outstanding at once (five running, jobs started 12:31-14:25Z, -one already past two hours; four queued), each holding one slot under the -shared 60-job ceiling, against a 10-30 minute normal scan. The run-id fallback +Strix runs were outstanding at once — five holding runner slots under the +shared 60-job ceiling (jobs started 12:31-14:25Z, one already past two hours) +and four more waiting in the queue behind them, which occupy no slot until a +runner is assigned — against a 10-30 minute normal scan. The run-id fallback in the workflow-level group made every main push its own group, so no newer main head ever retired an older scan. The workflow-level group now scopes `push` events as `push-`: a newer head of the same protected branch -supersedes the older scan exactly as a newer PR head does. This loses nothing -the gate consumes: a push scan covers the whole tree (`STRIX_TARGET_PATH` is -`./` outside PR scope) and publishes no `strix` commit status. `schedule` and -PR-less `repository_dispatch` runs still receive a unique run id. The +supersedes the older scan exactly as a newer PR head does. What a retired scan +gives up is its own report, not the gate's inputs: a push scan covers the whole +tree (`STRIX_TARGET_PATH` is `./` outside PR scope) and publishes no `strix` +commit status, so the newest head's scan is a complete scan *of the current +tree*. It is not a record of every earlier commit: code that entered and left +`main` between two heads, and findings a retired run never uploaded, are absent +from the newest report, and report collection preserves only runs that reach +it. A per-commit evidence-retention guarantee would need a separate, +verifiable preservation contract; this change does not provide one. `schedule` +and PR-less `repository_dispatch` runs still receive a unique run id. The `pr_number=${GITHUB_RUN_ID}` admission output is unchanged. Tradeoff, stated so a later reader of the security dashboard is not diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 0c05ca6f6f..9799094d7e 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -390,8 +390,9 @@ def test_strix_serializes_provider_evidence_per_repository_and_pr() -> None: because the run-id fallback made every main push its own group and nothing ever retired a superseded main scan. A push scan covers the whole tree (``STRIX_TARGET_PATH`` is ``./`` outside PR scope) and publishes no - ``strix`` commit status, so the newest branch head subsumes every older - one. ``schedule`` and ``repository_dispatch`` without a PR number keep a + ``strix`` commit status, so the newest head's scan is a complete scan of + the current tree (not a record of every earlier commit's findings). + ``schedule`` and ``repository_dispatch`` without a PR number keep a unique run id. """ workflow = workflow_text("strix.yml") From aa99c2c5c8b4467ae756b470ced32768408cb335 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 12:21:45 +0000 Subject: [PATCH 5/7] docs(strix): align the concurrency comment with the corrected retention and slot wording The workflow comment still said the newest head 'subsumes every older one' and that a cancelled push scan 'loses no evidence', which the same PR's doctoring amendment and contract-test docstring no longer claim. It also counted all nine outstanding runs as holding slots. Both now read the same way: a complete scan of the current tree rather than a per-commit record, and five holding runner slots with four queued. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .github/workflows/strix.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index b301e2318a..3049899745 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -80,19 +80,25 @@ concurrency: # Push scans coalesce per protected branch: a newer head of the same branch # supersedes the older scan exactly as a newer PR head does. A push scan # covers the whole tree (STRIX_TARGET_PATH is './' outside PR scope) and - # publishes no 'strix' commit status, so the newest head subsumes every - # older one. With the run-id fallback every main push was its own group and - # nothing ever retired a superseded main scan: on 2026-09-05 nine push/main - # runs were outstanding at once in this repository (five running for up to - # two hours, four queued) against a 10-30 minute normal scan, each holding a - # slot under the shared 60-job ceiling. schedule and repository_dispatch - # without a PR number keep a unique run id. + # publishes no 'strix' commit status, so the newest head's scan is a + # complete scan OF THE CURRENT TREE -- not a record of every earlier + # commit's findings: code that entered and left main between two heads, and + # findings a retired run never uploaded, are absent from it. With the run-id + # fallback every main push was its own group and nothing ever retired a + # superseded main scan: on 2026-09-05 nine push/main runs were outstanding + # at once in this repository against a 10-30 minute normal scan -- five + # holding runner slots under the shared 60-job ceiling (running for up to + # two hours) and four still queued, which occupy no slot until a runner is + # assigned. schedule and repository_dispatch without a PR number keep a + # unique run id. # Coverage trade, measured the same day: main moved 50 times in 24 hours # (median gap 8.4 min, mean 26.5 min, bursty), so with cancel-in-progress # only the final head of each merge burst completes a scan -- one completed # main scan per quiet window, not one per push. That is the intended - # exchange: a cancelled push scan loses no evidence a later whole-tree scan - # does not re-cover, and the weekly schedule scan is the floor. + # exchange: a cancelled push scan gives up its own report, and the scan that + # replaces it re-covers the current tree rather than that commit's history; + # a per-commit evidence-retention guarantee would need its own preservation + # contract. The weekly schedule scan is the floor. group: >- strix-security-scan-${{ github.event.pull_request.base.repo.full_name || From 37f7b0dc388924b626d251bbcad02640e5fa684c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 12:56:48 +0000 Subject: [PATCH 6/7] docs(strix): correct the stale on-trigger header that described non-cancelling concurrency The header still said Strix runs 'intentionally do not cancel in progress' and that GitHub keeps one active and one pending run per group, describing cancel-in-progress: false. The workflow has run cancel-in-progress: true with a repository+PR group for some time, and this PR extends that to push events per protected branch. The header now states the real grouping (repo+PR for native and dispatch PR runs, push- for pushes, unique run id for schedule and PR-less dispatch) and that a cancellation is a supersede signal rather than passing evidence, with the merge scheduler still requiring exact-head evidence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .github/workflows/strix.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/strix.yml b/.github/workflows/strix.yml index 3049899745..f15b29f564 100644 --- a/.github/workflows/strix.yml +++ b/.github/workflows/strix.yml @@ -44,12 +44,15 @@ on: # them, so the same doc/image-only decision is enforced by the # changed-scope job below. The run-name # includes the PR number and head SHA for status grouping, while the - # concurrency group is scoped per repository and event class to prevent - # shared-provider key rate-limit storms. Strix runs intentionally do not - # cancel in progress because a pre-job cancellation leaves no scanner log to - # review. GitHub keeps one active and one pending run per group; the merge - # scheduler re-dispatches exact-head evidence when a pending run is - # superseded. For PRs the merge scheduler manages, same-head Strix evidence + # concurrency group is scoped per repository AND pull request (native and + # dispatch PR runs share one group), or per protected branch for push + # events, to prevent shared-provider key rate-limit storms. That group + # runs with cancel-in-progress: true, so a newer head of the same PR or + # branch retires the older run: cancellation is a supersede signal, never + # passing evidence, and the merge scheduler still requires exact-head + # evidence before it will act. schedule and PR-less repository_dispatch + # keep a unique run id and so are never cancelled by a sibling. + # For PRs the merge scheduler manages, same-head Strix evidence # is still forced at merge time via repository_dispatch (which paths-ignore # does not affect), so merged code never loses evidence. paths-ignore: From 6791f5eb888cc4c1652e7b2894c1bde714d44fda Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 15:32:42 +0000 Subject: [PATCH 7/7] test(actions): strip YAML inline comments from the concurrency group slice workflow_level_concurrency_group() dropped whole-line comments but kept an inline one, so `group: # ` still satisfied every substring assertion while the real key had collapsed. The existing negative control only covered a comment above the key. Stripping follows YAML's own rules rather than cutting at the first `#`: a comment opens only at line start or after whitespace, and never inside a quoted scalar. Only the key's own line is stripped -- inside a folded block body `#` is literal content, so cutting there would corrupt a legitimate value. Two controls added: the inline-comment shape, which fails without this change, and a shape that must survive it (a quoted scalar containing `#` and a folded body containing `#`). Gate: 2960 passed, 1 skipped, 21 subtests, coverage 100%, interrogate 100%. Reported by CodeRabbit on 6e0bb0945. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4 --- .../test_required_workflow_queue_contract.py | 87 ++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index f4994f5403..ecc6ceca51 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -21,6 +21,31 @@ def workflow_text(name: str) -> str: return (REPO_ROOT / ".github" / "workflows" / name).read_text(encoding="utf-8") +def _strip_yaml_inline_comment(text: str) -> str: + """Drop a YAML inline comment from one scalar line. + + YAML opens a comment at ``#`` only when it starts the line or follows + whitespace, and never inside a quoted scalar, so a bare ``split("#")`` + would truncate a legitimate value that merely contains the character. + """ + index = 0 + quote = "" + while index < len(text): + char = text[index] + if quote: + if quote == '"' and char == "\\": + index += 2 + continue + if char == quote: + quote = "" + elif char in "\"'": + quote = char + elif char == "#" and (index == 0 or text[index - 1] in " \t"): + return text[:index] + index += 1 + return text + + def workflow_level_concurrency_group(workflow: str) -> str: """Return only the workflow-level ``concurrency.group`` value, comments removed. @@ -43,7 +68,7 @@ def workflow_level_concurrency_group(workflow: str) -> str: if not collecting: if re.match(r"^\s*group:", line): collecting = True - value.append(line.split("group:", 1)[1]) + value.append(_strip_yaml_inline_comment(line.split("group:", 1)[1])) continue if re.match(r"^\s*[A-Za-z][\w-]*:", line): break @@ -313,6 +338,66 @@ def test_concurrency_group_slice_ignores_the_comment_that_documents_it() -> None assert "opencode-review-dispatch-${{ github.repository }}" in group_value +def test_concurrency_group_slice_ignores_an_inline_comment_on_the_key() -> None: + """An inline comment beside a plain-scalar key must not satisfy the contract. + + The full-line negative control above does not cover this shape. YAML allows a + comment on the key's own line, so a change collapsing the group to the + repository alone could keep the documented expressions one space away and + leave every substring assertion green. + """ + inline = textwrap.dedent( + """\ + concurrency: + group: opencode-review-dispatch-${{ github.repository }} # ${{ github.event.client_payload.pr_number || github.run_id }} + cancel-in-progress: true + permissions: + contents: read + """ + ) + group_value = workflow_level_concurrency_group(inline) + + assert "opencode-review-dispatch-${{ github.repository }}" in group_value + assert "github.event.client_payload.pr_number" not in group_value + assert "github.run_id" not in group_value + + +def test_concurrency_group_slice_keeps_a_hash_that_is_not_a_comment() -> None: + """Stripping must follow YAML's rules rather than cutting at every ``#``. + + Two shapes would be corrupted by a naive ``split("#")``: a quoted scalar + containing the character, and a folded block body, where ``#`` is literal + content and never opens a comment. Only the key's own line is stripped. + """ + quoted = textwrap.dedent( + """\ + concurrency: + group: "release-#42-${{ github.repository }}" + cancel-in-progress: true + permissions: + contents: read + """ + ) + assert "release-#42-${{ github.repository }}" in workflow_level_concurrency_group( + quoted + ) + + folded = textwrap.dedent( + """\ + concurrency: + group: >- + release-${{ github.repository }}-#${{ + github.run_id }} + cancel-in-progress: true + permissions: + contents: read + """ + ) + folded_value = workflow_level_concurrency_group(folded) + assert "#${{" in folded_value + assert "github.run_id" in folded_value + + def test_concurrency_group_slice_reads_a_folded_multi_line_key() -> None: """The real key is a folded block, so the slice must join its continuation lines.""" folded = textwrap.dedent(