Skip to content

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

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

fix(actions): Lopu repairs failed PR checks#565
lopugit merged 2 commits into
github-actionsfrom
lopu/workflow-check-fix-33492956863

Conversation

@lopugit

@lopugit lopugit commented Sep 1, 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 — re-apply the Graphify snapshot bound after a main → develop sync

Diagnosis

sync-main-into-develop.yml merges main into develop and pushes the result
straight to develop when the merge is clean. That clean path never re-applies
the 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 branches
that each refreshed their own graph therefore hold snapshots at different
paths
, not conflicting bytes at one path. .gitattributes marks
graphify-out/snapshots/** -merge, so the merge is expected to be
conflict-free — and it is. Git simply unions both trees.

Each side was individually bounded to DEFAULT_SNAPSHOT_RETENTION = 1 when its
router built it (scripts/graphifypruneSnapshots runs on every mutation
and on ensure). Nothing re-applies that bound to the merge result, so
develop gains roughly one ~45 MB snapshot for every distinct branch graph it
absorbs.

Measured on PR #516 (developmain) at head dc052c6b:

Ref Snapshots Tracked bytes under graphify-out/snapshots
main @ 53c461c7 (base) 1 47,470,533
develop @ dc052c6b (head) 3 139,883,249

main is still correct — it matches the state PR #522 established when it cut
52 snapshots / 2.15 GiB down to 1 / 47 MiB. develop has already drifted back
to 3, and merging the standing promotion PR would have carried that drift onto
main. The snapshots entered develop through merge commits, not through the
chore(graphify): refresh … commits that created them, which is the signature
of 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 existing
id: merge step. After a clean git merge --no-edit origin/main, a new
bound_graphify_snapshots helper re-applies the bound before the step publishes
candidate_sha.

Design points:

  • The bound is applied inside the merge step, before candidate_sha is
    computed.
    The published SHA therefore always names the pruned commit, so the
    clean push and the blocked-push fallback (which pushes candidate_sha to
    sync/main-into-develop) stay consistent. A later step could not have done
    this without rewriting an already-published output.
  • Only the protected control-plane router runs. The helper is read with
    git show origin/github-actions:.github/scripts/graphify-cas.mjs into
    $RUNNER_TEMP. This job holds a push credential for develop, so
    product-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 except graphify-out.
  • No --depth on that fetch. The job checks out with fetch-depth: 0; a
    shallow fetch into an unshallow clone sets a shallow boundary that can break
    the subsequent git push origin develop.
  • Staging is scoped to graphify-out/snapshots. Pruning also activates the
    router's root compatibility aliases and takes a repository lock; those paths
    are ignored on product branches, and the scoped git add keeps them out of
    the commit even on a legacy branch whose ignore rules differ.
  • Fail-soft. A failure warns and lets the sync continue unpruned. Snapshot
    retention is a tree-hygiene invariant; it must not be able to block
    maindevelop synchronization.
  • The prune lands as a follow-on commit rather than an amend, so the merge
    commit keeps both parents and stays auditable.

Validation

All run locally in this session.

  1. YAML parsesyaml.safe_load on the edited workflow; jobs route, sync.
  2. Control-plane contractnode .github/scripts/workflow-control-plane-contract.mjs --self-testworkflow control-plane contract: self-test OK.
  3. Shell syntax — the id: merge run block extracted from the parsed YAML passes bash -n.
  4. Bug reproduced — built a scratch origin with main and develop each
    correctly bounded at 1 snapshot with distinct source fingerprints, plus a
    github-actions branch carrying the trusted router. A plain clean
    git merge --no-edit origin/main took develop from 1 → 2 tracked
    snapshots with no conflict.
  5. Fix verified end-to-end — running the bound_graphify_snapshots function
    extracted verbatim from the edited workflow against that state returned
    {"retention":1,"retained":1,"removed":1} and produced:
    • tracked snapshots back to 1
    • working tree clean (0 git status --porcelain entries — no stray ignored
      or untracked files staged or left behind)
    • merge commit preserved with both parents; prune recorded as a follow-on commit
    • git push origin develop succeeded; origin/develop ends at 1 snapshot
  6. Fail-soft verified — with refs/heads/github-actions removed from the
    scratch origin, the same function under set -euo pipefail emitted the
    warning and the step still reached and published candidate_sha.

Not changed

promote-develop-to-main.yml needs no equivalent step. It merges through the
GitHub API with no local tree to prune, and develop is the single choke point:
if develop stays bounded, every promotion carries exactly one snapshot to
main.

Source Lopu workflow run

@github-actions github-actions Bot mentioned this pull request Sep 1, 2026
@github-actions github-actions Bot added the lopu: mergeable The PR branches can currently be merged without conflicts label Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 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 1, 2026

Copy link
Copy Markdown
Contributor

Lopu review — head 75b1167f vs github-actions. Approving; no code change needed. The red check was an upstream outage, and I've repaired it by rerunning.

The failing check was GitHub, not this PR

control-plane / Find merge-conflicting PRs (job) died on gh: HTTP 502 after gh_read_retry exhausted its 2s/4s/8s ladder against a ~46s 502 window. Three things say upstream:

  1. This PR touches only sync-main-into-develop.yml; that job never loads it.
  2. The retry helper classified the 502 correctly — it just ran out of ladder.
  3. The same logical job passed at 11:41 in sibling run 33503719145, reading the same inventory including #565 itself.

Reran the failed job → attempt 2 is success, and the PR moved UNSTABLECLEAN.

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 gh_read_retry is the lever.

The change itself

Loading the helper via git show origin/github-actions:… rather than from the merged tree is the right call — this job holds a PAT, so running product-branch code here would be an escalation path. prune never re-enters invokeGraphify, so nothing repo-supplied executes. Also correct: every command in the function has an explicit || return 1, which is required here because if ! bound_graphify_snapshots suppresses set -e for the whole body.

Validated rather than eyeballed:

Check Result
Contract self-test on head, and on merge commit 858aac38 OK
Function body run verbatim on a repo seeded with two snapshot trees removed:1, trees 2 → 1, commit scoped to graphify-out/snapshots/…
No-op path (already at bound) removed:0, HEAD unchanged — no empty commit
Root aliases + .locks all IGNORED, stay out of the commit

One thing I checked that isn't obvious

The product scripts/graphify-cas.mjs and the control-plane .github/scripts/graphify-cas.mjs have diverged (control is a superset: chmodSync, normalizeGraphifyScopeArgs, --exclude). Since this PR deliberately runs the control copy against a product-written snapshot store, a schema drift would have made the whole fix a silent no-op on the branch it targets.

It doesn't. I ran the control copy against a copy of develop's real store:

{"retention":1,"retained":1,"removed":25}
graphify-out/snapshots: 1.1G → 51M   (26 trees → 1)

Which is also the case for merging this: develop is sitting on 26 snapshot trees / 1.1 GB against a bound of 1. Your "~45MB per distinct branch graph" estimate is spot on (41–51 MB each). Worth landing before the next promotion — details on #516.

🤖 Lopu · automated repository review

@github-actions

github-actions Bot commented Sep 1, 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 #565 · lopu/workflow-check-fix-33492956863github-actions

Compared 3a10b80d against github-actions @ 9f7f4fa3. One file:
.github/workflows/sync-main-into-develop.yml.

Check state

gh pr checks 565: 27 pass / 51 skipping / 0 fail, MERGEABLE / CLEAN. Nothing failing.
CodeQL snapshot for this head is empty; 565.json stays []. The one red advisory contract on
this tree (resolve-pr-conflicts-routing) is pre-existing on the base, fixed by #584.

What the PR does

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 they were built; nothing re-applied the bound to the merge result. This adds
a bound_graphify_snapshots helper that re-prunes after a clean merge and before the candidate SHA
is published.

Findings

No defects. The load-bearing choices, each checked:

  1. Control-plane code, not product-branch code. The helper fetches origin/github-actions and
    git shows graphify-cas.mjs out of it into $RUNNER_TEMP rather than executing the checked-out
    develop copy. That matters — this job holds SYNC_BRANCHES_PAT/CONFLICT_RESOLVER_PAT. I
    confirmed graphify-cas.mjs imports only node: builtins (crypto, fs, os, path,
    url, child_process), so extracting the single file is sufficient and it will not try to
    resolve a sibling out of $RUNNER_TEMP.
  2. resolveRepoRoot() uses process.cwd(), not the script's own location, so running the helper
    from $RUNNER_TEMP still prunes the develop checkout. Correct by construction, and the step's
    default working directory is that checkout.
  3. git add -u, not -A. Right for two independent reasons: prune only ever deletes
    already-tracked files, so -u is exactly the operation; and a branch carrying the broad
    graphify-out/snapshots/ ignore rule would make git add -A -- <ignored pathspec> exit non-zero,
    which would fail-soft the whole helper and silently skip the prune it exists to apply. -u never
    considers an untracked path at all, so activateSnapshot's ignored root aliases and lock/work
    state stay out of the commit.
  4. No --depth on the fetch, because the checkout is unshallow (fetch-depth: 0) and a shallow
    fetch would set a boundary that can break the develop push. Explicitly noted in the code.
  5. Ordering is right. The helper runs inside the clean-merge branch, before
    candidate_sha=$(git rev-parse 'HEAD^{commit}'), so the pruned commit is the one published and
    pushed. Git identity is configured at the top of the same step, so git commit will not fail for
    want of user.email.
  6. Fail-soft is the right severity. if ! bound_graphify_snapshots; then ::warning:: … fi — a
    failed prune should not block a main→develop sync. graphify-cas prune fails hard when no valid
    snapshot exists to retain, and that lands on this warning rather than on the sync.

Cross-PR interaction

Sole file; no overlap with any other PR in the batch, and clean in the whole-batch merge simulation.
Complementary to #574: #574 makes a prune stick inside the controller's own staging step, #565
re-applies the bound after a merge has unioned two snapshot trees. Both are needed — neither
subsumes the other.

Changes made

None.

Validation run

  • gh pr checks 565 / gh pr view 565.
  • sync-main-into-develop.yml YAML parse + bash -n on all 5 run: blocks → 0 syntax failures
    (this is a bash function definition nested inside a set -euo pipefail step, so syntax was the
    first thing worth proving).
  • Confirmed graphify-cas.mjs is self-contained (imports audited) and that its prune verb resolves
    the repo root from process.cwd().
  • workflow-control-plane-contract.mjs --self-test and promotion-worker-routing-contract.mjs → OK
    on this head.
  • Whole-batch merge simulation → clean.

View Lopu workflow run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Lopu review — head 75b1167f vs github-actions (ac680d64), merge base 1be2da36. Still approving; no change made on this branch. Second pass, this time reproducing the change end-to-end rather than reading it — plus two pre-existing control-plane defects found along the way that are not yours.

This PR

Reviewed against the merge base, not the base tip: git diff ac680d64..75b1167f looks like it deletes work in resolve-pr-conflicts.yml and feature-stack-progress.mjs, but that's only github-actions having moved ahead. The real change is one commit, one file, 30 added lines, zero deletions.

I rebuilt the scenario from scratch — a scratch origin with a github-actions branch carrying the trusted router, and main/develop each refreshed to exactly 1 snapshot at a distinct source fingerprint, using this repo's real .gitignore/.gitattributes — then ran bound_graphify_snapshots extracted verbatim from the edited workflow under the same set -euo pipefail:

Result
Bug reproduced clean merge, no conflict, snapshots 1 → 2
After the fix back to 1{"retention":1,"retained":1,"removed":1}
Working tree git status --porcelain empty
History merge commit keeps both parents; prune is a follow-on commit
candidate_sha names the prune commit, so the clean push and the blocked-push fallback agree
Push succeeded; origin/develop ends at 1
Idempotence second run → "removed":0, no empty commit
Fail-soft with refs/heads/github-actions gone, it warned and the step still published candidate_sha

Every claim in the description holds. Three details I checked that the description asserts and I wanted to confirm independently: graphify-cas.mjs imports only Node builtins, so running it from $RUNNER_TEMP is sound and resolveRepoRoot(process.cwd()) still finds the merged worktree; pruneSnapshots writes only under graphify-out/snapshots/v1/** and never touches the other tracked graphify path (cache/semantic-cas/), so the scoped git add covers exactly the mutated tracked files; and every root alias activateSnapshot creates is in .gitignore. Calling it as if ! bound_graphify_snapshots disables errexit for the function's whole dynamic extent, so the || return 1 on every command is load-bearing — all five are there.

Two observations, neither worth a commit: runs-on can resolve to a Vercel Sandbox runner where a missing node would silently degrade to the fail-soft path; and when develop is already up to date the helper can still push a standalone prune commit, which repairs pre-existing drift and is self-limiting.

Checks are green, CodeQL has no open alerts on this head, and the earlier gh: HTTP 502 on Find merge-conflicting PRs was indeed upstream — that job never loads the file you touched.

Not yours: two red contract advisories

The advisory comment above shows ⚠️ Conflict-resolver routing examples (exit 1). That is not caused by this PR, and there's a second one that only appears on the newer base:

  1. resolve-pr-conflicts-routing-contract.mjs requires every lopu-agent call to carry the legacy fallback credential slots. 51813961 added the verify_credential_vault live probe, which passes only thingtime-ci-router-secret. Bisected: parent 1905cd4d passes, 51813961 fails, and it has failed on every commit since. The workflow is rightlopu-credential-vault.mjs falls back to those legacy slots when the vault errors, so handing them to the probe would let the job that exists to verify the vault go green while the vault is down. 51813961 already carved this exception into workflow-control-plane-contract.mjs and just missed this sibling.
  2. promotion-worker-routing-contract.mjs pinned clause adjacency in the feature_stack_merge guard, so the legitimate && needs.feature_stack_plan.outputs.recovery != 'true' added by d5e984a0 broke it. Passes at 1be2da36, fails at ac680d64 — it landed after you branched.

Same failure mode both times: a controller change updated one deterministic contract and missed another that independently asserts overlapping shape. Both are protected .github/scripts/** files, so per controller policy I fixed them in the trusted checkout rather than on your branch — they'll arrive as their own PR against github-actions. Each fix is pinned by mutation tests (11 in total) proving the exemptions stay narrow: renaming verify_credential_vault, sneaking a second lopu-agent call into it, or dropping any required clause from the Feature Stack guard all still fail.

One thing for the owner rather than for me to decide: anthropic-api-key and anthropic-api-key-fallback are declared lopu-agent inputs that the action no longer reads at all — 0eff005b moved Anthropic credentials into the vault. The contract still requires four worker call sites to pass them. Whether those slots stay reserved for a future re-wire or get removed is a judgement call I left alone.

— Lopu (Claude Opus 5)

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu — third pass on 75b1167f, and this time I changed one thing

Still the right diagnosis and the right shape of fix. But my previous pass asserted that "the scoped git add covers exactly the mutated tracked files" — that holds on today's develop and not in general. One-word change made on this branch: git add -Agit add -u.

What the earlier pass missed

The last reproduction used "this repo's real .gitignore", which is exactly why it passed. stage-graphify-snapshots.mjs force-adds snapshot material specifically because "legacy product branches may still carry broad graphify-out/cache or graphify-out/snapshots ignore rules". On such a branch:

$ git add -A -- graphify-out/snapshots
The following paths are ignored by one of your .gitignore files:
graphify-out/snapshots
hint: Use -f if you really want to add them.
$ echo $?
1

That || return 1 fires before git commit. So the helper fail-softs, prints "syncing the merge unpruned", and silently skips the prune it exists to apply — reintroducing precisely the regrowth this PR fixes. Nothing is corrupted (the later steps only push SHAs, so the orphaned staged deletions die with the runner), but the fix becomes a no-op with only a warning to show for it.

Why -u and not --force

I measured all three against both ignore configurations, extracting bound_graphify_snapshots verbatim from the edited workflow:

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) 🌸

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — batch review note (conflict-batch 33508837483)

Reviewed and clean. I ran this end to end against the real develop, not a
fixture.
Throwaway --shared clone of f31864b, executing the exact helper
sequence:

node <helper from origin/github-actions> prune
  → {"active":"…/b01a0036…/69ef8143…","retention":1,"retained":1,"removed":25}   (0.5s, exit 0)
git diff --quiet -- graphify-out/snapshots   → exit 1 (deletions detected)
git add -u -- graphify-out/snapshots         → exit 0
git commit                                    → exit 0
before after
tracked snapshot files 130 5
tracked snapshot bytes 1,076.2 MiB 50.2 MiB
git status --porcelain empty

A 1,026 MiB reduction on a real branch, clean tree afterwards. For context,
main is at the same 26 trees / 1,077 MiB, and #485 alone would take develop to
34 trees / 1,409 MiB. Retention is 1.

The -u vs -A reasoning is the subtle part and it's correct — I reproduced
it
on a fixture carrying the broad graphify-out/snapshots/ ignore rule that
stage-graphify-snapshots.mjs force-adds around:

git add -A -- graphify-out/snapshots  →  exit 1  ("paths are ignored by one of your .gitignore files")
git add -u -- graphify-out/snapshots  →  exit 0, stages exactly the deletions

So -A really would have fail-softed the whole helper and silently skipped the
prune it exists to apply. Good comment, and it holds up.

Also checked: the router is fetched from origin/github-actions and run out of
$RUNNER_TEMP, never from the product branch — right call for a credentialed
job; the commit lands before candidate_sha is published; and
activateSnapshot()'s root-alias rewrites are gitignored on develop (only
graphify-out/.gitignore is tracked at that level), so nothing is left dirty for
a later clean-tree guard.


CodeQL — alert 93 (js/incomplete-url-substring-sanitization, high) on
.github/scripts/workflow-control-plane-contract.mjs:260. Not introduced here —
that file is untouched by this PR; CodeQL reports it against the head ref.

assert.ok(block.includes(ADMIN_MODEL_ENDPOINT), `${label}: fetches Thingtime Admin endpoint`);

block is a slice of workflow YAML source text read off disk, and
.includes() asserts the workflow file contains the Admin endpoint literal. No
URL is parsed, nothing is fetched, and no authorization or routing decision
derives from the result — the file only ever runs as
workflow-control-plane-contract.mjs --self-test from control-plane-ci.yml.
The rule's premise doesn't apply, so I recorded a false positive disposition
with that evidence rather than changing working code. No alert state was patched
from this session.


Housekeeping: this branch's merge-base is 1be2da3, not the current base tip
ac680d6, so its worktree still carries older resolve-pr-conflicts.yml /
rebase-pr-stacks.yml. Merge order is fine (I trial-merged all five controller
PRs onto ac680d6 in sequence — all clean, combined tree green on every contract
and every node --test suite), but a rebase before merge would put it on the
current control plane. Its one ⚠️ advisory is the pre-existing base-branch one
that #575 fixes.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Lopu CodeQL triage

Lopu dismissed only findings that were proven inapplicable against the exact reviewed PR head:

  • CodeQL alert #93false positive
    • Lopu evidence: workflow-control-plane-contract.mjs:260 is a control-plane self-test. block is workflow YAML source read from disk and includes() asserts that file contains the Admin endpoint literal; no URL is parsed and no security decision derives from it.

Real findings are never dismissed: Lopu fixes their implementation and lets the next CodeQL scan mark them fixed.

View Lopu workflow run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — fifth pass on 3a10b80d. Still approving, no change made.

Previous passes established that the fix works. I won't re-litigate that — I
re-ran the full end-to-end reproduction and it holds (bug 1 → 2 on a clean
merge, {"retention":1,"retained":1,"removed":1} after, empty
git status --porcelain, merge commit keeps both parents, candidate_sha names
the prune commit, fail-soft still publishes candidate_sha at exit 0). Checks
are green and CodeQL has zero open alerts on this head.

What's new this pass: I traced where the 26 trees actually came from. It
confirms your fix targets the real event — and it disproves the "Not changed"
section.

develop's regrowth was exactly the commit you're guarding

Counting distinct snapshot trees along the first-parent line:

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 4adda985Merge 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

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Lopu — repository review

The drift you're fixing is real and I measured it on develop today: DEFAULT_SNAPSHOT_RETENTION is 1, and graphify-out/snapshots/v1 carries 34 snapshot trees. That independently corroborates #574, and the two PRs fix different ends of the same leak rather than overlapping — #574 makes the router's prunes reach a commit, this re-applies the bound to a merge result that unioned two already-bounded sets.

I verified each load-bearing claim rather than trusting the comments

  • Executing the control plane's router, not the product branch's — fetching origin/github-actions and git show-ing the helper into $RUNNER_TEMP keeps product-branch code out of a credentialed job. Right posture, and stated explicitly. ✔
  • The helper is standalone-extractablegraphify-cas.mjs imports only node: builtins, no relative imports, so running it from $RUNNER_TEMP works. ✔
  • It operates on the right repositoryresolveRepoRoot(cwd = process.cwd()) shells to git rev-parse --show-toplevel, so a helper outside the worktree still resolves the develop checkout as root. ✔ Writing it to $RUNNER_TEMP also avoids creating the very gitlink fix(actions): Lopu repairs failed PR checks #576 is fixing. ✔
  • prune exists as a CLI subcommand routing to pruneSnapshotStore(). ✔
  • No --depth — correct, and the job already checks out fetch-depth: 0. ✔
  • Git identity is configured at lines 95–96 of the same step, before the new commit. ✔
  • git add -u rather than -A or --force — your reasoning holds and is worth keeping in the comment. On a branch carrying the broad graphify-out/snapshots/ ignore rule, git add -A on that pathspec exits 1 and would fail-soft the whole helper, silently skipping the prune it exists to apply; plain --force would clear the error but start tracking the derived graph.html and .staging-* scratch. -u never considers an untracked path at all. ✔
  • The git diff --quiet pre-check still works under ignore rules, since tracked files are diffed regardless of .gitignore. ✔
  • Ordering is right — the prune commit lands before candidate_sha is read, so the published SHA includes it. ✔
  • Fail-soft is appropriate — a missing valid snapshot warns and proceeds rather than blocking a main→develop sync on graph maintenance. ✔
  • activateSnapshot() refreshes the ignored root aliases; staging is scoped to graphify-out/snapshots and -u, so they can't leak into the commit. ✔

Validation: workflow-control-plane-contract, stage-graphify-snapshots, and promotion-worker-routing-contract self-tests all pass on this head; YAML parses. The one red advisory is pre-existing on the base and fixed by #575.

One observation, not a blocker

The 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.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — approve; the trust boundary and the -u choice are both right

The premise is measurable. 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. Nothing re-applied the bound afterwards. Today develop @ 7b6418bd tracks 1 411 MB across 1 014 graphify-out files, and a current PR head in this batch carries 27 distinct snapshot fingerprints where retention is 1. Together with #574 this is the accumulation path — both are needed.

Things I specifically verified rather than assumed:

  • Trust boundary. The helper is read from origin/github-actions via git show into $RUNNER_TEMP, never from the product branch being merged. This job holds SYNC_BRANCHES_PAT/CONFLICT_RESOLVER_PAT, so executing product-branch code here would be a privilege-escalation path. Code matches the comment. 👍
  • Root resolution. This was the failure mode I most expected — and it's handled: resolveRepoRoot() is git rev-parse --show-toplevel from process.cwd(), not from import.meta.url, so running the copy out of $RUNNER_TEMP still prunes the develop checkout. graphify-cas.mjs also imports only node: builtins, so it can't fail on a missing sibling module.
  • --depth deliberately omitted — correct, the checkout is fetch-depth: 0 and a shallow boundary can break the later git push origin develop.
  • git add -u reasoning is sound. -A would exit 1 against the broad graphify-out/snapshots/ ignore rule some branches carry — fail-softing the whole helper and silently skipping the prune it exists to apply — and --force would start tracking the derived graph.html and .staging-* scratch. Worth keeping that comment.
  • Errexit. Called as if ! bound_graphify_snapshots, so set -e is suspended for the function body and the intentional non-zero from git diff --quiet can't abort the step; every step that must stop carries its own || return 1.
  • Fail-soft posture is rightpruneSnapshotStore can legitimately fail ("No valid Graphify snapshot is available to retain") and a main → develop sync shouldn't be held hostage to that.

One thing for your eye (not blocking)

On the clean-merge path this now produces two commits, and candidate_sha becomes the retention commit rather than the merge commit. I traced the consumers — Push develop pushes HEAD, and the PR fallback only validates CANDIDATE_SHA against a 40-hex shape plus the unchanged live main/develop tips — so nothing assumes a two-parent candidate today. Just worth remembering if a future step ever inspects its parents.

Validation: applies cleanly with #573/#574/#575/#576/#577/#579 on ac680d64; on that combined tree all five contract self-tests pass, node --check over every .mjs and bash -n over every .sh are clean. The runtime path itself needs a real sync to exercise.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — I verified the non-obvious choices in bound_graphify_snapshots against the router. They all hold.

Merge order. The 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 seven-PR combined merge is conflict-free with all fourteen contracts passing, including #573's new global captured-stdout scan, which this PR's new shell function does not trip.

Reading note for anyone opening the Files tab: the two-dot diff looks much larger than the change, because graphify-cas.mjs and a snapshot swap landed on github-actions after this branch forked. The actual contribution is the 41-line helper.

What I checked against graphify-cas.mjs as it exists on github-actions:

  • prune exists and fails closed. runRouted dispatches it to pruneSnapshotStore, which fail()s with "No valid Graphify snapshot is available to retain" rather than pruning into an empty state. A failure returns 1, the caller emits ::warning::, and the sync proceeds unpruned. Fail-soft is the right posture for storage hygiene inside a sync that must not be blocked by it.
  • Git identity is configured earlier in the same step, so the git commit succeeds.
  • No --depth — correct, and the comment says why: this checkout is unshallow and a shallow fetch would set a boundary that can break the develop push.
  • git diff --quiet -- graphify-out/snapshots compares worktree against index, and nothing is staged at that point, so it correctly detects the unstaged deletions and short-circuits when prune removed nothing.
  • set -e interaction is safe — the helper is invoked as if ! bound_graphify_snapshots, which disables errexit inside the body, so the explicit || return 1 chain is doing the work. It's present on every step.

git add -u is the choice I'd defend hardest, because it looks arbitrary and isn't. git add -A exits 1 on the ignored pathspec for any branch carrying the broad graphify-out/snapshots/ ignore rule that stage-graphify-snapshots.mjs force-adds around — which would fail-soft the whole helper and silently skip the very prune it exists to apply. Plain --force clears that error but starts tracking the derived graph.html and .staging-* scratch the ignore rules exclude on purpose. -u never considers an untracked path at all, and since prune only ever deletes already-tracked files, it's exactly the operation. Worth keeping that comment if this is ever refactored.

One behavioural note, not a defect. pruneSnapshotStore selects selectSnapshot(root, currentFingerprint) ?? selectSnapshot(root). After a merge the source fingerprint has usually changed, so it activates the best available snapshot and retains that one — develop ends the sync holding a single snapshot that may not correspond to the merged source, until the router rebuilds. That's the documented "retain one active portable snapshot" contract in AI_ALL.md, and removed snapshots stay recoverable from Git history, so it's the intended trade. Stating it plainly so nobody reads it later as a bug.

Complementary to #574, which fixes the same accumulation from the staging side (prunes never reaching the commit at all). Independent; both needed.

No changes made.

Lopu · automated repository review · 0 open CodeQL alerts on 3a10b80d

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🤖 Lopu review — ✅ validated, no changes requested. One open question for you.

The diagnosis is correct and worth restating: snapshots are content-addressed per
source fingerprint, so each side of a sync carries a differently-named tree and
the merge unions them instead of conflicting. Both sides were bounded when
built, but nothing re-applied the bound afterwards — so develop drifts one
~45 MB snapshot past retention per distinct branch graph it absorbs. Silent,
cumulative, invisible to any per-branch check.

Three details I checked and agree with:

  • The pruner is fetched from origin/github-actions, not the merged tree.
    This job is credentialed; product-branch code must never execute in it. Right
    boundary.
  • No --depth on the fetch — this checkout is unshallow and a shallow fetch
    would set a boundary that can break the develop push. Real trap, easy to get
    wrong.
  • git add -u, not -A and not --force. -A would exit 1 on the ignored
    pathspec on branches carrying the broad graphify-out/snapshots/ ignore rule —
    fail-softing the whole helper and silently skipping the prune it exists to
    apply. --force would clear that but start tracking the derived graph.html /
    .staging-* scratch. -u sidesteps both.

I also traced that candidate_sha picks up the prune commit — git rev-parse 'HEAD^{commit}' runs after the helper, so the published candidate does include
the bound. ✅

❓ Open question: the bound is applied on the clean-merge path only — the
conflict path publishes without it. That's defensible if the conflict path has
its own resolution flow, but if a conflicted sync can also union snapshot trees,
the same drift would accumulate there unbounded and equally silently. I couldn't
establish from this diff alone whether the conflict path reaches publication with
an unbounded tree, so I've left it alone rather than guess — but it seems worth a
look.

Non-fatal failure handling (::warning:: and continue) is the right severity: an
unpruned sync beats a blocked main→develop sync.

workflow-control-plane-contract.mjs --self-test ✅ at this head. All checks green.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

🌸 Lopu — the trust-boundary handling here is the part I'd have asked for if it were missing

No changes needed. Fetching graphify-cas.mjs from origin/github-actions via git show instead of running the copy in the checked-out product tree is the right call and not an obvious one: this job is credentialed and pushes develop, so executing product-branch code here would be a privilege-escalation path. Good that it's explicit.

Things I verified rather than assumed:

  • resolveRepoRoot() is cwd-based (graphify-cas.mjs:167), so hosting the helper in $RUNNER_TEMP still targets the develop checkout it's invoked from — this would have been a silent no-op if root were derived from the script's own location.
  • prune is a real dispatched CLI verb (INTERNAL_COMMANDS, dispatch at graphify-cas.mjs:1074).
  • Ordering is right — the helper runs before candidate_sha="$(git rev-parse 'HEAD^{commit}')", so the published candidate includes the prune commit rather than pointing behind it.
  • Commit identity is configured at lines 95–96 of the same step, before the merge, so the extra commit is attributable.
  • git add -u over -A/--force is correct for the reasons given: prune only ever deletes tracked files, -A exits 1 on an ignored pathspec (which would fail-soft the whole helper and silently skip the prune it exists to apply), and plain --force would start tracking the derived snapshots/**/graph.html and .staging-* scratch.

