Skip to content
GhostbranchdocsGitHub

Sync

Keep the shadow repo up to date with your public repo, and let Ghostbranch tidy up after merges.

Other people’s pull requests keep landing on public main while you work. ghostbranch sync brings them into the shadow repo, so your next feature starts from today’s code.

It does three things, in order:

  1. Fast-forwards shadow main to public main. It never force-pushes and never resolves a conflict. If shadow main has commits public main doesn’t, it stops with SYNC_DIVERGED (exit 4) and a person sorts it out.
  2. Cancels the CI runs its own pushes started in the shadow repo (see below).
  3. Closes features whose pull request merged, if their branch hasn’t moved since they were published (see below).

Sync only reads the public repo. It never writes there.

Terminal
ghostbranch sync
Shadow main is up to date with public main (815a84b30f4a).

Add --plan to see what it would push and close, without doing it:

Terminal
ghostbranch sync --plan
Dry run; nothing was changed. This would:
(nothing)

Run ghostbranch sync first, before you start a feature, because a new feature starts from shadow main. If you forget, feature start refuses and creates nothing:

Terminal
ghostbranch feature start add-search
error: shadow main is 2 commit(s) behind public main, so a feature started now would be based on old code; run ghostbranch sync --repo you/project, then start the feature again [SHADOW_BEHIND]
Next:
ghostbranch sync --repo you/project
shadow main is behind public main; sync it (fast-forward only)
ghostbranch feature start add-search --repo you/project
then start the feature again, from the synced shadow main

Here public main had one merged pull request on it, which counts as two commits: the change and its merge commit. Other commands, such as status, only warn with SHADOW_BEHIND:

Terminal
ghostbranch status
you/project -> you/project-shadow: shadow main behind public main, overlay current, scheduled sync off
the features and Next are left out here; on stderr:
warning: shadow main is 2 commit(s) behind public main; run ghostbranch sync --repo you/project

The gate is never affected: it always rebases your feature onto public main, not shadow main.

Sync pushes to the shadow repo, and a push can start the shadow repo’s workflows that run on push. Those runs re-test code your public CI already tested, and they cost private Actions minutes. So sync cancels each run its pushes start, and lists them in its result. --no-cancel leaves them running.

To cancel runs, the token needs Actions write on the shadow repo. When sync can’t cancel a run, or can’t check, it warns CI_NOT_CANCELLED and names the runs it knows of. Nothing else goes wrong: they only cost minutes.

To stop those runs from starting at all, add a guard to each job that runs on push. ghostbranch audit suggests it, under PUSH_TRIGGERS_ON_SYNC:

.github/workflows/ci.yml
jobs:
test:
# Runs on pushes to the public repo only, not on sync's pushes to the shadow repo.
if: github.event_name != 'push' || github.repository == 'you/project'

A job that already has an if: combines the two: if: <existing> && (github.event_name != 'push' || github.repository == 'you/project'). It changes nothing for your public CI.

After your pull request merges, the next sync closes the feature for you. It deletes the feature’s branches and records in the shadow repo, closes the gate’s draft pull request, and says so. This sync followed the merge of pull request #23, add-search:

Terminal
ghostbranch sync
Shadow main fast-forwarded d17559e17a29 -> c63c081d62fb.
cancelled 1 push-triggered workflow run(s) in the shadow repo
closed merged feature add-search (PR #23)

Its fast-forward of shadow main started one workflow run that runs on push, and sync cancelled it, as above. That sync also listed older features for a person to close, left out here.

Sync closes a feature on its own only when nothing would be lost: its pull request merged, and its branch hasn’t moved since it was published. Add --no-close to skip closing.

An archived feature has no shadow branch left, only its archive, refs/ghostbranch/archive/<name>. Once its pull request merges, sync closes it and deletes the archive too, as long as the archive holds exactly what was published. If it doesn’t, the feature reads merged_moved, and a person closes it.

Anything else is left for a person, and status names it with the command to run:

  • merged_moved: the pull request merged, but the feature has commits that weren’t published, or an archived feature’s archive doesn’t match what was published. Promote them, or close the feature.
  • pr_closed: the pull request was closed without merging.
  • branch_gone: the feature’s branch was deleted in the shadow repo.

ghostbranch feature close <name> deletes such a feature. It would lose work that was never published, so the person confirms it with a one-time code at their own terminal. An agent grant doesn’t cover it. The ledger keeps the SHA of every branch it deletes, so a branch can be pushed back. Add --plan to see what it would delete.

Merged stacks close from the bottom up. See Stacked features.

By default you run sync yourself, and commands warn when shadow main falls behind. If you’d rather the shadow repo kept up on its own, turn on scheduled sync: a workflow in the shadow repo that runs sync on a timer.

  1. On the ghostbranch branch, set sync.schedule in .ghostbranch.yml to a cron schedule, such as "23 */6 * * *" (every six hours). Push the branch to the shadow repo.
  2. Run ghostbranch upgrade --plan to see the workflow it would add, then ghostbranch upgrade to add it.

Setting sync.schedule: "off" and running upgrade again removes it. ghostbranch status says whether it’s on.

Every six hours costs about 120 Actions minutes a month. If public commits change workflow files, the workflow needs a GHOSTBRANCH_SYNC_TOKEN secret in the shadow repo: a fine-grained token with Contents and Workflows write on the shadow repo only.

ghostbranch upgrade updates the files Ghostbranch manages on the overlay branch (today, only the scheduled sync workflow) to match your installed Ghostbranch and your .ghostbranch.yml. It writes one commit to the ghostbranch branch, then runs the CI audit. It doesn’t update Ghostbranch itself: that’s a new install.

When a managed file is missing, out of date or edited by hand, commands warn OVERLAY_OUTDATED. Look first with --plan, which also warns when it would replace a hand edit:

Terminal
ghostbranch upgrade --plan
Dry run; nothing was changed. This would:
(nothing)
SHADOW_BEHINDexit 4Shadow main is behind public main, so feature start refused to start a feature from it; nothing was created.
What to do

Run ghostbranch sync --repo <public>, then start the feature again.

SHADOW_BEHINDwarningShadow main is behind public main.
What to do

Run ghostbranch sync --repo <public> (fast-forward only). The gate is unaffected: it always rebases on public main.

SYNC_DIVERGEDexit 4A shadow branch has commits that the public branch does not.
What to do

A person investigates the branches in details.branches; Ghostbranch never force-pushes a shadow branch.

This needs a person.

CI_NOT_CANCELLEDwarningSync pushed, but its pushes may have started workflow runs in the shadow repo that it could not cancel.
Causes
  • the token lacks Actions write (or read) on the shadow repo
  • GitHub's Actions API failed
What to do

Nothing is required: the runs only cost Actions minutes; cancel them on the shadow repo's Actions tab if you like. To let sync cancel them, give the token Actions write on the shadow repo. To stop them starting, add the guard that PUSH_TRIGGERS_ON_SYNC in ghostbranch audit suggests. ghostbranch sync --no-cancel skips cancelling. details.runs lists the runs sync knows of, each with its id and url; it is empty when sync couldn't list them.

PUSH_TRIGGERS_ON_SYNCwarningThese workflows run on pushes to synced branches. ghostbranch sync pushes with your token, and so does a scheduled sync that uses GHOSTBRANCH_SYNC_TOKEN, so each sync starts them in the shadow repo, re-testing code public CI already tested, on private Actions minutes.
What to do

ghostbranch sync cancels the runs its pushes start when the token has Actions write on the shadow repo. To stop them for good, a person adds the guard in details.guard to those workflows' push-triggered jobs, for example if: github.event_name != 'push' || github.repository == 'org/project'. A job that already has an if: combines them: if: <existing> && (github.event_name != 'push' || github.repository == 'org/project'). Once every push-triggered job carries it, the audit stops reporting the file and sync stops looking for its runs. It changes nothing in the public repo's own CI. Or accept the minutes. A scheduled sync pushed with the workflow's default token starts no workflows.

FEATURE_NOT_CLOSEDwarningSync didn't close a merged feature: the close failed, sync couldn't survey the features and so closed none, another feature's record couldn't be read, so sync left the merged features open, or its PR merged into the public branch below it, not main.
What to do

If details.deleted is set, the feature is gone: close its gate draft PR in the shadow repo by hand. With details.reason merged_into_parent_branch, its PR merged into public branch details.base, not main: a person checks that branch holds its changes, then closes it (ghostbranch feature close <name>); sync doesn't. With details.unreadable, the features named there have records that can't be read, and any of them may be stacked on this one: a person closes each at the console (ghostbranch feature close <name>) or fixes its record, and the next sync closes this feature. Otherwise run ghostbranch feature show <name>, then feature close <name>. With no details.feature, sync couldn't survey the features and closed none: run ghostbranch status to see why.

OVERLAY_OUTDATEDwarningA Ghostbranch-managed file on the overlay branch is missing, out of date, or edited by hand.
What to do

Run ghostbranch upgrade --plan to see the change, then ghostbranch upgrade.