diff --git a/.github/workflows/store-delivery.yml b/.github/workflows/store-delivery.yml index b5d298a9..405db708 100644 --- a/.github/workflows/store-delivery.yml +++ b/.github/workflows/store-delivery.yml @@ -31,6 +31,11 @@ on: required: true default: false type: boolean + ios_build_number: + description: "Existing App Store Connect build number to submit without rebuilding" + required: false + default: "" + type: string permissions: contents: write @@ -301,6 +306,7 @@ jobs: --cache-dir "$RUNNER_TEMP/edgeever-bun-cache-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" - name: Build the iOS archive on EAS + if: ${{ inputs.ios_build_number == '' }} id: build env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} @@ -328,6 +334,7 @@ jobs: working-directory: apps/mobile - name: Upload archive to App Store Connect and TestFlight + if: ${{ inputs.ios_build_number == '' }} env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} run: | @@ -341,10 +348,10 @@ jobs: - name: Submit the uploaded build to App Review env: - APP_STORE_BUILD_NUMBER: ${{ steps.build.outputs.build_number }} - APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }} - APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} - APP_STORE_CONNECT_API_KEY_P8_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8_BASE64 }} + APP_STORE_BUILD_NUMBER: ${{ inputs.ios_build_number || steps.build.outputs.build_number }} + APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.EDGEEVER_APPLE_API_ISSUER }} + APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.EDGEEVER_APPLE_API_KEY_ID }} + APP_STORE_CONNECT_API_KEY_P8_BASE64: ${{ secrets.EDGEEVER_APPLE_API_KEY_BASE64 }} APP_STORE_VERSION: ${{ needs.plan.outputs.version }} run: | test -n "$APP_STORE_CONNECT_API_ISSUER_ID" diff --git a/scripts/store-delivery.mjs b/scripts/store-delivery.mjs index 4550d26f..810bb43a 100644 --- a/scripts/store-delivery.mjs +++ b/scripts/store-delivery.mjs @@ -14,6 +14,7 @@ Options: --platform android, ios, or both (default: both) --android-track production, alpha, beta, or internal (default: production) --recover-play-apk Skip Play upload and recover its already signed APK + --ios-build-number Submit an existing App Store Connect build without rebuilding --repository GitHub repository (default: ${DEFAULT_REPOSITORY}) --dry-run Print the workflow dispatch plan --help Show this help @@ -27,12 +28,14 @@ export const parseStoreDeliveryArgs = (argv) => { repository: DEFAULT_REPOSITORY, dryRun: false, recoverPlayApk: false, + iosBuildNumber: "", help: false, }; const valueOptions = new Map([ ["--release", "releaseTag"], ["--platform", "platform"], ["--android-track", "androidTrack"], + ["--ios-build-number", "iosBuildNumber"], ["--repository", "repository"], ]); @@ -79,6 +82,12 @@ export const parseStoreDeliveryArgs = (argv) => { if (!/^[^/\s]+\/[^/\s]+$/.test(options.repository)) { throw new Error("--repository must use owner/name format."); } + if (options.iosBuildNumber && !/^[1-9]\d*$/.test(options.iosBuildNumber)) { + throw new Error("--ios-build-number must be a positive integer."); + } + if (options.iosBuildNumber && options.platform === "android") { + throw new Error("--ios-build-number requires --platform ios or both."); + } return options; }; @@ -99,6 +108,8 @@ const run = (options) => { `android_track=${options.androidTrack}`, "-f", `recover_play_apk=${options.recoverPlayApk}`, + "-f", + `ios_build_number=${options.iosBuildNumber}`, ]; if (options.dryRun) { console.log(`gh ${args.join(" ")}`); diff --git a/scripts/store-delivery.test.mjs b/scripts/store-delivery.test.mjs index 05cf0937..88bafcb4 100644 --- a/scripts/store-delivery.test.mjs +++ b/scripts/store-delivery.test.mjs @@ -30,6 +30,19 @@ describe("store delivery command", () => { ).toBe(true); }); + test("supports submitting an existing iOS build without rebuilding", () => { + expect( + parseStoreDeliveryArgs([ + "--release", + "v1.7.0", + "--platform", + "ios", + "--ios-build-number", + "30", + ]).iosBuildNumber, + ).toBe("30"); + }); + test("rejects malformed release tags", () => { expect(() => parseStoreDeliveryArgs(["--release", "latest"])).toThrow( "stable vX.Y.Z", @@ -55,6 +68,19 @@ describe("store delivery command", () => { /edgeever-bun-cache-\$\{GITHUB_RUN_ID\}-\$\{GITHUB_RUN_ATTEMPT\}/g, ), ).toHaveLength(3); + expect(workflow).toContain("inputs.ios_build_number == ''"); + expect(workflow).toContain( + "APP_STORE_BUILD_NUMBER: ${{ inputs.ios_build_number || steps.build.outputs.build_number }}", + ); + expect(workflow).toContain( + "APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.EDGEEVER_APPLE_API_ISSUER }}", + ); + expect(workflow).toContain( + "APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.EDGEEVER_APPLE_API_KEY_ID }}", + ); + expect(workflow).toContain( + "APP_STORE_CONNECT_API_KEY_P8_BASE64: ${{ secrets.EDGEEVER_APPLE_API_KEY_BASE64 }}", + ); }); test("replaces the GitHub APK with the Play-signed universal APK", () => {