diff --git a/.github/workflows/android-google-play.yml b/.github/workflows/android-google-play.yml index 5dc29c2..c257776 100644 --- a/.github/workflows/android-google-play.yml +++ b/.github/workflows/android-google-play.yml @@ -1,24 +1,14 @@ name: Android Google Play on: - workflow_dispatch: + workflow_call: inputs: track: - description: Google Play track to receive the release - type: choice + type: string required: true - default: internal - options: - - internal - - production release_status: - description: Publish state for the selected track - type: choice + type: string required: true - default: draft - options: - - draft - - completed permissions: contents: read diff --git a/.github/workflows/ios-app-store-connect.yml b/.github/workflows/ios-app-store-connect.yml index 2011796..618462f 100644 --- a/.github/workflows/ios-app-store-connect.yml +++ b/.github/workflows/ios-app-store-connect.yml @@ -1,7 +1,7 @@ name: iOS App Store Connect on: - workflow_dispatch: + workflow_call: permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f455a85..0e39aa6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,9 +136,24 @@ jobs: dist/*.blockmap if-no-files-found: error - release: + app-store-connect: + needs: [check, build] + if: needs.check.outputs.already-released == 'false' + uses: ./.github/workflows/ios-app-store-connect.yml + secrets: inherit + + google-play: needs: [check, build] if: needs.check.outputs.already-released == 'false' + uses: ./.github/workflows/android-google-play.yml + with: + track: production + release_status: completed + secrets: inherit + + release: + needs: [check, build, app-store-connect, google-play] + if: needs.check.outputs.already-released == 'false' runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/mobile/RELEASING.md b/mobile/RELEASING.md index 52d3845..99b8374 100644 --- a/mobile/RELEASING.md +++ b/mobile/RELEASING.md @@ -58,14 +58,20 @@ OPENSCENE_KEY_PASSWORD=… build falls back to the debug key, so a local build still works for testing and only a real submission needs the keystore. -## App Store Connect automation +## Store distribution on main releases -The manually triggered **iOS App Store Connect** workflow builds an IPA and -uploads it to App Store Connect. It never submits an app for review: that -remains an explicit App Store Connect action after build processing and metadata -review. +The **release** workflow is the only store-distribution trigger. It runs when +an unreleased version is promoted to `main`, after the release verification and +desktop packaging jobs finish. It calls the iOS and Android distribution +workflows; they cannot be run manually. A push to `main` with an already tagged +version skips all release and store-distribution work. -Create the `app-store-production` GitHub Environment, restrict it to the `dev` +The iOS job builds an IPA and uploads it to App Store Connect. It never submits +an app for review: that remains an explicit App Store Connect action after build +processing and metadata review. The Android job uploads the signed AAB to the +Google Play **production** track with status `completed`. + +Create the `app-store-production` GitHub Environment, restrict it to the `main` branch, and require a reviewer before deploying. Store the following values as environment configuration (not in the repository): @@ -83,13 +89,8 @@ commit any of these files or their decoded values. ### Google Play automation -The manually triggered **Android Google Play** workflow builds a signed Android -App Bundle (AAB) and uploads it to the explicitly selected Play track. It -defaults to an `internal` track `draft`; selecting `production` and `completed` -is a deliberate release decision made at dispatch time. - Create the `play-store-production` GitHub Environment, restrict it to the -`dev` branch, and require a reviewer before deploying. Store the following +`main` branch, and require a reviewer before deploying. Store the following values as environment configuration (not in the repository): - Variable: `ANDROID_PACKAGE_NAME` (`com.sloki9637.openscene`). diff --git a/tests/releaseWorkflow.test.ts b/tests/releaseWorkflow.test.ts index 046d096..09df7c5 100644 --- a/tests/releaseWorkflow.test.ts +++ b/tests/releaseWorkflow.test.ts @@ -19,11 +19,11 @@ describe('release workflow', () => { // publish again, so every later step is gated on the tag being absent. expect(workflow).toContain('if git rev-parse -q --verify "refs/tags/${TAG}"'); expect(workflow).toContain("already-released=true"); - // Packaging and publishing are separate jobs, so each has to be gated too; - // a job without the guard would run on any push to main. + // Packaging, store distribution, and publishing are separate jobs, so each + // has to be gated too; a job without the guard would run on any push to main. expect(workflow).toContain("if: needs.check.outputs.already-released == 'false'"); const jobGates = workflow.match(/if: needs\.check\.outputs\.already-released == 'false'/g) ?? []; - expect(jobGates.length).toBe(2); + expect(jobGates.length).toBe(4); }); it('verifies the exact commit it packages', () => { @@ -81,10 +81,22 @@ describe('release workflow', () => { const uses = workflow.match(/uses: [^\n]+/g) ?? []; expect(uses.length).toBeGreaterThan(0); for (const use of uses) { + // Reusable workflows in this repository are versioned by the exact + // commit promoted to main, so they intentionally have a local path + // rather than an external action ref. + if (use.includes('uses: ./.github/workflows/')) continue; expect(use).toMatch(/@[0-9a-f]{40}/); } }); + it('distributes mobile stores only as part of a new main release', () => { + expect(workflow).toContain('uses: ./.github/workflows/ios-app-store-connect.yml'); + expect(workflow).toContain('uses: ./.github/workflows/android-google-play.yml'); + expect(workflow).toContain('track: production'); + expect(workflow).toContain('release_status: completed'); + expect(workflow).toContain('needs: [check, build, app-store-connect, google-play]'); + }); + it('keeps packaging able to run locally with the same inputs', () => { expect(packageJson.scripts.package).toBe('npm run build && electron-builder --publish never'); expect(packageJson.devDependencies['electron-builder']).toBeDefined();