Skip to content

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

Merged
lopugit merged 1 commit into
github-actionsfrom
lopu/workflow-check-fix-33571033461
Sep 3, 2026
Merged

fix(actions): Lopu repairs failed PR checks#579
lopugit merged 1 commit into
github-actionsfrom
lopu/workflow-check-fix-33571033461

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 owners from a stale base comparison

Scope note, stated up front

This fixes a real, log-verified controller defect found while investigating
PR #557's red CodeQL check. It is not the cause of that red check, and I
do not claim it will turn the check green. The red check is driven by a GitHub
comparison that cannot be generated for that head; see "What this does not
fix". The defect below is worth repairing on its own merits — it burns roughly
45 minutes of serialized runner time per PR head and violates the single-owner
invariant this workflow documents for itself.

Defect

.github/workflows/codeql-analysis.yml decides whether a PR's own
pull_request run already owns analysis by checking the parents of
refs/pull/N/merge against pulls/N.base.sha:

base_sha="$(jq -r '.base.sha' <<<"$pr_json")"
...
jq -e --arg base "$base_sha" --arg head "$live_head_sha" \
  'length == 2 and .[0] == $base and .[1] == $head'

pulls/N.base.sha is GitHub's cached base pointer. refs/pull/N/merge is
recomputed independently. Once the base branch advances the two skew, in both
directions. For PR #557 the merge ref had been refreshed onto the live
develop tip while the cached pointer lagged:

Value SHA
refs/pull/557/merge 5d4493390464947c7d04e7ddc063e05b33706b19
its parents 7b6418bd… (live develop tip), ebb640e5… (live head)
pulls/557.base.sha (cached) f31864b2…

7b6418bd… != f31864b2…, so a perfectly current merge ref was rejected. All
four dispatched backfill runs (33570993922, 33571026172, 33571689494,
33571733288) logged it verbatim:

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.

Consequences, all observed on this one head:

  1. Four dispatches analyzed refs/pull/557/head even though develop carries
    the pull_request: listener and PR docs: grow Thingtime's world-domination TODO garden #557's own run 33571087941 already
    owned analysis. refs/pull/557/head holds 8 analyses for ebb640e5 — 4
    actions and 4 javascript-typescript — where the design intends none.
  2. The misclassification also defeats the analysisSnapshots dedupe, which
    cannot see an analysis that is still running. So the 10-minute controller
    tick re-selected the PR three more times.
  3. queue: max then serialized four javascript-typescript jobs into one
    queue: 23:26:16→23:38:57, 23:39:01→23:47:08, 23:47:12→00:00:01,
    00:00:05→00:05:06. About 45 minutes of runner time for one head, three
    quarters of it redundant.

The workflow's own comment already names head-ref uploads at a live PR head as
a hazard to avoid, but its guard covers only the push path
(if: github.event_name == 'push' && …). The workflow_dispatch backfill path
is unguarded, and that is the path that fired here.

.github/scripts/codeql-open-pr-backfill.mjs carried the identical defect in
analysisSnapshotForPullRequest (parents[0] === baseSha), which is what
re-selected PR #557 on each tick. Fixing only the workflow would have left the
controller feeding it bad candidates.

This is not an outage, a stale result, or a cancellation. It reproduces
deterministically from the recorded API values.

Other open PRs confirm it is a live race rather than a one-off: #554 and #560
share the same stale cached pointer, but their merge refs still lag to match
it, so they pass. #291 and #295 have both values in sync. #557 landed on the
unlucky side of a skew that any PR can hit.

Change

Both call sites now accept the merge ref when its first parent matches
either accepted base — the cached pulls/N.base.sha or the live base
branch tip from git/ref/heads/<base.ref> — while the second parent must still
be the live head. That second condition is the half of the guard that actually
detects a stale merge ref, and it is unchanged.

  • .github/workflows/codeql-analysis.yml — read .base.ref, resolve the live
    base branch tip (shape-validated branch name, shape-validated SHA, gh exit
    status preserved rather than || true, matching the file's existing
    convention), and widen the jq predicate.
  • .github/scripts/codeql-open-pr-backfill.mjs
    analysisSnapshotForPullRequest takes an optional baseBranchSha;
    resolveLiveAnalysisSnapshots resolves it once per distinct base branch and
    memoizes.
  • .github/scripts/workflow-control-plane-contract.mjs — the contract pinned
    the old predicate (.[0] == $base and .[1] == $head,
    parents[0] === baseSha). Replaced with assertions that pin the fixed
    behaviour and the live-tip lookup in both files.

Fails safe: when the live tip cannot be resolved, behaviour is byte-for-byte
the previous conservative path.

Validation

Control-plane CI equivalents, all from $GITHUB_WORKSPACE/trusted:

Check Result
node --check on all 22 .github/scripts/*.mjs pass
bash -n on all .github/scripts/*.sh pass
yaml.safe_load on all 15 workflows + 2 composite actions pass
node .github/scripts/codeql-open-pr-backfill.mjs --self-test OK
node .github/scripts/workflow-control-plane-contract.mjs --self-test OK
git diff --check clean

Six new deterministic examples in the backfill self-test cover live-tip parent,
cached-base parent, absent tip, neither-base-matches, malformed tip, and
wrong-head-parent.

End-to-end replay. The scope step was extracted from the YAML with
yaml.safe_load and executed against a mock gh replaying the exact recorded
responses for PR #557, for both the pre-fix revision (git show HEAD:.github/workflows/codeql-analysis.yml, i.e. ac680d64 — the revision
that actually ran) and the fixed file:

=== BEFORE (ac680d64, the controller revision that ran) ===
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.
   analyze=true  analysis_ref=refs/pull/557/head  analysis_sha=ebb640e5…

=== AFTER (fixed controller) ===
PR #557 has a merge ref and its target carries the normal listener; that pull_request run owns analysis.
   analyze=false analysis_ref=  analysis_sha=

The pre-fix run reproduces the incident log verbatim; the fixed run yields the
intended single-owner no-op.

Eight-case regression matrix over the fixed step, all correct:

# Scenario Result
1 live-tip parent, listener present (the incident) analyze=false, PR run owns analysis
2 cached-base parent, listener present analyze=false
3 tip lookup unavailable, cached-base parent analyze=false
4 tip lookup unavailable, live-tip parent stale → exact head (fails safe)
5 conflicted PR, no merge ref exact head
6 base predates listener merge ref backfill
7 backfill_listener_owned: true merge ref backfill
8 both analyses already present no-op

The jq predicate was additionally proved directly against PR #557's live
parent array: old ⇒ stale, new ⇒ accepted; and against the #554/#560 shape
(merge ref lagging instead): both old and new ⇒ accepted.

What this does not fix

PR #557's CodeQL check run 100064989293 is timed_out because GitHub
cannot generate the comparison Advanced Security needs to classify alerts as
new-in-PR:

Source Lopu workflow run

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ Deterministic CI contract advisory

These examples are warning-only. They do not fail a required check or block this PR.

  • ✅ Develop-preview controller examples
  • ✅ Vercel prebuilt archive safety examples
  • ✅ Workflow control-plane examples
  • ✅ Signed Electron PR release examples
  • ⚠️ Conflict-resolver routing examples (exit 1)
Sanitized tail
    '            --effort low\n' +
    '            --max-turns 1\n' +
    '            --dangerously-skip-permissions\n' +
    '            --allowedTools ""\n' +
    '\n' +
    '      - name: Report the live credential result\n' +
    '        env:\n' +
    '          CREDENTIAL_SLOT: ${{ steps.live_probe.outputs.claude-credential-slot }}\n' +
    '          CREDENTIAL_NAME: ${{ steps.live_probe.outputs.claude-credential-name }}\n' +
    '        run: echo "Live Claude authentication succeeded with $CREDENTIAL_SLOT ($CREDENTIAL_NAME)."\n' +
    '\n' +
    '  route:\n' +
    '    if: >-\n' +
    "      inputs.promotion_source_pr == ''\n" +
    "      && inputs.promotion_plan_b64 == ''\n" +
    "      && (inputs.maintenance_operation == ''\n" +
    "          || inputs.maintenance_operation == 'manage-prs')\n" +
    "      && !(github.event_name == 'workflow_dispatch'\n" +
    "           && github.actor == 'github-actions[bot]'\n" +
    "           && github.ref_name == 'github-actions'\n" +
    "           && inputs.pr_number == ''\n" +
    "           && inputs.branch == 'lopu-internal-all-branch')\n" +
    "      && (github.event_name != 'issue_comment'\n" +
    '          || (github.event.issue.pull_request',
  expected: /anthropic-api-key-fallback:/u,
  operator: 'match',
  diff: 'simple'
}

Node.js v22.23.2
- ✅ Rebase ownership routing examples - ⚠️ Promotion-worker routing examples (exit 1)
Sanitized tail
    '#   YAML is loaded from an arbitrary PR base or head ref.\n' +
    '# - `ai-merge-paused` is a user-controlled, durable stop signal. Automation\n' +
    '#   never creates, adds, removes, or treats it as stale: when present, every\n' +
    '#   detector and worker abstains until a user removes it. This prevents base\n' +
    '#   branch movement from silently re-spending AI/Vercel/GitHub compute.\n' +
    '#\n' +
    "# graphify-out/** is never given to the AI. The repo's merge driver for\n" +
    '# graph.json is not configured in CI, so git would silently text-merge it into\n' +
    '# a mixed base+head union (the poisoned-pair state CLAUDE.md forbids). Instead,\n' +
    '# when BOTH sides touched graphify-out since the merge base, the whole\n' +
    '# directory is deterministically reset to the base side before anything else.\n' +
    '# AFTER the resolution is verified and committed, the graph is refreshed on a\n' +
    '# pristine reset tree and committed separately, so the pushed graph reflects\n' +
    '# the merged code. That ordering is required: the verify step asserts the\n' +
    '# staged graphify-out subtree still equals the base side, so refreshing\n' +
    '# earlier would fail its own check.\n' +
    '#\n' +
    '# The refresh includes LLM SEMANTIC extraction when a configured Lopu provider\n' +
    '# credential exists: `graphify extract` + `graphify cluster-only` through\n' +
    '# OPENAI_API_KEY, ANTHROPIC_API_KEY, or CLAUDE_CODE_OAUTH_TOKEN. The configured\n' +
    '# review backend is preferred, so Terra/Sol-based Lopu review and Graphify use\n' +
    '# the same OpenAI project credential while Claude remains a fallback. Extract\n' +
    '# is manifest-incremental, and unchanged\n' +
    '# content is served from the tracked content-addressed semantic cach'... 487078 more characters,
  expected: /feature_stack_merge:[\s\S]*?if: >-\s*!cancelled\(\)\s*&& needs\.feature_stack_plan\.result == 'success'\s*&& needs\.model_config\.result == 'success'/,
  operator: 'match',
  diff: 'simple'
}

Node.js v22.23.2
- ✅ Promotion-worker behavior examples - ✅ Promotion changelog examples - ✅ Feature promoter examples - ✅ All-branch builder examples

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — approve; the premise is visible in the live data

pulls/N.base.sha is GitHub's cached base pointer and refs/pull/N/merge is recomputed independently, so they skew in both directions as soon as the base branch moves — and develop/github-actions move constantly here, so the skew window isn't rare.

The consequence is observable on #557: its aggregate CodeQL check (100064989293) opened 23:27:20 and closed timed_out at 23:33:46, while the analyses themselves landed on refs/pull/557/merge @ 5d449339 (/language:actions 23:28:31, /language:javascript-typescript 23:32:57). Every green PR in this batch shows the opposite shape — the aggregate check opens after the analyses land and concludes in 2–3 seconds.

Implementation

Both sides accept either accepted base while still requiring the live head parent — that's the property that matters, since widening the base must not widen what counts as "this PR's head".

  • liveBaseSha is shape-validated (^[0-9a-f]{40,64}$) and the extra disjunct is guarded by liveBaseSha !== "", so a malformed or unavailable tip can never widen the stale-merge guard. Both cases are in the self-test.
  • optionalBranchTipSha validates the branch name against ^[A-Za-z0-9._/-]+$ before interpolating it into the API path, and caches per distinct base branch — a 200-PR sweep makes a handful of calls, not 200.
  • The catch rethrows anything that isn't 404/409/422, so a transport or auth failure still surfaces instead of silently degrading to "no live tip". Right call.
  • codeql-analysis.yml mirrors it exactly, and the 2>/dev/null there is fine because the empty-string fallback is explicitly handled.
  • The negative self-test cases are the ones that make this trustworthy: a merge matching neither base stays stale, and an outdated head parent is still rejected.

Validation

node .github/scripts/workflow-control-plane-contract.mjs --self-test   → OK
node .github/scripts/codeql-open-pr-backfill.mjs --self-test           → OK

All seven control-plane PRs in this batch (#565, #573, #574, #575, #576, #577, #579) apply cleanly onto ac680d64 with no conflicts, and on that combined tree every contract self-test passes plus node --check over every .mjs and bash -n over every .sh.

One note for the record: this removes the systematic cause, but #557's specific check is now a stale terminal state — a successful re-analysis did not reopen it, so that one needs a fresh scan on a new head rather than a code change.

@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 #579 · lopu/workflow-check-fix-33571033461github-actions

Compared 9b978184 against github-actions @ 9f7f4fa3.

Check state

gh pr checks 579: 27 pass / 52 skipping / 0 fail, MERGEABLE / CLEAN. No failure to
diagnose. CodeQL snapshot for this head is empty; 579.json stays [].

The two advisory contracts that are red on this tree (resolve-pr-conflicts-routing,
promotion-worker-routing) are pre-existing on the base 9f7f4fa3 — I ran both against the
target branch tip directly to confirm. #584 fixes them; nothing here caused them.

What the PR does

pulls/N.base.sha (GitHub's cached base pointer) and refs/pull/N/merge are refreshed
independently, so a freshly recomputed merge ref legitimately reports a first parent that is
the live base branch tip rather than the cached pointer. The old freshness check read that as
stale. This PR resolves the live tip (git/ref/heads/<base_ref>) and accepts either base as
.[0], in both the workflow's jq and the backfill selector, while still requiring .[1] == $head.

Findings

No defects. What I checked:

  1. The widening is one-sided and bounded. Only .[0] gains a second accepted value; the live
    head parent stays mandatory, so an outdated merge ref is still rejected. The self-test covers
    exactly that case (mergeOf(sha("b")), parents[1] = sha("9") → falls back to head).
  2. Malformed input cannot widen the guard. liveBaseSha is regex-gated to ^[0-9a-f]{40,64}$
    and the shell side gates base_ref on ^[A-Za-z0-9._/-]+$ before the lookup, so a hostile or
    absent value degrades to the original cached-base-only behaviour. Both directions are pinned by
    fixtures.
  3. The lookup is failure-tolerant and cached. optionalBranchTipSha swallows 404/409/422 only
    (re-throwing anything else) and memoises per distinct base branch, so the backfill does one extra
    API read per base branch, not per PR.
  4. Contract assertions are consistent with the code. The three new assert.match calls pin the
    live-tip lookup, the two-base jq shape, and the base_ref charset gate.

Cross-PR: the #588 conflict, and a merge recipe that compiles

#579 and #588 repair the same PR #557 incident from opposite sides, and both replaced the same
single line
in workflow-control-plane-contract.mjs. I merged them in a scratch clone:

  • git merge conflicts on that file — adjacency only, no semantic disagreement.
  • A naive keep-both union fails node --check (SyntaxError: missing ) after argument list).
    The conflict hunk cuts mid-statement: the ); on the line following >>>>>>> closes only one
    of the two sides' final assert.match(.
  • The resolution that works is keep both hunks and add one ); closing fix(actions): Lopu repairs failed PR checks #588's last assertion
    before fix(actions): Lopu repairs failed PR checks #579's block starts.

On that resolved tree I ran and confirmed green: node --check, the control-plane contract, the
resolver-routing contract, the promotion-worker contract, codeql-open-pr-backfill.mjs --self-test,
and a YAML parse + bash -n over every run: block of codeql-analysis.yml.

I also verified the assertions survive each other's YAML: #588's loosened .[0] == $base /
.[1] == $head split matches #579's multi-line jq, and #579's multi-line pin is untouched by
#588's mergeable_pr edits (different region of the same step).

Batch landing order

584 → 588 → 580 → 577 → 574 → 573 → 565 is clean end to end. #579 is the only PR in the batch
that needs a hand resolution
, and it is a one-line one. Landing #579 before #588 would leave the
same conflict on the other side, so the order does not avoid it either way.

Changes made

None. The code is correct; the conflict is a queue-sequencing matter, not something to pre-resolve
on this branch (rewriting the hunk here would just move the conflict).

Validation run

  • gh pr checks 579 / gh pr view 579.
  • workflow-control-plane-contract.mjs --self-test → OK on this head.
  • codeql-analysis.yml YAML parse + bash -n on both run: blocks → 0 failures.
  • Base-branch advisory baseline (proving the two red contracts predate this PR).
  • Scratch-clone #588 + #579 merge, hand resolution, and the five-contract matrix above.

View Lopu workflow run

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — PR #579

Verdict: sound. No changes made to this branch. I re-derived the diagnosis
from live GitHub state rather than trusting the recorded values, and it holds.
All checks on 9b978184 are passing or skipping — no red check to repair. The
trusted CodeQL snapshot for this head is empty, so dispositions stay [].

Verified independently

PR cached base.sha live develop tip merge-ref parents[0]
#557 f31864b2… 7b6418bd… 7b6418bd… (live tip)
#554 f31864b2… 7b6418bd… f31864b2… (cached)
#560 f31864b2… 7b6418bd… 7b6418bd… (live tip)

Running both predicates directly against those live parent arrays:

Case old new
#557 — parent = live tip (the incident) stale ❌ ACCEPT
#560 — parent = live tip stale ❌ ACCEPT
#554 — parent = cached base ACCEPT ACCEPT (unchanged)
genuinely stale base parent stale stale
stale head parent stale stale
tip lookup unavailable stale stale (fails safe) ✅
3-parent/octopus commit stale stale

Because parents[1] == $head is retained, the widening cannot produce a false
accept: a merge commit parented on the live base tip and the live head is
current by definition. The guard only loses the ability to reject a merge ref
that is stale solely in its base parent — precisely the misfiring case.

One correction to the PR description

#554 and #560 share the same stale cached pointer, but their merge refs still
lag to match it, so they pass.

That is no longer true of #560 — its merge ref has since been recomputed
onto 7b6418bd…, so #560 is now hitting the same misclassification as #557.
Two live PRs are affected, not one. Only the prose is stale; the code is right.

Also checked, all fine

  • Permissions — the new git/ref/heads/<branch> read needs contents: read
    and the scope job declares it, so it is not a silent-403 no-op.
  • Fail-safe claim holds — I confirmed from the runner that gh api --jq
    exits 1 and writes its JSON error body to stdout on a 404. Keeping the read
    inside if …; then handles that correctly, so a failed lookup leaves
    base_branch_sha="" and the predicate collapses to the original form.
  • Error conventionoptionalBranchTipSha soft-fails on 404/409/422 and
    rethrows otherwise, byte-identical to the adjacent optionalPullMergeCommit.
  • Cost — one extra read per distinct base branch, memoized including
    negative results.
  • Fix completeness — the only other cached-vs-live base comparison,
    merge-main-develop-sync-pr.mjs:64, defers rather than misclassifying. That
    is a deliberate pre-merge interlock and correctly left alone; widening it
    would weaken a real safety property.

Residual limitation (not blocking)

If the base advances twice in quick succession the merge ref can be parented on
an intermediate commit matching neither accepted base, and the original
misclassification recurs — rarer, not impossible, and it degrades to the old
conservative path. A complete fix would test ancestry rather than equality, at
the cost of more API calls. The current trade-off looks right to me.

Validation: codeql-open-pr-backfill.mjs --self-test OK (the 6 new
examples included), workflow-control-plane-contract.mjs --self-test OK
and that one genuinely re-reads the modified files, so the new assertions really
do pin the fixed behaviour.


Separate finding — a control-plane defect found during this review

Not part of this PR and deliberately not placed on this branch. While
searching for sibling copies of the gh api … || true pattern I found one in
resolve-pr-conflicts.yml, in the step that builds Lopu's own review manifest:

merge_sha="$(gh api "repos/$REPO/git/ref/pull/$number/merge" --jq '.object.sha' 2>/dev/null || true)"
if [ -n "$merge_sha" ] && ! [[ "$merge_sha" =~ ^[0-9a-f]{40}$ ]]; then
  echo "::error::PR #$number returned an invalid merge-ref SHA."
  exit 1
fi

|| true discards the non-zero exit and captures GitHub's JSON error body, so a
missing merge ref — a normal race while GitHub recomputes after a base
advance, exactly the churn this PR documents — hard-aborts worktree preparation
for the entire batch, even though merge_sha is only an optional widening
of the CodeQL alert filter downstream. Notably, codeql-analysis.yml already
carries a comment warning about this precise hazard; this was the last copy of
the anti-pattern.

Fixed in the trusted controller checkout, with a replay against a mock gh
using the real 404 body: 404 → before exit 1 / after continues with
merge_sha=""; 200 + valid SHA → unchanged; 200 + malformed SHA → still
a hard error. A dedicated Lopu fix PR against github-actions follows.

I also found — and deliberately did not change — a pre-existing contract
failure on ac680d64 itself (resolve-pr-conflicts.yml:733 omits
anthropic-api-key-fallback), currently masked because control-plane CI runs
contracts as non-blocking advisories. Its two resolutions have opposite security
implications, so that one is the owner's call; details are in the fix PR.

Posted by Lopu (github-actions[bot]) — automated repository review.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the guard is widened in exactly one dimension, and the self-test proves it

Merge order. The control-plane contracts run in a non-blocking advisory job, so green checks here don't certify them. Two are already red at github-actions tip 9f7f4fa3 and #575 is the only open PR that fixes either — land it first. This PR merges cleanly regardless; the full seven-PR combined merge is conflict-free with all fourteen contracts passing.

On the fix. The failure chain is precisely stated and matches what I can reproduce from the code: pulls/N.base.sha is a cached pointer, refs/pull/N/merge is recomputed independently, they skew in both directions once the base advances, so requiring parents[0] === baseSha made a current merge ref read as stale → the scan analyzed the exact head → Advanced Security bound the PR's aggregate check to that branch snapshot → the slow language landed after it had already closed timed_out.

The property that keeps this from being a hole is that only the first parent gains a second acceptable value; parents[1] === headSha stays mandatory. Your self-test asserts that directly — analysisSnapshotForPullRequest(skewed, { parents: [sha('b'), sha('9')] }, sha('b')) still falls back to refs/pull/5/head. That's the case a weaker test suite would have skipped, and it's the one that matters.

The five surrounding cases cover the shapes that actually occur (live-tip parent, cached-pointer parent, unavailable tip, neither, malformed tip), and the malformed-tip case specifically pins that a bad value can never widen the guard. Good.

Validation is symmetric on both sides, which is what I checked next: /^[0-9a-f]{40,64}$/u on the SHA and /^[A-Za-z0-9._/-]+$/u on the branch name — in JS and as [[ "$base_ref" =~ ^[A-Za-z0-9._/-]+$ ]] in the shell — before either is interpolated into an API path. optionalBranchTipSha caches per branch so a batch sharing develop costs one extra read total, and it only swallows 404/409/422; any other error still throws rather than silently degrading to the head-ref path. That last detail is easy to get wrong in the "make it optional" direction and you didn't.

And the workflow-control-plane-contract.mjs assertion pins the jq shape .[1] == $head and (.[0] == $base or ($branch_base != "" and .[0] == $branch_base)), so "head parent still required" is contract-enforced rather than only tested.

No changes made. codeql-open-pr-backfill.mjs --self-test and workflow-control-plane-contract.mjs --self-test both pass at this head and merged into the tip.

Lopu · automated repository review · 0 open CodeQL alerts on 9b978184

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — ✅ validated, no changes requested.

$ node .github/scripts/codeql-open-pr-backfill.mjs --self-test        # OK
$ node .github/scripts/workflow-control-plane-contract.mjs --self-test # OK

The base-pointer skew diagnosis is right: pulls/N.base.sha is GitHub's cached
base pointer and refs/pull/N/merge is recomputed independently, so the two skew
in both directions once the base branch advances. Requiring
parents[0] === baseSha exactly meant a freshly recomputed merge ref read as
stale → the analyzer fell back to the exact head → Advanced Security bound the
PR's aggregate CodeQL check to that branch snapshot → the slow language landed
after it had already closed timed_out. Fixing it in both implementations
(the shell selector in codeql-analysis.yml and the JS one in
codeql-open-pr-backfill.mjs) is the right call — two implementations of one
rule have to move together, and the control-plane contract now pins that they do.

What I checked most carefully is that the widening stays tight, and it does:

  • parents[1] === headSha is still required, so an outdated merge ref is
    still rejected. Only the base side widens.
  • The live tip is shape-validated (/^[0-9a-f]{40,64}$/) before it can widen
    anything — a malformed tip degrades to the original cached-base-only behaviour
    instead of accepting whatever it was handed.
  • The branch name is validated (/^[A-Za-z0-9._/-]+$/) before reaching the ref
    lookup, so it can't be used to reach an arbitrary API path.
  • optionalBranchTipSha swallows only 404/409/422 and re-throws everything
    else, so a real API failure isn't silently converted into "no live tip".
  • Cached per branch — one extra read per distinct base branch, not per PR.

The self-test covers the two cases that matter most: a merge parented on neither
accepted base still reads stale, and a malformed tip never widens the guard.

💭 Minor, not worth changing now:
analysisSnapshotForPullRequest is at three positional params with a default. If
a fourth ever shows up, an options object would age better.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the change is right; one correction to the motivating case, and a complementary fix

The change is correct. pulls/N.base.sha really is a cached pointer that skews from the independently recomputed refs/pull/N/merge, and accepting the live base-branch tip as a second valid first parent is the right shape. I checked the details that matter: liveBaseSha is regex-validated before it can widen anything; parents[1] === headSha stays required so an outdated merge ref is still rejected; optionalBranchTipSha caches per base branch and rethrows anything that isn't 404/409/422; and the workflow screens base_ref before it reaches a URL. The six new self-test cases cover both skew directions, the unavailable tip, the malformed tip, match-neither, and wrong-head. workflow-control-plane-contract.mjs --self-test is green.

The correction. This wouldn't, on its own, have prevented the #557 timeout it cites. The numbers:

value SHA
refs/pull/557/merge first parent 814ebfdc (computed 04:25Z)
cached pulls/557.base.sha f31864b2
live develop tip at 11:38Z 59f7c5f8

The merge ref matched neither accepted base — GitHub hadn't recomputed it for seven hours while develop advanced. So the widened acceptance reduces how often the head-ref fallback is reached, but doesn't close it.

What actually closes it. The fallback fires for a mergeable PR whose own pull_request run is concurrently analyzing refs/pull/N/merge. In the scope step:

if [ "$base_has_pr_listener" = true ] \
   && [ -n "$merge_sha" ] \          # <-- wrong fact
   && [ "$BACKFILL_LISTENER_OWNED" != true ]; then
  analyze=false

"GitHub published a merge ref" and "that merge ref is current" are different facts, and the freshness check clears merge_sha for the second. Reading it as the first sends the dispatch down the exact-head path beside the pull_request run, splitting one head's two languages across two refs — which is what closed #557's aggregate check timed_out. I've fixed that in the trusted controller checkout by recording mergeable_pr separately and gating ownership on it; it'll arrive as its own PR against github-actions. Note the workflow already documents this exact hazard for the push path ("Confirm this push still owns the analysis") — it's the same hazard one event away, unguarded.

Verified you two coexist, rather than assuming it. Applying my patch on top of a synthesized merge already containing this PR:

  • codeql-analysis.ymlapplies cleanly, different parts of the step.
  • workflow-control-plane-contract.mjs — one trivial conflict: this PR replaces the assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u); line my patch anchors after. Keeping your three assertions and appending mine resolves it, and --self-test is green with both applied.

Nothing to change here. Note the two failing contract advisories on this head fail identically on github-actions@9f7f4fa3 — inherited, not caused by this PR; #584 fixes both.

@github-actions github-actions Bot added the lopu: overlapping files This PR changes files also changed by another open PR label Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — heads-up: #588 lands on the same lines, and the two compose

Follow-on to the earlier notes on this branch. Nothing here changes my read of #579 itself — the widened parent check is still correct, and parents[1] === headSha keeps the live-head requirement, so a genuinely stale merge ref (parented on an older base) still fails both accepted bases and falls back to the head. The new self-test covers that case directly.

What's new since those notes is #588, opened after them. It fixes the same #557 CodeQL timed_out incident from the other side, and it edits the same region of workflow-control-plane-contract.mjs.

Verified: they conflict textually, not semantically

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

codeql-analysis.yml merges cleanly — both workflow guards coexist without a conflict.

The only real overlap is one line. This PR deletes

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

and it has to, because this PR rewrites the jq expression that line pins. #588 keeps it, having no reason to know it was about to change. So the resolution is: keep #579's three assertions, drop that line, keep #588's three.

I applied that union in a scratch clone — node --check passes and workflow-control-plane-contract.mjs --self-test returns OK, with the merged workflow carrying base_branch_sha (this PR, lines 138–145 / 178–182) and mergeable_pr (#588, lines 159–204) side by side.

The two are complementary

This PR stops a current merge ref being misread as stale. #588 stops staleness deciding ownership in the first place. #588's is the stronger guarantee — it removes the freshness dependency entirely — but this one still prevents the unnecessary exact-head fallback that created the duplicate scan. Worth landing both; the cost is the one line above.

Full detail is on #588, including the live check-run timings that confirm the incident.

Posted by Lopu, Thingtime's automated principal reviewer.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the #588 collision has a mechanical resolution; I built it and it's green

Short follow-up to my earlier "#588 lands on the same lines" note, now with the merge actually run rather than predicted.

Replaying the batch into github-actions@9f7f4fa3 with git merge-tree, #579 merges clean — it lands after #565, #573, #574, #575 and #577 with no conflict, and the resulting 7-PR stack passes all 13 control-plane contracts plus the four node --test suites.

The only collision is #588 landing afterwards, and it is confined to one hunk of workflow-control-plane-contract.mjs at line 893. codeql-analysis.yml auto-merges cleanly.

Resolution: keep both sides, drop #588's re-add of the line this PR replaces — the jq expression it pins no longer exists after this PR. I built exactly that union and ran it:

workflow control-plane contract: self-test OK

And the merged scope block reads correctly: base_branch_sha (this PR) widens the freshness test, mergeable_pr (#588) takes the ownership decision off the freshness result. This PR makes the false-stale read rarer; #588 makes it stop mattering when the base advances between the two API reads and it happens anyway. Both are worth having.

One thing I checked because it is the usual way a pair like this goes wrong: neither PR's contract fails on the other's workflow change. This PR asserts the jq shape, #588 asserts the ownership if and the mergeable_pr=true assignment — disjoint surfaces, and the union self-test confirms it.

No changes to this branch; nothing here needs repair.

— Lopu, reviewing at 9b978184 against github-actions@9f7f4fa3 (merge base ac680d64). gh pr checks clean, CodeQL snapshot empty. codeql-open-pr-backfill.mjs --self-test: OK.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Lopu repository review — batch review of 16 open PRs.

Fixing this in both places that make the decision — the inline jq in codeql-analysis.yml and analysisSnapshotForPullRequest in codeql-open-pr-backfill.mjs — is the part that matters most. They share no code path, so pinning both in one contract is the only thing that keeps the analyzer scope and the backfill selector from drifting apart again.

The self-tests cover the cases that decide whether this is a fix or a hole: live-tip parent accepted, cached-pointer parent still accepted, unavailable tip falls back to the original behaviour, a merge ref matching neither still rejected, a malformed tip never widens the guard, and the live-head second parent stays required throughout. That last one is what stops this degrading into "accept any merge commit."

Ordering note against #588

These two fix opposite halves of the same #557 incident and collide. Verified:

  • git merge-tree conflicts in .github/scripts/workflow-control-plane-contract.mjs.
  • codeql-analysis.yml merges cleanly, which is the trap — fix(actions): Lopu repairs failed PR checks #588 retains assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u), which this PR's rewritten jq no longer satisfies. I built the merged tree and confirmed fix(actions): Lopu repairs failed PR checks #588's contract fails against it. Because that contract only runs in the non-blocking advisory lane, a union resolution would leave it dead and green.

This is the side to merge first — it's the one that retires that assertion — then rebase #588's mergeable_pr work on top. Full detail is on #588.

Validation: full blocking control-plane-ci.yml verify suite passes in this worktree; workflow-control-plane-contract.mjs --self-test OK.

No changes made — the only finding is the ordering constraint, which isn't a defect in this branch.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — correct fix, plus a merge-order hazard with #588

The diagnosis holds: pulls/N.base.sha is GitHub's cached base pointer and
refs/pull/N/merge is recomputed independently, so requiring the cached
pointer as the merge commit's only acceptable first parent makes a freshly
recomputed
ref read as stale whenever the base has advanced. Accepting the
live base-branch tip as a second valid parent is the right correction, and
keeping the live head parent required is what stops it being a loosening — the
"matches neither" and "wrong head parent" self-test cases pin exactly that.

optionalBranchTipSha caching per branch, validating the branch name before
the lookup, and swallowing only 404/409/422 while rethrowing everything else is
the right shape too.

Merge-order note. #588 fixes the same PR #557 incident from the other end
(it stops a cleared merge_sha from deciding listener ownership). The two
reinforce each other, but they do not merge cleanly as text — and the risky
half merges quietly:

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

This PR deletes the \.\[0\] == \$base and \.\[1\] == \$head assertion that
#588 keeps, so a naive resolution is wrong in either direction:

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

Verified resolution (real merge, then run): take this PR's three
replacement assertions, keep all of #588's mergeable_pr assertions, drop only
#588's retained old-jq line →

workflow control-plane contract: self-test OK

Also worth stating for anyone reading a base-tip diff of this branch: the
apparent deletion of .github/scripts/graphify-cas.mjs and its test is
merge-base drift, not part of this PR. That file was added on the base after
this branch was cut. Against the true merge base (ac680d64) this PR is three
files, +135/−7.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu review — no defects. This fixes the false "stale merge ref" verdict at its root, and the guards around the widening are right.

The core insight is correct and worth restating: pulls/N.base.sha is GitHub's cached base pointer while refs/pull/N/merge is recomputed independently, so the two skew in both directions once the base advances. Requiring the cached pointer alone made a perfectly current merge ref read as stale.

What I checked specifically, because widening an acceptance predicate is where this kind of change goes wrong:

  • The head parent stays required. Only parents[0] is widened; parents[1] === headSha is untouched, so an outdated merge ref is still rejected. Your mergeOf(sha("b")), parents[1] = sha("9") case pins it.
  • A malformed tip can't widen anything. liveBaseSha is only accepted against ^[0-9a-f]{40,64}$, and the workflow gates the lookup on [[ "$base_ref" =~ ^[A-Za-z0-9._/-]+$ ]] before interpolating a branch name into a URL. Both pinned by new contract assertions.
  • Fail-soft goes the safe direction. A failed tip lookup leaves base_branch_sha="", collapsing the predicate back to today's cached-base-only behaviour — never to a wider acceptance. The analysisSnapshotForPullRequest(skewed, mergeOf(sha("a")), "") case covers exactly this.
  • optionalBranchTipSha caches per branch, so a repository-wide backfill does one tip read per distinct base branch rather than one per PR. That matters here — this runs across every open PR.

One note, not an objection: the new workflow read uses a bare gh api … 2>/dev/null rather than a retry helper. codeql-analysis.yml has no gh_read_retry (all seven of its gh api calls are bare), so it's consistent with the file, and the failure mode is the fail-soft above rather than a silent widening. Fine as-is; worth knowing if that file ever grows a retry helper.

Mechanical conflict with #588 — both are wanted

#588 attacks the same incident from the other side: it separates "GitHub published a merge ref" from "the ref we captured is current", so ownership stays correct even when the ref genuinely is stale. Yours reduces how often it's wrongly called stale. Land both.

The conflict is in workflow-control-plane-contract.mjs only — you replace the assert.match(codeql, /\.\[0\] == \$base and \.\[1\] == \$head/u); one-liner, #588 keeps it and appends after it. Resolution: take your three replacements, drop the retained one-liner from #588's side, keep #588's five additions. codeql-analysis.yml auto-merges. I applied that in a scratch merge and the combined contract passes against the combined workflow.

Validation

codeql-open-pr-backfill.mjs --self-test ✅ (including all six new skew cases) · workflow-control-plane-contract.mjs ✅ · stage-graphify-snapshots.mjs ✅ · merges clean onto the tip ✅

Recommended batch order, validated end-to-end: #565#574#573#577#580#584#579#588.

Posted by Lopu, Thingtime's PR manager.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Lopu · heads-up on the #588 interaction (no change needed here)

This PR and #588 both repair the PR #557 CodeQL timed_out, from opposite sides — this one widens the merge-ref freshness check to accept the live base branch tip; #588 stops the ownership gate from reading a cleared merge_sha as "unmergeable". Complementary, and I would keep both.

They conflict, though. I ran the merges rather than guessing: git merge-tree conflicts in both orders, in .github/scripts/workflow-control-plane-contract.mjs. codeql-analysis.yml auto-merges cleanly.

The hazard is the resolution, not the conflict. This PR correctly replaces

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

with three stronger assertions, because the yml no longer carries that single-line spelling. #588 keeps that line untouched. Take both sides — the natural resolution — and the stale assertion survives against a yml that no longer matches it. I built the union tree and confirmed the contract fails there at line 893. Advisory-only, so it would not block anything; it would just warn forever about a property that is fully intact.

I fixed it on #588's branch, not this one — its line 893 now pins .[0] == $base and .[1] == $head as two separate invariant assertions that both spellings satisfy. The union of the two PRs is green in either merge order with that in place.

On this PR itself

No changes needed. A few things I checked rather than assumed:

  • optionalBranchTipSha caches per base branch, so a 200-PR sweep costs one lookup per distinct base rather than per PR.
  • It swallows only 404/409/422 and rethrows everything else, so a 500 cannot silently widen the guard.
  • Both sides validate the branch name before the lookup (^[A-Za-z0-9._/-]+$ in the workflow, the $branch_base != "" guard in the jq), and a malformed tip can never widen acceptance — there is a self-test for exactly that.
  • The six new backfill cases cover the right set: both accepted bases, the unavailable-tip fallback, neither-base rejection, malformed tip, and outdated-head rejection.

Contract self-test is green in this worktree. The two advisory contracts that do fail here (resolve-pr-conflicts-routing-contract.mjs at resolve-pr-conflicts.yml:733, and promotion-worker-routing-contract.mjs) fail identically on github-actions @ 9f7f4fa — pre-existing, unrelated to this PR, and fixed by #584.

Merge order: this one, then #588, taking both blocks in the contract conflict. Either direction works.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — this PR now has a position in the queue, and the ); resolution holds at the end of the batch

Short note; I am not re-treading the diagnosis or the fix, both of which I re-verified and found
correct at 9b978184. No failing check on this head (27 pass / 52 skipping / 0 fail), and the
CodeQL snapshot for it is empty.

The open question on this PR has been sequencing rather than correctness. I ran the whole
nine-PR controller batch and it resolves like this:

#584 → #588 → #580 → #577 → #574 → #573 → #565   all clean, no hand edits
#579                                             one hand resolution
#575                                             close as superseded by #584

#579 is the last step, and it is the only PR in the batch that needs a hand merge.

The ); recipe established on the #588 thread is correct, and I re-confirmed it in the harder
position — applied on top of seven already-merged PRs rather than pairwise against the bare
base. Keep both conflict hunks and insert one ); closing #588's last assertion before this PR's
block begins. Stripping only the markers still leaves the first side unterminated.

On the resulting tree I ran, and all passed:

  • node --check over every .github/scripts/*.mjs and bash -n over every *.sh — 0 failures
  • codeql-open-pr-backfill.mjs --self-test — OK (this PR's own fixtures, on the merged tree)
  • workflow-control-plane-contract.mjs --self-test — OK, i.e. both fix(actions): Lopu repairs failed PR checks #588's mergeable_pr
    lifecycle assertions and this PR's live-base-tip assertions hold simultaneously
  • all 11 advisory contracts — 0 failing
  • codeql-analysis.yml YAML parse + bash -n on both run: blocks — 0 failures

So the two fixes genuinely compose: #588 keeps mergeable_pr gating ownership, this PR keeps the
second accepted first parent, and neither side's assertions fail against the other's YAML. The
conflict really is adjacency and nothing more.

One thing worth saying plainly: the order does not avoid the conflict. Landing #579 before #588
leaves the identical conflict on the other side. Somebody resolves it once either way, and doing it
last means it is resolved against the tree that actually ships.

Lopu · principal PR and repository manager · batch review of 16 open PRs

@lopugit
lopugit merged commit 4919410 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
@github-actions github-actions Bot removed the lopu: overlapping files This PR changes files also changed by another open PR 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