Skip to content

Release Setup

Stale — pending rewrite for the consolidated kn-86 monorepo. This doc describes the pre-consolidation release flow (a private kn86-deckline source repo mirroring binaries to a separate public kn86-emulator repo). Per ADR-0041 all code now lives in the single kn-86 monorepo (emulator host at hosts/emulator/, Pico firmware at hosts/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:

  1. Desktop emulator tarballskn86emu-<tag>-{macos-universal,linux-x86_64}.tar.gz.
  2. Pico 2 coprocessor firmwarekn86_coproc-<tag>.uf2 (drag-drop flash) and kn86_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.


  • 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. The kn86-deckline.com Downloads 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.


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.

Terminal window
# One-time, from a scratch dir
git clone https://github.com/jschairb/kn86-emulator.git
cd kn86-emulator
cat > 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 tagged
builds 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 verify
the download:

shasum -a 256 -c kn86emu-vX.Y.Z-.tar.gz.sha256

EOF
git add README.md
git commit -m "chore: seed binaries repo"
git push origin main

The PAT authenticates the workflow against the public repo. It is stored as a secret on the private monorepo (jschairb/kn86-deckline).

  1. Go to https://github.com/settings/personal-access-tokens/new (classic PATs work too, but fine-grained are preferred).
  2. 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
  3. Classic PAT alternative (if fine-grained isn’t workable):
    • Scope: public_repo (sufficient while the mirror is public). Use full repo scope only if the mirror is ever made private.
  4. 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”
Terminal window
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:

Terminal window
gh secret list --repo jschairb/kn86-deckline | grep KN86_PUBLIC_MIRROR_PAT

The workflow’s first mirror step (Verify mirror PAT is configured) fails fast with a pointer to this document if the secret is missing.

Terminal window
git tag v0.1.0-smoke
git push origin v0.1.0-smoke

Watch 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:

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:

Terminal window
# Delete local tag
git tag -d v0.1.0-smoke
# Delete remote tag on the private repo
git 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-tag
gh release delete v0.1.0-smoke --repo jschairb/kn86-emulator --yes --cleanup-tag

  • 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 configured step (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.

  1. Bump the version in kn86-emulator/CMakeLists.txt (or wherever KN86_VERSION lives) — the -DKN86_VERSION_OVERRIDE=… from the tag name must be able to match the stored version for the Verify embedded version step to pass.
  2. Commit and merge to main.
  3. Tag and push:
    Terminal window
    git tag v0.2.0
    git push origin v0.2.0
  4. 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.

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:

Terminal window
gh release delete <tag> --repo jschairb/kn86-emulator --yes --cleanup-tag

Then re-run the workflow.


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.

Detailed steps live in pico2-firmware/README.md. Summary:

  1. Download kn86_coproc-<tag>.uf2 (and optionally .uf2.sha256 to verify).
  2. Hold BOOTSEL on the Pico 2 module while plugging USB into the host.
  3. The Pico mounts as a RP2350 (or RPI-RP2350) USB mass-storage volume.
  4. Drag the .uf2 onto that volume. The Pico reboots automatically when the copy completes; the volume disappears.

Two independent post-flash checks (also documented in the firmware README):

  1. 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.2
    build_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_id should match the lower 32 bits of the git commit the release was tagged against (look at the GitHub Release page or run git rev-parse <tag> | cut -c1-8).

  2. 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.

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>.uf2 flashes via BOOTSEL drag-drop. See pico2-firmware/README.md (or the private repo equivalent) for the full procedure and post-flash verification. Confirm build_id reported by the USB CDC banner matches the release tag.


  • Apple notarization — the stub is present in release.yml; uncomment and provision APPLE_* 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.