repo sync — fast-forward-only update of an imported repo from its upstream #23
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:ci
scope:cli
scope:docs
scope:manifests
scope:packaging
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/stoke#23
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
repo importis one-shot: it creates a repository and cannot advance an existing one. Keeping an imported repo current means hand-splicing the stoke token into a git remote, where it lands in.git/configand shell history (measured while refreshingheavy-duty/ceremonyfrom GitHub; details in the 2026-08-04 comment below). The CLI already hands credentials to git safely:gitAuthEnv(), used byrepo clone— a sync path reuses it.Epic #27 (finding 2) narrowed this spec: a fast-forward-only verb cannot serve a diverged tree (
heavy-duty/ceremonycarries ~30 local Forgejo-compatibility commits on purpose — evidence in ceremony#197/#198), and no flag should pretend otherwise. The diverged-tree procedure is documented merge work that already exists as ceremony'sdocs/UPSTREAM-SYNC.md— delivered by ceremony#200, closed 2026-08-05 — not a stoke verb.Spec
Decisions, not options:
stoke repo sync -o <owner> -r <repo> --from <upstream-url> [--branch <branch>] [--tags] [--dry-run]docs/UPSTREAM-SYNC.md). There is no--forceflag.--branchdefaults to the forge repository's default branch.--tagsalso pushes tags. Existing forge tags are never moved or deleted; an upstream tag that moved is reported, skipped, and makes the exit code non-zero.--dry-runprints exactly what would move (branchold..new, tags to be created) and writes nothing.gitAuthEnv(): the token never appears in argv, in remotes, or in.git/config.docs/UPSTREAM-SYNC.md(delivered by ceremony#200).mirror: trueat migration) — a forge-side per-repo setting, not a CLI verb; a repo wanting scheduled read-only mirroring should be imported as a mirror instead.release create+ #25's asset upload; not folded into this verb.Tasks
repo syncinsrc/cli.jsper the spec (fetch upstream, fast-forward check, push viagitAuthEnv())--tags(moved-tag report/skip/non-zero),--dry-run, default-branch resolution.git/configheavy-duty/stoke, not a fork (fork PRs stall on the CI approval gate — see !28/!29)state:needs-human, request@andresby hand — the engine's own request 404s on this forge and its sweep log reports that failure as a success (#36, defect 1)Acceptance criteria
--tags) tags, with the token visible nowhere in argv, remotes, or.git/config--dry-runreports the exact moves and provably writes nothingTest plan
Test suite drives a local upstream fixture: fast-forward advance; second run is a no-op; non-ff refusal (the case that must fail); moved-tag skip with non-zero exit;
--dry-runno-op; credential-leak check over argv and on-disk config. Manual proof: sync a scratch import twice, then add a local commit on the forge copy and confirm the refusal.Dependencies
Part of #27. No blockers. Related: #24 and #25 (same mirroring workflow); the diverged-tree procedure lives in ceremony's
docs/UPSTREAM-SYNC.md, delivered by ceremony#200 (closed). That link is deliberately not pinned to a commit: it is cited as the live procedure a reader is sent to, not as quoted evidence, and the pin it replaced (c2ef6a2, 2026-08-05) had gone two revisions stale — ceremonyfdb7d75(2026-08-24) added the 0.6.2 port record and theupstream-0.6.xvs0.6.xtag disambiguation, and ceremony#269/7e96748(2026-08-27) replaced the deferral paragraph with the operator's ruling that the next campaign merges rather than ports, advancing.upstream-ref. Neither loosens this issue's ff-only narrowing — a 488-commit merge campaign is even less a CLI verb than it was — and both were invisible behind the old pin.Still present in the apt build (
1.2.0) and onmain(1.3.0) as of 2026-08-04 — re-confirmed while mirroringgithub.com/heavy-duty/crewonto the forge.stoke repo --helplistslist / create / import / rename / import-batch / transfer; nothing advances an existing repo.The full manual fallback still costs a hand-injected credential:
Two adjacent gaps surfaced in the same task and are filed separately: #24 (
repo createcannot target an org) and #25 (release createcannot upload assets). Arepo syncthat covered branches, tags and release objects would close all three for the mirroring use case.feature: no way to update an already-imported repo — add repo syncto repo sync — fast-forward-only update of an imported repo from its upstreamTriage: title and body rewritten per epic #27, finding 2 — the verb is narrowed to the undiverged-mirror case. Decisions now in the body: fast-forward only with no
--force; diverged trees refused by name (merge procedure belongs to ceremony#200); pull-mirrors and release mirroring explicitly out of scope. The original body's--releasesflag is dropped — release mirroring composes fromrelease create+ #25 instead of growing this verb. Spec is complete; labelingready.Triage — cross-repo pointer repaired, 2026-08-20.
This issue told a builder four times that the diverged-tree procedure "lives in"
/ "is owned by" ceremony#200. Measured this tick: ceremony#200 closed
2026-08-05T19:12:40Z — it closed thirteen days before the 2026-08-18 spec
revision that wrote those pointers, so following one has never landed anywhere
but a closed issue.
What it delivered is real and readable:
docs/UPSTREAM-SYNC.mdon ceremony
main(c2ef6a2), verified present this tick — the procedure plus atable of standing resolutions (
VERSION,CEREMONY_SELF_REF,.github/labels.conf,drills/*,CHANGELOG.md) that each sync would otherwise re-decide. It alsoconfirms, in its own words, that the two trees "diverge in opposite directions on
purpose" — so this issue's reason for refusing diverged trees is unchanged and
still true.
Body edited: Context, the fast-forward refusal bullet, the Explicitly out of
scope bullet, and Dependencies now name that doc (crediting ceremony#200 as
what delivered it) instead of pointing at the closed issue. Nothing about the
spec, tasks, criteria or scope changed — only where the builder is sent to read,
which matters most for the refusal path, whose error text is meant to tell a
user where the merge procedure is.
Labels re-read before writing (
ready+enhancement, set 2026-08-18T00:23:52Z,no label event since): unchanged and correct. Still a real gap —
mainisunmoved at
4c618589andsrc/cli.jshas nosyncsubcommand.Triage — collision edge added,
ready→blocked, 2026-08-20. No spec, task,acceptance criterion or estimate changed, and the narrowed scope from finding 2 is
untouched. This is an ordering edge only: nothing here waits on #25's content — the
"composable from #25's asset upload" line under out of scope is an observation about
callers, not a build order — and when #25 closes this issue flips back to
readyunchanged.The finding. #23, #24 and #25 all carried
readyfrom a single batch write on2026-08-18T00:23:52Z, and none declared a collision edge, so all three were concurrently
claimable. Each body already said "all three touch
src/cli.js", but only as Relatedprose, which constrains nothing.
src/cli.jsis a single 1653-line file in which every command group is chained(
repoat L326,releaseat L985). #24 editsrepo create(L424) andcreateRepo()insrc/api.js; #25 editsrelease create(L1057) and addsrelease uploadplus theasset-upload path in
src/api.js; #23 adds arepo syncsubcommand inside the samerepogroup #24 modifies and reusesgitAuthEnv(). All three also add a section toREADME.md.This issue is the tightest case of the three: it inserts a subcommand into the same
repogroup #24 is editing. Under TRIAGE.md's collision rule,
disjoint regions inside a shared file are explicitly not an exemption.
Order chosen: #24 (P1) → #25 (P2) → #23 (P3) — epic #27's own declared priority. This
issue is last because the epic ranks it P3 and largest, not because it is worth less;
it stays fully specced and buildable the moment #25 closes. Recorded as finding 3 on #27.
Triage — retracted, 2026-08-20. The collision edge above was wrong; this issue is
readyagain and claimable now.Blocked by #25has been removed from the body and theDependencies section restored verbatim. The finding-2 scope narrowing was never touched.
Why the edge was wrong. The collision rule
(#288) governs one deliverable carried by two issues, not any two issues that touch one
large file, and its "disjoint regions do not waive it" clause means regions of that shared
deliverable. I read "all three touch
src/cli.js" as the collision and it is not one.The sweep that enforces the rule settles it:
deliverable_key(issueflow-reconcile 0.6.1,L433) keys on the title's em-dash prefix, normalized and case-folded. The three keys are
repo sync(#23),repo create --owner(#24) andrelease create --asset / release upload(#25) — three distinct deliverables, so the collision flag would not fire on this trio, and
it would be right not to.
Ordinary same-file contention between distinct deliverables is a rebase — the builder's
normal cost, already carrying its own PR label (
blocker:conflict). It is not a board gate,and making it one would have left two smaller, higher-priority issues unclaimable behind the
largest for no doctrinal reason.
Net effect on this issue: none.
ready, unassigned, no blockers, claimable today —including concurrently with #24 and #25. Recorded as finding 3 on epic #27.
Triage — re-pinned the
docs/UPSTREAM-SYNC.mdcitations fromc2ef6a2tobranch/main; no label, no scope, no criterion changed. Same edit on #27, whose Dependencies carried the third copy of the link and now explains it inline.This issue sends a reader to that doc twice — in the Context rescope and in the refusal path's out-of-scope statement — as the live procedure, not as quoted evidence. A commit pin is right for evidence and wrong for that: the pinned revision was 2026-08-05 and the doc has moved twice since.
fdb7d75, 2026-08-24 — adds the 0.6.2 port record: the content baseline isupstream-0.6.3, ported rather than merged, so.upstream-refstayed at8c3a4d1, and tags are disambiguatedupstream-0.6.x(upstream's line) vs bare0.6.x(cut on this forge).7e96748(ceremony#269), 2026-08-27 — replaces the deferral paragraph with the operator's ruling that the next campaign merges rather than ports, and so advances the ancestry baseline. The sentence it replaced froze upstream0.7.4as a ceiling; ceremony's own changelog records that it "expired before it was written".Neither touches this issue's spec, and I checked that before editing rather than after. The ff-only narrowing (epic #27, finding 2) rests on diverged trees being documented merge work rather than a CLI verb, and the correction sizes that campaign at 92 files, +25,121/−971 over 488 commits — further from a verb, not closer. The refusal path, the
--tagsand--dry-runshapes, and the three out-of-scope items all stand as written.Worth naming because it is a rot direction, not just a stale link: on 2026-08-21 triage verified this exact pin byte-identical to ceremony
mainand recorded a re-fire condition — re-check when ceremony#229/#231 land. Both are irrelevant to what actually moved the file; the change came from #229's siblingfdb7d75and from #269, neither of them named. A conditional that names the issues expected to invalidate a fact only fires on the futures you thought of. The branch link removes the condition entirely — the citation cannot freeze again.(One caveat kept honest: the branch route is right here because both links are prose citations with no line anchors. Anything quoting line numbers still gets a commit pin — see #36, which pins
0.6.1and0.6.3deliberately.)Starting #23. I’ll add the fast-forward-only verb on a same-repo worktree.\n\nDesign / plan of record:\n- use an ephemeral bare Git directory; never persist either remote or the token;\n- resolve an omitted branch from the forge repository’s symbolic ;\n- fetch forge/upstream branch tips into separate temporary refs and refuse non-ancestor updates with both SHAs and the documented diverged-tree boundary;\n- compare upstream and forge tags, omit moved tags, include new tags only, and return non-zero when any moved tag is found;\n- make print the exact branch/tag ref movements without pushing;\n- drive the behavior with local bare-repository tests first, including a Git argv recorder and on-disk config/token leak checks;\n- document the verb, its three explicit non-goals, and add the required changelog fragment.\n\nI’ll checkpoint each completed worklog item in the draft PR body and push at least every 15 minutes while active.
Starting #23. I’ll add the fast-forward-only
repo syncverb on a same-repo worktree.Design / plan of record:
HEAD;--dry-runprint the exact branch/tag ref movements without pushing;Correction: the immediately preceding comment’s formatting was damaged in transport; this is the plan of record.
I’ll checkpoint each completed worklog item in the draft PR body and push at least every 15 minutes while active.
glm-bot-andresmgsl referenced this issue2026-08-31 18:26:50 +00:00
Triage, 2026-08-31T18:45Z — transition record for the
Closes #23merge, and the claim released.!41 merged 18:42:40Z by @andres (merge commit
c34a8b04d23480ce5767e2e72ef398e913d651ee, head316bec5855b3d4f8f003df6f580b3d5cb4a34e38), and theCloses #23in its body auto-closed this issue the same second. The engine builds derived transitions only fromRefsreferences and enumeratesissues?state=open, so noclaimed→post-mergemove ran and no transition comment was written — on a close that skips the derived move, triage writes the record in the same tick. That is this comment.claimedis now removed; the assignee stays as build attribution, matching the #26/#24/#25/#1 precedent.The
Closeswas correct, not a slip: all five acceptance criteria here are pre-merge and reviewable on the PR, so theRefsrule never bound and nothing is stranded inpost-merge.What the merge proves — re-measured by triage at
c34a8b04, not read off the PRrepo syncimplementedsrc/cli.js:456.command('sync'); new modulesrc/repo-sync.js(177 lines)src/cli.js+src/repo-sync.js;--dry-run/--tagsboth wiredtest/sync.test.js— 11 integration testsREADME.md:296describes it;README.md:317states it "deliberately does not merge diverged trees, configure Forgejo pull-mirrors, or copy releases" — the explicit non-goals this issue demandedbuild/23-repo-synconheavy-duty/stoke; no CI approval gate hit.git/configsrc/repo-sync.jsnever builds a credentialed URL (no interpolated token found); it receivesenvand passes it tospawnSync('git', …), so authentication is environment-only. All three panelists verified this path independently at the merged headPanel at the merged head, none stale: @glm-bot-andresmgsl 18:29:36Z, @claude-bot-andresmgsl 18:32:06Z, @kimi-bot-andresmgsl 18:32:14Z — all APPROVED at
316bec58, which is exactly the commit that merged.ci / testandlabels / labelsgreen at that head.One task was NOT exercised — recorded, not quietly ticked
Task "When the PR reaches
state:needs-human, request@andresby hand — the engine's own request 404s on this forge" never happened. !41 reachedstate:needs-humanat 18:34:13Z and @andres merged it 8 minutes later at 18:42:40Z with no hand request recorded; the onlyreview_requestevents on !41 are the three bots, requested by the builder at 18:22:26–27Z. Overtaken by events rather than skipped — but it is not evidence the hand-request step works, and #36 defect 1 fired live in that exact window (sweep runs 605/607 both loggedHTTP 404 from 'POST repos/heavy-duty/stoke/pulls/41/requested_reviewers'). Same shape as #32's Task 2, left unticked for the same reason.No body boxes were ticked here. This issue is closed and the merge is fully evidenced above; ticking twelve boxes on a closed issue adds nothing the record does not already carry, and the one that would have been wrong is named explicitly.
— triage (@claude-bot-andresmgsl)