feat: add fast-forward repo sync #41

Merged
andres merged 5 commits from build/23-repo-sync into main 2026-08-31 18:42:40 +00:00

Closes #23

Summary

Add stoke repo sync for fast-forward-only updates of undiverged imported repositories, with race-safe tag handling, dry-run reporting, and environment-only Forgejo credentials.

Worklog

  • Claim #23, record the design, and verify the 117-test baseline
  • Add the branch fast-forward core through a failing integration test
  • Resolve the default branch and make repeat syncs explicit no-ops
  • Refuse diverged branches with both SHAs and the documented boundary
  • Create only new tags and report/refuse moved tags
  • Make --dry-run report exact changes without writes
  • Prove the token is absent from Git argv, remotes, output, and Git config
  • Document the verb, explicit non-goals, and add changelog.d/23.md
  • Reconcile source and destination ref races with exact reporting
  • Extend credential proof to encoded auth and live Git config values
  • Run the complete automated and manual scratch-import verification
  • Complete exact-head verification for the signal/ready handoff

Acceptance criteria

  • Syncing an undiverged imported repo advances the branch and (with --tags) tags, with the token visible nowhere in argv, remotes, or .git/config
  • A diverged branch is refused: non-zero exit, error names both SHAs, nothing is pushed
  • A moved upstream tag is skipped with a report and a non-zero exit; the forge tag is untouched
  • --dry-run reports the exact moves and provably writes nothing
  • README states what the verb does not do (diverged trees, pull-mirrors, releases)

Verification

  • npm test — 128 passed, 0 failed
  • npm run check:governance — 4 identities and 5 scope rows valid
  • node --test test/sync.test.js — 11 integration tests cover fast-forward, second-run no-op, default-branch resolution, divergence, tag movement, dry-run, source/destination races, and raw/Base64 credential leakage
  • TDD race proof — four new race tests failed against 04e6ba6, then all passed at 316bec5855b3d4f8f003df6f580b3d5cb4a34e38
  • Manual scratch import — advanced c23a620..c1c6b3f, reported the second run as current, then refused forge-only ac2ac6d against upstream c1c6b3f at exit 1 and preserved the forge head
  • npm pack --dry-run --jsonstoke@1.3.0, 8 files, 33,678 bytes
  • git diff --check origin/main...HEAD — clean
  • Internal re-review — no Critical, Important, or Minor findings at 316bec5855b3d4f8f003df6f580b3d5cb4a34e38
  • Forgejo checks — ci / test and labels / labels successful at the same head

Round log

Round at 316bec58

Round passed with no written reply.

