diff --git a/.github/workflows/pr-fast-ci.yml b/.github/workflows/pr-fast-ci.yml index 9127082..22435b5 100644 --- a/.github/workflows/pr-fast-ci.yml +++ b/.github/workflows/pr-fast-ci.yml @@ -89,8 +89,14 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - name: Run native app Swift tests - run: swift test --package-path native-app + - name: Run native app xcodebuild tests + working-directory: native-app + run: >- + xcodebuild + -scheme APW-Package + -destination 'platform=macOS' + -derivedDataPath .xcode-derived + test validate-secrets: name: Validate Secrets diff --git a/docs/bootstrap/onboarding.md b/docs/bootstrap/onboarding.md index 7ad2536..2b87a74 100644 --- a/docs/bootstrap/onboarding.md +++ b/docs/bootstrap/onboarding.md @@ -41,6 +41,7 @@ - Docker, service-container, browser, and `container:` workloads stay on GitHub-hosted runners. - Keep PR checks cheap. Add heavy validation to `scripts/ci/run-extended-validation.sh` instead of the PR lane. - APW extended validation requires both Rust (`cargo`) and the macOS Swift toolchain, so the `extended-checks` job must run on the org macOS self-hosted pool (`[self-hosted, private, macOS, ARM64, xcode]`) rather than the Synology shell-only pool. + - Extended validation runs `scripts/ci/run-native-app-preflight.sh`, which exercises the Swift package through `xcodebuild`, builds `APW.app`, verifies codesign, and confirms the associated-domain entitlement is embedded. - Rust builds OpenSSL through the `openssl` crate's vendored feature, so the macOS runner needs source-build tools (`cc`, `make`, and `perl`) but does not require Homebrew, pkg-config, or a system OpenSSL prefix. ## Release Prep diff --git a/native-app/APW.entitlements b/native-app/APW.entitlements new file mode 100644 index 0000000..ed7f22a --- /dev/null +++ b/native-app/APW.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + webcredentials:example.com + + + diff --git a/scripts/build-native-app.sh b/scripts/build-native-app.sh index d88ebff..337a8ad 100755 --- a/scripts/build-native-app.sh +++ b/scripts/build-native-app.sh @@ -12,6 +12,7 @@ MACOS_DIR="$CONTENTS_DIR/MacOS" RESOURCES_DIR="$CONTENTS_DIR/Resources" PLIST_PATH="$CONTENTS_DIR/Info.plist" EXECUTABLE_PATH="$PACKAGE_DIR/.build/release/$EXECUTABLE_NAME" +ENTITLEMENTS_PATH="$PACKAGE_DIR/APW.entitlements" VERSION="$(awk -F ' = ' '$1 == "version" { gsub(/"/, "", $2); print $2; exit }' "$ROOT_DIR/rust/Cargo.toml")" if [[ -z "$VERSION" ]]; then @@ -59,7 +60,7 @@ cat >"$PLIST_PATH" </dev/null 2>&1; then - if ! codesign -s - --force --deep "$APP_DIR" 2>/dev/null; then + if ! codesign -s - --force --deep --entitlements "$ENTITLEMENTS_PATH" "$APP_DIR" 2>/dev/null; then echo "Warning: ad-hoc code signing failed for $APP_DIR. The bundle may be rejected by Gatekeeper." >&2 fi fi diff --git a/scripts/ci/run-extended-validation.sh b/scripts/ci/run-extended-validation.sh index 31aae44..88b16b0 100755 --- a/scripts/ci/run-extended-validation.sh +++ b/scripts/ci/run-extended-validation.sh @@ -50,6 +50,7 @@ fi require_tool cargo require_tool swift +require_tool xcodebuild run_step "Rust native app end-to-end tests" \ cargo test --manifest-path rust/Cargo.toml --test native_app_e2e @@ -60,8 +61,8 @@ run_step "Rust security regression tests" \ run_step "Rust clippy" \ cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings -run_step "Swift native app release build" \ - ./scripts/build-native-app.sh +run_step "Native app xcodebuild, signing, and entitlement preflight" \ + bash scripts/ci/run-native-app-preflight.sh echo echo "APW extended validation passed." diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh index 46dacc7..557bb12 100755 --- a/scripts/ci/run-fast-checks.sh +++ b/scripts/ci/run-fast-checks.sh @@ -42,5 +42,6 @@ done < <(find .github/scripts scripts -type f -name '*.sh' -print0) ./scripts/test-render-homebrew-formula.sh ./scripts/test-extended-validation-config.sh +./scripts/test-native-app-preflight-config.sh echo "APW fast checks passed." diff --git a/scripts/ci/run-native-app-preflight.sh b/scripts/ci/run-native-app-preflight.sh new file mode 100755 index 0000000..19a5cf2 --- /dev/null +++ b/scripts/ci/run-native-app-preflight.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT_DIR" + +require_tool() { + local tool="$1" + if ! command -v "$tool" >/dev/null 2>&1; then + echo "Missing required tool: $tool" >&2 + exit 1 + fi +} + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Native app preflight requires macOS." >&2 + exit 1 +fi + +require_tool xcodebuild +require_tool swift +require_tool codesign +require_tool plutil + +PLIST_BUDDY="/usr/libexec/PlistBuddy" +if [[ ! -x "$PLIST_BUDDY" ]]; then + echo "Missing required tool: $PLIST_BUDDY" >&2 + exit 1 +fi + +( + cd native-app + xcodebuild \ + -scheme APW-Package \ + -destination 'platform=macOS' \ + -derivedDataPath .xcode-derived \ + test +) + +./scripts/build-native-app.sh >/dev/null + +app_dir="native-app/dist/APW.app" +source_entitlements="native-app/APW.entitlements" +entitlements_file="$(mktemp "${TMPDIR:-/tmp}/apw-entitlements.XXXXXX")" +expected_domains_file="$(mktemp "${TMPDIR:-/tmp}/apw-expected-domains.XXXXXX")" +actual_domains_file="$(mktemp "${TMPDIR:-/tmp}/apw-actual-domains.XXXXXX")" +trap 'rm -f "$entitlements_file" "$expected_domains_file" "$actual_domains_file"' EXIT + +codesign --verify --deep --strict --verbose=2 "$app_dir" +codesign -d --entitlements :- "$app_dir" >"$entitlements_file" 2>/dev/null + +if ! plutil -lint "$source_entitlements" >/dev/null; then + echo "APW source entitlements are not valid plist output." >&2 + exit 1 +fi + +if ! plutil -lint "$entitlements_file" >/dev/null; then + echo "APW.app entitlements are not valid plist output." >&2 + exit 1 +fi + +"$PLIST_BUDDY" -c "Print :com.apple.developer.associated-domains" "$source_entitlements" >"$expected_domains_file" +"$PLIST_BUDDY" -c "Print :com.apple.developer.associated-domains" "$entitlements_file" >"$actual_domains_file" + +if ! cmp -s "$expected_domains_file" "$actual_domains_file"; then + echo "APW.app associated-domain entitlements differ from native-app/APW.entitlements." >&2 + diff -u "$expected_domains_file" "$actual_domains_file" >&2 || true + exit 1 +fi + +echo "Native app xcodebuild, codesign, and entitlement preflight passed." diff --git a/scripts/test-native-app-preflight-config.sh b/scripts/test-native-app-preflight-config.sh new file mode 100755 index 0000000..6c8704b --- /dev/null +++ b/scripts/test-native-app-preflight-config.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT="scripts/ci/run-native-app-preflight.sh" + +require_line() { + local pattern="$1" + local message="$2" + if ! grep -Fq "$pattern" "$SCRIPT"; then + echo "$message" >&2 + exit 1 + fi +} + +require_line "xcodebuild" "Native app preflight must run xcodebuild tests." +require_line "codesign --verify --deep --strict" "Native app preflight must verify the signed app bundle." +require_line "codesign -d --entitlements :-" "Native app preflight must extract embedded entitlements." +require_line "Print :com.apple.developer.associated-domains" "Native app preflight must compare associated-domain entitlements." +require_line "cmp -s \"\$expected_domains_file\" \"\$actual_domains_file\"" "Native app preflight must fail on entitlement drift." + +echo "Native app preflight contract test passed."