294 lines
19 KiB
Markdown
294 lines
19 KiB
Markdown
# Releasing GridCraft
|
||
|
||
Every push to the `release` branch runs `.github/workflows/release.yml`. The workflow builds
|
||
signed installers for macOS, Windows, Linux, FreeBSD and the web, then creates or updates a
|
||
**draft** GitHub Release named `GridCraft v<version>`. Nobody sees a draft until a maintainer
|
||
publishes it.
|
||
|
||
This is GridCraft's implementation of the shared [release playbook](release-playbook.md)
|
||
(craftrules `release/playbook.md`), copied from DesignCraft and renamed. User-facing names say
|
||
**GridCraft**. Files, binaries and ids stay lowercase
|
||
(`gridcraft-<version>-<platform>-<arch>.<ext>`, `ai.storyteller.gridcraft`).
|
||
|
||
| App-specific value | Where |
|
||
|---|---|
|
||
| Display name `GridCraft`, binaries `gridcraft` + `gridcraft-cli` | every script and workflow |
|
||
| Bundle / app id `ai.storyteller.gridcraft` | `packaging/macos/Info.plist.in`, `packaging/linux/*`, flatpak manifest |
|
||
| WiX UpgradeCode `5511327B-F305-41BE-92D0-EB84944E7C4D` (fixed forever) | `packaging/windows/gridcraft.wxs` |
|
||
| File types `.xlsx` `.xlsm` `.xlsb` `.ods` `.csv` `.tsv` | Info.plist (`CFBundleDocumentTypes`), WiX (ProgIds/OpenWithProgids), `.desktop` `MimeType=`, `ai.storyteller.gridcraft.mime.xml`, metainfo |
|
||
| Web app dir `apps/gridcraft-web` | `packaging/web/package.sh` |
|
||
| App colour `#1f9d55` (ink `#147a40`), **proposed** | `assets/app-icon/README.md`, metainfo `<branding>` |
|
||
|
||
## Prerequisites still owned by the app crates
|
||
|
||
The pipeline references crates that don't exist yet; the first release run fails until they do:
|
||
|
||
- **`apps/gridcraft`** (the GUI binary `gridcraft`) and **`apps/gridcraft-web`** (trunk
|
||
site) must be workspace members. `apps/gridcraft-cli` exists and supports `--version`.
|
||
- **Windows resources:** move `packaging/windows/app-build.rs.in` to `apps/gridcraft/build.rs`
|
||
and add `winresource = "0.1"` under `[build-dependencies]` (instructions at the top of the
|
||
file). `package.ps1` sets `GRIDCRAFT_REQUIRE_WINRES=1`, so a release build without the icon
|
||
fails, and it checks that `gridcraft.exe` is a GUI-subsystem binary, so `main.rs` needs
|
||
`#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]`.
|
||
- **Build info:** CI exports `GRIDCRAFT_BUILD_SHA` and `GRIDCRAFT_BUILD_DATE`; read them
|
||
with `option_env!` (fallback: "dev build") for `--version` and *Help › About GridCraft*.
|
||
- **Runtime icon:** embed `assets/app-icon/gridcraft-macos-512.png` (macOS Dock) and
|
||
`hicolor/256x256/apps/ai.storyteller.gridcraft.png` (Windows/Linux) as the window icon, and
|
||
use `ai.storyteller.gridcraft` as the Wayland app id / `StartupWMClass`.
|
||
- **`cargo xtask` alias:** the docs use `cargo xtask …`, which needs
|
||
`.cargo/config.toml` with `[alias] xtask = "run --package xtask --"`. Until it exists, use
|
||
`cargo run -p xtask -- …`. The workflows already call `cargo run -p xtask` directly.
|
||
|
||
## Cutting a release
|
||
|
||
1. **Bump the version** on `main`. The only place it lives is `[workspace.package] version`
|
||
in the root `Cargo.toml`:
|
||
|
||
```sh
|
||
cargo xtask version # prints the current version, e.g. 0.1.0
|
||
cargo xtask version set 0.2.0 # or 0.2.0-rc.1; updates Cargo.toml and Cargo.lock
|
||
```
|
||
|
||
Commit the change (`Cargo.toml` + `Cargo.lock`) through the normal review flow.
|
||
2. **Merge `main` into `release`** (or fast-forward it) and push. The workflow starts by itself.
|
||
3. **Wait for the draft.** Notarization and the FreeBSD VM are the slow parts. The Releases
|
||
page then has a draft `GridCraft v0.2.0`, tagged `v0.2.0` on the pushed commit, with every
|
||
artifact and `SHA256SUMS.txt`. The notes are generated from the merged PRs.
|
||
4. **Check it.** Download an installer or two and read the job summaries. Any `::warning::`
|
||
there means a signing secret was missing and that artifact is unsigned.
|
||
5. **Add the parity delta** to the notes: `git diff v<previous> v<this> -- docs/parity-checklist.md`
|
||
(regenerated by `cargo xtask parity`).
|
||
6. **Publish** the draft in the GitHub UI. Publishing creates the `v0.2.0` tag. Versions with a
|
||
pre-release suffix (`-rc.1`) are marked as pre-releases.
|
||
|
||
Pushing to `release` again before you publish rebuilds the same draft and replaces its assets.
|
||
After the draft is published, the workflow refuses to touch that version again, so bump first.
|
||
|
||
**Test runs:** *Actions → Release → Run workflow* runs the whole pipeline by hand. The optional
|
||
`version` input (such as `0.2.0-rc.1`) overrides `Cargo.toml` for that run only; each job
|
||
applies it with `cargo xtask version set` before building, so the binaries report it too. The
|
||
signing jobs and the draft release need the `release` environment, which only the `release`
|
||
branch can use. Run from any other branch, it's a dry run: Linux, Flatpak, FreeBSD and web build
|
||
and upload their artifacts, the environment refuses macOS and Windows, and no release is drafted
|
||
(`gh workflow run release.yml --ref <branch>`).
|
||
|
||
## What gets built
|
||
|
||
| Platform | Artifacts | Built on |
|
||
|---|---|---|
|
||
| macOS 11+ (universal: Apple silicon + Intel) | `gridcraft-<v>-macos-universal.dmg`, `gridcraft-cli-<v>-macos-universal.zip` | `macos-15` |
|
||
| Windows 10+ x64 | `gridcraft-<v>-windows-x64.msi`, `gridcraft-<v>-windows-x64-portable.zip` | `windows-latest` |
|
||
| Windows 10+ x86 (32-bit) | `gridcraft-<v>-windows-x86.msi`, `gridcraft-<v>-windows-x86-portable.zip` | `windows-latest` |
|
||
| Windows 11 ARM64 | `gridcraft-<v>-windows-arm64.msi`, `…-portable.zip` (cross-compiled, signed like x64) | `windows-latest`; installed and run on `windows-11-arm` by `windows-arm64.yml` |
|
||
| Linux x86_64 | `gridcraft-<v>-linux-x86_64.{AppImage,AppImage.zsync,deb,rpm,tar.gz}` | `ubuntu-22.04` |
|
||
| Linux aarch64 | `gridcraft-<v>-linux-aarch64.{AppImage,AppImage.zsync,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` |
|
||
| Linux riscv64 | `gridcraft-<v>-linux-riscv64.tar.gz` (cross-compiled, glibc >= 2.39; CLI smoke-tested under QEMU) | `ubuntu-24.04` |
|
||
| Flatpak x86_64, aarch64 | `gridcraft-<v>-linux-<arch>.flatpak` (repackages the Linux tarball) | `ubuntu-24.04`, `ubuntu-24.04-arm` |
|
||
| FreeBSD 14 x86_64 | `gridcraft-<v>-freebsd-x86_64.tar.gz` | FreeBSD 14.3 VM (`freebsd.yml`, called by `release.yml`) |
|
||
| Web | `gridcraft-web-<v>.zip` (static site; see [`packaging/web/README.md`](../packaging/web/README.md)) | `ubuntu-latest` |
|
||
|
||
### macOS
|
||
|
||
`packaging/macos/package.sh` builds `aarch64-apple-darwin` and `x86_64-apple-darwin` with
|
||
`MACOSX_DEPLOYMENT_TARGET=11.0`, joins them with `lipo`, and assembles `GridCraft.app`:
|
||
|
||
- `Info.plist` comes from `Info.plist.in`: bundle id `ai.storyteller.gridcraft`,
|
||
`LSMinimumSystemVersion` 11.0, `NSHighResolutionCapable`, category productivity, and
|
||
`CFBundleDocumentTypes` for `.xlsx`, `.xlsm`, `.xlsb`, `.ods`, `.csv` and `.tsv` with rank
|
||
**Alternate**, so GridCraft is offered in *Open With* without taking over Numbers' (or another
|
||
app's) defaults. `.xlsm` and `.xlsb` get imported UTIs in case macOS doesn't know them.
|
||
- **Signing** goes inside-out with the hardened runtime and a secure timestamp; no `--deep` on
|
||
the final signature; `entitlements.plist` is deliberately empty.
|
||
- **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then
|
||
stapled. The DMG (with an `Applications` link) is signed, notarized and stapled too, and
|
||
checked with `codesign --verify --strict`, `stapler validate` and `spctl -a -vvv`.
|
||
Its Finder window (background, icon size and positions) comes from
|
||
[`packaging/macos/dmg/`](../packaging/macos/dmg/README.md), and its volume is named `GridCraft`
|
||
without the version, which the window's background needs; the DMG file name keeps the version.
|
||
- **CLI:** the universal `gridcraft-cli` is signed, zipped and notarized.
|
||
|
||
Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization:
|
||
|
||
```sh
|
||
packaging/macos/package.sh # universal; needs both rustup targets
|
||
packaging/macos/package.sh --arch aarch64 # quicker, host-only
|
||
open dist/release/gridcraft-*-macos-*.dmg
|
||
```
|
||
|
||
### Windows
|
||
|
||
`packaging/windows/package.ps1 -Arch x64|x86|arm64` builds with `-C target-feature=+crt-static`
|
||
(no VC++ redistributable needed), checks both PE headers (machine matches `-Arch`; `gridcraft.exe`
|
||
is GUI, `gridcraft-cli.exe` console), signs both executables, builds the MSI with WiX v5, signs
|
||
the MSI, and zips a portable build.
|
||
|
||
- `gridcraft.wxs`: per-machine install into Program Files, advertised Start Menu shortcut, App
|
||
Paths (Win+R `gridcraft`). File types: ProgIds `GridCraft.Workbook` (`.xlsx`, `.xlsm`) and
|
||
`GridCraft.TextData` (`.csv`, `.tsv`), listed under each extension's `OpenWithProgids` and
|
||
in `Capabilities` + `RegisteredApplications`, so GridCraft appears in *Open with* and
|
||
*Settings › Default apps* but never silently becomes the default. The MSI version is the
|
||
numeric `X.Y.Z`; same-version upgrades let release candidates replace each other.
|
||
- **Desktop shortcut:** full installer UI offers an unchecked checkbox on a new installation.
|
||
Unattended installs can use `msiexec /i gridcraft.msi /qn DESKTOPSHORTCUT=1` to opt in
|
||
(or `DESKTOPSHORTCUT=0` to opt out). The choice is stored per machine and reused on upgrades;
|
||
repair retains the installed component choice, and uninstall removes the managed shortcut.
|
||
The Start Menu shortcut is always installed. Run
|
||
the Packaging lint workflow’s Windows job to build small fixture installers and check
|
||
installation, repair, upgrade, opt-out and cleanup, including removal by SYSTEM after
|
||
installation by the runner account. The shortcut marker uses `HKMU`, which resolves to
|
||
`HKLM` for this per-machine package; it leaves no installing-user registry marker. Unlike
|
||
a literal `HKLM` key path, MSI registry root -1 also meets ICE43's shortcut rule without
|
||
suppressing validation ([WiX registry roots](https://docs.firegiant.com/wix/schema/wxs/registryvalue/),
|
||
[ICE43](https://learn.microsoft.com/windows/win32/msi/ice43)). Its
|
||
`packaging/windows/test-desktop-shortcut.ps1` script requires a GitHub-hosted Windows runner.
|
||
- **Installer review images:** the same Windows job renders the compiled MSI's dialogs through
|
||
Windows Installer's native preview API and uploads PNGs plus install logs in
|
||
`desktop-shortcut-msi-evidence`, on success or failure. These are native dialog previews;
|
||
the separate silent lifecycle test verifies installation behavior. Normal full UI presents
|
||
the shortcut choice and then completion; cancel/failure and files-in-use dialogs are support
|
||
paths. To review on a maintainer branch, run **Packaging lint** with that branch selected
|
||
(or `gh workflow run packaging-lint.yml --repo storytold/gridcraft --ref BRANCH`). No release
|
||
environment, signing credentials or Rust build is required. Interactive checkbox behavior
|
||
still needs a Windows desktop check; previews alone do not establish that behavior.
|
||
- **Signing:** `packaging/windows/sign.ps1` uses `signtool` with SHA-256 and an RFC 3161
|
||
timestamp, from a `.pfx` (`WINDOWS_CERTIFICATE*`) if present, otherwise **Azure Trusted
|
||
Signing** (`AZURE_*`, the storytold setup). With neither it warns and leaves files unsigned.
|
||
|
||
Locally on Windows: `dotnet tool install -g wix --version 5.0.2`, then
|
||
`pwsh packaging/windows/package.ps1 -Arch x64`.
|
||
|
||
### Linux
|
||
|
||
`packaging/linux/package.sh` stages one FHS tree (both binaries,
|
||
`ai.storyteller.gridcraft.desktop` with `MimeType=` for the four types, hicolor icons 16–512 px
|
||
+ scalable SVG, AppStream metainfo, shared-mime-info) and builds the AppImage (appimagetool),
|
||
`.deb` and `.rpm` (nfpm, `nfpm.yaml`) and `.tar.gz` from it. Built on Ubuntu 22.04, so the
|
||
binaries need glibc ≥ 2.35. X11/Wayland/xkbcommon/Vulkan/EGL are loaded at runtime; the
|
||
packages declare them (see `nfpm.yaml`).
|
||
|
||
Each AppImage embeds update information
|
||
(`gh-releases-zsync|storytold|gridcraft|latest|gridcraft-*-linux-<arch>.AppImage.zsync`), and the
|
||
matching `.zsync` is published beside it, so AppImageUpdate and AppImageLauncher fetch only the
|
||
changed blocks of the newest published release. appimagetool writes the `.zsync` when
|
||
`zsyncmake` (the `zsync` package) is installed; without it the script warns and skips it.
|
||
|
||
Flatpak: `packaging/linux/flatpak/ai.storyteller.gridcraft.yml` builds from source and is ready
|
||
for a Flathub submission (freedesktop 25.08; Wayland + X11 fallback, `dri`, IPC; Documents and
|
||
Downloads; everything else through portals); build it by hand with the commands in its header.
|
||
The single-file `.flatpak` on each release comes from `packaging/linux/flatpak-bundle.sh` and
|
||
`ai.storyteller.gridcraft.bundle.yml`, which repackage the release's Linux tarball (no Rust
|
||
build) and smoke-test the result in the sandbox. packaging-lint checks that both manifests
|
||
agree on runtime and finish-args.
|
||
|
||
Locally (on Linux): install [nfpm](https://nfpm.goreleaser.com/install/), then
|
||
`packaging/linux/package.sh` (or `--formats "deb tar"`).
|
||
|
||
### FreeBSD
|
||
|
||
GitHub has no FreeBSD runners. `.github/workflows/freebsd.yml` boots a FreeBSD 14.3 VM
|
||
(`vmactions/freebsd-vm`). Run by hand from `release`, it builds and tests the workspace; called
|
||
from `release.yml` with `package: true` it runs `packaging/freebsd/package.sh`, which builds the
|
||
release binaries and writes `gridcraft-<v>-freebsd-x86_64.tar.gz` (`bin/` + `share/`, extract
|
||
under `/usr/local`). The target dir lives outside the synced checkout so only `dist/release`
|
||
comes back from the VM. No signing (checksums are in `SHA256SUMS.txt`).
|
||
|
||
### Web
|
||
|
||
`packaging/web/package.sh` runs `trunk build --release --dist dist/web --public-url ./` in
|
||
`apps/gridcraft-web` and zips the site with sample `_headers` / `.htaccess` and the hosting guide
|
||
([`packaging/web/README.md`](../packaging/web/README.md)). Relative URLs only, so it works under
|
||
any path and in an iframe; the script fails on root-absolute URLs.
|
||
|
||
## Secrets and GitHub setup (org admin, once)
|
||
|
||
All signing secrets live in the repository's **`release` environment**, deployable only from the
|
||
`release` branch; a ruleset restricts who can push that branch. Every job in `release.yml`
|
||
declares `environment: release`; the FreeBSD job is a called workflow and needs no secrets.
|
||
Each secret is optional: if one is missing that platform's artifacts are unsigned and the run
|
||
shows a `::warning::`. Never store these as org- or repo-level Actions secrets (playbook §3).
|
||
|
||
| Secret | Used for |
|
||
|---|---|
|
||
| `APPLE_CERTIFICATE` | single-line base64 `.p12`: "Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" |
|
||
| `APPLE_CERTIFICATE_PASSWORD` | password for that `.p12` |
|
||
| `KEYCHAIN_PASSWORD` | password for the temporary CI keychain (random if unset) |
|
||
| `APPLE_ID` | Apple ID used by `notarytool` |
|
||
| `APPLE_PASSWORD` | app-specific password for that Apple ID |
|
||
| `APPLE_TEAM_ID` | `DJ6XS33FX8` |
|
||
| `WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD` | `.pfx` signing (unused by storytold; Azure is used) |
|
||
| `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` | service principal for Azure Trusted Signing |
|
||
| `AZURE_SIGNING_ENDPOINT` | `https://eus.codesigning.azure.net` |
|
||
| `AZURE_SIGNING_ACCOUNT`, `AZURE_CERT_PROFILE` | `LearningMachinesInc`, `learningmachines` |
|
||
|
||
`GITHUB_TOKEN` creates the release; only the final job gets `contents: write`.
|
||
|
||
The exact commands (from craftrules `release/signing-setup.md`, steps 1–3), run by an org
|
||
admin. Done for storytold/gridcraft on 2026-10-07; the `release-managers` team also has write
|
||
access to the repo, which its ruleset bypass needs.
|
||
|
||
```sh
|
||
REPO=storytold/gridcraft
|
||
RMTEAM_ID=19837241 # release-managers team; `gh api orgs/storytold/teams/release-managers --jq .id`
|
||
|
||
# 1a. release branch from the default branch head
|
||
gh api repos/$REPO/git/ref/heads/release >/dev/null 2>&1 || \
|
||
gh api --method POST repos/$REPO/git/refs -f ref=refs/heads/release \
|
||
-f sha="$(gh api repos/$REPO/git/ref/heads/$(gh api repos/$REPO --jq .default_branch) --jq .object.sha)"
|
||
|
||
# 1b. release environment, deployable only from the release branch
|
||
printf '{"deployment_branch_policy":{"protected_branches":false,"custom_branch_policies":true}}' \
|
||
| gh api --method PUT repos/$REPO/environments/release --input -
|
||
gh api --method POST repos/$REPO/environments/release/deployment-branch-policies -f name=release -f type=branch >/dev/null 2>&1 || true
|
||
|
||
# 1c. ruleset: only org admins + release-managers may create/update/delete/force-push release
|
||
cat > /tmp/ruleset.json <<JSON
|
||
{ "name":"release","target":"branch","enforcement":"active",
|
||
"conditions":{"ref_name":{"include":["refs/heads/release"],"exclude":[]}},
|
||
"rules":[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"non_fast_forward"}],
|
||
"bypass_actors":[{"actor_id":0,"actor_type":"OrganizationAdmin","bypass_mode":"always"},
|
||
{"actor_id":$RMTEAM_ID,"actor_type":"Team","bypass_mode":"always"}] }
|
||
JSON
|
||
gh api --method POST repos/$REPO/rulesets --input /tmp/ruleset.json >/dev/null 2>&1 || echo "ruleset exists"
|
||
|
||
# 2. sync the twelve secrets into the release environment (run in ~/secrets/storytold/)
|
||
for s in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID KEYCHAIN_PASSWORD \
|
||
AZURE_TENANT_ID AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_SIGNING_ENDPOINT AZURE_SIGNING_ACCOUNT AZURE_CERT_PROFILE; do
|
||
[ -f "$s.txt" ] && gh secret set "$s" --env release --repo "$REPO" < "$s.txt" && echo " set $s" || echo " MISSING $s.txt"
|
||
done
|
||
gh secret list --env release --repo "$REPO" --json name --jq '[.[].name]|sort|join(", ")'
|
||
|
||
# 3. test build (rc version so it can't collide with a real release)
|
||
gh workflow run release.yml --repo $REPO --ref release -f version=0.1.0-rc.1
|
||
RID=$(gh run list --repo $REPO -w release.yml -L1 --json databaseId --jq '.[0].databaseId')
|
||
until [ "$(gh run view $RID --repo $REPO --json status --jq .status)" = completed ]; do sleep 60; done
|
||
gh run view $RID --repo $REPO --json conclusion,jobs --jq '.conclusion, (.jobs[]|"[\(.conclusion)] \(.name)")'
|
||
```
|
||
|
||
In the macOS job log expect `status: Accepted` and `The staple and validate action worked!`; in
|
||
the Windows jobs, `Successfully signed` and a passing `signtool verify /pa`. Then check every
|
||
artifact installs and launches: Gatekeeper accepts the DMG with no warning, SmartScreen shows
|
||
the publisher.
|
||
|
||
## Icons
|
||
|
||
`assets/app-icon/gridcraft.svg` is a **placeholder** (an original ledger-page drawing on
|
||
GridCraft green) until the owner makes GridCraft's engraved creature portrait (craftrules
|
||
`standards/icon-design.md`). `packaging/icons.sh` regenerates the 1024 px PNG, the macOS 512 px
|
||
PNG, the `.icns` (iconutil), the `.ico` (`cargo xtask ico`) and the hicolor PNGs from it; it
|
||
needs `resvg`. The outputs are committed, so packaging never needs those tools. Every file has a
|
||
row in `ATTRIBUTION.md` (`cargo xtask assets`).
|
||
|
||
## Checks
|
||
|
||
- `.github/workflows/packaging-lint.yml` (seconds, on changes to `packaging/`, the workflows or
|
||
the icons): actionlint, shellcheck, a PowerShell parse, xmllint, a WiX icon-id check,
|
||
`desktop-file-validate`, `appstreamcli validate`, and a Flatpak manifest check.
|
||
- `.github/workflows/windows-arm64.yml` (on `release` pushes): packages ARM64 on x64, then
|
||
installs, runs and uninstalls the MSI on a Windows 11 ARM64 runner.
|
||
- `.github/workflows/freebsd.yml`: FreeBSD build + tests (manual run from `release`).
|
||
|
||
**Only the `release` branch builds.** No build workflow runs on `main` or pull requests; every
|
||
build job checks `github.ref == 'refs/heads/release'`, so a manual run from another branch is
|
||
skipped. Only `packaging-lint.yml` (no build, no secrets) runs on `main` and PRs.
|
||
- Locally: `cargo xtask ci` (fmt, clippy, tests, assets, layers, wasm, parity check).
|