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."