Skip to content

fix(actions): Lopu repairs failed PR checks - #588

Merged
lopugit merged 6 commits into
github-actionsfrom
lopu/workflow-check-fix-33626155317
Sep 3, 2026
Merged

fix(actions): Lopu repairs failed PR checks#588
lopugit merged 6 commits into
github-actionsfrom
lopu/workflow-check-fix-33626155317

Conversation

@lopugit

@lopugit lopugit commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Lopu controller check repair

Lopu identified a failed PR check whose root cause is in the protected controller/workflow code.

Lopu controller fix — duplicate CodeQL analysis owner closes a PR's aggregate check timed_out

Symptom

PR #557 (docs: grow Thingtime's world-domination TODO garden, head
bb151336b799a1cde9f6add600f1e5976357bc2b) carries a red required-looking
CodeQL check:

  • check run 100230355382 (app github-advanced-security)
  • conclusion: timed_out, opened 2026-09-02T11:30:19Z, closed 11:36:46Z

The PR is documentation-only and its trusted CodeQL alert snapshot is empty, so
nothing in the PR's contents explains the failure.

Root cause

.github/workflows/codeql-analysis.yml, scope job, workflow_dispatch branch.

Two facts were collapsed into one variable:

  1. GitHub published refs/pull/N/merge (i.e. the PR is mergeable, so a
    pull_request run exists and is analyzing that merge ref itself).
  2. The published merge ref is current (its parents match the live base/head).

The freshness check clears merge_sha when (2) fails. The listener-ownership
decision then read that cleared value as if (1) had failed:

if [ "$base_has_pr_listener" = true ] \
   && [ -n "$merge_sha" ] \                     # <-- wrong fact
   && [ "$BACKFILL_LISTENER_OWNED" != true ]; then
  analyze=false                                 # pull_request run owns analysis
...
else
  analysis_ref="refs/pull/$PR_NUMBER/head"      # <-- taken instead

GitHub recomputes refs/pull/N/merge lazily. For #557 it was last computed at
04:25Z (first parent 814ebfdc, develop's tip at that time) while develop
advanced repeatedly through the morning, so at 11:29Z the ref was genuinely
stale — but the PR was still mergeable and its pull_request run
(33624831516) was analyzing refs/pull/557/merge normally.

Verbatim from the dispatched run's Select one analysis owner job
(run 33625651090, job 100232646196):

PR #557 has a stale merge ref; ignoring it because its parents do not match the live base and head.
PR #557 has no merge ref; Lopu will analyze its exact head while conflict management continues.

So one PR head was analyzed under two different refs at once:

run event analysis ref actions javascript-typescript
33624831516 pull_request refs/pull/557/merge 11:30:20Z 11:36:09Z
33624842347 workflow_dispatch refs/pull/557/head 11:30:29Z 11:41:49Z (job ran 12m43s)
33624901816 workflow_dispatch refs/pull/557/head 11:31:50Z 11:51:58Z (queued 12m behind the above)
33625651090 workflow_dispatch refs/pull/557/head 12:03:55Z

Advanced Security opened the PR's aggregate CodeQL check on the first
head-ref analysis (11:30:19Z) and then waited for the second configured
language on that same ref. The duplicate scans were themselves the
contention that stretched Analyze (javascript-typescript) from 7m02s (on the
uncontended pull_request run) to 12m43s, so it landed at 11:41:49Z — five
minutes after the check had already closed timed_out at 11:36:46Z. GitHub
does not reopen a completed check run, so the later successful analyses could
not clear it.

This is the identical hazard the workflow already documents and guards for the
push path ("Confirm this push still owns the analysis"), one event away and
unguarded.

Fix (in $GITHUB_WORKSPACE/trusted only)

  • .github/workflows/codeql-analysis.yml — record mergeable_pr when GitHub
    publishes a well-formed merge SHA, and keep it set when the separate
    freshness check clears merge_sha. Gate listener ownership on mergeable_pr.
    A listener-owned mergeable PR now always declines the dispatched scan,
    fresh ref or stale, because its own pull_request run owns analysis. Only a
    PR GitHub cannot merge (no ref at all, hence no pull_request run) still
    takes the deliberate exact-head fallback.
  • .github/scripts/workflow-control-plane-contract.mjs — pin the rule: the two
    facts must stay separately recorded, ownership must be decided by
    mergeable_pr, and it must not regress to -n "$merge_sha".

No behaviour change for: PRs targeting a branch without the listener (all three
ref states unchanged), conflicted PRs (still head-scanned), and
backfill_listener_owned=true activation backfills (still analyze).

Validation run

  • python3 -c "yaml.safe_load(...)" on codeql-analysis.yml — parses, jobs
    ['scope', 'analyze'].

  • node .github/scripts/workflow-control-plane-contract.mjs --self-test
    workflow control-plane contract: self-test OK (fails on the pre-fix file via
    the three new assertions).

  • node .github/scripts/build-all-branch.mjs --self-test — OK.

  • node .github/scripts/deploy-develop-pr-preview.mjs --self-test — OK.

  • git diff --check — clean.

  • Decision-matrix simulation of the edited block, 8 cases, all pass:

    base listener merge ref backfill result
    yes fresh no analyze=false (pull_request owns)
    yes stale no analyze=false (was head — the bug)
    yes absent (conflicted) no analyze=true, head
    yes fresh yes analyze=true, merge
    yes stale yes analyze=true, head
    no fresh no analyze=true, merge
    no stale no analyze=true, head
    no absent no analyze=true, head

    Replaying the pre-fix block on row 2 reproduces analyze=true ref=head.

Relationship to open PR #579

PR #579 (lopu/workflow-check-fix-33571033461github-actions) widens the
freshness check to also accept the live base branch tip as a valid first
parent. That is a correct and complementary improvement — it reduces how often
the stale verdict is reached — but it would not have prevented this failure:
#557's merge-ref first parent (814ebfdc, computed 04:25Z) matched neither
the cached pulls/557.base.sha (f31864b2) nor develop's live tip at
11:29Z (59f7c5f8). A lazily recomputed merge ref can always be arbitrarily
far behind, so the ownership gate is the part that has to be correct.

Verified interaction (synthesized merge of github-actions@9f7f4fa +
#584 + #573 + #577 + #580 + #579 + #574 + #565, then this patch applied with
git apply --3way):

  • .github/workflows/codeql-analysis.ymlapplies cleanly; the two edits
    are in different parts of the scope step.
  • .github/scripts/workflow-control-plane-contract.mjs — one trivial conflict:
    fix(actions): Lopu repairs failed PR checks #579 replaces the single assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u); line that this patch anchors after. Resolution is to keep
    fix(actions): Lopu repairs failed PR checks #579's three replacement assertions and append this patch's three after them.
    With that resolution,
    node .github/scripts/workflow-control-plane-contract.mjs --self-test
    reports workflow control-plane contract: self-test OK, so both changes
    coexist.

They are independent in intent and both should land.

Source Lopu workflow run

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — approve, but land this together with #579: they collide textually and compose semantically

First review note on this branch. The change is right, and the incident it cites is real — I pulled the live records rather than taking the comment's word for it:

Record Value
CodeQL aggregate check on #557 head bb151336 started 11:30:19Z, timed_out 11:36:46Z
run 33624842347Analyze (actions) completed 11:30:38Z
run 33624842347Analyze (javascript-typescript) completed 11:42:10Z

The aggregate check opened against the first snapshot to land and closed ~5.5 minutes before the slow language arrived. That is exactly the mechanism described, so gating ownership on mergeable_pr instead of the freshness-cleared merge_sha addresses the actual cause.

The part no single PR view shows: #588 and #579 conflict

Both are MERGEABLE/CLEAN against base, because GitHub only tests each against github-actions — never against each other. Merged in sequence:

9f7f4fa3 + 565, 573, 574, 575, 577, 579, 580   → all clean
         + 588                                  → CONFLICT
             .github/scripts/workflow-control-plane-contract.mjs

codeql-analysis.yml itself merges with no conflict at all.

The conflict is textual, and the resolution is one line

Both PRs insert assertions at the same point in assertControlPlaneContract. The only genuine overlap is the single pinned line

assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u);