Closes #23 ## Summary Add `stoke repo sync` for fast-forward-only updates of undiverged imported repositories, with race-safe tag handling, dry-run reporting, and environment-only Forgejo credentials. ## Worklog - [x] Claim #23, record the design, and verify the 117-test baseline - [x] Add the branch fast-forward core through a failing integration test - [x] Resolve the default branch and make repeat syncs explicit no-ops - [x] Refuse diverged branches with both SHAs and the documented boundary - [x] Create only new tags and report/refuse moved tags - [x] Make `--dry-run` report exact changes without writes - [x] Prove the token is absent from Git argv, remotes, output, and Git config - [x] Document the verb, explicit non-goals, and add `changelog.d/23.md` - [x] Reconcile source and destination ref races with exact reporting - [x] Extend credential proof to encoded auth and live Git config values - [x] Run the complete automated and manual scratch-import verification - [x] Complete exact-head verification for the signal/ready handoff ## Acceptance criteria - [x] Syncing an undiverged imported repo advances the branch and (with `--tags`) tags, with the token visible nowhere in argv, remotes, or `.git/config` - [x] A diverged branch is refused: non-zero exit, error names both SHAs, nothing is pushed - [x] A moved upstream tag is skipped with a report and a non-zero exit; the forge tag is untouched - [x] `--dry-run` reports the exact moves and provably writes nothing - [x] README states what the verb does not do (diverged trees, pull-mirrors, releases) ## Verification - `npm test` — 128 passed, 0 failed - `npm run check:governance` — 4 identities and 5 scope rows valid - `node --test test/sync.test.js` — 11 integration tests cover fast-forward, second-run no-op, default-branch resolution, divergence, tag movement, dry-run, source/destination races, and raw/Base64 credential leakage - TDD race proof — four new race tests failed against `04e6ba6`, then all passed at `316bec5855b3d4f8f003df6f580b3d5cb4a34e38` - Manual scratch import — advanced `c23a620..c1c6b3f`, reported the second run as current, then refused forge-only `ac2ac6d` against upstream `c1c6b3f` at exit 1 and preserved the forge head - `npm pack --dry-run --json` — `stoke@1.3.0`, 8 files, 33,678 bytes - `git diff --check origin/main...HEAD` — clean - Internal re-review — no Critical, Important, or Minor findings at `316bec5855b3d4f8f003df6f580b3d5cb4a34e38` - Forgejo checks — `ci / test` and `labels / labels` successful at the same head ## Round log <!-- round:316bec5855b3d4f8f003df6f580b3d5cb4a34e38 --> **Round at 316bec58** _Round passed with no written reply._
codex-bot-andresmgsl added 1 commit 2026-08-31 16:52:21 +00:00
feat: fast-forward repository branches
All checks were successful
labels / labels (pull_request) Successful in 11s
ci / test (pull_request) Successful in 16s
64b3d9df94
forgejo-actions added the
scope:cli
state:addressing
labels 2026-08-31 16:52:38 +00:00
codex-bot-andresmgsl changed title from feat: add fast-forward repo sync to WIP: feat: add fast-forward repo sync 2026-08-31 16:52:44 +00:00
codex-bot-andresmgsl added 1 commit 2026-08-31 16:54:44 +00:00
feat: resolve repository default branch
All checks were successful
labels / labels (pull_request) Successful in 10s
ci / test (pull_request) Successful in 15s
ea6c1a4fe9
forgejo-actions added
state:building
and removed
state:addressing
labels 2026-08-31 16:55:00 +00:00
codex-bot-andresmgsl added 1 commit 2026-08-31 16:57:11 +00:00
feat: sync safe tags and support dry runs
All checks were successful
labels / labels (pull_request) Successful in 16s
ci / test (pull_request) Successful in 30s
b21a1387a5
codex-bot-andresmgsl added 1 commit 2026-08-31 16:59:18 +00:00
docs: explain repository sync boundaries
All checks were successful
labels / labels (pull_request) Successful in 16s
ci / test (pull_request) Successful in 28s
04e6ba60e8
forgejo-actions added the
scope:docs
label 2026-08-31 17:01:58 +00:00
codex-bot-andresmgsl added 1 commit 2026-08-31 17:14:22 +00:00
fix: reconcile repository sync races
All checks were successful
labels / labels (pull_request) Successful in 10s
ci / test (pull_request) Successful in 15s
316bec5855
Author
Member

📣 round answered at head 316bec5855

📣 round answered at head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38
codex-bot-andresmgsl changed title from WIP: feat: add fast-forward repo sync to feat: add fast-forward repo sync 2026-08-31 17:19:53 +00:00
forgejo-actions added
blocker:unrequested
state:addressing
and removed
state:building
labels 2026-08-31 17:20:58 +00:00
codex-bot-andresmgsl requested review from glm-bot-andresmgsl 2026-08-31 18:22:26 +00:00
codex-bot-andresmgsl requested review from claude-bot-andresmgsl 2026-08-31 18:22:27 +00:00
codex-bot-andresmgsl requested review from kimi-bot-andresmgsl 2026-08-31 18:22:27 +00:00
codex-bot-andresmgsl added the
state:bots-reviewing
label 2026-08-31 18:22:28 +00:00
forgejo-actions removed the
blocker:unrequested
state:addressing
labels 2026-08-31 18:22:48 +00:00

🔎 reviewing head 316bec5855

  • npm test at head: 128 pass / 0 fail; check:governance clean
  • merged 523a455 (main, 1.4.0) into the head: clean merge, 128/128, changelog.d resolves to 23.md only
  • mutation-tested the failure cases; ran manual probes for annotated tags, dry-run + moved tag, empty-repo default branch, and credential scope
  • all 5 acceptance criteria settled; composing the verdict
🔎 reviewing head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38 - `npm test` at head: 128 pass / 0 fail; `check:governance` clean - merged 523a455 (main, 1.4.0) into the head: clean merge, 128/128, changelog.d resolves to 23.md only - mutation-tested the failure cases; ran manual probes for annotated tags, dry-run + moved tag, empty-repo default branch, and credential scope - all 5 acceptance criteria settled; composing the verdict

🔎 reviewing head 316bec5855

  • acceptance criteria 1–5 all verified against code + tests + README
  • npm test 128/128 pass (after npm ci in throwaway worktree); sync suite 11/11
  • check:governance pass; diff --check clean; commit status at head: success
  • credential safety confirmed: env-only GIT_CONFIG_* auth, ephemeral bare repo, no argv/remote/config token path
  • verdict: approve (composed, submitting)
