Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions .github/workflows/ios-testflight.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,13 @@ jobs:
PY
)"
export DEVELOPER_DIR="$xcode_path/Contents/Developer"
sudo xcode-select --switch "$DEVELOPER_DIR"
echo "DEVELOPER_DIR=$DEVELOPER_DIR" >> "$GITHUB_ENV"
[[ "$(xcode-select -p)" == "$DEVELOPER_DIR" ]] || { echo '::error::System Xcode selection does not match Xcode 26'; exit 1; }
[[ "$(xcrun --find xcodebuild)" == "$DEVELOPER_DIR/usr/bin/xcodebuild" ]] || { echo '::error::xcrun is not resolving tools from Xcode 26'; exit 1; }
ios_sdk_version="$(xcrun --sdk iphoneos --show-sdk-version)"
ios_sdk_major="${ios_sdk_version%%.*}"
[[ "$ios_sdk_major" =~ ^[0-9]+$ && "$ios_sdk_major" -ge 26 ]] || { echo "::error::Selected iPhoneOS SDK $ios_sdk_version is older than 26"; exit 1; }
node scripts/ios-release/readiness.mjs config > "$RUNNER_TEMP/ios-release-config.json"
xcodebuild -version
major=$(xcodebuild -version | awk '/Xcode/{split($2,v,".");print v[1]}')
Expand Down Expand Up @@ -305,11 +311,28 @@ jobs:
export API_PRIVATE_KEYS_DIR
API_PRIVATE_KEYS_DIR=$(mktemp -d "$RUNNER_TEMP/ios-upload-key.XXXXXX")
key_file="$API_PRIVATE_KEYS_DIR/AuthKey_$ASC_KEY_ID.p8"
trap 'rm -f "$key_file"; rmdir "$API_PRIVATE_KEYS_DIR"' EXIT
validation_log="$RUNNER_TEMP/ios-altool-validation.log"
upload_log="$RUNNER_TEMP/ios-altool-upload.log"
trap 'rm -f "$key_file" "$validation_log" "$upload_log"; rmdir "$API_PRIVATE_KEYS_DIR"' EXIT
printf '%s' "$ASC_PRIVATE_KEY" > "$key_file"
ipa='apps/remote-ios/src-tauri/gen/apple/build/arm64/ORG2 Remote.ipa'
xcrun altool --validate-app -f "$ipa" --type ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
xcrun altool --upload-app -f "$ipa" --type ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"

run_altool() {
local phase="$1"
local log="$2"
shift 2
local status
set +e
xcrun altool "$@" 2>&1 | tee "$log"
status="${PIPESTATUS[0]}"
set -e
node scripts/ios-release/readiness.mjs altool "$phase" "$status" "$log"
}

run_altool validation "$validation_log" \
--validate-app -f "$ipa" --type ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
run_altool upload "$upload_log" \
--upload-app -f "$ipa" --type ios --apiKey "$ASC_KEY_ID" --apiIssuer "$ASC_ISSUER_ID"
echo 'Upload accepted. Processing, export-compliance questions, tester access and review are separate App Store Connect steps.' >> "$GITHUB_STEP_SUMMARY"

- name: Remove iOS signing materials
Expand Down
41 changes: 40 additions & 1 deletion scripts/ios-release/readiness.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { resolve, join } from "node:path";
import { pathToFileURL } from "node:url";

export const bundleId = "org2ai.org2.remote";
export const minimumIosSdkMajor = 26;

export function releaseConfig(version, build, team) {
assert.match(
Expand Down Expand Up @@ -44,6 +45,12 @@ export function validateDistribution(
"Wrong release version",
);
assert.equal(info.CFBundleVersion, build, "Wrong build number");
const sdkMatch = /^iphoneos(\d+)(?:\.\d+)*$/.exec(info.DTSDKName ?? "");
assert.ok(sdkMatch, "Built iPhoneOS SDK metadata missing or invalid");
assert.ok(
Number(sdkMatch[1]) >= minimumIosSdkMajor,
`App must be built with iOS ${minimumIosSdkMajor} SDK or newer`,
);
assert.ok(
info.NSCameraUsageDescription?.trim(),
"Camera purpose string missing",
Expand Down Expand Up @@ -216,6 +223,25 @@ export function validatePrivacyManifest(privacy) {
);
}

export function validateAltoolResult(output, phase, status) {
const successPattern = {
validation: /No errors validating archive|VALIDATION SUCCEEDED/i,
upload: /UPLOAD SUCCEEDED|No errors uploading/i,
}[phase];
assert.ok(successPattern, "Unknown App Store Connect phase");
assert.equal(status, 0, `App Store Connect ${phase} command failed`);
assert.doesNotMatch(
output,
/(^|\s)ERROR:|VERIFY FAILED|UPLOAD FAILED|Failed to validate package|Failed to upload package/i,
`App Store Connect ${phase} reported a failure`,
);
assert.match(
output,
successPattern,
`App Store Connect ${phase} did not return a recognized success response`,
);
}

if (
process.argv[1] &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
Expand All @@ -237,8 +263,21 @@ if (
console.log(
"Signed iOS bundle structural checks passed; manual release checklist still required.",
);
} else if (
process.argv[2] === "altool" &&
process.argv[3] &&
process.argv[4] &&
process.argv[5]
) {
validateAltoolResult(
readFileSync(resolve(process.argv[5]), "utf8"),
process.argv[3],
Number(process.argv[4]),
);
} else {
throw new Error("Usage: readiness.mjs config | app <app-path>");
throw new Error(
"Usage: readiness.mjs config | app <app-path> | altool <phase> <status> <log-path>",
);
}
} catch {
console.error(
Expand Down
41 changes: 41 additions & 0 deletions scripts/ios-release/readiness.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
import test from "node:test";
import {
releaseConfig,
validateAltoolResult,
validateDistribution,
validatePrivacyManifest,
bundleId,
Expand Down Expand Up @@ -33,6 +34,7 @@ function fixture() {
CFBundleIdentifier: bundleId,
CFBundleShortVersionString: expected.version,
CFBundleVersion: expected.build,
DTSDKName: "iphoneos26.0",
NSCameraUsageDescription: "Scan pairing codes",
NSMicrophoneUsageDescription: "Record dictation",
NSSpeechRecognitionUsageDescription: "Transcribe dictation",
Expand Down Expand Up @@ -61,6 +63,33 @@ test("release config rejects shell-like and invalid version/build/team inputs",
assert.throws(() => releaseConfig(...args));
}
});

test("rejects an altool API failure even when altool exits successfully", () => {
assert.throws(() =>
validateAltoolResult(
"ERROR: Failed to validate package. UPLOAD FAILED (409)",
"upload",
0,
),
);
});

test("requires an explicit altool success marker", () => {
assert.throws(() =>
validateAltoolResult(
"Transfer completed without a final response",
"upload",
0,
),
);
assert.doesNotThrow(() =>
validateAltoolResult(
"UPLOAD SUCCEEDED",
"upload",
0,
),
);
});
test("accepts matching distribution metadata, including legacy App ID prefixes", () => {
const f = fixture();
validateDistribution(f.info, f.profile, f.entitlements, f.expected);
Expand Down Expand Up @@ -178,6 +207,18 @@ for (const [name, mutate] of [
f.info.CFBundleVersion = "9";
},
],
[
"outdated iOS SDK",
(f) => {
f.info.DTSDKName = "iphoneos18.5";
},
],
[
"missing iOS SDK metadata",
(f) => {
delete f.info.DTSDKName;
},
],
[
"missing camera permission",
(f) => {
Expand Down
Loading