# Releasing Cutting a version is one push. Everything below the tag happens in CI. > **Shipping the very first release?** Use > [FIRST-RELEASE.md](FIRST-RELEASE.md) instead — a linear runbook for the > one-time steps that can never happen again, since winget will not accept an > automated update to a package that does not exist yet. ```powershell git tag v0.2.0 git push origin v0.2.0 ``` ## Before the tag 1. **Bump `version` in `Cargo.toml`.** The release workflow refuses to build if the tag and the manifest disagree — a mismatch would ship a binary whose `keys --version` contradicts the package version, which then propagates into both the winget and Scoop manifests, since both key off it. 2. **Move the `CHANGELOG.md` entries** from `[Unreleased]` into a new `## [x.y.z]` section, and add the compare/tag links at the bottom. That section becomes the release notes verbatim. 3. **Commit and let CI go green.** CI builds and smoke-tests the MSVC release binary on every push, so a red CI is a release that would have failed anyway. 4. **Run the interactive interface in a real terminal.** Its state machine has unit tests, but nothing in CI ever watches it draw. Search, reveal, copy, roll, the previous-value overlay, resize the window, Ctrl-C mid-render. 5. **Build and install the installer locally, and check its behaviour** — see below. This is the highest-risk step in the whole process, because the winget manifest hardcodes a value that only an actual install can confirm. ### Checking the installer by hand ```powershell $env:KEYFORGE_VERSION = '0.2.0' cargo build --release --target x86_64-pc-windows-msvc & "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" packaging\inno\keyforge.iss # -> dist\ ``` Install it, then confirm all of these. Each one has burned somebody before: - **The ProductCode matches the winget manifest**, read back rather than derived: ```powershell Get-ChildItem HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | Where-Object { $_.GetValue('DisplayName') -like '*keyforge*' } | Select-Object PSChildName, @{n='V';e={$_.GetValue('DisplayVersion')}} ``` - **PATH gained exactly one entry**, and installing a second time adds no more. - **The `Path` value is still `REG_EXPAND_SZ`** and any `%VAR%` references in it survived. Turning them into literals would break other tools, silently. - **The Start Menu shortcut carries the app identity** — `Get-StartApps` should list `keyforge` with `AppID: ElJoshua08.KeyForge`. Without it, rotation notifications do not appear and there is nothing to diagnose. - **A silent uninstall keeps the vault.** Back up `%APPDATA%\keyforge` first, run `unins000.exe /VERYSILENT`, and check `vault.bin` is byte-identical and PATH is back to exactly what it was. ## What the tag triggers `.github/workflows/release.yml`: 1. Checks the tag against `Cargo.toml`. 2. Locates `ISCC.exe`, before the build rather than after — `lto = "fat"` takes minutes, and discovering there is no installer compiler then wastes all of them. 3. Builds `--release --locked --target x86_64-pc-windows-msvc`. 4. Checks the binary reports the tagged version. 5. Packages `keyforge--x86_64-pc-windows-msvc.zip` (containing `keys.exe`, `keys-notify.exe`, `LICENSE`, `README.md`) and the bare `keys.exe`. 6. Builds `keyforge--setup.exe`, with the version passed in as `KEYFORGE_VERSION` — and checks the version stamped into the resulting binary, which is a channel neither of the two gates above can see. 7. Writes `SHA256SUMS.txt` over everything in `dist\`, LF-terminated and BOM-free. 8. Publishes the GitHub release with notes from the changelog. Four assets, then. The publish step globs `dist\`, so anything produced into it ships by merely existing. Then `.github/workflows/winget.yml` fires on the published release — but only once the `WINGET_PUBLISHED` repository variable is set, because the action it uses can publish *updates* and not first submissions. See below. If something goes wrong after the tag is pushed, fix it, delete the tag locally and remotely, and re-tag. Nothing is published until step 8. ## After the release 1. **Check the four assets are there**, and that the setup.exe's hash matches its line in `SHA256SUMS.txt`. 2. **First release only — submit the winget manifest by hand.** Komac detects the installer type but does not run the installer, so it cannot know the `ProductCode`; let it generate, then edit, then submit. Full instructions in [packaging/winget/README.md](packaging/winget/README.md). 3. **When that PR merges**, turn the automation on: `gh variable set WINGET_PUBLISHED --body true --repo ElJoshua08/keyforge`, and re-sync `packaging/winget/*.yaml` to whatever `ManifestVersion` Komac actually published against. 4. **First release only — create the Scoop bucket**, with the real hash from `SHA256SUMS.txt`. See [packaging/scoop/README.md](packaging/scoop/README.md). A bucket that references assets which do not exist is worse than no bucket. 5. **Every release:** update `PackageVersion`, `InstallerUrl`, `AppsAndFeaturesEntries.DisplayVersion`, and `ReleaseNotesUrl` in `packaging/winget/*.yaml`. `ProductCode` never changes. 6. Verify the end result the way a user would: ```powershell winget install ElJoshua08.KeyForge keys --version # in a NEW shell — PATH changes do not reach an open one ``` ## Testing a manifest without publishing `winget validate` parses every file in the directory it is given, and `packaging/winget` contains a README, so validate a copy of just the manifests: ```powershell $tmp = New-Item -ItemType Directory -Force "$env:TEMP\keyforge-winget" Copy-Item packaging\winget\*.yaml $tmp -Force winget validate --manifest $tmp ``` And, against a real released asset, an actual install from the local manifest (needs `winget settings` with `LocalManifestFiles` enabled, and an elevated shell): ```powershell winget install --manifest $tmp ```