Skip to content

chore: release v0.0.85 #99

chore: release v0.0.85

chore: release v0.0.85 #99

Workflow file for this run

name: Releaser
on:
push:
tags:
- 'v*'
workflow_dispatch:
defaults:
run:
shell: bash
jobs:
pantry:
name: pantry (${{ matrix.platform.name }})
strategy:
fail-fast: false
matrix:
platform:
- { os: macos-26, name: darwin-arm64, deps: '' }
- { os: ubuntu-latest, name: linux-x86-64, deps: 'sudo apt-get update && sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev' }
runs-on: ${{ matrix.platform.os }}
permissions:
contents: write
id-token: write
env:
# Pinned, not inherited from the runner. `zig build` with no -Dtarget
# builds for the host, which sets the deployment target to the build
# machine's own OS version — that is how the published binary came to
# demand macOS 15.7.7 while PLATFORMS.md advertised 10.15. 15.0 is the
# floor the last release actually had, rounded down to the release
# itself, so nothing that runs craft today stops.
DEPLOYMENT_TARGET: '15.0'
ARM64_TARGET: aarch64-macos.15.0
X64_TARGET: x86_64-macos.15.0
# macOS 26 is where the window controls were restyled.
SDK_FLOOR: '26'
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Pantry (provides Bun, Zig, etc.)
uses: pantry-pm/pantry/packages/action@main
- name: Install System Dependencies
if: matrix.platform.deps != ''
run: ${{ matrix.platform.deps }}
- name: Install Dependencies
run: bun install
- name: Extract Version
id: version
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
- name: First-party Zig dependencies
uses: ./.github/actions/first-party-zig-deps
- name: Build Native Binary
run: bun run build:core
# ── Cross-compile all targets ────────────────────────────────
- name: First-party Zig dependencies
uses: ./.github/actions/first-party-zig-deps
- name: Cross-compile additional targets (macOS)
if: runner.os == 'macOS'
working-directory: packages/zig
run: |
VERSION="${{ steps.version.outputs.version }}"
SDK_PATH="$(xcrun --show-sdk-path)"
mkdir -p zig-out/cross
echo "::group::Cross-compile darwin-x64"
eval "$(pantry env | sed -n '/^export /,$p')" && zig build -Doptimize=ReleaseSafe -Dversion="$VERSION" -Dtarget="$X64_TARGET" -Dmacos-sdk="$SDK_PATH"
mkdir -p zig-out/cross/darwin-x64 && cp zig-out/bin/craft zig-out/cross/darwin-x64/craft
echo "::endgroup::"
# Restore native ARM64 binary. `zig-out/bin` is cleared first because
# the publish step zips that directory whole and unfiltered, so
# anything a cross build left in it ships to users under the host's
# archive name.
# `-Dmacos-sdk` is not optional once `-Dtarget` is given, even for
# the host's own architecture: naming a target puts Zig in
# cross-compilation mode, where it stops looking for the system SDK
# on its own and every `-framework Cocoa` fails to resolve.
rm -rf zig-out/bin
eval "$(pantry env | sed -n '/^export /,$p')" && zig build -Doptimize=ReleaseSafe -Dversion="$VERSION" -Dtarget="$ARM64_TARGET" -Dmacos-sdk="$SDK_PATH"
# AppKit decides how it draws a window's controls from the SDK recorded
# in LC_BUILD_VERSION, so that it can restyle them without restyling
# every app built before the change. macOS 26 restyled the traffic
# lights. A binary built against an older SDK therefore draws last
# year's, and no downstream app embedding this runtime can override it —
# this job is the only place it can be fixed.
#
# Assert it rather than trust the runner label: an image bump or a
# changed Xcode default would otherwise put the old buttons back with
# nothing in the log to say so.
- name: Verify macOS binaries opt into the current appearance
if: runner.os == 'macOS'
working-directory: packages/zig
run: |
check() {
local binary="$1"
local build_version
build_version="$(otool -l "$binary" | grep -A 4 LC_BUILD_VERSION)"
local sdk minos
sdk="$(echo "$build_version" | awk '/sdk/ { print $2 }')"
minos="$(echo "$build_version" | awk '/minos/ { print $2 }')"
echo "$binary: minos=$minos sdk=$sdk"
if [ "${sdk%%.*}" -lt "$SDK_FLOOR" ]; then
echo "::error::$binary is linked against the macOS $sdk SDK; $SDK_FLOOR or newer is required for the current window controls." >&2
exit 1
fi
# A deployment target that tracks the build machine is how the
# floor silently became 15.7.7 while the docs said 10.15.
if [ "$minos" != "$DEPLOYMENT_TARGET" ]; then
echo "::error::$binary declares minos=$minos, expected $DEPLOYMENT_TARGET." >&2
exit 1
fi
}
check zig-out/bin/craft
check zig-out/cross/darwin-x64/craft
- name: First-party Zig dependencies
uses: ./.github/actions/first-party-zig-deps
- name: Cross-compile additional targets (Linux)
if: runner.os == 'Linux'
working-directory: packages/zig
run: |
VERSION="${{ steps.version.outputs.version }}"
mkdir -p zig-out/cross
# Only cross-compile targets that do not require a target sysroot.
# Linux/FreeBSD GUI builds link GTK/WebKit system libraries, so
# non-host triples need dedicated sysroots before they are reliable.
for target_spec in \
"x86_64-windows:windows-x64:craft.exe"; do
IFS=: read -r zig_target dir_name bin_name <<< "$target_spec"
echo "::group::Cross-compile $dir_name"
eval "$(pantry env | sed -n '/^export /,$p')"
zig build -Doptimize=ReleaseSafe -Dversion="$VERSION" -Dtarget="$zig_target"
mkdir -p "zig-out/cross/$dir_name" && cp "zig-out/bin/$bin_name" "zig-out/cross/$dir_name/$bin_name"
echo "Built $dir_name"
echo "::endgroup::"
done
# Restore native linux-x64 binary. Clearing `zig-out/bin` first is
# load-bearing: the windows cross build above leaves craft.exe there,
# nothing removes it, and the publish step zips the whole directory —
# so craft-linux-x64.zip shipped a Windows PE binary alongside the
# Linux one.
rm -rf zig-out/bin
eval "$(pantry env | sed -n '/^export /,$p')" && zig build -Doptimize=ReleaseSafe -Dversion="$VERSION"
# ── macOS Code Signing & Notarization ──────────────────────────
# The signing credentials live encrypted in `.env.production`, not as
# seven repository secrets.
#
# One key instead of seven values: the ciphertext is committed and
# reviewable, rotating a certificate is a commit rather than seven trips
# through a settings page, and the same file works locally — `buddy
# env:get` reads a value on a laptop with no GitHub involved.
# What that trades away
# is that the ciphertext is public, so the whole of the secrecy is
# `DOTENV_PRIVATE_KEY_PRODUCTION`, which is the one thing that stays a
# repository secret.
#
# Decrypted into `$GITHUB_ENV` so every step below reads `$APPLE_ID` the
# way it always did. Each value is masked first: `$GITHUB_ENV` does *not*
# mask on its own, and an unmasked notarization password is one `set -x`
# away from the log.
- name: Load macOS release credentials
if: runner.os == 'macOS'
env:
DOTENV_PRIVATE_KEY_PRODUCTION: ${{ secrets.DOTENV_PRIVATE_KEY_PRODUCTION }}
run: |
set -euo pipefail
# Missing signing material stops the *signing*, not the release.
#
# This used to `exit 1`, which took the whole darwin leg down with it —
# including the publish step at the end of this job. The result was
# releases carrying linux and windows binaries and no macOS ones at
# all, from v0.0.37 to v0.0.78, and Pantry (which only surfaces a
# version whose release has the full prebuilt set) pinned at 0.0.37
# for a year. Nobody noticed, because the tag, the changelog and the
# GitHub release all still appeared.
#
# A macOS binary Pantry can install unsigned beats no macOS binary at
# all: it is fetched programmatically, so it carries no quarantine
# attribute and Gatekeeper does not gate it. When the secret is
# present this behaves exactly as before.
if [ -z "${DOTENV_PRIVATE_KEY_PRODUCTION:-}" ]; then
echo "::warning::DOTENV_PRIVATE_KEY_PRODUCTION is not set — publishing macOS binaries unsigned."
echo "::warning::Set it to restore signing and notarization: gh secret set DOTENV_PRIVATE_KEY_PRODUCTION"
echo "SIGNING_AVAILABLE=false" >> "$GITHUB_ENV"
exit 0
fi
missing=""
for name in APPLE_CERTIFICATE_BASE64 APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY KEYCHAIN_PASSWORD APPLE_ID APPLE_APP_PASSWORD APPLE_TEAM_ID; do
value="$(bunx --bun buddy env:get "$name" --file .env.production 2>/dev/null || true)"
if [ -z "$value" ]; then
missing="$missing $name"
continue
fi
echo "::add-mask::$value"
printf '%s=%s\n' "$name" "$value" >> "$GITHUB_ENV"
done
if [ -n "$missing" ]; then
echo "::warning::Empty in .env.production:$missing — publishing macOS binaries unsigned."
echo "SIGNING_AVAILABLE=false" >> "$GITHUB_ENV"
exit 0
fi
echo "SIGNING_AVAILABLE=true" >> "$GITHUB_ENV"
- name: Import signing certificate
if: runner.os == 'macOS' && env.SIGNING_AVAILABLE == 'true'
run: |
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
rm certificate.p12
- name: Sign macOS binary
if: runner.os == 'macOS' && env.SIGNING_AVAILABLE == 'true'
env:
# Default to the strict distribution entitlements; CI can opt in to
# the dev plist (sandbox off, library validation off) by setting
# CRAFT_ENTITLEMENTS=dev for local debugging — never for release.
ENTITLEMENTS_VARIANT: ${{ vars.CRAFT_ENTITLEMENTS || 'distribution' }}
run: |
set -euo pipefail
ENTITLEMENTS="scripts/entitlements-${ENTITLEMENTS_VARIANT}.plist"
if [ ! -f "$ENTITLEMENTS" ]; then
echo "::error file=$ENTITLEMENTS::missing — required for hardened-runtime signing"
exit 1
fi
echo "Using entitlements: $ENTITLEMENTS"
for binary in \
packages/zig/zig-out/bin/craft \
packages/zig/zig-out/cross/darwin-x64/craft; do
codesign --force --options runtime --sign "$APPLE_SIGNING_IDENTITY" \
--entitlements "$ENTITLEMENTS" "$binary"
codesign --verify --deep --strict "$binary"
done
echo "macOS ARM64 and x64 binaries signed successfully"
- name: Notarize macOS binaries
if: runner.os == 'macOS' && env.SIGNING_AVAILABLE == 'true'
run: |
set -euo pipefail
for tuple in \
"packages/zig/zig-out/bin/craft:arm64" \
"packages/zig/zig-out/cross/darwin-x64/craft:x64"; do
IFS=: read -r binary architecture <<< "$tuple"
archive="craft-${architecture}.zip"
ditto -c -k --keepParent "$binary" "$archive"
# A raw Mach-O binary has no stapler-supported container. Submit
# an archive containing the exact signed binary and require
# Apple's final Accepted result; Gatekeeper retrieves that ticket
# online for the binary distributed by the release.
xcrun notarytool submit "$archive" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait \
--timeout 30m
codesign --verify --deep --strict "$binary"
rm "$archive"
done
# ── Publish & Release ──────────────────────────────
# Pantry action handles everything:
# 1. Publishes to pantry registry (zig package)
# 2. Auto-packages binaries from zig-out/ into platform-named zips
# 3. Creates/updates GitHub release with all artifacts + changelog
- name: Publish & Release
uses: pantry-pm/pantry/packages/action@main
with:
install: 'false'
publish: 'zig'
package-dir: packages/zig
token: ${{ secrets.PANTRY_TOKEN }}
release: 'true'
release-changelog: CHANGELOG.md
release-token: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}
# Replaces the old `notify` job, which pointed at
# .github/actions/discord-notify — a path that has never existed in
# this repository. Its `hashFiles` guard skipped the only real step,
# so the job checked out the repo, did nothing and reported success:
# a failed release looked notified. The action does this natively.
discord-webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
notification-title: craft ${{ github.ref_name }}
env:
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}
# ── Gate: is this release actually installable? ─────────────────────
#
# v0.0.82 published with no macOS binaries at all. The darwin leg failed at
# notarization, so its "Publish & Release" step was skipped — but the linux
# leg had already created the GitHub release, and it created it complete-
# looking: tag, changelog, linux and windows zips, npm packages, the zig
# package on the registry. Everything except the two assets that make craft
# installable on a Mac.
#
# Nothing noticed. Pantry's recipe only surfaces a version whose release
# carries the whole prebuilt set, so v0.0.82 was quietly unlistable forever
# while looking, from GitHub, exactly like every other release.
#
# `fail-fast: false` on the matrix is deliberate and stays — one platform's
# signing trouble should not throw away another's build. This job is the other
# half of that bargain: the run goes red, naming the assets that are missing.
verify-release:
name: verify release assets
needs: [pantry]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Every platform pantry installs must have shipped
env:
GH_TOKEN: ${{ secrets.PAT_TOKEN || secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
# Kept in step with the `required` list in pantry's recipe:
# packages/ts-pantry/src/recipes/craft-native.org.ts
REQUIRED="craft-darwin-arm64.zip craft-darwin-x64.zip craft-linux-x64.zip"
echo "Assets on $TAG:"
ASSETS=$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets -q '.assets[].name')
echo "$ASSETS" | sed 's/^/ /'
MISSING=""
for asset in $REQUIRED; do
echo "$ASSETS" | grep -qx "$asset" || MISSING="$MISSING $asset"
done
if [ -n "$MISSING" ]; then
echo "::error::$TAG is missing:$MISSING — pantry will never list this version."
echo "::error::Re-run the failed platform leg, then re-run this job. Publishing a release without the full prebuilt set leaves a version that looks released and cannot be installed."
exit 1
fi
echo "All required assets present."
# Tell the registry this version exists, with the token this workflow
# already holds to publish to it.
#
# This used to POST a `repository_dispatch` to pantry-pm/pantry, which
# needed a GitHub PAT with write access to a repository this one does not
# own — a second credential, per publishing project, for the sole purpose
# of poking a workflow. The secret was never created, so the step warned
# and did nothing on every release, and the registry only ever learned
# about a new version when its cron next swept all several hundred
# packages. PANTRY_TOKEN authenticates to the registry itself, which is
# the thing that actually needs to know, so ask it directly.
#
# Best-effort: the sweep remains the backstop, so an unreachable registry
# warns rather than failing a release that is otherwise complete.
- name: Tell the pantry registry to index this version
if: ${{ success() }}
env:
PANTRY_TOKEN: ${{ secrets.PANTRY_TOKEN }}
run: |
if [ -z "${PANTRY_TOKEN:-}" ]; then
echo "::warning::No PANTRY_TOKEN — the registry will pick this up on its next scheduled sweep instead."
exit 0
fi
code=$(curl -sS -o /tmp/index.out -w '%{http_code}' --max-time 30 \
-X POST https://registry.pantry.dev/api/rebuild \
-H "Authorization: Bearer $PANTRY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"domain":"craft-native.org"}')
if [ "$code" = "200" ]; then
echo "Registry notified: craft-native.org queued for indexing."
else
# Not fatal: the release is good, and the sweep will still find it.
echo "::warning::Registry returned $code — it will index this on its next sweep."
cat /tmp/index.out || true
fi
npm:
name: npm
# Ordered after the binaries, not gated on them.
#
# `needs:` alone waits for every matrix leg to *succeed*, and the
# darwin-arm64 leg exits early when the Apple signing secrets are absent —
# so a missing certificate has been silently blocking every npm publish
# since v0.0.55. Fifteen releases where the tag, the changelog and the
# GitHub release all landed and not one package reached the registry.
#
# Nothing here consumes the binary job's artifacts: this checks out, runs
# `bun install`, and publishes packages built from TypeScript. Signing a Zig
# binary and publishing an SDK are independent, and the workflow now says
# so. `!cancelled()` rather than `always()` so a cancelled run still stops.
needs: [pantry]
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Pantry (provides Bun, Zig, etc.)
uses: pantry-pm/pantry/packages/action@main
- name: Install Dependencies
run: bun install
- name: Publish to npm
run: pantry publish --npm --access public
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}