meteor-decomp uses Semantic Versioning 2.0.0 driven
entirely by git tags. The annotated git tag vX.Y.Z is the sole source of
truth — there is nothing in the tree to version. This repo has no
Cargo.toml and no VERSION file, so a release writes no commit back to the
branch: it only creates and pushes a tag (and publishes a GitHub Release).
developis the default branch and the integration branch for day-to-day decomp work. It is unprotected — commit/push to it directly (agent orchestration included) or via PR, whatever fits the moment. Nothing merged intodevelopproduces a release or a tag.masteris the protected release branch. A release is cut by opening a PR fromdevelopintomaster; when it merges,release.ymlfires (tag + GitHub Release + Discord).masterrequires a pull request (no direct pushes) but no approving review, so you can self-merge your owndevelop→masterrelease PR. Any merge method works — squash, merge commit, or rebase (see Whypull_request_target, notpush).
Flow: work on develop → release PR develop → master → merge → automatic
tag + GitHub Release + Discord announcement.
Branch protection on
masterdoes not block the release automation: the workflow pushes only a tag (refs/tags/vX.Y.Z), and tag pushes are not gated by branch-protection rules — only commits to the branch are, which this workflow never makes. So the defaultGITHUB_TOKENstill suffices (see Why no PAT).
.github/workflows/release.yml runs when a develop → master release PR
merges (a pull_request_target closed + merged event), or on a manual
workflow_dispatch, and:
- reads the highest existing
vX.Y.Ztag, - picks a bump level (see below),
- creates an annotated
vX.Y.Ztag on the merge commit and pushes only that tag (no commit, no file edits — nothing is rewritten in the tree), - publishes a GitHub Release with auto-generated notes, best-effort appended with the current decomp progress (see Progress tie-in), and
- posts a Discord release notification (see Discord release notification).
After a release, git describe --tags on master reports the new X.Y.Z.
| Bump | How to trigger |
|---|---|
| Patch | Default. A merged develop → master release PR (or manual dispatch) with no release label → Z increments. |
| Minor | Add the release:minor label to the PR before merging → Y+1, Z=0. |
| Major | Add the release:major label to the PR before merging → X+1, Y=0, Z=0. |
The label is read from the PR associated with the merge commit; if both labels
are present, release:major wins. A run with no associated PR (a manual
workflow_dispatch, or a merge GitHub doesn't link to a PR) is treated as a
patch bump.
The release:minor and release:major labels must exist in the repo.
Because the next version is computed from the highest tag, you can also bump out-of-band by pushing a tag yourself:
git tag -a v0.2.0 -m v0.2.0 && git push origin v0.2.0The automation then continues patch-incrementing from there (v0.2.1, …). This
is handy for the first minor/major when no PR is involved.
The release PR merges develop into master, and develop is saturated with
[skip ci] commits (the reconcile bot stamps every progress-doc commit with
[skip ci]). A squash merge folds all the squashed commit messages —
[skip ci] markers included — into the squash commit's body. GitHub then scans
the merged commit's message, sees [skip ci], and skips every workflow for
that push (including release.yml). With a push: branches: [master] trigger,
a squash-merged release therefore silently never releases — no tag, no
GitHub Release, no Discord ping. (This is exactly what happened to the first
release PR after the Discord step was added.)
pull_request_target keys off the PR-merge event, not the pushed commit, so
it is immune to [skip ci] and fires regardless of merge method (squash,
merge commit, or rebase). The job is gated on pull_request.merged == true (so a
PR closed without merging never releases) and base.ref == 'master'.
Safety: despite the
pull_request_targetevent, this workflow never checks out or runs PR head code — it checks outmasterand reads only git tags and the merged PR's labels. The usualpull_request_targetrisk (running attacker-controlled code with secrets in scope) does not apply.
release.yml also exposes a manual Run workflow button (workflow_dispatch,
no inputs). Use it to cut a release on demand — e.g. to recover a push that was
skipped before this trigger fix landed, or to force a patch bump. It always
checks out and releases master HEAD, computing the next version from the
highest tag exactly like an automatic run.
gh workflow run release.yml --repo swstegall/meteor-decompThis workflow pushes only a tag — it never commits anything back to the
branch. Tag pushes are not gated by branch-protection rules (only commits to
the branch are), so even though master is protected, the default GITHUB_TOKEN
(github.token) can:
- push the
vX.Y.Ztag, and - create the GitHub Release.
So no fine-grained RELEASE_PAT is required (unlike the sibling
garlemald-server / garlemald-client release workflows, which commit a version
bump back to a protected branch and therefore do need a PAT). The checkout uses
the default token.
Because nothing is committed back to master, there is no re-trigger to guard
against:
- The workflow triggers on a PR merge into
master(and manual dispatch). - Pushing a tag creates no PR-close event and matches no trigger here, so
creating
vX.Y.Zcannot re-run this workflow.
So there is deliberately no bot-identity if guard and no [skip ci]
marker — both are only needed when a workflow pushes a commit back to the
branch it triggers on, which this one never does.
The Release body is best-effort augmented with current decomp progress. The
workflow runs tools/progress.py (after pip install pyyaml), captures the
overall (YAML status): and overall (_rosetta/*.cpp): summary lines, and
edits the just-created Release to prepend a short "Decomp progress at
vX.Y.Z" block above GitHub's auto-generated notes.
The create-then-edit approach is used because gh release create cannot combine
--generate-notes with --notes-file; editing afterward keeps the generated
notes and the progress block. The whole progress attempt is wrapped so that a
failure (PyYAML missing, network blip, progress.py change, no YAML present)
never fails the release — the tag and Release publish regardless.
After the tag and GitHub Release publish, release.yml's final step POSTs a
Discord embed announcing the new release to the release-announcements channel
(mirrors the sibling garlemald-server / garlemald-client release announcers).
Because every reached run is a genuine release — a merged develop → master
release PR, or a manual workflow_dispatch, both of which compute and publish a
new version — each successful release is announced; there is no separate event guard.
The embed reads:
Meteor Decomp vX.Y.Z — A new Meteor Decomp release is available: vX.Y.Z.
…linked to the Release page.
This step requires a repo secret DISCORD_RELEASE_WEBHOOK_URL (the Discord
webhook URL for the release-announcements channel — distinct from
DISCORD_PR_WEBHOOK_URL and DISCORD_WEBHOOK_URL). It is a separate webhook so
release pings land in their own channel and a leaked/rotated webhook is scoped to
one purpose. Hardening matches the other Discord workflows:
- the payload is assembled with
jq --arg(every value escaped), so the tag can never break out of the shell or the JSON; - a missing secret logs a
::warning::and skips the post (exit 0) rather than failing — so the release still succeeds before the secret is configured; - a webhook failure (unreachable/rejected) is likewise non-fatal — the tag and Release have already published.
If
DISCORD_RELEASE_WEBHOOK_URLis not set, releases still cut normally; only the Discord ping is skipped (with a warning in the job log). Add the secret to enable announcements:gh secret set DISCORD_RELEASE_WEBHOOK_URL --repo swstegall/meteor-decomp.
The sequence starts from an initial v0.1.0 tag on master HEAD, created
once by a maintainer as a seed step outside this workflow:
git tag -a v0.1.0 -m v0.1.0 <master-HEAD-sha> && git push origin v0.1.0The first develop → master release PR merged after the release automation
lands bumps it to v0.1.1 (or the labeled minor/major). The workflow never
creates v0.1.0 itself.