which #579 must delete, because #579 rewrites the jq expression that line pins. This PR keeps it. So the union is: take #579's three assertions, drop that line, keep this PR's three.

I applied exactly that in a scratch integration clone and verified:

node --check .github/scripts/workflow-control-plane-contract.mjs        OK
node .github/scripts/workflow-control-plane-contract.mjs --self-test    OK

and the merged codeql-analysis.yml carries both guards coherently:

138-145, 178-182   base_branch_sha + the widened parent check   (#579)
159, 171, 187-204  mergeable_pr + the ownership gate            (#588)

Why both are worth having

They are different defenses for the same incident, not duplicates:

#588 is the more robust of the two, since it removes the dependency on freshness entirely — but #579 still prevents the unnecessary exact-head fallback that produced the duplicate scan. Landing both costs one line of conflict resolution.

One caveat on the green checks

gh pr checks 588 is 27 SUCCESS / 52 SKIPPED / 0 failing, but that does not mean the contract suite passes here. resolve-pr-conflicts-routing-contract and promotion-worker-routing-contract are both red on this head — and on github-actions itself — because they run in control-plane-ci.yml's non-blocking contract-advisories job. Neither failure is caused by this PR; #584 (or #575) fixes both. Worth landing that first so a real regression here would actually stand out.

Suggested order: #584#565/#573/#574/#577/#580#579 + #588 with the union resolution above.

Posted by Lopu, Thingtime's automated principal reviewer.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review

Lopu reviewed this PR against github-actions as Thingtime's principal PR and repository manager. Using Claude Opus 5.

Lopu found no justified local change to publish from this review pass.

Lopu review — PR #588 · lopu/workflow-check-fix-33626155317github-actions

Compared head 6b79b4bc against target base github-actions@9f7f4fa3 (full head vs
base, not the merge commit). Base tip is still 9f7f4fa3 — zero drift since the
manifest snapshot — and mergeable: MERGEABLE, mergeStateStatus: CLEAN.

Diff under review: 2 files, +111/−4.

  • .github/workflows/codeql-analysis.yml — separates "GitHub published a merge
    ref" (mergeable_pr) from "the ref it published is current" (merge_sha), and
    gates dispatched-scan ownership on the former.
  • .github/scripts/workflow-control-plane-contract.mjs — pins that separation,
    and relaxes one over-specific freshness assertion so it survives PR fix(actions): Lopu repairs failed PR checks #579's
    multi-line rewrite of the same jq program.

Check state

gh pr checks 588: 0 failing, 0 cancelled, 0 timed out (27 pass, rest
skipping). There is no failure, cancellation, or required action on this head to
diagnose. REVIEW_DISPATCH_ID is lopu-review:33723961862, the routine review
handoff (control-plane / Hand off one Lopu repository review session) — not a
check-run:/workflow-run:/issue-comment:/inline-comment: wake. So this pass
is a correctness review, not a repair.

CodeQL

Trusted alert snapshot for this head is empty ([]). Nothing to fix and
nothing to dispose. 588.json in the dispositions directory left at [];
no alert state was PATCHed.

Findings

1. The core fix is correct — verified by replay, not by reading

I extracted the ownership block programmatically from the YAML (lines 132–222)
rather than transcribing it, stubbed only the two gh api calls, and ran it under
set -euo pipefail. Then I did the same with the block extracted from the base
commit's file, using the identical harness and stubs.

base listener merge ref backfill pre-fix post-fix
yes fresh no analyze=false analyze=false
yes stale no analyze=true ref=head analyze=false
yes absent no analyze=true ref=head unchanged
yes fresh yes analyze=true ref=merge unchanged
yes stale yes analyze=true ref=head (+::warning::) unchanged
yes absent yes analyze=true ref=head unchanged
no fresh / stale / absent merge / head / head unchanged
yes malformed SHA no analyze=true ref=head unchanged

Exactly one row changes, and it is the row the PR claims. The refless case — a
conflicted PR, the only kind with no pull_request run of its own — still reaches
the exact-head fallback. Reachability of the elif [ -n "$merge_sha" ] arm is
unchanged.

2. The new contract assertions have teeth — 6/6 mutations caught

I injected each regression the contract claims to prevent and re-ran
--self-test. All six fail the contract:

mutation result
revert the whole block to the pre-fix file fails (bites)
revert the gate to -n "$merge_sha" fails (bites)
re-collapse: add mergeable_pr=false beside the freshness merge_sha="" fails (bites)
hoist mergeable_pr=true out of the SHA-validated branch fails (bites)
drop .[0] == $base from the freshness jq fails (bites)
drop .[1] == $head from the freshness jq fails (bites)
rename the variable fails (bites)

The assert.deepEqual over the assignment list is the load-bearing one: it pins
the variable's whole lifecycle (initialized false, set true exactly once, only
where a well-formed published merge SHA was captured), which is what catches the
write-side re-collapse that the three read-side assertions alone would miss.

Note on method: my first mutation run reported three assertions "blind." That
was my harness, not the contract — "\$" inside a Python string literal is a
literal backslash-dollar, so three of the replacements silently matched nothing
and were no-ops. Re-run with the mutations written to a file and asserted
non-empty (assert t != orig), all six bite. Recorded because a mutation test
that silently mutates nothing always reports "guard blind."

3. New: this is the majority state of the repo, not a rare race

No previous pass measured the population; they established the mechanism on
#557 specifically. I evaluated the live freshness predicate against every open PR:

  • 19 open PRs. 0 are refless. 8 fresh. 11 (58%) stale.
  • Every one of the 11 is a first-parent (base) mismatch. The second-parent
    (head) comparison never fires — head skew does not occur, because a head change
    retriggers pull_request and GitHub recomputes the ref.

So pre-fix, 11 of 19 open PRs would take the exact-head fallback on a handoff
dispatch, beside their own live pull_request merge-ref run — #557's split-ref
shape, right now, on more than half the repo. That materially raises this PR's
value above "fixes one incident."

It also isolates which half of the freshness test is load-bearing: .[1] == $head
is the real safety property; .[0] == $base is the noise source. That is why
gating ownership on it was the wrong cut, and why #579 widening only .[0]
targets the correct half.

4. New: how #579 and #588 compose, quantified

The "land both" constraint has been asserted repeatedly on this PR but never
measured. Replaying #579's widened predicate (accept the live base branch tip as
.[0]) over the same 11:

count
fresh today 8
stale today, rescued by #579 alone 8
stale today, still stale after #579 3 (#568, #564, #499)

Those 3 have merge refs whose first parent is neither the cached base.sha nor
the live tip — lazily computed against an intermediate base state, exactly the
class #579 cannot close because a lazily recomputed ref can be arbitrarily far
behind. #579 takes exposure 11 → 3; #588 takes it to 0. Both should land;
#588 is the backstop and the one that closes the class.

5. Gate input verified sound

The fix makes base_has_pr_listener the sole remaining gate on whether a
mergeable PR is centrally re-analyzed, so I verified its input rather than
assuming it. It is computed by grep -Eq '^ pull_request:$' against the base's
workflow file at the cached pulls/N.base.sha — and that pointer is stale for
14 of 19 open PRs. In practice it does not bite: the anchored grep matches at all
three live base tips (main, develop, github-actions) and at all 19 cached
base SHAs — 19/19 detected
. The anchor also correctly excludes the
pull_request_target: trigger two lines below it on main/develop. The
residual risk is confined to a listener activation/deactivation window, which is
precisely what backfill_listener_owned exists to cover.

6. Residual — unchanged, deliberate, and now visible

backfill_listener_owned=true + mergeable + stale still routes to
refs/pull/N/head, structurally the same split-ref shape as #557. It is opted
into, it is bounded (the completion check suppresses the repeat), it is
pre-existing and untouched by this PR, and it is now the one state that emits a
real ::warning::. Narrowing it risks leaving a PR unscanned. Correctly left
alone.

Changes made

None. No defect found in this head, and no speculative churn. The one
diagnostic defect a previous pass identified — the concurrency alarm firing on the
no-listener path, where no pull_request run can exist — is already fixed on this
branch in 6b79b4bc; I re-ran the 12-state replay against it and every analyze /
analysis_ref decision is identical to the pre-split behaviour, so that hunk is
diagnostics-only as claimed.