Two notes, neither blocking:

  1. The conflict branch of git merge --no-edit origin/main is untouched, so a sync that resolves through the conflict path doesn't get the bound re-applied. Consistent with the PR's scope (the union-without-conflict case is the one that drifts), but worth a follow-up if conflicted syncs accumulate too.
  2. This branch is 13 commits behind base and predates graphify-cas.mjs landing on github-actions. That doesn't break anything — the helper reads the script from origin/github-actions at run time, where it now exists — but the two-dot diff currently looks like it deletes graphify-cas.mjs and a large graphify-out/ slice. It doesn't; those are base commits this branch hasn't absorbed. A refresh would make the diff read honestly.

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 github-actions@9f7f4fa3 — inherited; #584 fixes it.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

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. develop then drifts one snapshot past retention per distinct branch graph it absorbs.

The three decisions that could have gone wrong are all reasoned correctly:

  • Fetching the helper from the protected control plane (git show origin/github-actions:.github/scripts/graphify-cas.mjs) rather than executing it from the product branch. Right call in a credentialed job — that trust boundary is the whole point.
  • git add -u, not -A or --force. The comment earns this one: a branch carrying the broad graphify-out/snapshots/ ignore rule makes git add -A exit 1 on the ignored pathspec, which — because the helper is invoked fail-soft as if ! bound_graphify_snapshots — would have silently skipped the very prune it exists to apply. Plain --force would clear that but start tracking the derived graph.html and .staging-* scratch. -u never considers an untracked path at all, which is exactly the operation since prune only deletes already-tracked files.
  • No --depth on the fetch, since this checkout is unshallow and a shallow boundary can break the subsequent develop push.

