Skip to content

fix: harden Finder extension signing #6

fix: harden Finder extension signing

fix: harden Finder extension signing #6

Workflow file for this run

name: Release
# Builds, signs, notarizes, and publishes a Dory release when a version tag (e.g. v0.1.0) is
# pushed, or on manual dispatch.
#
# Required repository secrets:
# DEVELOPER_ID_CERT_P12_BASE64 base64 of your "Developer ID Application" .p12
# DEVELOPER_ID_CERT_PASSWORD the .p12 export password
# KEYCHAIN_PASSWORD any string, used for the throwaway CI keychain
# NOTARY_APPLE_ID Apple ID email for notarytool
# NOTARY_TEAM_ID Apple Developer Team ID
# NOTARY_APPLE_PASSWORD app-specific password for that Apple ID
# SPARKLE_PRIVATE_KEY Sparkle EdDSA private key from generate_keys/sign_update
# SPARKLE_ED_PRIVATE_KEY accepted legacy secret-name fallback
# HOMEBREW_TAP_DEPLOY_KEY Ed25519 private deploy key with write access only to
# Augani/homebrew-dory
# DORY_LAN_PEER_SSH noninteractive SSH peer on the physical LAN
# DORY_LAN_HOST_IPV4 release Mac's physical-LAN IPv4 address
# DORY_TAILSCALE_PEER_SSH noninteractive SSH peer over Tailscale
# DORY_TAILSCALE_HOST_IPV4 release Mac's Tailscale IPv4 address
# Required repository variable:
# DORY_SOURCE_GATE_IMAGE digest-pinned image containing python3
# DORY_RELEASE_ALPINE_IMAGE digest-pinned Alpine fixture used by every release smoke/soak
# DORY_RELEASE_NONNATIVE_BUILD_IMAGE digest-pinned amd64 Node/Alpine BuildKit fixture
# DORY_RELEASE_SSH_CLIENT_IMAGE digest-pinned Apple-silicon fixture containing sh and ssh-add
# DORY_EXTERNAL_VOLUME_TEST_ROOT dedicated writable directory on external APFS release media;
# its volume root must contain .dory-release-external-volume with the exact contents
# DORY-DEDICATED-RELEASE-APFS-V1, authorizing the gate to unmount/remount that whole volume
# DORY_CORPORATE_DNS_SERVER resolver address active only through the release VPN
# DORY_CORPORATE_VPN_PROBE_HOST internal split-DNS HTTPS endpoint hostname
# DORY_CORPORATE_VPN_PROBE_URL HTTPS URL on that exact internal hostname
# DORY_TAILSCALE_EXIT_NODE real exit-node hostname/IP used for route churn certification
# DORY_RELEASE_ECR_REGISTRY ACCOUNT.dkr.ecr.REGION.amazonaws.com test registry
# DORY_RELEASE_ECR_REPOSITORY pre-created disposable retry-test repository
# DORY_RELEASE_ECR_REGION AWS region containing that repository
# DORY_RELEASE_AWS_ROLE_ARN GitHub-OIDC role scoped to the disposable ECR repository
# DORY_BENCH_IPERF_IMAGE digest-pinned arm64 iperf3 fixture
# DORY_BENCH_NODE_IMAGE digest-pinned arm64 Node/Corepack fixture
# DORY_BENCH_POSTGRES_IMAGE digest-pinned arm64 PostgreSQL fixture
# DORY_BENCH_REDIS_IMAGE digest-pinned arm64 Redis fixture
# DORY_BENCH_RUBY_IMAGE digest-pinned arm64 Ruby/Bundler fixture
# DORY_BENCH_COMPOSER_IMAGE digest-pinned arm64 Composer/PHP fixture
# DORY_BENCH_CURL_IMAGE digest-pinned arm64 curl fixture
# DORY_BENCH_PROBE_URL credential-free controlled HTTPS probe
# DORY_BENCH_DOWNLOAD_URL credential-free controlled fixed-byte HTTPS payload
# DORY_BENCH_DOWNLOAD_BYTES exact payload byte count
#
# NOTE: Dory.app targets macOS 14+ and the public production track is Apple Silicon first.
# Intel jobs are roadmap-only and never block or contribute artifacts to the public release.
# The full bundle includes a built-in engine on macOS 14+: Sonoma uses the bundled
# Virtualization.framework dory-vmm tier, while supported macOS 15+ hosts use dory-hv.
# Guest kernels/initfs are rebuilt from this commit in architecture-native jobs and passed to the
# release jobs as same-run artifacts. A checkout never relies on ignored/stale guest/out files.
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g. 0.1.0)'
required: true
permissions:
contents: read
concurrency:
group: dory-public-release
cancel-in-progress: false
jobs:
release-configuration:
name: Required release credentials and tap access
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
id-token: write
env:
DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }}
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
NOTARY_APPLE_PASSWORD: ${{ secrets.NOTARY_APPLE_PASSWORD }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY || secrets.SPARKLE_ED_PRIVATE_KEY }}
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
DORY_LAN_PEER_SSH: ${{ secrets.DORY_LAN_PEER_SSH }}
DORY_LAN_HOST_IPV4: ${{ secrets.DORY_LAN_HOST_IPV4 }}
DORY_TAILSCALE_PEER_SSH: ${{ secrets.DORY_TAILSCALE_PEER_SSH }}
DORY_TAILSCALE_HOST_IPV4: ${{ secrets.DORY_TAILSCALE_HOST_IPV4 }}
DORY_SOURCE_GATE_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
DORY_RELEASE_ALPINE_IMAGE: ${{ vars.DORY_RELEASE_ALPINE_IMAGE }}
DORY_RELEASE_NONNATIVE_BUILD_IMAGE: ${{ vars.DORY_RELEASE_NONNATIVE_BUILD_IMAGE }}
DORY_RELEASE_SSH_CLIENT_IMAGE: ${{ vars.DORY_RELEASE_SSH_CLIENT_IMAGE }}
DORY_EXTERNAL_VOLUME_TEST_ROOT: ${{ vars.DORY_EXTERNAL_VOLUME_TEST_ROOT }}
DORY_CORPORATE_DNS_SERVER: ${{ vars.DORY_CORPORATE_DNS_SERVER }}
DORY_CORPORATE_VPN_PROBE_HOST: ${{ vars.DORY_CORPORATE_VPN_PROBE_HOST }}
DORY_CORPORATE_VPN_PROBE_URL: ${{ vars.DORY_CORPORATE_VPN_PROBE_URL }}
DORY_TAILSCALE_EXIT_NODE: ${{ vars.DORY_TAILSCALE_EXIT_NODE }}
DORY_RELEASE_ECR_REGISTRY: ${{ vars.DORY_RELEASE_ECR_REGISTRY }}
DORY_RELEASE_ECR_REPOSITORY: ${{ vars.DORY_RELEASE_ECR_REPOSITORY }}
DORY_RELEASE_ECR_REGION: ${{ vars.DORY_RELEASE_ECR_REGION }}
DORY_RELEASE_AWS_ROLE_ARN: ${{ vars.DORY_RELEASE_AWS_ROLE_ARN }}
DORY_BENCH_IPERF_IMAGE: ${{ vars.DORY_BENCH_IPERF_IMAGE }}
DORY_BENCH_NODE_IMAGE: ${{ vars.DORY_BENCH_NODE_IMAGE }}
DORY_BENCH_POSTGRES_IMAGE: ${{ vars.DORY_BENCH_POSTGRES_IMAGE }}
DORY_BENCH_REDIS_IMAGE: ${{ vars.DORY_BENCH_REDIS_IMAGE }}
DORY_BENCH_RUBY_IMAGE: ${{ vars.DORY_BENCH_RUBY_IMAGE }}
DORY_BENCH_COMPOSER_IMAGE: ${{ vars.DORY_BENCH_COMPOSER_IMAGE }}
DORY_BENCH_CURL_IMAGE: ${{ vars.DORY_BENCH_CURL_IMAGE }}
DORY_BENCH_PROBE_URL: ${{ vars.DORY_BENCH_PROBE_URL }}
DORY_BENCH_DOWNLOAD_URL: ${{ vars.DORY_BENCH_DOWNLOAD_URL }}
DORY_BENCH_DOWNLOAD_BYTES: ${{ vars.DORY_BENCH_DOWNLOAD_BYTES }}
steps:
- name: Obtain short-lived ECR credentials through GitHub OIDC
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
with:
role-to-assume: ${{ vars.DORY_RELEASE_AWS_ROLE_ARN }}
aws-region: ${{ vars.DORY_RELEASE_ECR_REGION }}
role-session-name: DoryReleasePreflight
role-duration-seconds: 900
- name: Preflight required credentials and advertised Homebrew tap access
run: |
missing=0
for name in \
DEVELOPER_ID_CERT_P12_BASE64 \
DEVELOPER_ID_CERT_PASSWORD \
KEYCHAIN_PASSWORD \
NOTARY_APPLE_ID \
NOTARY_APPLE_PASSWORD \
SPARKLE_PRIVATE_KEY \
HOMEBREW_TAP_DEPLOY_KEY \
DORY_LAN_PEER_SSH \
DORY_LAN_HOST_IPV4 \
DORY_TAILSCALE_PEER_SSH \
DORY_TAILSCALE_HOST_IPV4 \
DORY_SOURCE_GATE_IMAGE \
DORY_RELEASE_ALPINE_IMAGE \
DORY_RELEASE_NONNATIVE_BUILD_IMAGE \
DORY_RELEASE_SSH_CLIENT_IMAGE \
DORY_EXTERNAL_VOLUME_TEST_ROOT \
DORY_CORPORATE_DNS_SERVER \
DORY_CORPORATE_VPN_PROBE_HOST \
DORY_CORPORATE_VPN_PROBE_URL \
DORY_TAILSCALE_EXIT_NODE \
DORY_RELEASE_ECR_REGISTRY \
DORY_RELEASE_ECR_REPOSITORY \
DORY_RELEASE_ECR_REGION \
DORY_RELEASE_AWS_ROLE_ARN \
DORY_BENCH_IPERF_IMAGE \
DORY_BENCH_NODE_IMAGE \
DORY_BENCH_POSTGRES_IMAGE \
DORY_BENCH_REDIS_IMAGE \
DORY_BENCH_RUBY_IMAGE \
DORY_BENCH_COMPOSER_IMAGE \
DORY_BENCH_CURL_IMAGE \
DORY_BENCH_PROBE_URL \
DORY_BENCH_DOWNLOAD_URL \
DORY_BENCH_DOWNLOAD_BYTES; do
if [ -z "${!name}" ]; then
echo "required release secret is missing: $name" >&2
missing=1
fi
done
[ "$missing" = 0 ] || exit 1
printf '%s\n' "$DORY_SOURCE_GATE_IMAGE" | grep -Eq '^.+@sha256:[0-9a-f]{64}$' \
|| { echo "DORY_SOURCE_GATE_IMAGE must be digest-pinned" >&2; exit 1; }
for name in DORY_RELEASE_ALPINE_IMAGE DORY_RELEASE_NONNATIVE_BUILD_IMAGE \
DORY_RELEASE_SSH_CLIENT_IMAGE DORY_BENCH_IPERF_IMAGE DORY_BENCH_NODE_IMAGE \
DORY_BENCH_POSTGRES_IMAGE DORY_BENCH_REDIS_IMAGE DORY_BENCH_RUBY_IMAGE \
DORY_BENCH_COMPOSER_IMAGE DORY_BENCH_CURL_IMAGE; do
printf '%s\n' "${!name}" | grep -Eq '^.+@sha256:[0-9a-f]{64}$' \
|| { echo "$name must be digest-pinned" >&2; exit 1; }
done
for name in DORY_BENCH_PROBE_URL DORY_BENCH_DOWNLOAD_URL; do
case "${!name}" in https://*) ;; *) echo "$name must use HTTPS" >&2; exit 1 ;; esac
case "${!name}" in *[[:space:]@]*) echo "$name must not contain credentials or whitespace" >&2; exit 1 ;; esac
done
printf '%s\n' "$DORY_BENCH_DOWNLOAD_BYTES" | grep -Eq '^[1-9][0-9]*$' \
|| { echo "DORY_BENCH_DOWNLOAD_BYTES must be a positive integer" >&2; exit 1; }
case "$DORY_CORPORATE_VPN_PROBE_URL" in
https://"$DORY_CORPORATE_VPN_PROBE_HOST"|https://"$DORY_CORPORATE_VPN_PROBE_HOST"/*) ;;
*) echo "DORY_CORPORATE_VPN_PROBE_URL must use the exact split-DNS host over HTTPS" >&2; exit 1 ;;
esac
printf '%s\n' "$DORY_RELEASE_ECR_REGISTRY" \
| grep -Eq '^[0-9]{12}\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com$' \
|| { echo "DORY_RELEASE_ECR_REGISTRY must be an ECR registry host" >&2; exit 1; }
printf '%s\n' "$DORY_RELEASE_ECR_REPOSITORY" \
| grep -Eq '^[a-z0-9]+([._/-][a-z0-9]+)*$' \
|| { echo "DORY_RELEASE_ECR_REPOSITORY is invalid" >&2; exit 1; }
printf '%s\n' "$DORY_RELEASE_ECR_REGION" \
| grep -Eq '^[a-z]{2}(-gov)?-[a-z]+-[0-9]+$' \
|| { echo "DORY_RELEASE_ECR_REGION is invalid" >&2; exit 1; }
case "$DORY_RELEASE_ECR_REGISTRY" in
*".dkr.ecr.$DORY_RELEASE_ECR_REGION.amazonaws.com") ;;
*) echo "DORY_RELEASE_ECR_REGISTRY and DORY_RELEASE_ECR_REGION disagree" >&2; exit 1 ;;
esac
printf '%s\n' "$DORY_RELEASE_AWS_ROLE_ARN" \
| grep -Eq '^arn:aws:iam::[0-9]{12}:role/[A-Za-z0-9+=,.@_/-]+$' \
|| { echo "DORY_RELEASE_AWS_ROLE_ARN is invalid" >&2; exit 1; }
command -v aws >/dev/null \
|| { echo "AWS CLI is required for release ECR preflight" >&2; exit 1; }
aws sts get-caller-identity >/dev/null
aws ecr describe-repositories --region "$DORY_RELEASE_ECR_REGION" \
--repository-names "$DORY_RELEASE_ECR_REPOSITORY" >/dev/null
ssh_dir="$RUNNER_TEMP/homebrew-tap-ssh"
install -d -m 0700 "$ssh_dir"
printf '%s\n' "$HOMEBREW_TAP_DEPLOY_KEY" > "$ssh_dir/key"
chmod 0600 "$ssh_dir/key"
curl -fsSL --retry 3 --connect-timeout 15 --max-time 60 \
https://api.github.com/meta -o "$ssh_dir/github-meta.json"
python3 - "$ssh_dir/github-meta.json" "$ssh_dir/known_hosts" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
metadata = json.load(handle)
keys = metadata.get("ssh_keys", [])
assert keys and all(key.startswith("ssh-") for key in keys), "GitHub SSH metadata is missing"
with open(sys.argv[2], "w", encoding="utf-8") as handle:
for key in keys:
handle.write(f"github.com {key}\n")
PY
export GIT_SSH_COMMAND="ssh -i $ssh_dir/key -o IdentitiesOnly=yes -o UserKnownHostsFile=$ssh_dir/known_hosts -o StrictHostKeyChecking=yes"
git clone --quiet --depth 1 --no-checkout \
git@github.com:Augani/homebrew-dory.git "$RUNNER_TEMP/homebrew-tap-preflight"
git -C "$RUNNER_TEMP/homebrew-tap-preflight" push --dry-run origin \
HEAD:refs/heads/dory-release-preflight >/dev/null
rust-workspace:
name: Linux full Rust quality gate
needs: release-configuration
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
with:
components: rustfmt, clippy
- name: Format full Rust workspace
working-directory: dory-core
run: cargo fmt --all -- --check
- name: Lint full Rust workspace
working-directory: dory-core
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: Test full Rust workspace
working-directory: dory-core
run: cargo test --workspace --locked
guest-assets-arm64:
name: Build verified arm64 guest assets
needs: release-configuration
runs-on: ubuntu-24.04-arm
timeout-minutes: 180
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- name: Install guest build prerequisites
run: sudo apt-get update && sudo apt-get install -y binutils e2fsprogs file patchelf zstd
- name: Expose rust-lld for the static Linux guest agent
run: |
rust_lld="$(find "$(rustc --print sysroot)/lib/rustlib" -type f -name rust-lld | head -1)"
test -x "$rust_lld"
mkdir -p "$RUNNER_TEMP/rust-lld-bin"
ln -sf "$rust_lld" "$RUNNER_TEMP/rust-lld-bin/rust-lld"
echo "$RUNNER_TEMP/rust-lld-bin" >> "$GITHUB_PATH"
- name: Build and verify headless, GPU, and initfs payloads
run: |
DORY_EXPERIMENTAL_GPU=0 guest/kernel/build.sh arm64
DORY_EXPERIMENTAL_GPU=1 guest/kernel/build.sh arm64
guest/initfs/build.sh arm64
DORY_EXPERIMENTAL_GPU=0 guest/kernel/verify-build.sh arm64
DORY_EXPERIMENTAL_GPU=1 guest/kernel/verify-build.sh arm64
guest/initfs/verify-build.sh arm64
- name: Upload same-commit arm64 guest payload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-guest-arm64-${{ github.sha }}
retention-days: 30
if-no-files-found: error
path: |
guest/out/Image
guest/out/Image.zst
guest/out/config-arm64
guest/out/kernel-build-arm64.stamp
guest/out/Image-gpu
guest/out/Image-gpu.zst
guest/out/config-arm64-gpu
guest/out/kernel-build-arm64-gpu.stamp
guest/out/initfs-arm64.ext4
guest/out/dory-agent-arm64
guest/out/initfs-build-arm64.stamp
prepublication-quality:
name: macOS pre-publication quality gate
needs: release-configuration
runs-on: macos-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- name: Select newest installed Xcode
run: |
newest="$(ls -d /Applications/Xcode*.app | sort -V | tail -1)"
echo "selected: $newest"
echo "DEVELOPER_DIR=$newest/Contents/Developer" >> "$GITHUB_ENV"
- name: Download Metal toolchain (Xcode 26 ships without it)
run: xcodebuild -downloadComponent MetalToolchain || true
- name: Build shared Rust guest-control client
run: scripts/build-dory-ffi-xcframework.sh --if-needed
- name: P0 smoke harness regression tests
run: bash scripts/test-p0-smoke.sh
- name: Dory app and offline quality suite
run: bash scripts/ci-test.sh
- name: Dory core Swift tests
working-directory: dory-core-swift
run: swift test
- name: DoryHV tests
working-directory: Packages/ContainerizationEngine
run: swift test
- name: Dory UI tests
run: |
ui_derived_data="$RUNNER_TEMP/dory-ui-derived-data"
xcodebuild clean \
-project Dory.xcodeproj \
-scheme 'Dory UI Tests' \
-destination 'platform=macOS' \
-derivedDataPath "$ui_derived_data"
xcodebuild build-for-testing \
-project Dory.xcodeproj \
-scheme 'Dory UI Tests' \
-destination 'platform=macOS' \
-derivedDataPath "$ui_derived_data" \
-parallel-testing-enabled NO \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_REQUIRED=YES \
CODE_SIGN_IDENTITY=-
scripts/clean-xcode-products.sh --root "$ui_derived_data"
codesign --verify --deep --strict "$ui_derived_data/Build/Products/Debug/Dory.app"
codesign --verify --deep --strict "$ui_derived_data/Build/Products/Debug/DoryUITests-Runner.app"
xcodebuild test-without-building \
-project Dory.xcodeproj \
-scheme 'Dory UI Tests' \
-destination 'platform=macOS' \
-derivedDataPath "$ui_derived_data" \
-parallel-testing-enabled NO \
CODE_SIGNING_ALLOWED=YES \
CODE_SIGNING_REQUIRED=YES \
CODE_SIGN_IDENTITY=-
release_candidate:
name: Build, sign, notarize, and stage immutable candidate
needs: [rust-workspace, prepublication-quality, guest-assets-arm64]
# Publication is intentionally bound to the dedicated physical Apple-silicon release host.
# Hosted/nested runners are not eligible. Dory launches from one clean v1 data-drive schema;
# pre-release Dory formats are not release fixtures.
runs-on: [self-hosted, macOS, arm64, dory, release]
timeout-minutes: 720
outputs:
version: ${{ steps.ver.outputs.version }}
sha256: ${{ steps.build.outputs.sha256 }}
dmg: ${{ steps.build.outputs.dmg }}
zip: ${{ steps.build.outputs.zip }}
zip_arm64: ${{ steps.build.outputs.zip_arm64 }}
manifest: ${{ steps.build.outputs.manifest }}
appcast: ${{ steps.build.outputs.appcast }}
app_update: ${{ steps.build.outputs.app_update }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable
- name: Download same-commit arm64 guest payload
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-guest-arm64-${{ github.sha }}
path: guest/out
- name: Independently verify every downloaded guest payload
run: |
DORY_EXPERIMENTAL_GPU=0 guest/kernel/verify-build.sh arm64
DORY_EXPERIMENTAL_GPU=1 guest/kernel/verify-build.sh arm64
guest/initfs/verify-build.sh arm64
- name: Prove the tracked release source exactly matches the commit
run: |
git diff --exit-code
test -z "$(git status --porcelain --untracked-files=no)"
git rev-parse HEAD | grep -qx "$GITHUB_SHA"
- name: Resolve version
id: ver
run: |
if [ -n "${{ github.event.inputs.version }}" ]; then
[ "$GITHUB_REF" = refs/heads/main ] || {
echo "Manual public releases must run from main, not $GITHUB_REF" >&2
exit 1
}
V="${{ github.event.inputs.version }}"
else
V="${GITHUB_REF_NAME#v}"
fi
git fetch --no-tags origin main
git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
echo "Release commit $GITHUB_SHA is not reachable from main" >&2
exit 1
}
echo "version=$V" >> "$GITHUB_OUTPUT"
- name: Select the pinned Xcode 26.6 release toolchain
run: |
xcode_app=/Applications/Xcode-26.6.0-Release.Candidate.app
test -x "$xcode_app/Contents/Developer/usr/bin/xcodebuild"
echo "selected: $xcode_app"
echo "DEVELOPER_DIR=$xcode_app/Contents/Developer" >> "$GITHUB_ENV"
- name: Record and prove physical Apple-silicon host facts
run: |
facts="$RUNNER_TEMP/dory-arm64-host-facts.txt"
{
sw_vers
uname -a
printf 'hw.model='; sysctl -n hw.model
printf 'kern.hv_support='; sysctl -n kern.hv_support
printf 'kern.hv_vmm_present='; sysctl -in kern.hv_vmm_present 2>/dev/null || printf '0\n'
printf 'hw.optional.arm64='; sysctl -in hw.optional.arm64 2>/dev/null || printf '0\n'
} | tee "$facts"
test "$(uname -m)" = arm64
test "$(sysctl -n kern.hv_support)" = 1
test "$(sysctl -in kern.hv_vmm_present 2>/dev/null || printf 0)" != 1
test "$(sysctl -in hw.optional.arm64 2>/dev/null || printf 0)" = 1
case "$(sysctl -n hw.model)" in VirtualMac*) exit 1 ;; esac
echo 'DORY_RELEASE_PHYSICAL_ARM64_CONFIRMED=1' >> "$GITHUB_ENV"
- name: Ensure Metal toolchain (SwiftTerm ships Metal shaders)
run: xcodebuild -downloadComponent MetalToolchain || true
- name: Preserve previous released appcast history
run: |
previous="$RUNNER_TEMP/previous-appcast.xml"
if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 15 --max-time 60 \
https://github.com/Augani/dory/releases/latest/download/appcast.xml \
-o "$previous"; then
python3 - "$previous" <<'PY'
import sys
import xml.etree.ElementTree as ET
sparkle = "http://www.andymatuschak.org/xml-namespaces/sparkle"
dory = "https://augani.github.io/dory/appcast"
ET.register_namespace("sparkle", sparkle)
ET.register_namespace("dory", dory)
tree = ET.parse(sys.argv[1])
items = tree.getroot().findall("./channel/item")
assert items, "previous release appcast has no item"
for item in items:
assert item.findtext(f"{{{sparkle}}}minimumSystemVersion") == "14.0", \
"previous release appcast would regress the macOS 14 floor"
# Releases before 0.4 all use data/component schema 1. Normalize their
# historical items once so the first transactional updater can enforce an
# exact contract without dropping the signed enclosure history.
for name in ("dataSchemaVersion", "minimumReadableDataSchema", \
"maximumReadableDataSchema", "componentCatalogSchema"):
if item.find(f"{{{dory}}}{name}") is None:
ET.SubElement(item, f"{{{dory}}}{name}").text = "1"
tree.write(sys.argv[1], encoding="utf-8", xml_declaration=True)
PY
cp "$previous" website/public/appcast.xml
else
echo "No prior release appcast asset exists; using the checked-in bootstrap history."
fi
- name: Import Developer ID certificate
env:
CERT_BASE64: ${{ secrets.DEVELOPER_ID_CERT_P12_BASE64 }}
CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
KEYCHAIN="$RUNNER_TEMP/dory-signing.keychain-db"
echo "$CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import "$RUNNER_TEMP/cert.p12" -P "$CERT_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN"
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security list-keychains -d user -s "$KEYCHAIN" login.keychain
rm -f "$RUNNER_TEMP/cert.p12"
- name: Build, sign, and notarize
id: build
env:
NOTARY_APPLE_ID: ${{ secrets.NOTARY_APPLE_ID }}
# Team ID is not secret (it appears in every signed app); fall back to the project's team.
NOTARY_TEAM_ID: ${{ secrets.NOTARY_TEAM_ID || '864H636QW4' }}
NOTARY_PASSWORD: ${{ secrets.NOTARY_APPLE_PASSWORD }}
# This job publishes publicly, so development escape hatches are deliberately unavailable.
# The public production contract is intentionally Apple-Silicon-only for this phase.
DORY_PUBLIC_RELEASE: '1'
DORY_BUNDLE_ENGINE: '1'
DORY_REQUIRE_BUNDLE_ASSETS: '1'
DORY_REQUIRE_DEVELOPER_ID_SIGNATURES: '1'
DORY_BUNDLE_VENUS: '1'
DORY_BUNDLE_VENUS_REQUIRED: '1'
DORY_RELEASE_VARIANTS: 'arm64'
DORY_BUILD_APPCAST: '1'
DORY_BUILD_APP_UPDATE: '1'
DORY_RELEASE_ASSET_BASE_URL: https://github.com/Augani/dory/releases/download/v${{ steps.ver.outputs.version }}
DORY_SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY || secrets.SPARKLE_ED_PRIVATE_KEY }}
DORY_RELEASE_SOURCE_COMMIT: ${{ github.sha }}
run: scripts/release.sh "${{ steps.ver.outputs.version }}" "${{ github.run_number }}"
- name: Validate public release outputs
run: |
scripts/validate-release-outputs.sh \
release-build \
"${{ steps.ver.outputs.version }}" \
"${{ github.run_number }}"
- name: Extract the exact signed Sparkle update candidate
id: sparkle_candidate
env:
UPDATE_ZIP: ${{ steps.build.outputs.app_update }}
DORY_SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY || secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
set -euo pipefail
test -s "$UPDATE_ZIP"
candidate_root="$RUNNER_TEMP/dory-release-update-candidate"
rm -rf "$candidate_root"
mkdir -p "$candidate_root/extracted" "$candidate_root/evidence"
python3 - "$UPDATE_ZIP" release-build/release-manifest.json <<'PY'
import hashlib
import json
import pathlib
import sys
import zipfile
archive_path, manifest_path = sys.argv[1:3]
with zipfile.ZipFile(archive_path) as archive:
for name in archive.namelist():
path = pathlib.PurePosixPath(name)
if path.is_absolute() or ".." in path.parts:
raise SystemExit(f"unsafe Sparkle ZIP member: {name}")
if path.parts and path.parts[0] not in {"Dory.app", "__MACOSX"}:
raise SystemExit(f"unexpected Sparkle ZIP root: {name}")
digest = hashlib.sha256()
with open(archive_path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
with open(manifest_path, encoding="utf-8") as handle:
manifest = json.load(handle)
expected_name = pathlib.Path(archive_path).name
records = {record["name"]: record for record in manifest["artifacts"]}
record = records[expected_name]
assert record["sha256"] == digest.hexdigest(), "Sparkle candidate ZIP differs from release manifest"
PY
ditto -x -k "$UPDATE_ZIP" "$candidate_root/extracted"
test -d "$candidate_root/extracted/Dory.app"
test "$(find "$candidate_root/extracted" -type d -name Dory.app -print | wc -l | tr -d ' ')" = 1
shasum -a 256 "$UPDATE_ZIP" > "$candidate_root/evidence/app-update.sha256"
scripts/verify-sparkle-update.sh \
"$candidate_root/extracted/Dory.app" \
"$UPDATE_ZIP" \
release-build/appcast.xml \
> "$candidate_root/evidence/sparkle-verification.txt"
printf 'app=%s\n' "$candidate_root/extracted/Dory.app" >> "$GITHUB_OUTPUT"
- name: Resolve the exact release-build Sparkle source checkout
id: sparkle_source
run: |
set -euo pipefail
revision="$(python3 - <<'PY'
import json
with open("Dory.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved", encoding="utf-8") as handle:
payload = json.load(handle)
pins = [pin for pin in payload["pins"] if pin["identity"] == "sparkle"]
assert len(pins) == 1, "expected one Sparkle package pin"
assert pins[0]["state"].get("version") == "2.9.4", "unexpected Sparkle release pin"
print(pins[0]["state"]["revision"])
PY
)"
sparkle_source=""
while IFS= read -r checkout; do
if [ "$(git -C "$checkout" rev-parse HEAD 2>/dev/null || true)" = "$revision" ] \
&& [ -z "$(git -C "$checkout" status --porcelain --untracked-files=no)" ]; then
sparkle_source="$checkout"
break
fi
done < <({
find release-build/DerivedData -type d -path '*/SourcePackages/checkouts/Sparkle' -prune -print
find "$HOME/Library/Developer/Xcode/DerivedData" \
-type d -path '*/SourcePackages/checkouts/Sparkle' -prune -print
} | awk '!seen[$0]++')
test -n "$sparkle_source"
printf 'path=%s\n' "$sparkle_source" >> "$GITHUB_OUTPUT"
- name: Exercise the notarized direct-download candidate on a clean physical Mac
timeout-minutes: 30
env:
DORY_RELEASE_CLEAN_USER: '1'
DORY_RELEASE_EXTERNAL_VOLUME_ROOT: ${{ vars.DORY_EXTERNAL_VOLUME_TEST_ROOT }}
DORY_RELEASE_LOCK_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
DORY_RELEASE_FIXTURE_IMAGE: ${{ vars.DORY_RELEASE_ALPINE_IMAGE }}
DORY_RELEASE_NONNATIVE_BUILD_IMAGE: ${{ vars.DORY_RELEASE_NONNATIVE_BUILD_IMAGE }}
DORY_RELEASE_SSH_CLIENT_IMAGE: ${{ vars.DORY_RELEASE_SSH_CLIENT_IMAGE }}
DORY_RELEASE_RUN_PHYSICAL_SLEEP: '1'
DORY_RELEASE_CORPORATE_DNS_SERVER: ${{ vars.DORY_CORPORATE_DNS_SERVER }}
DORY_RELEASE_CORPORATE_VPN_PROBE_HOST: ${{ vars.DORY_CORPORATE_VPN_PROBE_HOST }}
DORY_RELEASE_CORPORATE_VPN_PROBE_URL: ${{ vars.DORY_CORPORATE_VPN_PROBE_URL }}
DORY_RELEASE_TAILSCALE_EXIT_NODE: ${{ vars.DORY_TAILSCALE_EXIT_NODE }}
run: |
scripts/direct-dmg-install-gate.sh \
--dmg "${{ steps.build.outputs.dmg }}" \
--sbom "release-build/Dory-${{ steps.ver.outputs.version }}.cdx.json" \
--release-manifest release-build/release-manifest.json \
--version "${{ steps.ver.outputs.version }}" \
--build "${{ github.run_number }}" \
--source-commit "$GITHUB_SHA" \
--workroot "$RUNNER_TEMP/dory-release-direct-dmg" \
--confirm CLEAN-RELEASE-USER-DMG-INSTALL
- name: Exercise the exact Sparkle update candidate on a clean physical Mac
timeout-minutes: 20
env:
DORY_RELEASE_CLEAN_USER: '1'
run: |
scripts/sparkle-install-relaunch-gate.sh \
--candidate-app "${{ steps.sparkle_candidate.outputs.app }}" \
--update-zip "${{ steps.build.outputs.app_update }}" \
--appcast release-build/appcast.xml \
--release-manifest release-build/release-manifest.json \
--sbom "release-build/Dory-${{ steps.ver.outputs.version }}.cdx.json" \
--sparkle-source "${{ steps.sparkle_source.outputs.path }}" \
--version "${{ steps.ver.outputs.version }}" \
--build "${{ github.run_number }}" \
--source-commit "$GITHUB_SHA" \
--signing-identity "Developer ID Application" \
--workroot "$RUNNER_TEMP/dory-release-live-sparkle" \
--confirm CLEAN-RELEASE-USER-SPARKLE-INSTALL
- name: Interrupt a signed app/component update and prove automatic last-good rollback
timeout-minutes: 45
env:
DORY_RELEASE_CLEAN_USER: '1'
DORY_SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY || secrets.SPARKLE_ED_PRIVATE_KEY }}
DORY_RELEASE_FIXTURE_IMAGE: ${{ vars.DORY_RELEASE_ALPINE_IMAGE }}
run: |
set -euo pipefail
sign_update="$(find "$HOME/Library/Developer/Xcode/DerivedData" \
-path '*/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update' \
-type f -perm -111 -print | sort | tail -n 1)"
test -x "$sign_update"
scripts/interrupted-upgrade-rollback-gate.sh \
--candidate-app "${{ steps.sparkle_candidate.outputs.app }}" \
--sign-update "$sign_update" \
--version "${{ steps.ver.outputs.version }}" \
--build "${{ github.run_number }}" \
--source-commit "$GITHUB_SHA" \
--fixture-image "$DORY_RELEASE_FIXTURE_IMAGE" \
--signing-identity "Developer ID Application" \
--workroot "$RUNNER_TEMP/dory-release-live-transactional-upgrade" \
--confirm CLEAN-RELEASE-USER-INTERRUPTED-UPGRADE
- name: Upload live release-gate evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-live-release-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 30
if-no-files-found: warn
path: |
${{ runner.temp }}/dory-arm64-host-facts.txt
${{ runner.temp }}/dory-release-direct-dmg/evidence
${{ runner.temp }}/dory-release-live-*
- name: Stage immutable public candidate for long qualification
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 30
if-no-files-found: error
compression-level: 0
path: |
release-build/*.zip
release-build/*.dmg
release-build/*.tar.gz
release-build/*.cdx.json
release-build/appcast.xml
release-build/release-manifest.json
homebrew_install_certification:
name: Clean exact-candidate Homebrew install and uninstall
needs: release_candidate
runs-on: [self-hosted, macOS, arm64, dory, release]
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download the immutable candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Install, launch, and uninstall through Homebrew under normal quarantine
env:
DORY_RELEASE_CLEAN_USER: '1'
VERSION: ${{ needs.release_candidate.outputs.version }}
run: |
scripts/homebrew-install-gate.sh \
--candidate-dir release-build \
--version "$VERSION" \
--build "${{ github.run_number }}" \
--source-commit "$GITHUB_SHA" \
--workroot "$RUNNER_TEMP/dory-homebrew-install" \
--confirm CLEAN-RELEASE-USER-HOMEBREW-INSTALL
- name: Retain Homebrew install evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-homebrew-install-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: warn
path: ${{ runner.temp }}/dory-homebrew-install/evidence
release_qualification:
name: Exact candidate 8-hour + 25-hour qualification
needs: [release_candidate, homebrew_install_certification]
runs-on: [self-hosted, macOS, arm64, dory, release]
timeout-minutes: 1800
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download immutable public candidate before the job token expires
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Select the pinned Xcode 26.6 release toolchain
run: |
xcode_app=/Applications/Xcode-26.6.0-Release.Candidate.app
test -x "$xcode_app/Contents/Developer/usr/bin/xcodebuild"
echo "selected: $xcode_app"
echo "DEVELOPER_DIR=$xcode_app/Contents/Developer" >> "$GITHUB_ENV"
- name: Obtain short-lived ECR credentials through GitHub OIDC
uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1
with:
role-to-assume: ${{ vars.DORY_RELEASE_AWS_ROLE_ARN }}
aws-region: ${{ vars.DORY_RELEASE_ECR_REGION }}
role-session-name: DoryReleaseQualification
role-duration-seconds: 21600
- name: Run concurrent release-duration gates and retain runner-local evidence
env:
DORY_SOURCE_GATE_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
DORY_RELEASE_QUALIFICATION_IMAGE: ${{ vars.DORY_RELEASE_ALPINE_IMAGE }}
DORY_RELEASE_SSH_CLIENT_IMAGE: ${{ vars.DORY_RELEASE_SSH_CLIENT_IMAGE }}
DORY_RELEASE_ECR_REGISTRY: ${{ vars.DORY_RELEASE_ECR_REGISTRY }}
DORY_RELEASE_ECR_REPOSITORY: ${{ vars.DORY_RELEASE_ECR_REPOSITORY }}
DORY_RELEASE_ECR_REGION: ${{ vars.DORY_RELEASE_ECR_REGION }}
run: |
scripts/qualify-release-candidate.sh \
--build-dir release-build \
--version "${{ needs.release_candidate.outputs.version }}" \
--build "${{ github.run_number }}" \
--source-commit "${{ github.sha }}" \
--confirm QUALIFY-EXACT-DORY-RELEASE
performance_qualification:
name: Exact candidate isolated and interleaved performance evidence
needs: release_candidate
runs-on: [self-hosted, macOS, arm64, dory, benchmark]
timeout-minutes: 720
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download immutable public candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Run exact-candidate clean-account campaign
env:
DORY_RELEASE_CLEAN_USER: '1'
DORY_RELEASE_BENCHMARK_USER: '1'
run: |
scripts/qualify-release-performance.sh \
--candidate-dir release-build \
--version "${{ needs.release_candidate.outputs.version }}" \
--build "${{ github.run_number }}" \
--source-commit "$GITHUB_SHA" \
--workroot "$RUNNER_TEMP/dory-release-performance" \
--alpine-image "${{ vars.DORY_RELEASE_ALPINE_IMAGE }}" \
--iperf-image "${{ vars.DORY_BENCH_IPERF_IMAGE }}" \
--node-image "${{ vars.DORY_BENCH_NODE_IMAGE }}" \
--postgres-image "${{ vars.DORY_BENCH_POSTGRES_IMAGE }}" \
--redis-image "${{ vars.DORY_BENCH_REDIS_IMAGE }}" \
--ruby-image "${{ vars.DORY_BENCH_RUBY_IMAGE }}" \
--composer-image "${{ vars.DORY_BENCH_COMPOSER_IMAGE }}" \
--curl-image "${{ vars.DORY_BENCH_CURL_IMAGE }}" \
--probe-url "${{ vars.DORY_BENCH_PROBE_URL }}" \
--download-url "${{ vars.DORY_BENCH_DOWNLOAD_URL }}" \
--download-bytes "${{ vars.DORY_BENCH_DOWNLOAD_BYTES }}" \
--confirm CLEAN-BENCHMARK-USER-DELETE-ENGINE-DATA
- name: Retain exact performance evidence for publication
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-performance-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: error
compression-level: 0
path: ${{ runner.temp }}/dory-release-performance/Dory-${{ needs.release_candidate.outputs.version }}-performance-evidence.zip
sonoma_vz_certification:
name: Exact candidate macOS 14 VZ + IPv6 + LAN/Tailscale source certification
needs: release_candidate
runs-on: [self-hosted, macOS, arm64, dory, sonoma, lan]
timeout-minutes: 180
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download immutable public candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Prove physical Apple-silicon Sonoma host
run: |
set -euo pipefail
os_version="$(sw_vers -productVersion)"
test "$(uname -m)" = arm64
test "${os_version%%.*}" = 14
test "$(sysctl -n kern.hv_support)" = 1
test "$(sysctl -in kern.hv_vmm_present 2>/dev/null || printf 0)" != 1
case "$(sysctl -n hw.model)" in VirtualMac*) exit 1 ;; esac
sw_vers | tee "$RUNNER_TEMP/dory-sonoma-host-facts.txt"
uname -a | tee -a "$RUNNER_TEMP/dory-sonoma-host-facts.txt"
sysctl -n hw.model | sed 's/^/hw.model=/' | tee -a "$RUNNER_TEMP/dory-sonoma-host-facts.txt"
- name: Extract and verify exact notarized app
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
run: |
set -euo pipefail
app_root="$RUNNER_TEMP/dory-sonoma-candidate"
rm -rf "$app_root"
mkdir -p "$app_root"
ditto -x -k "release-build/Dory-$VERSION-app-update.zip" "$app_root"
codesign --verify --strict --deep "$app_root/Dory.app"
xcrun stapler validate "$app_root/Dory.app"
echo "SONOMA_APP=$app_root/Dory.app" >> "$GITHUB_ENV"
- name: Prove VZ native IPv6, publication policy, restart, and cleanup
env:
LAN_PEER_SSH: ${{ secrets.DORY_LAN_PEER_SSH }}
LAN_HOST_IPV4: ${{ secrets.DORY_LAN_HOST_IPV4 }}
TAILSCALE_PEER_SSH: ${{ secrets.DORY_TAILSCALE_PEER_SSH }}
TAILSCALE_HOST_IPV4: ${{ secrets.DORY_TAILSCALE_HOST_IPV4 }}
SOURCE_SERVER_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
SSH_CLIENT_IMAGE: ${{ vars.DORY_RELEASE_SSH_CLIENT_IMAGE }}
run: |
set -euo pipefail
test -n "$LAN_PEER_SSH" && test -n "$LAN_HOST_IPV4"
test -n "$TAILSCALE_PEER_SSH" && test -n "$TAILSCALE_HOST_IPV4"
test -n "$SOURCE_SERVER_IMAGE"
test -n "$SSH_CLIENT_IMAGE"
test -n "${SSH_AUTH_SOCK:-}" && test -S "$SSH_AUTH_SOCK"
ssh-add -L >/dev/null
scripts/vz-native-ipv6-gate.sh \
--dory-vmm "$SONOMA_APP/Contents/Helpers/dory-vmm" \
--dory-hv "$SONOMA_APP/Contents/Helpers/dory-hv" \
--gvproxy "$SONOMA_APP/Contents/Helpers/gvproxy" \
--gvproxy-provenance "$SONOMA_APP/Contents/Resources/gvproxy-provenance.txt" \
--payload-inventory "$SONOMA_APP/Contents/Resources/dory-payload-sha256.txt" \
--kernel "$SONOMA_APP/Contents/Resources/dory-hv-kernel-arm64.lzfse" \
--rootfs "$SONOMA_APP/Contents/Resources/dory-engine-rootfs-arm64.ext4.lzfse" \
--docker "$SONOMA_APP/Contents/Helpers/docker" \
--workroot "$RUNNER_TEMP/dory-vz-sonoma" \
--fixture-image "${{ vars.DORY_RELEASE_ALPINE_IMAGE }}" \
--ssh-client-image "$SSH_CLIENT_IMAGE" \
--require-sonoma \
--require-external \
--app "$SONOMA_APP" \
--lan-host-address "$LAN_HOST_IPV4" \
--lan-peer-ssh "$LAN_PEER_SSH" \
--tailscale-host-address "$TAILSCALE_HOST_IPV4" \
--tailscale-peer-ssh "$TAILSCALE_PEER_SSH" \
--source-server-image "$SOURCE_SERVER_IMAGE" \
--source-confirm PHYSICAL-VZ-SOURCE-PRESERVATION
- name: Upload Sonoma certification evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-sonoma-vz-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: warn
path: |
${{ runner.temp }}/dory-sonoma-host-facts.txt
${{ runner.temp }}/dory-vz-sonoma
source_preserving_lan_certification:
name: Exact candidate physical LAN + Tailscale source-IP certification
needs: release_candidate
runs-on: [self-hosted, macOS, arm64, dory, lan]
timeout-minutes: 240
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download immutable public candidate
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Extract and bind exact app/runtime artifacts
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
run: |
set -euo pipefail
root="$RUNNER_TEMP/dory-source-lan-candidate"
rm -rf "$root"
mkdir -p "$root/app" "$root/runtime"
ditto -x -k "release-build/Dory-$VERSION-app-update.zip" "$root/app"
tar -xzf "release-build/dory-engine-$VERSION-arm64.tar.gz" -C "$root/runtime"
codesign --verify --strict --deep "$root/app/Dory.app"
xcrun stapler validate "$root/app/Dory.app"
echo "SOURCE_LAN_APP=$root/app/Dory.app" >> "$GITHUB_ENV"
echo "SOURCE_LAN_RUNTIME=$root/runtime/dory-engine-$VERSION-arm64" >> "$GITHUB_ENV"
- name: Certify exact remote source over physical LAN
env:
PEER_SSH: ${{ secrets.DORY_LAN_PEER_SSH }}
HOST_IPV4: ${{ secrets.DORY_LAN_HOST_IPV4 }}
SERVER_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
run: |
set -euo pipefail
test -n "$PEER_SSH" && test -n "$HOST_IPV4" && test -n "$SERVER_IMAGE"
scripts/source-preserving-lan-gate.sh \
--app "$SOURCE_LAN_APP" \
--runtime "$SOURCE_LAN_RUNTIME" \
--docker "$SOURCE_LAN_APP/Contents/Helpers/docker" \
--host-address "$HOST_IPV4" \
--peer-ssh "$PEER_SSH" \
--mode lan \
--server-image "$SERVER_IMAGE" \
--workroot "$RUNNER_TEMP/dory-source-lan-physical" \
--ssh-option StrictHostKeyChecking=yes \
--confirm PHYSICAL-SOURCE-PRESERVATION
- name: Certify exact remote source over Tailscale
env:
PEER_SSH: ${{ secrets.DORY_TAILSCALE_PEER_SSH }}
HOST_IPV4: ${{ secrets.DORY_TAILSCALE_HOST_IPV4 }}
SERVER_IMAGE: ${{ vars.DORY_SOURCE_GATE_IMAGE }}
run: |
set -euo pipefail
test -n "$PEER_SSH" && test -n "$HOST_IPV4" && test -n "$SERVER_IMAGE"
scripts/source-preserving-lan-gate.sh \
--app "$SOURCE_LAN_APP" \
--runtime "$SOURCE_LAN_RUNTIME" \
--docker "$SOURCE_LAN_APP/Contents/Helpers/docker" \
--host-address "$HOST_IPV4" \
--peer-ssh "$PEER_SSH" \
--mode tailscale \
--server-image "$SERVER_IMAGE" \
--workroot "$RUNNER_TEMP/dory-source-lan-tailscale" \
--ssh-option StrictHostKeyChecking=yes \
--confirm PHYSICAL-SOURCE-PRESERVATION
- name: Upload source-preservation evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-source-lan-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: warn
path: |
${{ runner.temp }}/dory-source-lan-physical/evidence
${{ runner.temp }}/dory-source-lan-tailscale/evidence
homebrew_cask_audit:
name: Strict exact-candidate Homebrew cask audit
needs: release_candidate
runs-on: macos-15
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Download immutable candidate for checksum binding
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Audit the exact staged cask on an isolated compatible macOS host
env:
HOMEBREW_NO_AUTO_UPDATE: '1'
VERSION: ${{ needs.release_candidate.outputs.version }}
ARM64_SHA256: ${{ needs.release_candidate.outputs.sha256 }}
run: |
set -euo pipefail
audit_tap="doryci/release-audit"
evidence="$RUNNER_TEMP/dory-homebrew-audit-evidence"
rm -rf "$evidence"
brew untap --force "$audit_tap" >/dev/null 2>&1 || true
trap 'brew untap --force "$audit_tap" >/dev/null 2>&1 || true' EXIT
brew tap-new "$audit_tap" >/dev/null
audit_root="$(brew --repository "$audit_tap")"
mkdir -p "$audit_root/Casks" "$evidence"
actual_sha="$(shasum -a 256 "release-build/Dory-$VERSION.zip" | awk '{print $1}')"
test "$actual_sha" = "$ARM64_SHA256"
cp Casks/dory.rb "$audit_root/Casks/dory.rb"
sed -i '' -E "s/ version \"[^\"]+\"/ version \"$VERSION\"/" \
"$audit_root/Casks/dory.rb"
sed -i '' -E "s/ sha256 \"[0-9a-f]+\"/ sha256 \"$ARM64_SHA256\"/" \
"$audit_root/Casks/dory.rb"
grep -qF "version \"$VERSION\"" "$audit_root/Casks/dory.rb"
grep -qF "sha256 \"$ARM64_SHA256\"" "$audit_root/Casks/dory.rb"
brew style "$audit_root/Casks/dory.rb" 2>&1 | tee "$evidence/style.log"
brew audit --cask --strict "$audit_tap/dory" 2>&1 \
| tee "$evidence/audit.log"
cp "$audit_root/Casks/dory.rb" "$evidence/dory.rb"
brew --version > "$evidence/brew-version.txt"
xcodebuild -version > "$evidence/xcode-version.txt"
sw_vers > "$evidence/macos-version.txt"
printf 'source_commit=%s\nrun_id=%s\nrun_attempt=%s\nversion=%s\narm64_sha256=%s\nstatus=PASS\n' \
"$GITHUB_SHA" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$VERSION" "$ARM64_SHA256" \
> "$evidence/manifest.txt"
- name: Retain exact-cask audit evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-homebrew-audit-evidence-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: error
path: ${{ runner.temp }}/dory-homebrew-audit-evidence
publish_release:
name: Publish only the exact qualified candidate
needs: [release_candidate, release_qualification, performance_qualification, sonoma_vz_certification, source_preserving_lan_certification, homebrew_cask_audit, homebrew_install_certification]
runs-on: [self-hosted, macOS, arm64, dory, release]
timeout-minutes: 120
permissions:
contents: write
outputs:
version: ${{ needs.release_candidate.outputs.version }}
sha256: ${{ needs.release_candidate.outputs.sha256 }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Download the exact candidate with fresh job credentials
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-release-candidate-${{ github.sha }}-${{ github.run_attempt }}
path: release-build
- name: Download exact-candidate physical sleep/wake evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-live-release-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: live-release-evidence
- name: Download exact-candidate Sonoma VZ evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-sonoma-vz-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: sonoma-vz-evidence
- name: Download exact-candidate LAN/Tailscale source-preservation evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-source-lan-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: source-lan-evidence
- name: Download exact-candidate Homebrew audit evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-homebrew-audit-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: homebrew-audit-evidence
- name: Download exact-candidate Homebrew install evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-homebrew-install-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: homebrew-install-evidence
- name: Download exact-candidate performance evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-performance-evidence-${{ github.sha }}-${{ github.run_attempt }}
path: performance-evidence
- name: Verify performance evidence binding and internal digests
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
BUILD: ${{ github.run_number }}
run: |
set -euo pipefail
archive="performance-evidence/Dory-$VERSION-performance-evidence.zip"
test -s "$archive"
root="$RUNNER_TEMP/dory-performance-publication"
rm -rf "$root"
mkdir -p "$root"
unzip -q "$archive" -d "$root"
evidence="$root/Dory-$VERSION-performance-evidence"
test -s "$evidence/manifest.json"
test -s "$evidence/sha256.txt"
(cd "$evidence" && shasum -a 256 -c sha256.txt)
python3 - "$evidence/manifest.json" release-build/release-manifest.json \
"release-build/Dory-$VERSION-app-update.zip" "release-build/Dory-$VERSION.cdx.json" \
"$VERSION" "$BUILD" "$GITHUB_SHA" <<'PY'
import hashlib, json, pathlib, sys
manifest_path, release_path, update_path, sbom_path, version, build, commit = sys.argv[1:]
manifest = json.loads(pathlib.Path(manifest_path).read_text(encoding="utf-8"))
assert manifest == {**manifest}, "performance manifest must be an object"
assert manifest["schemaVersion"] == 1
assert manifest["kind"] == "dev.dory.performance-qualification"
assert manifest["status"] == "PASS" and manifest["releaseQualifying"] is True
candidate = manifest["candidate"]
assert candidate["version"] == version
assert candidate["build"] == build
assert candidate["sourceCommit"] == commit
def digest(path):
value = hashlib.sha256()
with open(path, "rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
value.update(chunk)
return value.hexdigest()
assert candidate["releaseManifestSHA256"] == digest(release_path)
assert candidate["appUpdateSHA256"] == digest(update_path)
assert candidate["sbomSHA256"] == digest(sbom_path)
sbom = json.loads(pathlib.Path(sbom_path).read_text(encoding="utf-8"))
values = [row["value"] for row in sbom["metadata"]["component"]["properties"]
if row["name"] == "dev.dory.app.tree.sha256"]
assert values == [candidate["appTreeSHA256"]]
assert manifest["campaigns"] == [
"isolated", "user-workflows", "developer-workflows", "registry-npm", "external-network"
]
assert manifest["cleanup"] == "PASS"
PY
- name: Verify Homebrew audit evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
ARM64_SHA256: ${{ needs.release_candidate.outputs.sha256 }}
run: |
python3 - \
homebrew-audit-evidence/manifest.txt \
"$GITHUB_SHA" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" "$VERSION" "$ARM64_SHA256" <<'PY'
import sys
path, commit, run_id, attempt, version, digest = sys.argv[1:7]
values = {}
with open(path, encoding="utf-8") as handle:
for line in handle:
key, separator, value = line.rstrip("\n").partition("=")
if not separator or key in values:
raise SystemExit(f"malformed Homebrew audit evidence: {line!r}")
values[key] = value
expected = {
"source_commit": commit,
"run_id": run_id,
"run_attempt": attempt,
"version": version,
"arm64_sha256": digest,
"status": "PASS",
}
if values != expected:
raise SystemExit(f"Homebrew audit evidence mismatch: {values!r} != {expected!r}")
PY
test -s homebrew-audit-evidence/dory.rb
grep -qF "version \"$VERSION\"" homebrew-audit-evidence/dory.rb
grep -qF "sha256 \"$ARM64_SHA256\"" homebrew-audit-evidence/dory.rb
- name: Verify Homebrew install evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
ARM64_SHA256: ${{ needs.release_candidate.outputs.sha256 }}
run: |
python3 - \
homebrew-install-evidence/manifest.txt \
"$GITHUB_SHA" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" \
"$VERSION" "${{ github.run_number }}" "$ARM64_SHA256" <<'PY'
import sys
path, commit, run_id, attempt, version, build, digest = sys.argv[1:8]
values = {}
with open(path, encoding="utf-8") as handle:
for line in handle:
key, separator, value = line.rstrip("\n").partition("=")
if not separator or key in values:
raise SystemExit(f"malformed Homebrew install evidence: {line!r}")
values[key] = value
expected = {
"source_commit": commit,
"run_id": run_id,
"run_attempt": attempt,
"version": version,
"build": build,
"zip_sha256": digest,
"normal_quarantine": "PASS",
"gatekeeper": "PASS",
"sbom": "PASS",
"first_launch": "PASS",
"data_drive_preserved": "PASS",
"zap_preserved_data": "PASS",
"zap_removed_transient_state": "PASS",
"docker_plugin_restoration": "PASS",
"profile_restoration": "PASS",
"status": "PASS",
}
if values != expected:
raise SystemExit(f"Homebrew install evidence mismatch: {values!r} != {expected!r}")
PY
- name: Verify Sparkle install evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
BUILD: ${{ github.run_number }}
run: |
set -euo pipefail
manifests="$(find live-release-evidence -path '*/dory-release-live-sparkle/*/evidence/manifest.txt' -type f -print)"
test "$(printf '%s\n' "$manifests" | awk 'NF { count++ } END { print count + 0 }')" = 1
manifest="$(printf '%s\n' "$manifests" | awk 'NF { print; exit }')"
candidate_root="$RUNNER_TEMP/dory-sparkle-evidence-candidate"
rm -rf "$candidate_root"
mkdir -p "$candidate_root"
ditto -x -k "release-build/Dory-$VERSION-app-update.zip" "$candidate_root"
candidate_team="$(codesign -dv --verbose=4 "$candidate_root/Dory.app" 2>&1 | sed -n 's/^TeamIdentifier=//p')"
scripts/verify-sparkle-install-evidence.py \
--manifest "$manifest" \
--app-update "release-build/Dory-$VERSION-app-update.zip" \
--appcast release-build/appcast.xml \
--release-manifest release-build/release-manifest.json \
--sbom "release-build/Dory-$VERSION.cdx.json" \
--gate-script scripts/sparkle-install-relaunch-gate.sh \
--package-resolved Dory.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved \
--candidate-team "$candidate_team" \
--source-commit "$GITHUB_SHA" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--version "$VERSION" \
--build "$BUILD"
- name: Verify interrupted transactional-upgrade evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
BUILD: ${{ github.run_number }}
run: |
set -euo pipefail
manifests="$(find live-release-evidence \
-path '*/dory-release-live-transactional-upgrade/*/evidence/manifest.txt' \
-type f -print)"
test "$(printf '%s\n' "$manifests" | awk 'NF { count++ } END { print count + 0 }')" = 1
manifest="$(printf '%s\n' "$manifests" | awk 'NF { print; exit }')"
evidence="$(dirname "$manifest")"
tree_sha="$(python3 - "release-build/Dory-$VERSION.cdx.json" <<'PY'
import json, sys
payload = json.load(open(sys.argv[1], encoding="utf-8"))
rows = payload["metadata"]["component"]["properties"]
values = [row["value"] for row in rows if row["name"] == "dev.dory.app.tree.sha256"]
assert len(values) == 1
print(values[0])
PY
)"
python3 - "$manifest" "$GITHUB_SHA" "$VERSION" "$BUILD" "$tree_sha" <<'PY'
import sys
path, commit, version, build, tree = sys.argv[1:]
values = {}
for line in open(path, encoding="utf-8"):
key, separator, value = line.rstrip("\n").partition("=")
assert separator and key not in values, line
values[key] = value
assert values["status"] == "PASS"
assert values["release_qualifying"] == "true"
assert values["source_commit"] == commit
assert values["candidate_version"] == version
assert values["candidate_build"] == build
assert values["candidate_tree_sha256"] == tree
for key in (
"exact_last_good_app_restored", "signed_component_generation_restored",
"durable_data_not_downgraded", "durable_volume_sentinel_preserved",
"preexisting_container_preserved", "published_port_preserved",
"exact_smoke_failure_retained", "initial_clean_user_state_restored",
):
assert values[key] == "PASS", key
PY
scripts/transactional-upgrade-gate.sh \
--record "$evidence/transaction.json" \
--interruption-evidence "$evidence/interruption-evidence.json" \
--expect-state rolledBack
- name: Verify direct DMG install evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
run: |
set -euo pipefail
manifests="$(find live-release-evidence -path '*/dory-release-direct-dmg/evidence/manifest.txt' -type f -print)"
test "$(printf '%s\n' "$manifests" | awk 'NF { count++ } END { print count + 0 }')" = 1
manifest="$(printf '%s\n' "$manifests" | awk 'NF { print; exit }')"
dmg_sha="$(shasum -a 256 "release-build/Dory-$VERSION.dmg" | awk '{print $1}')"
python3 - "$manifest" "$GITHUB_SHA" "$GITHUB_RUN_ID" "$GITHUB_RUN_ATTEMPT" \
"$VERSION" "${{ github.run_number }}" "$dmg_sha" <<'PY'
import sys
path, commit, run_id, attempt, version, build, digest = sys.argv[1:8]
values = {}
with open(path, encoding="utf-8") as handle:
for line in handle:
key, separator, value = line.rstrip("\n").partition("=")
if not separator or key in values:
raise SystemExit(f"malformed direct DMG evidence: {line!r}")
values[key] = value
expected = {
"source_commit": commit,
"run_id": run_id,
"run_attempt": attempt,
"version": version,
"build": build,
"dmg_sha256": digest,
"normal_quarantine": "PASS",
"gatekeeper": "PASS",
"sbom": "PASS",
"live_smoke": "PASS",
"initial_clean_user_state_restored": "PASS",
"status": "PASS",
}
if values != expected:
raise SystemExit(f"direct DMG evidence mismatch: {values!r} != {expected!r}")
PY
- name: Verify physical sleep/wake evidence binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
run: |
set -euo pipefail
manifests="$(find live-release-evidence -path '*/sleep-wake/*/manifest.txt' -type f -print)"
test "$(printf '%s\n' "$manifests" | awk 'NF { count++ } END { print count + 0 }')" = 1
sleep_manifest="$(printf '%s\n' "$manifests" | awk 'NF { print; exit }')"
sleep_root="$(dirname "$sleep_manifest")"
test -s "$sleep_root/results.tsv"
extracted="$RUNNER_TEMP/dory-sleep-evidence-candidate"
rm -rf "$extracted"
mkdir -p "$extracted"
ditto -x -k "release-build/Dory-$VERSION-app-update.zip" "$extracted"
scripts/verify-sleep-wake-evidence.py \
--manifest "$sleep_manifest" \
--results "$sleep_root/results.tsv" \
--evidence-root "$sleep_root" \
--app "$extracted/Dory.app" \
--source-commit "$GITHUB_SHA" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--cycles 5 \
--auto-wake-seconds 30 \
--custom-dns "${{ vars.DORY_CORPORATE_DNS_SERVER }}" \
--probe-host "${{ vars.DORY_CORPORATE_VPN_PROBE_HOST }}" \
--probe-url "${{ vars.DORY_CORPORATE_VPN_PROBE_URL }}" \
--tailscale-exit-node "${{ vars.DORY_TAILSCALE_EXIT_NODE }}"
- name: Verify durable qualification and candidate digest binding
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
BUILD: ${{ github.run_number }}
SOURCE_COMMIT: ${{ github.sha }}
PRIMARY_SHA256: ${{ needs.release_candidate.outputs.sha256 }}
run: |
set -euo pipefail
qualification="$HOME/.dory-release-qualification/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${GITHUB_SHA}"
complete="$qualification/qualification.complete.json"
scripts/verify-release-qualification.sh \
--build-dir release-build \
--qualification "$qualification" \
--version "$VERSION" \
--build "$BUILD" \
--source-commit "$SOURCE_COMMIT" \
--run-id "$GITHUB_RUN_ID" \
--run-attempt "$GITHUB_RUN_ATTEMPT" \
--primary-sha256 "$PRIMARY_SHA256"
git rev-parse HEAD | grep -qx "$SOURCE_COMMIT"
staged="$RUNNER_TEMP/dory-release-qualification-evidence"
rm -rf "$staged"
mkdir -p "$staged"
cp "$complete" "$staged/"
cp -R "$qualification/evidence" "$staged/"
echo "QUALIFICATION=$qualification" >> "$GITHUB_ENV"
- name: Revalidate notarization and Sparkle after candidate download
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
DORY_SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY || secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
extracted="$RUNNER_TEMP/dory-qualified-publication"
rm -rf "$extracted"
mkdir -p "$extracted"
ditto -x -k "release-build/Dory-$VERSION-app-update.zip" "$extracted"
codesign --verify --strict --deep "$extracted/Dory.app"
codesign -dv --verbose=4 "$extracted/Dory.app" 2>&1 \
| grep -q 'Authority=Developer ID Application'
xcrun stapler validate "$extracted/Dory.app"
spctl --assess --type execute --verbose=4 "$extracted/Dory.app" \
> "$RUNNER_TEMP/dory-publication-gatekeeper.txt" 2>&1
! grep -qi 'assessment system is disabled' "$RUNNER_TEMP/dory-publication-gatekeeper.txt"
grep -q 'source=Notarized Developer ID' "$RUNNER_TEMP/dory-publication-gatekeeper.txt"
scripts/verify-sparkle-update.sh \
"$extracted/Dory.app" \
"release-build/Dory-$VERSION-app-update.zip" \
release-build/appcast.xml
vz_manifest="$(find sonoma-vz-evidence -type f -name manifest.txt -print)"
test "$(printf '%s\n' "$vz_manifest" | awk 'NF { count++ } END { print count + 0 }')" = 1
for proof in status sonoma vz_file_handle_network fresh_boot restart graceful_cleanup \
docker_bridge_ipv6 container_global_ipv6 dns_aaaa registry_aaaa ipv6_tcp_loopback \
wildcard_ipv4_ipv6_loopback explicit_ipv4_loopback unpublish_cleanup \
ssh_agent_forwarding ssh_agent_fresh_boot ssh_agent_restart \
external_ipv6_tcp physical_source_preservation \
lan_tcp_udp_source_preserved tailscale_tcp_udp_source_preserved \
explicit_loopback_remote_isolation interface_specific_privileged_tcp \
source_helper_restart_recovery source_engine_restart_recovery \
source_unpublish_cleanup source_privileged_tcp_unpublish_cleanup \
source_pf_reference_cleanup source_ipv4_forwarding_cleanup \
source_memory_pressure_lan source_memory_pressure_tailscale \
source_dns_pressure source_configd_pressure_liveness \
host_boot_session_unchanged host_panic_report_absence; do
grep -qx "$proof=PASS" "$vz_manifest"
done
grep -qx 'architecture=arm64' "$vz_manifest"
grep -qx 'release_qualifying=true' "$vz_manifest"
grep -qx 'gvproxy_version=v0.8.9-dory1' "$vz_manifest"
gvproxy_sha="$(shasum -a 256 "$extracted/Dory.app/Contents/Helpers/gvproxy" | awk '{print $1}')"
grep -qx "gvproxy_sha256=$gvproxy_sha" "$vz_manifest"
grep -qx 'gvproxy_build_sha256=bd9183f5dbe2bd27d7ea57f2f2dd4d5ce26487eeb1fa8c82cd81bad4df50e0c0' "$vz_manifest"
grep -qx 'verified_sha256=bd9183f5dbe2bd27d7ea57f2f2dd4d5ce26487eeb1fa8c82cd81bad4df50e0c0' \
"$extracted/Dory.app/Contents/Resources/gvproxy-provenance.txt"
helper_sha="$(shasum -a 256 "$extracted/Dory.app/Contents/Helpers/dory-vmm" | awk '{print $1}')"
grep -qx "dory_vmm_sha256=$helper_sha" "$vz_manifest"
app_sha="$(shasum -a 256 "$extracted/Dory.app/Contents/MacOS/Dory" | awk '{print $1}')"
grep -qx "app_executable_sha256=$app_sha" "$vz_manifest"
grep -Eq '^fixture_image=.+@sha256:[0-9a-f]{64}$' "$vz_manifest"
grep -Eq '^ssh_client_image=.+@sha256:[0-9a-f]{64}$' "$vz_manifest"
grep -Eq '^source_server_image=.+@sha256:[0-9a-f]{64}$' "$vz_manifest"
grep -qx 'source_memory_pressure_mib=960' "$vz_manifest"
grep -qx 'source_memory_pressure_rounds=10' "$vz_manifest"
vz_boot_before="$(awk -F= '$1 == "host_boot_epoch_before" {print $2; exit}' "$vz_manifest")"
vz_boot_after="$(awk -F= '$1 == "host_boot_epoch_after" {print $2; exit}' "$vz_manifest")"
printf '%s\n' "$vz_boot_before" | grep -Eq '^[0-9]+$'
test "$vz_boot_after" = "$vz_boot_before"
test ! -s "$(dirname "$vz_manifest")/new-host-panic-reports.txt"
lan_manifests="$(find source-lan-evidence -type f -name manifest.txt -print)"
test "$(printf '%s\n' "$lan_manifests" | awk 'NF { count++ } END { print count + 0 }')" = 2
hv_sha="$(shasum -a 256 "$extracted/Dory.app/Contents/Helpers/dory-hv" | awk '{print $1}')"
for mode in lan tailscale; do
manifest="$(printf '%s\n' "$lan_manifests" | while IFS= read -r candidate; do grep -qx "mode=$mode" "$candidate" && printf '%s\n' "$candidate"; done)"
test "$(printf '%s\n' "$manifest" | awk 'NF { count++ } END { print count + 0 }')" = 1
for proof in status tcp_source_preserved udp_source_preserved explicit_loopback_isolated \
interface_specific_privileged_tcp \
helper_restart_recovery engine_restart_recovery tcp_unpublish_cleanup \
udp_unpublish_cleanup privileged_tcp_unpublish_cleanup \
pf_cleanup route_cleanup pf_reference_cleanup \
ipv4_forwarding_cleanup memory_pressure_source_preserved \
docker_dns_pressure configd_pressure_liveness \
host_boot_session_unchanged host_panic_report_absence; do
grep -qx "$proof=PASS" "$manifest"
done
grep -qx 'architecture=arm64' "$manifest"
grep -qx 'release_qualifying=true' "$manifest"
grep -qx 'peer_transport=ssh' "$manifest"
grep -qx "app_executable_sha256=$app_sha" "$manifest"
grep -qx "dory_hv_sha256=$hv_sha" "$manifest"
grep -qx "gvproxy_sha256=$gvproxy_sha" "$manifest"
grep -qx 'gvproxy_build_sha256=bd9183f5dbe2bd27d7ea57f2f2dd4d5ce26487eeb1fa8c82cd81bad4df50e0c0' "$manifest"
grep -Eq '^observed_source_ipv4=([0-9]{1,3}\.){3}[0-9]{1,3}$' "$manifest"
grep -Eq '^server_image=.+@sha256:[0-9a-f]{64}$' "$manifest"
grep -qx 'memory_pressure_mib=960' "$manifest"
grep -qx 'memory_pressure_rounds=20' "$manifest"
boot_before="$(awk -F= '$1 == "host_boot_epoch_before" {print $2; exit}' "$manifest")"
boot_after="$(awk -F= '$1 == "host_boot_epoch_after" {print $2; exit}' "$manifest")"
printf '%s\n' "$boot_before" | grep -Eq '^[0-9]+$'
test "$boot_after" = "$boot_before"
test ! -s "$(dirname "$manifest")/new-host-panic-reports.txt"
done
- name: Upload final qualification evidence
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-release-qualification-${{ github.sha }}-${{ github.run_attempt }}
retention-days: 90
if-no-files-found: error
path: ${{ runner.temp }}/dory-release-qualification-evidence
- name: Package stable reliability evidence
env:
VERSION: ${{ needs.release_candidate.outputs.version }}
BUILD: ${{ github.run_number }}
SOURCE_COMMIT: ${{ github.sha }}
PRIMARY_SHA256: ${{ needs.release_candidate.outputs.sha256 }}
run: |
set -euo pipefail
source="$RUNNER_TEMP/dory-release-qualification-evidence"
root="$RUNNER_TEMP/Dory-$VERSION-reliability-evidence"
archive="$RUNNER_TEMP/Dory-$VERSION-reliability-evidence.zip"
test -s "$source/qualification.complete.json"
test -d "$source/evidence"
rm -rf "$root" "$archive" "$archive.sha256"
mkdir -p "$root"
cp "$source/qualification.complete.json" "$root/"
cp -R "$source/evidence" "$root/"
{
printf 'source_commit=%s\n' "$SOURCE_COMMIT"
printf 'run_id=%s\n' "$GITHUB_RUN_ID"
printf 'run_attempt=%s\n' "$GITHUB_RUN_ATTEMPT"
printf 'version=%s\n' "$VERSION"
printf 'build=%s\n' "$BUILD"
printf 'primary_archive_sha256=%s\n' "$PRIMARY_SHA256"
printf 'status=PASS\n'
} > "$root/candidate-binding.txt"
(
cd "$root"
find . -type f ! -name sha256.txt -print \
| sed 's#^\./##' \
| LC_ALL=C sort \
| while IFS= read -r relative; do
digest="$(shasum -a 256 "$relative" | awk '{print $1}')"
printf '%s %s\n' "$digest" "$relative"
done > sha256.txt
while IFS=' ' read -r digest relative; do
test "$(shasum -a 256 "$relative" | awk '{print $1}')" = "$digest"
done < sha256.txt
)
(
cd "$RUNNER_TEMP"
COPYFILE_DISABLE=1 zip -X -q -r "$(basename "$archive")" "$(basename "$root")"
)
(
cd "$(dirname "$archive")"
shasum -a 256 "$(basename "$archive")" > "$(basename "$archive").sha256"
shasum -a 256 -c "$(basename "$archive").sha256"
)
- name: Upload generated appcast
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: dory-appcast
path: release-build/appcast.xml
if-no-files-found: error
- name: Publish GitHub Release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
tag_name: v${{ needs.release_candidate.outputs.version }}
name: Dory ${{ needs.release_candidate.outputs.version }}
files: |
release-build/Dory-${{ needs.release_candidate.outputs.version }}-arm64.zip
release-build/Dory-${{ needs.release_candidate.outputs.version }}.zip
release-build/Dory-${{ needs.release_candidate.outputs.version }}-arm64.dmg
release-build/Dory-${{ needs.release_candidate.outputs.version }}.dmg
release-build/Dory-${{ needs.release_candidate.outputs.version }}-app-update.zip
release-build/dory-engine-${{ needs.release_candidate.outputs.version }}-arm64.tar.gz
release-build/Dory-${{ needs.release_candidate.outputs.version }}.cdx.json
release-build/release-manifest.json
release-build/appcast.xml
performance-evidence/Dory-${{ needs.release_candidate.outputs.version }}-performance-evidence.zip
${{ runner.temp }}/Dory-${{ needs.release_candidate.outputs.version }}-reliability-evidence.zip
${{ runner.temp }}/Dory-${{ needs.release_candidate.outputs.version }}-reliability-evidence.zip.sha256
fail_on_unmatched_files: true
generate_release_notes: true
body: |
Native Docker & Linux containers for Apple Silicon, a free, open-source alternative to
OrbStack and Docker Desktop. Intel support is planned after the Apple Silicon production
contract is complete.
**0.4 trust release**
- Supported dedicated-VM agent sandboxes with enforced egress, scoped mounts and
credentials, non-root execution, resource caps, TTL cleanup, and rollback.
- Reason-coded staged readiness, bounded targeted repairs, incident provenance, and
attributed resource/storage/network diagnostics.
- Guided corporate proxy, registry CA, split-DNS, VPN, and route reconciliation.
- Transactional Sparkle/component upgrades with next-launch smoke tests, automatic
last-known-good rollback, and an export route when durable schema rollback is unsafe.
- Build Activity with durable Dory-launched build history, logs, cache visibility, and
cancellation; exact-selection transactional migration with completeness evidence.
- Verified scheduled local machine recovery bundles with retention and periodic
disposable boot proof, isolated from manual snapshots.
- Explicit-scope confirmation or recoverable undo for destructive UI, keyboard, menu,
CLI, migration, cleanup, component, and missing-drive paths.
- Exact-candidate physical, duration, compatibility, migration, update, security, and
performance evidence bound to the shipped manifest and SBOM.
**Apple Silicon downloads**
| Asset | What it is |
|---|---|
| `Dory-${{ needs.release_candidate.outputs.version }}-arm64.dmg` / `.zip` | Full app optimized for Apple silicon |
| `Dory-${{ needs.release_candidate.outputs.version }}.dmg` / `.zip` | Compatibility alias for the arm64 build |
| `dory-engine-${{ needs.release_candidate.outputs.version }}-arm64.tar.gz` | Headless engine runtime, no GUI — `./dory-engine start`, then `docker context use dory-engine` |
| `Dory-${{ needs.release_candidate.outputs.version }}.cdx.json` | CycloneDX 1.6 SBOM for the exact shipped app tree |
| `Dory-${{ needs.release_candidate.outputs.version }}-performance-evidence.zip` | Raw isolated/interleaved benchmark data, provenance, correctness evidence, and generated summaries |
| `Dory-${{ needs.release_candidate.outputs.version }}-reliability-evidence.zip` | Candidate-bound eight-hour resource/file/API and 25-hour unchanged-connection qualification records |
**Install**
```sh
brew install --cask Augani/dory/dory
```
…or download the matching `.dmg` below and drag Dory to Applications.
Dory.app and its built-in engine run on macOS 14 Sonoma or later. Sonoma uses the
bundled Virtualization.framework `dory-vmm` tier; supported macOS 15+ hosts use
Dory's raw `dory-hv` tier. An existing Docker-compatible engine remains selectable
from Settings → Engine Backend.
```
arm64 zip sha256: ${{ needs.release_candidate.outputs.sha256 }}
```
Read next: `README.md`, `CHANGELOG.md`, `COMPATIBILITY.md`, and the documentation at
https://augani.github.io/dory/docs/.
- name: Remove runner-local qualification state after successful publication
run: rm -rf "$QUALIFICATION"
publish-pages:
name: Publish generated appcast to the live Sparkle feed
needs: publish_release
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Download the appcast generated from the signed update ZIP
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: dory-appcast
path: appcast-artifact
- name: Validate generated live feed input
run: |
test -s appcast-artifact/appcast.xml
python3 - appcast-artifact/appcast.xml "${{ needs.publish_release.outputs.version }}" <<'PY'
import os
import sys
import urllib.parse
import xml.etree.ElementTree as ET
path, version = sys.argv[1:3]
sparkle = "http://www.andymatuschak.org/xml-namespaces/sparkle"
dory = "https://augani.github.io/dory/appcast"
item = ET.parse(path).getroot().find("./channel/item")
assert item is not None, "generated appcast has no current item"
assert item.findtext(f"{{{sparkle}}}shortVersionString") == version, "generated appcast version mismatch"
assert item.findtext(f"{{{sparkle}}}minimumSystemVersion") == "14.0", "generated appcast macOS floor mismatch"
assert item.findtext(f"{{{dory}}}dataSchemaVersion") == "1", "generated appcast data schema mismatch"
assert item.findtext(f"{{{dory}}}minimumReadableDataSchema") == "1", "generated appcast minimum readable schema mismatch"
assert item.findtext(f"{{{dory}}}maximumReadableDataSchema") == "1", "generated appcast maximum readable schema mismatch"
assert item.findtext(f"{{{dory}}}componentCatalogSchema") == "1", "generated appcast component schema mismatch"
enclosure = item.find("enclosure")
assert enclosure is not None, "generated appcast has no enclosure"
name = os.path.basename(urllib.parse.urlparse(enclosure.attrib["url"]).path)
assert name == f"Dory-{version}-app-update.zip", f"generated appcast points at {name}"
PY
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
cache-dependency-path: website/package-lock.json
- run: npm ci
working-directory: website
- run: npm run build
working-directory: website
- name: Overlay the exact release appcast onto the complete site
run: |
test -d docs-build
cp appcast-artifact/appcast.xml docs-build/appcast.xml
cmp appcast-artifact/appcast.xml docs-build/appcast.xml
- uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5
- uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: docs-build
- id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
- name: Verify the actual SUFeedURL serves this release
run: |
expected='<sparkle:shortVersionString>${{ needs.publish_release.outputs.version }}</sparkle:shortVersionString>'
for attempt in $(seq 1 18); do
if curl -fsSL --connect-timeout 10 --max-time 30 \
"https://augani.github.io/dory/appcast.xml?release=${{ needs.publish_release.outputs.version }}&run=${{ github.run_id }}" \
| grep -qF "$expected"; then
echo "Live Sparkle feed serves ${{ needs.publish_release.outputs.version }}"
exit 0
fi
[ "$attempt" -eq 18 ] || sleep 5
done
echo "Live SUFeedURL did not converge to ${{ needs.publish_release.outputs.version }}" >&2
exit 1
# Keeps the Homebrew cask in this repo (the tap) current after every release.
bump-cask:
needs: [publish_release, publish-pages]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
ref: main
- name: Bump cask to the released version
run: |
V="${{ needs.publish_release.outputs.version }}"
S="${{ needs.publish_release.outputs.sha256 }}"
sed -i -E "s/ version \"[^\"]+\"/ version \"$V\"/" Casks/dory.rb
sed -i -E "s/ sha256 \"[0-9a-f]+\"/ sha256 \"$S\"/" Casks/dory.rb
grep -qF "version \"$V\"" Casks/dory.rb \
|| { echo "cask version was not bumped to $V" >&2; exit 1; }
grep -qF "sha256 \"$S\"" Casks/dory.rb \
|| { echo "cask sha256 was not bumped for $V" >&2; exit 1; }
git add Casks/dory.rb
if git diff --cached --quiet; then echo "cask already current"; exit 0; fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore: bump Homebrew cask to v$V"
git push
# Sync the same cask to the Augani/homebrew-dory tap so `brew install --cask Augani/dory/dory`
# picks it up. The private deploy key is repository-scoped to the tap; no personal or
# organization-wide token is copied into Actions. Publication fails if push proof or remote
# convergence fails.
- name: Sync cask to the homebrew-dory tap
env:
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
run: |
if [ -z "$HOMEBREW_TAP_DEPLOY_KEY" ]; then
echo "HOMEBREW_TAP_DEPLOY_KEY is required because release notes advertise Augani/dory/dory" >&2
exit 1
fi
V="${{ needs.publish_release.outputs.version }}"
S="${{ needs.publish_release.outputs.sha256 }}"
ssh_dir="$RUNNER_TEMP/homebrew-tap-ssh"
install -d -m 0700 "$ssh_dir"
printf '%s\n' "$HOMEBREW_TAP_DEPLOY_KEY" > "$ssh_dir/key"
chmod 0600 "$ssh_dir/key"
curl -fsSL --retry 3 --connect-timeout 15 --max-time 60 \
https://api.github.com/meta -o "$ssh_dir/github-meta.json"
python3 - "$ssh_dir/github-meta.json" "$ssh_dir/known_hosts" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as handle:
keys = json.load(handle).get("ssh_keys", [])
assert keys and all(key.startswith("ssh-") for key in keys), "GitHub SSH metadata is missing"
with open(sys.argv[2], "w", encoding="utf-8") as handle:
for key in keys:
handle.write(f"github.com {key}\n")
PY
export GIT_SSH_COMMAND="ssh -i $ssh_dir/key -o IdentitiesOnly=yes -o UserKnownHostsFile=$ssh_dir/known_hosts -o StrictHostKeyChecking=yes"
git clone --depth 1 git@github.com:Augani/homebrew-dory.git tap
cp Casks/dory.rb tap/Casks/dory.rb
cd tap
if git diff --quiet; then
echo "tap already current"
else
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "chore: update Dory cask to v$V"
git push
fi
for attempt in $(seq 1 12); do
remote="$(curl -fsSL --connect-timeout 10 --max-time 30 \
"https://raw.githubusercontent.com/Augani/homebrew-dory/main/Casks/dory.rb?release=$V&attempt=$attempt" || true)"
if grep -qF "version \"$V\"" <<< "$remote" \
&& grep -qF "sha256 \"$S\"" <<< "$remote"; then
echo "homebrew-dory serves $V"
exit 0
fi
[ "$attempt" -eq 12 ] || sleep 5
done
echo "homebrew-dory did not converge to $V" >&2
exit 1