diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index 842cbfd3db..156c415632 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -49,6 +49,9 @@ on: source-sha: description: Exact commit used for the build value: ${{ jobs.build.outputs.source-sha }} + artifact-name: + description: Workflow artifact containing the AAB and provenance + value: ${{ jobs.build.outputs.artifact-name }} secrets: EXPO_TOKEN: required: true @@ -88,9 +91,10 @@ jobs: group: android-build cancel-in-progress: false outputs: - package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} - version-code: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }} + package-version: ${{ steps.package.outputs.version }} + version-code: ${{ steps.aab-version.outputs.version-code }} source-sha: ${{ steps.source.outputs.sha }} + artifact-name: android-aab-${{ github.run_id }} steps: - name: ⬇️ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -114,6 +118,11 @@ jobs: distribution: "temurin" java-version: "17" + - name: 🔧 Setup bundletool + uses: amyu/setup-bundletool@cc2e1857284660bd625e43f2c8a45626f034302f # v1.1 + with: + version: "1.18.3" + - name: 🔤 Compile translations uses: ./.github/actions/compile-i18n @@ -139,28 +148,61 @@ jobs: sentry-release: ${{ steps.env.outputs.release-version }} sentry-dist: ${{ steps.env.outputs.bundle-identifier }} - - name: 📚 Get version from package.json - id: get-build-info - run: bash scripts/setGitHubOutput.sh + - name: 📚 Get package version + id: package + run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT" - # Hands the built bundle off to the submit / universalApk jobs. Retention is - # deliberately short (1 day) since it's only an intra-run handoff artifact. + - name: 🔢 Read version code from AAB + id: aab-version + run: | + version_code=$(bundletool dump manifest --bundle=build.aab --xpath=/manifest/@android:versionCode) + [[ "$version_code" =~ ^[1-9][0-9]*$ ]] || { + echo "::error::Could not read a positive Android version code from build.aab" + exit 1 + } + echo "AAB version code: $version_code" + echo "version-code=$version_code" >> "$GITHUB_OUTPUT" + + - name: Record artifact provenance + env: + SOURCE_SHA: ${{ steps.source.outputs.sha }} + PACKAGE_VERSION: ${{ steps.package.outputs.version }} + VERSION_CODE: ${{ steps.aab-version.outputs.version-code }} + run: | + checksum=$(sha256sum build.aab | awk '{print $1}') + jq -n \ + --arg sourceSha "$SOURCE_SHA" \ + --arg packageVersion "$PACKAGE_VERSION" \ + --arg versionCode "$VERSION_CODE" \ + --arg artifactFilename "build.aab" \ + --arg artifactSha256 "$checksum" \ + '{sourceSha: $sourceSha, packageVersion: $packageVersion, versionCode: $versionCode, artifactFilename: $artifactFilename, artifactSha256: $artifactSha256, submissionState: "notStarted"}' \ + > android-build-metadata.json + cat android-build-metadata.json + + # Keep the exact build and its provenance available for inspection and failed-job retries. - name: 🚀 Upload AAB artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: android-aab-${{ github.run_id }} - retention-days: 1 + retention-days: 14 if-no-files-found: error - path: build.aab + path: | + build.aab + android-build-metadata.json - name: 📝 Write build summary env: - REMOTE_VERSION_CODE: ${{ steps.get-build-info.outputs.BSKY_ANDROID_VERSION_CODE }} + VERSION_CODE: ${{ steps.aab-version.outputs.version-code }} + SOURCE_SHA: ${{ steps.source.outputs.sha }} run: | + artifact_sha256=$(jq -r .artifactSha256 android-build-metadata.json) { echo "### Android build number" echo - echo "\`$REMOTE_VERSION_CODE\`" + echo "- Version code: \`$VERSION_CODE\`" + echo "- Source SHA: \`$SOURCE_SHA\`" + echo "- AAB SHA-256: \`$artifact_sha256\`" } >> "$GITHUB_STEP_SUMMARY" submit: @@ -192,6 +234,29 @@ jobs: PROFILE: ${{ inputs.profile || 'testflight-android' }} run: pnpm eas submit -p android --profile $PROFILE --non-interactive --path build.aab + - name: Record submission receipt + env: + SOURCE_SHA: ${{ needs.build.outputs.source-sha }} + PACKAGE_VERSION: ${{ needs.build.outputs.package-version }} + VERSION_CODE: ${{ needs.build.outputs.version-code }} + run: | + artifact_sha256=$(jq -r .artifactSha256 android-build-metadata.json) + jq -n \ + --arg sourceSha "$SOURCE_SHA" \ + --arg packageVersion "$PACKAGE_VERSION" \ + --arg versionCode "$VERSION_CODE" \ + --arg artifactSha256 "$artifact_sha256" \ + '{sourceSha: $sourceSha, packageVersion: $packageVersion, versionCode: $versionCode, artifactSha256: $artifactSha256, submissionState: "submitted"}' \ + > android-submission.json + + - name: Upload submission receipt + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: android-submission-${{ github.run_id }} + path: android-submission.json + retention-days: 14 + if-no-files-found: error + - name: 🔔 Notify Slack of Play Store Submission if: ${{ inputs.profile == 'production' }} uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index 4b71e5b5b4..84c90aecd6 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -10,6 +10,10 @@ on: options: - testflight - production + submit: + type: boolean + description: Submit the build to App Store Connect + default: true testFlightGroup: type: choice description: TestFlight group to assign the build to after submitting @@ -34,6 +38,10 @@ on: type: string description: Build profile to use required: true + submit: + type: boolean + description: Submit the build to App Store Connect + default: true testFlightGroup: type: string description: TestFlight group to assign the build to after submitting ("none" to skip) @@ -58,6 +66,9 @@ on: source-sha: description: Exact commit used for the build value: ${{ jobs.build.outputs.source-sha }} + artifact-name: + description: Workflow artifact containing the IPA and provenance + value: ${{ jobs.build.outputs.artifact-name }} secrets: EXPO_TOKEN: required: true @@ -98,6 +109,7 @@ jobs: package-version: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} build-number: ${{ steps.ipa-build-number.outputs.build-number }} source-sha: ${{ steps.source.outputs.sha }} + artifact-name: ios-build-${{ github.run_id }} steps: - name: ⬇️ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -225,26 +237,48 @@ jobs: echo "IPA build number: $build_number" echo "build-number=$build_number" >> "$GITHUB_OUTPUT" - # Hand the IPA and dSYM off to the submit job. Retention is deliberately short since - # this artifact only exists to bridge the two jobs within a single run. + - name: Record artifact provenance + env: + SOURCE_SHA: ${{ steps.source.outputs.sha }} + PACKAGE_VERSION: ${{ steps.get-build-info.outputs.PACKAGE_VERSION }} + BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }} + run: | + artifact="$BUILD_DIR/Bluesky.ipa" + checksum=$(shasum -a 256 "$artifact" | awk '{print $1}') + jq -n \ + --arg sourceSha "$SOURCE_SHA" \ + --arg packageVersion "$PACKAGE_VERSION" \ + --arg buildNumber "$BUILD_NUMBER" \ + --arg artifactFilename "Bluesky.ipa" \ + --arg artifactSha256 "$checksum" \ + '{sourceSha: $sourceSha, packageVersion: $packageVersion, buildNumber: $buildNumber, artifactFilename: $artifactFilename, artifactSha256: $artifactSha256, submissionState: "notStarted"}' \ + > ios-build-metadata.json + cat ios-build-metadata.json + + # Keep the exact build and its provenance available for inspection and failed-job retries. - name: 🚀 Upload build artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ios-build-${{ github.run_id }} - retention-days: 1 + retention-days: 14 if-no-files-found: error path: | ${{ env.BUILD_DIR }}/Bluesky.ipa ${{ env.BUILD_DIR }}/Bluesky.app.dSYM.zip + ios-build-metadata.json - name: 📝 Write build summary env: - REMOTE_BUILD_NUMBER: ${{ steps.get-build-info.outputs.BSKY_IOS_BUILD_NUMBER }} + BUILD_NUMBER: ${{ steps.ipa-build-number.outputs.build-number }} + SOURCE_SHA: ${{ steps.source.outputs.sha }} run: | + artifact_sha256=$(jq -r .artifactSha256 ios-build-metadata.json) { echo "### iOS build number" echo - echo "\`$REMOTE_BUILD_NUMBER\`" + echo "- Build number: \`$BUILD_NUMBER\`" + echo "- Source SHA: \`$SOURCE_SHA\`" + echo "- IPA SHA-256: \`$artifact_sha256\`" } >> "$GITHUB_STEP_SUMMARY" submit: @@ -252,6 +286,7 @@ jobs: # Submission and dSYM upload are I/O bound and don't need the xlarge builder. runs-on: macos-26 needs: [build] + if: ${{ inputs.submit != false }} steps: - name: ⬇️ Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -274,6 +309,29 @@ jobs: - name: 🚀 Deploy run: pnpm eas submit -p ios --non-interactive --path ios-build/Bluesky.ipa + - name: Record submission receipt + env: + SOURCE_SHA: ${{ needs.build.outputs.source-sha }} + PACKAGE_VERSION: ${{ needs.build.outputs.package-version }} + BUILD_NUMBER: ${{ needs.build.outputs.build-number }} + run: | + artifact_sha256=$(jq -r .artifactSha256 ios-build/ios-build-metadata.json) + jq -n \ + --arg sourceSha "$SOURCE_SHA" \ + --arg packageVersion "$PACKAGE_VERSION" \ + --arg buildNumber "$BUILD_NUMBER" \ + --arg artifactSha256 "$artifact_sha256" \ + '{sourceSha: $sourceSha, packageVersion: $packageVersion, buildNumber: $buildNumber, artifactSha256: $artifactSha256, submissionState: "submitted"}' \ + > ios-submission.json + + - name: Upload submission receipt + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: ios-submission-${{ github.run_id }} + path: ios-submission.json + retention-days: 14 + if-no-files-found: error + - name: 🪲 Upload dSYM to Sentry env: SENTRY_ORG: blueskyweb diff --git a/docs/release-model.md b/docs/release-model.md index 6d657b970e..03a7c835cf 100644 --- a/docs/release-model.md +++ b/docs/release-model.md @@ -59,3 +59,7 @@ The release model can be exercised locally with `node scripts/release/cli.mjs`. ## Manual preview The **Prepare Cactus Release** workflow accepts a release version and optional source ref. It validates the checked-out package and Expo versions, generates a provisional changelog from commit titles, uploads the prepared release document as an artifact, and summarizes every derived identifier. It has read-only repository permissions and does not create a branch, tag, commit, or GitHub Release. + +## Build provenance + +Native build artifacts are retained with metadata recording their exact source SHA, package version, artifact-derived build number, filename, SHA-256 checksum, and submission state. Store submission can be disabled so production-profile artifacts can be inspected without publishing them.