The early git diff --quiet return avoids an empty commit, and degrading to a ::warning:: rather than blocking the sync is the right trade-off for a retention bound.

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 control-plane-ci.yml verify suite passes in this worktree; promotion-worker-routing-contract.mjs is green here too. Only PR touching this file.

FYI: resolve-pr-conflicts-routing-contract.mjs --self-test fails in this worktree, but it fails identically on the unmodified base at 9f7f4fa3 — pre-existing and unrelated to this file. #584 fixes it.

No changes made.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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:

  • Product-branch code never executes in the credentialed job. Fetching the helper from origin/github-actions instead of running it out of the merged tree is exactly right for a step holding SYNC_BRANCHES_PAT.
  • Relocating the helper to $RUNNER_TEMP is safe. graphify-cas.mjs imports only node: builtins, and its CLI resolves the repo via resolveRepoRoot()git rev-parse --show-toplevel on cwd, not on the script path. So the move can't change what it prunes.
  • git add -u is the right call and your reasoning holds: -A exits 1 against the broad graphify-out/snapshots/ ignore rule some branches carry, and --force would start tracking the derived graph.html/.staging-* those ignores exist to exclude.

Fail-soft is contained too — set -e is suspended inside a function called as an if ! condition, every command carries an explicit || return 1, and a mid-helper failure can only leave staged index entries, which git push origin develop (pushing HEAD) can't carry to the remote.

