From 86a60371d639bd55123af259fbc1f097eeabe90d Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Sun, 7 Jun 2026 19:01:31 -0700 Subject: [PATCH] Add standalone TestFlight retry job --- .../workflows/apple-distribute-testers.yml | 39 +-------- .forgejo/workflows/publish-store-uploads.yml | 82 ++++++++----------- Scripts/ci/distribute-testflight.sh | 60 ++++++++++++++ ...nfrastructure-and-store-upload-pipeline.md | 7 +- 4 files changed, 101 insertions(+), 87 deletions(-) create mode 100755 Scripts/ci/distribute-testflight.sh diff --git a/.forgejo/workflows/apple-distribute-testers.yml b/.forgejo/workflows/apple-distribute-testers.yml index c94c43f..b9de081 100644 --- a/.forgejo/workflows/apple-distribute-testers.yml +++ b/.forgejo/workflows/apple-distribute-testers.yml @@ -472,44 +472,7 @@ jobs: TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} TESTFLIGHT_INTERNAL_GROUP: Internal IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} - run: | - set -euo pipefail - Scripts/ci/check-release-config.sh app-store - api_key_json="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-api-key.XXXXXX.json")" - key_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-key.XXXXXX.p8")" - if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then - cp "$ASC_API_KEY_PATH" "$key_file" - else - printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_file" - fi - KEY_FILE="$key_file" python3 -c 'import json, os, pathlib; print(json.dumps({"key_id": os.environ["ASC_API_KEY_ID"], "issuer_id": os.environ["ASC_API_ISSUER_ID"], "key": pathlib.Path(os.environ["KEY_FILE"]).read_text(), "duration": 1200, "in_house": False}))' > "$api_key_json" - wait_processing_timeout="${TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS:-7200}" - case "$wait_processing_timeout" in - ""|*[!0-9]*) - echo "::error ::TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS must be an integer number of seconds." >&2 - exit 1 - ;; - esac - - args=( - run pilot - "api_key_path:${api_key_json}" - "app_identifier:${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}" - "app_platform:ios" - "distribute_only:true" - "build_number:${BUILD_NUMBER}" - "wait_processing_interval:30" - "wait_processing_timeout_duration:${wait_processing_timeout}" - "distribute_external:${TESTFLIGHT_DISTRIBUTE_EXTERNAL:-false}" - "uses_non_exempt_encryption:${IOS_USES_NON_EXEMPT_ENCRYPTION:-false}" - ) - if [[ -n "${TESTFLIGHT_GROUPS:-}" ]]; then - args+=("groups:${TESTFLIGHT_GROUPS}") - elif [[ "${TESTFLIGHT_DISTRIBUTE_EXTERNAL:-false}" != "true" && -n "${TESTFLIGHT_INTERNAL_GROUP:-}" ]]; then - args+=("groups:${TESTFLIGHT_INTERNAL_GROUP}") - fi - nix run nixpkgs#fastlane -- "${args[@]}" - rm -f "$api_key_json" "$key_file" + run: Scripts/ci/distribute-testflight.sh publish-sparkle: name: Publish Sparkle diff --git a/.forgejo/workflows/publish-store-uploads.yml b/.forgejo/workflows/publish-store-uploads.yml index dcbedc2..40fb74a 100644 --- a/.forgejo/workflows/publish-store-uploads.yml +++ b/.forgejo/workflows/publish-store-uploads.yml @@ -222,7 +222,7 @@ jobs: release-ios-testflight: name: Release (TestFlight iOS Testers) needs: upload-app-store - if: ${{ always() && (github.event.inputs.upload_app_store == 'true' || github.event.inputs.distribute_testflight == 'true') && (github.event.inputs.upload_app_store != 'true' || needs.upload-app-store.result == 'success') }} + if: ${{ github.event.inputs.upload_app_store == 'true' }} runs-on: namespace-profile-linux-medium steps: - name: Checkout @@ -256,55 +256,45 @@ jobs: TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} TESTFLIGHT_INTERNAL_GROUP: Internal IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} - run: | - set -euo pipefail - Scripts/ci/check-release-config.sh app-store - api_key_json="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-api-key.XXXXXX.json")" - key_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-key.XXXXXX.p8")" - if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then - cp "$ASC_API_KEY_PATH" "$key_file" - else - printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_file" - fi - KEY_FILE="$key_file" python3 -c 'import json, os, pathlib; print(json.dumps({"key_id": os.environ["ASC_API_KEY_ID"], "issuer_id": os.environ["ASC_API_ISSUER_ID"], "key": pathlib.Path(os.environ["KEY_FILE"]).read_text(), "duration": 1200, "in_house": False}))' > "$api_key_json" + run: Scripts/ci/distribute-testflight.sh - distribute_external="${TESTFLIGHT_DISTRIBUTE_EXTERNAL:-false}" - if [[ -z "$distribute_external" ]]; then - distribute_external="false" - fi - uses_non_exempt="${IOS_USES_NON_EXEMPT_ENCRYPTION:-false}" - if [[ -z "$uses_non_exempt" ]]; then - uses_non_exempt="false" - fi - wait_processing_timeout="${TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS:-7200}" - case "$wait_processing_timeout" in - ""|*[!0-9]*) - echo "::error ::TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS must be an integer number of seconds." >&2 - exit 1 - ;; - esac + distribute-ios-testflight: + name: Distribute (TestFlight iOS Testers) + if: ${{ github.event.inputs.upload_app_store != 'true' && github.event.inputs.distribute_testflight == 'true' }} + runs-on: namespace-profile-linux-medium + steps: + - name: Checkout + uses: https://code.forgejo.org/actions/checkout@v4 + with: + token: ${{ github.token }} + fetch-depth: 0 - args=( - run pilot - "api_key_path:${api_key_json}" - "app_identifier:${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}" - "app_platform:ios" - "distribute_only:true" - "build_number:${BUILD_NUMBER}" - "wait_processing_interval:30" - "wait_processing_timeout_duration:${wait_processing_timeout}" - "distribute_external:${distribute_external}" - "uses_non_exempt_encryption:${uses_non_exempt}" - ) + - name: Ensure Nix + shell: bash + run: bash Scripts/ci/ensure-nix.sh - if [[ -n "${TESTFLIGHT_GROUPS}" ]]; then - args+=("groups:${TESTFLIGHT_GROUPS}") - elif [[ "$distribute_external" != "true" && -n "${TESTFLIGHT_INTERNAL_GROUP:-}" ]]; then - args+=("groups:${TESTFLIGHT_INTERNAL_GROUP}") - fi + - name: Configure Age + shell: bash + env: + AGE_FORGE_SSH_KEY: ${{ secrets.AGE_FORGE_SSH_KEY }} + run: Scripts/resolve-age-identity.sh >> "$GITHUB_ENV" - nix run nixpkgs#fastlane -- "${args[@]}" - rm -f "$api_key_json" "$key_file" + - name: Decrypt Release Secrets + uses: ./.forgejo/actions/decrypt-release-secrets + + - name: Validate App Store Credentials + shell: bash + run: Scripts/ci/check-release-config.sh app-store + + - name: Distribute iOS Build To TestFlight + shell: bash + env: + BUILD_NUMBER: ${{ github.event.inputs.build_number }} + TESTFLIGHT_DISTRIBUTE_EXTERNAL: ${{ github.event.inputs.testflight_distribute_external || 'false' }} + TESTFLIGHT_GROUPS: ${{ github.event.inputs.testflight_groups || '' }} + TESTFLIGHT_INTERNAL_GROUP: Internal + IOS_USES_NON_EXEMPT_ENCRYPTION: ${{ github.event.inputs.ios_uses_non_exempt_encryption || 'false' }} + run: Scripts/ci/distribute-testflight.sh upload-sparkle: name: Upload (Sparkle) diff --git a/Scripts/ci/distribute-testflight.sh b/Scripts/ci/distribute-testflight.sh new file mode 100755 index 0000000..8ac6271 --- /dev/null +++ b/Scripts/ci/distribute-testflight.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +Scripts/ci/check-release-config.sh app-store + +build_number="${BUILD_NUMBER:?BUILD_NUMBER is required}" +distribute_external="${TESTFLIGHT_DISTRIBUTE_EXTERNAL:-false}" +testflight_groups="${TESTFLIGHT_GROUPS:-}" +testflight_internal_group="${TESTFLIGHT_INTERNAL_GROUP:-Internal}" +uses_non_exempt="${IOS_USES_NON_EXEMPT_ENCRYPTION:-false}" +wait_processing_timeout="${TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS:-7200}" + +if [[ -z "$distribute_external" ]]; then + distribute_external="false" +fi +if [[ -z "$uses_non_exempt" ]]; then + uses_non_exempt="false" +fi +case "$wait_processing_timeout" in + ""|*[!0-9]*) + echo "::error ::TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS must be an integer number of seconds." >&2 + exit 1 + ;; +esac + +api_key_json="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-api-key.XXXXXX.json")" +key_file="$(mktemp "${RUNNER_TEMP:-/tmp}/burrow-appstore-key.XXXXXX.p8")" +cleanup() { + rm -f "$api_key_json" "$key_file" +} +trap cleanup EXIT + +if [[ -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then + cp "$ASC_API_KEY_PATH" "$key_file" +else + printf '%s' "$ASC_API_KEY_BASE64" | base64 --decode > "$key_file" +fi + +KEY_FILE="$key_file" python3 -c 'import json, os, pathlib; print(json.dumps({"key_id": os.environ["ASC_API_KEY_ID"], "issuer_id": os.environ["ASC_API_ISSUER_ID"], "key": pathlib.Path(os.environ["KEY_FILE"]).read_text(), "duration": 1200, "in_house": False}))' > "$api_key_json" + +args=( + run pilot + "api_key_path:${api_key_json}" + "app_identifier:${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}" + "app_platform:ios" + "distribute_only:true" + "build_number:${build_number}" + "wait_processing_interval:30" + "wait_processing_timeout_duration:${wait_processing_timeout}" + "distribute_external:${distribute_external}" + "uses_non_exempt_encryption:${uses_non_exempt}" +) + +if [[ -n "$testflight_groups" ]]; then + args+=("groups:${testflight_groups}") +elif [[ "$distribute_external" != "true" && -n "$testflight_internal_group" ]]; then + args+=("groups:${testflight_internal_group}") +fi + +nix run nixpkgs#fastlane -- "${args[@]}" diff --git a/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md b/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md index f155823..b835180 100644 --- a/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md +++ b/evolution/proposals/BEP-0009-release-infrastructure-and-store-upload-pipeline.md @@ -195,9 +195,10 @@ The same change also makes the forge itself the release authority: release tags `apple-distribute-testers` and `publish-store-uploads` wait up to 7200 seconds by default and allow Forgejo variable `TESTFLIGHT_WAIT_PROCESSING_TIMEOUT_SECONDS` to tune that ceiling without a - repo change. `publish-store-uploads` can also run with - `distribute_testflight=true` and `upload_app_store=false` to distribute an - already-uploaded build without re-uploading or rebuilding the IPA. + repo change. `publish-store-uploads` has a separate no-`needs` + `distribute-ios-testflight` retry job for `distribute_testflight=true` and + `upload_app_store=false`, so skipped upload/signing jobs cannot suppress + distribution of an already-uploaded build. - Add `infra/releases` as the OpenTofu boundary for the GCS release and package repository backup buckets. Garage is the required S3-compatible primary distribution target after host bootstrap. `infra/identity` remains the boundary for Google KMS signing keys, the Authentik WIF pool/provider, and the runner service account. - Serve `releases.burrow.net` and `packages.burrow.net` from the forge Caddy edge as stable Burrow domains backed by the GCS release and package buckets.