fix: harden Finder extension signing #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # 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 |