#!/bin/bash # Round01.5 Phase 1/2 local QA: # - Entitlement model unit tests # - App target build and authorization report field smoke test # - Fixture state coverage # - Release manifest template validity # - Formal release script refuses missing Developer ID / notarization config set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" OUTPUT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" SOURCE_ROOT="$OUTPUT_ROOT/C1.source" # shellcheck source=build-output-paths.sh source "$SCRIPT_DIR/build-output-paths.sh" APP="$BUILD_CURRENT_APP" REPORT="$BUILD_REPORT_ROOT/round015-entitlement-fixture-qa.json" AUTHORIZATION_REPORT="$BUILD_REPORT_ROOT/round015-authorization-report.json" SWIFT_TEST_LOG="$BUILD_REPORT_ROOT/round015-entitlement-swift-test.log" SWIFT_BUILD_LOG="$BUILD_REPORT_ROOT/round015-swift-build.log" RELEASE_MISSING_LOG="$BUILD_REPORT_ROOT/round015-release-missing-config.log" RELEASE_CONFIG_LOG="$BUILD_REPORT_ROOT/round015-release-config.log" MANIFEST_LOG="$BUILD_REPORT_ROOT/round015-generate-release-manifest.log" REPORT_WAIT="${ALIGNER_ROUND015_REPORT_WAIT:-12.0}" APP_PID="" fail() { echo "round015-entitlement-fixture-qa.sh failed: $*" >&2 exit 1 } aligner_pids_for_current_app() { ps -axo pid=,args= | while read -r pid command; do if [[ "$command" == "$APP/Contents/MacOS/Aligner"* ]]; then echo "$pid" fi done } stop_current_aligner() { for pid in $(aligner_pids_for_current_app); do kill "$pid" 2>/dev/null || true done for _ in {1..30}; do [ -z "$(aligner_pids_for_current_app)" ] && return sleep 0.1 done fail "current Aligner app did not exit before QA" } cleanup() { if [ -n "${APP_PID:-}" ]; then kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true APP_PID="" fi stop_current_aligner } trap cleanup EXIT run_swift_tests() { ( cd "$SOURCE_ROOT" swift test --filter EntitlementPolicyTests ) >"$SWIFT_TEST_LOG" 2>&1 || { head -c 6000 "$SWIFT_TEST_LOG" >&2 fail "EntitlementPolicyTests failed" } } run_app_build() { ( cd "$SOURCE_ROOT" swift build -c debug ) >"$SWIFT_BUILD_LOG" 2>&1 || { head -c 6000 "$SWIFT_BUILD_LOG" >&2 fail "swift build failed" } } validate_manifest_templates() { /usr/bin/python3 -m json.tool "$SCRIPT_DIR/release-manifest-template.json" >/dev/null /usr/bin/python3 -m json.tool "$SCRIPT_DIR/public-latest-manifest-template.json" >/dev/null } validate_release_guardrails() { if ( unset ALIGNER_DEVELOPER_IDENTITY ALIGNER_NOTARY_PROFILE ALIGNER_RELEASE_GITHUB_REPOSITORY "$SCRIPT_DIR/release-app.sh" --check-config ) >"$RELEASE_MISSING_LOG" 2>&1; then fail "release-app.sh must reject missing Developer ID config" fi rg -q "ALIGNER_DEVELOPER_IDENTITY" "$RELEASE_MISSING_LOG" \ || fail "release missing-config log must mention ALIGNER_DEVELOPER_IDENTITY" ALIGNER_DEVELOPER_IDENTITY="Developer ID Application: Fixture (TEAMID)" \ ALIGNER_NOTARY_PROFILE="fixture-profile" \ ALIGNER_RELEASE_GITHUB_REPOSITORY="fixture/aligner" \ "$SCRIPT_DIR/release-app.sh" --check-config >"$RELEASE_CONFIG_LOG" 2>&1 \ || { head -c 6000 "$RELEASE_CONFIG_LOG" >&2 fail "release-app.sh --check-config should accept complete fixture config" } } release_dir_for_current_version() { local version local build version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$SOURCE_ROOT/Resources/Aligner-Info.plist" 2>/dev/null)" \ || fail "unable to read CFBundleShortVersionString" build="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$SOURCE_ROOT/Resources/Aligner-Info.plist" 2>/dev/null)" \ || fail "unable to read CFBundleVersion" printf '%s/Aligner-%s-build%s\n' "$BUILD_ROOT" "$version" "$build" } validate_manifest_generation() { local release_dir release_dir="$(release_dir_for_current_version)" "$SCRIPT_DIR/generate-release-manifest.sh" "$release_dir" >"$MANIFEST_LOG" 2>&1 || { head -c 6000 "$MANIFEST_LOG" >&2 fail "generate-release-manifest.sh failed" } /usr/bin/python3 - "$release_dir/release-manifest.json" "$release_dir/public-latest.json" <<'PY' import json import pathlib import sys internal_path = pathlib.Path(sys.argv[1]) public_path = pathlib.Path(sys.argv[2]) internal = json.loads(internal_path.read_text(encoding="utf-8")) public = json.loads(public_path.read_text(encoding="utf-8")) def fail(message): print(json.dumps({"internal": internal, "public": public}, indent=2, ensure_ascii=False), file=sys.stderr) print(message, file=sys.stderr) sys.exit(1) if internal.get("product") != "Aligner" or public.get("product") != "Aligner": fail("manifest product must be Aligner") if internal.get("version") != public.get("version"): fail("internal/public version must match") if internal.get("build") != public.get("build"): fail("internal/public build must match") expected_tag = f"v{internal.get('version')}-build{internal.get('build')}" if internal.get("expectedGitTag") != expected_tag: fail("internal expectedGitTag must match version/build") if internal.get("gitTag") not in ("", expected_tag): fail("internal gitTag must be empty or match expectedGitTag") if internal.get("dmgSHA256") != public.get("sha256"): fail("internal/public SHA must match") if public.get("notarized") is not False: fail("public manifest must not claim notarized without explicit release env") if public.get("downloadURL") != "": fail("public manifest must not invent a download URL") if "license" in json.dumps(internal, ensure_ascii=False).lower(): fail("release manifest must not contain license material") PY } wait_for_authorization_report() { /usr/bin/python3 - "$AUTHORIZATION_REPORT" "$REPORT_WAIT" <<'PY' import json import sys import time path = sys.argv[1] timeout = float(sys.argv[2]) deadline = time.time() + timeout last_report = None while time.time() < deadline: try: with open(path, "r", encoding="utf-8") as file: report = json.load(file) last_report = report auth = report.get("authorization", {}) if auth.get("fixtureState") == "trialActive": sys.exit(0) except FileNotFoundError: pass except json.JSONDecodeError: pass time.sleep(0.1) if last_report is not None: print(json.dumps(last_report, indent=2, ensure_ascii=False), file=sys.stderr) print("authorization report did not become ready", file=sys.stderr) sys.exit(1) PY } assert_authorization_report() { /usr/bin/python3 - "$AUTHORIZATION_REPORT" <<'PY' import json import sys with open(sys.argv[1], "r", encoding="utf-8") as file: report = json.load(file) def fail(message): print(json.dumps(report, indent=2, ensure_ascii=False), file=sys.stderr) print(message, file=sys.stderr) sys.exit(1) auth = report.get("authorization") if not isinstance(auth, dict): fail("authorization report must be an object") if auth.get("fixtureState") != "trialActive": fail("authorization.fixtureState must be trialActive") if auth.get("state") != "trialActive": fail("authorization.state must be trialActive") if auth.get("proFeature") != "windowShortcutActivation": fail("authorization.proFeature must be windowShortcutActivation") if auth.get("proFeatureUnlocked") is not True: fail("trialActive must unlock the Pro feature in report") if auth.get("licenseKeyPresent") is not False: fail("trialActive fixture without debug key must not report licenseKeyPresent") if "fixture-valid-key" in json.dumps(report, ensure_ascii=False): fail("report must not contain fixture plaintext license material") PY } run_authorization_report_smoke() { "$SCRIPT_DIR/package-app.sh" >/dev/null stop_current_aligner rm -f "$AUTHORIZATION_REPORT" "$APP/Contents/MacOS/Aligner" \ --round0-skip-permissions \ --round01-open-quick-switch \ --round01-fixture-app-count=1 \ --round01-fixture-windows-per-app=1 \ --round01-disable-screenshot-refresh \ --round01-quick-switch-report="$AUTHORIZATION_REPORT" \ --round015-license-state=trialActive \ --round015-now=2026-06-24T00:00:00Z & APP_PID=$! wait_for_authorization_report assert_authorization_report kill "$APP_PID" 2>/dev/null || true wait "$APP_PID" 2>/dev/null || true APP_PID="" } write_report() { /usr/bin/python3 - "$REPORT" "$SWIFT_TEST_LOG" "$SWIFT_BUILD_LOG" "$AUTHORIZATION_REPORT" "$RELEASE_MISSING_LOG" "$RELEASE_CONFIG_LOG" <<'PY' import json import os import pathlib import sys from datetime import datetime, timezone report = { "qa": "round015-entitlement-fixture", "createdAt": datetime.now(timezone.utc).isoformat(), "checks": { "entitlementPolicyTests": "passed", "swiftBuild": "passed", "authorizationReportFixture": "passed", "releaseManifestTemplates": "passed", "releaseManifestGeneration": "passed", "releaseGuardRejectsMissingDeveloperID": "passed", "releaseGuardAcceptsCompleteFixtureConfig": "passed", "sensitiveLicenseMaterialInReport": "absent", }, "logs": { "swiftTest": sys.argv[2], "swiftBuild": sys.argv[3], "authorizationReport": sys.argv[4], "releaseMissingConfig": sys.argv[5], "releaseConfig": sys.argv[6], "manifestGeneration": os.environ.get("ROUND015_MANIFEST_LOG", ""), }, } pathlib.Path(sys.argv[1]).write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n") PY } run_swift_tests run_app_build validate_manifest_templates validate_release_guardrails run_authorization_report_smoke validate_manifest_generation ROUND015_MANIFEST_LOG="$MANIFEST_LOG" \ write_report echo "Round01.5 entitlement fixture QA passed" echo "Report: $REPORT"