v0.5.1166 #176
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Packages | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| publish_npm: | |
| description: "Publish npm packages (use for the first run / to bypass release trigger)" | |
| type: boolean | |
| default: false | |
| permissions: | |
| contents: write | |
| actions: read | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Gate: wait for Tests + Simulator Tests to pass on this commit before we | |
| # publish anything downstream (Homebrew bottle, apt .deb, npm tarballs). | |
| # Both workflows are triggered by the same `push: tags: ['v*']` event that | |
| # the /release skill does, so they run in parallel with this job. We poll | |
| # `gh run list --commit <sha>` until each reports a conclusion. Fails fast | |
| # if either workflow failed — npm publish is not reversible. | |
| # | |
| # Skipped on workflow_dispatch so the "first-run / bypass" lever still works. | |
| # --------------------------------------------------------------------------- | |
| await-tests: | |
| runs-on: ubuntu-latest | |
| # Job-level timeout must match the bash `deadline=$(( SECONDS + N * 60 ))` | |
| # below. Both must move together. | |
| # - v0.5.876: 60 → 90 (Tests had grown past 60 min wall) | |
| # - v0.5.883: 90 → 120 (macOS-14 runners queue 30-60+ min during | |
| # busy hours; Tests itself takes ~30 min, but Release Packages | |
| # starts at the same time as Tests and runs out of budget before | |
| # Tests gets dequeued + executes. 120 absorbs typical runner | |
| # queue + Tests wall-time). | |
| timeout-minutes: 120 | |
| steps: | |
| - name: Wait for Tests + Simulator Tests (iOS) to pass | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| SHA: ${{ github.sha }} | |
| EVENT: ${{ github.event_name }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$EVENT" = "workflow_dispatch" ]; then | |
| echo "workflow_dispatch — bypassing test gate (manual publish)." | |
| exit 0 | |
| fi | |
| echo "Gating release on commit $SHA" | |
| # Query by workflow *filename* via `gh api`, not by display name via | |
| # `gh run list --workflow`. v0.5.155 shipped the latter and watched | |
| # both gate workflows succeed while the poller logged "no run found | |
| # yet" for 45 straight minutes — the `gh run list` name→id lookup | |
| # silently returned [] in the release-event context, and the | |
| # `2>/dev/null || echo '[]'` fallback made it look identical to an | |
| # empty page. `gh api /repos/{repo}/actions/workflows/<file>/runs` | |
| # skips the lookup, and we no longer swallow gh's stderr — a real | |
| # API error now gets one retry then fails the gate loudly instead | |
| # of burning the 45-min budget. | |
| for wf_file in "test.yml" "simctl-tests.yml"; do | |
| echo "::group::Waiting for $wf_file" | |
| # 120-min budget per workflow. | |
| # - v0.5.876: 60 → 90 (Tests wall-time grew past 60). | |
| # - v0.5.883: 90 → 120. v0.5.882 hit 90-min wall because | |
| # macOS-14 runner queue delayed doc-tests start by an | |
| # hour. Tests itself runs ~30 min, but Release Packages | |
| # starts at tag-time and runs out of budget waiting for | |
| # queue + execution. 120 absorbs typical busy-hour queue. | |
| deadline=$(( SECONDS + 120 * 60 )) | |
| retry_on_error=1 | |
| while :; do | |
| set +e | |
| # `per_page=20` (was 1) — return the LAST 20 runs on this SHA | |
| # ordered by created_at desc, then `select(.conclusion == | |
| # "success")` to find the first successful one. Pre-fix | |
| # per_page=1 returned only the newest run on the SHA, which | |
| # might be in_progress while an older run on the SAME SHA was | |
| # already green — the gate hung until timeout. Happened twice | |
| # during v0.5.386's release-packages cycle (the main-branch | |
| # Tests run was green, but a tag-triggered Tests run on the | |
| # same SHA was newer + still running, locking the gate). | |
| api_out=$(gh api \ | |
| "/repos/$REPO/actions/workflows/$wf_file/runs?head_sha=$SHA&per_page=20" \ | |
| 2>&1) | |
| rc=$? | |
| set -e | |
| if [ $rc -ne 0 ]; then | |
| echo " gh api failed (rc=$rc):" | |
| echo "$api_out" | sed 's/^/ /' | |
| if [ $retry_on_error -gt 0 ]; then | |
| retry_on_error=0 | |
| echo " retrying once after 10s" | |
| sleep 10 | |
| continue | |
| fi | |
| echo " gh api still failing after retry — failing gate." | |
| exit 1 | |
| fi | |
| retry_on_error=1 | |
| # Look for ANY successful run on this SHA (order-independent). | |
| success_url=$(echo "$api_out" | jq -r '.workflow_runs[] | select(.status == "completed" and .conclusion == "success") | .html_url' | head -1) | |
| # Look for any failed/cancelled/timed-out completed run as a | |
| # FAIL signal — but only if there's NO successful run for | |
| # this SHA. This way a flaky run that someone reran to green | |
| # doesn't block the release. | |
| fail_url=$(echo "$api_out" | jq -r '.workflow_runs[] | select(.status == "completed" and .conclusion != "success") | .html_url' | head -1) | |
| # Newest run for the "still running" status line. | |
| latest_status=$(echo "$api_out" | jq -r '.workflow_runs[0].status // empty') | |
| latest_url=$(echo "$api_out" | jq -r '.workflow_runs[0].html_url // empty') | |
| if [ -n "$success_url" ]; then | |
| echo " OK $wf_file passed: $success_url" | |
| break | |
| elif [ -z "$latest_status" ]; then | |
| echo " no run found yet for $wf_file on $SHA — waiting" | |
| elif [ "$latest_status" = "completed" ] && [ -n "$fail_url" ]; then | |
| echo " FAIL $wf_file: every completed run on this SHA failed" | |
| echo " most-recent: $fail_url" | |
| exit 1 | |
| else | |
| echo " $wf_file: latest=$latest_status ($latest_url) — waiting" | |
| fi | |
| if [ $SECONDS -ge $deadline ]; then | |
| echo " timed out waiting for $wf_file" | |
| exit 1 | |
| fi | |
| sleep 30 | |
| done | |
| echo "::endgroup::" | |
| done | |
| echo "All gate workflows passed — proceeding to build/publish." | |
| # --------------------------------------------------------------------------- | |
| # Build release binaries for all platforms | |
| # --------------------------------------------------------------------------- | |
| build: | |
| needs: await-tests | |
| strategy: | |
| # Don't let one platform's build failure cancel the others (mirrors | |
| # build-cross). A broken cross-host target should not strand the | |
| # binaries that did build — partial publish beats no publish. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| artifact: perry-macos-aarch64 | |
| - os: macos-15 | |
| target: x86_64-apple-darwin | |
| artifact: perry-macos-x86_64 | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-gnu | |
| artifact: perry-linux-x86_64 | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| artifact: perry-linux-aarch64 | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-musl | |
| artifact: perry-linux-x86_64-musl | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-musl | |
| artifact: perry-linux-aarch64-musl | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| artifact: perry-windows-x86_64 | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| # Pin the macOS deployment target so bottle object files carry a | |
| # stable LC_VERSION_MIN / LC_BUILD_VERSION tag regardless of which | |
| # GitHub runner OS built them. Otherwise users linking against the | |
| # bottle on macOS 14 see `ld: warning: ... was built for newer 'macOS' | |
| # version (15.x) than being linked (14.x)` across every stdlib .o. | |
| MACOSX_DEPLOYMENT_TARGET: "13.0" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| # Cache cargo registry + target/ per (matrix.target, Cargo.lock). | |
| # First run on a target is still a cold build, but every subsequent | |
| # release-tag on the same Cargo.lock reuses the compiled crates — | |
| # webkit6/gstreamer/swc/etc. dominate cold cargo and recompile takes | |
| # 10-15 min per matrix entry. Cache hit drops that to ~30s. | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "release-${{ matrix.target }}" | |
| # #4856 — evict workspace-crate fingerprints from the restored cache. | |
| # rust-cache's own workspace cleanup misses cross-target dirs, so a | |
| # restored target/ can make cargo treat perry-runtime & co. as | |
| # up-to-date and silently reuse a staticlib built from older sources | |
| # (v0.5.1150 shipped Apple cross runtimes missing | |
| # perry_macos_bundle_chdir that way). Deleting the .fingerprint | |
| # entries forces every workspace crate to rebuild from the checkout | |
| # while keeping the expensive external-dependency precompiles cached. | |
| # Note `cargo clean -p` is NOT equivalent: it only covers the host | |
| # layout, not target/<triple>/ dirs (verified on cargo 1.8x). | |
| - name: Evict workspace crates from restored cache (#4856) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r pkg; do | |
| rm -rf target/release/.fingerprint/"$pkg"-* \ | |
| target/*/release/.fingerprint/"$pkg"-* | |
| done < <(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name') | |
| # Closes #394: Homebrew bottle was missing libperry_ui_ios.a so | |
| # `perry compile foo.ts -o app --target ios[-simulator]` failed at link | |
| # with `perry/ui imported but libperry_ui_ios.a not found`. Both macOS | |
| # legs of the matrix now also cross-compile perry-runtime / perry-stdlib | |
| # / perry-ui-ios for BOTH the iOS device and simulator triples and ship | |
| # the resulting .a files alongside the host artifacts. Device libs use | |
| # the canonical _ios suffix (`libperry_ui_ios.a`); simulator libs use | |
| # `_ios_sim` (`libperry_ui_ios_sim.a`) so they don't collide in the | |
| # bottle's flat lib dir. library_search.rs's cross-compile branch | |
| # picks the right variant per --target. tier-3 tvOS/visionOS/watchOS | |
| # are still a follow-up (need nightly + -Zbuild-std). | |
| - name: Install iOS Rust targets (macOS) | |
| if: runner.os == 'macOS' | |
| run: rustup target add aarch64-apple-ios aarch64-apple-ios-sim | |
| # Closes #396 / #397 / #398: tvOS, visionOS, and watchOS are Rust Tier-3 | |
| # targets — no prebuilt std, so we install nightly + rust-src and use | |
| # `-Zbuild-std=core,std,panic_abort` below to compile the standard library | |
| # from source for each (device, sim) triple. Stable stays the default | |
| # toolchain; we only invoke nightly explicitly via `cargo +nightly` for | |
| # the Tier-3 cross-builds. | |
| - name: Install Rust nightly + rust-src for Tier-3 Apple targets (macOS) | |
| if: runner.os == 'macOS' | |
| run: rustup toolchain install nightly --component rust-src --profile minimal | |
| - name: Install GTK4 development libraries (Linux glibc) | |
| if: runner.os == 'Linux' && !endsWith(matrix.target, '-musl') | |
| # libgstreamer1.0-dev added in v0.5.456 — perry-ui-gtk4 pulls in | |
| # gstreamer-sys via the perry/media (#351, v0.5.440) GTK4 backend | |
| # for streaming audio playback. Mirrors the doc-tests-gtk4 fix | |
| # from v0.5.441 (commit a535a984). | |
| # libunwind-dev added in v0.5.461 — on ubuntu-22.04 (jammy) | |
| # libgstreamer1.0-dev hard-depends on libunwind-dev which is held | |
| # back in the runner image, so apt-get install fails with "Unable | |
| # to correct problems" unless libunwind-dev is named explicitly. | |
| # ubuntu-24.04-arm doesn't have this issue but the explicit name | |
| # is harmless there. | |
| # libwebkitgtk-6.0-dev + libshumate-dev added v0.5.855 — perry-ui-gtk4 | |
| # builds (via the workflow's `-p perry-ui-gtk4` step) pull in webkit6 | |
| # (which transitively needs javascriptcoregtk-6.0.pc + webkitgtk-6.0.pc | |
| # + libsoup-3.0.pc) and libshumate-sys (#517 MapView). Mirrors the | |
| # apt step in test.yml's doc-tests-gtk4 job. | |
| run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libunwind-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libwebkitgtk-6.0-dev libshumate-dev | |
| - name: Install musl tools (Linux musl) | |
| if: endsWith(matrix.target, '-musl') | |
| run: sudo apt-get update && sudo apt-get install -y musl-tools | |
| - name: Build perry | |
| # Build only the CLI crate; the runtime libraries are built in | |
| # the next step. (Perry is V8-free — the former perry-jsruntime | |
| # crate, which downloaded a prebuilt V8 that 404'd on musl, was | |
| # removed.) | |
| run: cargo build --release --target ${{ matrix.target }} -p perry | |
| - name: Build runtime libraries | |
| run: | | |
| cargo build --release --target ${{ matrix.target }} -p perry-runtime | |
| cargo build --release --target ${{ matrix.target }} -p perry-stdlib | |
| - name: Build panic=abort runtime variant (Unix) | |
| # Out-of-tree installs can't rebuild the runtime, so ship the | |
| # panic=abort profile prebuilt: `perry compile` links it for | |
| # runtime-only apps with no catch_unwind callers (games, CLIs), | |
| # dropping unwind tables/landing pads (~12-18% of the binary). | |
| # See optimized_libs.rs (find_runtime_abort_library selection). | |
| # Separate CARGO_TARGET_DIR so the profile override doesn't | |
| # invalidate the main build's incremental cache. | |
| if: runner.os != 'Windows' | |
| run: | | |
| CARGO_TARGET_DIR=target-abort CARGO_PROFILE_RELEASE_PANIC=abort \ | |
| cargo build --release --target ${{ matrix.target }} -p perry-runtime | |
| cp "target-abort/${{ matrix.target }}/release/libperry_runtime.a" \ | |
| "target/${{ matrix.target }}/release/libperry_runtime_abort.a" | |
| - name: Build native ext libraries (Unix) | |
| # #2532 — the perry-ext-* wrapper crates ship the host functions for | |
| # node:http (server), ws, net, zlib, fastify, the db drivers, etc. | |
| # They are NOT dependencies of perry-stdlib, so `cargo build -p | |
| # perry-stdlib` above does not produce them. An out-of-tree install | |
| # needs the prebuilt `libperry_ext_*.a` sitting next to | |
| # libperry_runtime.a so `perry compile` can link them without a | |
| # workspace checkout (see optimized_libs.rs::resolve_prebuilt_ext_libs). | |
| # Best-effort per crate: a wrapper that can't build on this host must | |
| # not fail the whole release — the packaging glob below ships | |
| # whatever was produced. | |
| if: runner.os != 'Windows' | |
| run: | | |
| for d in crates/perry-ext-*; do | |
| [ -d "$d" ] || continue | |
| name=$(basename "$d") | |
| echo "::group::build $name" | |
| cargo build --release --target ${{ matrix.target }} -p "$name" \ | |
| || echo " (skipped $name — failed to build on this host)" | |
| echo "::endgroup::" | |
| done | |
| - name: Build UI library (macOS) | |
| if: runner.os == 'macOS' | |
| run: cargo build --release --target ${{ matrix.target }} -p perry-ui-macos | |
| # Closes #394: cross-compile the runtime + stdlib + UI for both the | |
| # iOS device and simulator triples (both Tier-2 stable). Library | |
| # filenames are disambiguated in the staging step — device libs keep | |
| # the canonical `_ios` name, simulator libs get a `_sim` variant | |
| # suffix so the bottle can ship both side-by-side. tvOS/visionOS/ | |
| # watchOS are Tier-3 (need nightly + -Zbuild-std) and out of scope. | |
| - name: Build iOS cross-compile libraries (macOS) | |
| # Only on the arm64 mac job. These iOS/Apple cross-libs are aarch64 and | |
| # identical regardless of host, so building them on the x86_64 mac runner | |
| # is redundant — and the macos-15 x86_64 runner fails `libsqlite3-sys` | |
| # bindgen on the iOS target (libclang/SDK env). The macOS staging step | |
| # copies these libs only `if [ -f ]`, so the x86_64 bottle degrades | |
| # gracefully; the arm64 bottle + the build-cross tarballs still ship them. | |
| if: runner.os == 'macOS' && matrix.target == 'aarch64-apple-darwin' | |
| run: | | |
| for triple in aarch64-apple-ios aarch64-apple-ios-sim; do | |
| cargo build --release --target "$triple" -p perry-runtime | |
| cargo build --release --target "$triple" -p perry-stdlib | |
| cargo build --release --target "$triple" -p perry-ui-ios | |
| done | |
| # Closes #396 / #397 / #398: cross-compile runtime + stdlib + UI for the | |
| # Tier-3 Apple platforms (tvOS, visionOS, watchOS). Both device and | |
| # simulator triples per platform; nightly + `-Zbuild-std=...` builds std | |
| # from source. Note watchOS device uses the unusual `arm64_32` triple | |
| # (32-bit pointers, 64-bit registers) — the only non-`aarch64-*` device | |
| # triple in this set, so we keep the device/sim pairs explicit instead | |
| # of deriving them. Library filenames are disambiguated in the staging | |
| # step (device → `_<plat>.a`, sim → `_<plat>_sim.a`); library_search.rs's | |
| # `apple_class_lib_name` already maps each `--target` to the correct | |
| # variant (covered by the `handles_other_class_suffixes` unit test). | |
| - name: Build tvOS/visionOS/watchOS cross-compile libraries (macOS) | |
| # arm64 mac job only — same rationale as the iOS step above. | |
| if: runner.os == 'macOS' && matrix.target == 'aarch64-apple-darwin' | |
| run: | | |
| set -e | |
| # Format: "<platform>:<device-triple>:<sim-triple>" | |
| # watchOS dropped v0.5.888: ring 0.17.14 has a pointer-size | |
| # assertion that fails for `arm64_32-apple-watchos` (32-bit | |
| # pointers with 64-bit regs). Sim target (aarch64-apple-watchos-sim) | |
| # also dropped to keep the matrix simple — re-add both when | |
| # ring publishes a fix or we pin to a working pre-0.17.14 | |
| # version. | |
| for entry in \ | |
| "tvos:aarch64-apple-tvos:aarch64-apple-tvos-sim" \ | |
| "visionos:aarch64-apple-visionos:aarch64-apple-visionos-sim"; do | |
| plat="${entry%%:*}" | |
| rest="${entry#*:}" | |
| dev="${rest%%:*}" | |
| sim="${rest#*:}" | |
| for triple in "$dev" "$sim"; do | |
| echo "::group::$plat $triple" | |
| cargo +nightly build --release \ | |
| -Z build-std=core,std,panic_abort \ | |
| --target "$triple" \ | |
| -p perry-runtime -p perry-stdlib "-p" "perry-ui-${plat}" | |
| echo "::endgroup::" | |
| done | |
| done | |
| - name: Build UI library (Linux glibc) | |
| if: runner.os == 'Linux' && !endsWith(matrix.target, '-musl') | |
| run: cargo build --release --target ${{ matrix.target }} -p perry-ui-gtk4 | |
| - name: Build UI library (Windows) | |
| if: runner.os == 'Windows' | |
| run: cargo build --release --target ${{ matrix.target }} -p perry-ui-windows | |
| # Closes #872: cross-compile the Android runtime libs on the Windows | |
| # leg so the WinGet zip includes `libperry_runtime.a` / | |
| # `libperry_stdlib.a` for `aarch64-linux-android`. Without these, | |
| # `perry compile --target android` from a WinGet-installed perry | |
| # bails at link time with "Could not find libperry_runtime.a". | |
| # GitHub-hosted Windows runners pre-install the Android NDK at | |
| # `%ANDROID_NDK_HOME%`; we point cargo at its `aarch64-linux-android-*` | |
| # clang as the cross-linker. | |
| - name: Install aarch64-linux-android Rust target (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: rustup target add aarch64-linux-android | |
| - name: Build Android cross-compile libraries (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| env: | |
| # API level 24 matches every other Android leg we cross-build for | |
| # elsewhere (perry-ui-android's build.rs assumes the same NDK | |
| # surface). | |
| ANDROID_API_LEVEL: "24" | |
| run: | | |
| if (-not $env:ANDROID_NDK_HOME) { | |
| $env:ANDROID_NDK_HOME = $env:ANDROID_NDK_LATEST_HOME | |
| } | |
| if (-not $env:ANDROID_NDK_HOME) { | |
| Write-Error "ANDROID_NDK_HOME not set on this Windows runner — needed for #872 android cross-build." | |
| exit 1 | |
| } | |
| $toolchain = Join-Path $env:ANDROID_NDK_HOME "toolchains/llvm/prebuilt/windows-x86_64/bin" | |
| $linker = Join-Path $toolchain "aarch64-linux-android$($env:ANDROID_API_LEVEL)-clang.cmd" | |
| if (-not (Test-Path $linker)) { | |
| $linker = Join-Path $toolchain "aarch64-linux-android$($env:ANDROID_API_LEVEL)-clang.exe" | |
| } | |
| if (-not (Test-Path $linker)) { | |
| Write-Error "Android NDK linker not found at $linker — NDK layout drifted; update the version-suffixed path." | |
| exit 1 | |
| } | |
| $env:CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER = $linker | |
| $env:CC_aarch64_linux_android = $linker | |
| $env:AR_aarch64_linux_android = Join-Path $toolchain "llvm-ar.exe" | |
| cargo build --release --target aarch64-linux-android -p perry-runtime | |
| cargo build --release --target aarch64-linux-android -p perry-stdlib | |
| # #4856 — defense in depth behind the cache eviction above: assert | |
| # every runtime archive this leg built (host triple + any Apple/ | |
| # Android cross triples) defines the sentinel #[no_mangle] symbols. | |
| # A stale cached archive fails the release here instead of breaking | |
| # consumer links with `undefined symbol: _perry_macos_bundle_chdir`. | |
| - name: Verify runtime archives are fresh (#4856) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| libs=$(find target -path "*/release/libperry_runtime.a" -o -path "*/release/perry_runtime.lib") | |
| if [ -z "$libs" ]; then | |
| echo "::error::no built runtime archives found under target/ — nothing to verify" | |
| exit 1 | |
| fi | |
| # shellcheck disable=SC2086 | |
| ./scripts/check_runtime_symbols.sh $libs | |
| - name: Package release archive (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| mkdir -p staging | |
| cp target/${{ matrix.target }}/release/perry staging/ | |
| # Include static libraries for linking (runtime, stdlib, UI) | |
| for lib in libperry_runtime.a libperry_runtime_abort.a libperry_stdlib.a libperry_ui_macos.a libperry_ui_gtk4.a; do | |
| if [ -f "target/${{ matrix.target }}/release/$lib" ]; then | |
| cp "target/${{ matrix.target }}/release/$lib" staging/ | |
| fi | |
| done | |
| # #2532 — ship the perry-ext-* staticlibs so an out-of-tree install | |
| # can link node:http server / ws / net / zlib / fastify / db | |
| # drivers without the workspace source. `perry compile` finds them | |
| # via the same exe-dir / PERRY_LIB_DIR search as the runtime/stdlib | |
| # libs (optimized_libs.rs::resolve_prebuilt_ext_libs). | |
| for lib in target/${{ matrix.target }}/release/libperry_ext_*.a; do | |
| [ -f "$lib" ] && cp "$lib" staging/ | |
| done | |
| # Closes #394: macOS legs also ship iOS-cross-compiled libs. | |
| # Device variant uses the canonical `_ios` suffix; simulator | |
| # variant gets a `_sim` suffix so both can coexist in the | |
| # bottle's flat lib dir. library_search.rs's cross-compile | |
| # branch composes the right name based on --target. | |
| if [ "${{ runner.os }}" = "macOS" ]; then | |
| dev_dir="target/aarch64-apple-ios/release" | |
| if [ -f "$dev_dir/libperry_runtime.a" ]; then | |
| cp "$dev_dir/libperry_runtime.a" "staging/libperry_runtime_ios.a" | |
| fi | |
| if [ -f "$dev_dir/libperry_stdlib.a" ]; then | |
| cp "$dev_dir/libperry_stdlib.a" "staging/libperry_stdlib_ios.a" | |
| fi | |
| if [ -f "$dev_dir/libperry_ui_ios.a" ]; then | |
| cp "$dev_dir/libperry_ui_ios.a" "staging/libperry_ui_ios.a" | |
| fi | |
| sim_dir="target/aarch64-apple-ios-sim/release" | |
| if [ -f "$sim_dir/libperry_runtime.a" ]; then | |
| cp "$sim_dir/libperry_runtime.a" "staging/libperry_runtime_ios_sim.a" | |
| fi | |
| if [ -f "$sim_dir/libperry_stdlib.a" ]; then | |
| cp "$sim_dir/libperry_stdlib.a" "staging/libperry_stdlib_ios_sim.a" | |
| fi | |
| if [ -f "$sim_dir/libperry_ui_ios.a" ]; then | |
| cp "$sim_dir/libperry_ui_ios.a" "staging/libperry_ui_ios_sim.a" | |
| fi | |
| # Closes #396 / #397 / #398: same pattern as iOS for the Tier-3 | |
| # Apple platforms. Device triple varies (watchOS = arm64_32) so | |
| # we drive the staging copy from the explicit triple list above. | |
| # watchOS dropped — see the build step above. | |
| for entry in \ | |
| "tvos:aarch64-apple-tvos:aarch64-apple-tvos-sim" \ | |
| "visionos:aarch64-apple-visionos:aarch64-apple-visionos-sim"; do | |
| plat="${entry%%:*}" | |
| rest="${entry#*:}" | |
| dev_triple="${rest%%:*}" | |
| sim_triple="${rest#*:}" | |
| dev_dir="target/${dev_triple}/release" | |
| sim_dir="target/${sim_triple}/release" | |
| if [ -f "$dev_dir/libperry_runtime.a" ]; then | |
| cp "$dev_dir/libperry_runtime.a" "staging/libperry_runtime_${plat}.a" | |
| fi | |
| if [ -f "$dev_dir/libperry_stdlib.a" ]; then | |
| cp "$dev_dir/libperry_stdlib.a" "staging/libperry_stdlib_${plat}.a" | |
| fi | |
| if [ -f "$dev_dir/libperry_ui_${plat}.a" ]; then | |
| cp "$dev_dir/libperry_ui_${plat}.a" "staging/libperry_ui_${plat}.a" | |
| fi | |
| if [ -f "$sim_dir/libperry_runtime.a" ]; then | |
| cp "$sim_dir/libperry_runtime.a" "staging/libperry_runtime_${plat}_sim.a" | |
| fi | |
| if [ -f "$sim_dir/libperry_stdlib.a" ]; then | |
| cp "$sim_dir/libperry_stdlib.a" "staging/libperry_stdlib_${plat}_sim.a" | |
| fi | |
| if [ -f "$sim_dir/libperry_ui_${plat}.a" ]; then | |
| cp "$sim_dir/libperry_ui_${plat}.a" "staging/libperry_ui_${plat}_sim.a" | |
| fi | |
| done | |
| fi | |
| cd staging | |
| tar czf ../${{ matrix.artifact }}.tar.gz * | |
| # Bundle xwin.exe so `perry setup windows` can download the MS CRT + Windows | |
| # SDK without requiring users to have Rust installed. xwin is pinned here to | |
| # control updates — bump the version when switching to a newer xwin release. | |
| # See crates/perry/src/commands/setup.rs::windows_wizard for the consumer. | |
| - name: Build xwin.exe for bundling (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| cargo install xwin --locked --version 0.9.0 --root xwin-install --target ${{ matrix.target }} | |
| - name: Package release archive (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path staging | |
| Copy-Item "target/${{ matrix.target }}/release/perry.exe" staging/ | |
| Copy-Item "xwin-install/bin/xwin.exe" staging/ | |
| # Include static libraries for linking (runtime, stdlib, UI) | |
| $libs = @("perry_runtime.lib", "perry_stdlib.lib", "perry_ui_windows.lib") | |
| foreach ($lib in $libs) { | |
| $path = "target/${{ matrix.target }}/release/$lib" | |
| if (Test-Path $path) { | |
| Copy-Item $path staging/ | |
| } | |
| } | |
| # #872: stage the Android cross-compiled archives next to the | |
| # Windows libs. perry's library_search.rs walks | |
| # `<install-dir>/<triple>/release/<libname>.a` so we mirror that | |
| # layout inside the zip — the `aarch64-linux-android/release/` | |
| # subdir lives alongside `perry.exe`. | |
| $androidLibDir = "staging/aarch64-linux-android/release" | |
| New-Item -ItemType Directory -Force -Path $androidLibDir | Out-Null | |
| $androidLibs = @("libperry_runtime.a", "libperry_stdlib.a") | |
| foreach ($lib in $androidLibs) { | |
| $src = "target/aarch64-linux-android/release/$lib" | |
| if (Test-Path $src) { | |
| Copy-Item $src $androidLibDir | |
| } | |
| } | |
| Compress-Archive -Path staging/* -DestinationPath "${{ matrix.artifact }}.zip" | |
| # SHA256 sidecars uploaded next to each archive so package-manager | |
| # manifests (Scoop autoupdate, future winget/chocolatey/Homebrew bumps) | |
| # can resolve hashes by URL without re-downloading the archive on the | |
| # bumper machine. The sidecar format is `<hash> <filename>` (two | |
| # spaces — same as `sha256sum`'s default), which Scoop's hash.url | |
| # mode parses out of the box. | |
| - name: Compute SHA256 sidecar (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| # Linux ships `sha256sum` (coreutils); macOS ships `shasum -a 256` | |
| # but no `sha256sum`. Both produce the same `<hash> <filename>` | |
| # output format that Scoop / Homebrew / dpkg parse from the first | |
| # whitespace-separated field. Pre-fix the macOS legs of this matrix | |
| # bombed at v0.5.386's release-packages.yml run with | |
| # `sha256sum: command not found` (exit 127), cancelling the | |
| # x86_64-apple-darwin sibling via fail-fast and shipping a release | |
| # without macOS binaries. | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.tar.gz.sha256" | |
| else | |
| shasum -a 256 "${{ matrix.artifact }}.tar.gz" > "${{ matrix.artifact }}.tar.gz.sha256" | |
| fi | |
| cat "${{ matrix.artifact }}.tar.gz.sha256" | |
| - name: Compute SHA256 sidecar (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $hash = (Get-FileHash -Algorithm SHA256 "${{ matrix.artifact }}.zip").Hash.ToLower() | |
| # Match `sha256sum` output exactly (two-space separator) for cross-platform | |
| # tooling that grep-cuts the first field. | |
| "$hash ${{ matrix.artifact }}.zip" | Out-File -Encoding ascii "${{ matrix.artifact }}.zip.sha256" | |
| Get-Content "${{ matrix.artifact }}.zip.sha256" | |
| - name: Upload release asset (Unix) | |
| if: runner.os != 'Windows' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # workflow_dispatch fallback: `github.event.release.tag_name` is | |
| # only set for `release` events, so when triggered manually we | |
| # look up the latest published release tag and upload to that. | |
| # Pre-fix the v0.5.386 release shipped with NO assets uploaded | |
| # because the original `release` run failed at await-tests, my | |
| # workflow_dispatch bypass succeeded the build but skipped this | |
| # upload step entirely. | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: uploading to latest release tag $TAG" | |
| fi | |
| gh release upload "$TAG" "${{ matrix.artifact }}.tar.gz" --clobber | |
| gh release upload "$TAG" "${{ matrix.artifact }}.tar.gz.sha256" --clobber | |
| - name: Upload release asset (Windows) | |
| if: runner.os == 'Windows' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| $tag = "${{ github.event.release.tag_name }}" | |
| if (-not $tag) { | |
| $tag = (gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| Write-Host "workflow_dispatch fallback: uploading to latest release tag $tag" | |
| } | |
| gh release upload "$tag" "${{ matrix.artifact }}.zip" --clobber | |
| gh release upload "$tag" "${{ matrix.artifact }}.zip.sha256" --clobber | |
| - name: Upload build artifact (Unix, for deb + npm-publish jobs) | |
| if: runner.os != 'Windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: staging/ | |
| - name: Upload build artifact (Windows, for npm-publish job) | |
| if: runner.os == 'Windows' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: staging/ | |
| # --------------------------------------------------------------------------- | |
| # Build per-target cross-compile staticlib bundles (issue #1083) | |
| # | |
| # The host bundles emitted by `build:` above ship only host-triple .a's, so | |
| # every consumer build worker that cross-compiles to a non-host target has | |
| # to re-derive the cross toolchain env on each `perry update` (Apple | |
| # sysroot vars, NDK clang paths, MSVC `lib.exe` shims, alsa/audio dev | |
| # headers, aws-lc-sys jitterentropy disable, etc.) and pay a 5-25 min | |
| # cargo rebuild per target. We already build these libs correctly in | |
| # release CI's `build:` matrix — this job just packages them into | |
| # standalone tarballs so the worker can `tar xzf` instead of rebuild. | |
| # | |
| # Each `perry-cross-<triple>.tar.gz` ships: | |
| # libperry_runtime.a (the cross-compiled runtime) | |
| # libperry_stdlib.a (the cross-compiled stdlib) | |
| # libperry_ui_<plat>.a (the cross-compiled UI backend, when applicable) | |
| # manifest.json (perry_version + target_triple + sha256+size per file) | |
| # | |
| # Each matrix entry runs on the natural host runner for its toolchain: | |
| # - Apple targets: macos-14 (real Xcode + SDKs) | |
| # - Android : ubuntu-24.04 + pre-installed NDK | |
| # - Windows MSVC : windows-latest (clang-cl + lib.exe) | |
| # — so no Linux→Apple sysroot or Linux→Windows VM dance is needed inside | |
| # this workflow. | |
| # --------------------------------------------------------------------------- | |
| build-cross: | |
| needs: await-tests | |
| strategy: | |
| # Each leg is independent; let one Tier-3 failure not cancel the | |
| # other targets so we still ship the bundles that did build. | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # --- Apple, Tier-2 (stable) --------------------------------------- | |
| - os: macos-14 | |
| target: aarch64-apple-darwin | |
| ui_crate: perry-ui-macos | |
| ui_lib: libperry_ui_macos.a | |
| tier3: false | |
| - os: macos-15 | |
| target: x86_64-apple-darwin | |
| ui_crate: perry-ui-macos | |
| ui_lib: libperry_ui_macos.a | |
| tier3: false | |
| - os: macos-14 | |
| target: aarch64-apple-ios | |
| ui_crate: perry-ui-ios | |
| ui_lib: libperry_ui_ios.a | |
| tier3: false | |
| - os: macos-14 | |
| target: aarch64-apple-ios-sim | |
| ui_crate: perry-ui-ios | |
| ui_lib: libperry_ui_ios.a | |
| tier3: false | |
| # --- Apple, Tier-3 (nightly + build-std) -------------------------- | |
| # tvOS / visionOS are Rust Tier-3: no prebuilt std, so we install | |
| # nightly + rust-src and pass `-Zbuild-std=core,std,panic_abort` | |
| # in the build step below. Mirrors the existing `build:` job's | |
| # tvos/visionos handling. watchOS is currently dropped — see the | |
| # comment in `build:` (ring 0.17.14 pointer-size assertion fails | |
| # for `arm64_32-apple-watchos`). | |
| - os: macos-14 | |
| target: aarch64-apple-tvos | |
| ui_crate: perry-ui-tvos | |
| ui_lib: libperry_ui_tvos.a | |
| tier3: true | |
| - os: macos-14 | |
| target: aarch64-apple-tvos-sim | |
| ui_crate: perry-ui-tvos | |
| ui_lib: libperry_ui_tvos.a | |
| tier3: true | |
| - os: macos-14 | |
| target: aarch64-apple-visionos | |
| ui_crate: perry-ui-visionos | |
| ui_lib: libperry_ui_visionos.a | |
| tier3: true | |
| - os: macos-14 | |
| target: aarch64-apple-visionos-sim | |
| ui_crate: perry-ui-visionos | |
| ui_lib: libperry_ui_visionos.a | |
| tier3: true | |
| # --- Android (NDK on Linux runner) -------------------------------- | |
| # perry-ui-android requires the NDK clang++ wrapper and JNI; the | |
| # existing test.yml leg already cross-compiles it on ubuntu-24.04, | |
| # so we mirror that env here. x86_64 included so emulator builds | |
| # also have prebuilt libs. | |
| - os: ubuntu-24.04 | |
| target: aarch64-linux-android | |
| ui_crate: perry-ui-android | |
| ui_lib: libperry_ui_android.a | |
| tier3: false | |
| - os: ubuntu-24.04 | |
| target: x86_64-linux-android | |
| ui_crate: perry-ui-android | |
| ui_lib: libperry_ui_android.a | |
| tier3: false | |
| # --- Windows MSVC (native runner) --------------------------------- | |
| # Only x86_64 today. perry's compile.rs maps `--target windows` | |
| # to `x86_64-pc-windows-msvc` (compile.rs:1009); ARM64 Windows | |
| # support is a separate follow-up — re-add `aarch64-pc-windows-msvc` | |
| # here once the CLI knows about it. | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| ui_crate: perry-ui-windows | |
| ui_lib: perry_ui_windows.lib | |
| tier3: false | |
| runs-on: ${{ matrix.os }} | |
| env: | |
| # Match `build:` for macOS object-file version tags so bottle-linked | |
| # .a's don't emit "built for newer 'macOS' version" warnings against | |
| # users on macOS 13. | |
| MACOSX_DEPLOYMENT_TARGET: "13.0" | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install Rust stable + cross target | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install Rust nightly + rust-src (Tier-3 only) | |
| if: matrix.tier3 | |
| run: rustup toolchain install nightly --component rust-src --profile minimal | |
| # Same cache shape as `build:` so the cross legs share crate compiles | |
| # across release cycles. Keyed on (target, Cargo.lock) — first run on a | |
| # new target is cold, but every subsequent release-tag reuses the | |
| # SWC/llvm-sys/webkit etc. precompile from the prior cycle. | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| shared-key: "release-cross-${{ matrix.target }}" | |
| # #4856 — evict workspace-crate fingerprints from the restored cache | |
| # so perry-runtime/stdlib/UI always rebuild from the checkout. The | |
| # v0.5.1150 Apple cross bundles shipped a stale pre-#4833 | |
| # libperry_runtime.a (missing perry_macos_bundle_chdir) because the | |
| # restored cache let cargo skip the rebuild. Mirrors the eviction | |
| # step in `build:` — see the comment there for why `cargo clean -p` | |
| # doesn't work (host layout only, misses target/<triple>/). | |
| - name: Evict workspace crates from restored cache (#4856) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| while IFS= read -r pkg; do | |
| rm -rf target/release/.fingerprint/"$pkg"-* \ | |
| target/*/release/.fingerprint/"$pkg"-* | |
| done < <(cargo metadata --no-deps --format-version 1 | jq -r '.packages[].name') | |
| # --- Per-runner toolchain wiring -------------------------------------- | |
| # Each of these blocks installs what the cross target needs that's NOT | |
| # already on the runner image. Apple runners get Xcode for free; Linux | |
| # runners get GTK/audio dev headers for the host build, but we don't | |
| # need those here because we're cross-compiling. | |
| - name: Wire Android NDK (Linux runner, android targets) | |
| if: startsWith(matrix.target, 'aarch64-linux-android') || startsWith(matrix.target, 'x86_64-linux-android') | |
| run: | | |
| set -euo pipefail | |
| # Reuse test.yml's NDK discovery — same precedence chain. | |
| NDK="" | |
| if [ -n "${ANDROID_NDK_HOME:-}" ]; then | |
| NDK="$ANDROID_NDK_HOME" | |
| elif [ -d "${ANDROID_HOME:-}/ndk-bundle" ]; then | |
| NDK="$ANDROID_HOME/ndk-bundle" | |
| elif [ -d "${ANDROID_HOME:-}/ndk" ]; then | |
| NDK=$(ls -1d "$ANDROID_HOME/ndk/"*/ 2>/dev/null | sort -V | tail -1) | |
| NDK="${NDK%/}" | |
| fi | |
| if [ -z "$NDK" ]; then | |
| echo "::error::No Android NDK found on runner — cannot cross-build ${{ matrix.target }}" | |
| exit 1 | |
| fi | |
| echo "ANDROID_NDK_HOME=$NDK" >> "$GITHUB_ENV" | |
| HOST_TAG=linux-x86_64 | |
| TOOLCHAIN="$NDK/toolchains/llvm/prebuilt/$HOST_TAG/bin" | |
| TRIPLE="${{ matrix.target }}" | |
| # NDK clang wrappers are named `<triple><api>-clang` (e.g. | |
| # `aarch64-linux-android24-clang`); the API suffix is mandatory | |
| # — `<triple>-clang` (no API) doesn't exist. We glob the highest | |
| # API present on the runner image, matching test.yml's logic. | |
| CLANG=$(ls "$TOOLCHAIN"/${TRIPLE}*-clang 2>/dev/null | sort -V | tail -1) | |
| CLANGXX=$(ls "$TOOLCHAIN"/${TRIPLE}*-clang++ 2>/dev/null | sort -V | tail -1) | |
| if [ -z "$CLANG" ]; then | |
| echo "::error::Could not locate NDK clang under $TOOLCHAIN for $TRIPLE" | |
| exit 1 | |
| fi | |
| # cc-rs lookup vars: `CC_<triple>` / `CXX_<triple>` / `AR_<triple>` | |
| # with `-` → `_`. The cargo-target linker var uses uppercase + `_`. | |
| TRIPLE_UNDER=$(echo "$TRIPLE" | tr '-' '_') | |
| TRIPLE_UPPER=$(echo "$TRIPLE_UNDER" | tr '[:lower:]' '[:upper:]') | |
| { | |
| echo "CC_${TRIPLE_UNDER}=$CLANG" | |
| echo "CXX_${TRIPLE_UNDER}=$CLANGXX" | |
| echo "AR_${TRIPLE_UNDER}=$TOOLCHAIN/llvm-ar" | |
| echo "CARGO_TARGET_${TRIPLE_UPPER}_LINKER=$CLANG" | |
| } >> "$GITHUB_ENV" | |
| echo "NDK wired for $TRIPLE: $CLANG" | |
| # The aarch64 MSVC target is Tier-2 but Rust still requires the cross | |
| # std component to be installed by name (the matrix `targets:` field | |
| # above handles that). Nothing else needed — windows-latest already | |
| # ships clang-cl + lib.exe through Visual Studio. | |
| # --- Build the three (or two, when UI is N/A) static libs ------------ | |
| - name: Build runtime + stdlib + UI (stable, Tier-2) | |
| if: ${{ !matrix.tier3 }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cargo build --release --target ${{ matrix.target }} -p perry-runtime | |
| cargo build --release --target ${{ matrix.target }} -p perry-stdlib | |
| # UI crate is matrix-driven. perry-ui-android needs the NDK env | |
| # we wired above; perry-ui-windows builds against the MSVC SDK | |
| # already on windows-latest. Both go through plain `cargo build`. | |
| cargo build --release --target ${{ matrix.target }} -p ${{ matrix.ui_crate }} | |
| - name: Build runtime + stdlib + UI (nightly + build-std, Tier-3) | |
| if: matrix.tier3 | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cargo +nightly build --release \ | |
| -Z build-std=core,std,panic_abort \ | |
| --target ${{ matrix.target }} \ | |
| -p perry-runtime -p perry-stdlib -p ${{ matrix.ui_crate }} | |
| # #4856 — defense in depth behind the cache eviction above: a stale | |
| # cached runtime archive fails the release here instead of breaking | |
| # every consumer executable link on the build workers. | |
| - name: Verify runtime archive is fresh (#4856) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| lib="target/${{ matrix.target }}/release/libperry_runtime.a" | |
| # Windows MSVC emits perry_runtime.lib (no `lib` prefix). | |
| [ -f "$lib" ] || lib="target/${{ matrix.target }}/release/perry_runtime.lib" | |
| ./scripts/check_runtime_symbols.sh "$lib" | |
| # --- Package + manifest ---------------------------------------------- | |
| # Manifest schema (per issue #1083): | |
| # { "perry_version": "<x.y.z>", "target_triple": "<triple>", | |
| # "files": [{"path": "...", "sha256": "...", "size": N}, ...] } | |
| # Computed on the runner (sha256sum on Linux, shasum on macOS, | |
| # PowerShell Get-FileHash on Windows). We write the manifest with a | |
| # plain shell here-doc + jq concatenation; jq is on every GHA runner | |
| # so we don't pull in a separate scripting dep. | |
| - name: Stage cross artifacts + manifest (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TARGET="${{ matrix.target }}" | |
| UI_LIB="${{ matrix.ui_lib }}" | |
| PERRY_VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') | |
| STAGING="cross-staging" | |
| rm -rf "$STAGING" | |
| mkdir -p "$STAGING" | |
| # Required runtime + stdlib (always present on a successful build). | |
| for lib in libperry_runtime.a libperry_stdlib.a; do | |
| src="target/${TARGET}/release/${lib}" | |
| if [ ! -f "$src" ]; then | |
| echo "::error::Expected $src after build but it's missing — aborting bundle." | |
| exit 1 | |
| fi | |
| cp "$src" "$STAGING/$lib" | |
| done | |
| # UI lib (optional — perry-ui-android failures are non-fatal in | |
| # principle, but we built it above so this should exist; we hard-fail | |
| # if it's missing so the worker doesn't see a silently-degraded | |
| # bundle). | |
| UI_SRC="target/${TARGET}/release/${UI_LIB}" | |
| if [ ! -f "$UI_SRC" ]; then | |
| echo "::error::UI lib $UI_SRC missing after build — aborting bundle." | |
| exit 1 | |
| fi | |
| cp "$UI_SRC" "$STAGING/$UI_LIB" | |
| # SHA256 + size for each staged file. macOS ships `shasum -a 256`, | |
| # Linux ships `sha256sum` — wrap both behind one alias. | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha() { sha256sum "$1" | cut -d' ' -f1; } | |
| else | |
| sha() { shasum -a 256 "$1" | cut -d' ' -f1; } | |
| fi | |
| # `stat -c %s` (GNU) / `stat -f %z` (BSD) — choose at runtime. | |
| if stat -c %s "$STAGING/libperry_runtime.a" >/dev/null 2>&1; then | |
| sz() { stat -c %s "$1"; } | |
| else | |
| sz() { stat -f %z "$1"; } | |
| fi | |
| # Build the manifest with jq so we get correct JSON escaping | |
| # (paths and triples are safe ASCII today, but the schema is the | |
| # contract — let jq own it). | |
| files_json=$(jq -n '[]') | |
| for f in libperry_runtime.a libperry_stdlib.a "$UI_LIB"; do | |
| files_json=$(jq -n --argjson acc "$files_json" \ | |
| --arg path "$f" \ | |
| --arg sha "$(sha "$STAGING/$f")" \ | |
| --argjson size "$(sz "$STAGING/$f")" \ | |
| '$acc + [{path: $path, sha256: $sha, size: $size}]') | |
| done | |
| jq -n --arg v "$PERRY_VERSION" --arg t "$TARGET" --argjson files "$files_json" \ | |
| '{perry_version: $v, target_triple: $t, files: $files}' \ | |
| > "$STAGING/manifest.json" | |
| cat "$STAGING/manifest.json" | |
| tar czf "perry-cross-${TARGET}.tar.gz" -C "$STAGING" . | |
| - name: Stage cross artifacts + manifest (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $target = "${{ matrix.target }}" | |
| $uiLib = "${{ matrix.ui_lib }}" | |
| $perryVersion = (Select-String -Path Cargo.toml -Pattern '^version\s*=\s*"([^"]+)"' | | |
| Select-Object -First 1).Matches[0].Groups[1].Value | |
| $staging = "cross-staging" | |
| if (Test-Path $staging) { Remove-Item $staging -Recurse -Force } | |
| New-Item -ItemType Directory -Path $staging | Out-Null | |
| # Windows MSVC builds emit `perry_runtime.lib` / `perry_stdlib.lib` | |
| # (no `lib` prefix, `.lib` extension) — match the host-build | |
| # behavior at line ~448 above. | |
| $libs = @("perry_runtime.lib", "perry_stdlib.lib", $uiLib) | |
| foreach ($lib in $libs) { | |
| $src = "target/$target/release/$lib" | |
| if (-not (Test-Path $src)) { | |
| Write-Error "Expected $src after build but it's missing — aborting bundle." | |
| exit 1 | |
| } | |
| Copy-Item $src (Join-Path $staging $lib) | |
| } | |
| # Build manifest with sha256 + size for each file. | |
| $entries = @() | |
| foreach ($lib in $libs) { | |
| $path = Join-Path $staging $lib | |
| $hash = (Get-FileHash -Algorithm SHA256 $path).Hash.ToLower() | |
| $size = (Get-Item $path).Length | |
| $entries += [pscustomobject]@{ path = $lib; sha256 = $hash; size = $size } | |
| } | |
| $manifest = [pscustomobject]@{ | |
| perry_version = $perryVersion | |
| target_triple = $target | |
| files = $entries | |
| } | |
| $manifest | ConvertTo-Json -Depth 5 | Out-File -Encoding utf8 (Join-Path $staging "manifest.json") | |
| Get-Content (Join-Path $staging "manifest.json") | |
| # tar is available on windows-latest (BSD tar 3.x preinstalled by | |
| # MS since 2018); produce the same .tar.gz extension as Unix so | |
| # consumers only have one filename pattern to handle. | |
| tar czf "perry-cross-$target.tar.gz" -C $staging . | |
| - name: Compute SHA256 sidecar (Unix) | |
| if: runner.os != 'Windows' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ARCHIVE="perry-cross-${{ matrix.target }}.tar.gz" | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "$ARCHIVE" > "$ARCHIVE.sha256" | |
| else | |
| shasum -a 256 "$ARCHIVE" > "$ARCHIVE.sha256" | |
| fi | |
| cat "$ARCHIVE.sha256" | |
| - name: Compute SHA256 sidecar (Windows) | |
| if: runner.os == 'Windows' | |
| shell: pwsh | |
| run: | | |
| $archive = "perry-cross-${{ matrix.target }}.tar.gz" | |
| $hash = (Get-FileHash -Algorithm SHA256 $archive).Hash.ToLower() | |
| "$hash $archive" | Out-File -Encoding ascii "$archive.sha256" | |
| Get-Content "$archive.sha256" | |
| - name: Upload cross release asset (Unix) | |
| if: runner.os != 'Windows' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: uploading to latest release tag $TAG" | |
| fi | |
| ARCHIVE="perry-cross-${{ matrix.target }}.tar.gz" | |
| gh release upload "$TAG" "$ARCHIVE" --clobber | |
| gh release upload "$TAG" "$ARCHIVE.sha256" --clobber | |
| - name: Upload cross release asset (Windows) | |
| if: runner.os == 'Windows' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| shell: pwsh | |
| run: | | |
| $tag = "${{ github.event.release.tag_name }}" | |
| if (-not $tag) { | |
| $tag = (gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| Write-Host "workflow_dispatch fallback: uploading to latest release tag $tag" | |
| } | |
| $archive = "perry-cross-${{ matrix.target }}.tar.gz" | |
| gh release upload "$tag" "$archive" --clobber | |
| gh release upload "$tag" "$archive.sha256" --clobber | |
| # Always upload as a workflow artifact too — lets a maintainer | |
| # pull the bundle off a workflow_dispatch run that didn't target an | |
| # existing release (e.g. a smoke-test on a branch). | |
| - name: Upload cross build artifact (for inspection) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: perry-cross-${{ matrix.target }} | |
| path: | | |
| perry-cross-${{ matrix.target }}.tar.gz | |
| perry-cross-${{ matrix.target }}.tar.gz.sha256 | |
| # --------------------------------------------------------------------------- | |
| # Update Homebrew tap | |
| # --------------------------------------------------------------------------- | |
| homebrew: | |
| needs: build | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get release info | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # workflow_dispatch fallback: github.event.release.tag_name is | |
| # only set for `release` events. When triggered manually, fall | |
| # back to the latest published release tag — same pattern the | |
| # upload step uses (see v0.5.392 for the publish-jobs sweep). | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: using latest release tag $TAG" | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Download source tarball and compute SHA256 | |
| id: sha | |
| run: | | |
| curl -sL "https://github.com/${{ github.repository }}/archive/refs/tags/${{ steps.release.outputs.tag }}.tar.gz" -o source.tar.gz | |
| SHA256=$(sha256sum source.tar.gz | cut -d' ' -f1) | |
| echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" | |
| - name: Download macOS ARM64 bottle and compute SHA256 | |
| id: bottle_arm64 | |
| run: | | |
| curl -sL "https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.tag }}/perry-macos-aarch64.tar.gz" -o perry-macos-aarch64.tar.gz | |
| SHA256=$(sha256sum perry-macos-aarch64.tar.gz | cut -d' ' -f1) | |
| echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" | |
| - name: Download macOS x86_64 bottle and compute SHA256 | |
| id: bottle_x64 | |
| run: | | |
| curl -sL "https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.tag }}/perry-macos-x86_64.tar.gz" -o perry-macos-x86_64.tar.gz | |
| SHA256=$(sha256sum perry-macos-x86_64.tar.gz | cut -d' ' -f1) | |
| echo "sha256=${SHA256}" >> "$GITHUB_OUTPUT" | |
| - name: Update Homebrew tap | |
| env: | |
| TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| run: | | |
| git clone "https://x-access-token:${TAP_TOKEN}@github.com/PerryTS/homebrew-perry.git" tap | |
| cd tap | |
| mkdir -p Formula | |
| cat > Formula/perry.rb << 'FORMULA_EOF' | |
| class Perry < Formula | |
| desc "Native TypeScript compiler — compiles TypeScript to native executables" | |
| homepage "https://github.com/PerryTS/perry" | |
| version "${{ steps.release.outputs.version }}" | |
| license "MIT" | |
| on_macos do | |
| if Hardware::CPU.arm? | |
| url "https://github.com/PerryTS/perry/releases/download/${{ steps.release.outputs.tag }}/perry-macos-aarch64.tar.gz" | |
| sha256 "${{ steps.bottle_arm64.outputs.sha256 }}" | |
| else | |
| url "https://github.com/PerryTS/perry/releases/download/${{ steps.release.outputs.tag }}/perry-macos-x86_64.tar.gz" | |
| sha256 "${{ steps.bottle_x64.outputs.sha256 }}" | |
| end | |
| end | |
| on_linux do | |
| url "https://github.com/PerryTS/perry/archive/refs/tags/${{ steps.release.outputs.tag }}.tar.gz" | |
| sha256 "${{ steps.sha.outputs.sha256 }}" | |
| depends_on "rust" => :build | |
| end | |
| def install | |
| if OS.mac? | |
| bin.install "perry" | |
| lib.install Dir["libperry_*.a"] | |
| else | |
| system "cargo", "build", "--release" | |
| system "cargo", "build", "--release", "-p", "perry-runtime", "-p", "perry-stdlib" | |
| bin.install "target/release/perry" | |
| lib.install Dir["target/release/libperry_*.a"] | |
| end | |
| end | |
| def caveats | |
| <<~EOS | |
| Perry requires a C linker to link compiled executables. | |
| macOS: Xcode Command Line Tools (xcode-select --install) | |
| Linux: GCC or Clang (sudo apt install build-essential) | |
| Quick start: | |
| echo 'console.log("hello")' > hello.ts | |
| perry hello.ts -o hello && ./hello | |
| EOS | |
| end | |
| test do | |
| assert_match "perry", shell_output("#{bin}/perry --version") | |
| (testpath/"test.ts").write('console.log("works");') | |
| system bin/"perry", testpath/"test.ts", "-o", testpath/"test" | |
| assert_equal "works\n", shell_output(testpath/"test") | |
| end | |
| end | |
| FORMULA_EOF | |
| # Remove leading whitespace from heredoc indentation | |
| sed -i 's/^ //' Formula/perry.rb | |
| git config user.name "perry-bot" | |
| git config user.email "bot@perryts.com" | |
| git add Formula/perry.rb | |
| git commit -m "perry ${VERSION}" || echo "No changes" | |
| git push | |
| # --------------------------------------------------------------------------- | |
| # Build .deb packages and update APT repository | |
| # --------------------------------------------------------------------------- | |
| apt: | |
| needs: build | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| include: | |
| - artifact: perry-linux-x86_64 | |
| arch: amd64 | |
| - artifact: perry-linux-aarch64 | |
| arch: arm64 | |
| steps: | |
| - name: Get release info | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: using latest release tag $TAG" | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Download build artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: ${{ matrix.artifact }} | |
| path: staging/ | |
| - name: Build .deb packages | |
| run: | | |
| VERSION="${{ steps.release.outputs.version }}" | |
| ARCH="${{ matrix.arch }}" | |
| umask 022 | |
| require_staging_file() { | |
| local path="staging/$1" | |
| if [ ! -f "$path" ]; then | |
| echo "Missing required release artifact: $path" >&2 | |
| exit 1 | |
| fi | |
| } | |
| write_control() { | |
| local pkg_dir="$1" | |
| local package="$2" | |
| local depends="$3" | |
| local description="$4" | |
| local details="$5" | |
| { | |
| echo "Package: ${package}" | |
| echo "Version: ${VERSION}" | |
| echo "Section: devel" | |
| echo "Priority: optional" | |
| echo "Architecture: ${ARCH}" | |
| if [ -n "$depends" ]; then | |
| echo "Depends: ${depends}" | |
| fi | |
| echo "Maintainer: Perry Team <hello@perryts.com>" | |
| echo "Homepage: https://github.com/PerryTS/perry" | |
| echo "Description: ${description}" | |
| echo " ${details}" | |
| } > "${pkg_dir}/DEBIAN/control" | |
| } | |
| init_package() { | |
| local pkg_dir="$1" | |
| local package="$2" | |
| local depends="$3" | |
| local description="$4" | |
| local details="$5" | |
| rm -rf "$pkg_dir" | |
| mkdir -p "${pkg_dir}/DEBIAN" | |
| chmod 755 "$pkg_dir" "${pkg_dir}/DEBIAN" | |
| write_control "$pkg_dir" "$package" "$depends" "$description" "$details" | |
| } | |
| copy_static_archive() { | |
| local lib="$1" | |
| local pkg_dir="$2" | |
| require_staging_file "$lib" | |
| mkdir -p "${pkg_dir}/usr/lib/perry" | |
| cp "staging/$lib" "${pkg_dir}/usr/lib/perry/" | |
| # Strip DWARF debug info but keep the symbol table. Perry's | |
| # codegen linker still needs the #[no_mangle] symbols, while | |
| # release users do not need archive debug sections. | |
| strip --strip-debug "${pkg_dir}/usr/lib/perry/$lib" || true | |
| chmod 644 "${pkg_dir}/usr/lib/perry/$lib" | |
| } | |
| build_deb() { | |
| local pkg_dir="$1" | |
| # `-Zxz -z9` overrides dpkg-deb's default (zstd on noble) with | |
| # xz level 9. xz is consistently smaller on the libperry_*.a | |
| # archive bodies, and every dpkg version Perry targets supports it. | |
| dpkg-deb --build -Zxz -z9 "$pkg_dir" | |
| } | |
| check_deb_sizes() { | |
| local max_git_blob_bytes=99000000 | |
| local failed=0 | |
| shopt -s nullglob | |
| local debs=( *.deb ) | |
| if [ "${#debs[@]}" -eq 0 ]; then | |
| echo "No .deb packages were produced" >&2 | |
| exit 1 | |
| fi | |
| for deb in "${debs[@]}"; do | |
| local size | |
| size=$(stat -c %s "$deb") | |
| if [ "$size" -gt "$max_git_blob_bytes" ]; then | |
| echo "$deb is $size bytes; GitHub rejects git blobs near 100 MB" >&2 | |
| failed=1 | |
| fi | |
| done | |
| if [ "$failed" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| } | |
| require_staging_file perry | |
| require_staging_file libperry_runtime.a | |
| require_staging_file libperry_stdlib.a | |
| MAIN_DEPENDS="gcc | clang, perry-runtime (= ${VERSION}), perry-stdlib (= ${VERSION})" | |
| if [ -f staging/libperry_ui_gtk4.a ]; then | |
| MAIN_DEPENDS="${MAIN_DEPENDS}, perry-ui-gtk4 (= ${VERSION})" | |
| fi | |
| MAIN_PKG="perry_${VERSION}_${ARCH}" | |
| init_package "$MAIN_PKG" \ | |
| "perry" \ | |
| "$MAIN_DEPENDS" \ | |
| "Native TypeScript compiler" \ | |
| "Perry compiles TypeScript source code directly to native executables using Cranelift for code generation." | |
| mkdir -p "${MAIN_PKG}/usr/bin" "${MAIN_PKG}/etc/perry" | |
| install -m 755 staging/perry "${MAIN_PKG}/usr/bin/perry" | |
| echo "/usr/lib/perry" > "${MAIN_PKG}/etc/perry/lib-path" | |
| build_deb "$MAIN_PKG" | |
| RUNTIME_PKG="perry-runtime_${VERSION}_${ARCH}" | |
| init_package "$RUNTIME_PKG" \ | |
| "perry-runtime" \ | |
| "" \ | |
| "Perry runtime static library" \ | |
| "Static runtime archive required by the Perry compiler when linking generated executables." | |
| copy_static_archive libperry_runtime.a "$RUNTIME_PKG" | |
| build_deb "$RUNTIME_PKG" | |
| STDLIB_PKG="perry-stdlib_${VERSION}_${ARCH}" | |
| init_package "$STDLIB_PKG" \ | |
| "perry-stdlib" \ | |
| "" \ | |
| "Perry standard library static archive" \ | |
| "Static standard library archive required by the Perry compiler when linking generated executables." | |
| copy_static_archive libperry_stdlib.a "$STDLIB_PKG" | |
| build_deb "$STDLIB_PKG" | |
| if [ -f staging/libperry_ui_gtk4.a ]; then | |
| UI_PKG="perry-ui-gtk4_${VERSION}_${ARCH}" | |
| init_package "$UI_PKG" \ | |
| "perry-ui-gtk4" \ | |
| "" \ | |
| "Perry GTK4 UI static archive" \ | |
| "Static GTK4 UI archive used by Perry programs that import the Linux UI backend." | |
| copy_static_archive libperry_ui_gtk4.a "$UI_PKG" | |
| build_deb "$UI_PKG" | |
| fi | |
| check_deb_sizes | |
| - name: Upload .deb release assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ steps.release.outputs.tag }}" | |
| shopt -s nullglob | |
| debs=( *.deb ) | |
| if [ "${#debs[@]}" -eq 0 ]; then | |
| echo "No .deb packages were produced" >&2 | |
| exit 1 | |
| fi | |
| for deb in "${debs[@]}"; do | |
| gh release upload "$TAG" "$deb" \ | |
| --repo "${{ github.repository }}" --clobber | |
| done | |
| - name: Upload .deb artifacts (for apt-repo job) | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: deb-${{ matrix.arch }} | |
| path: "*.deb" | |
| # --------------------------------------------------------------------------- | |
| # Update APT repository (GitHub Pages) | |
| # --------------------------------------------------------------------------- | |
| apt-repo: | |
| needs: apt | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get release info | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: using latest release tag $TAG" | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | |
| - name: Download .deb packages | |
| uses: actions/download-artifact@v8 | |
| with: | |
| pattern: deb-* | |
| merge-multiple: true | |
| path: debs/ | |
| - name: Clone APT repo | |
| env: | |
| APT_TOKEN: ${{ secrets.APT_REPO_TOKEN }} | |
| run: git clone "https://x-access-token:${APT_TOKEN}@github.com/PerryTS/perry-apt.git" apt-repo | |
| - name: Import GPG key | |
| env: | |
| GPG_PRIVATE_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }} | |
| run: echo "$GPG_PRIVATE_KEY" | gpg --batch --import | |
| - name: Update repository | |
| env: | |
| GPG_KEY_ID: ${{ secrets.APT_GPG_KEY_ID }} | |
| run: | | |
| cd apt-repo | |
| max_git_blob_bytes=99000000 | |
| oversized=0 | |
| for deb in ../debs/*.deb; do | |
| size=$(stat -c %s "$deb") | |
| if [ "$size" -gt "$max_git_blob_bytes" ]; then | |
| echo "$deb is $size bytes; GitHub rejects git blobs near 100 MB" >&2 | |
| oversized=1 | |
| fi | |
| done | |
| if [ "$oversized" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| # Copy new .deb files into pool | |
| mkdir -p pool/main/p/perry | |
| cp ../debs/*.deb pool/main/p/perry/ | |
| # Generate Packages index for each architecture | |
| for ARCH in amd64 arm64; do | |
| mkdir -p dists/stable/main/binary-${ARCH} | |
| dpkg-scanpackages --arch ${ARCH} pool/ > dists/stable/main/binary-${ARCH}/Packages | |
| gzip -9c dists/stable/main/binary-${ARCH}/Packages > dists/stable/main/binary-${ARCH}/Packages.gz | |
| done | |
| # Generate Release file | |
| cd dists/stable | |
| cat > Release << EOF | |
| Origin: PerryTS | |
| Label: Perry | |
| Suite: stable | |
| Codename: stable | |
| Architectures: amd64 arm64 | |
| Components: main | |
| Description: Perry native TypeScript compiler | |
| EOF | |
| sed -i 's/^ //' Release | |
| # Add checksums | |
| { | |
| echo "MD5Sum:" | |
| find main/ -type f | while read f; do | |
| echo " $(md5sum "$f" | cut -d' ' -f1) $(wc -c < "$f") $f" | |
| done | |
| echo "SHA256:" | |
| find main/ -type f | while read f; do | |
| echo " $(sha256sum "$f" | cut -d' ' -f1) $(wc -c < "$f") $f" | |
| done | |
| } >> Release | |
| # Sign | |
| gpg --batch --yes --default-key "${GPG_KEY_ID}" -abs -o Release.gpg Release | |
| gpg --batch --yes --default-key "${GPG_KEY_ID}" --clearsign -o InRelease Release | |
| cd ../.. | |
| # Export public key for users | |
| gpg --armor --export "${GPG_KEY_ID}" > perry.gpg.pub | |
| git config user.name "perry-bot" | |
| git config user.email "bot@perryts.com" | |
| git add -A | |
| git commit -m "perry ${{ steps.release.outputs.version }}" | |
| git push | |
| # --------------------------------------------------------------------------- | |
| # Publish to winget (Windows Package Manager) | |
| # --------------------------------------------------------------------------- | |
| winget: | |
| needs: build | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: windows-latest | |
| steps: | |
| - name: Get release info | |
| id: release | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $tag = "${{ github.event.release.tag_name }}" | |
| if (-not $tag) { | |
| $tag = (gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| Write-Host "workflow_dispatch fallback: using latest release tag $tag" | |
| } | |
| $version = $tag -replace '^v', '' | |
| echo "version=$version" >> $env:GITHUB_OUTPUT | |
| echo "tag=$tag" >> $env:GITHUB_OUTPUT | |
| - name: Download wingetcreate | |
| shell: pwsh | |
| run: | | |
| Invoke-WebRequest "https://aka.ms/wingetcreate/latest" -OutFile wingetcreate.exe | |
| - name: Sync WINGET_PAT-user's winget-pkgs fork with upstream | |
| shell: pwsh | |
| env: | |
| GH_TOKEN: ${{ secrets.WINGET_PAT }} | |
| # wingetcreate --submit uses the AUTHENTICATED USER'S fork of | |
| # microsoft/winget-pkgs, not the org's (PerryTS/winget-pkgs was a | |
| # red herring left over from a misread of how wingetcreate works). | |
| # Query the token's current user, resolve `<user>/winget-pkgs`, | |
| # then merge-upstream. microsoft/winget-pkgs gets thousands of | |
| # commits per day — any fork untouched for a week is already behind | |
| # enough that wingetcreate's internal SyncFork call will throw the | |
| # opaque "forked repository could not be synced" error we saw on | |
| # the v0.5.158 first-real-release run. | |
| run: | | |
| $user = (gh api /user --jq '.login').Trim() | |
| Write-Host "Authenticated user: $user" | |
| Write-Host "Syncing $user/winget-pkgs from microsoft/winget-pkgs" | |
| gh repo sync "$user/winget-pkgs" --source microsoft/winget-pkgs --force | |
| - name: Update winget manifest | |
| shell: pwsh | |
| env: | |
| WINGET_PAT: ${{ secrets.WINGET_PAT }} | |
| run: | | |
| $url = "https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.tag }}/perry-windows-x86_64.zip" | |
| ./wingetcreate.exe update PerryTS.Perry ` | |
| --urls $url ` | |
| --version "${{ steps.release.outputs.version }}" ` | |
| --token $env:WINGET_PAT ` | |
| --submit | |
| # --------------------------------------------------------------------------- | |
| # Notify build workers to update perry | |
| # --------------------------------------------------------------------------- | |
| update-workers: | |
| needs: build | |
| if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Get release version | |
| id: release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| TAG="${{ github.event.release.tag_name }}" | |
| if [ -z "$TAG" ]; then | |
| TAG=$(gh release list --limit 1 --json tagName --jq '.[0].tagName') | |
| echo "workflow_dispatch fallback: using latest release tag $TAG" | |
| fi | |
| VERSION="${TAG#v}" | |
| echo "version=${VERSION}" >> "$GITHUB_OUTPUT" | |
| - name: Trigger worker perry update | |
| env: | |
| HUB_ADMIN_SECRET: ${{ secrets.HUB_ADMIN_SECRET }} | |
| run: | | |
| echo "Triggering perry update on build workers (expected: ${{ steps.release.outputs.version }})..." | |
| RESPONSE=$(curl -sf -X POST "https://hub.perryts.com/api/v1/admin/update-perry" \ | |
| -H "Authorization: Bearer ${HUB_ADMIN_SECRET}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"expected_version\": \"${{ steps.release.outputs.version }}\"}" 2>&1) || true | |
| echo "Hub response: ${RESPONSE}" | |
| # --------------------------------------------------------------------------- | |
| # Publish npm packages (@perryts/perry + 7 per-platform packages) | |
| # | |
| # Uses OIDC / Trusted Publishers (no long-lived NPM_TOKEN). Each of the 8 | |
| # package names must be registered on npmjs.com with this repo + workflow | |
| # as a Trusted Publisher. See npm/README.md for the one-time setup. | |
| # --------------------------------------------------------------------------- | |
| npm-publish: | |
| needs: build | |
| if: > | |
| github.event_name == 'release' || | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_npm == 'true') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write # REQUIRED for OIDC + npm --provenance | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Upgrade npm (OIDC Trusted Publisher requires >= 11.5.1) | |
| # Node 20 ships npm 10.x; versions below 11.5.1 silently fail the OIDC | |
| # handshake and the registry returns a misleading 404 ("'pkg@ver' is | |
| # not in this registry") on PUT. See npm/cli#9088. | |
| run: npm install -g npm@latest | |
| - name: Download all build artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: release-artifacts/ | |
| - name: Stage npm packages | |
| run: ./scripts/stage-npm.sh release-artifacts/ | |
| - name: Sanity-check staged packages | |
| run: | | |
| for dir in npm/perry npm/perry-*; do | |
| echo "=== $dir ===" | |
| ls -la "$dir" | |
| if [ -f "$dir/package.json" ]; then | |
| node -e "const p=require('./$dir/package.json'); console.log(p.name, p.version)" | |
| else | |
| echo "MISSING package.json in $dir" >&2 | |
| exit 1 | |
| fi | |
| done | |
| - name: Publish platform packages | |
| # `./` prefix is required — `npm publish npm/foo` parses `npm/foo` as | |
| # a GitHub shorthand (user/repo) and tries ssh://git@github.com/npm/foo.git. | |
| # See run 24632227482 npm-publish job for the symptom. | |
| # | |
| # Skip versions already on the registry so the job is idempotent: at | |
| # v0.5.1151 a transient artifact digest-mismatch killed the first run | |
| # after the two darwin packages had published, and every re-run then | |
| # died on the first package with "You cannot publish over the | |
| # previously published versions" — the remaining six packages could | |
| # never go out (run 27239815088). | |
| run: | | |
| set -e | |
| for pkg in ./npm/perry-*; do | |
| name=$(node -p "require('$pkg/package.json').name") | |
| ver=$(node -p "require('$pkg/package.json').version") | |
| if npm view "$name@$ver" version >/dev/null 2>&1; then | |
| echo "=== $name@$ver already published — skipping ===" | |
| continue | |
| fi | |
| echo "=== publishing $pkg ===" | |
| npm publish "$pkg" --access public --provenance | |
| done | |
| - name: Publish wrapper (@perryts/perry) last | |
| run: | | |
| set -e | |
| name=$(node -p "require('./npm/perry/package.json').name") | |
| ver=$(node -p "require('./npm/perry/package.json').version") | |
| if npm view "$name@$ver" version >/dev/null 2>&1; then | |
| echo "$name@$ver already published — skipping" | |
| else | |
| npm publish ./npm/perry --access public --provenance | |
| fi |