Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/ci-checkout-installer-hardening.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@kitsunekode/kunai": patch
---

Fix Windows installer optional deps so yt-dlp is still offered after mpv, and improve binary-download errors when a GitHub Release has no assets.

CI: require `actions/checkout` before the local `setup-bun-monorepo` composite (nested checkout never ran), assert release uploads are non-empty, and smoke host-native binaries on Linux/macOS/Windows after the weekly 8-target build.
5 changes: 5 additions & 0 deletions .changeset/ci-shared-test-stabilization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@kitsunekode/kunai": patch
---

Stabilize shared CLI CI failures that only surfaced after checkout/installer CI started running again: durable diagnostics retention timestamp, install.ps1 dry-run without LOCALAPPDATA, and mock.module leak across app-shell unit tests. Gate live Miruro network checks behind KUNAI_LIVE_PROVIDERS.
11 changes: 11 additions & 0 deletions .docs/repo-infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ Pull requests and pushes to `main` use the composite setup action
Bun store cache, per-job `.turbo` cache prefixes, `TURBO_SCM_BASE` on PRs, and
`TURBO_TOKEN` / `TURBO_TEAM` for remote Turbo cache.

**Hard rule:** every job that uses `./.github/actions/setup-bun-monorepo` must run
`actions/checkout` first. GitHub resolves local composite actions from the
workspace before any step runs, so nesting checkout inside the composite fails
with `Can't find 'action.yml' … Did you forget to run actions/checkout`.

**Parallel jobs** (`.github/workflows/ci.yml`):

| Job | PR | Main |
Expand All @@ -78,8 +83,14 @@ Bun store cache, per-job `.turbo` cache prefixes, `TURBO_SCM_BASE` on PRs, and
| `test` | `turbo run test --affected` | full |
| `build-cli` | `bun run build` + `bun run pkg:check` when CLI paths change | same on main |
| `build-binaries` | 2 Linux targets via Turbo when CLI/installer paths change | same |
| `installer-*` | shellcheck/PSScriptAnalyzer + Docker glibc/musl smoke when installer paths change | same |
| `checks-docs` | docs gate when docs paths change | same |

**Full 8-target workflow** (`.github/workflows/build-binaries.yml`, weekly /
manual / path-triggered): cross-compiles all release assets on `ubuntu-latest`,
then a `native-smoke` matrix boots the host-matching binary on
`ubuntu-latest` / `macos-latest` / `windows-latest`.

Install cache key: `${{ runner.os }}-bun-store-${{ hashFiles('bun.lock') }}` covering
`~/.bun/install/cache` only (Bun reconstructs `node_modules` from the store).

Expand Down
8 changes: 1 addition & 7 deletions .github/actions/setup-bun-monorepo/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Setup Bun monorepo
description: Checkout, Bun, dependency caches, and install for Kunai workspace jobs.
description: Bun, dependency caches, and install for Kunai workspace jobs. Callers must run actions/checkout first — local composite actions are not on disk until the repo is checked out.

inputs:
turbo-cache-prefix:
Expand All @@ -13,12 +13,6 @@ inputs:
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
Expand Down
80 changes: 80 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ name: Build all release binaries

# Full 8-target cross-compile is too slow for every PR (~5+ min). Run on a
# schedule, manually, or when release binaries workflow inputs change.
# Cross-compile happens on Linux; a follow-up matrix smokes the host-native
# darwin/windows/linux artifacts so we catch "builds but won't run" failures.
on:
workflow_dispatch:
schedule:
Expand All @@ -12,7 +14,9 @@ on:
- "apps/cli/scripts/build-binaries.ts"
- "apps/cli/scripts/build-shared.ts"
- "apps/cli/scripts/verify-release-binaries.sh"
- "apps/cli/src/services/update/platform-assets.ts"
- ".github/workflows/build-binaries.yml"
- ".github/actions/setup-bun-monorepo/action.yml"

concurrency:
group: build-binaries-${{ github.workflow }}-${{ github.ref }}
Expand All @@ -32,6 +36,12 @@ jobs:
timeout-minutes: 35

steps:
# Local composite actions require checkout before `uses: ./…`.
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: binaries-all
Expand All @@ -57,3 +67,73 @@ jobs:
apps/cli/dist/bin/kunai-windows-x64.exe
apps/cli/dist/bin/kunai-windows-arm64.exe
apps/cli/dist/bin/SHA256SUMS

# Native smoke: run the host-matching binary on each OS. Cross-compile alone
# only proves the Linux runner can emit the files — not that they boot.
native-smoke:
name: Native smoke (${{ matrix.os }})
needs: all-targets
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
asset: kunai-linux-x64
- os: macos-latest
asset: kunai-darwin-arm64
- os: windows-latest
asset: kunai-windows-x64.exe

