Release Setup
Stale — pending rewrite for the consolidated
kn-86monorepo. This doc describes the pre-consolidation release flow (a privatekn86-decklinesource repo mirroring binaries to a separate publickn86-emulatorrepo). Per ADR-0041 all code now lives in the singlekn-86monorepo (emulator host athosts/emulator/, Pico firmware athosts/device/firmware/); the per-repo mirror topology and cross-repo PATs below no longer reflect reality. The replacement release pipeline is a follow-on task. Treat the specifics here as historical until then.
One-time setup for the KN-86 release pipeline that publishes tagged binaries to
both the private monorepo (jschairb/kn86-deckline) and the public binaries
repo (jschairb/kn86-emulator).
A single release tag (e.g. v0.2.0) produces two distinct artifact families:
- Desktop emulator tarballs —
kn86emu-<tag>-{macos-universal,linux-x86_64}.tar.gz. - Pico 2 coprocessor firmware —
kn86_coproc-<tag>.uf2(drag-drop flash) andkn86_coproc-<tag>.elf(debug symbols). See “Pico 2 firmware” below.
Workflow file: .github/workflows/release.yml
Tracked by: GWP-142
(child of GWP-130: kn86-deckline.com Downloads + release sync). Pico 2
firmware integration tracked by GWP-299.
Why two repos?
Section titled “Why two repos?”- Private monorepo (
jschairb/kn86-deckline) — full project source; the release workflow runs here because this is where the emulator code lives. - Public binaries repo (
jschairb/kn86-emulator) — ships prebuilt archives and checksums only. No source. Thekn86-deckline.comDownloads page links to this repo’s Releases so users can grab binaries without source access.
The workflow’s mirror job pushes a Release (tag, title, notes, assets) to the
public repo without ever cloning or pushing source to it. Only .tar.gz and
.sha256 files are uploaded.
One-time setup
Section titled “One-time setup”1. Seed the public repo
Section titled “1. Seed the public repo”The mirror job creates releases on jschairb/kn86-emulator via
gh release create --target <default_branch>. An empty repo has no default
branch, so the first release would fail. Push a minimal commit (typically a
README explaining what the repo is for) to seed the default branch.
# One-time, from a scratch dirgit clone https://github.com/jschairb/kn86-emulator.gitcd kn86-emulatorcat > README.md <<'EOF'# KN-86 Emulator — Binary Releases
Prebuilt binaries for the Kinoshita KN-86 Deckline desktop emulator.Source lives in the private development repo; this repo ships taggedbuilds via the release automation in that repo.
Download the latest release from the **Releases** tab.
Each release archive contains:- `kn86emu` — the emulator executable (macOS universal2 or Linux x86_64)- `assets/` — test cartridges and fonts (when present)
Each release also publishes a `.sha256` file per archive so you can verifythe download:shasum -a 256 -c kn86emu-vX.Y.Z-
EOFgit add README.mdgit commit -m "chore: seed binaries repo"git push origin main2. Create the mirror PAT
Section titled “2. Create the mirror PAT”The PAT authenticates the workflow against the public repo. It is stored as
a secret on the private monorepo (jschairb/kn86-deckline).
- Go to https://github.com/settings/personal-access-tokens/new (classic PATs work too, but fine-grained are preferred).
- Fine-grained PAT (preferred):
- Token name:
kn86-emulator mirror (from kn86-deckline CI) - Expiration: 1 year (set a calendar reminder to rotate)
- Repository access: Only select repositories →
jschairb/kn86-emulator - Repository permissions:
- Contents: Read and write (required to create/update releases and tags)
- Metadata: Read-only (granted automatically)
- Everything else: no access
- Token name:
- Classic PAT alternative (if fine-grained isn’t workable):
- Scope:
public_repo(sufficient while the mirror is public). Use fullreposcope only if the mirror is ever made private.
- Scope:
- Copy the token once — you will not see it again.
3. Add the token as a repo secret on the PRIVATE repo
Section titled “3. Add the token as a repo secret on the PRIVATE repo”gh secret set KN86_PUBLIC_MIRROR_PAT \ --repo jschairb/kn86-deckline \ --body "<paste-token-here>"Or via UI: jschairb/kn86-deckline → Settings → Secrets and variables →
Actions → New repository secret → Name KN86_PUBLIC_MIRROR_PAT, value = token.
Verify it’s set:
gh secret list --repo jschairb/kn86-deckline | grep KN86_PUBLIC_MIRROR_PATThe workflow’s first mirror step (Verify mirror PAT is configured) fails
fast with a pointer to this document if the secret is missing.
4. Smoke test
Section titled “4. Smoke test”git tag v0.1.0-smokegit push origin v0.1.0-smokeWatch the run at https://github.com/jschairb/kn86-deckline/actions. Expect
three jobs: Build (macos-universal), Build (linux-x86_64), then
Publish Release (private) and Mirror to public repo in sequence.
When green, verify on both sides:
- Private: https://github.com/jschairb/kn86-deckline/releases/tag/v0.1.0-smoke
- Public: https://github.com/jschairb/kn86-emulator/releases/tag/v0.1.0-smoke
Both should show the same .tar.gz + .sha256 asset pairs. The public
release’s auto-generated source archives refer to the mirror repo’s own
contents (seed README), not private source.
Clean up the smoke test:
# Delete local taggit tag -d v0.1.0-smoke# Delete remote tag on the private repogit push --delete origin v0.1.0-smoke# Delete both releases (the tag deletion will trigger deletion of the# private release, but the mirror release lives on a separate tag in the# public repo and must be deleted explicitly):gh release delete v0.1.0-smoke --repo jschairb/kn86-deckline --yes --cleanup-taggh release delete v0.1.0-smoke --repo jschairb/kn86-emulator --yes --cleanup-tagRotation
Section titled “Rotation”- Fine-grained PAT: expires on the date you set. Watch the GitHub email reminder (sent ~7 days before expiry) and repeat steps 2–3 with a fresh token. Delete the old token from https://github.com/settings/personal-access-tokens.
- Rotation cadence: every 12 months minimum, or immediately after any suspected leak.
- If rotation is late: the first tagged release after expiry will fail at
the
Verify mirror PAT is configuredstep (the secret is still set but the underlying token is rejected by the API). The private release still succeeds, so production isn’t blocked — the public mirror just lags until the token is refreshed.
How to trigger a real release
Section titled “How to trigger a real release”- Bump the version in
kn86-emulator/CMakeLists.txt(or whereverKN86_VERSIONlives) — the-DKN86_VERSION_OVERRIDE=…from the tag name must be able to match the stored version for theVerify embedded versionstep to pass. - Commit and merge to
main. - Tag and push:
Terminal window git tag v0.2.0git push origin v0.2.0 - Stable releases use no hyphen in the tag (
v1.2.3). Pre-releases use a hyphen (v1.2.3-rc1,v0.1.0-beta) and are marked as GitHub “pre-release” on both repos automatically.
Troubleshooting
Section titled “Troubleshooting”Mirror job fails with ERROR: Mirror repo jschairb/kn86-emulator has no default branch.
→ The public repo is still empty. Run step 1 above.
Mirror job fails with ERROR: KN86_PUBLIC_MIRROR_PAT secret is not set.
→ Run step 3 above.
Mirror job fails with HTTP 403: Resource not accessible by personal access token
→ Fine-grained PAT is missing “Contents: Read and write” on the mirror repo.
Regenerate the PAT (step 2) with the correct permissions and update the
secret (step 3).
Private release succeeds but mirror release shows no assets.
→ Check the Collect release files step log on the mirror job — the
download-artifact output may be pointing at an unexpected layout.
Release is re-run on the same tag and fails with already exists.
→ The workflow’s mirror step attempts to delete any prior release for the tag
before re-creating it. If that still fails, delete manually:
gh release delete <tag> --repo jschairb/kn86-emulator --yes --cleanup-tagThen re-run the workflow.
Pico 2 firmware
Section titled “Pico 2 firmware”The release pipeline produces a kn86_coproc-<tag>.uf2 for the Raspberry Pi
Pico 2 coprocessor on every tagged release (per GWP-299). It ships as a
separate release asset alongside the desktop emulator tarballs.
Why a separate artifact (not bundled into the .kn86fw Pi image)
Section titled “Why a separate artifact (not bundled into the .kn86fw Pi image)”The Pi Zero 2 W and the Pico 2 are distinct compute units with distinct update workflows:
- The Pi system image (
.kn86fw) is delivered to operators via the OTA path described in ADR-0011 and ADR-0020. The device reboots into the staged slot. - The Pico 2 coprocessor lives behind the internal USB hub (ADR-0018) and is flashed via BOOTSEL drag-drop — a physical button-hold + USB mass-storage workflow that has nothing to do with the Pi rootfs A/B flip.
Bundling the .uf2 into the .kn86fw image would require: (a) extending
tools/kn86fw with an embed flag, (b) teaching the
Pi’s update applier to drive the Pico into BOOTSEL via GPIO, expose the USB
mass-storage volume, copy the .uf2, and confirm reboot, and (c) reconciling
two independent versioning chains in one image header. The win — one fewer
file in the operator’s hands — does not justify the operational surface
that buys.
If a future hardware revision integrates the coprocessor flash so the Pi can write to it directly without BOOTSEL, this calculus changes. Until then, the two artifacts ship side by side.
End-user flashing workflow
Section titled “End-user flashing workflow”Detailed steps live in pico2-firmware/README.md.
Summary:
- Download
kn86_coproc-<tag>.uf2(and optionally.uf2.sha256to verify). - Hold BOOTSEL on the Pico 2 module while plugging USB into the host.
- The Pico mounts as a
RP2350(orRPI-RP2350) USB mass-storage volume. - Drag the
.uf2onto that volume. The Pico reboots automatically when the copy completes; the volume disappears.
Verifying a flashed firmware
Section titled “Verifying a flashed firmware”Two independent post-flash checks (also documented in the firmware README):
-
USB CDC banner — open the Pico’s USB CDC serial port (
/dev/cu.usbmodem*on macOS,/dev/ttyACM*on Linux) and look for:=== KN-86 coprocessor firmware (Phase 3: PSG + OLED) ===Build: v0.1.0 (v0.1.0-3-gabc1234), proto v0.2build_id: 0x12345678 (lower 32 bits of git HEAD; coprocessor-protocol.md sec 4.3)OK: CRC-16/CCITT-FALSE self-test passed (vector -> 0x29B1).Ready. UART0 @ 1000000 baud, awaiting Pi link.The
build_idshould match the lower 32 bits of the git commit the release was tagged against (look at the GitHub Release page or rungit rev-parse <tag> | cut -c1-8). -
Onboard LED heartbeat — the user LED toggles at 1 Hz. A 50 ms / 50 ms rapid panic flash means the boot CRC self-test failed; do not ship that build.
Release notes template
Section titled “Release notes template”Every release announcement should list both artifact families and the flashing pointer. The auto-generated GitHub release notes already enumerate every uploaded asset; add a manual sentence above the asset list along the lines of:
Pico 2 coprocessor:
kn86_coproc-<tag>.uf2flashes via BOOTSEL drag-drop. Seepico2-firmware/README.md(or the private repo equivalent) for the full procedure and post-flash verification. Confirmbuild_idreported by the USB CDC banner matches the release tag.
What this workflow does NOT do
Section titled “What this workflow does NOT do”- Apple notarization — the stub is present in
release.yml; uncomment and provisionAPPLE_*secrets when ready to ship signed macOS binaries. - Windows builds — not in scope.
- Publish source to the public repo — never. Only binaries + checksums.
- Delete the private release if the mirror fails — the private release is the source of truth; mirror is best-effort. Rerun the workflow to retry the mirror without re-running the build.