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
16 changes: 3 additions & 13 deletions .github/workflows/android-google-play.yml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ios-app-store-connect.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: iOS App Store Connect

on:
workflow_dispatch:
workflow_call:

permissions:
contents: read
Expand Down
17 changes: 16 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 13 additions & 12 deletions mobile/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand All @@ -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`).
Expand Down
18 changes: 15 additions & 3 deletions tests/releaseWorkflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
Expand Down
Loading