From 5738050cca914b51dc756f78e07c6c4dd4526327 Mon Sep 17 00:00:00 2001 From: vineyardbovines Date: Wed, 26 Aug 2026 10:28:46 -0400 Subject: [PATCH] Split production and TestFlight OTA workflows --- .github/workflows/deploy-production-ota.yml | 144 ++++++++++++++++++ ...s-update.yml => deploy-testflight-ota.yml} | 51 +------ .ota/README.md | 10 +- docs/deploy-ota.md | 15 +- scripts/resolveOtaTarget.sh | 12 +- 5 files changed, 164 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/deploy-production-ota.yml rename .github/workflows/{bundle-deploy-eas-update.yml => deploy-testflight-ota.yml} (86%) diff --git a/.github/workflows/deploy-production-ota.yml b/.github/workflows/deploy-production-ota.yml new file mode 100644 index 0000000000..1999538406 --- /dev/null +++ b/.github/workflows/deploy-production-ota.yml @@ -0,0 +1,144 @@ +--- +name: Deploy Production OTA + +on: + push: + tags: + - 'ota-*' + +permissions: + contents: read + +env: + DENIS_RELEASE_TAG: denis-v0.1.1 + +jobs: + validate: + if: github.repository == 'bluesky-social/social-app' + name: Validate production OTA + runs-on: ubuntu-latest + outputs: + runtime-version: ${{ steps.target.outputs.runtime-version }} + ios-build-number: ${{ steps.target.outputs.ios-build-number }} + android-version-code: ${{ steps.target.outputs.android-version-code }} + steps: + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: ๐Ÿง Resolve and validate OTA target + id: target + run: bash scripts/resolveOtaTarget.sh + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + + - name: ๐Ÿ”ง Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: pnpm + + - name: ๐Ÿ“ท Check native fingerprint and install dependencies + id: fingerprint + uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0 + with: + profile: production + previous-commit-tag: ${{ steps.target.outputs.runtime-version }} + + - name: ๐Ÿ›‘ Reject native changes + if: ${{ steps.fingerprint.outputs.includes-changes }} + run: | + echo "::error::Production OTA contains native changes and requires a full app release." + exit 1 + + - name: ๐Ÿ”ค Compile translations + uses: ./.github/actions/compile-i18n + + - name: ๐Ÿงน Lint check + run: pnpm lint + + - name: ๐Ÿ’… Prettier check + run: pnpm prettier --check . + + - name: ๐Ÿ”Ž Type check + run: pnpm typecheck + + publish: + name: Publish production OTA + needs: [validate] + runs-on: ubuntu-latest + environment: production-ota + permissions: + id-token: write + contents: read + concurrency: + group: production-ota-deploy + cancel-in-progress: false + steps: + - name: โฌ‡๏ธ Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10 + + - name: ๐Ÿ”ง Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: package.json + cache: pnpm + + - name: ๐Ÿ”‘ Check for EXPO_TOKEN + run: > + if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then + echo "You must provide an EXPO_TOKEN secret linked to this project's Expo account in this repo's secrets." + exit 1 + fi + + - name: ๐Ÿ”จ Setup EAS + uses: expo/expo-github-action@eab7a230208c952974db8c3245cfd78402c7b385 # 9.0.0 + with: + eas-version: '19.0.5' + packager: 'pnpm --allow-build=dtrace-provider' + token: ${{ secrets.EXPO_TOKEN }} + + - name: ๐Ÿช› Setup jq + uses: dcarbone/install-jq-action@4fcb5062d7ce9bc4382d1a352d19ba3ba2c317c1 # v4.0.1 + + - name: โœ๏ธ Write environment variables + id: env + uses: ./.github/actions/write-env + with: + env-token: ${{ secrets.ENV_TOKEN }} + sentry-dsn: ${{ secrets.SENTRY_DSN }} + bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} + gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} + google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} + expo-public-env: production + + - name: ๐Ÿ—๏ธ Create Bundle + run: > + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_RELEASE=${{ steps.env.outputs.release-version }} + SENTRY_DIST=${{ steps.env.outputs.bundle-identifier }} + pnpm export + + - name: โ˜๏ธ Configure AWS credentials (denis) + uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3 + with: + role-to-assume: arn:aws:iam::007404326489:role/denis-ci-publish + aws-region: us-east-2 + + - name: โฌ‡๏ธ Setup denis CLI + uses: ./.github/actions/setup-denis + with: + release-tag: ${{ env.DENIS_RELEASE_TAG }} + app-id: ${{ vars.SYNC_INTERNAL_APP_ID }} + private-key: ${{ secrets.SYNC_INTERNAL_PK }} + + - name: ๐Ÿš€ Publish OTA to denis (S3) + run: pnpm use-build-number bash scripts/denisPublish.sh + env: + RUNTIME_VERSION: ${{ needs.validate.outputs.runtime-version }} + CHANNEL_NAME: production + BSKY_IOS_BUILD_NUMBER: ${{ needs.validate.outputs.ios-build-number }} + BSKY_ANDROID_VERSION_CODE: ${{ needs.validate.outputs.android-version-code }} diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/deploy-testflight-ota.yml similarity index 86% rename from .github/workflows/bundle-deploy-eas-update.yml rename to .github/workflows/deploy-testflight-ota.yml index 0320e1b99e..426da78ee0 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/deploy-testflight-ota.yml @@ -1,24 +1,12 @@ --- -name: Bundle and Deploy EAS Update +name: Deploy TestFlight OTA on: push: branches: - main - workflow_dispatch: - inputs: - channel: - type: choice - description: Deployment channel to use - options: - - testflight - - production - runtimeVersion: - type: string - description: Runtime version for non-production manual updates (production derives this from the OTA manifest) - required: false -# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code +# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code. permissions: contents: read @@ -30,7 +18,7 @@ env: jobs: bundleDeploy: if: github.repository == 'bluesky-social/social-app' - name: Bundle and Deploy EAS Update + name: Bundle and Deploy TestFlight OTA runs-on: ubuntu-latest # id-token: write lets this job mint an OIDC token to assume the denis # publish role; actions: read loads the fingerprint baseline artifact; @@ -46,9 +34,6 @@ jobs: # A version bump forces a native build even if the fingerprint is unchanged changes-detected: ${{ steps.fingerprint.outputs.includes-changes || steps.version.outputs.version-changed }} - runtime-version: ${{ steps.target.outputs.runtime-version }} - ios-build-number: ${{ steps.target.outputs.ios-build-number }} - android-version-code: ${{ steps.target.outputs.android-version-code }} steps: - name: โฌ‡๏ธ Checkout @@ -56,16 +41,6 @@ jobs: with: fetch-depth: 0 - # A production OTA is an immutable, reviewed release artifact. The tag - # pins the bundle source SHA and its committed manifest pins the native - # builds that are allowed to receive it. - - name: ๐Ÿง Resolve and validate OTA target - id: target - env: - CHANNEL: ${{ inputs.channel || 'testflight' }} - INPUT_RUNTIME_VERSION: ${{ inputs.runtimeVersion }} - run: bash scripts/resolveOtaTarget.sh - - name: ๐Ÿ”‘ Check for EXPO_TOKEN run: > if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then @@ -108,7 +83,6 @@ jobs: - name: โฌ‡๏ธ Load fingerprint baseline id: baseline - if: ${{ (inputs.channel || 'testflight') == 'testflight' }} env: GH_TOKEN: ${{ github.token }} REPOSITORY_ID: ${{ github.repository_id }} @@ -141,8 +115,7 @@ jobs: id: fingerprint uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0 with: - profile: ${{ inputs.channel || 'testflight' }} - previous-commit-tag: ${{ steps.target.outputs.runtime-version }} + profile: testflight # The recordBaseline job uploads this marker after a successful deploy; # on the native path, that requires both builds to succeed. A missing # marker forces native builds so they can seed the baseline safely. @@ -196,7 +169,7 @@ jobs: bitdrift-api-key: ${{ secrets.BITDRIFT_API_KEY }} gcp-project-id: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} google-services-token: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - expo-public-env: ${{ inputs.channel || 'testflight' }} + expo-public-env: testflight - name: ๐Ÿ—๏ธ Create Bundle if: ${{ !steps.fingerprint.outputs.includes-changes && @@ -229,20 +202,14 @@ jobs: !steps.version.outputs.version-changed }} run: pnpm use-build-number bash scripts/denisPublish.sh env: - RUNTIME_VERSION: ${{ steps.target.outputs.runtime-version }} - CHANNEL_NAME: ${{ inputs.channel || 'testflight' }} - # Production values come from the reviewed manifest. On automatic - # testflight runs these remain empty and the wrapper reads EAS. - BSKY_IOS_BUILD_NUMBER: ${{ steps.target.outputs.ios-build-number }} - BSKY_ANDROID_VERSION_CODE: ${{ steps.target.outputs.android-version-code }} + CHANNEL_NAME: testflight buildIfNecessaryIOS: name: Build and Submit iOS needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here - if: ${{ inputs.channel != 'production' && - needs.bundleDeploy.outputs.changes-detected && github.repository == + if: ${{ needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }} uses: ./.github/workflows/build-submit-ios.yml with: @@ -270,8 +237,7 @@ jobs: needs: [bundleDeploy] # Gotta check if its NOT '[]' because any md5 hash in the outputs is detected as a possible secret and won't be # available here - if: ${{ inputs.channel != 'production' && - needs.bundleDeploy.outputs.changes-detected && github.repository == + if: ${{ needs.bundleDeploy.outputs.changes-detected && github.repository == 'bluesky-social/social-app' }} # build-submit-android.yml contains an attachToRelease job that requests contents: write. # That job is skipped here (it needs a production tag build), but GitHub statically @@ -316,7 +282,6 @@ jobs: runs-on: ubuntu-latest needs: [bundleDeploy, buildIfNecessaryIOS, buildIfNecessaryAndroid] if: ${{ always() && - (inputs.channel || 'testflight') == 'testflight' && needs.bundleDeploy.result == 'success' && (needs.bundleDeploy.outputs.changes-detected != 'true' || (needs.buildIfNecessaryIOS.result == 'success' && diff --git a/.ota/README.md b/.ota/README.md index dbc21b5538..6b672827bf 100644 --- a/.ota/README.md +++ b/.ota/README.md @@ -23,11 +23,11 @@ git tag ota-1.131.1-1 git push origin ota-1.131.1-1 ``` -Run **Bundle and Deploy EAS Update** from that tag and select `production`. The -workflow rejects branches, mismatched versions, missing native release tags, -and OTA commits that are not descended from the native release. Runtime and -build numbers are read from the intent; production values typed into the -workflow form are ignored. +Pushing the tag starts **Deploy Production OTA** automatically. The workflow +rejects mismatched versions, missing native release tags, native changes, and +OTA commits that are not descended from the native release. Runtime and build +numbers are read from the intent. Publishing waits at the protected +`production-ota` environment. Use `ota-1.131.1-2` and `.ota/1.131.1-2.json` for the next OTA. Base it on the previous OTA so that each update contains all earlier fixes. diff --git a/docs/deploy-ota.md b/docs/deploy-ota.md index bb24e9c579..d05518cc64 100644 --- a/docs/deploy-ota.md +++ b/docs/deploy-ota.md @@ -86,17 +86,14 @@ git push origin ota-1.131.1-1 The sequence in the filename and tag must match. See [`.ota/README.md`](../.ota/README.md) for the complete contract. -### 7. Run the GitHub actions -You'll need to run two separate actions: one to deploy the iOS/Android OTA -itself, and one to build the web Docker container. +Pushing the tag automatically starts the **Deploy Production OTA** workflow. +It validates and prepares the exact tagged bundle, then waits at the protected +`production-ota` environment before publishing. -**For the iOS/Android OTA,** head to [Actions > Bundle and Deploy EAS -Update](https://github.com/bluesky-social/social-app/actions/workflows/bundle-deploy-eas-update.yml) -and run the action. +### 7. Build web -| Steps | | -| ----- | --- | -| Select your immutable tag `ota-1.x.0-x`, select `production` in the dropdown, and click "Run workflow". Runtime and build numbers are read from the reviewed OTA intent. | ![workflow](./img/ota_action.png) | +The production OTA and TestFlight OTA workflows are separate. Production never +uses the automatic TestFlight build-number or native-rebuild behavior. > [!NOTE] > Production OTAs are bound to the specific native build they target. The diff --git a/scripts/resolveOtaTarget.sh b/scripts/resolveOtaTarget.sh index d4c7e448ca..76ad5a3010 100644 --- a/scripts/resolveOtaTarget.sh +++ b/scripts/resolveOtaTarget.sh @@ -16,18 +16,8 @@ is_build_number() { [[ "$1" =~ ^[1-9][0-9]*$ ]] } -if [ "${CHANNEL:-testflight}" != "production" ]; then - if [ -n "${INPUT_RUNTIME_VERSION:-}" ] && ! is_version "$INPUT_RUNTIME_VERSION"; then - error "runtimeVersion must use x.y.z format" - fi - echo "runtime-version=${INPUT_RUNTIME_VERSION:-}" >> "$GITHUB_OUTPUT" - echo "ios-build-number=" >> "$GITHUB_OUTPUT" - echo "android-version-code=" >> "$GITHUB_OUTPUT" - exit 0 -fi - [ "${GITHUB_REF_TYPE:-}" = "tag" ] || - error "Production OTAs must be dispatched from an immutable OTA tag, not '${GITHUB_REF_NAME:-unknown}'." + error "Production OTAs must run from an immutable OTA tag, not '${GITHUB_REF_NAME:-unknown}'." if [[ ! "${GITHUB_REF_NAME:-}" =~ ^ota-([0-9]+\.[0-9]+\.[0-9]+)-([1-9][0-9]*)$ ]]; then error "Production OTA tag must use ota--, for example ota-1.131.1-1."