Skip to content

Commit 6d194ff

Browse files
claude-codePaebbelsclaude
committed
Synchronize tags.
'gh repo sync' knows branches only, so a fork drifts behind its upstream in releases even while its branches are current - 'ghdl/ghdl' has 46 tags, 'Paebbels/ghdl' has 19. A configuration line gains an optional fourth field, colon separated, listing tag names or regular expressions: '<upstream>=<fork>:<branches>[:<tagPatterns>]'. A line without it behaves exactly as before, and a line may carry tag patterns without any branch. Patterns are matched against the whole tag name with 'grep -P', joined into one alternation because GNU grep rejects several '-e' patterns together with '-P'. A matching tag missing from the fork is created pointing at the same object. A tag that moved in the upstream repository is reported as an error and left alone - rewriting it would discard whatever the fork's tag points at - and the run continues. Tags are never deleted. 'created-tags' is a new output parameter. Co-Authored-By: Patrick Lehmann <Paebbels@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RnpaXYDwfNwnJrQew78j3f
1 parent 191622e commit 6d194ff

7 files changed

Lines changed: 209 additions & 24 deletions

File tree

‎.github/actions/check-outputs/action.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ inputs:
2222
description: Actual number of synchronized branches.
2323
type: string
2424
required: true
25+
created-tags:
26+
description: Actual number of created tags.
27+
type: string
28+
required: true
2529
skipped:
2630
description: Actual number of skipped entries.
2731
type: string
@@ -34,6 +38,10 @@ inputs:
3438
description: Expected number of synchronized branches.
3539
type: string
3640
required: true
41+
expected-created-tags:
42+
description: Expected number of created tags.
43+
type: string
44+
required: true
3745
expected-skipped:
3846
description: Expected number of skipped entries.
3947
type: string
@@ -50,9 +58,11 @@ runs:
5058
shell: bash
5159
env:
5260
SYNCHRONIZED: ${{ inputs.synchronized }}
61+
CREATED_TAGS: ${{ inputs.created-tags }}
5362
SKIPPED: ${{ inputs.skipped }}
5463
ERRORS: ${{ inputs.errors }}
5564
EXPECTED_SYNCHRONIZED: ${{ inputs.expected-synchronized }}
65+
EXPECTED_CREATED_TAGS: ${{ inputs.expected-created-tags }}
5666
EXPECTED_SKIPPED: ${{ inputs.expected-skipped }}
5767
EXPECTED_ERRORS: ${{ inputs.expected-errors }}
5868
run: |
@@ -78,6 +88,7 @@ runs:
7888
}
7989
8090
checkOutput "synchronized" "${SYNCHRONIZED}" "${EXPECTED_SYNCHRONIZED}"
91+
checkOutput "created-tags" "${CREATED_TAGS}" "${EXPECTED_CREATED_TAGS}"
8192
checkOutput "skipped" "${SKIPPED}" "${EXPECTED_SKIPPED}"
8293
checkOutput "errors" "${ERRORS}" "${EXPECTED_ERRORS}"
8394

‎.github/workflows/Verification.yml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,14 @@ jobs:
4343
uses: ./.github/actions/check-outputs
4444
with:
4545
synchronized: ${{ steps.synchronize.outputs.synchronized }}
46+
created-tags: ${{ steps.synchronize.outputs.created-tags }}
4647
skipped: ${{ steps.synchronize.outputs.skipped }}
4748
errors: ${{ steps.synchronize.outputs.errors }}
4849
# 1 branch of 'ghdl' + 1 branch of 'progit2' + 2 branches of 'itertree'
4950
expected-synchronized: 4
5051
# 1 commented out organisation + 1 commented out repository
52+
# Dry-run reads no repository, so no tag is created.
53+
expected-created-tags: 0
5154
expected-skipped: 2
5255
expected-errors: 0
5356

@@ -73,9 +76,11 @@ jobs:
7376
uses: ./.github/actions/check-outputs
7477
with:
7578
synchronized: ${{ steps.synchronize.outputs.synchronized }}
79+
created-tags: ${{ steps.synchronize.outputs.created-tags }}
7680
skipped: ${{ steps.synchronize.outputs.skipped }}
7781
errors: ${{ steps.synchronize.outputs.errors }}
7882
expected-synchronized: 1
83+
expected-created-tags: 0
7984
expected-skipped: 0
8085
# 1 missing 'Missing.repos' + 4 malformed lines in 'ghdl.repos'
8186
expected-errors: 5

‎README.md‎

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
# Synchronize Forks
55

