v1.0.0: Merge pull request #4 from pyTooling/dev
The first release of pyTooling/SynchronizeForks — a reusable GitHub Action synchronizing the branches and tags
of forked repositories of a GitHub organisation or user account with their upstream repositories.
Forks don't update themselves. GitHub offers a Sync fork button per repository and per branch, but no automation for
a whole namespace — and nothing at all for tags. This action reads a list of forks, their branches and their tag
patterns from simple configuration files and synchronizes them one by one — usually from a scheduled workflow running
once a day.
The algorithm was previously an inline shell script, copied into each repository's workflow. It now lives here, and a
consuming repository keeps only its configuration files.
New Features
-
The action. A consuming workflow is one step:
- name: 🔄 Synchronize forked repositories uses: pyTooling/SynchronizeForks@v1 with: github-token: ${{ secrets.GH_TOKEN }}
The token needs write access to the contents of every listed fork, so a workflow's automatic
GITHUB_TOKENisn't
sufficient. -
Configuration files, compatible with the inline script's format.
.ALL.reposlists the upstream organisations;
each one has a matching<organisation>.reposfile listing its forks as
<upstream>=<fork>:<branches>[:<tagPatterns>]. Comments (#) and empty lines are ignored. The fourth field is new
and optional, so an existing repository migrates by replacing its two script steps — nothing in its*.reposfiles
has to change. -
Input parameters.
github-tokenis the only required one.Parameter Default Purpose target-organisation${{ github.repository_owner }}The namespace owning the forks — the constant each copy had to edit. directory'.'Where the configuration files live. index-file'.ALL.repos'The index file's name. forcefalseHard reset the fork's branch ( gh repo sync --force).
| create-missing-branches | false | Create a listed branch the fork doesn't have yet. See below. |
| dry-run | false | Print the commands instead of running them; reads no repository. |
| fail-on-error | true | Report the counters without failing the job. |
-
A branch the fork doesn't have can be created.
gh repo syncupdates a branch; it can't create one — so a branch
added upstream after the fork was made, or every branch but one when the fork was made with Copy the default branch
only, could never be synchronized. Withcreate-missing-branches: truethe action creates it from the upstream
repository's branch head, and the next run synchronizes it normally:📂 OSVVM/AXI4 ⇒ PLC2/OSVVM-AXI4 🌱 dev — created from OSVVM/AXI4@a1b2c3dIt's a single API call — a fork and its upstream share one object network, so no clone, fetch or push is involved.
It's off by default, because the branch head is read from<upstream>: check that field in your configuration
file before enabling it. While disabled, a missing branch is a counted error saying exactly that. -
Tags are synchronized too, which
gh repo synccannot do at all — a fork drifts behind its upstream in
releases even while its branches are current.ghdl/ghdlhas 46 tags;Paebbels/ghdlhas 19.The optional fourth field of a configuration line says which tags to follow, as fixed names or regular expressions
matched against the whole tag name:ghdl/ghdl=ghdl:master:v\d+\.\d+.* OSVVM/OSVVM=OSVVM:main,dev:nightly,v\d+\.\d+\.\d+ antonblanchard/microwatt=microwatt::v\d+\.\d+A matching tag the fork doesn't have is created at the same object. One that already matches is counted. One that
moved upstream is reported and deliberately left alone — rewriting a tag discards whatever the fork's tag points
at, which isn't a decision an unattended nightly job should make. Both sides are resolved to the commit they point
at, so the report says which is older:☢️ v1.0.0 — moved in 'OSVVM/OSVVM' ↪ fork: 90e6af7 2024-03-11 14:22:05 UTC ↪ upstream: d3d07ba 2025-07-02 09:41:18 UTCTags are never deleted from a fork, and a tag existing only in the fork is left untouched. As the third line above
shows, a fork may be followed for its tags alone, with no branches. -
Output parameters
synchronized,created,created-tags,skippedanderrors, plus a summary block and a
Not synchronized:list of what failed — so a run over a large namespace says what went wrong without scrolling
through the log. -
A symbol per kind of event, documented in the README: 🏭 organisation, 📂 fork, ✅ branch synchronized, 🌱 branch
created, 🏷️ tag created, 🟰 tags already up to date, ☢️ tag moved, ℹ️ nothing to do, 🚫 commented out, 🚧 dry-run,
❌ error. -
One collapsible group per organisation, and the upstream repository named in every progress line and every error
annotation.
Changes
- The
<upstream>field of a configuration line is reported in the progress and error output. It should name the real
upstream repository. - Values reach the shell through the environment rather than by string interpolation into the script.
Bug Fixes
Three defects of the inline script, each of which ended a run green:
- A configuration file without a final newline lost its last repository.
while read -r linereturns non-zero on
an unterminated last line, so the loop body never ran for it. - A missing
<organisation>.reposfile was silent. The organisation loop read from< <(cat "${file}");cat
wrote to stderr, the inner loop iterated zero times, and the error counter stayed at0. A typo in.ALL.repos
synchronized nothing and reported success. It's a counted error now. - A multi-line error annotation was truncated. A workflow command is a single line, so the output of a failed
gh repo syncwas cut at its first newline. Newlines are escaped as%0A.
Malformed lines — no =, no :, or an empty <upstream>, <fork> and tag list — are counted errors as well,
instead of producing a nonsensical gh repo sync command.
Documentation
README.md walks through a consuming repository: its file layout, the complete Synchronize.yml, the .ALL.repos
entry point, one <organisation>.repos file with a worked example, a sample log, the symbol table, the parameter
tables, the missing-branch and tag-synchronization behaviour, the error handling, how an existing repository migrates,
and the repositories using this action.
Unit Tests
.github/workflows/Verification.yml runs the action against the fixtures in tests/ in dry-run mode — no token, no
repository touched — and asserts all five output parameters for a valid configuration, for an invalid one, and that
counted errors make the step fail.
Others
The repository releases itself, with reusable workflows from
pyTooling/Actions@r8: PrepareJob.yml classifies a run, and a merge commit
on main is tagged with the release pull-request's title and re-run on that tag, where PublishReleaseNotes.yml
publishes this page. A local UpdateVersionBranch.yml then opens the pull-request that moves the v1 branch to this
release, rewriting the references that have to name the branch rather than main.
None of that changes the action: action.yml is byte-for-byte what #3 left behind.
Related Issues and Pull-Requests
- #1 — Add the 'SynchronizeForks' composite action.
- #2 — Create a branch that is missing in the fork.
- #3 — Synchronize tags.
- #5 — Rename the workflow to 'Pipeline.yml' and release from it.
- #6 — Propose the version branch update from the tagged pipeline.
Conversions of the repositories that carried the inline script: Paebbels/Synchronize#3 and VHDL/Synchronize#1.
Runs on GitHub-hosted runners with no setup: it needs the GitHub CLI (gh), and — for tag patterns only — a grep
with PCRE support (-P).
The algorithm comes from Paebbels/SynchronizeForks; its copyright is
carried over — Patrick Lehmann from 2024, The pyTooling Authors from 2026.
🤖 Generated with Claude Code
Published from Pipeline workflow triggered by github-actions[bot] on 2026-09-10 23:09:05 UTC.
This automatic release was created by pyTooling/Actions::Release.yml