Garlemald Client uses Semantic Versioning 2.0.0 driven by
git tags. The git tag vX.Y.Z is the source of truth; [package].version in
Cargo.toml (which stamps the launcher binary and the embedded Windows resource)
and Cargo.lock are kept in lockstep automatically. This mirrors the
Garlemald-Server release workflow;
the only difference is [package].version here vs. [workspace.package].version
there.
developis the default branch and the integration branch for day-to-day work. Branch features offdevelopand PR back into it.developis protected and requires the CI checks (fmt/clippy/build-teston Linux/macOS/Windows) plus an open pull request before merging (no approving review is required, so you can merge your own PR once CI is green).mainis the protected release branch. A release is cut by opening a PR fromdevelopintomain; when it merges, the push tomaintriggersrelease.yml(version bump + tag), which in turn triggersrelease-binaries.yml(per-platform launcher artifacts + the Discord announcement). Nothing merged intodevelopproduces a release or a tag — only thedevelop→mainmerge does.
Flow: feature → develop (CI-gated) → release PR develop → main (CI-gated) →
automatic bump + tag + GitHub Release with launcher artifacts + Discord announcement.
mainintentionally does not require a pull request, so the release automation can push the bump commit straight to it viaRELEASE_PAT.The version bump lands on
mainonly (the tag is the source of truth), sodevelop'sCargo.tomlversion may lag the latest tag between releases. This does not cause merge conflicts — onlyrelease.ymlever edits the version line, so eachdevelop→mainmerge keepsmain's higher version automatically. If you'd rather keepdevelop's version current, periodically mergemainback intodevelop(a standard git-flow back-merge).
.github/workflows/release.yml runs on every push/merge to main (i.e. when a
develop → main release PR merges) and:
- reads the highest existing
vX.Y.Ztag, - picks a bump level (see below),
- rewrites
[package].versioninCargo.tomland runscargo update --workspaceto syncCargo.lock, - commits that back to
mainaschore(release): vX.Y.Z, - pushes an annotated
vX.Y.Ztag, and - publishes a GitHub Release with auto-generated notes.
After a release, git describe --tags on main and [package].version report the
same X.Y.Z, and cargo build stamps the launcher (and its Windows resource
version) with it.
| Bump | How to trigger |
|---|---|
| Patch | Default. Any merge to main 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. |
Guidance: bump minor for a new launcher feature, major for a breaking
config-file or Wine-prefix layout change. The label is read from the PR associated
with the merge commit; if both are present, release:major wins.
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, …).
The workflow pushes the version-bump commit to main, which is branch-protected
(requires the fmt/clippy/build-test checks from issue #9). The default
GITHUB_TOKEN cannot push to a protected branch, so the workflow uses a Personal
Access Token:
- Create a fine-grained PAT owned by a repo admin, scoped to only
Garlemald-Client, with Repository permissions → Contents: Read and write. - Save it as the repository secret
RELEASE_PAT. - Keep branch protection's "Do not allow administrators to bypass" OFF
(
enforce_admins: false). The admin-owned PAT relies on that exemption to push the bump commit past the required checks. If you turn admin enforcement on, the bump push will be rejected.
The release:minor / release:major labels must exist in the repo (created as
part of issue #8).
Security note. Because
RELEASE_PATis admin-owned andenforce_adminsis off, a leaked token can push tomainbypassing the required checks — a strictly larger capability than vanilla Contents: write. Give it a short expiry and rotate on a schedule. The release step also needscrates.ioreachable (it runscargo update --workspace); a registry/network blip fails the run, which is safe to re-run.
A PAT push re-triggers workflows (unlike the default token), so the bump commit's
push to main would re-run this workflow. That loop is broken by the job's if
guard, which skips commits authored by github-actions[bot].
The bump commit is deliberately not marked [skip ci]. [skip ci] would
suppress not just this workflow but also the tag-push event — and a future
per-platform binary-release workflow (mirroring Garlemald-Server's
release-binaries.yml) is expected to trigger on that tag push, so a skip marker
would silently prevent those builds.
release-binaries.yml builds an ad-hoc (unsigned) .app by default — Gatekeeper
warns on first launch. To ship a Gatekeeper-clean, Developer ID-signed and
notarized .app, add the secrets below; the workflow auto-activates signing +
notarization when they are present (it gates on the cert and API-key secrets and
otherwise falls back to ad-hoc — no workflow edit needed to switch on/off).
What the signed path does: scripts/package-macos.sh --sign codesigns the universal
binary and bundle with the Hardened Runtime (--options runtime), a secure
timestamp, and assets/entitlements.plist (the wry/WKWebView JIT needs
com.apple.security.cs.allow-jit); CI then xcrun notarytool submit --waits the
zip, stapler staples the ticket onto the bundle, re-zips, and runs spctl as a
final Gatekeeper gate.
Required repository secrets (all from a paid Apple Developer Program account;
you must be Account Holder/Admin). Signing requires both MACOS_CERT_P12_BASE64
and ASC_API_KEY_P8_BASE64; if either is missing the build stays ad-hoc:
| Secret | What it is |
|---|---|
MACOS_CERT_P12_BASE64 |
A Developer ID Application cert + private key, exported as .p12, base64-encoded. |
MACOS_CERT_PWD |
The export password set on the .p12. Must be non-empty — macOS security import cannot import a passwordless .p12 (it fails MAC verification), so set an export password when you export the cert. |
ASC_API_KEY_P8_BASE64 |
An App Store Connect team API key (.p8), base64-encoded (downloads once). |
ASC_KEY_ID |
The 10-character Key ID. |
ASC_ISSUER_ID |
The Issuer ID (UUID) from App Store Connect → Users and Access → Integrations. |
(The signing identity is derived automatically by SHA-1 hash from the imported
cert, so there is no MACOS_CERT_IDENTITY secret to keep in sync.)
(The CI keychain password is generated per run inside the workflow, so it is not a secret you provide.)
Keep branch protection's "Do not allow administrators to bypass" OFF
(enforce_admins: false) — unrelated to signing, but the same precondition the
release bot relies on.
Runtime caveat (verify after the first signed release): the launcher downloads the Sikarugir Wine runtime and spawns it as a subprocess (it does not load those dylibs in-process, so the notarized launcher needs no
disable-library-validation).ureqdownloads are usually not quarantined, so the spawned Wine should run under the notarized launcher — but confirm the game launches on a clean Mac. If Gatekeeper blocks the Wine binary, the fix is small: stripcom.apple.quarantine(and/or ad-hoc re-sign) the runtime insrc/platform/macos.rs::ensure_runtime_downloadedafter extraction.
The sequence starts from the v0.1.0 tag on main. The first merge after the
release automation lands bumps it to v0.1.1 (or the labeled minor/major).