Skip to content

fleet-refresh: keep conflicted bot PRs moving - #131

Merged
askalf merged 6 commits into
mainfrom
fleet-refresh
Oct 16, 2026
Merged

askalf merged 6 commits into
mainfrom
fleet-refresh

Conversation

@askalf

@askalf askalf commented Oct 16, 2026 •

Copy link
Copy Markdown
Owner

Why

#129 and #130 were opened two minutes apart. Both bumped the README's headline count and appended a row to the same table, so #130 conflicted with main the moment #129 merged. GitHub runs no pull_request or pull_request_review workflow on a PR with a merge conflict, so when Redline approved #130 ninety seconds later, neither Fleet status nor the review relay ran. fleet/review stayed at "Waiting on Redline" and the PR sat five hours until main was merged into it by hand.

What

.github/workflows/fleet-refresh.yml runs on the default branch after every push to main, every half hour, and on demand. It checks out only the default branch's scripts and never runs PR code.

scripts/fleet-refresh.mjs, for every open same-repo PR:

  • A conflicted PR by one of our identities on a bot branch (isBotPr; Dependabot excluded, it rebases its own and a push by us stops it) gets main merged into it in a scratch clone. Conflicts of the receipts shape are resolved: rows both sides added to the same Markdown table are kept, main's first; the headline count line takes main's number plus the PR's increment, and the PR's new names. Anything else is left alone and the PR gets one comment saying so.
  • The merge commit is pushed with the workflow token. Events from that token start no workflow runs, so the script dispatches self-test on the branch (the workflow now accepts workflow_dispatch) and dispatches Redline when the diff touches code (a docs-only PR is approved by the deterministic sweep). The dispatches are reconciled on every run, not fired once: each open PR whose head is a merge commit this script pushed gets whichever of those checks the head still lacks, so a dispatch that failed is retried next run.
  • Every open PR's lanes are re-posted via scripts/fleet-status.mjs, so a status that went stale for any reason is corrected on the next run.

Tests: scripts/fleet-refresh.test.mjs covers the resolver, a real git merge against a scratch bare remote (resolved, unresolvable, clean, stale head, dry run), and the CLI end to end against a stubbed GitHub, including the child fleet-status.mjs run and a push whose dispatch fails and is retried on the next run. fleet-status.test.mjs is updated for the dispatchable self-test and the new repo-only runner expression.

Notes

  • The merge commit carries no trailers, per the ai-attribution rule.
  • If a PAT or App token were ever added as a secret, the push would fire the normal PR events and the dispatch step would become redundant; it is harmless either way.

Two receipts PRs opened minutes apart both bump the README's headline count and
append a row to the same table, so the second conflicts with main as soon as the
first merges. GitHub runs no pull_request or pull_request_review workflow on a
conflicted PR, so fleet-status.yml never heard Redline's approval of #130, the
fleet/review status stayed at "Waiting on Redline", and the PR sat five hours
until main was merged into it by hand.

fleet-refresh.yml runs on the default branch after every push to main, every
half hour, and on demand. scripts/fleet-refresh.mjs merges main into each
conflicted bot PR of ours (askalf or github-actions on a bot branch; Dependabot
rebases its own), resolving the receipts shape: rows both sides added to the
same table are kept, main's first, and the headline count takes main's number
plus the PR's increment and the PR's new names. Any other conflict gets one
comment on the PR and is left alone. A push by the workflow token starts no
workflow run, so the script dispatches self-test (now dispatchable) on the
branch, Redline where the diff is code, and posts the fleet lanes itself; it
also re-posts the lanes on every open PR, so a stale status is corrected on the
next run. No PR code is executed: a head is fetched into a scratch clone and
merged as text.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: request changes. The merge resolver can leave the headline count inconsistent with the receipts table, and failed check dispatches are not retried after a successful push. The PR description also needs its tool credit lines removed.

1. Blocking: scripts/fleet-refresh.mjs:174

  try { resolved = resolveConflicts(readFileSync(join(dir, f), 'utf8')); } catch { resolved = null; }

If two PRs each add one receipt for projects already in the headline list, both change the count from 22 to 23 with identical names. Git merges that identical headline edit without conflict. The appended table rows conflict and this resolver keeps both, but only processes conflict hunks, leaving the headline at 23 for 24 receipts. The current integration fixture gives each side a different new project name, so it misses this case.

Suggested fix:

Reconcile the headline increment using the base and both branch versions even when the headline itself has no conflict markers. Add an integration test where both sides increment the count with unchanged project names.

2. Blocking: scripts/fleet-refresh.mjs:291

} catch (e) {
  console.error(`::error::#${n}: pushed ${res.sha.slice(0, 7)} but could not dispatch its checks: ${e.message}`);
  process.exitCode = 1;
}

If the push succeeds but a dispatch request fails, the new head has no required check run. On the next scheduled run, mergeable is true and line 251 skips dispatch entirely. Re-running the failed workflow therefore cannot recover the missing checks, leaving the PR blocked until someone dispatches them manually.

