Require immutable tags for production OTAs
This commit is contained in:
@@ -15,16 +15,8 @@ on:
|
|||||||
- production
|
- production
|
||||||
runtimeVersion:
|
runtimeVersion:
|
||||||
type: string
|
type: string
|
||||||
description: Runtime version (in x.x.x format) that this update is for
|
description: Runtime version for non-production manual updates (production derives this from the OTA manifest)
|
||||||
required: true
|
required: false
|
||||||
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)
|
|
||||||
|
|
||||||
# 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:
|
permissions:
|
||||||
@@ -54,8 +46,26 @@ jobs:
|
|||||||
# A version bump forces a native build even if the fingerprint is unchanged
|
# A version bump forces a native build even if the fingerprint is unchanged
|
||||||
changes-detected: ${{ steps.fingerprint.outputs.includes-changes ||
|
changes-detected: ${{ steps.fingerprint.outputs.includes-changes ||
|
||||||
steps.version.outputs.version-changed }}
|
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:
|
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
|
- name: 🔑 Check for EXPO_TOKEN
|
||||||
run: >
|
run: >
|
||||||
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
if [ -z "${{ secrets.EXPO_TOKEN }}" ]; then
|
||||||
@@ -63,34 +73,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
- name: ⬇️ Fetch commits from base branch
|
||||||
if: ${{ github.ref != 'refs/heads/main' }}
|
if: ${{ github.ref != 'refs/heads/main' }}
|
||||||
run: git fetch origin main:main --depth 100
|
run: git fetch origin main:main --depth 100
|
||||||
@@ -160,7 +142,7 @@ jobs:
|
|||||||
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
|
uses: bluesky-social/github-actions/fingerprint-native@abc6a46eb4badf243f55bfd7d6cec42722456300 # v0.3.0
|
||||||
with:
|
with:
|
||||||
profile: ${{ inputs.channel || 'testflight' }}
|
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;
|
# The recordBaseline job uploads this marker after a successful deploy;
|
||||||
# on the native path, that requires both builds to succeed. A missing
|
# on the native path, that requires both builds to succeed. A missing
|
||||||
# marker forces native builds so they can seed the baseline safely.
|
# marker forces native builds so they can seed the baseline safely.
|
||||||
@@ -247,12 +229,12 @@ jobs:
|
|||||||
!steps.version.outputs.version-changed }}
|
!steps.version.outputs.version-changed }}
|
||||||
run: pnpm use-build-number bash scripts/denisPublish.sh
|
run: pnpm use-build-number bash scripts/denisPublish.sh
|
||||||
env:
|
env:
|
||||||
RUNTIME_VERSION: ${{ inputs.runtimeVersion }}
|
RUNTIME_VERSION: ${{ steps.target.outputs.runtime-version }}
|
||||||
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
CHANNEL_NAME: ${{ inputs.channel || 'testflight' }}
|
||||||
# When set (required for production), these take precedence over the
|
# Production values come from the reviewed manifest. On automatic
|
||||||
# global EAS counters inside the use-build-number wrapper
|
# testflight runs these remain empty and the wrapper reads EAS.
|
||||||
BSKY_IOS_BUILD_NUMBER: ${{ inputs.iosBuildNumber }}
|
BSKY_IOS_BUILD_NUMBER: ${{ steps.target.outputs.ios-build-number }}
|
||||||
BSKY_ANDROID_VERSION_CODE: ${{ inputs.androidVersionCode }}
|
BSKY_ANDROID_VERSION_CODE: ${{ steps.target.outputs.android-version-code }}
|
||||||
|
|
||||||
buildIfNecessaryIOS:
|
buildIfNecessaryIOS:
|
||||||
name: Build and Submit iOS
|
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
|
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
|
You'll need to run two separate actions: one to deploy the iOS/Android OTA
|
||||||
itself, and one to build the web Docker container.
|
itself, and one to build the web Docker container.
|
||||||
|
|
||||||
@@ -73,20 +96,12 @@ and run the action.
|
|||||||
|
|
||||||
| Steps | |
|
| 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]
|
> [!NOTE]
|
||||||
> Production OTAs are bound to the specific native build they target, so the
|
> Production OTAs are bound to the specific native build they target. The
|
||||||
> workflow requires the build numbers to be entered manually. There is no need
|
> workflow rejects branches and validates that the tag, intent, package
|
||||||
> to change the global EAS build counters (and doing so is no longer necessary
|
> version, native release ancestry, and build numbers agree before publishing.
|
||||||
> 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.
|
|
||||||
|
|
||||||
**For web,** head to [Actions >
|
**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)
|
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" |  |
|
| 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,
|
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
|
copy the most recent commit hash. Post this hash in `#ops-deploys` and request
|
||||||
someone with web deploy access deploy the built container.
|
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
|
In about five minutes, the new deployment should be deployed and devices will
|
||||||
begin downloading and installing in the background.
|
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