Skip to content

ci: automatic releases from develop, versioned by upstream - #9

Merged
HerrSammyDE merged 2 commits into
developfrom
ci/automatic-releases
Aug 3, 2026
Merged

ci: automatic releases from develop, versioned by upstream#9
HerrSammyDE merged 2 commits into
developfrom
ci/automatic-releases

Conversation

@HerrSammyDE

@HerrSammyDE HerrSammyDE commented Aug 3, 2026

Copy link
Copy Markdown
Member

Releases needed a human to push a v* tag and a second human to click Publish on the resulting draft. Both steps are gone: a merge into develop now cuts the release by itself, in the shape emeraldhost/system-node-agent uses.

The version is never chosen

system-node-agent derives its version from Conventional Commits via release-please. That is the exact opposite of what we need here, so the pipeline shape was kept and the version source replaced.

prepare fetches pterodactyl/wings' tags into refs/upstream-tags/ and picks the newest stable one that is an ancestor of develop. Since we merge the upstream tag commit itself, that is precisely "which upstream version is in develop" — our releases keep carrying upstream's numbering, with nothing to maintain by hand and no way for it to drift.

The private ref namespace is not cosmetic: our fork carries tags with the same names pointing at our own merge commits (our v1.13.1 is f7ba42d, upstream's is e771816), so a plain git fetch --tags is rejected with "would clobber existing tag".

Verified against the real history:

Branch Detected
develop 1.13.1 — already published, run is a no-op
upgrade-1.13.2 (#8) 1.13.2 — merging it cuts the release
this branch 1.13.1 — merging it alone releases nothing

sort -V picking 1.13.1 over 1.7.5 also confirms the ordering is numeric, not lexical. Release candidates (v1.11.0-rc.1) are filtered out.

Nothing becomes visible until it is verified

push develop
  └─ prepare ──► upstream version + CHANGELOG gate + DRAFT release
       ├─ binary.yaml ─┐
       └─ docker.yaml ─┴─► publish ──► verify assets, move :latest by digest, un-draft

The draft holds only tag_name and target_commitish, so an aborted run leaves no tag behind — GitHub creates the tag on publish. /releases/latest excludes drafts and :latest is not pushed by the image build, so a failure anywhere before publish leaves the previous release and the previous :latest completely untouched.

docker.yaml had to change, or images would have stopped silently

It listened on release: published. That only ever fired because a human clicked Publish — a release published with GITHUB_TOKEN triggers no workflow runs. Automating that click without converting docker.yaml into a workflow_call target would have quietly ended every image build. It keeps its push: develop trigger for :develop.

Image tags: :1.13.2 and :v1.13.2 (the latter is what we publish today, kept for compatibility), plus :latest moved by digest in publish.

Also in here

  • Empty release notes are now impossible. A missing CHANGELOG.md section fails the job instead of publishing an empty body — which is what would have happened on the next release, since there was no ## v1.13.2 section. The match is anchored, so v1.13.2 no longer also matches v1.13.20.
  • Dropped the release/vX branch and its sed bump of system/const.go. Inherited from upstream, it wrote a version into a branch nobody uses while the real version comes from ldflags — and a git push mid-release is exactly what makes a second run for the same version fail.
  • Added SHA256SUMS, without which the verification step has little to verify.
  • Added a smoke test asserting the built binary reports the version it was built with, catching a broken ldflags path that would otherwise ship a binary reporting develop.
  • FORK_CHANGES.md now marks the pipeline as fork-specific, so upstream's release.yaml does not get merged over it.

Behaviour changes to be aware of

  • Pushing a v* tag by hand no longer releases anything. Releases come from develop and nowhere else.
  • Re-releasing a version is an explicit force: true dispatch, which deletes the release and its tag and cuts it again from the current develop. Deleting a release in the UI deliberately does not resurrect it — otherwise an accidental deletion would rebuild it from whatever happens to be on develop at the time.
  • Recovery from a failed run is "Re-run failed jobs" on the original run. prepare treats an existing draft as resume; only a published release blocks a new run.

Verification

  • actionlint (with shellcheck) clean on all three workflows.
  • Version detection and the CHANGELOG extraction were run against this repo's real history and CHANGELOG.md, results above.

Merge order

#8 is already merged, so develop sits on upstream v1.13.2 with no release cut — the old workflow only fired on a tag push. Merging this PR therefore cuts v1.13.2 immediately, which is the intended first live run, and it is safe by construction: a failure anywhere leaves the release a draft and :latest untouched.

The ## v1.13.2 changelog section the gate requires rides along in this PR. It was written for #8 but landed on that branch after the PR had already been merged, so develop does not have it — without it here, the first automatic run would fail on its own gate.

Releases needed a human to push a v* tag and a second human to click Publish
on the resulting draft. Both steps are gone: a merge into develop now cuts the
release by itself.

The version is never chosen. `prepare` fetches pterodactyl/wings' tags into
refs/upstream-tags/ (a private namespace, because our fork carries tags with
the same names pointing at our own merge commits) and picks the newest stable
one that is an ancestor of develop. That is precisely "which upstream version
is in develop", so our releases keep carrying upstream's numbering with nothing
to maintain by hand.

Nothing becomes visible until everything is verified. The draft release holds
only tag_name and target_commitish, so an aborted run leaves no tag behind;
binary.yaml and docker.yaml attach their output to it; and only once both
succeed does `publish` check the assets are all present, move :latest by digest
and un-draft. A failure anywhere before that leaves the previous release and
the previous :latest untouched, and /releases/latest ignores drafts throughout.

docker.yaml had to become a workflow_call target. It listened on
`release: published`, which only ever fired because a human clicked Publish —
a release published with GITHUB_TOKEN triggers no workflow runs, so automating
that click would have silently ended every image build.

Also:
- Empty release notes are now impossible: a missing CHANGELOG.md section fails
  the job instead of publishing a release with an empty body. The section match
  is anchored, so v1.13.2 no longer also matches v1.13.20.
- Dropped the release/vX branch and its sed bump of system/const.go. It wrote a
  version into a branch nobody uses while the real version comes from ldflags,
  and a git push mid-release is exactly what breaks a second run.
- Added SHA256SUMS, and a smoke test asserting the built binary reports the
  version it was built with rather than "develop".
- Re-releasing a version is an explicit `force: true` dispatch. Deleting a
  release by hand deliberately does not resurrect it.
Release notes are extracted from the matching CHANGELOG.md section, so without
this the release for v1.13.2 would carry an empty body.
@HerrSammyDE
HerrSammyDE merged commit 3475f00 into develop Aug 3, 2026
6 checks passed
@HerrSammyDE
HerrSammyDE deleted the ci/automatic-releases branch August 3, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant