fix(actions): Lopu repairs failed PR checks - #565
Conversation
|
These examples are warning-only. They do not fail a required check or block this PR.
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
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
|
|
Lopu review — head The failing check was GitHub, not this PR
Reran the failed job → attempt 2 is Nit for later, not worth touching the protected control plane over: the ladder only sleeps ~14s total, so it can't ride out a ~46s burst. If this recurs, widening The change itselfLoading the helper via Validated rather than eyeballed:
One thing I checked that isn't obviousThe product It doesn't. I ran the control copy against a copy of Which is also the case for merging this: 🤖 Lopu · automated repository review |
Lopu repository reviewLopu 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 #565 ·
|
|
Lopu review — head This PRReviewed against the merge base, not the base tip: I rebuilt the scenario from scratch — a scratch origin with a
Every claim in the description holds. Three details I checked that the description asserts and I wanted to confirm independently: Two observations, neither worth a commit: Checks are green, CodeQL has no open alerts on this head, and the earlier Not yours: two red contract advisoriesThe advisory comment above shows
Same failure mode both times: a controller change updated one deterministic contract and missed another that independently asserts overlapping shape. Both are protected One thing for the owner rather than for me to decide: — Lopu (Claude Opus 5) |
🤖 Lopu — third pass on
|
| ignore config | git add -A |
git add -u |
git add -A --force |
|---|---|---|---|
develop today |
works | works | — |
broad graphify-out/snapshots/ |
rc=1, prune skipped | works | works, but tracks the ignored ~27 MB graph.html |
--force would clear the error and start committing the very files the ignore rules exclude on purpose (snapshots/**/graph.html, .staging-*). -u is also just the semantically exact call: prune only ever deletes already-tracked files, so "update tracked paths only" is the operation, and it never considers an untracked path at all.
End-to-end after the change, against a scratch origin carrying the real router:
| ignore config | fn exit | prune committed | trees after | ignored graph.html tracked |
tree |
|---|---|---|---|---|---|
develop today |
0 | yes | 1 | 0 | clean |
broad graphify-out/snapshots/ |
0 | yes | 1 | 0 | clean |
Before the change the second row was exit 1 / not committed. YAML parses, bash -n clean across all 5 run: blocks, fail-soft path still returns non-zero and leaves the tree clean.
To be clear about severity: this is latent, not live. Today's develop ignores only sub-patterns (snapshots/**/graph.html, .staging-*), not the directory, so -A would have worked right now. But .gitignore is a product-branch file any PR can change, and the failure mode is a silent fail-soft — it would regress without a signal.
Stale numbers in the description, in your favour
The body cites main at 1 snapshot and develop at 3. Current state is 26 trees on both (develop 1,128,478,540 bytes; main 1,129,716,437) — the regrowth has already reached main. Not a defect in this PR; it just makes the case stronger.
Checks: 77 green, 0 failing. CodeQL: no open alerts on this head.
— Lopu (Claude Opus 5) 🌸
|
🌸 Lopu — batch review note (conflict-batch Reviewed and clean. I ran this end to end against the real
A 1,026 MiB reduction on a real branch, clean tree afterwards. For context, The So Also checked: the router is fetched from CodeQL — alert 93 ( assert.ok(block.includes(ADMIN_MODEL_ENDPOINT), `${label}: fetches Thingtime Admin endpoint`);
Housekeeping: this branch's merge-base is |
Lopu CodeQL triageLopu dismissed only findings that were proven inapplicable against the exact reviewed PR head:
Real findings are never dismissed: Lopu fixes their implementation and lets the next CodeQL scan mark them fixed. |
🌸 Lopu — fifth pass on
|
| tree count | commit | subject |
|---|---|---|
| 52 → 1 | 27e4c854 |
Merge PR #522 (the big cleanup) |
| 1 → 2 → 3 | 598ac039, 2ec6a3b3 |
Merge PRs #527, #535 from sync/main-into-develop |
| 3 → 1 | 505e7ad1 |
chore(lopu): apply repository review improvements |
| 1 → 26 | 96b352f8 |
Merge remote-tracking branch 'origin/main' into develop |
That last commit message is exactly what git merge --no-edit origin/main
produces. +25 trees in one commit, on the unguarded clean path this PR fixes.
Your diagnosis is confirmed against the real history, not just a fixture.
But main did not get its 26 from develop
The description's "Not changed" section argues promote-develop-to-main.yml
needs no equivalent step because "develop is the single choke point: if
develop stays bounded, every promotion carries exactly one snapshot to
main."
main went 1 → 26 at 4adda985 — Merge pull request #566 from
lopugit/lopu/feature-stack-ci-…-to-main:
4adda985^1 = fc5b14d3 1 tree (main, via PR #569)
4adda985^2 = b34043d3 26 trees ("Merge remote-tracking branch 'origin/main' into codex/repair-feature-s…")
4adda985 = 26 trees ← union
So main was re-bulked by a Feature Stack PR merged through the GitHub merge
API, from a branch that had itself unioned 26 trees via a branch-side
git merge origin/main. develop was not in that path at all.
There are therefore at least two more unbounded union paths this PR doesn't
guard: branch-side git merge origin/main in the rebase/resolver control plane,
and ordinary PR merges into main/develop. Merging this keeps develop
bounded going forward, but main stays at 26 trees / 1,077 MiB with no recovery
path from this change. That's an argument for landing it and scheduling the
rest — not against landing it.
The systemic cause: prune deletions are never staged
pruneSnapshots runs on every mutation (graphify-cas.mjs:944, :962, :986)
and deletes stale trees from the worktree. Nothing commits those deletions:
stage-graphify-snapshots.mjs::stageGraphifySnapshots enumerates files that
exist on disk and force-git adds them. Its complete set of git verbs is
add, add --force, restore, ls-files, diff, commit, config, init
— no git add -u, no git rm, no -A anywhere in the module.
Seen directly on a refresh commit:
$ git diff --name-status 387f4752 45bd623e -- graphify-out/snapshots/v1
5 A ← five additions, zero deletions
trees: parent=25 child=26
45bd623e is chore(graphify): refresh Feature Stack graph. It ran the router,
which pruned 24 trees off the disk — and then committed only the 5 new files.
That is why branches reach 26/49/52 trees despite prune running every time.
Which makes a nice point in your favour: the helper in this PR is the only
place in the control plane that gets this right, because it uses git add -u.
The one-verb change in 3a10b80d is the same insight the shared stager is
missing.
What I deliberately did not do
I did not ship a fix for the staging gap. It's a different file from the one
this PR touches, and stageGraphifySnapshots is called from
rebase-conflict-round/action.yml, resolve-pr-conflicts.yml and
rebase-pr-stacks.yml — staging deletions there changes what those commits
contain, and I can't exercise those integration paths from this session.
Shipping it unvalidated would be worse than reporting it. It wants its own PR
against github-actions, and it's the change that stops the regrowth at its
source rather than at one merge path.
Small correction while I'm here: the -A → -u rationale cites a broad
graphify-out/snapshots/ ignore rule, but main and develop don't currently
carry it — they ignore only snapshots/**/graph.html, .graphify_* and
.staging-*, so -A would work today. The change is still right: the stager's
own self-test constructs exactly that legacy branch shape, so the module
supports it, and -u is the exact verb for a delete-only operation. Correct
change, slightly over-stated justification.
Ship it. Then the stager.
🤖 Lopu · automated repository review · Claude Opus 5
|
Lopu — repository review The drift you're fixing is real and I measured it on I verified each load-bearing claim rather than trusting the comments
Validation: One observation, not a blockerThe bound is applied only on the clean-merge path; the conflict path hands off to the resolver and isn't covered. That reads as a deliberate scope choice, but it does mean a conflicted sync can still land an unbounded union. Worth a follow-up once #574 is in — #574 may well cover it from the staging side anyway. Verdict: approve. No changes needed. Merge alongside #574. |
🤖 Lopu review — approve; the trust boundary and the
|
🌸 Lopu — I verified the non-obvious choices in
|
|
🤖 Lopu review — ✅ validated, no changes requested. One open question for you. The diagnosis is correct and worth restating: snapshots are content-addressed per Three details I checked and agree with:
I also traced that ❓ Open question: the bound is applied on the clean-merge path only — the Non-fatal failure handling (
|
🌸 Lopu — the trust-boundary handling here is the part I'd have asked for if it were missingNo changes needed. Fetching Things I verified rather than assumed:
Two notes, neither blocking:
Coordination note. #574 is the complement: it makes prunes reach the commit at all, this re-bounds a merge result nothing had re-bounded. No file overlap. Verified they merge cleanly together and alongside #584/#573/#577/#580/#579, with all 15 control-plane contracts green on the combined tree. The one failing advisory on this head fails identically on |
|
Lopu repository review — batch review of 16 open PRs. Nice catch on the merge-shaped hole: content-addressed snapshots mean each side of a sync carries a differently named tree, so the merge unions them instead of conflicting, and nothing re-applied the bound to the result. The three decisions that could have gone wrong are all reasoned correctly:
The early This is the sibling of #574 — that one fixes retention never reaching the commit, this fixes it being undone by a merge. Different mechanisms, both real, and they merge cleanly. Worth landing together. Validation: full blocking FYI: No changes made. |
|
Lopu review — no defects found. Approving on the merits. The premise is right and easy to miss: Graphify snapshots are content-addressed per source fingerprint, so a main→develop merge unions two differently-named trees instead of conflicting, and nothing re-applied retention to the result. Re-pruning before the candidate SHA is published is the correct place. Three details I checked rather than assumed:
Fail-soft is contained too — Validation:
Recommended batch order for the nine Posted by Lopu, Thingtime's PR manager. |
Lopu · reading this diff, and the #574 pairingTwo-dot diff artifact — and this branch has the worst case of it. This PR forks furthest back of the nine ( git diff $(git merge-base github-actions HEAD)...HEAD
# .github/workflows/sync-main-into-develop.yml | 41 +++++++On the change. Graphify snapshots are content-addressed per source fingerprint, so each side of a main→develop sync carries a differently-named tree and the merge unions them rather than conflicting. Both sides were bounded when built; nothing re-applied the bound to the merge result — so develop drifted one ~45 MB snapshot past retention per distinct branch graph it absorbed. Silent, cumulative, invisible to every check. Things I verified rather than took on trust:
Pairs with #574. That PR fixes One follow-up, not a blocker: the helper only runs on the Queue position. Nine controller PRs are open against |
Lopu controller check repair
Lopu identified a failed PR check whose root cause is in the protected controller/workflow code.
Lopu controller fix — re-apply the Graphify snapshot bound after a main → develop sync
Diagnosis
sync-main-into-develop.ymlmergesmainintodevelopand pushes the resultstraight to
developwhen the merge is clean. That clean path never re-appliesthe Graphify snapshot retention bound, and that is the root cause of tracked
snapshot regrowth on
develop.Portable Graphify snapshots are content-addressed under
graphify-out/snapshots/v1/<source-fingerprint>/<artifact-hash>/. Two branchesthat each refreshed their own graph therefore hold snapshots at different
paths, not conflicting bytes at one path.
.gitattributesmarksgraphify-out/snapshots/** -merge, so the merge is expected to beconflict-free — and it is. Git simply unions both trees.
Each side was individually bounded to
DEFAULT_SNAPSHOT_RETENTION = 1when itsrouter built it (
scripts/graphify→pruneSnapshotsruns on every mutationand on
ensure). Nothing re-applies that bound to the merge result, sodevelopgains roughly one ~45 MB snapshot for every distinct branch graph itabsorbs.
Measured on PR #516 (
develop→main) at headdc052c6b:graphify-out/snapshotsmain@53c461c7(base)develop@dc052c6b(head)mainis still correct — it matches the state PR #522 established when it cut52 snapshots / 2.15 GiB down to 1 / 47 MiB.
develophas already drifted backto 3, and merging the standing promotion PR would have carried that drift onto
main. The snapshots entereddevelopthrough merge commits, not through thechore(graphify): refresh …commits that created them, which is the signatureof this exact gap.
The conflict path already rebuilds Graphify through the resolver, so only the
clean path was unguarded.
Change
.github/workflows/sync-main-into-develop.yml— one step touched, the existingid: mergestep. After a cleangit merge --no-edit origin/main, a newbound_graphify_snapshotshelper re-applies the bound before the step publishescandidate_sha.Design points:
candidate_shaiscomputed. The published SHA therefore always names the pruned commit, so the
clean push and the blocked-push fallback (which pushes
candidate_shatosync/main-into-develop) stay consistent. A later step could not have donethis without rewriting an already-published output.
git show origin/github-actions:.github/scripts/graphify-cas.mjsinto$RUNNER_TEMP. This job holds a push credential fordevelop, soproduct-branch code (
scripts/graphify-cas.mjs) must never execute in it.Reading from the protected branch keeps the same trust source the existing
"Check out the trusted sync merger" step uses, without writing a checkout into
the merged worktree — an in-tree checkout would have contaminated
computeSourceFingerprint, which hashes everything exceptgraphify-out.--depthon that fetch. The job checks out withfetch-depth: 0; ashallow fetch into an unshallow clone sets a shallow boundary that can break
the subsequent
git push origin develop.graphify-out/snapshots. Pruning also activates therouter's root compatibility aliases and takes a repository lock; those paths
are ignored on product branches, and the scoped
git addkeeps them out ofthe commit even on a legacy branch whose ignore rules differ.
retention is a tree-hygiene invariant; it must not be able to block
main→developsynchronization.commit keeps both parents and stays auditable.
Validation
All run locally in this session.
yaml.safe_loadon the edited workflow; jobsroute,sync.node .github/scripts/workflow-control-plane-contract.mjs --self-test→workflow control-plane contract: self-test OK.id: mergerunblock extracted from the parsed YAML passesbash -n.mainanddevelopeachcorrectly bounded at 1 snapshot with distinct source fingerprints, plus a
github-actionsbranch carrying the trusted router. A plain cleangit merge --no-edit origin/maintookdevelopfrom 1 → 2 trackedsnapshots with no conflict.
bound_graphify_snapshotsfunctionextracted verbatim from the edited workflow against that state returned
{"retention":1,"retained":1,"removed":1}and produced:git status --porcelainentries — no stray ignoredor untracked files staged or left behind)
git push origin developsucceeded;origin/developends at 1 snapshotrefs/heads/github-actionsremoved from thescratch origin, the same function under
set -euo pipefailemitted thewarning and the step still reached and published
candidate_sha.Not changed
promote-develop-to-main.ymlneeds no equivalent step. It merges through theGitHub API with no local tree to prune, and
developis the single choke point:if
developstays bounded, every promotion carries exactly one snapshot tomain.Source Lopu workflow run