196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# Releasing PhotoCraft
|
||
|
||
Every push to the `release` branch runs `.github/workflows/release.yml`. The workflow builds
|
||
signed installers for macOS, Windows, Linux and the web, then creates or updates a **draft**
|
||
GitHub Release named `PhotoCraft v<version>`. Nobody sees a draft until a maintainer publishes it.
|
||
|
||
This is PhotoCraft's implementation of the shared
|
||
[release playbook](release-playbook.md). User-facing names say **PhotoCraft**. Files, binaries
|
||
and ids stay lowercase (`photocraft-<version>-<platform>-<arch>.<ext>`, `ai.storyteller.photocraft`).
|
||
|
||
## 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.** After about 30 to 45 minutes (notarization is the slow part), the
|
||
Releases page has a draft `PhotoCraft 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 look at the job summaries. Any
|
||
`::warning::` there means a signing secret was missing and that artifact is unsigned.
|
||
5. **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 it 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. The jobs apply
|
||
it with `cargo xtask version set` before building, so the binaries report it too. The run still
|
||
needs the `release` environment, which only the `release` branch can use, so pick that branch
|
||
in the dialog.
|
||
|
||
## What gets built
|
||
|
||
| Platform | Artifacts | Built on |
|
||
|---|---|---|
|
||
| macOS 11+ (universal: Apple silicon + Intel) | `photocraft-<v>-macos-universal.dmg`, `photocraft-cli-<v>-macos-universal.zip` | `macos-15` |
|
||
| Windows 10+ x64 | `photocraft-<v>-windows-x64.msi`, `photocraft-<v>-windows-x64-portable.zip` | `windows-latest` |
|
||
| Windows 10+ x86 (32-bit) | `photocraft-<v>-windows-x86.msi`, `photocraft-<v>-windows-x86-portable.zip` | `windows-latest` |
|
||
| Linux x86_64 | `photocraft-<v>-linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` |
|
||
| Linux aarch64 | `photocraft-<v>-linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` |
|
||
| Web | `photocraft-web-<v>.zip` (static site; see [`packaging/web/README.md`](../packaging/web/README.md)) | `ubuntu-latest` |
|
||
|
||
Every binary reports its version, the commit and the build date: `photocraft --version`,
|
||
`photocraft-cli --version`, and *Help › About PhotoCraft*. CI sets `PHOTOCRAFT_BUILD_SHA` and
|
||
`PHOTOCRAFT_BUILD_DATE`, and `crates/engine/src/build_info.rs` reads them at compile time. A plain
|
||
`cargo build` doesn't set them and reports `0.2.0 (dev build)`.
|
||
|
||
### 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 `PhotoCraft.app`:
|
||
|
||
- `Info.plist` is generated from `Info.plist.in`. The bundle id is `ai.storyteller.photocraft`.
|
||
The plist sets `LSMinimumSystemVersion` 11.0, `NSHighResolutionCapable`, and document types:
|
||
`.pcraft` (Owner), plus PSD/PSB and the image formats PhotoCraft reads (Alternate, so it never
|
||
takes over Preview's defaults). The icon is `assets/app-icon/photocraft.icns`.
|
||
- **Signing** goes inside-out with the hardened runtime and a secure timestamp. The executable
|
||
is signed first, then the bundle. There's no `--deep` on the final signature. The
|
||
entitlements (`entitlements.plist`) are deliberately empty.
|
||
- **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then the
|
||
ticket is stapled to the app. The app goes on a DMG (`hdiutil`, with an `Applications` link
|
||
to drag onto). The DMG is signed, notarized and stapled too. The script checks the results
|
||
with `codesign --verify --strict`, `stapler validate` and `spctl -a -vvv`.
|
||
- **CLI:** the universal `photocraft-cli` is signed with the hardened runtime, zipped and
|
||
notarized. A bare binary can't hold a stapled ticket, so Gatekeeper looks it up online.
|
||
|
||
Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization.
|
||
That's enough to check the bundle and the DMG on your own Mac:
|
||
|
||
```sh
|
||
packaging/macos/package.sh # universal; needs both rustup targets
|
||
packaging/macos/package.sh --arch aarch64 # quicker, host-only
|
||
open dist/release/photocraft-*-macos-*.dmg
|
||
```
|
||
|
||
### Windows
|
||
|
||
`packaging/windows/package.ps1 -Arch x64|x86` builds with `-C target-feature=+crt-static`.
|
||
The static C runtime means neither the MSI nor the portable zip needs the Visual C++
|
||
redistributable, which matters for a standalone installer and costs only about 100 KB. The flag
|
||
goes in `CARGO_TARGET_<TRIPLE>_RUSTFLAGS`, so host build scripts aren't affected.
|
||
|
||
- `apps/photocraft/build.rs` embeds the icon (`assets/app-icon/photocraft.ico`) and
|
||
VERSIONINFO with the `winresource` crate. It only does this when targeting Windows. Elsewhere
|
||
it's a no-op, and the web build doesn't touch that crate.
|
||
- Release builds use the GUI subsystem, so Start Menu launches don't open a console window.
|
||
- `photocraft.wxs` (WiX v5) is a per-machine install into Program Files with an advertised
|
||
Start Menu shortcut. PhotoCraft becomes the default app for `.pcraft` and is listed under
|
||
"Open with" for PSD/PSB and image files. It also registers App Paths (Win+R `photocraft`).
|
||
The MSI version is the numeric `X.Y.Z`, because MSI has no pre-release field. Same-version
|
||
upgrades are allowed so that release candidates replace each other.
|
||
- **Signing:** `packaging/windows/sign.ps1` signs both `.exe` files and then the `.msi` with
|
||
`signtool`, using SHA-256 and an RFC 3161 timestamp. It uses whichever material is present:
|
||
1. a `.pfx` file (`WINDOWS_CERTIFICATE`, `WINDOWS_CERTIFICATE_PASSWORD`), timestamped by
|
||
DigiCert, or
|
||
2. Azure Trusted Signing (`AZURE_*`): the script downloads the
|
||
`Microsoft.Trusted.Signing.Client` dlib and timestamps with Microsoft's server.
|
||
|
||
The script is the single place to change when the Windows signing setup changes.
|
||
|
||
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 and makes every format from it. The tree
|
||
holds both binaries, `ai.storyteller.photocraft.desktop`, hicolor icons from 16 px to 512 px
|
||
plus a scalable SVG, AppStream metainfo, and a shared-mime-info file for `.pcraft`, `.psb` and
|
||
`.qoi`.
|
||
|
||
Why these formats:
|
||
|
||
- **AppImage** runs on any distribution without installing anything. It's the
|
||
download-and-go option and the fallback for distros the packages don't cover. It's built
|
||
with the maintained `AppImage/appimagetool`, whose static runtime doesn't need libfuse2.
|
||
- **.deb** covers Debian, Ubuntu, Mint, Pop!_OS and elementary. **.rpm** covers Fedora, RHEL
|
||
and its clones, and openSUSE. Both integrate with the menu, MIME and icon caches (the
|
||
`postinst.sh` hook) and uninstall cleanly. Both are built by
|
||
[nfpm](https://nfpm.goreleaser.com/): one maintained, dependency-free tool and one config
|
||
(`nfpm.yaml`). It doesn't need cargo-deb and cargo-generate-rpm metadata in our crates, and
|
||
it can build both formats on Ubuntu.
|
||
- **.tar.gz** is for people who manage their own `/opt` or `~/.local`.
|
||
- **Flatpak** is the route into Flathub, GNOME Software and KDE Discover.
|
||
`packaging/linux/flatpak/ai.storyteller.photocraft.yml` is ready for a Flathub submission.
|
||
CI only validates it. A real build needs vendored crate sources (`cargo-sources.json` from
|
||
`flatpak-cargo-generator.py`) and adds 20+ minutes per architecture. Build it by hand with
|
||
the commands in the manifest's header.
|
||
|
||
All Linux binaries are built on Ubuntu 22.04 and need **glibc ≥ 2.35**: Ubuntu 22.04+,
|
||
Debian 12+, Fedora 36+, RHEL 10, openSUSE Tumbleweed. They link only glibc and libgcc_s. X11,
|
||
Wayland, xkbcommon, Vulkan and EGL are loaded at runtime from the system, which is also where
|
||
the GPU driver has to come from. That's why the packages declare them as dependencies and the
|
||
AppImage doesn't bundle them.
|
||
|
||
Locally (on Linux): install [nfpm](https://nfpm.goreleaser.com/install/), then
|
||
`packaging/linux/package.sh` (or `--formats "deb tar"`).
|
||
|
||
### Web
|
||
|
||
`packaging/web/package.sh` runs `trunk build --release` (see `apps/photocraft-web/Trunk.toml`)
|
||
and zips `dist/web` together with sample `_headers` and `.htaccess` files and the hosting guide.
|
||
The site only uses relative URLs, so it works under any path and in an iframe.
|
||
[`packaging/web/README.md`](../packaging/web/README.md) covers MIME types, compression,
|
||
caching, the iframe snippet and the `?webgl` / `?cpu` flags.
|
||
|
||
## Secrets
|
||
|
||
All secrets live in the repository's **`release` environment** (*Settings → Environments →
|
||
release*). Restrict its deployment branches to `release`. Every job in `release.yml` declares
|
||
`environment: release`, so only pushes to that branch, or manual runs on it, can read the
|
||
secrets. Each secret is optional. If one is missing, that platform's artifacts are unsigned and
|
||
the run shows a `::warning::`.
|
||
|
||
| Secret | Used for |
|
||
|---|---|
|
||
| `APPLE_CERTIFICATE` | base64 `.p12` with "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` | base64 `.pfx` code-signing certificate |
|
||
| `WINDOWS_CERTIFICATE_PASSWORD` | password for that `.pfx` |
|
||
| `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` | service principal for Azure Trusted Signing (an alternative to the `.pfx`) |
|
||
| `AZURE_SIGNING_ENDPOINT` | e.g. `https://eus.codesigning.azure.net` |
|
||
| `AZURE_SIGNING_ACCOUNT`, `AZURE_CERT_PROFILE` | Trusted Signing account and certificate profile names |
|
||
|
||
`GITHUB_TOKEN` creates the release. Only the final job gets `contents: write`.
|
||
|
||
On macOS, `packaging/macos/import-cert.sh` decodes the `.p12` into a temporary keychain
|
||
(`security create-keychain`, then `security import`, then `set-key-partition-list` so codesign
|
||
can use the key without a prompt). It exports the identity's SHA-1 as `MACOS_SIGN_IDENTITY`.
|
||
The keychain is deleted at the end of the job.
|
||
|
||
## Icons
|
||
|
||
`assets/app-icon/photocraft.svg` is the canonical icon: the owner's ArtCraft drawing of a
|
||
nine-tailed kitsune, vectorised (MIT OR Apache-2.0, see `LICENSE.txt` there; palette and
|
||
geometry in `README.md`). `packaging/icons.sh` regenerates the 1024 px PNG, the `.icns`, the `.ico` (packed by
|
||
`cargo xtask ico`) and the hicolor PNGs from it. It needs `resvg`, plus `iconutil` on macOS.
|
||
The outputs are committed, so packaging never needs those tools.
|
||
|
||
## Checks
|
||
|
||
`.github/workflows/packaging-lint.yml` runs in seconds on any change to `packaging/`, the
|
||
workflows or the icons. It runs actionlint, shellcheck, a PowerShell parse, xmllint,
|
||
`desktop-file-validate`, `appstreamcli validate`, and a YAML check of the Flatpak manifest.
|