Suggested fix:

Retry missing check dispatches independently of whether the branch currently needs a merge, and test a successful push followed by a dispatch failure and a subsequent refresh.

3. Blocking: PR description:29

🤖 Generated with Claude Code

The description credits a tool as an author rather than the committer. The footer repeats that credit and links to a tool session.

Suggested fix:

Remove both tool credit lines and the session links from the PR description.

rule:none

A push that succeeded while its dispatch failed left the new head without the
required self-test check: the next run saw a mergeable PR and skipped it. Every
run now looks at each open PR whose head is a merge commit this script pushed
and dispatches whichever of self-test and Redline that head still lacks, so a
failed dispatch is retried next run. Redline is known by a workflow_dispatch run
titled with the head or by its verdict there; self-test by its check run.
Two PRs that each add a receipt for a project already named in the headline make
the same edit (22 to 23, same names). Git merges that cleanly while the rows
conflict, and the count ended one short. The headline is now recomputed from the
merge base and both sides in every file both changed: main's count plus the PR's
increment, main's names plus the PR's new ones.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: request changes. The headline reconciliation and dispatch retry paths address the earlier code findings, with regression tests for both. The merge path still crashes when both branches delete a file, and the unresolved-conflict comment contradicts the status refresh performed by this script.

1. Blocking: scripts/fleet-refresh.mjs:212

const reconciled = reconcileCountLine({ base: show(mergeBase, f), ours: show(headSha, f), theirs: show(baseSha, f), merged: readFileSync(join(dir, f), 'utf8') });

If main and the bot branch both delete other.txt while adding conflicting README receipts, Git accepts the shared deletion and the resolver resolves the README. However, both changedBy lists include other.txt, so this unconditional read throws ENOENT. The exception escapes refreshBranch and the CLI, preventing the merge push and skipping dispatch reconciliation and lane updates for all remaining PRs.

Suggested fix:

Restrict reconciliation to files that exist as regular files in the merged tree, handling shared deletions explicitly. Add an integration test with a shared deletion alongside a resolvable README conflict.

2. Blocking: scripts/fleet-refresh.mjs:254

+ 'Until the conflict is resolved by hand, GitHub runs no pull_request or review workflow here, so the fleet/* statuses cannot refresh.\n\n'

The comment tells maintainers that fleet statuses cannot refresh until manual conflict resolution, but the script proceeds to run fleet-status.mjs for this same unresolved PR and repeats that update on subsequent scheduled runs. The stated limitation is contradicted by the behavior this change adds.

Suggested fix:

Say that the branch needs manual conflict resolution without claiming that fleet statuses cannot refresh.

rule:none

A path both sides deleted is in both changed-file lists but not in the merged
tree, and reading it threw, which ended the run before any other PR was served.
The headline pass now looks only at regular files in the merged tree, and a
merge that throws fails the run for that PR alone. The comment on an
unresolvable conflict now says the conflict blocks merging and that Fleet
refresh keeps the statuses current itself, instead of claiming they cannot
refresh.
Both branches adding the same receipt kept its row once but summed both
increments, so the headline overcounted by one. The reconcile now subtracts the
rows both sides added that the merged table holds once, and declines the merge
when the count cannot be squared with the table.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: request changes. The earlier dispatch-retry, shared-deletion, and conflict-comment findings are addressed, with regression coverage for the code paths. The headline reconciliation still overcounts when a shared table row contains more than one fix.

1. Blocking: scripts/fleet-refresh.mjs:144

const shared = Math.max(0, rows(ours) + rows(theirs) - rows(base) - rows(merged));
const added = increment - shared;

This subtracts shared rows from a count of fixes, but a row can contain multiple fixes, as the existing Hugging Face and ioredis rows do. For example, starting at 22, both branches add the same row containing two fixes and each adds one distinct one-fix row, taking both headlines to 25. The conflicting table merge keeps the shared row once and both distinct rows. Here shared is 1 and added is 2, so reconciliation writes 27 instead of 26 and pushes the incorrect count.

Suggested fix:

Deduplicate and count receipt identities rather than assuming one fix per row, or decline automatic reconciliation when shared rows contain multiple receipts. Add an integration test with a shared two-fix row and a distinct receipt on each branch.

rule:none

A table row can hold several receipts, so a shared row counted as one fix while
the headline had counted each receipt. Overlap is now the set of pull request
links both sides added to rows, which the merged table holds once. The header
comment states the invariants the script keeps instead of retelling an incident.

@sprayberry-redline sprayberry-redline left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: approve. The refresh path preserves both branches' receipts, reconciles shared fixes by link, and retries missing dispatches independently of mergeability. The earlier findings are addressed, with regression tests for shared multi-fix rows, shared deletions, and dispatch failures after a successful push.

@askalf
askalf merged commit 5a42840 into main Oct 16, 2026
7 checks passed
@askalf
askalf deleted the fleet-refresh branch October 16, 2026 10:51
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.

2 participants