diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml index f1c54c6cb0..8043077374 100644 --- a/.github/workflows/build-submit-ios.yml +++ b/.github/workflows/build-submit-ios.yml @@ -33,6 +33,11 @@ on: type: string description: TestFlight group to assign the build to after submitting ("none" to skip) default: none + changelog: + type: string + description: TestFlight "What to Test" notes (only applied when a group is selected) + required: false + default: '' runner: type: string description: Runner for the build job (defaults to macos-26-xlarge) diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml index d4e18cc510..89f2780fd7 100644 --- a/.github/workflows/bundle-deploy-eas-update.yml +++ b/.github/workflows/bundle-deploy-eas-update.yml @@ -54,6 +54,8 @@ 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 }} + # Changelog since the last native testflight build, for TestFlight notes and Slack + notes: ${{ steps.notes.outputs.notes }} steps: - name: 🔑 Check for EXPO_TOKEN @@ -177,6 +179,57 @@ jobs: retention-days: 1 if-no-files-found: error + # Native builds go out to the QA Team TestFlight group, so generate a changelog of + # what changed since the last native testflight build. That build's commit SHA is + # stored as a "testflight-native-build-commit" artifact, advanced by recordBaseline + # only after both native builds succeed, so a failed run's commits roll into the + # next successful build's notes rather than being silently dropped. + - name: 📝 Generate release notes + id: notes + if: ${{ steps.fingerprint.outputs.includes-changes || + steps.version.outputs.version-changed }} + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY_ID: ${{ github.repository_id }} + run: | + prev="" + url=$(gh api \ + "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=testflight-native-build-commit&per_page=100" \ + --jq "[.artifacts[] | select( + .expired == false and + .workflow_run.head_branch == \"main\" and + .workflow_run.head_repository_id == (\$ENV.REPOSITORY_ID | tonumber) + )] | max_by(.created_at) | .archive_download_url" \ + 2>/dev/null || true) + if [ -n "$url" ] && [ "$url" != "null" ]; then + if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o marker.zip "$url" \ + && unzip -o -q marker.zip; then + prev=$(tr -d '[:space:]' < testflight-native-build-commit.txt 2>/dev/null || true) + fi + rm -f marker.zip testflight-native-build-commit.txt + fi + + if [ -n "$prev" ] && git cat-file -e "${prev}^{commit}" 2>/dev/null; then + echo "Generating notes since previous native build: $prev" + range="${prev}..HEAD" + else + echo "No reachable previous native build commit; falling back to last 30 commits." + range="HEAD~30..HEAD" + fi + notes=$(git log --no-merges --pretty=format:'- %s' "$range" 2>/dev/null | head -n 50) + if [ -z "$notes" ]; then + notes="No new commits since the last native build." + fi + # Cap the whole changelog: TestFlight "What to Test" allows 4000 characters and + # this also keeps the Slack message a reasonable size. head -c caps the combined + # stream; cut -c would only cap each line independently. + notes=$(printf '%s' "$notes" | head -c 3900) + { + echo "notes<> "$GITHUB_OUTPUT" + - name: 🔤 Compile translations uses: ./.github/actions/compile-i18n @@ -265,7 +318,8 @@ jobs: uses: ./.github/workflows/build-submit-ios.yml with: profile: testflight - testFlightGroup: none + testFlightGroup: "QA Team" + changelog: ${{ needs.bundleDeploy.outputs.notes }} # OTA rebuilds don't need the xlarge builder used for releases runner: macos-26 # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, @@ -316,6 +370,66 @@ jobs: ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + notifyIOS: + name: Notify Slack of iOS build + needs: [bundleDeploy, buildIfNecessaryIOS] + runs-on: ubuntu-latest + steps: + - name: 📝 Build Slack payload + id: payload + env: + NOTES: ${{ needs.bundleDeploy.outputs.notes }} + VERSION: ${{ needs.buildIfNecessaryIOS.outputs.package-version }} + BUILD_NUMBER: ${{ needs.buildIfNecessaryIOS.outputs.build-number }} + run: | + text="*New iOS build available in TestFlight (QA Team)* + Version ${VERSION} (${BUILD_NUMBER}) + + ${NOTES}" + payload=$(jq -n --arg text "$text" '{text: $text}') + { + echo "payload<> "$GITHUB_OUTPUT" + + - name: 🔔 Notify Slack + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} + webhook-type: incoming-webhook + payload: ${{ steps.payload.outputs.payload }} + + notifyAndroid: + name: Notify Slack of Android build + needs: [bundleDeploy, buildIfNecessaryAndroid] + runs-on: ubuntu-latest + steps: + - name: 📝 Build Slack payload + id: payload + env: + NOTES: ${{ needs.bundleDeploy.outputs.notes }} + VERSION: ${{ needs.buildIfNecessaryAndroid.outputs.package-version }} + VERSION_CODE: ${{ needs.buildIfNecessaryAndroid.outputs.version-code }} + run: | + text="*New Android build available (Internal track)* + Version ${VERSION} (${VERSION_CODE}) + + ${NOTES}" + payload=$(jq -n --arg text "$text" '{text: $text}') + { + echo "payload<> "$GITHUB_OUTPUT" + + - name: 🔔 Notify Slack + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} + webhook-type: incoming-webhook + payload: ${{ steps.payload.outputs.payload }} + # Advance the fingerprint baseline only after BOTH native builds have shipped # the new native surface. This replaces the old actions/cache baseline, which # only advanced on cache eviction and so silently froze - freezing meant every @@ -360,3 +474,20 @@ jobs: path: native-fingerprint.json retention-days: 90 if-no-files-found: error + + # The changelog marker only moves when native builds actually shipped; an OTA-only + # deploy must not advance it, or the next build's notes would miss those commits. + - name: ✏️ Write native build commit marker + if: ${{ needs.bundleDeploy.outputs.changes-detected == 'true' }} + env: + GITHUB_SHA: ${{ github.sha }} + run: echo "$GITHUB_SHA" > testflight-native-build-commit.txt + + - name: 🚀 Record native build commit marker + if: ${{ needs.bundleDeploy.outputs.changes-detected == 'true' }} + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: testflight-native-build-commit + path: testflight-native-build-commit.txt + retention-days: 90 + if-no-files-found: error diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml deleted file mode 100644 index bd5b9ba351..0000000000 --- a/.github/workflows/nightly-build.yml +++ /dev/null @@ -1,200 +0,0 @@ ---- -name: Nightly Build - -on: - schedule: - - cron: "10 2 * * *" # run at 2:10 AM UTC, after the nightly i18n job - workflow_dispatch: - -# Deploys happen via EAS using EXPO_TOKEN; the GITHUB_TOKEN only checks out code -permissions: - contents: read - -jobs: - # Generate the changelog once, shared by both platforms. The range covers commits since - # the previous nightly, whose commit SHA is stored as a "nightly-build-commit" artifact - # (advanced by the record job below, only after both builds succeed). - prepare: - name: Prepare release notes - if: github.repository == 'bluesky-social/social-app' - runs-on: ubuntu-latest - permissions: - contents: read - actions: read - outputs: - notes: ${{ steps.notes.outputs.notes }} - steps: - - name: ⬇️ Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - - - name: 📝 Generate release notes - id: notes - env: - GH_TOKEN: ${{ github.token }} - run: | - # Find the most recent non-expired nightly-build-commit artifact and read its SHA - # The artifacts API returns results newest-first, so the most recent marker is on - # page 1 — no --paginate needed (which would run the jq aggregation per page and - # could emit multiple URLs). Take the first non-expired match. - prev="" - url=$(gh api \ - "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=nightly-build-commit&per_page=100" \ - --jq 'first(.artifacts[] | select(.expired == false)) | .archive_download_url' \ - 2>/dev/null || true) - if [ -n "$url" ] && [ "$url" != "null" ]; then - if curl -sSL -H "Authorization: Bearer $GH_TOKEN" -o marker.zip "$url" \ - && unzip -o -q marker.zip; then - prev=$(cat nightly-build-commit.txt 2>/dev/null | tr -d '[:space:]') - fi - rm -f marker.zip nightly-build-commit.txt - fi - - if [ -n "$prev" ] && git cat-file -e "${prev}^{commit}" 2>/dev/null; then - echo "Generating notes since previous nightly: $prev" - range="${prev}..HEAD" - else - echo "No reachable previous nightly commit; falling back to last 30 commits." - range="HEAD~30..HEAD" - fi - notes=$(git log --no-merges --pretty=format:'- %s' "$range" 2>/dev/null | head -n 50) - if [ -z "$notes" ]; then - notes="Nightly build — no new commits since the last nightly." - fi - # Cap the whole changelog to keep the Slack message a reasonable size. - # head -c caps the combined stream; cut -c would only cap each line independently. - notes=$(printf '%s' "$notes" | head -c 3900) - { - echo "notes<> "$GITHUB_OUTPUT" - - ios: - name: Nightly iOS Build - needs: [prepare] - uses: ./.github/workflows/build-submit-ios.yml - with: - profile: testflight - testFlightGroup: "QA Team" - # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, - # so the nightly build never hands the reusable workflow the entire repo secret store. - secrets: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - ENV_TOKEN: ${{ secrets.ENV_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} - EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} - GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }} - ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }} - ASC_KEY_P8_BASE64: ${{ secrets.ASC_KEY_P8_BASE64 }} - SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} - - android: - name: Nightly Android Build - needs: [prepare] - # build-submit-android.yml contains an attachToRelease job that requests contents: write. - # That job is skipped for nightly (it needs a production tag build), but GitHub statically - # validates the reusable-workflow permission ceiling, so the caller must grant it here. - permissions: - contents: write - uses: ./.github/workflows/build-submit-android.yml - with: - profile: testflight-android - # Pass only the secrets the reusable workflow declares, rather than `secrets: inherit`, - # so the nightly build never hands the reusable workflow the entire repo secret store. - secrets: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - ENV_TOKEN: ${{ secrets.ENV_TOKEN }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN }} - BITDRIFT_API_KEY: ${{ secrets.BITDRIFT_API_KEY }} - EXPO_PUBLIC_GCP_PROJECT_ID: ${{ secrets.EXPO_PUBLIC_GCP_PROJECT_ID }} - GOOGLE_SERVICES_TOKEN: ${{ secrets.GOOGLE_SERVICES_TOKEN }} - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SLACK_CLIENT_ALERT_WEBHOOK: ${{ secrets.SLACK_CLIENT_ALERT_WEBHOOK }} - ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} - ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} - ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} - - notify-ios: - name: Notify Slack of iOS nightly - needs: [prepare, ios] - runs-on: ubuntu-latest - steps: - - name: 📝 Build Slack payload - id: payload - env: - NOTES: ${{ needs.prepare.outputs.notes }} - VERSION: ${{ needs.ios.outputs.package-version }} - BUILD_NUMBER: ${{ needs.ios.outputs.build-number }} - run: | - text="*Nightly iOS build available in TestFlight (QA Team)* - Version ${VERSION} (${BUILD_NUMBER}) - - ${NOTES}" - payload=$(jq -n --arg text "$text" '{text: $text}') - { - echo "payload<> "$GITHUB_OUTPUT" - - - name: 🔔 Notify Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} - webhook-type: incoming-webhook - payload: ${{ steps.payload.outputs.payload }} - - notify-android: - name: Notify Slack of Android nightly - needs: [prepare, android] - runs-on: ubuntu-latest - steps: - - name: 📝 Build Slack payload - id: payload - env: - NOTES: ${{ needs.prepare.outputs.notes }} - VERSION: ${{ needs.android.outputs.package-version }} - VERSION_CODE: ${{ needs.android.outputs.version-code }} - run: | - text="*Nightly Android build available (Internal track)* - Version ${VERSION} (${VERSION_CODE}) - - ${NOTES}" - payload=$(jq -n --arg text "$text" '{text: $text}') - { - echo "payload<> "$GITHUB_OUTPUT" - - - name: 🔔 Notify Slack - uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 - with: - webhook: ${{ secrets.NIGHTLY_BUILDS_SLACK_WEBHOOK }} - webhook-type: incoming-webhook - payload: ${{ steps.payload.outputs.payload }} - - # Advance the nightly marker only after both builds succeed, so a failed night's commits - # roll into the next successful nightly's notes rather than being silently dropped. - record: - name: Record nightly commit - needs: [ios, android] - runs-on: ubuntu-latest - steps: - - name: ✏️ Write nightly commit marker - env: - GITHUB_SHA: ${{ github.sha }} - run: echo "$GITHUB_SHA" > nightly-build-commit.txt - - - name: 🚀 Upload nightly commit marker - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: nightly-build-commit - path: nightly-build-commit.txt - retention-days: 90