🔎 reviewing head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38 - acceptance criteria 1–5 all verified against code + tests + README - npm test 128/128 pass (after npm ci in throwaway worktree); sync suite 11/11 - check:governance pass; diff --check clean; commit status at head: success - credential safety confirmed: env-only GIT_CONFIG_* auth, ephemeral bare repo, no argv/remote/config token path - verdict: approve (composed, submitting)

🔎 reviewing head 316bec5855

Review complete. Verified against issue #23 acceptance criteria:

  • Fast-forward-only branch advance and divergence refusal.
  • Default branch resolution, --tags handling, --dry-run no-write reporting.
  • Credential-safe gitAuthEnv() path; token kept out of argv/remotes/config.
  • README covers the verb and its explicit non-goals.
  • changelog.d/23.md records the change.

Could not run npm test locally (node unavailable in this box); relied on CI status (successful on this head) and code reading.

🔎 reviewing head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38 Review complete. Verified against issue #23 acceptance criteria: - Fast-forward-only branch advance and divergence refusal. - Default branch resolution, --tags handling, --dry-run no-write reporting. - Credential-safe gitAuthEnv() path; token kept out of argv/remotes/config. - README covers the verb and its explicit non-goals. - changelog.d/23.md records the change. Could not run npm test locally (node unavailable in this box); relied on CI status (successful on this head) and code reading.
glm-bot-andresmgsl approved these changes 2026-08-31 18:29:36 +00:00
glm-bot-andresmgsl left a comment
Member

Approve — at head 316bec5855, reviewed against issue #23.

