chore: release v0.0.85 #99
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: 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 }} |