steps:
- uses: actions/checkout@v5

- name: Download release binaries
uses: actions/download-artifact@v4
with:
name: kunai-release-binaries
path: apps/cli/dist/bin

- name: Smoke host binary (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
BIN="apps/cli/dist/bin/${{ matrix.asset }}"
chmod +x "$BIN"
cd apps/cli/dist/bin
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c SHA256SUMS | grep -F "${{ matrix.asset }}: OK"
else
want="$(awk -v a="${{ matrix.asset }}" '$2==a {print $1}' SHA256SUMS)"
got="$(shasum -a 256 "${{ matrix.asset }}" | awk '{print $1}')"
test -n "$want" && test "$want" = "$got"
echo "${{ matrix.asset }}: OK"
fi
version_out="$("./${{ matrix.asset }}" --version)"
echo "$version_out"
grep -q '^kunai ' <<<"$version_out"
"./${{ matrix.asset }}" --help >/dev/null
echo "✓ ${{ matrix.asset }} boots on ${{ matrix.os }}"

- name: Smoke host binary (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$bin = "apps/cli/dist/bin/${{ matrix.asset }}"
if (-not (Test-Path $bin)) { throw "missing $bin" }
$sums = Get-Content apps/cli/dist/bin/SHA256SUMS
$want = ($sums | Where-Object { $_ -match "\s$([regex]::Escape('${{ matrix.asset }}'))$" }) -replace '\s.*', ''
$got = (Get-FileHash -Path $bin -Algorithm SHA256).Hash.ToLower()
if ([string]::IsNullOrEmpty($want) -or $want.ToLower() -ne $got) {
throw "Checksum mismatch for ${{ matrix.asset }} (expected '$want', got '$got')"
}
$versionOut = & $bin --version
Write-Host $versionOut
if ($versionOut -notmatch '^kunai ') {
throw "--version must print kunai semver, got: $versionOut"
}
& $bin --help | Out-Null
Write-Host "✓ ${{ matrix.asset }} boots on ${{ matrix.os }}"
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: fmt
Expand All @@ -87,6 +92,11 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: lint
Expand All @@ -105,6 +115,11 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: typecheck
Expand All @@ -123,6 +138,11 @@ jobs:
timeout-minutes: 20

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: test
Expand Down Expand Up @@ -168,6 +188,11 @@ jobs:
timeout-minutes: 20

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: build
Expand All @@ -186,6 +211,11 @@ jobs:
timeout-minutes: 20

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: docs
Expand All @@ -207,6 +237,11 @@ jobs:
timeout-minutes: 15

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: binaries
Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ jobs:
published: ${{ steps.changesets.outputs.published }}

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: release
Expand Down Expand Up @@ -100,6 +105,11 @@ jobs:
contents: write

steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
filter: blob:none

- uses: ./.github/actions/setup-bun-monorepo
with:
turbo-cache-prefix: release-binaries
Expand All @@ -121,11 +131,13 @@ jobs:
# download contract (`releases/latest/download/<asset>` and
# `releases/download/v<version>/<asset>`) resolves to these binaries.
- name: Upload binaries to GitHub Release
id: upload
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.ver.outputs.tag }}
make_latest: true
body_path: .release/kunai-${{ steps.ver.outputs.tag }}.md
fail_on_unmatched_files: true
files: |
apps/cli/dist/bin/kunai-linux-x64
apps/cli/dist/bin/kunai-linux-arm64
Expand All @@ -138,3 +150,16 @@ jobs:
apps/cli/dist/bin/SHA256SUMS
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Guard against empty "latest" releases that break install.sh / install.ps1.
- name: Assert release has binary assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
tag="${{ steps.ver.outputs.tag }}"
count="$(gh release view "$tag" --json assets --jq '.assets | length')"
echo "Release $tag asset count: $count"
test "$count" -ge 9
gh release view "$tag" --json assets --jq '.assets[].name' | grep -qx 'SHA256SUMS'
gh release view "$tag" --json assets --jq '.assets[].name' | grep -qx 'kunai-linux-x64'
12 changes: 11 additions & 1 deletion RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,27 @@ Short one-line summary of the release.
- **Release Guard** (`.github/workflows/release-guard.yml`): PR-time check that `package.json`, both changelogs, pending changesets, and installer/release paths agree.
- **Release** (`.github/workflows/release.yml`): runs on pushes to `main` that touch release paths. Two jobs:
1. **Publish** — `bun run ci` + `bun run build` + `bun run pkg:check` + `bun run guard` + `changesets/action` (`bun run version:packages` then `bun run release` for npm).
2. **Binaries** (only after a real publish) — `bun run build:binaries` (all 8 targets), `verify-release-binaries.sh`, merges per-asset SHA256 checksums into `.release/kunai-v<version>.json`, then `softprops/action-gh-release` uploads assets + `SHA256SUMS` to tag `v<version>` with body from `.release/kunai-v<version>.md`.
2. **Binaries** (only after a real publish) — `bun run build:binaries` (all 8 targets), `verify-release-binaries.sh`, merges per-asset SHA256 checksums into `.release/kunai-v<version>.json`, then `softprops/action-gh-release` uploads assets + `SHA256SUMS` to tag `v<version>` with body from `.release/kunai-v<version>.md`. The job asserts the published release has ≥9 assets (`fail_on_unmatched_files` + `gh release view` count) so `install.sh` / `install.ps1` never see an empty `latest`.
- `version:packages` runs `changeset version`, mirrors changelog, and regenerates `.release/kunai-v*.md` via `bun run release:notes`.
- Publish uses OIDC (`id-token: write`) with npm provenance enabled.
- **CI** (`.github/workflows/ci.yml`): parallel Turbo jobs (`fmt`, `lint`, `typecheck`, `test`, `build-cli`, `build-binaries`); on installer-related diffs, runs Docker `install.sh → upgrade → uninstall` smoke after building linux glibc + musl binaries (images cached; binaries not rebuilt in Docker job).
- **Build all release binaries** (`.github/workflows/build-binaries.yml`): weekly/manual 8-target cross-compile plus native smoke on Linux/macOS/Windows runners.

