#!/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"
|