Validation: workflow-control-plane-contract ✅ · promotion-worker-routing-contract ✅ · stage-graphify-snapshots ✅ · merges clean onto the tip ✅

resolve-pr-conflicts-routing-contract fails in this worktree, but it fails identically on the unmodified github-actions tip (the credential-vault probe assertion, fixed by #584) — not caused by this PR.

Recommended batch order for the nine lopu/workflow-check-fix PRs, validated end-to-end in a scratch merge: #565#574#573#577#580#584#579#588, with #575 closed as superseded by #584. That sequence merges clean except for one mechanical conflict between #579 and #588 (contract file only), and with it resolved all five control-plane contracts pass — 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 · reading this diff, and the #574 pairing

Two-dot diff artifact — and this branch has the worst case of it. This PR forks furthest back of the nine (1be2da3), so GitHub's "Files changed" view shows graphify-cas.*, feature-stack-progress.mjs, CHANGELOG.md, a resolve-pr-conflicts.yml rewrite and ~1,500 lines of graph churn. None of that is in this PR. The real change is one file, +41/-0:

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:

  • The helper comes from the control plane, not the product branch. It fetches origin/github-actions and git shows graphify-cas.mjs out to $RUNNER_TEMP. This job is credentialed and pushes develop; running product-branch code in it would cross a privilege boundary. Most important line in the diff.
  • resolveRepoRoot() defaults to process.cwd() (graphify-cas.mjs:167), not the script's own location — so invoking from $RUNNER_TEMP against the develop checkout resolves the right repo. prune exists at line 1074.
  • Ordering — the helper runs before candidate_sha=$(git rev-parse ...), so the prune commit is inside the published candidate rather than orphaned after it.
  • git add -u, not -A or --force. The comment nails both halves and both are real: -A exits 1 on the ignored pathspec on any branch carrying the broad graphify-out/snapshots/ rule, which would fail-soft the whole helper and silently skip the prune it exists to apply; plain --force would clear that but start tracking the derived snapshots/**/graph.html (~27 MB) and .staging-* scratch.
  • No --depth on the fetch, for the stated and correct reason — this checkout is unshallow and a shallow boundary can break the develop push.

Pairs with #574. That PR fixes stage-graphify-snapshots.mjs so pruned trees are actually staged as deletions; without it a prune can be computed and still never reach a commit. Different files, they merge cleanly, and landing both is what makes retention real end to end.

One follow-up, not a blocker: the helper only runs on the git merge --no-edit origin/main success branch, so a sync that resolves through conflicts still drifts. Defensible — the conflict path hands off elsewhere — but worth a note. I did not widen a credentialed job's behaviour beyond what this PR set out to do.

Queue position. Nine controller PRs are open against github-actions; simulated cumulatively with the contract run after each merge, this one goes first and cleanly: 565 → 573 → 574 → 575 → 577 → 579 → 580 all clean and green. Conflicts start at #584 and between #579/#588.

@lopugit
lopugit merged commit ac89e1e into github-actions Sep 3, 2026
78 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