6-
A reusable GitHub Action synchronizing forked repositories of a GitHub organisation or user account with their upstream
7-
repositories.
6+
A reusable GitHub Action synchronizing the branches and tags of forked repositories of a GitHub organisation or user
7+
account with their upstream repositories.
88

99
Forks don't update themselves. GitHub offers a *Sync fork* button per repository and per branch, but no automation for
1010
a whole namespace. This action reads a list of forks and their branches from simple configuration files and
@@ -86,27 +86,28 @@ file each, so they are collected in an `_Others` (or `_Misc`) file.
8686
8787
Each of these files lists one fork per line, in the format:
8888
89-
`<upstream>=<fork>:<branches>`
89+
`<upstream>=<fork>:<branches>[:<tagPatterns>]`
9090
91-
| Element | Format | Meaning |
92-
|--------------|---------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|
93-
| `<upstream>` | `<organisation>/<repository>` | The repository the fork was created from. It's reported in the action's progress and error output, so it should name the real upstream. |
94-
| `<fork>` | `<repository>` | The forked repository. No organisation or account, because that's the `target-organisation` parameter. |
95-
| `<branches>` | `<branch>[,<branch>,...]` | Comma separated list of branches to synchronize. |
91+
| Element | Format | Meaning |
92+
|-----------------|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|
93+
| `<upstream>` | `<organisation>/<repository>` | The repository the fork was created from. It's reported in the action's progress and error output, so it should name the real upstream. |
94+
| `<fork>` | `<repository>` | The forked repository. No organisation or account, because that's the `target-organisation` parameter. |
95+
| `<branches>` | `<branch>[,<branch>,...]` | Comma separated list of branches to synchronize. May be empty when tag patterns are given. |
96+
| `<tagPatterns>` | `<pattern>[,<pattern>,...]` | *Optional.* Comma separated list of tag names or regular expressions. See [Tag Synchronization](#tag-synchronization). |
9697
9798
A line starting with `#` is a comment and skips that fork. Empty lines are ignored.
9899
99100
```
100-
OSVVM/OSVVM=OSVVM:main,dev
101+
OSVVM/OSVVM=OSVVM:main,dev:v\d+\.\d+.*
101102
OSVVM/OSVVM-Scripts=OSVVM-Scripts:main,dev
102103
OSVVM/AXI4=OSVVM-AXI4:main,dev
103104
OSVVM/Ethernet=OSVVM-Ethernet:main
104105
#OSVVM/AvalonST=OSVVM-AvalonST:main
105106
```
106107
107-
Read for the `PLC2` namespace, those five lines synchronize `PLC2/OSVVM` (branches `main` and `dev`) from
108-
`OSVVM/OSVVM`, `PLC2/OSVVM-Scripts` from `OSVVM/OSVVM-Scripts`, `PLC2/OSVVM-AXI4` from `OSVVM/AXI4`,
109-
`PLC2/OSVVM-Ethernet` (branch `main` only) from `OSVVM/Ethernet`, and skip `PLC2/OSVVM-AvalonST`.
108+
Read for the `PLC2` namespace, those five lines synchronize `PLC2/OSVVM` (branches `main` and `dev`, plus every tag
109+
matching `v\d+\.\d+.*`) from `OSVVM/OSVVM`, `PLC2/OSVVM-Scripts` from `OSVVM/OSVVM-Scripts`, `PLC2/OSVVM-AXI4` from
110+
`OSVVM/AXI4`, `PLC2/OSVVM-Ethernet` (branch `main` only) from `OSVVM/Ethernet`, and skip `PLC2/OSVVM-AvalonST`.
110111
111112
Both Unix and Windows line endings are accepted, and a missing final newline doesn't drop the last entry.
112113
@@ -119,13 +120,16 @@ Every organisation becomes a collapsible group, and each fork reports the branch
119120
📂 OSVVM/OSVVM ⇒ PLC2/OSVVM
120121
✅ gh repo sync PLC2/OSVVM --branch main
121122
✅ gh repo sync PLC2/OSVVM --branch dev
123+
🏷️ v2.1.0 — created from OSVVM/OSVVM@a1b2c3d
124+
🏷️ 12 tag(s) already up to date
122125
📂 OSVVM/AXI4 ⇒ PLC2/OSVVM-AXI4
123126
❌ gh repo sync PLC2/OSVVM-AXI4 --branch dev
124127
↪ failed to sync: HTTP 404: Not Found
125128
🚫 #OSVVM/AvalonST=OSVVM-AvalonST:main
126129

127130
Summary:
128131
Synchronized branches: 2
132+
Created tags: 1
129133
Skipped entries: 1
130134
Errors: 1
131135

@@ -150,10 +154,43 @@ Not synchronized:
150154
| Parameter | Description |
151155
|----------------|---------------------------------------------------------------------------|
152156
| `synchronized` | Number of successfully synchronized branches. |
157+
| `created-tags` | Number of tags created in a fork. |
153158
| `skipped` | Number of skipped organisations and repositories (commented out lines). |
154159
| `errors` | Number of counted errors. |
155160
156-
In dry-run mode, `synchronized` counts the branches that *would* have been synchronized.
161+
In dry-run mode, `synchronized` counts the branches that *would* have been synchronized, the configured tag patterns
162+
are printed, and no repository is read.
163+
164+
## Tag Synchronization
165+
166+
`gh repo sync` knows branches only — a fork's tags are never updated by it, which is why a fork drifts behind its
167+
upstream in releases even while its branches are current. `Paebbels/ghdl` is a live example: `ghdl/ghdl` has 46 tags,
168+
the fork has 19.
169+
170+
The optional fourth field of a configuration line says which tags to follow:
171+
172+
```
173+
ghdl/ghdl=ghdl:master:v\d+\.\d+.*
174+
OSVVM/OSVVM=OSVVM:main,dev:nightly,v\d+\.\d+\.\d+
175+
antonblanchard/microwatt=microwatt::v\d+\.\d+
176+
```
177+
178+
A pattern is either a fixed tag name or a regular expression, and it has to match the **whole** tag name. Patterns are
179+
comma separated, and a line may carry tags without any branch, as the third line shows. Matching is done with
180+
`grep -P`, so PCRE syntax including `\d`, `\w` and `{n,m}` is available. Beware that a fixed name is a regular
181+
expression too: `v1.0` matches `v1x0` as well.
182+
183+
For every matching tag of the upstream repository:
184+
185+
* the fork doesn't have it → it's created, pointing at the same object:
186+
`🏷️ v2.1.0 — created from OSVVM/OSVVM@a1b2c3d`
187+
* the fork has it, at the same object → counted as up to date, reported as one line per repository,
188+
* the fork has it, at a **different** object → the tag moved upstream. It's reported as an error and **left alone**,
189+
because rewriting it would silently discard whatever the fork's tag points at:
190+
`❌ v1.0.0 — moved in 'OSVVM/OSVVM' (fork 90e6af7, upstream d3d07ba)`
191+
The run continues with the next tag. Delete the tag in the fork to let the next run recreate it.
192+
193+
Tags are never deleted from the fork, and a tag that exists only in the fork is left untouched.
157194
158195
## Error Handling
159196
@@ -164,7 +201,9 @@ Each of these is counted, reported as a GitHub Actions error annotation, and let
164201
* an `<organisation>.repos` file listed in the index file doesn't exist,
165202
* a repository line is malformed — no `=`, no `:`, or an empty `<upstream>`, `<fork>` or branch list,
166203
* `gh repo sync` fails for a fork's branch. Its output is quoted below the failed command, and the annotation names the
167-
upstream repository, the fork and the branch.
204+
upstream repository, the fork and the branch,
205+
* a tag moved in the upstream repository, or a tag can't be created in the fork,
206+
* the tags of a repository can't be read, or the runner's `grep` has no `-P` support.
168207
169208
A failing fork doesn't stop the run: every other fork is still synchronized, and the summary lists what wasn't.
170209
@@ -185,12 +224,15 @@ A repository that carries the inline script keeps its `*.repos` files and replac
185224
with the `uses:` step shown above. `targetOrganisation=<name>`, the constant that had to be edited in every copy,
186225
becomes the `target-organisation` parameter — and can be dropped where the namespace is the one the workflow runs in.
187226
188-
The configuration file format is unchanged. One field is worth a look while converting: `<upstream>` is reported in the
189-
progress and error output, so a copied placeholder there makes the log name the wrong repository.
227+
The configuration file format is backwards compatible — a line without the tag field behaves exactly as before. Two
228+
fields are worth a look while converting: `<upstream>` is reported in the progress and error output, so a copied
229+
placeholder there makes the log name the wrong repository, and adding tag patterns is what stops the fork from drifting
230+
behind in releases.
190231
191232
## Dependencies
192233
193234
* [GitHub CLI (`gh`)](https://cli.github.com/), pre-installed on GitHub-hosted runners.
235+
* `grep` with PCRE support (`-P`), for tag patterns only. GNU grep on the Linux runners has it.
194236
195237
## Contributors
196238

0 commit comments

Comments
 (0)