Require immutable tags for production OTAs
This commit is contained in:
@@ -15,16 +15,8 @@ on:
|
||||
- production
|
||||
runtimeVersion:
|
||||
type: string
|
||||
description: Runtime version (in x.x.x format) that this update is for
|
||||
required: true
|
||||
iosBuildNumber:
|
||||
type: string
|
||||
description: iOS build number of the native build this update targets
|
||||
(required for production)
|
||||
androidVersionCode:
|
||||
type: string
|
||||
description: Android version code of the native build this update
|
||||
targets (required for production)
|
||||
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
|
||||
permissions:
|
||||
@@ -54,8 +46,26 @@ 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
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
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
|
||||
@@ -63,34 +73,6 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate the version if one is supplied. This should generally happen if the update is for a production client
|
||||
- name: 🧐 Validate version
|
||||
env:
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
if: ${{ inputs.runtimeVersion }}
|
||||
run: |
|
||||
[[ "$RUNTIME_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] && echo "Version is valid" || exit 1
|
||||
|
||||
# Production OTAs are bound to the specific native build they target, so
|
||||
# the build numbers must be entered manually rather than read from the
|
||||
# global EAS counters, which advance with every testflight build and so
|
||||
# point past older production releases
|
||||
- name: 🧐 Validate build numbers
|
||||
if: ${{ inputs.channel == 'production' }}
|
||||
env:
|
||||
IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
||||
ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
||||
run: |
|
||||
[[ "$IOS_BUILD_NUMBER" =~ ^[0-9]+$ ]] ||
|
||||
(echo "A numeric iosBuildNumber is required for production updates" && exit 1)
|
||||
[[ "$ANDROID_VERSION_CODE" =~ ^[0-9]+$ ]] ||
|
||||
(echo "A numeric androidVersionCode is required for production updates" && exit 1)
|
||||
|
||||
- name: ⬇️ Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: ⬇️ Fetch commits from base branch
|
||||
if: ${{ github.ref != 'refs/heads/main' }}
|
||||
run: git fetch origin main:main --depth 100
|
||||
@@ -160,7 +142,7 @@ jobs:
|
||||
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
|
||||
with:
|
||||
profile: ${{ inputs.channel || 'testflight' }}
|
||||
previous-commit-tag: ${{ inputs.runtimeVersion }}
|
||||
previous-commit-tag: ${{ steps.target.outputs.runtime-version }}
|
||||
# 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.
|
||||
@@ -247,12 +229,12 @@ jobs:
|
||||
!steps.version.outputs.version-changed }}
|
||||
run: pnpm use-build-number bash scripts/denisPublish.sh
|
||||
env:
|
||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
||||
RUNTIME_VERSION: ${{ steps.target.outputs.runtime-version }}
|
||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||
# When set (required for production), these take precedence over the
|
||||
# global EAS counters inside the use-build-number wrapper
|
||||
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
||||
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
||||
# 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 }}
|
||||
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Production OTA intents
|
||||
|
||||
Production OTA metadata is committed here so that it is reviewed with the code
|
||||
being deployed. This is similar to a changeset: the file declares the release
|
||||
target, while an immutable Git tag declares the exact source commit.
|
||||
|
||||
For the first OTA targeting native release `1.131.1`, create
|
||||
`.ota/1.131.1-1.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtimeVersion": "1.131.1",
|
||||
"iosBuildNumber": 12345,
|
||||
"androidVersionCode": 67890
|
||||
}
|
||||
```
|
||||
|
||||
Commit and review the intent together with the OTA changes. After the OTA branch
|
||||
is ready, tag its tip using the matching name:
|
||||
|
||||
```sh
|
||||
git tag ota-1.131.1-1 <commit-sha>
|
||||
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.
|
||||
|
||||
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.
|
||||
+30
-15
@@ -63,7 +63,30 @@ Run this and commit the result as the last commit on the OTA branch.
|
||||
pnpm intl:release
|
||||
```
|
||||
|
||||
### 6. Run the GitHub actions
|
||||
### 6. Declare and tag the OTA
|
||||
|
||||
Add a reviewed OTA intent at `.ota/<version>-<sequence>.json`. For example,
|
||||
`.ota/1.131.1-1.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"runtimeVersion": "1.131.1",
|
||||
"iosBuildNumber": 12345,
|
||||
"androidVersionCode": 67890
|
||||
}
|
||||
```
|
||||
|
||||
Commit the intent, then tag the exact commit that should be deployed:
|
||||
|
||||
```sh
|
||||
git tag ota-1.131.1-1 <commit-sha>
|
||||
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.
|
||||
|
||||
@@ -73,20 +96,12 @@ and run the action.
|
||||
|
||||
| Steps | |
|
||||
| ----- | --- |
|
||||
| Select your OTA branch `1.x.0-ota-x`, select `production` in the dropdown, enter the git tag of the latest release `1.x.0`, enter the iOS build number and Android version code you found in **Step 1**, and click "Run workflow" |  |
|
||||
| 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. |  |
|
||||
|
||||
> [!NOTE]
|
||||
> Production OTAs are bound to the specific native build they target, so the
|
||||
> workflow requires the build numbers to be entered manually. There is no need
|
||||
> to change the global EAS build counters (and doing so is no longer necessary
|
||||
> for OTAs - they are only used when producing new native builds).
|
||||
|
||||
> [!NOTE]
|
||||
> If you do enter an incorrect version here, the deployment will either:
|
||||
> - Fail, because the action cannot find a commit with your misentered version
|
||||
> - Succeed, but with no users receiving the update. This is because the
|
||||
> version and build numbers you entered will not match any clients in the
|
||||
> wild, so none will be able to receive the update.
|
||||
> Production OTAs are bound to the specific native build they target. The
|
||||
> workflow rejects branches and validates that the tag, intent, package
|
||||
> version, native release ancestry, and build numbers agree before publishing.
|
||||
|
||||
**For web,** head to [Actions >
|
||||
build-and-push-bskyweb-aws](https://github.com/bluesky-social/social-app/actions/workflows/build-and-push-bskyweb-aws.yaml)
|
||||
@@ -96,13 +111,13 @@ and run the action.
|
||||
| ----- | --- |
|
||||
| Select your OTA branch `1.x.0-ota-x` and click "Run workflow" |  |
|
||||
|
||||
### 7. Deploy web
|
||||
### 8. Deploy web
|
||||
|
||||
Once the web Docker container build finishes, go to your `1.x.0-ota-x` branch,
|
||||
copy the most recent commit hash. Post this hash in `#ops-deploys` and request
|
||||
someone with web deploy access deploy the built container.
|
||||
|
||||
### 8. Confirm successful deployment
|
||||
### 9. Confirm successful deployment
|
||||
|
||||
In about five minutes, the new deployment should be deployed and devices will
|
||||
begin downloading and installing in the background.
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
set -o nounset
|
||||
|
||||
error() {
|
||||
echo "::error::$*"
|
||||
exit 1
|
||||
}
|
||||
|
||||
is_version() {
|
||||
[[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
|
||||
}
|
||||
|
||||
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}'."
|
||||
|
||||
if [[ ! "${GITHUB_REF_NAME:-}" =~ ^ota-([0-9]+\.[0-9]+\.[0-9]+)-([1-9][0-9]*)$ ]]; then
|
||||
error "Production OTA tag must use ota-<version>-<sequence>, for example ota-1.131.1-1."
|
||||
fi
|
||||
|
||||
tag_version="${BASH_REMATCH[1]}"
|
||||
sequence="${BASH_REMATCH[2]}"
|
||||
manifest=".ota/${tag_version}-${sequence}.json"
|
||||
[ -f "$manifest" ] || error "The OTA tag must contain its reviewed manifest at $manifest."
|
||||
|
||||
jq -e '
|
||||
type == "object" and
|
||||
(keys | sort) == ["androidVersionCode", "iosBuildNumber", "runtimeVersion"] and
|
||||
(.runtimeVersion | type == "string") and
|
||||
(.iosBuildNumber | type == "number" and floor == .) and
|
||||
(.androidVersionCode | type == "number" and floor == .)
|
||||
' "$manifest" >/dev/null || error "$manifest has an invalid schema."
|
||||
|
||||
runtime_version="$(jq -r '.runtimeVersion' "$manifest")"
|
||||
ios_build_number="$(jq -r '.iosBuildNumber' "$manifest")"
|
||||
android_version_code="$(jq -r '.androidVersionCode' "$manifest")"
|
||||
package_version="$(jq -r '.version' package.json)"
|
||||
|
||||
is_version "$runtime_version" || error "Manifest runtimeVersion must use x.y.z format."
|
||||
is_build_number "$ios_build_number" || error "Manifest iosBuildNumber must be a positive integer."
|
||||
is_build_number "$android_version_code" || error "Manifest androidVersionCode must be a positive integer."
|
||||
[ "$runtime_version" = "$tag_version" ] ||
|
||||
error "Manifest runtimeVersion '$runtime_version' does not match OTA tag version '$tag_version'."
|
||||
[ "$runtime_version" = "$package_version" ] ||
|
||||
error "Manifest runtimeVersion '$runtime_version' does not match package.json version '$package_version'."
|
||||
|
||||
# The native release tag is both the fingerprint baseline and proof that this
|
||||
# OTA was built on top of the native release it targets.
|
||||
git rev-parse --verify --quiet "refs/tags/$runtime_version^{commit}" >/dev/null ||
|
||||
error "Native release tag '$runtime_version' does not exist."
|
||||
git merge-base --is-ancestor "$runtime_version" HEAD ||
|
||||
error "OTA commit $(git rev-parse HEAD) is not descended from native release $runtime_version."
|
||||
|
||||
echo "Production OTA target validated:"
|
||||
echo " source: $(git rev-parse HEAD) ($GITHUB_REF_NAME)"
|
||||
echo " runtime: $runtime_version"
|
||||
echo " iOS build: $ios_build_number"
|
||||
echo " Android build: $android_version_code"
|
||||
|
||||
echo "runtime-version=$runtime_version" >> "$GITHUB_OUTPUT"
|
||||
echo "ios-build-number=$ios_build_number" >> "$GITHUB_OUTPUT"
|
||||
echo "android-version-code=$android_version_code" >> "$GITHUB_OUTPUT"
|
||||
Reference in New Issue
Block a user