**npm vs GitHub Release artifacts:** npm publishes only `dist/kunai.js` and `dist/assets/**` (allowlisted in `apps/cli/package.json` `files`). Standalone binaries live under `dist/bin/` on disk and ship exclusively via the **binaries** job to GitHub Releases — `bun run pkg:check` fails if `dist/bin/` appears in the npm tarball.

## GitHub release tags

Prefer tag `vX.Y.Z` with binary assets attached by the **binaries** job (not a separate empty GitHub release from Changesets alone). Release notes body comes from `.release/kunai-vX.Y.Z.md`. Avoid duplicate `@kitsunekode/kunai@X.Y.Z` releases with empty bodies.

### Backfilling an empty release

If `releases/latest` has a tag but **zero assets** (installers 404), do **not** tell users to curl the default binary path. Either:

1. Re-run the Release workflow after CI is green so the **binaries** job uploads assets, or
2. Manually: `bun run build:binaries` → `bash apps/cli/scripts/verify-release-binaries.sh` → `gh release upload vX.Y.Z apps/cli/dist/bin/kunai-* apps/cli/dist/bin/SHA256SUMS --clobber`.

Until assets exist, point users at `npm i -g @kitsunekode/kunai` / `bun i -g @kitsunekode/kunai`.

## Local release utilities

- `bun run changeset` → create release intent files
Expand Down
10 changes: 10 additions & 0 deletions apps/cli/src/app-shell/root-shell-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ export function resolveHelpScope(state: SessionState): KeyScope {
}
}

/**
* Pure Esc → transition helper for root-owned overlays.
*
* Runtime Esc ownership lives in `root-overlay-shell.tsx` via
* `resolveOverlayBackStack` (`overlay-back-stack.ts`): filters, nested panes,
* confirmations, and surface-owned Esc clear first; only then does the shell
* dispatch `CLOSE_TOP_OVERLAY`. Wiring this helper into that path would create
* a second Esc owner and risk double-close. Keep this as the pure
* "overlay open → CLOSE_TOP_OVERLAY" contract used by SessionState tests.
*/
export function resolveEscTransition(state: SessionState): StateTransition | null {
if (state.activeModals.length > 0) {
return { type: "CLOSE_TOP_OVERLAY" };
Expand Down
11 changes: 11 additions & 0 deletions apps/cli/src/app-shell/shell-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ export function ShellFrame({
onResolve("help");
return;
}
// Footer-owned letters are already resolved by useShellInput. Forwarding
// them again to onUnhandledInput double-dispatches post-play/playback
// actions (open tracks twice, replay twice, …) so the first press looks
// like a no-op. Surfaces that own letters themselves opt in via
// letterKeysHandledExternally and still receive the key here.
if (!letterKeysHandledExternally) {
const matchKey = input.toLowerCase();
if (footerActions.some((action) => action.key === matchKey && !action.disabled)) {
return;
}
}
onUnhandledInput?.(input, key);
});

Expand Down
Loading
Loading