6767 run : node scripts/check-version.mjs
6868
6969 # ---------------------------------------------------------------------------
70- # Build: one matrix leg per (os, target). macOS covers both arches via cross
71- # targets on the arm64 runner. Linux arm64 and Windows arm64 can be added as
72- # further legs once cross-toolchains / arm runners are wired (see NOTE below).
70+ # Build: one matrix leg per (os, target), covering x64 and arm64 on all three
71+ # platforms. macOS and Windows reach their second arch by cross-compiling on
72+ # the x64/arm64 runner they already use; Linux gets a native arm64 runner,
73+ # because Tauri links the system WebKitGTK stack and cross-compiling it would
74+ # mean carrying an arm64 sysroot.
7375 # ---------------------------------------------------------------------------
7476 build :
7577 name : Build ${{ matrix.target }}
@@ -104,15 +106,22 @@ jobs:
104106 - os : ubuntu-22.04
105107 target : x86_64-unknown-linux-gnu
106108 args : " "
109+ # Linux arm64, built natively on the arm64 runner (free for public
110+ # repositories). Same 22.04 image as the x64 leg, so both AppImages
111+ # share a glibc floor.
112+ - os : ubuntu-22.04-arm
113+ target : aarch64-unknown-linux-gnu
114+ args : " "
107115 # Windows x64
108116 - os : windows-latest
109117 target : x86_64-pc-windows-msvc
110118 args : " "
111- # NOTE: per-arch left as documented best-effort:
112- # - Linux arm64 (aarch64-unknown-linux-gnu) needs a cross toolchain
113- # or an arm64 runner and the arm64 webkit dev packages.
114- # - Windows arm64 (aarch64-pc-windows-msvc) needs the arm64 MSVC
115- # target; MakeAppx would also need ProcessorArchitecture="arm64".
119+ # Windows arm64 (cross-compiled on the x64 runner, as macOS Intel is).
120+ # Only the Rust target changes; Node, pnpm and the frontend build stay
121+ # on the x64 image.
122+ - os : windows-latest
123+ target : aarch64-pc-windows-msvc
124+ args : " --target aarch64-pc-windows-msvc"
116125
117126 steps :
118127 - name : Checkout repository
@@ -135,9 +144,13 @@ jobs:
135144 DEBIAN_FRONTEND : noninteractive
136145 run : |
137146 sudo apt-get update -o DPkg::Lock::Timeout=300
138- # patchelf is dropped: the AppImage bundler needs it, but it already
139- # ships in the ubuntu runner image, so installing it is pure download.
147+ # patchelf is installed explicitly: the AppImage bundler needs it,
148+ # and while the x64 runner image ships it, the arm64 images carry a
149+ # documented subset. A missing patchelf fails the arm64 leg, and
150+ # `publish` needs every leg -- so the whole release is blocked to save
151+ # one small download.
140152 sudo apt-get install -y -o DPkg::Lock::Timeout=300 \
153+ patchelf \
141154 libwebkit2gtk-4.1-dev \
142155 libappindicator3-dev \
143156 librsvg2-dev
@@ -249,37 +262,16 @@ jobs:
249262 # with the CHANGES.md-derived notes. Per-arch selection via --target.
250263 args : ${{ matrix.args }}
251264
252- # -----------------------------------------------------------------------
253- # Standalone CLI (skillkeeper): build and archive it for this target so it
254- # can be installed with the one-line install script (scripts/install.*),
255- # independent of the desktop bundle. The archive holds just the binary at
256- # its root and is named by target triple, so the script builds the
257- # download URL straight from the detected platform.
258- # -----------------------------------------------------------------------
259- - name : Build the CLI
260- shell : bash
261- run : cargo build --release -p skillkeeper-cli --target ${{ matrix.target }}
262-
263- - name : Archive the CLI (tar.gz)
264- if : matrix.os != 'windows-latest'
265- shell : bash
266- run : |
267- mkdir -p dist-cli
268- tar -czf "dist-cli/skillkeeper-cli-${{ matrix.target }}.tar.gz" \
269- -C "target/${{ matrix.target }}/release" skillkeeper
270-
271- - name : Archive the CLI (zip)
272- if : matrix.os == 'windows-latest'
273- shell : pwsh
274- run : |
275- New-Item -ItemType Directory -Force -Path dist-cli | Out-Null
276- Compress-Archive -Path "target/${{ matrix.target }}/release/skillkeeper.exe" -DestinationPath "dist-cli/skillkeeper-cli-${{ matrix.target }}.zip" -Force
277-
278265 # -----------------------------------------------------------------------
279266 # Windows MSIX (MakeAppx): package the built exe + tile assets into an
280267 # MSIX using the appx identity dev.lorem.SkillKeeper / CN=Lorem Dev.
281268 # Best-effort: authored manifest + MakeAppx pack; unsigned (Store signs
282269 # on ingest). Does not fail the build if MakeAppx/exe are unavailable.
270+ #
271+ # Runs BEFORE the CLI build, and must stay there: the CLI crate's binary
272+ # is also named skillkeeper.exe and lands in target/<triple>/release, the
273+ # very path `tauri build --target` wrote the desktop app to. Build the CLI
274+ # first and the MSIX would ship the CLI instead of the app.
283275 # -----------------------------------------------------------------------
284276 - name : Package Windows MSIX (MakeAppx)
285277 if : matrix.os == 'windows-latest'
@@ -303,19 +295,29 @@ jobs:
303295 }
304296 Write-Host "MSIX version: $msixVersion"
305297
306- # Locate the built executable (target name = crate name skillkeeper).
307- $exe = Get-ChildItem -Path "target" -Recurse -Filter "skillkeeper.exe" |
308- Where-Object { $_.FullName -match "release" -and $_.FullName -notmatch "deps" } |
309- Select-Object -First 1
310- if (-not $exe) {
311- Write-Warning "skillkeeper.exe not found; skipping MSIX packaging."
298+ # Both Windows legs package an MSIX, so the architecture has to reach
299+ # the manifest AND the file name -- the publish job refuses two assets
300+ # sharing a basename, and an arm64 package declaring x64 would install
301+ # on neither.
302+ $arch = if ("${{ matrix.target }}".StartsWith("aarch64")) { "arm64" } else { "x64" }
303+ Write-Host "MSIX architecture: $arch"
304+
305+ # The desktop app crate and the CLI crate BOTH produce a binary named
306+ # skillkeeper.exe, so a recursive search would be a coin toss between
307+ # them. Take the exact path this leg's `tauri build` wrote to:
308+ # target/<triple>/release when --target was passed, target/release
309+ # otherwise.
310+ $releaseDir = if ("${{ matrix.args }}" -match "--target") { "target/${{ matrix.target }}/release" } else { "target/release" }
311+ $exePath = Join-Path $releaseDir "skillkeeper.exe"
312+ if (-not (Test-Path $exePath)) {
313+ Write-Warning "skillkeeper.exe not found at $exePath; skipping MSIX packaging."
312314 exit 0
313315 }
314316
315317 $stage = Join-Path $env:RUNNER_TEMP "msix"
316318 $assets = Join-Path $stage "Assets"
317319 New-Item -ItemType Directory -Force -Path $assets | Out-Null
318- Copy-Item $exe.FullName (Join-Path $stage "skillkeeper.exe") -Force
320+ Copy-Item $exePath (Join-Path $stage "skillkeeper.exe") -Force
319321
320322 # Tile assets from the Tauri icons dir (Square* + StoreLogo).
321323 $iconDir = "apps/desktop/src-tauri/icons"
@@ -342,7 +344,7 @@ jobs:
342344 ' <Identity Name="dev.lorem.SkillKeeper"',
343345 ' Publisher="CN=Lorem Dev"',
344346 (' Version="' + $msixVersion + '"'),
345- ' ProcessorArchitecture="x64 " />',
347+ ( ' ProcessorArchitecture="' + $arch + ' " />') ,
346348 ' <Properties>',
347349 ' <DisplayName>SkillKeeper</DisplayName>',
348350 ' <PublisherDisplayName>Lorem Dev</PublisherDisplayName>',
@@ -378,7 +380,9 @@ jobs:
378380 ) -join "`r`n"
379381 Set-Content -Path (Join-Path $stage "AppxManifest.xml") -Value $manifest -Encoding UTF8
380382
381- # Locate MakeAppx.exe from the installed Windows SDK.
383+ # Locate MakeAppx.exe from the installed Windows SDK. The x64 filter
384+ # picks the HOST tool, which stays x64 on both legs -- the package's
385+ # own architecture is the manifest's ProcessorArchitecture, above.
382386 $makeappx = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter "makeappx.exe" -ErrorAction SilentlyContinue |
383387 Where-Object { $_.FullName -match "x64" } |
384388 Sort-Object FullName -Descending | Select-Object -First 1
@@ -389,7 +393,7 @@ jobs:
389393
390394 $outDir = "apps/desktop/dist-msix"
391395 New-Item -ItemType Directory -Force -Path $outDir | Out-Null
392- $msix = Join-Path $outDir "SkillKeeper-$msixVersion.msix"
396+ $msix = Join-Path $outDir "SkillKeeper-$msixVersion-$arch .msix"
393397 & $makeappx.FullName pack /d $stage /p $msix /o
394398 # Native tools do not trip $ErrorActionPreference; check the exit code
395399 # explicitly so a failed pack is a real (visible) step failure instead
@@ -401,6 +405,32 @@ jobs:
401405 }
402406 Write-Host "Built MSIX: $msix"
403407
408+ # -----------------------------------------------------------------------
409+ # Standalone CLI (skillkeeper): build and archive it for this target so it
410+ # can be installed with the one-line install script (scripts/install.*),
411+ # independent of the desktop bundle. The archive holds just the binary at
412+ # its root and is named by target triple, so the script builds the
413+ # download URL straight from the detected platform.
414+ # -----------------------------------------------------------------------
415+ - name : Build the CLI
416+ shell : bash
417+ run : cargo build --release -p skillkeeper-cli --target ${{ matrix.target }}
418+
419+ - name : Archive the CLI (tar.gz)
420+ if : matrix.os != 'windows-latest'
421+ shell : bash
422+ run : |
423+ mkdir -p dist-cli
424+ tar -czf "dist-cli/skillkeeper-cli-${{ matrix.target }}.tar.gz" \
425+ -C "target/${{ matrix.target }}/release" skillkeeper
426+
427+ - name : Archive the CLI (zip)
428+ if : matrix.os == 'windows-latest'
429+ shell : pwsh
430+ run : |
431+ New-Item -ItemType Directory -Force -Path dist-cli | Out-Null
432+ Compress-Archive -Path "target/${{ matrix.target }}/release/skillkeeper.exe" -DestinationPath "dist-cli/skillkeeper-cli-${{ matrix.target }}.zip" -Force
433+
404434 # -----------------------------------------------------------------------
405435 # Upload per-target bundles + the MSIX. Covers both the default target
406436 # dir (linux/windows) and the cross-target dir (macOS --target).
@@ -481,6 +511,16 @@ jobs:
481511 echo "Staged $(find dist -type f | wc -l) files:"
482512 ls -1 dist
483513
514+ # The mcp.yml JSON Schema is generated from the Rust types by `cargo test`
515+ # and committed, so it ships straight from the checkout -- no build leg
516+ # produces it. It must be attached to EVERY release: the docs tell editors
517+ # to read it from `releases/latest/download/mcp.schema.json`, and that URL
518+ # resolves against whichever release is newest.
519+ - name : Stage the mcp.yml JSON Schema
520+ run : |
521+ test -f schemas/mcp.schema.json
522+ cp schemas/mcp.schema.json dist/mcp.schema.json
523+
484524 # Checksum every staged asset. Written outside dist/ first so the glob
485525 # cannot include the (empty) checksums.txt the redirection would create.
486526 - name : Generate SHA-256 checksums
0 commit comments