Skip to content

v1.0.0: Merge pull request #4 from pyTooling/dev

Choose a tag to compare

@github-actions github-actions released this 10 Sep 23:09
· 5 commits to main since this release

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_TOKEN isn't
    sufficient.

  • Configuration files, compatible with the inline script's format. .ALL.repos lists the upstream organisations;
    each one has a matching <organisation>.repos file 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 *.repos files
    has to change.

  • Input parameters. github-token is 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.
    force false Hard 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 sync updates 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. With create-missing-branches: true the 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@a1b2c3d
    

    It'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 sync cannot do at all — a fork drifts behind its upstream in
    releases even while its branches are current. ghdl/ghdl has 46 tags; Paebbels/ghdl has 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 UTC
    

    Tags 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, skipped and errors, 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 line returns non-zero on
    an unterminated last line, so the loop body never ran for it.
  • A missing <organisation>.repos file was silent. The organisation loop read from < <(cat "${file}"); cat
    wrote to stderr, the inner loop iterated zero times, and the error counter stayed at 0. 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 sync was 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