Skip to content

Release Provider Bundle (Swift) #105

Release Provider Bundle (Swift)

Release Provider Bundle (Swift) #105

Workflow file for this run

name: Release Provider Bundle (Swift)
# Swift provider release pipeline.
#
# Builds and ships:
# - Darkbloom-macOS-arm64.zip (people: final notarized + stapled app)
# - darkbloom-bundle-macos-arm64.tar.gz (coordinator/self-update verifier)
# - bin/darkbloom (provider CLI)
# - bin/darkbloom-enclave (Secure Enclave attestation/sign helper)
# - bin/mlx.metallib (compiled Metal kernels for the matching MLX)
# - Darkbloom.app (SwiftUI main plus the provisioned provider helper;
# managed locators use the nested CLI and the outer CLI path is an alias)
# - Darkbloom.app/Contents/Helpers/darkbloom-fan-helper (dormant opt-in root helper)
# - Darkbloom.app/Contents/Resources/*.bundle (SwiftPM resources, incl. the
# app's DarkbloomProvider_DarkbloomApp.bundle with its default.metallib)
#
# App identity: Darkbloom.app keeps CFBundleIdentifier io.darkbloom.provider
# (see scripts/bundle-macos-app.sh for the four contracts pinning it); the
# co-bundled darkbloom CLI is signed with an explicit matching
# --identifier so demoting it to nested code does not change its identity.
#
# install.sh creates a backward-compatibility symlink from the legacy
# `eigeninference-enclave` name to `darkbloom-enclave` for existing
# installations that referenced the old binary name.
#
# No DMG and no Python runtime. The legacy `release.yml` was deleted
# alongside `app/` once we committed to a Swift-only build.
#
# Swift cutover tag conventions:
# - vX.Y.Z -> prod Swift release (reviewer approval required)
# - vX.Y.Z-swift -> accepted alias while the migration is in progress
# - vX.Y.Z-swift.N -> accepted alias while the migration is in progress
# Dev releases use workflow_dispatch so the requested version can remain
# byte-identical to the checked-in provider and coordinator source constants.
#
# Repo secrets use DEV_/PROD_ prefixes so both environments can remain at
# repo level. The release job still binds the resolved GitHub environment so
# production tags must pass the environment's deployment protection rules:
# DEV_R2_ACCESS_KEY_ID / PROD_R2_ACCESS_KEY_ID
# DEV_R2_SECRET_ACCESS_KEY / PROD_R2_SECRET_ACCESS_KEY
# DEV_R2_ENDPOINT / PROD_R2_ENDPOINT
# DEV_R2_BUCKET / PROD_R2_BUCKET
# DEV_R2_PUBLIC_URL / PROD_R2_PUBLIC_URL
# DEV_COORDINATOR_URL / PROD_COORDINATOR_URL
# DEV_RELEASE_KEY / PROD_RELEASE_KEY
# Apple signing secrets are shared (same cert for both envs).
on:
push:
tags:
- 'v*.*.*'
- 'v*-swift'
- 'v*-swift.*'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [dev, prod]
default: dev
publish_release:
description: 'Publish release (false retains dev qualification artifacts only)'
required: true
type: boolean
default: true
version_override:
description: 'Optional version string when running manually (otherwise derived from tag)'
required: false
type: string
permissions:
contents: write # gh release create
env:
DEVELOPER_ID: 'Developer ID Application: Eigen Labs, Inc. (SLDQ2GJ6TL)'
APPLE_TEAM_ID: 'SLDQ2GJ6TL'
CLI_NAME: 'darkbloom'
APP_BINARY_NAME: 'DarkbloomApp'
ENCLAVE_NAME: 'darkbloom-enclave'
FAN_HELPER_NAME: 'darkbloom-fan-helper'
FAN_HELPER_ID: 'io.darkbloom.fan-helper'
MIN_MACOS: '14.0'
APP_ARCHIVE_NAME: 'Darkbloom-macOS-arm64.zip'
LEGACY_BUNDLE_NAME: 'darkbloom-bundle-macos-arm64.tar.gz'
# The mlx.metallib (compiled Metal GPU kernels) is BUILT FROM SOURCE from the
# MLX fork pinned at libs/mlx-swift/Source/Cmlx/mlx — NOT fetched from a PyPI
# wheel. Building from source guarantees the GPU kernels match the exact fork
# commit the host C++ links against (incl. the resource-count trim and the M5
# _nax kernels) and removes the dependency on a published wheel (there is no
# mlx==0.32.0 on PyPI). The _nax kernels are only compiled when SDK >= 26.2
# AND deployment target >= 26.2 AND Metal >= 4.0 (see
# mlx/backend/metal/kernels/CMakeLists.txt), so we pin the deployment target.
MLX_METALLIB_DEPLOYMENT_TARGET: '26.2'
jobs:
resolve-env:
runs-on: blacksmith-4vcpu-ubuntu-2404
outputs:
environment: ${{ steps.pick.outputs.environment }}
version: ${{ steps.pick.outputs.version }}
publish_release: ${{ steps.pick.outputs.publish_release }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- id: pick
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_ENVIRONMENT: ${{ inputs.environment }}
INPUT_PUBLISH_RELEASE: ${{ inputs.publish_release }}
INPUT_VERSION_OVERRIDE: ${{ inputs.version_override }}
REF_NAME: ${{ github.ref_name }}
REF_TYPE: ${{ github.ref_type }}
run: |
set -euo pipefail
# Push tags always publish to prod. An absent manual input retains
# the historical publishing default; an explicit false must survive.
PUBLISH_RELEASE=true
case "$EVENT_NAME" in
push) ENV=prod ;;
workflow_dispatch)
ENV="${INPUT_ENVIRONMENT:-prod}"
PUBLISH_RELEASE="${INPUT_PUBLISH_RELEASE:-true}"
;;
*) echo "::error::Unsupported release event"; exit 1 ;;
esac
case "$ENV" in
dev|prod) ;;
*) echo "::error::Unsupported release environment"; exit 1 ;;
esac
case "$PUBLISH_RELEASE" in
true|false) ;;
*) echo "::error::publish_release must be true or false"; exit 1 ;;
esac
if [ "$PUBLISH_RELEASE" = "false" ] && [ "$ENV" != "dev" ]; then
echo "::error::Qualification-only runs require environment=dev"
exit 1
fi
if [ "$REF_TYPE" = "tag" ] && [[ "$REF_NAME" == *-dev.* ]]; then
echo "::error::-dev tags are unsupported by the exact-version release contract; use workflow_dispatch with environment=dev"
exit 1
fi
if [ "$ENV" = "prod" ] && [ "$REF_TYPE" != "tag" ]; then
echo "::error::Production publication requires a source-matching release tag"
exit 1
fi
echo "environment=$ENV" >> "$GITHUB_OUTPUT"
echo "publish_release=$PUBLISH_RELEASE" >> "$GITHUB_OUTPUT"
if [ -n "$INPUT_VERSION_OVERRIDE" ]; then
VERSION="$INPUT_VERSION_OVERRIDE"
elif [ "$REF_TYPE" != "tag" ]; then
VERSION=$(awk -F'\"' '/public static let version =/ { print $2 }' \
provider-swift/Sources/ProviderCore/ProviderCore.swift)
else
REF="${REF_NAME#v}"
VERSION="${REF%-swift*}"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Resolved env=$ENV version=$VERSION publish_release=$PUBLISH_RELEASE"
- name: Fail before approval on version drift
run: ./scripts/check-release-version.sh "${{ steps.pick.outputs.version }}"
build-and-release:
name: Build, sign, notarize, qualify (publish optional)
needs: [resolve-env]
runs-on: blacksmith-12vcpu-macos-latest
environment: ${{ needs.resolve-env.outputs.environment }}
env:
VERSION: ${{ needs.resolve-env.outputs.version }}
ENV_PREFIX: ${{ needs.resolve-env.outputs.environment }}
PUBLISH_RELEASE: ${{ needs.resolve-env.outputs.publish_release }}
steps:
- name: Resolve env-specific secrets
if: needs.resolve-env.outputs.publish_release == 'true'
env:
ENV_PREFIX: ${{ needs.resolve-env.outputs.environment }}
DEV_R2_ACCESS_KEY_ID: ${{ secrets.DEV_R2_ACCESS_KEY_ID }}
PROD_R2_ACCESS_KEY_ID: ${{ secrets.PROD_R2_ACCESS_KEY_ID }}
DEV_R2_SECRET_ACCESS_KEY: ${{ secrets.DEV_R2_SECRET_ACCESS_KEY }}
PROD_R2_SECRET_ACCESS_KEY: ${{ secrets.PROD_R2_SECRET_ACCESS_KEY }}
DEV_R2_ENDPOINT: ${{ secrets.DEV_R2_ENDPOINT }}
PROD_R2_ENDPOINT: ${{ secrets.PROD_R2_ENDPOINT }}
DEV_R2_BUCKET: ${{ secrets.DEV_R2_BUCKET }}
PROD_R2_BUCKET: ${{ secrets.PROD_R2_BUCKET }}
DEV_R2_PUBLIC_URL: ${{ secrets.DEV_R2_PUBLIC_URL }}
PROD_R2_PUBLIC_URL: ${{ secrets.PROD_R2_PUBLIC_URL }}
DEV_COORDINATOR_URL: ${{ secrets.DEV_COORDINATOR_URL }}
PROD_COORDINATOR_URL: ${{ secrets.PROD_COORDINATOR_URL }}
DEV_RELEASE_KEY: ${{ secrets.DEV_RELEASE_KEY }}
PROD_RELEASE_KEY: ${{ secrets.PROD_RELEASE_KEY }}
# Legacy unprefixed prod secrets (pre-DEV_/PROD_ split). Fall back
# to these when PROD_* is empty so we don't have to duplicate every
# secret in the GitHub UI just to satisfy the new naming scheme.
LEGACY_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
LEGACY_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
LEGACY_R2_ENDPOINT: ${{ secrets.R2_ENDPOINT }}
LEGACY_R2_BUCKET: ${{ vars.R2_BUCKET }}
LEGACY_R2_PUBLIC_URL: ${{ secrets.R2_PUBLIC_URL }}
LEGACY_COORDINATOR_URL: ${{ secrets.COORDINATOR_URL }}
LEGACY_RELEASE_KEY: ${{ secrets.RELEASE_KEY }}
run: |
set -euo pipefail
PREFIX=$(echo "$ENV_PREFIX" | tr '[:lower:]' '[:upper:]')
for key in R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_ENDPOINT R2_BUCKET R2_PUBLIC_URL COORDINATOR_URL RELEASE_KEY; do
primary="${PREFIX}_${key}"
legacy="LEGACY_${key}"
outkey=$(echo "$key" | tr '[:upper:]' '[:lower:]')
value="${!primary:-}"
if [ -z "$value" ] && [ "$PREFIX" = "PROD" ]; then
value="${!legacy:-}"
if [ -n "$value" ]; then
echo "::notice::Using legacy unprefixed secret for ${key} (set PROD_${key} to silence this)"
fi
fi
if [ -z "$value" ]; then
echo "::error::No value resolved for ${PREFIX}_${key} (also tried LEGACY_${key})"
exit 1
fi
echo "${outkey}=${value}" >> "$GITHUB_ENV"
done
- name: Checkout (with submodules)
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
fetch-depth: 0 # gh release --generate-notes wants full history
- name: Validate release version integrity
run: ./scripts/check-release-version.sh "$VERSION"
- name: Set up Go for prompt parity
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.4.0
with:
go-version: '1.25.0'
- name: Show toolchain versions
run: |
set -euo pipefail
go version
xcodebuild -version
swift --version
xcrun --sdk macosx --show-sdk-version
uname -a
- name: Import Developer ID certificate
env:
P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12 }}
P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
echo "$P12_BASE64" | base64 --decode > /tmp/cert.p12
security create-keychain -p "build" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "build" "$KEYCHAIN_PATH"
CURRENT_KEYCHAINS=()
while IFS= read -r keychain; do
keychain=${keychain//\"/}
[ -z "$keychain" ] || CURRENT_KEYCHAINS+=("$keychain")
done < <(security list-keychains -d user)
security list-keychains -d user -s \
"$KEYCHAIN_PATH" "${CURRENT_KEYCHAINS[@]}"
security import /tmp/cert.p12 -k "$KEYCHAIN_PATH" \
-P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign -f pkcs12
security set-key-partition-list -S apple-tool:,apple: \
-s -k "build" "$KEYCHAIN_PATH"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
rm -f /tmp/cert.p12
- name: Install awscli (R2)
if: needs.resolve-env.outputs.publish_release == 'true'
run: |
if ! command -v aws >/dev/null 2>&1; then
brew install awscli >/dev/null 2>&1 || true
fi
aws --version
- name: Restore SwiftPM cache
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: |
~/Library/Caches/org.swift.swiftpm
~/Library/org.swift.swiftpm
provider-swift/.build
libs/mlx-swift-lm/.build
key: spm-v3-${{ runner.os }}-${{ github.sha }}-${{ hashFiles('provider-swift/Package.resolved', 'libs/mlx-swift/Package.swift', 'libs/mlx-swift-lm/Package.swift') }}
restore-keys: |
spm-v3-${{ runner.os }}-
- name: Discard metallibs restored by the generic SwiftPM cache
run: |
for root in provider-swift/.build libs/mlx-swift-lm/.build; do
[ ! -d "$root" ] || find "$root" -type f -name mlx.metallib -delete
done
- name: Verify production prompt parity
run: |
rustup toolchain install 1.88.0 --profile minimal
rustup override set 1.88.0
./scripts/verify-prompt-parity.sh
# The root helper is the sole metallib builder. The Actions cache stores
# its private cache directory; the helper still runs on every release and
# authorizes the exact source/toolchain/deployment/JIT contract itself.
- name: Resolve source-matched metallib cache namespace
id: mlxkey
run: |
set -euo pipefail
MLX_SRC="libs/mlx-swift/Source/Cmlx/mlx"
test -f "$MLX_SRC/mlx/version.h" || { echo "::error::mlx submodule not checked out at $MLX_SRC"; exit 1; }
SOURCE_STATUS=$(git -C "$MLX_SRC" status --porcelain=v1 --untracked-files=all)
if [ -n "$SOURCE_STATUS" ]; then
echo "::error::release metallib source checkout is dirty"
printf '%s\n' "$SOURCE_STATUS"
exit 1
fi
MLX_SHA=$(git -C "$MLX_SRC" rev-parse HEAD)
HELPER_SHA=$(shasum -a 256 scripts/fetch-metallib.sh | cut -d' ' -f1)
SDK=$(xcrun --sdk macosx --show-sdk-version)
XCODE=$(xcodebuild -version | tr -cs '[:alnum:].' '-')
KEY="metallib-helper-v1-${MLX_SHA}-${HELPER_SHA}-${XCODE}-sdk${SDK}-dt${MLX_METALLIB_DEPLOYMENT_TARGET}-jitoff"
echo "key=$KEY" >> "$GITHUB_OUTPUT"
echo "mlx_sha_short=${MLX_SHA:0:12}" >> "$GITHUB_OUTPUT"
echo "Metallib cache namespace: $KEY"
- name: Restore source-matched metallib cache
id: metallib-cache
uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ${{ runner.temp }}/metallib-cache
key: ${{ steps.mlxkey.outputs.key }}
- name: Build source-matched mlx.metallib through root helper
id: metallib
env:
METALLIB_CACHE_DIR: ${{ runner.temp }}/metallib-cache
run: |
set -euo pipefail
command -v cmake >/dev/null 2>&1 || brew install cmake
MLX_SRC="libs/mlx-swift/Source/Cmlx/mlx"
test -z "$(git -C "$MLX_SRC" status --porcelain=v1 --untracked-files=all)"
./scripts/fetch-metallib.sh "$RUNNER_TEMP/metallib"
test -z "$(git -C "$MLX_SRC" status --porcelain=v1 --untracked-files=all)" || {
echo "::error::metallib helper modified the release source checkout"
exit 1
}
MLIB="$RUNNER_TEMP/metallib/mlx.metallib"
test -s "$MLIB" || { echo "::error::metallib missing at $MLIB"; exit 1; }
echo "metallib=$MLIB" >> "$GITHUB_OUTPUT"
- name: Save source-matched metallib cache
if: steps.metallib-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
with:
path: ${{ runner.temp }}/metallib-cache
key: ${{ steps.mlxkey.outputs.key }}
# ----------------------------------------------------------------------
# Build (tests run by CI, not the release workflow).
# ----------------------------------------------------------------------
- name: Build provider-swift (release)
working-directory: provider-swift
run: |
set -euo pipefail
swift build -c release --product darkbloom
swift build -c release --product darkbloom-enclave
swift build -c release --product darkbloom-fan-helper
# DarkbloomApp (SwiftUI) ships co-bundled with the CLI inside
# Darkbloom.app, so it is a first-class build product of the same
# release (same tree, same version).
swift build -c release --product DarkbloomApp
BIN_DIR=$(swift build -c release --show-bin-path)
test -x "$BIN_DIR/$CLI_NAME"
test -x "$BIN_DIR/$ENCLAVE_NAME"
test -x "$BIN_DIR/$FAN_HELPER_NAME"
test -x "$BIN_DIR/$APP_BINARY_NAME"
test -d "$BIN_DIR/DarkbloomProvider_DarkbloomApp.bundle"
echo "BIN_DIR=$BIN_DIR" >> "$GITHUB_ENV"
/usr/bin/find "$BIN_DIR" -mindepth 1 -maxdepth 1 -print \
| LC_ALL=C sort
REPORTED=$("$BIN_DIR/$CLI_NAME" --version)
../scripts/check-release-version.sh "$VERSION" "$REPORTED"
- name: Save SwiftPM cache
uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0
if: always()
with:
path: |
~/Library/Caches/org.swift.swiftpm
~/Library/org.swift.swiftpm
provider-swift/.build
libs/mlx-swift-lm/.build
key: spm-v3-${{ runner.os }}-${{ github.sha }}-${{ hashFiles('provider-swift/Package.resolved', 'libs/mlx-swift/Package.swift', 'libs/mlx-swift-lm/Package.swift') }}
# Unit tests are run by CI (ci.yml + integration.yml) on push to
# master. Skipped here to avoid flaky live MLX inference tests
# blocking signed releases on CI hardware timing differences.
# ----------------------------------------------------------------------
# Stage the app, every SwiftPM runtime bundle, and flat verifier files.
# ----------------------------------------------------------------------
- name: Embed provisioning profile
env:
PROVISIONING_PROFILE_BASE64: ${{ secrets.PROVISIONING_PROFILE_BASE64 }}
run: |
set -euo pipefail
if [ -z "${PROVISIONING_PROFILE_BASE64:-}" ]; then
echo "::error::PROVISIONING_PROFILE_BASE64 is required (PR #146 persistent SE key)"
exit 1
fi
echo "$PROVISIONING_PROFILE_BASE64" | base64 --decode > /tmp/embedded.provisionprofile
# Decode the CMS-wrapped profile to its plist payload so we can
# introspect entitlements + expiry. Without this, a profile that
# doesn't grant SLDQ2GJ6TL.io.darkbloom.provider (or is expired)
# passes codesign but fails at runtime → silent attestation break.
security cms -D -i /tmp/embedded.provisionprofile > /tmp/profile.plist
python3 - <<'PY'
import plistlib, sys
from datetime import datetime, timezone
with open('/tmp/profile.plist', 'rb') as f:
p = plistlib.load(f)
errors = []
team = p.get('TeamIdentifier', [])
if 'SLDQ2GJ6TL' not in team:
errors.append(f"TeamIdentifier missing SLDQ2GJ6TL: {team}")
ents = p.get('Entitlements', {})
groups = ents.get('keychain-access-groups', [])
# Accept exact match or wildcard (SLDQ2GJ6TL.*) which covers
# SLDQ2GJ6TL.io.darkbloom.provider at runtime.
has_group = any(
g == 'SLDQ2GJ6TL.io.darkbloom.provider' or g == 'SLDQ2GJ6TL.*'
for g in groups
)
if not has_group:
errors.append(f"keychain-access-groups missing SLDQ2GJ6TL.io.darkbloom.provider or wildcard: {groups}")
app_id = ents.get('application-identifier', '')
# Developer ID profiles may omit application-identifier entirely;
# accept empty or wildcard alongside exact match.
if app_id and not (app_id.endswith('io.darkbloom.provider') or app_id.endswith('*')):
errors.append(f"application-identifier mismatch: {app_id}")
# v0.6.0 APNs code-identity: the embedded profile MUST grant
# aps-environment=production, or the signed binary AMFI-kills at launch
# (restricted entitlement without an authorizing profile). This is the
# only build-time guard against shipping the entitlement with a stale,
# non-push profile. Profiles key it short ('aps-environment') or long.
aps = ents.get('aps-environment') or ents.get('com.apple.developer.aps-environment')
if aps != 'production':
errors.append(
f"profile does not grant aps-environment=production (got {aps!r}); "
"regenerate the push-enabled provisioning profile and update PROVISIONING_PROFILE_BASE64"
)
expiry = p.get('ExpirationDate')
if expiry is not None:
# ExpirationDate is a naive UTC datetime from plistlib.
now = datetime.utcnow()
days_left = (expiry - now).days
if days_left < 30:
errors.append(f"Profile expires in {days_left} days (renew before cutover): {expiry}")
else:
print(f"Profile valid for {days_left} more days")
else:
print("::warning::Provisioning profile has no ExpirationDate field")
name = p.get('Name', '<unknown>')
uuid = p.get('UUID', '<unknown>')
print(f"Profile: name={name!r} uuid={uuid} team={team} app_id={app_id}")
if errors:
for e in errors:
print(f"::error::{e}", file=sys.stderr)
sys.exit(1)
print("Provisioning profile verification passed")
PY
echo "profile_available=true" >> "$GITHUB_OUTPUT"
id: profile
- name: Stage and sign bundle
id: bundle
run: |
set -euo pipefail
STAGE=/tmp/darkbloom-bundle
rm -rf "$STAGE"
# Assemble the combined .app: SwiftUI DarkbloomApp main executable,
# co-bundled provider CLI, fonts, compiled SpatialField metallib,
# fan helper + marker, and the release-stamped Info.plist. Keeping
# the .app shape (and embedding the provisioning profile below) is
# what authorizes keychain-access-groups for the persistent SE key.
APP="$STAGE/Darkbloom.app"
PROVIDER_APP="$APP/Contents/Helpers/DarkbloomProvider.app"
scripts/bundle-macos-app.sh \
"$BIN_DIR" \
"${{ steps.metallib.outputs.metallib }}" \
"$APP" \
"$VERSION"
if [ "${{ steps.profile.outputs.profile_available }}" = "true" ]; then
install -m 0644 \
/tmp/embedded.provisionprofile \
"$APP/Contents/embedded.provisionprofile"
install -m 0644 /tmp/embedded.provisionprofile \
"$PROVIDER_APP/Contents/embedded.provisionprofile"
echo "Provisioning profile embedded in GUI and provider helper bundles"
fi
# Copy every product resource bundle into the canonical signed-app
# resource directory before signing/notarizing. PagedAttention uses
# a catchable locator instead of SwiftPM's fatal Bundle.module
# accessor and searches this sealed location. This includes
# mlx-swift-lm_MLXLMCommon.bundle/pagedattention.metal and future
# dependency resource bundles.
RESOURCE_MANIFEST=/tmp/darkbloom-swiftpm-resource-bundles.txt
scripts/stage-swiftpm-resource-bundles.sh \
"$BIN_DIR" "$APP" "$RESOURCE_MANIFEST"
# Package-real runtime gate. Before its first GPU access the staged
# child decodes the retained Gemma config, proves authoritative
# overwrite of all three low-level controls, snapshots the early R1
# latch/AOT capability without arming counters, then runs the
# existing GPT-OSS paged-kernel shapes. The v0.7.6 artifact still
# fails here because its resource bundle is absent.
PRE_SIGN_SMOKE_OUTPUT=$(DARKBLOOM_NO_UPDATE_CHECK=1 \
DARKBLOOM_GEMMA4_PREFILL_CHUNK_EVAL=18 \
MLX_GEMMA4_FUSED_WEIGHTED_UNSORT=1 \
MLX_GATHER_QMM_EXPERT_SLICES=1 \
"$PROVIDER_APP/Contents/MacOS/$CLI_NAME" runtime-smoke)
printf '%s\n' "$PRE_SIGN_SMOKE_OUTPUT"
printf '%s\n' "$PRE_SIGN_SMOKE_OUTPUT" \
| grep -Fqx 'gemma-optimizations-runtime-smoke: ok'
# Hardened-runtime sign each binary and resource, then the bundle.
# mlx.metallib must be signed before the bundle or codesign rejects
# it as an unsigned subcomponent.
# The fan helper runs as root only after a separate, explicit sudo
# command. Give it its own identity and no provider APNs/keychain
# entitlements; ordinary installation leaves it sealed and dormant.
codesign --force --options runtime --timestamp \
--identifier "$FAN_HELPER_ID" \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$APP/Contents/Helpers/$FAN_HELPER_NAME"
FAN_HELPER_REQUIREMENT="anchor apple generic and identifier \"$FAN_HELPER_ID\" and certificate leaf[subject.OU] = \"$APPLE_TEAM_ID\""
codesign --verify --strict --verbose=2 \
"-R=$FAN_HELPER_REQUIREMENT" \
"$APP/Contents/Helpers/$FAN_HELPER_NAME"
# AMFI authorizes the CLI profile only when the CLI is a bundle's
# main executable. Sign its real helper app after its colocated code.
codesign --force --options runtime --timestamp \
--keychain "$KEYCHAIN_PATH" --sign "$DEVELOPER_ID" \
"$PROVIDER_APP/Contents/MacOS/mlx.metallib"
codesign --force --options runtime --timestamp \
--entitlements provider-swift/entitlements-enclave.plist \
--keychain "$KEYCHAIN_PATH" --sign "$DEVELOPER_ID" \
"$PROVIDER_APP/Contents/MacOS/$ENCLAVE_NAME"
codesign --force --options runtime --timestamp \
--identifier "io.darkbloom.provider" \
--entitlements provider-swift/entitlements.plist \
--keychain "$KEYCHAIN_PATH" --sign "$DEVELOPER_ID" "$PROVIDER_APP"
codesign --verify --deep --strict --verbose=2 "$PROVIDER_APP"
# Copy already-signed helper code so both runtime and legacy views
# carry identical bytes and detached metallib signature attributes.
ditto "$PROVIDER_APP/Contents/MacOS/mlx.metallib" "$APP/Contents/MacOS/mlx.metallib"
ditto "$PROVIDER_APP/Contents/MacOS/$ENCLAVE_NAME" "$APP/Contents/MacOS/$ENCLAVE_NAME"
test "$(readlink "$APP/Contents/MacOS/$CLI_NAME")" = \
'../Helpers/DarkbloomProvider.app/Contents/MacOS/darkbloom'
cmp "$APP/Contents/MacOS/mlx.metallib" "$PROVIDER_APP/Contents/MacOS/mlx.metallib"
cmp "$APP/Contents/MacOS/$ENCLAVE_NAME" "$PROVIDER_APP/Contents/MacOS/$ENCLAVE_NAME"
# GUI main executable: its own entitlements (network only — no
# keychain-access-groups / application-identifier / aps-environment)
# so profile authorization stays a CLI-only property. Its signing
# identifier is derived from Info.plist => io.darkbloom.provider.
codesign --force --options runtime --timestamp \
--entitlements scripts/entitlements.plist \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$APP/Contents/MacOS/$APP_BINARY_NAME"
codesign --force --options runtime --timestamp \
--entitlements scripts/entitlements.plist \
--keychain "$KEYCHAIN_PATH" \
--sign "$DEVELOPER_ID" "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
codesign --verify --verbose=2 "$PROVIDER_APP/Contents/MacOS/$CLI_NAME"
codesign --verify --verbose=2 "$APP/Contents/MacOS/$APP_BINARY_NAME"
# Packaged post-sign smoke: the release-candidate evaluator must be
# exposed by the exact signed main executable. This checks command
# wiring/help only; CI never downloads or runs model weights.
SIGNED_BENCHMARK_HELP=$(DARKBLOOM_NO_UPDATE_CHECK=1 \
"$PROVIDER_APP/Contents/MacOS/$CLI_NAME" benchmark --help)
printf '%s\n' "$SIGNED_BENCHMARK_HELP" \
| grep -F -- '--scheduler-prefill-decision'
# Persistent Secure Enclave key requires keychain-access-groups
# bound to the team-scoped access group. If the entitlement is
# missing, providers fall back to ephemeral SE keys and PR #146
# silently no-ops. Fail the build instead of shipping a broken
# attestation.
EXPECTED_ACCESS_GROUP="SLDQ2GJ6TL.io.darkbloom.provider"
ENTITLEMENTS=$(codesign -d --entitlements - --xml "$PROVIDER_APP/Contents/MacOS/$CLI_NAME" 2>&1 || true)
if ! echo "$ENTITLEMENTS" | grep -q "keychain-access-groups"; then
echo "::error::Signed CLI is missing keychain-access-groups entitlement"
echo "$ENTITLEMENTS"
exit 1
fi
if ! echo "$ENTITLEMENTS" | grep -q "$EXPECTED_ACCESS_GROUP"; then
echo "::error::Signed CLI is missing access group $EXPECTED_ACCESS_GROUP"
echo "$ENTITLEMENTS"
exit 1
fi
# v0.6.0 APNs code-identity assertions on the signed CLI:
# (a) aps-environment present AND == production (parse the value, not a
# bare grep — a dev profile would set 'development' and silently
# register against the wrong APNs host).
# (b) get-task-allow ABSENT — its absence (not "hardened runtime" per
# se) is what blocks even-root debugger attach / dylib injection;
# Developer ID + notarization strips it.
rm -f /tmp/cli-ents.plist
codesign -d --entitlements /tmp/cli-ents.plist --xml "$PROVIDER_APP/Contents/MacOS/$CLI_NAME" 2>/dev/null || true
# Fail loudly if extraction produced nothing — otherwise the
# get-task-allow check below would pass vacuously on an empty file.
if [ ! -s /tmp/cli-ents.plist ]; then
echo "::error::Failed to extract entitlements from signed CLI for APNs/get-task-allow verification"
exit 1
fi
APS_ENV=$(/usr/libexec/PlistBuddy -c 'Print :com.apple.developer.aps-environment' /tmp/cli-ents.plist 2>/dev/null || echo "")
if [ "$APS_ENV" != "production" ]; then
echo "::error::Signed CLI aps-environment must be 'production' (got '${APS_ENV:-<absent>}'). Regenerate the push provisioning profile + entitlements (v0.6.0 APNs code-identity)."
exit 1
fi
if /usr/libexec/PlistBuddy -c 'Print :com.apple.security.get-task-allow' /tmp/cli-ents.plist 2>/dev/null | grep -qi true; then
echo "::error::Signed CLI has get-task-allow=true — defeats even-root injection resistance. It must be absent (Developer ID + notarized strips it)."
exit 1
fi
echo "APNs code-identity entitlements verified: aps-environment=production, get-task-allow absent"
if [ "${{ steps.profile.outputs.profile_available }}" = "true" ]; then
if [ ! -f "$APP/Contents/embedded.provisionprofile" ]; then
echo "::error::Provisioning profile secret set but not embedded in app bundle"
exit 1
fi
else
echo "::error::PROVISIONING_PROFILE_BASE64 secret is required to authorize the keychain-access-groups entitlement on provider machines"
exit 1
fi
echo "Entitlement verification passed: $EXPECTED_ACCESS_GROUP authorized via embedded provisioning profile"
# App identity guards. The bundle's designated requirement must keep
# resolving to the pinned identifier: the provider self-updater
# (DarkbloomCodeSignature.swift) and install.sh
# DARKBLOOM_DESIGNATED_REQUIREMENT both refuse anything else.
APP_DR=$(codesign -d -r- "$APP" 2>&1 | awk -F' => ' '/designated/{print $2; exit}')
echo "$APP_DR" | grep -qF 'identifier "io.darkbloom.provider"' || {
echo "::error::Bundle designated requirement lost io.darkbloom.provider: $APP_DR"
exit 1
}
echo "$APP_DR" | grep -qF 'anchor apple generic' || {
echo "::error::Bundle designated requirement lost the Apple anchor: $APP_DR"
exit 1
}
CLI_SIGNING_ID=$(codesign -dvvv "$PROVIDER_APP/Contents/MacOS/$CLI_NAME" 2>&1 | awk -F= '/^Identifier=/{print $2; exit}')
if [ "$CLI_SIGNING_ID" != "io.darkbloom.provider" ]; then
echo "::error::Co-bundled CLI identifier drifted to '$CLI_SIGNING_ID' — fan-helper XPC, APNs topic, and keychain group authorization depend on io.darkbloom.provider"
exit 1
fi
# The GUI main executable must NOT carry the CLI's restricted
# entitlements — profile authorization stays a CLI-only property.
rm -f /tmp/app-ents.plist
codesign -d --entitlements /tmp/app-ents.plist --xml "$APP/Contents/MacOS/$APP_BINARY_NAME" 2>/dev/null || true
# Fail loudly on empty extraction so the loop below cannot pass vacuously.
if [ ! -s /tmp/app-ents.plist ]; then
echo "::error::Failed to extract entitlements from signed DarkbloomApp"
exit 1
fi
for FORBIDDEN in \
keychain-access-groups \
com.apple.developer.aps-environment \
com.apple.application-identifier \
com.apple.security.get-task-allow
do
if /usr/libexec/PlistBuddy -c "Print :$FORBIDDEN" /tmp/app-ents.plist >/dev/null 2>&1; then
echo "::error::Signed DarkbloomApp must not carry '$FORBIDDEN' — restricted entitlements are CLI-only (see scripts/entitlements.plist)"
exit 1
fi
done
# App payload completeness (assembly contract of scripts/bundle-macos-app.sh).
test -x "$APP/Contents/MacOS/$APP_BINARY_NAME"
test -s "$APP/Contents/Resources/Chivo-Regular.ttf"
test -s "$APP/Contents/Resources/Chivo-Medium.ttf"
test -s "$APP/Contents/Resources/DarkbloomProvider_DarkbloomApp.bundle/default.metallib"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$APP/Contents/Info.plist")" = "io.darkbloom.provider"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$APP/Contents/Info.plist")" = "$APP_BINARY_NAME"
echo "App identity verified: DR pins io.darkbloom.provider, CLI id pinned, GUI restricted-entitlement-free"
# Flat verifier layout alongside the .app bundle. Registration
# requires regular executable darkbloom + enclave files and a
# non-executable metallib here, then byte-matches all three against
# the app copies before making the release visible.
mkdir -p "$STAGE/bin"
cp "$PROVIDER_APP/Contents/MacOS/$CLI_NAME" "$STAGE/bin/$CLI_NAME"
cp "$APP/Contents/MacOS/$ENCLAVE_NAME" "$STAGE/bin/$ENCLAVE_NAME"
cp "$APP/Contents/MacOS/mlx.metallib" "$STAGE/bin/mlx.metallib"
# This zip is input to Apple's notary service only. It is created
# before stapling and MUST NEVER be uploaded as a release asset. The
# downloadable Darkbloom-macOS-arm64.zip is rebuilt from the stapled
# app in the next step.
# Running the local launch probes can attach host-specific access
# metadata to this staged app. It is not distribution metadata.
if xattr -p com.apple.macl "$APP" >/dev/null 2>&1; then
xattr -d com.apple.macl "$APP"
fi
rm -f /tmp/darkbloom-notarization-submission.zip
ditto -c -k --keepParent \
"$APP" /tmp/darkbloom-notarization-submission.zip
echo "stage=$STAGE" >> "$GITHUB_OUTPUT"
- name: Notarize bundle
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
set -euo pipefail
xcrun notarytool submit /tmp/darkbloom-notarization-submission.zip \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait --timeout 15m \
--output-format json | tee /tmp/notary-result.json
read -r NOTARY_ID NOTARY_STATUS < <(python3 - <<'PY'
import json
with open('/tmp/notary-result.json', encoding='utf-8') as result_file:
result = json.load(result_file)
submission_id = result.get('id')
status = result.get('status')
if not submission_id or not status:
raise SystemExit('notarytool result is missing id or status')
print(submission_id, status)
PY
)
if [ "$NOTARY_STATUS" != "Accepted" ]; then
echo "::error::Apple notarization $NOTARY_ID finished with status $NOTARY_STATUS (expected Accepted)"
if ! xcrun notarytool log "$NOTARY_ID" \
--apple-id "$APPLE_ID" \
--password "$APPLE_APP_PASSWORD" \
--team-id "$APPLE_TEAM_ID"; then
echo "::warning::Could not retrieve Apple's rejection log for $NOTARY_ID"
fi
exit 1
fi
# Stapler supports executable bundles, not individual Mach-O files.
# Require an attached ticket and a local Gatekeeper acceptance before
# the distribution artifact can be rebuilt or uploaded.
STAGE="${{ steps.bundle.outputs.stage }}"
APP="$STAGE/Darkbloom.app"
PROVIDER_APP="$APP/Contents/Helpers/DarkbloomProvider.app"
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
spctl --assess --type execute --verbose=4 "$APP"
codesign --verify --deep --strict --verbose=2 "$APP"
# Stapling the bundle must not change the executable bytes registered
# with the coordinator and verified by provider self-update.
cmp "$STAGE/bin/$CLI_NAME" "$PROVIDER_APP/Contents/MacOS/$CLI_NAME"
cmp "$STAGE/bin/$ENCLAVE_NAME" "$APP/Contents/MacOS/$ENCLAVE_NAME"
cmp "$STAGE/bin/mlx.metallib" "$APP/Contents/MacOS/mlx.metallib"
# Build BOTH distributable views only after stapling. The app zip is
# the public, app-first download. The legacy tar remains the sole
# coordinator/self-update artifact because its regular bin/ files are
# part of the registered hash contract.
APP_ARCHIVE="/tmp/$APP_ARCHIVE_NAME"
LEGACY_BUNDLE="/tmp/$LEGACY_BUNDLE_NAME"
if xattr -p com.apple.macl "$APP" >/dev/null 2>&1; then
xattr -d com.apple.macl "$APP"
fi
rm -f "$APP_ARCHIVE" "$LEGACY_BUNDLE"
ditto -c -k --sequesterRsrc --keepParent "$APP" "$APP_ARCHIVE"
# mlx.metallib carries its nested code signature in com.apple.cs.*
# xattrs. PAX preserves them; the archive preflight rejects all
# unrelated xattrs before any extractor sees the bundle.
COPYFILE_DISABLE=1 /usr/bin/tar \
--no-acls \
--no-fflags \
--no-mac-metadata \
-czf "$LEGACY_BUNDLE" \
-C "${{ steps.bundle.outputs.stage }}" .
# Apply the same bounded raw-header walk used by clean-machine shell
# installs before this exact archive can be extracted or uploaded.
bash scripts/install.sh --preflight-release-archive "$LEGACY_BUNDLE"
tar tzf "$LEGACY_BUNDLE" | sort | tee /tmp/darkbloom-bundle-files.txt
grep -qx './bin/darkbloom' /tmp/darkbloom-bundle-files.txt
grep -qx './bin/darkbloom-enclave' /tmp/darkbloom-bundle-files.txt
grep -qx './bin/mlx.metallib' /tmp/darkbloom-bundle-files.txt
if grep -qx './bin/darkbloom-fan-helper' /tmp/darkbloom-bundle-files.txt; then
echo "::error::fan helper must not be present in the flat verifier layout"
exit 1
fi
grep -qx \
'./Darkbloom.app/Contents/Helpers/darkbloom-fan-helper' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/darkbloom-runtime-capabilities/fan-helper-v1' \
/tmp/darkbloom-bundle-files.txt
while IFS= read -r bundle_name; do
grep -qx \
"./Darkbloom.app/Contents/Resources/${bundle_name}/" \
/tmp/darkbloom-bundle-files.txt
done < /tmp/darkbloom-swiftpm-resource-bundles.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/mlx-swift-lm_MLXLMCommon.bundle/pagedattention.metal' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/darkbloom-runtime-capabilities/paged-kernel-v1' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/MacOS/DarkbloomApp' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/Chivo-Regular.ttf' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/Chivo-Medium.ttf' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/Resources/DarkbloomProvider_DarkbloomApp.bundle/default.metallib' \
/tmp/darkbloom-bundle-files.txt
grep -qx \
'./Darkbloom.app/Contents/embedded.provisionprofile' \
/tmp/darkbloom-bundle-files.txt
# Verify the FINAL archived layout, not merely the staging tree.
# This runs after signing, notarization, stapling, and tar rebuild,
# and before any upload or coordinator registration.
SMOKE_ROOT=/tmp/darkbloom-final-artifact-smoke
rm -rf "$SMOKE_ROOT"
mkdir -p "$SMOKE_ROOT"
# Match installer/updater extraction exactly. Preflight permits only
# metallib com.apple.cs.* xattrs, which the deep signature requires.
/usr/bin/tar \
-xzp \
-m \
--no-acls \
--no-fflags \
--no-mac-metadata \
--no-same-owner \
-f "$LEGACY_BUNDLE" \
-C "$SMOKE_ROOT"
FINAL_APP_BIN="$SMOKE_ROOT/Darkbloom.app/Contents/MacOS"
FINAL_PROVIDER_APP="$SMOKE_ROOT/Darkbloom.app/Contents/Helpers/DarkbloomProvider.app"
FINAL_PROVIDER_BIN="$FINAL_PROVIDER_APP/Contents/MacOS"
test "$(readlink "$FINAL_APP_BIN/$CLI_NAME")" = \
'../Helpers/DarkbloomProvider.app/Contents/MacOS/darkbloom'
test ! -L "$FINAL_PROVIDER_BIN/$CLI_NAME"
test "$(stat -f '%Lp' "$FINAL_PROVIDER_BIN/mlx.metallib")" = 644
cmp "$FINAL_PROVIDER_BIN/mlx.metallib" "$FINAL_APP_BIN/mlx.metallib"
cmp "$FINAL_PROVIDER_BIN/$CLI_NAME" "$SMOKE_ROOT/bin/$CLI_NAME"
test -s "$FINAL_PROVIDER_APP/Contents/embedded.provisionprofile"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$FINAL_PROVIDER_APP/Contents/Info.plist")" = "$VERSION"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$FINAL_PROVIDER_APP/Contents/Info.plist")" = 'io.darkbloom.provider'
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$FINAL_PROVIDER_APP/Contents/Info.plist")" = 'darkbloom'
FINAL_FLAT_BIN="$SMOKE_ROOT/bin"
FINAL_APP_METALLIB="$FINAL_APP_BIN/mlx.metallib"
FINAL_FLAT_METALLIB="$FINAL_FLAT_BIN/mlx.metallib"
test "$(stat -f '%Lp' "$FINAL_PROVIDER_BIN/$CLI_NAME")" = 755
test "$(stat -f '%Lp' "$FINAL_APP_BIN/$ENCLAVE_NAME")" = 755
test "$(stat -f '%Lp' "$FINAL_APP_METALLIB")" = 644
test "$(stat -f '%Lp' "$FINAL_FLAT_BIN/$CLI_NAME")" = 755
test "$(stat -f '%Lp' "$FINAL_FLAT_BIN/$ENCLAVE_NAME")" = 755
test "$(stat -f '%Lp' "$FINAL_FLAT_METALLIB")" = 644
test -s "$FINAL_APP_METALLIB"
test -s "$FINAL_FLAT_METALLIB"
cmp "$FINAL_FLAT_METALLIB" "$FINAL_APP_METALLIB"
# Repeat the helper's completeness contract against the final
# extracted, signed bytes rather than the pre-signing input.
NAX_SYMBOL="_nax"
GEMV_SYMBOL="gemv"
R1_BUILDER_SYMBOL="build_gemma4_sorted_expert_tiles_bm32"
R1_BUILDER_E256_SYMBOL="build_sorted_expert_tiles_bm32_e256"
R1_KERNEL_SYMBOL="affine_gather_qmm_gemma4_expert_tiles_bfloat16_t_gs_64_b_4_alN_true_bm_32_bn_32_bk_32"
QMV_WIDE_W4_M2_SYMBOL="affine_qmv_wide_bfloat16_t_gs_64_b_4_nv_2_kl_8_batch_0"
QMV_WIDE_W4_M4_BATCHED_SYMBOL="affine_qmv_wide_bfloat16_t_gs_64_b_4_nv_4_kl_8_batch_1"
QMV_WIDE_W8_M2_SYMBOL="affine_qmv_wide_bfloat16_t_gs_64_b_8_nv_2_kl_8_batch_0"
QMV_WIDE_W8_M4_BATCHED_SYMBOL="affine_qmv_wide_bfloat16_t_gs_64_b_8_nv_4_kl_8_batch_1"
for symbol in \
"$NAX_SYMBOL" \
"$GEMV_SYMBOL" \
"$R1_BUILDER_SYMBOL" \
"$R1_BUILDER_E256_SYMBOL" \
"$R1_KERNEL_SYMBOL" \
"$QMV_WIDE_W4_M2_SYMBOL" \
"$QMV_WIDE_W4_M4_BATCHED_SYMBOL" \
"$QMV_WIDE_W8_M2_SYMBOL" \
"$QMV_WIDE_W8_M4_BATCHED_SYMBOL"
do
MATCHES=$(strings "$FINAL_FLAT_METALLIB" | grep -F -c "$symbol" || true)
if [ "$MATCHES" -eq 0 ]; then
echo "::error::final signed metallib is missing required symbol: $symbol"
exit 1
fi
done
FINAL_FAN_HELPER="$SMOKE_ROOT/Darkbloom.app/Contents/Helpers/$FAN_HELPER_NAME"
FINAL_FAN_MARKER="$SMOKE_ROOT/Darkbloom.app/Contents/Resources/darkbloom-runtime-capabilities/fan-helper-v1"
test -f "$FINAL_FAN_HELPER"
test ! -L "$FINAL_FAN_HELPER"
test -x "$FINAL_FAN_HELPER"
test "$(stat -f '%Lp' "$FINAL_FAN_HELPER")" = "755"
test -f "$FINAL_FAN_MARKER"
test ! -L "$FINAL_FAN_MARKER"
test "$(tr -d '[:space:]' < "$FINAL_FAN_MARKER")" = "1"
LC_ALL=C grep -a -q -F 'darkbloom-fan-helper-v1' \
"$FINAL_PROVIDER_BIN/$CLI_NAME"
FAN_HELPER_REQUIREMENT="anchor apple generic and identifier \"$FAN_HELPER_ID\" and certificate leaf[subject.OU] = \"$APPLE_TEAM_ID\""
codesign --verify --strict --verbose=2 \
"-R=$FAN_HELPER_REQUIREMENT" "$FINAL_FAN_HELPER"
codesign --verify --deep --strict --verbose=2 \
"$SMOKE_ROOT/Darkbloom.app"
FINAL_SMOKE_OUTPUT=$(DARKBLOOM_NO_UPDATE_CHECK=1 \
DARKBLOOM_GEMMA4_PREFILL_CHUNK_EVAL=18 \
MLX_GEMMA4_FUSED_WEIGHTED_UNSORT=1 \
MLX_GATHER_QMM_EXPERT_SLICES=1 \
"$FINAL_PROVIDER_BIN/$CLI_NAME" runtime-smoke)
printf '%s\n' "$FINAL_SMOKE_OUTPUT"
printf '%s\n' "$FINAL_SMOKE_OUTPUT" \
| grep -Fqx 'gemma-optimizations-runtime-smoke: ok'
REPORTED=$("$FINAL_PROVIDER_BIN/$CLI_NAME" --version)
./scripts/check-release-version.sh "$VERSION" "$REPORTED"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \
"$SMOKE_ROOT/Darkbloom.app/Contents/Info.plist")" = "$VERSION"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$SMOKE_ROOT/Darkbloom.app/Contents/Info.plist")" = "$VERSION"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
"$SMOKE_ROOT/Darkbloom.app/Contents/Info.plist")" = "io.darkbloom.provider"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' \
"$SMOKE_ROOT/Darkbloom.app/Contents/Info.plist")" = "DarkbloomApp"
# GUI main executable survived notarization + stapling intact.
test -x "$SMOKE_ROOT/Darkbloom.app/Contents/MacOS/DarkbloomApp"
# Verify the exact public download after rebuilding it from the
# stapled app. This is deliberately independent from the tar checks:
# a valid legacy updater artifact must not mask a stale/pre-staple or
# incomplete human-facing zip.
ZIP_SMOKE_ROOT=/tmp/darkbloom-app-zip-smoke
rm -rf "$ZIP_SMOKE_ROOT"
mkdir -p "$ZIP_SMOKE_ROOT"
ditto -x -k "$APP_ARCHIVE" "$ZIP_SMOKE_ROOT"
ROOT_ENTRY_COUNT=$(/usr/bin/find "$ZIP_SMOKE_ROOT" \
-mindepth 1 -maxdepth 1 -print | wc -l | tr -d '[:space:]')
if [ "$ROOT_ENTRY_COUNT" != "1" ] \
|| [ ! -d "$ZIP_SMOKE_ROOT/Darkbloom.app" ]; then
echo "::error::$APP_ARCHIVE_NAME must contain exactly one top-level Darkbloom.app"
/usr/bin/find "$ZIP_SMOKE_ROOT" -mindepth 1 -maxdepth 2 -print
exit 1
fi
ZIP_APP="$ZIP_SMOKE_ROOT/Darkbloom.app"
ZIP_PROVIDER_APP="$ZIP_APP/Contents/Helpers/DarkbloomProvider.app"
test "$(readlink "$ZIP_APP/Contents/MacOS/$CLI_NAME")" = \
'../Helpers/DarkbloomProvider.app/Contents/MacOS/darkbloom'
test ! -L "$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME"
test -s "$ZIP_PROVIDER_APP/Contents/embedded.provisionprofile"
cmp "$ZIP_APP/Contents/MacOS/mlx.metallib" "$ZIP_PROVIDER_APP/Contents/MacOS/mlx.metallib"
test -x "$ZIP_APP/Contents/MacOS/$APP_BINARY_NAME"
test -x "$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME"
test -x "$ZIP_APP/Contents/MacOS/$ENCLAVE_NAME"
test -s "$ZIP_APP/Contents/MacOS/mlx.metallib"
test -x "$ZIP_APP/Contents/Helpers/$FAN_HELPER_NAME"
test "$(stat -f '%Lp' "$ZIP_APP/Contents/Helpers/$FAN_HELPER_NAME")" = "755"
test -s "$ZIP_APP/Contents/Resources/Chivo-Regular.ttf"
test -s "$ZIP_APP/Contents/Resources/Chivo-Medium.ttf"
test -s "$ZIP_APP/Contents/Resources/DarkbloomProvider_DarkbloomApp.bundle/default.metallib"
test -s "$ZIP_APP/Contents/Resources/mlx-swift-lm_MLXLMCommon.bundle/pagedattention.metal"
test -f "$ZIP_APP/Contents/Resources/darkbloom-runtime-capabilities/paged-kernel-v1"
test -f "$ZIP_APP/Contents/Resources/darkbloom-runtime-capabilities/fan-helper-v1"
test -s "$ZIP_APP/Contents/embedded.provisionprofile"
while IFS= read -r bundle_name; do
test -d "$ZIP_APP/Contents/Resources/$bundle_name"
test -d "$ZIP_PROVIDER_APP/Contents/Resources/$bundle_name"
done < /tmp/darkbloom-swiftpm-resource-bundles.txt
cmp "$APP/Contents/MacOS/$APP_BINARY_NAME" \
"$ZIP_APP/Contents/MacOS/$APP_BINARY_NAME"
cmp "$PROVIDER_APP/Contents/MacOS/$CLI_NAME" \
"$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME"
cmp "$APP/Contents/MacOS/$ENCLAVE_NAME" \
"$ZIP_APP/Contents/MacOS/$ENCLAVE_NAME"
cmp "$APP/Contents/MacOS/mlx.metallib" \
"$ZIP_APP/Contents/MacOS/mlx.metallib"
codesign --verify --deep --strict --verbose=2 "$ZIP_APP"
xcrun stapler validate "$ZIP_APP"
spctl --assess --type execute --verbose=4 "$ZIP_APP"
ZIP_APP_DR=$(codesign -d -r- "$ZIP_APP" 2>&1 \
| awk -F' => ' '/designated/{print $2; exit}')
echo "$ZIP_APP_DR" | grep -qF 'identifier "io.darkbloom.provider"'
echo "$ZIP_APP_DR" | grep -qF 'anchor apple generic'
ZIP_CLI_SIGNING_ID=$(codesign -dvvv \
"$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME" 2>&1 \
| awk -F= '/^Identifier=/{print $2; exit}')
test "$ZIP_CLI_SIGNING_ID" = "io.darkbloom.provider"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \
"$ZIP_APP/Contents/Info.plist")" = "$VERSION"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$ZIP_APP/Contents/Info.plist")" = "$VERSION"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
"$ZIP_APP/Contents/Info.plist")" = "io.darkbloom.provider"
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' \
"$ZIP_APP/Contents/Info.plist")" = "$APP_BINARY_NAME"
# Keep the operator-facing, non-destructive qualification command
# exercised against the exact post-staple public artifact.
./scripts/qualify-signed-macos-app.sh \
--expected-version "$VERSION" "$APP_ARCHIVE"
ZIP_REPORTED=$("$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME" --version)
./scripts/check-release-version.sh "$VERSION" "$ZIP_REPORTED"
ZIP_SMOKE_OUTPUT=$(DARKBLOOM_NO_UPDATE_CHECK=1 \
"$ZIP_PROVIDER_APP/Contents/MacOS/$CLI_NAME" runtime-smoke)
printf '%s\n' "$ZIP_SMOKE_OUTPUT"
printf '%s\n' "$ZIP_SMOKE_OUTPUT" \
| grep -Fqx 'gemma-optimizations-runtime-smoke: ok'
BINARY_HASH=$(shasum -a 256 "$SMOKE_ROOT/bin/$CLI_NAME" | cut -d' ' -f1)
BUNDLE_HASH=$(shasum -a 256 "$LEGACY_BUNDLE" | cut -d' ' -f1)
METALLIB_HASH=$(shasum -a 256 "$FINAL_FLAT_METALLIB" | cut -d' ' -f1)
APP_ARCHIVE_HASH=$(shasum -a 256 "$APP_ARCHIVE" | cut -d' ' -f1)
{
echo "BINARY_HASH=$BINARY_HASH"
echo "BUNDLE_HASH=$BUNDLE_HASH"
echo "METALLIB_HASH=$METALLIB_HASH"
echo "APP_ARCHIVE_HASH=$APP_ARCHIVE_HASH"
} >> "$GITHUB_ENV"
echo "Binary hash: $BINARY_HASH"
echo "Bundle hash: $BUNDLE_HASH"
echo "Metallib hash: $METALLIB_HASH"
echo "App zip hash: $APP_ARCHIVE_HASH"
ls -lh "$APP_ARCHIVE" "$LEGACY_BUNDLE"
# This path is reached only after ALL signing/notary/archive/runtime
# checks above succeed. Never upload the submission ZIP, raw notary log,
# staging directory, keychain, or the rest of /tmp.
- name: Write dev qualification manifest
if: needs.resolve-env.outputs.environment == 'dev' && needs.resolve-env.outputs.publish_release == 'false'
run: |
python3 scripts/release-qualification-manifest.py \
--artifact-dir /tmp \
--notary-result /tmp/notary-result.json \
--output /tmp/darkbloom-qualification-manifest.json
- name: Retain qualified dev artifacts
if: needs.resolve-env.outputs.environment == 'dev' && needs.resolve-env.outputs.publish_release == 'false'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: darkbloom-dev-qualification-${{ github.run_id }}-${{ github.run_attempt }}
path: |
/tmp/Darkbloom-macOS-arm64.zip
/tmp/darkbloom-bundle-macos-arm64.tar.gz
/tmp/darkbloom-qualification-manifest.json
if-no-files-found: error
compression-level: 0
retention-days: 30
- name: Upload release artifacts to R2
if: needs.resolve-env.outputs.publish_release == 'true'
env:
AWS_ACCESS_KEY_ID: ${{ env.r2_access_key_id }}
AWS_SECRET_ACCESS_KEY: ${{ env.r2_secret_access_key }}
R2_ENDPOINT: ${{ env.r2_endpoint }}
R2_BUCKET: ${{ env.r2_bucket }}
run: |
set -euo pipefail
PREFIX="s3://${R2_BUCKET}/releases/v${VERSION}"
aws s3 cp "/tmp/$LEGACY_BUNDLE_NAME" \
"${PREFIX}/${LEGACY_BUNDLE_NAME}" \
--endpoint-url "$R2_ENDPOINT" --only-show-errors
aws s3 cp "/tmp/$APP_ARCHIVE_NAME" \
"${PREFIX}/${APP_ARCHIVE_NAME}" \
--endpoint-url "$R2_ENDPOINT" --only-show-errors
# Latest pointer for `install.sh` discovery. Publish under both
# the canonical `darkbloom-bundle` name and the legacy
# `eigeninference-bundle` name for backward compatibility with
# any caller that hardcoded the old filename.
aws s3 cp "/tmp/$LEGACY_BUNDLE_NAME" \
"s3://${R2_BUCKET}/releases/latest/${LEGACY_BUNDLE_NAME}" \
--endpoint-url "$R2_ENDPOINT" --only-show-errors
aws s3 cp "/tmp/$LEGACY_BUNDLE_NAME" \
"s3://${R2_BUCKET}/releases/latest/eigeninference-bundle-macos-arm64.tar.gz" \
--endpoint-url "$R2_ENDPOINT" --only-show-errors
- name: Register release with coordinator
if: needs.resolve-env.outputs.publish_release == 'true'
env:
COORDINATOR_URL: ${{ env.coordinator_url }}
RELEASE_KEY: ${{ env.release_key }}
R2_PUBLIC_URL: ${{ env.r2_public_url }}
run: |
set -euo pipefail
BUNDLE_URL="${R2_PUBLIC_URL}/releases/v${VERSION}/${LEGACY_BUNDLE_NAME}"
TAG_MSG=$(git tag -l --format='%(contents:subject)%0a%(contents:body)' "$GITHUB_REF_NAME" 2>/dev/null || echo "")
if [ -z "$TAG_MSG" ] || [ "$TAG_MSG" = $'\n' ]; then
TAG_MSG="Release v${VERSION}"
fi
# The Swift provider does NOT expose python_hash / runtime_hash, and
# its only template fact is the mlx_metallib hash (metallib_hash
# below). Per-model-family template hashes were CI fabrications no
# provider could ever echo — registering them armed a coordinator
# gate that zeroed fleet routing (2026-08-31 incident). Register
# only facts the provider actually reports.
python3 - <<PY > /tmp/release-payload.json
import json, os
payload = {
"version": os.environ["VERSION"],
"platform": "macos-arm64",
"backend": "mlx-swift",
"binary_hash": os.environ["BINARY_HASH"],
"bundle_hash": os.environ["BUNDLE_HASH"],
"metallib_hash": os.environ["METALLIB_HASH"],
"url": "${BUNDLE_URL}",
"changelog": """${TAG_MSG}""".strip(),
}
print(json.dumps(payload))
PY
curl -fsSL -X POST "${COORDINATOR_URL}/v1/releases" \
-H "Authorization: Bearer ${RELEASE_KEY}" \
-H "Content-Type: application/json" \
-d @/tmp/release-payload.json \
-o /tmp/release-registration-response.json
python3 - <<'PY'
import json
with open("/tmp/release-registration-response.json", encoding="utf-8") as handle:
response = json.load(handle)
release = response.get("release")
if not isinstance(release, dict):
raise SystemExit("coordinator registration response omitted release")
for field in ("has_app", "has_fan_helper", "has_paged_kernel"):
if release.get(field) is not True:
raise SystemExit(
f"coordinator did not derive {field}=true from the signed artifact"
)
print(json.dumps(response, separators=(",", ":")))
PY
echo "Release v${VERSION} registered with coordinator"
- name: Create GitHub Release
if: needs.resolve-env.outputs.publish_release == 'true' && needs.resolve-env.outputs.environment == 'prod' && github.ref_type == 'tag'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
cat > /tmp/release-notes.md <<NOTES
## Darkbloom v${VERSION} for macOS
### Download the app
Download **\`${APP_ARCHIVE_NAME}\`** from Assets, unzip it, and open
**Darkbloom.app**. The signed, notarized, and stapled app installs itself at
\`~/.darkbloom/Darkbloom.app\` before onboarding, creates a safe
\`~/Applications/Darkbloom.app\` symlink when that path is available, and
reopens the installed copy. It includes its provider CLI and runtime resources.
Direct downloads and Terminal installs use the same writable canonical path,
background-service path, and managed updater. After the first handoff, open
the installed app from \`~/Applications\` or \`~/.darkbloom\`; delete the
extracted copy after the installed app opens successfully instead of reopening
an older copy later.
The legacy tar asset remains available for the coordinator verifier and
provider updater; it is not the human-facing download.
**Binary hash:** \`${BINARY_HASH}\`
**Bundle hash:** \`${BUNDLE_HASH}\`
**Metallib hash:** \`${METALLIB_HASH}\` (built from mlx \`${{ steps.mlxkey.outputs.mlx_sha_short }}\`)
**App zip hash:** \`${APP_ARCHIVE_HASH}\`
**Signed by:** \`${DEVELOPER_ID}\`
**Notarized and stapled:** yes
**Min macOS:** ${MIN_MACOS}
### Terminal install
\`\`\`bash
curl -fsSL ${coordinator_url}/install.sh | bash
\`\`\`
NOTES
gh release create "${{ github.ref_name }}" \
"/tmp/$APP_ARCHIVE_NAME" \
"/tmp/$LEGACY_BUNDLE_NAME" \
--title "${{ github.ref_name }}" \
--notes-file /tmp/release-notes.md \
--generate-notes
- name: Cleanup keychain
if: always()
run: |
security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true