Worktree left clean (git status --porcelain empty). No controller fix written:
the root cause is not a live failure in .github/workflows/**, .github/actions/**
or .github/scripts/**, so no lopu-workflow-fix.md was produced.

Operational finding (not a code defect, no action taken)

Conversation comment 5511880220 on this PR, authored by github-actions[bot],
has a 25-character body that is literally @/tmp/lopu-588-comment.md — a prior
Lopu publication passed an @file argument to an endpoint that took it literally,
so the intended review text was never posted. It carries no
<!-- thingtime-lopu-conversation:v1 --> marker, so per the editing rule I did not
touch it. Worth repairing in whatever Lopu step posts these: build the JSON body
and pipe it (gh api --input -) rather than relying on @-expansion. I used that
form for this pass's comment.

Validation run

check result
python3 yaml.safe_load(codeql-analysis.yml) parses; jobs ['scope', 'analyze']
node .github/scripts/workflow-control-plane-contract.mjs --self-test workflow control-plane contract: self-test OK
node --check workflow-control-plane-contract.mjs OK
ownership-block replay, 10 input states, head vs base 1 row changes, as documented
contract mutation suite, 6 injected regressions 6/6 caught
build-all-branch --self-test OK
deploy-develop-pr-preview --self-test 88/88 passed
codeql-open-pr-backfill --self-test OK
merge-main-develop-sync-pr --self-test OK
promote-features-to-main --self-test OK
git diff --check clean
git status --porcelain empty
gh pr checks 588 0 failing / 0 cancelled / 0 timed out
CodeQL snapshot empty; dispositions left []

Verdict: approve, no changes. Land #579 first (using the corrected
keep-both-blocks-plus-one-); resolution already recorded on this PR), then this.

— Lopu, reviewing 6b79b4bc against github-actions@9f7f4fa3.

View Lopu workflow run

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@/tmp/lopu-588-comment.md

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — I simulated the whole batch landing. Seven merge clean; this PR and #584 are the only two that don't.

Earlier notes on this branch already say "land #588 with #579, they collide textually and compose semantically." This is the follow-through: I ran the merge instead of predicting it, and I have the exact resolution.

The batch merge order, verified

Replaying every open lopu/workflow-check-fix-* PR into github-actions@9f7f4fa3 with git merge-tree --write-tree, in PR order:

Step Result
#565#573#574#575#577#579#580 clean at every step
#584 (after #575) CONFLICT — both files (duplicate of #575)
#588 (after #579) CONFLICT — one hunk of workflow-control-plane-contract.mjs

I then materialised the 7-PR stack and ran the full control-plane suite on it — all 13 contracts plus graphify-cas, rebase-index-fingerprint, rebase-related-edits and lopu-pr-status:

stage-graphify-snapshots.mjs --self-test            OK
workflow-control-plane-contract.mjs --self-test     OK
resolve-pr-conflicts-routing-contract.mjs --self-test OK
promotion-worker-routing-contract.mjs               OK
promotion-worker-contract.sh                        OK
rebase-ownership-routing-contract.sh                OK
build-all-branch.mjs --self-test                    OK
merge-main-develop-sync-pr.mjs --self-test          OK
deploy-develop-pr-preview.mjs --self-test           OK
promotion-pr-changelog.mjs --self-test              OK
promote-features-to-main.mjs --self-test            OK
electron-pr-release-contract.mjs                    OK
classify-claude-credential-failure.mjs --self-test  OK
graphify-cas / rebase-index-fingerprint / rebase-related-edits / lopu-pr-status  OK

Worth stating plainly: #573's new assertCapturedStdoutStaysClean guard is green over #565's, #577's and #580's newly-added shell, not just over the tree it was written against. That was the composition risk in this batch and it does not bite.

The #588#579 conflict, and its exact resolution

codeql-analysis.yml auto-merges cleanly — only the contract collides, in one hunk at line 893:

Resolution: keep both sides, and drop #588's re-add of the replaced line. #579 rewrites the jq expression that pattern pins, so it no longer exists in the workflow and re-adding the assertion would fail.

I built that union and ran it:

workflow control-plane contract: self-test OK

I also read the auto-merged scope block end to end. The two changes are genuinely complementary, in this order:

base_branch_sha=""                       # ← #579: live base tip as a 2nd accepted parent
...
mergeable_pr=false                       # ← #588: "GitHub published a merge ref"
if candidate_merge_sha="$(gh api ...)"; then
  if [[ ... ]]; then merge_sha=...; mergeable_pr=true; fi
fi
if [ -n "$merge_sha" ]; then             # ← #579 widens this freshness test
  ... .[1] == $head and (.[0] == $base or ($branch_base != "" and .[0] == $branch_base))
fi
if [ "$base_has_pr_listener" = true ] \
   && [ "$mergeable_pr" = true ] \       # ← #588: ownership no longer reads the cleared merge_sha

#579 makes the false-stale read rarer; #588 makes it stop mattering when it still happens (the base can always advance between the two reads). Neither subsumes the other.

What I'd do

Land the seven clean ones, then #588 with the union resolution above. #584/#575 need a close/keep decision first — see my note on #584.

No changes made to this branch: the change is correct as written, and a merge-time conflict is not something to pre-empt on the branch.

— Lopu, reviewing at 5925205b against github-actions@9f7f4fa3. gh pr checks clean, CodeQL snapshot empty (nothing dismissed).

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the workflow fix is right; the contract that shipped with it did not actually hold it in place. Strengthened on this branch.

Earlier notes on this branch verified the #557 incident and the #579 interaction. I re-ran both from scratch rather than inheriting them, and then went after the part no previous pass tested: does the new guard catch the regression it was written to catch?

It did not.

The gap, demonstrated

The PR adds three assertions to assertControlPlaneContract. All three pin how mergeable_pr is read — that it exists, that the gate tests it, that the gate is not the old -n "$merge_sha" form. The regression that matters is a write.

One line added to the freshness check:

  merge_sha=""
  mergeable_pr=false      # <-- re-collapses the two facts this PR separated

reproduces #557 exactly — and the contract stays green:

mutation pre-fix behaviour restored? contract verdict (as published)
A — mergeable_pr=false in the freshness block yesanalyze=true, ref=refs/pull/557/head self-test OKmissed
B — initializer flipped to mergeable_pr=true conflicted PRs stop being scanned at all self-test OKmissed
C — gate reverted to -n "$merge_sha" yes ❌ caught

Mutation A is the whole incident back, under a guard reporting workflow control-plane contract: self-test OK. Mutation B is the mirror failure: every PR looks mergeable, so the one case with no pull_request run — a conflicted PR — gets declined and never scanned.

What I changed (+19 lines, contract only)

Pin the variable's lifecycle rather than its individual reads:

assert.deepEqual(
  codeql.match(/^\s*mergeable_pr=\S+$/gmu)?.map((assignment) => assignment.trim()),
  ["mergeable_pr=false", "mergeable_pr=true"],
  "`mergeable_pr` is initialized false and set true exactly once, so the freshness check cannot clear it",
);
assert.match(
  codeql,
  /merge_sha="\$candidate_merge_sha"\n\s+mergeable_pr=true$/mu,
  "`mergeable_pr` records the published merge SHA GitHub actually returned, never an unconditional default",
);

Two assignments, in that order, with the true bound to the site where the well-formed published merge SHA is captured. Nothing between the lookup and the ownership gate can touch it. assert.deepEqual and occurrence-counting are already house style in this file.

codeql-analysis.yml is unchanged — the workflow fix is correct as written.

After: A ❌, B ❌, C ❌, and a control mutation (rewording an operator echo, no logic change) still ✅ — so it is not brittle.

#579 union: re-verified, resolution unchanged

Reproduced the merge in an isolated clone. #579 into github-actions@9f7f4fa3 is clean; #588 after it conflicts in one hunk of the contract, and codeql-analysis.yml auto-merges cleanly. The published resolution still stands — keep #579's three assertions, drop #588's re-add of the \.\[0\] == \$base and \.\[1\] == \$head line #579 replaces, keep #588's three. I built that union, layered these two assertions on top, and got self-test OK; the re-collapse mutation still fails on the merged tree. My lines land after #588's block, so the conflict shape is unchanged.

Validation

contract --self-test OK · node --check OK · codeql-analysis.yml YAML parse OK (jobs ['scope','analyze']) · bash -n on the extracted scope script OK · build-all-branch / deploy-develop-pr-preview (88/88) / merge-main-develop-sync-pr / promotion-pr-changelog / promote-features-to-main / stage-graphify-snapshots / classify-claude-credential-failure OK · git diff --check clean · gh pr checks 588 27 pass / 0 failing · CodeQL snapshot empty, nothing dismissed.

One housekeeping note

Four comments across #573 (2), #575 (1) and #588 (1) have a body of literally @/tmp/lopu-*.md — an ad-hoc gh … --body "@file" where --body-file was meant (gh does not expand @file for --body). No controller script does this, so it is a session-command habit rather than a control-plane defect. They lack the thingtime-lopu-conversation:v1 marker, so I left them for a human to clear.

Verdict: approve. The workflow change fixes the real cause of the #557 aggregate-check timeout; the guard now genuinely prevents its return.

— Lopu, reviewing at 5925205b against github-actions@9f7f4fa3.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review — batch review of 16 open PRs.

The diagnosis here is right, and the lifecycle pin is the good part: rather than only constraining how mergeable_pr is read, the deepEqual over its assignment list means re-collapsing the two facts by adding mergeable_pr=false to the freshness check fails loudly instead of silently reproducing #557.

This PR and #579 are not independently mergeable

They fix opposite halves of the same #557 incident, and they collide in a way that's easy to miss. I checked rather than assumed:

1. They conflict in the contract file.

$ git merge-tree --write-tree 21b85551 9b978184
CONFLICT (content): Merge conflict in .github/scripts/workflow-control-plane-contract.mjs

(That and #584#575 are the only conflicting pairs among the nine controller PRs.)

2. codeql-analysis.yml merges cleanly — which is the trap. #579 rewrites the merge-parent jq into a multi-line form. This PR keeps assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u). I built the merged tree and ran this PR's contract against it:

AssertionError: The input did not match the regular expression
  /\.\[0\] == \$base and \.\[1\] == \$head/u

So resolving the .mjs conflict the obvious way — keep both sides' new assertions, keep the retained old one — produces a contract that fails. And workflow-control-plane-contract.mjs --self-test runs only in the non-blocking advisory lane, so it would sit green in CI while the contract guarding CodeQL routing was dead. That's the same failure mode #584 is fixing two contracts' worth of, one event away.

Suggested order

Merge #579 first — it's the side that rewrites the jq and retires that assertion, so it resolves cleanly in that direction — then rebase this onto it. The mergeable_pr work here is orthogonal and survives intact; the resolution is "take #579's replacement of the base-parent assertion, keep this PR's mergeable_pr block, drop the retained old line."

I deliberately have not pre-adopted #579's changes here. This PR is correct and green against its own base, and rewriting it to accommodate an unmerged sibling would be speculative churn.

Validation: full blocking control-plane-ci.yml verify suite passes in this worktree; workflow-control-plane-contract.mjs --self-test OK against this PR's own changes.

No changes made.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — I checked which dispatcher actually produced the #557 duplicates. All three came from the lane this PR gates.

Earlier passes on this branch established the mechanism from the run logs and then strengthened the contract so the regression that matters — re-collapsing the two facts with a mergeable_pr=false write — fails loudly. Both hold up; I re-ran them rather than inheriting them. So I went at the one thing no pass had checked: codeql-analysis.yml has two callers, and this gate only affects one of them. If the #557 storm had come from the other, the fix would be aimed at the wrong lane.

It didn't.

Attribution

run event head_branch actor
33624831516 pull_request codex/thingtime-world-domination-todos-… lopugit
33624842347 workflow_dispatch main github-actions[bot]
33624901816 workflow_dispatch main github-actions[bot]
33625651090 workflow_dispatch main github-actions[bot]

head_branch: main identifies the dispatcher unambiguously. codeql-pr-handoff.yml uses gh workflow run … --ref "$DEFAULT_BRANCH" and default_branch is main; codeql-open-pr-backfill.mjs dispatches with ref: "github-actions". The handoff never sets backfill_listener_owned, so all three duplicates land on exactly the branch this PR flips to analyze=false. Zero came from the backfill lane.

It recurred, and the merge-ref lane was healthy the whole time

From code-scanning/analyses:

refs/pull/557/merge @ cd9b26e5   actions 11:30:20Z   jsts 11:36:09Z     <- one clean pair
refs/pull/557/head  @ bb151336   actions 11:30:29Z, 11:31:50Z, 11:39:47Z
                                 jsts    11:41:49Z, 11:51:34Z, 12:03:55Z <- six uploads, one head

And the previous head f9e8b057 shows the same doubling at 05:38:43Z / 05:45:55Z — so this fired on every synchronize whose merge ref happened to be lazily stale, not only on the one that went red. Aggregate check confirmed straight from the API: 100230355382, github-advanced-security, head_sha bb151336, 11:30:19Z → 11:36:46Z, timed_out. The PR body's account matches.

Why declining at the gate is the right cut, rather than tightening the idempotency guard

analyze runs under group: codeql-<language>-<event_name>-<analysis_sha> with queue: max, cancel-in-progress: false. All three dispatches share workflow_dispatch + bb151336, so they land in one group and serialize:

run Analyze (javascript-typescript)
33624831516 (merge ref) 11:29:16Z → 11:36:18Z 7m02s, different group, unimpeded
33624842347 (head ref) 11:29:27Z → 11:42:10Z 12m43s
33624901816 (head ref) 11:42:13Z → 11:51:58Z starts 3s after its predecessor ends
33625651090 (head ref) 11:52:01Z → 12:04:32Z starts 3s after its predecessor ends

That three-second cadence is a queue, not variance. So once one duplicate head-ref scan exists, each repeat handoff dispatch for the same head adds another ~10–13 minutes to the head-ref queue instead of converging.

The existing scope idempotency check is a completion check ("both categories already uploaded for analysis_ref@analysis_sha"), not a concurrency check — at 11:31:50Z it saw only actions uploaded and correctly let a second full scan start. Tightening it would still race. Gating ownership removes the class outright. This PR takes the correct cut, and it is the same shape as the guard the push path already carries.

One thing that reads backwards and is worth writing down

The incident runs say head_branch: main, which invites "this fix has to reach main before it does anything." It doesn't. main's codeql-analysis.yml is a 48-line trigger-only listener whose control-plane job is uses: …/codeql-analysis.yml@github-actions. The executable scope job exists only here. Landing this on github-actions is sufficient.

Two follow-ups, neither blocking this PR

1. The backfill lane keeps the residual — and its "don't pile on" guard has a blind spot. backfill_listener_owned=true still routes a listener-owned mergeable PR with a stale merge ref to refs/pull/N/head. That is bounded and convergent (one extra scan per head, then the completion check suppresses it), and planBackfill skips heads that activePrHeadKeys() reports active. But activePrHeadKeys() matches display_title against /^Lopu CodeQL PR #(\d+) @ ([0-9a-f]{40,64})$/, and handoff-dispatched runs execute main's listener, which has no run-name: — so they are titled Lopu CodeQL all branches (verified on run 33676241941) and their run.pull_requests[] is keyed to main, not the scanned PR. Neither arm sees them. After this PR that largely stops mattering, because handoff dispatches for listener-owned PRs exit in ~3s without analyzing — which is one more argument for landing it. Separate file, separate failure; I did not bundle it.

2. resolve-pr-conflicts-routing-contract.mjs --self-test is red here — and identically red at this PR's base 9f7f4fa3 (resolve-pr-conflicts.yml:733, the credential-probe lopu-agent call omits anthropic-api-key-fallback:). Not caused by #588; it is already visible as ⚠️ Conflict-resolver routing examples (exit 1) in this PR's own advisory comment, and #584 fixes exactly it. No duplicate controller fix from me.

Validation

workflow-control-plane-contract --self-test OK · codeql-analysis.yml YAML parses, jobs ['scope','analyze'] · node --check OK · git diff --check clean · build-all-branch / deploy-develop-pr-preview (88/88) / merge-main-develop-sync-pr / promotion-pr-changelog / promote-features-to-main / stage-graphify-snapshots / classify-claude-credential-failure / extract-vercel-prebuilt.py all OK · gh pr checks 588 shows nothing failing, cancelled, or timed out · trusted CodeQL snapshot empty, dispositions left [], nothing dismissed.

I also re-derived the reachability of the rewritten block: mergeable_pr is initialized before use under set -euo pipefail, and the elif [ -n "$merge_sha" ] arm keeps exactly its pre-change reachability (it still requires base_has_pr_listener=false or BACKFILL_LISTENER_OWNED=true). The only behavioural delta is the intended one.

Verdict: approve, unchanged. Sequencing constraint from the earlier pass still stands — merge #579 first, then rebase this onto it.

One cosmetic note for whoever rebases: the new exact-head fallback message says "a pull_request run may be analyzing its merge ref concurrently", but that arm is also reached when base_has_pr_listener=false, where no pull_request run exists. The hedge keeps it from being wrong, and reshaping that hunk for log precision isn't worth disturbing the validated #579 resolution — so I left it.

— Lopu, reviewing 21b85551 against github-actions@9f7f4fa3. No changes made.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — merge-order hazard with #579

This PR is correct and I have no changes to request. Flagging one thing that
will not announce itself.

#579 fixes the same PR #557 incident from the other end — it teaches the
staleness test to accept the live base-branch tip, so the merge ref is less
often judged stale, while this PR stops a stale judgement from deciding
listener ownership at all. Semantically they compose well and the merged
workflow is better than either alone; I built the merged file and read it.

They do not compose as text, and the dangerous half merges silently:

File On a real git merge
.github/workflows/codeql-analysis.yml ✅ auto-merges, no markers
.github/scripts/workflow-control-plane-contract.mjs ❌ conflicts

The trap is that this PR keeps
assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u)
and #579 rewrites that exact jq expression. I ran both crossings:

588 contract vs 579 workflow → FAIL  /\.\[0\] == \$base and \.\[1\] == \$head/
579 contract vs 588 workflow → FAIL  /gh api "repos\/\$REPO\/git\/ref\/heads\/\$base_ref"/

Because the YAML half resolves quietly, a conflict-marker-only resolution looks
fine and then goes red in the advisory lane afterwards.

Verified resolution — I performed the real merge and ran it: keep #579's
three replacement assertions, keep all of this PR's mergeable_pr
assertions, and drop only the retained \.\[0\] == \$base … line (#579's
assertions supersede it and are strictly stronger). Result on the merged tree:

workflow control-plane contract: self-test OK

Whichever of the two merges second needs to reconcile the assertions, not
just clear the markers.

Two asides on this branch, neither caused by it: the
resolve-pr-conflicts-routing-contract and promotion-worker-routing-contract
advisories fail here and on the base tip#584 fixes both. And a diff
against the base tip appears to show graphify-cas.mjs being deleted; that is
merge-base drift, not part of any PR in this batch.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu review — no defects. Careful work, and the cited incident is real — I verified it against the live API rather than taking the commit message for it:

On PR #557 @ bb151336, the aggregate CodeQL check started 2026-09-02T11:30:19Z and closed timed_out at 11:36:46Z. Run 33624842347 is the Lopu CodeQL all branches workflow_dispatch you name. The failure shape matches the diagnosis exactly.

The separation you've drawn is the right one: "did GitHub publish a merge ref at all" and "is the ref we captured current" are different facts, and only the first should decide listener ownership. Gating on the freshness-cleared merge_sha is what sent a dispatched backfill down the exact-head fallback beside a live pull_request merge-ref run.

The lifecycle assertion is the strongest part of the contract:

assert.deepEqual(
  codeql.match(/^\s*mergeable_pr=\S+$/gmu)?.map(a => a.trim()),
  ["mergeable_pr=false", "mergeable_pr=true"],
)

The other three assertions pin how mergeable_pr is read but not how it's written, and the regression that matters is a write — adding mergeable_pr=false beside the merge_sha="" in the freshness check would re-collapse the two facts and reproduce #557 while leaving all three green. Pinning the whole lifecycle (two assignments, that order, nothing in between) is the correct way to pin a state machine. The doesNotMatch on the old [ -n "$merge_sha" ] gate covers the other direction.

Splitting the log message so a stale ref no longer reports as absent is a real improvement too — the old log claimed the ref was absent on the line directly after announcing it was stale, which is what hid the concurrent pull_request run.

One mechanical conflict with #579 — both are wanted

#579 attacks the same incident from the other side: it stops a freshly-recomputed merge ref being misread as stale in the first place (pulls/N.base.sha and refs/pull/N/merge refresh independently, so they skew both ways when the base advances). Yours makes the ownership decision robust even when the ref genuinely is stale. Complementary — land both.

They conflict in workflow-control-plane-contract.mjs only:

Resolution: keep #579's three replacements, drop the retained one-liner from this side, keep your five additions. codeql-analysis.yml itself auto-merges cleanly.

I applied exactly that in a scratch merge and the combined contract passes against the combined codeql-analysis.yml.

Validation

workflow-control-plane-contract.mjs ✅ · stage-graphify-snapshots.mjs ✅ · incident verified against the live check-run API ✅ · composed with #579 (conflict resolved) ✅

Recommended batch order, validated end-to-end: #565#574#573#577#580#584#579#588. All five control-plane contracts pass at the end of it, versus two red on the tip today.

Posted by Lopu, Thingtime's PR manager.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu · this PR and #579 fix the same incident and break each other

Both this PR and #579 repair the PR #557 CodeQL timed_out, from opposite sides:

  • fix(actions): Lopu repairs failed PR checks #579 — the merge-ref freshness check required the first parent to equal pulls/N.base.sha only. That cached pointer and refs/pull/N/merge are refreshed independently, so a legitimately-recomputed merge ref read as stale. fix(actions): Lopu repairs failed PR checks #579 also accepts the live base branch tip.
  • this PR — even when the ref reads stale, the ownership gate should not conclude "no merge ref". Separating merge_sha (freshness) from mergeable_pr (existence) is what stops the dispatched backfill racing a live pull_request run on the exact head.

Both fixes are worth having, and I would keep both. But they collide, and I ran the merge rather than reasoning about it:

git merge-tree conflicts in both orders#579 → #588 and #588 → #579 — in .github/scripts/workflow-control-plane-contract.mjs. codeql-analysis.yml auto-merges cleanly in both directions.

The conflict itself is easy. The resolution is the hazard. #579 rewrites the merge-parent jq program into a multi-line two-base form and deletes the assertion that pinned the old single-line spelling. This PR keeps that assertion untouched at line 893:

assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u);

Take both sides — the obvious resolution — and that assertion survives against a yml that no longer contains the text. I built exactly that union tree (git's own auto-merged codeql-analysis.yml + both contract blocks) and ran the contract:

AssertionError [ERR_ASSERTION]: The input did not match the regular expression
  /\.\[0\] == \$base and \.\[1\] == \$head/u
  at assertControlPlaneContract (...workflow-control-plane-contract.mjs:893:10)

Advisory lane, so it would not block a merge — it would just start posting a contract-advisory comment on every control-plane PR, about a property that is completely intact. Exactly the "permanently red contract has stopped checking anything" failure that #584 and #575 are separately fixing elsewhere in this queue.

What I changed on this branch

I replaced that one brittle assertion with two on the invariant itself:

assert.match(codeql, /\.\[0\] == \$base/u, "…first parent against the PR base");
assert.match(codeql, /\.\[1\] == \$head/u, "…second parent must be the live head");

Both spellings satisfy these, so the check keeps testing the property without pinning one rendering of the jq program.

Verified: this worktree's contract is green, and the union tree of #579 + this PR is now green too (it failed before the change).

I deliberately left #579 alone — its three replacement assertions are strictly stronger than what they replaced, so there is nothing to fix on that side.

Merge either order; take both blocks in the contract conflict.

Nothing else in the two PRs interacts: #579's other assertions target codeql-open-pr-backfill.mjs, which this PR does not touch, and this PR's mergeable_pr lifecycle assertions sit in a region #579 leaves alone. I checked each one against the merged tree.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the fix holds up; one diagnostic was wrong, and the merge recipe I left earlier does not compile

Re-reviewed 20daa03f against github-actions@9f7f4fa3 (both still live — base tip unchanged, mergeable_state: clean). No failing check on this PR; every context is pass or skipping, and the CodeQL alert snapshot for this head is empty. So this is a correctness review, not a repair.

The core fix is right, and I checked it the hard way

I replayed the edited ownership block over all 12 reachable input states instead of re-reading the matrix in the description. The delta against the pre-fix block is exactly one row:

base listener merge ref backfill pre-fix post-fix
yes stale no analyze=true ref=head analyze=false

Eleven states unchanged. Separating "GitHub published a merge ref" from "that ref is current" is the right cut, and a conflicted PR — the only one with no pull_request run of its own — still gets the exact-head fallback.

The incident evidence checks out live, not just in prose:

check-run 100230355382  name=CodeQL  app=github-advanced-security
head_sha=bb151336…  conclusion=timed_out
started=11:30:19Z  completed=11:36:46Z

And the new contract assertions have teeth — I injected each regression they claim to prevent and confirmed the intended one fires: the pre-fix yml, mergeable_pr=false added beside the freshness merge_sha="", mergeable_pr=true hoisted out of the SHA-validated branch, and the gate reverted to -n "$merge_sha". Four for four.

One defect, fixed on this branch

The second hunk exists to make the logs legible, but the line it added says

a pull_request run may be analyzing its merge ref concurrently

whenever mergeable_pr=true. A concurrent pull_request run can only exist when the base carries the listener. Enumerating what actually reaches that branch:

listener merge ref backfill old message truth
yes stale yes concurrent run correct
no stale no concurrent run no listener ⇒ no run can exist
no stale yes concurrent run no listener ⇒ no run can exist

So the alarm fired on the ordinary no-listener path, where nothing competes — in the one hunk whose entire purpose is diagnostic accuracy for this incident class. I split it into three accurate cases and promoted the single genuine hazard to a real ::warning:: annotation. Diagnostics only: I re-ran the 12-state replay and every analyze / analysis_ref decision is identical.

⚠️ Correcting my own earlier guidance

My comment above closes with "take both blocks in the contract conflict." That does not compile. With a real git merge in both orders, the conflict region ends mid-assert.match( on both sides, and the closing ); sits outside it:

954:     "`mergeable_pr` records the published merge SHA GitHub actually returned, …",
955: =======
975:     "only a well-formed base branch name reaches the live tip lookup",
976: >>>>>>> theirs
977:   );

Stripping only the markers leaves the first side unterminated:

SyntaxError: missing ) after argument list

That is strictly worse than the single failing assertion the advice was written to avoid — the module fails to import, so every control-plane contract example stops running, silently, in the advisory lane.

Correct recipe: keep both blocks and insert one ); between them. Verified green in both merge orders, and with #584 on top the whole CI contract list passes (workflow-control-plane, resolve-pr-conflicts routing, promotion-worker routing, develop-preview 88/88, all-branch, backfill).

A note on how I got there, since it cuts against the earlier claim: my first two reproductions were both wrong. git apply --3way of a squashed patch invented a conflict shape git would never produce, and a --shared clone silently lacked the fetched PR refs, so a "clean merge" was really no merge at all. The numbers above are from real merges in both orders.

Residual, and not this PR's job

backfill_listener_owned=true + mergeable + stale still analyzes the exact head — structurally the same split-ref shape as #557, but deliberately opted into, and narrowing it risks leaving a PR permanently unscanned. Left alone; after this change it is the one state that emits a ::warning::, so it is visible if it ever bites.

Unrelated red you may notice

resolve-pr-conflicts-routing-contract and promotion-worker-routing-contract already fail on github-actions@9f7f4fa3 itself, untouched by this PR. They are advisory, which is why this PR is green; #584 / #580 / #575 repair them, and I confirmed both pass in the merged tree with #584.

Verdict: correct, narrowly scoped, ready to land — with the ); when it meets #579.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — I measured the population. This isn't a rare race: 58% of open PRs are in the state this PR fixes, right now.

Re-reviewed 6b79b4bc against github-actions@9f7f4fa3 (base tip unchanged, CLEAN). No failing check on this head, CodeQL snapshot empty, dispositions left []. So: correctness review, no repair, and no changes from me.

Earlier passes established the mechanism on #557 and then strengthened the contract. I re-ran both from scratch rather than inheriting them — both hold — and then went at the one thing no pass had done: nobody measured how often the fixed path is taken. It turns out to be the common case.

Every open PR, evaluated against the live freshness predicate

count
open PRs 19
merge ref absent (conflicted — the only refless case) 0
merge ref fresh 8
merge ref stale 11 (58%)

Pre-fix, all 11 take analyze=true ref=refs/pull/N/head on a handoff dispatch — beside their own live pull_request merge-ref run. That is #557's split-ref shape, available on more than half the repo at any moment, not a corner the base happened to advance into once.

And every one of the 11 is a first-parent mismatch. .[1] == $head never fires — head skew can't happen, because a head change retriggers pull_request and GitHub recomputes the ref. Which isolates the design point cleanly: .[1] == $head is the safety property; .[0] == $base is the noise source. Gating ownership on a predicate whose failures are ~100% base-skew was the wrong cut, and it's also why #579 widening only .[0] is aimed at the right half.

The #579 relationship, finally with numbers

This PR has said "land both, they compose" several times (mine included) without anyone quantifying it. Replaying #579's widened predicate — accept the live base branch tip as .[0] — over those same 11:

count
rescued by #579 alone 8
still stale after #579 3#568, #564, #499
#568  .[0]=9e7664da   cached base=f56959a7   live tip=4387af92   -> matches neither
#564  .[0]=9e7664da   cached base=b0804623   live tip=4387af92   -> matches neither
#499  .[0]=27fec941   cached base=4adda985   live tip=4387af92   -> matches neither

Their merge refs were computed against an intermediate base state — the class #579 structurally cannot close, because a lazily recomputed ref can sit arbitrarily far behind. #579 takes exposure 11 → 3. This PR takes it to 0. That's the argument for both, with a number on it.

I also verified the input the fix now leans on

This change makes base_has_pr_listener the sole gate deciding whether a mergeable PR gets centrally re-analyzed, so I checked it instead of assuming it. It's computed by grep -Eq '^ pull_request:$' against the base workflow at the cached pulls/N.base.sha — and that pointer is stale for 14 of 19 open PRs, the same skew #579 is about.

It doesn't bite: the anchored grep matches at all three live base tips and at all 19 cached base SHAs, 19/19. The $ anchor also correctly declines the pull_request_target: trigger two lines below it on main/develop. Residual risk is confined to a listener activation window — which is what backfill_listener_owned is for. Sound.

Replay and mutation results, re-derived

Block extracted programmatically from the YAML (not transcribed), stubs only on the two gh api calls, same harness against the base commit's block: exactly one row changeslistener + stale + no backfill, analyze=true ref=headanalyze=false. Refless PRs still get the exact-head fallback. And all six regressions the contract claims to prevent fail it, including the write-side re-collapse the deepEqual lifecycle pin exists to catch.

One method note, since it cuts against my own first result: my initial mutation run reported three assertions "blind." That was my harness — "\$" in a Python literal is backslash-dollar, so three replacements silently matched nothing. A mutation test that mutates nothing always reports the guard is blind. Re-run with assert t != orig, it's 6/6.

Verdict

Approve, no changes. The diagnostic defect flagged last pass is already fixed in 6b79b4bc, and I confirmed that hunk is decision-neutral. Sequencing still stands: #579 first with the corrected keep-both-blocks-plus-one-); resolution, then this.

Housekeeping: comment 5511880220 on this PR has a 25-character body that is literally @/tmp/lopu-588-comment.md — an earlier Lopu pass passed an @file argument to an endpoint that took it literally, so that review was never actually posted. It carries no thingtime-lopu-conversation:v1 marker, so I left it alone rather than edit an unmarked comment. Whichever step posts these should build the JSON body and pipe it via gh api --input -, which is how this one went out.

— Lopu, reviewing 6b79b4bc against github-actions@9f7f4fa3. No changes made.

@github-actions github-actions Bot added lopu: conflicting GitHub reports merge conflicts for the current PR snapshot and removed lopu: mergeable The PR branches can currently be merged without conflicts lopu: overlapping files This PR changes files also changed by another open PR labels Sep 3, 2026
…ve-pr588-controller

# Conflicts:
#	.github/scripts/workflow-control-plane-contract.mjs
@github-actions github-actions Bot added lopu: unknown state GitHub is still computing the PR branch state lopu: mergeable The PR branches can currently be merged without conflicts and removed lopu: conflicting GitHub reports merge conflicts for the current PR snapshot lopu: unknown state GitHub is still computing the PR branch state labels Sep 3, 2026
@lopugit
lopugit merged commit 22bc5a0 into github-actions Sep 3, 2026
79 checks passed
@github-actions github-actions Bot removed the lopu: mergeable The PR branches can currently be merged without conflicts label Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant