Keep a long-lived head branch in sync with a base branch. Merges automatically
when it can, including the lockfile churn that defeats a plain git merge,
and opens a pull request when a human is needed.
Built for the dependency-staging flow: a dependencies branch collects bot
bumps, this action keeps it current with the trunk, and delivery to the trunk
stays a single reviewed PR.
On each run it merges base into head:
- clean merge -> push
head - only lockfiles conflict -> regenerate them against the merged manifest, push
head - a source file conflicts -> open a PR
head->baseto resolve by hand
A lockfile conflict is the common case when bumps pile up on head while the
trunk also moves; regenerating the lock (cargo fetch / npm install --package-lock-only --ignore-scripts) resolves it without a human, keeping the
already-locked versions. A real source conflict is escalated to a PR so it is
resolved on head, never on the trunk.
name: sync
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
concurrency:
group: branch-sync
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: voidmason/branch-sync-action@v1
with:
token: ${{ secrets.SYNC_TOKEN }}
base: master
head: dependencies| Input | Required | Description |
|---|---|---|
token |
yes | PAT, see below. |
base |
yes | Branch to sync from. |
head |
yes | Branch to sync into. |
name |
no | Committer name. Default voidmason. |
email |
no | Committer email. Default voidmason@users.noreply.github.com. |
A PAT is required, not the default GITHUB_TOKEN: a push made with
GITHUB_TOKEN does not trigger the downstream workflows on head (the lite CI
that validates a regenerated lock), and on protected repositories it cannot push.
Permissions:
| Permission | Why |
|---|---|
| Contents: read+write | fetch on checkout, push to head |
| Pull requests: read+write | list (dedup) and create the conflict PR |
| Workflows: write | only if a sync may carry changes under .github/workflows |
A classic PAT with repo (plus workflow) covers the same.
- The runner needs the toolchain for your lockfiles:
cargoforCargo.lock,npmforpackage-lock.json.ubuntu-latestships both; add a setup step before this action if your project pins a specific toolchain. - The
headbranch must already exist.
- The
concurrencygroup in the usage example is load-bearing: the action pushesheadwithout a lease, so two overlapping runs can race into a non-fast-forward rejection. Keep one sync per ref and do not cancel in progress. A rejected push goes red and self-heals on the next sync. - A source conflict opens a PR and stops there: until that PR is merged, the whole sync (including lockfile bumps) is held - the merge is all-or-nothing. Resolve the PR to unblock.
- A lockfile that merges textually is pushed without being reconciled against
the merged manifest: a clean text merge can still be semantically stale.
Validating that is left to the downstream CI on
head(the lite CI the PAT exists to trigger), same as for a regenerated lock.
MIT, see LICENSE.