Acceptance criteria — all verified by running, not reading:

  1. Fast-forward advance with credential safety — PASS. test/sync.test.js drives real git against bare forge/upstream fixtures: branch advances, --tags creates only new tags, and the credential test asserts the raw token and the Base64 credential appear in neither argv (logged via a git wrapper), nor stdout/stderr, nor the ephemeral repo config, nor the forge/upstream repo configs. gitAuthEnv() (src/cli.js:400) passes the token only through GIT_CONFIG_* env, and repo-sync.js works in an ephemeral bare repo deleted in finally — no persistent config exists to leak into.
  2. Divergence refusal — PASS. merge-base --is-ancestor gate; the test proves exit 1, both SHAs named, the out-of-scope statement, and the forge head unchanged. No --force flag exists anywhere in the surface.
  3. Moved tag — PASS. Test proves exit 1, report naming both SHAs on stderr, and the forge tag byte-identical afterwards; the push path is race-reconciled (a tag created mid-push is reclassified as moved, not overwritten — covered by an injected-race test).
  4. Dry run — PASS. Test proves the exact old..new branch line and the tag-create line print while the forge branch stays at oldSha and the tag is absent; both push sites are guarded by !dryRun/if (dryRun) continue.
  5. README non-goals — PASS. The repo sync section names all three exclusions (diverged trees → ceremony's UPSTREAM-SYNC.md, pull-mirrors, release mirroring) and the no-force guarantee.

What I ran: npm ci then npm test → 128/128 pass; node --test test/sync.test.js → 11/11; npm run check:governance → 4 identities, 5 scope rows valid; git diff --check origin/main...HEAD → clean; combined commit status at this head → success. Changelog line present (changelog.d/23.md). I am on this repo's panel (labels.conf panel=), so this verdict is a gate, not advisory.

Could not verify: the builder's live manual scratch-import against the production forge — I relied on the integration tests driving the real git binary against real bare repositories over file:// remotes (same code paths), plus the green forge CI at this exact head. The historical TDD race-proof at intermediate commits is also taken on the PR body's evidence; it does not affect the head's correctness.

Non-blocking nit (builder's discretion): if the forge branch is already ahead of upstream (contains it), the push-race reconciliation reports "Refusing diverged branch" — safe refusal with both SHAs, but the wording overstates divergence for the forge-ahead case.

Approve — at head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38, reviewed against issue #23. **Acceptance criteria — all verified by running, not reading:** 1. Fast-forward advance with credential safety — PASS. `test/sync.test.js` drives real `git` against bare forge/upstream fixtures: branch advances, `--tags` creates only new tags, and the credential test asserts the raw token and the Base64 credential appear in neither argv (logged via a git wrapper), nor stdout/stderr, nor the ephemeral repo config, nor the forge/upstream repo configs. `gitAuthEnv()` (src/cli.js:400) passes the token only through `GIT_CONFIG_*` env, and `repo-sync.js` works in an ephemeral bare repo deleted in `finally` — no persistent config exists to leak into. 2. Divergence refusal — PASS. `merge-base --is-ancestor` gate; the test proves exit 1, both SHAs named, the out-of-scope statement, and the forge head unchanged. No `--force` flag exists anywhere in the surface. 3. Moved tag — PASS. Test proves exit 1, report naming both SHAs on stderr, and the forge tag byte-identical afterwards; the push path is race-reconciled (a tag created mid-push is reclassified as moved, not overwritten — covered by an injected-race test). 4. Dry run — PASS. Test proves the exact `old..new` branch line and the tag-create line print while the forge branch stays at oldSha and the tag is absent; both push sites are guarded by `!dryRun`/`if (dryRun) continue`. 5. README non-goals — PASS. The `repo sync` section names all three exclusions (diverged trees → ceremony's UPSTREAM-SYNC.md, pull-mirrors, release mirroring) and the no-force guarantee. **What I ran:** `npm ci` then `npm test` → 128/128 pass; `node --test test/sync.test.js` → 11/11; `npm run check:governance` → 4 identities, 5 scope rows valid; `git diff --check origin/main...HEAD` → clean; combined commit status at this head → success. Changelog line present (`changelog.d/23.md`). I am on this repo's panel (`labels.conf` panel=), so this verdict is a gate, not advisory. **Could not verify:** the builder's live manual scratch-import against the production forge — I relied on the integration tests driving the real `git` binary against real bare repositories over file:// remotes (same code paths), plus the green forge CI at this exact head. The historical TDD race-proof at intermediate commits is also taken on the PR body's evidence; it does not affect the head's correctness. **Non-blocking nit (builder's discretion):** if the forge branch is already *ahead* of upstream (contains it), the push-race reconciliation reports "Refusing diverged branch" — safe refusal with both SHAs, but the wording overstates divergence for the forge-ahead case.
claude-bot-andresmgsl approved these changes 2026-08-31 18:32:06 +00:00
claude-bot-andresmgsl left a comment
Member

Approve at 316bec5855b3d4f8f003df6f580b3d5cb4a34e38. All five acceptance criteria on #23 are met, and every failure case I mutated is genuinely load-bearing. Nits below are non-blocking.

Acceptance criteria

# criterion how I settled it
1 ff advance + --tags, token nowhere npm test (128/128) plus a manual scratch fixture: bf71368..420672c advanced, annotated tag v1 created, grep TOKTOK forge/o/r.git/config → absent
2 diverged branch refused, both SHAs, nothing pushed repo sync refuses a diverged forge branch asserts exit 1, both SHAs, /Diverged trees are out of scope/, and forge main still at the forge-only SHA
3 moved tag skipped + reported + non-zero, forge tag untouched manual run exited 1, refs/tags/moved stayed at the old SHA while refs/tags/v1 was created in the same run
4 --dry-run reports exactly, writes nothing manual --tags --dry-run printed the branch move, the tag to create, and the moved-tag skip, exited 1, and left forge main at the old SHA with refs/tags/v1 absent
5 README states the non-goals README.md names diverged trees, pull-mirrors, and releases explicitly, and points at ceremony's docs/UPSTREAM-SYNC.md

Closes #23 is the right keyword here: #23 has no post-merge criterion, so nothing is left open behind the close.

What I ran

  • Full suite at head, in a detached worktree after npm ci: npm test128 pass / 0 fail; npm run check:governance → 4 identities, 5 scope rows.
  • Merged 523a455 (main at 1.4.0) into the head in a second throwaway worktree — the branch is behind the 1.4.0 release. Clean merge; changelog.d/ correctly resolves to 23.md alone (the release's deletions of 1/24/25/30.md survive); package.json stays at 1.4.0; 128/128 and governance clean on the merged tree. So the state a human would actually merge is green, not just the head in isolation.
  • Mutation tests — each failure case actually fails:
    • dry-run guard removed (if (!dryRun && oldSha !== newSha)if (oldSha !== newSha)) → 1 fail
    • moved-tag branch neutered (} else if (forgeSha !== tag.sha)else if (false)) → 1 fail
    • process.exitCode = 1 on moved tags removed → 2 fails
    • token spliced into the forge remote URL → 1 fail (the argv/config leak test earns its keep)
    • both divergence checks neutered → 2 fails
    • One nuance worth recording: removing only the pre-push merge-base --is-ancestor guard leaves all 11 sync tests green, because git's own non-fast-forward rejection then routes into the post-push race check, which raises the byte-identical divergenceError. The behaviour is covered twice over; the pre-push guard just isn't isolated by a test. Not a defect — the guard is what keeps criterion 2's "nothing is pushed" literally true rather than "rejected by the remote" — but a reader should not read those two tests as pinning that specific line.
  • Credential scope probe (beyond the suite, which uses file:// remotes and so never exercises http.*.extraHeader at all): two local HTTP listeners, GIT_CONFIG_KEY_0=http://127.0.0.1:8081.extraHeader. The forge origin received Authorization: Basic ...; a different-host upstream on :8082 received AUTH=None. So syncing a GitHub upstream does not hand the Forgejo token to GitHub. This is the one security question the AC's wording ("argv, remotes, .git/config") does not reach, and it comes out right.
  • Manual behaviour probes: annotated upstream tag lands on the forge as a real tag object (cat-file -ttag, message preserved) rather than a dereferenced commit; a second run reports main is up to date at <sha>; empty forge repo with no --branch gives Could not resolve the forge repository default branch; nonexistent --branch surfaces git's own couldn't find remote ref; no stoke-repo-sync-* directories survive in /tmp.
  • Forgejo checks at this head: ci / test and labels / labels both success.

What I could not verify

  • No HTTPS forge round trip. Every fixture and probe used file:// remotes, so the real push against forgejo.heavyduty.builders — auth handshake, ref-update permissions, tag-creation policy — was not exercised end to end by me. I relied on the builder's manual scratch-import record in the PR body (c23a620..c1c6b3f, then the exit-1 refusal of forge-only ac2ac6d) plus my own header-scope probe above.
  • No real GitHub upstream. The upstream side was always a local bare repo, so I did not observe GitHub's ref-advertisement behaviour or a large real tag namespace.

Nits — non-blocking, your call

  1. One git fetch per upstream tag. syncRepository loops fetch refs/tags/<name>:refs/stoke/upstream-tags/<name> and then rev-parse for every tag. I measured a 42-tag upstream: 84 tag-related git invocations, 92 total, and this runs under --dry-run too. A single fetch --no-tags <url> 'refs/tags/*:refs/stoke/upstream-tags/*' followed by one for-each-ref is one round trip instead of N, and it is a more consistent snapshot, not less — the per-tag loop widens the window your own source-race test covers rather than narrowing it. Against a real HTTPS upstream with a few hundred tags the difference is minutes.
  2. --dry-run output is byte-identical to a real run. syncRepository returns dryRun in its result and src/cli.js never reads it, so a log or a CI transcript cannot tell a rehearsal from a push. A (dry run) suffix on the branch line would make dryRun earn its place in the return value.
  3. syncRepository reassigns its destructured branch parameter when resolving the default. A separate const resolvedBranch reads more clearly and keeps the returned branch obviously the same value the pushes used.

Correctness of the race handling reads right to me on close inspection: the branch retry re-fetches and re-checks ancestry before pushing again, a non-conflict push failure falls through to a second push that throws git's real error rather than swallowing it, and the tag path only reclassifies as moved after confirming the destination ref actually exists at a different SHA. The four race tests are real tests — they drive the collision through a PATH git wrapper rather than asserting on a mock.

**Approve** at `316bec5855b3d4f8f003df6f580b3d5cb4a34e38`. All five acceptance criteria on #23 are met, and every failure case I mutated is genuinely load-bearing. Nits below are non-blocking. ## Acceptance criteria | # | criterion | how I settled it | |---|---|---| | 1 | ff advance + `--tags`, token nowhere | `npm test` (128/128) plus a manual scratch fixture: `bf71368..420672c` advanced, annotated tag `v1` created, `grep TOKTOK forge/o/r.git/config` → absent | | 2 | diverged branch refused, both SHAs, nothing pushed | `repo sync refuses a diverged forge branch` asserts exit 1, both SHAs, `/Diverged trees are out of scope/`, and forge `main` still at the forge-only SHA | | 3 | moved tag skipped + reported + non-zero, forge tag untouched | manual run exited 1, `refs/tags/moved` stayed at the old SHA while `refs/tags/v1` was created in the same run | | 4 | `--dry-run` reports exactly, writes nothing | manual `--tags --dry-run` printed the branch move, the tag to create, and the moved-tag skip, exited 1, and left forge `main` at the old SHA with `refs/tags/v1` absent | | 5 | README states the non-goals | `README.md` names diverged trees, pull-mirrors, and releases explicitly, and points at ceremony's `docs/UPSTREAM-SYNC.md` | `Closes #23` is the right keyword here: #23 has no post-merge criterion, so nothing is left open behind the close. ## What I ran - **Full suite at head**, in a detached worktree after `npm ci`: `npm test` → **128 pass / 0 fail**; `npm run check:governance` → 4 identities, 5 scope rows. - **Merged `523a455` (main at 1.4.0) into the head** in a second throwaway worktree — the branch is behind the 1.4.0 release. Clean merge; `changelog.d/` correctly resolves to `23.md` alone (the release's deletions of `1/24/25/30.md` survive); `package.json` stays at `1.4.0`; **128/128** and governance clean on the merged tree. So the state a human would actually merge is green, not just the head in isolation. - **Mutation tests** — each failure case actually fails: - dry-run guard removed (`if (!dryRun && oldSha !== newSha)` → `if (oldSha !== newSha)`) → 1 fail - moved-tag branch neutered (`} else if (forgeSha !== tag.sha)` → `else if (false)`) → 1 fail - `process.exitCode = 1` on moved tags removed → 2 fails - token spliced into the forge remote URL → 1 fail (the argv/config leak test earns its keep) - **both** divergence checks neutered → 2 fails - One nuance worth recording: removing **only** the pre-push `merge-base --is-ancestor` guard leaves all 11 sync tests green, because git's own non-fast-forward rejection then routes into the post-push race check, which raises the byte-identical `divergenceError`. The *behaviour* is covered twice over; the pre-push guard just isn't isolated by a test. Not a defect — the guard is what keeps criterion 2's "nothing is pushed" literally true rather than "rejected by the remote" — but a reader should not read those two tests as pinning that specific line. - **Credential scope probe** (beyond the suite, which uses `file://` remotes and so never exercises `http.*.extraHeader` at all): two local HTTP listeners, `GIT_CONFIG_KEY_0=http://127.0.0.1:8081.extraHeader`. The forge origin received `Authorization: Basic ...`; a different-host upstream on `:8082` received `AUTH=None`. So syncing a GitHub upstream does not hand the Forgejo token to GitHub. This is the one security question the AC's wording ("argv, remotes, `.git/config`") does not reach, and it comes out right. - **Manual behaviour probes**: annotated upstream tag lands on the forge as a real tag object (`cat-file -t` → `tag`, message preserved) rather than a dereferenced commit; a second run reports `main is up to date at <sha>`; empty forge repo with no `--branch` gives `Could not resolve the forge repository default branch`; nonexistent `--branch` surfaces git's own `couldn't find remote ref`; no `stoke-repo-sync-*` directories survive in `/tmp`. - **Forgejo checks at this head**: `ci / test` and `labels / labels` both `success`. ## What I could not verify - **No HTTPS forge round trip.** Every fixture and probe used `file://` remotes, so the real push against `forgejo.heavyduty.builders` — auth handshake, ref-update permissions, tag-creation policy — was not exercised end to end by me. I relied on the builder's manual scratch-import record in the PR body (`c23a620..c1c6b3f`, then the exit-1 refusal of forge-only `ac2ac6d`) plus my own header-scope probe above. - **No real GitHub upstream.** The upstream side was always a local bare repo, so I did not observe GitHub's ref-advertisement behaviour or a large real tag namespace. ## Nits — non-blocking, your call 1. **One `git fetch` per upstream tag.** `syncRepository` loops `fetch refs/tags/<name>:refs/stoke/upstream-tags/<name>` and then `rev-parse` for every tag. I measured a 42-tag upstream: 84 tag-related git invocations, 92 total, and this runs under `--dry-run` too. A single `fetch --no-tags <url> 'refs/tags/*:refs/stoke/upstream-tags/*'` followed by one `for-each-ref` is one round trip instead of N, and it is a *more* consistent snapshot, not less — the per-tag loop widens the window your own source-race test covers rather than narrowing it. Against a real HTTPS upstream with a few hundred tags the difference is minutes. 2. **`--dry-run` output is byte-identical to a real run.** `syncRepository` returns `dryRun` in its result and `src/cli.js` never reads it, so a log or a CI transcript cannot tell a rehearsal from a push. A `(dry run)` suffix on the branch line would make `dryRun` earn its place in the return value. 3. **`syncRepository` reassigns its destructured `branch` parameter** when resolving the default. A separate `const resolvedBranch` reads more clearly and keeps the returned `branch` obviously the same value the pushes used. Correctness of the race handling reads right to me on close inspection: the branch retry re-fetches and re-checks ancestry before pushing again, a non-conflict push failure falls through to a second push that throws git's real error rather than swallowing it, and the tag path only reclassifies as `moved` after confirming the destination ref actually exists at a different SHA. The four race tests are real tests — they drive the collision through a `PATH` git wrapper rather than asserting on a mock.
kimi-bot-andresmgsl approved these changes 2026-08-31 18:32:14 +00:00
kimi-bot-andresmgsl left a comment
Member

Approved.

Reviewed against issue #23 acceptance criteria:

  • stoke repo sync is wired in src/cli.js and delegates to src/repo-sync.js.
  • Fast-forward-only: merge-base --is-ancestor guards the push; divergence refusal names both SHAs and references ceremony's docs/UPSTREAM-SYNC.md.
  • Default branch is resolved from the forge repo's symbolic HEAD when --branch is omitted.
  • --tags creates new upstream tags and reports/skips moved tags with a non-zero exit.
  • --dry-run reports branch and tag moves without writing.
  • Credentials flow through gitAuthEnv(); the token stays out of argv, remotes, and .git/config.
  • README documents the verb and its explicit non-goals (diverged trees, pull-mirrors, releases).
  • changelog.d/23.md records the change.

Verification notes:

  • I could not run npm test or npm run check:governance locally because Node.js is not installed in this review box. I relied on the CI status, which reports the test job as successful on head 316bec5855b3d4f8f003df6f580b3d5cb4a34e38, and on reading the test suite (test/sync.test.js — 11 integration tests covering ff advance, default-branch resolution, no-op, divergence, tag movement, dry-run, source/destination races, and credential leakage).
  • I verified the three-way merge result with current main is clean and produces the expected tree (CHANGELOG.md plus changelog.d/23.md only); the stale pre-1.4.0 base does not re-introduce consumed fragments.

No functional blockers.

Approved. Reviewed against issue #23 acceptance criteria: - `stoke repo sync` is wired in `src/cli.js` and delegates to `src/repo-sync.js`. - Fast-forward-only: `merge-base --is-ancestor` guards the push; divergence refusal names both SHAs and references ceremony's `docs/UPSTREAM-SYNC.md`. - Default branch is resolved from the forge repo's symbolic HEAD when `--branch` is omitted. - `--tags` creates new upstream tags and reports/skips moved tags with a non-zero exit. - `--dry-run` reports branch and tag moves without writing. - Credentials flow through `gitAuthEnv()`; the token stays out of argv, remotes, and `.git/config`. - README documents the verb and its explicit non-goals (diverged trees, pull-mirrors, releases). - `changelog.d/23.md` records the change. Verification notes: - I could not run `npm test` or `npm run check:governance` locally because Node.js is not installed in this review box. I relied on the CI status, which reports the test job as successful on head `316bec5855b3d4f8f003df6f580b3d5cb4a34e38`, and on reading the test suite (`test/sync.test.js` — 11 integration tests covering ff advance, default-branch resolution, no-op, divergence, tag movement, dry-run, source/destination races, and credential leakage). - I verified the three-way merge result with current `main` is clean and produces the expected tree (`CHANGELOG.md` plus `changelog.d/23.md` only); the stale pre-1.4.0 base does not re-introduce consumed fragments. No functional blockers.
forgejo-actions added
state:needs-human
and removed
state:bots-reviewing
labels 2026-08-31 18:34:13 +00:00
andres merged commit c34a8b04d2 into main 2026-08-31 18:42:40 +00:00

Triage, 2026-08-31T18:42Z — the sweep's release-shaped warning on this PR is a FALSE POSITIVE. Do not apply the release label.

Every sweep since 18:22Z has logged:

::warning::labels: #41 is release-shaped (version 1.4.0 -> 1.3.0 at its head) but carries no release label — the merge door reads that label as declared intent and will refuse without it

Tick 47's standing rule is to read those warnings as a work queue and apply release after verifying — that is exactly what happened on !40. Verified here, and the verdict is the opposite: this one must not be actioned.

What was measured

  • This PR does not touch package.json at all. Its diff is README.md, changelog.d/23.md, src/cli.js, src/repo-sync.js, test/sync.test.js — five files, none of them the manifest.
  • The head reads 1.3.0 only because the branch point predates the release: true merge-base is fb5cb474 (package.json 1.3.0), while main moved to 1.4.0 in !40 (3f943cf9) at 16:56:50Z, four minutes after this PR was opened.
  • The merge result is 1.4.0, not 1.3.0 — proven, not assumed: git merge-tree --write-tree origin/main 316bec58 exits 0 (no conflict) and the resulting tree f3c228d1 carries package.json 1.4.0, CHANGELOG.md, and exactly one fragment, changelog.d/23.md.

So there is no downgrade to declare, and release on a feature PR would assert an intent that does not exist.

The defect this exposes

The release-shape detector compares the PR head's manifest version against the base branch head's, without asking whether the PR's own diff changes that file. Any branch cut before a release merge therefore manufactures a phantom "downgrade" and solicits a label that would be wrong. Recorded here rather than filed as an eighth defect on #36 — that issue is claimed and mid-build, and widening its scope under the builder is what made its own criteria unmeetable three times over. It will be filed separately once !42 lands.

Optional, not required: updating this branch from main clears the warning at the source, since the head would then carry 1.4.0. @claude-bot-andresmgsl already test-merged main into this head cleanly at 18:25Z (128/128, comment 30915), so it is a safe no-op for the review — the panel's three approvals are at 316bec58 and a merge commit would reset them, so this is a choice for the builder and @andres, not a triage ask. Merging as-is is correct and safe.

Unchanged: the tag instruction on #32 still stands

changelog.d/23.md riding into the merged tree is the visible marker of the hazard already recorded on #32 (comment 30873): tag v1.4.0 at 523a45587f12298ded00a988318a2c0ce25d2f85, not at main's head after this PR lands. Tagging the post-merge head would ship repo sync inside a 1.4.0 whose CHANGELOG.md never mentions it, while package.json would still read 1.4.0 — nothing in the repo would catch it. No labels were changed on this PR.

— triage (@claude-bot-andresmgsl)

**Triage, 2026-08-31T18:42Z — the sweep's `release`-shaped warning on this PR is a FALSE POSITIVE. Do not apply the `release` label.** Every sweep since 18:22Z has logged: > `::warning::labels: #41 is release-shaped (version 1.4.0 -> 1.3.0 at its head) but carries no release label — the merge door reads that label as declared intent and will refuse without it` Tick 47's standing rule is to read those warnings as a work queue and apply `release` after verifying — that is exactly what happened on !40. **Verified here, and the verdict is the opposite: this one must not be actioned.** ## What was measured - **This PR does not touch `package.json` at all.** Its diff is `README.md`, `changelog.d/23.md`, `src/cli.js`, `src/repo-sync.js`, `test/sync.test.js` — five files, none of them the manifest. - The head reads `1.3.0` only because the branch point predates the release: true merge-base is **`fb5cb474`** (`package.json` `1.3.0`), while `main` moved to `1.4.0` in !40 (`3f943cf9`) at 16:56:50Z, four minutes after this PR was opened. - **The merge result is `1.4.0`, not `1.3.0`** — proven, not assumed: `git merge-tree --write-tree origin/main 316bec58` exits **0** (no conflict) and the resulting tree `f3c228d1` carries `package.json` **1.4.0**, `CHANGELOG.md`, and exactly one fragment, `changelog.d/23.md`. So there is no downgrade to declare, and `release` on a feature PR would assert an intent that does not exist. ## The defect this exposes The release-shape detector compares the **PR head's** manifest version against the **base branch head's**, without asking whether the PR's own diff changes that file. Any branch cut before a release merge therefore manufactures a phantom "downgrade" and solicits a label that would be wrong. Recorded here rather than filed as an eighth defect on #36 — that issue is claimed and mid-build, and widening its scope under the builder is what made its own criteria unmeetable three times over. It will be filed separately once !42 lands. **Optional, not required:** updating this branch from `main` clears the warning at the source, since the head would then carry `1.4.0`. @claude-bot-andresmgsl already test-merged `main` into this head cleanly at 18:25Z (128/128, comment 30915), so it is a safe no-op for the review — the panel's three approvals are at `316bec58` and a merge commit would reset them, so **this is a choice for the builder and @andres, not a triage ask.** Merging as-is is correct and safe. ## Unchanged: the tag instruction on #32 still stands `changelog.d/23.md` riding into the merged tree is the visible marker of the hazard already recorded on #32 (comment 30873): **tag `v1.4.0` at `523a45587f12298ded00a988318a2c0ce25d2f85`, not at `main`'s head after this PR lands.** Tagging the post-merge head would ship `repo sync` inside a `1.4.0` whose `CHANGELOG.md` never mentions it, while `package.json` would still read `1.4.0` — nothing in the repo would catch it. No labels were changed on this PR. — triage (@claude-bot-andresmgsl)
codex-bot-andresmgsl requested review from andres 2026-08-31 18:44:16 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
4 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/stoke#41
No description provided.