diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml index e01402824f..bdf4c04c96 100644 --- a/.github/workflows/build-submit-android.yml +++ b/.github/workflows/build-submit-android.yml @@ -261,16 +261,16 @@ jobs: GH_TOKEN: ${{ github.token }} TAG: ${{ github.ref_name }} run: | - status=$(curl -sS -o /dev/null -w '%{http_code}' \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}") - if [ "$status" = "200" ]; then + if response=$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" 2>&1); then echo "Found GitHub Release for tag $TAG" echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "No GitHub Release found for tag $TAG (HTTP $status); skipping APK attachment." + elif [[ "$response" == *"(HTTP 404)"* ]]; then + echo "No GitHub Release found for tag $TAG; skipping APK attachment." echo "exists=false" >> "$GITHUB_OUTPUT" + else + echo "::error::Could not check for a GitHub Release for tag $TAG." + echo "$response" + exit 1 fi - name: ⬇️ Download APK artifact diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..671effc2cd --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,184 @@ +--- +name: Release + +on: + # Publishing a release guarantees the release exists before production submissions begin. + # The release job below only accepts numeric semver tags matching package.json. + release: + types: [published] + +# Key each parent run by tag so a newly published release cannot evict a different pending run. +# The reusable platform workflows retain their own concurrency groups to serialize app builds. +concurrency: + group: release-${{ github.event.release.tag_name }} + cancel-in-progress: false + +# Permissions are granted per job. The mobile build jobs only receive the secrets declared by +# their reusable workflows rather than the repository's entire secret store. +permissions: + contents: read + +jobs: + release: + name: Validate release tag + if: github.repository == 'bluesky-social/social-app' + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + steps: + - name: ⬇️ Checkout release commit + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: 🔎 Validate release tag and version + id: version + env: + RELEASE_TAG: ${{ github.ref_name }} + run: | + version=$(jq -r '.version' package.json) + if ! [[ "$RELEASE_TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Release tag must be a semver version such as 1.130.0; found '$RELEASE_TAG'." + exit 1 + fi + + if [ "$RELEASE_TAG" != "$version" ]; then + echo "::error file=package.json::Release tag is $RELEASE_TAG, but package.json is version $version." + exit 1 + fi + + echo "Releasing $version from $GITHUB_SHA" + echo "version=$version" >> "$GITHUB_OUTPUT" + + ios: + name: Production iOS Build + needs: [release] + uses: ./.github/workflows/build-submit-ios.yml + with: + profile: production + testFlightGroup: "QA Team" + 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: Production Android Build + needs: [release] + # The reusable workflow's APK attachment job needs this permission. The build and submit + # jobs remain read-only through their own job/workflow permission declarations. + permissions: + contents: write + uses: ./.github/workflows/build-submit-android.yml + with: + profile: production + 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 }} + + bump-version: + name: Open next version PR + needs: [release, ios, android] + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + # The builds can take long enough for main to advance. Base the bump on the latest main, + # but verify its version is still the one that this workflow just released. + - name: ⬇️ Checkout latest main + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: main + persist-credentials: false + + - name: 🧮 Compute next version + id: version + env: + RELEASED: ${{ needs.release.outputs.version }} + run: | + current=$(jq -r '.version' package.json) + if [ "$current" != "$RELEASED" ]; then + echo "::error file=package.json::main is now version $current, but this workflow released $RELEASED. Refusing to overwrite a newer version." + exit 1 + fi + + next=$(echo "$RELEASED" | jq -R -r 'split(".") | "\(.[0]).\(.[1] | tonumber + 1).0"') + echo "Bumping $RELEASED to $next" + echo "next=$next" >> "$GITHUB_OUTPUT" + echo "branch=bot/version-bump-$next" >> "$GITHUB_OUTPUT" + + - name: 🔁 Check for an existing version-bump PR + id: dedup + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.version.outputs.branch }} + run: | + existing=$(gh pr list --state open --head "$BRANCH" --json url --jq '.[0].url // ""') + if [ -n "$existing" ]; then + echo "Version-bump PR already exists: $existing" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: ✏️ Bump version + if: steps.dedup.outputs.skip == 'false' + env: + NEXT: ${{ steps.version.outputs.next }} + run: | + jq --indent 2 --arg version "$NEXT" '.version = $version' package.json > package.json.tmp + mv package.json.tmp package.json + + changed=$(git diff --numstat package.json | awk '{print $1"+"$2}') + if [ "$changed" != "1+1" ]; then + echo "::error file=package.json::Expected the version bump to change exactly one line." + git diff -- package.json + exit 1 + fi + + - name: 📤 Create branch and pull request + if: steps.dedup.outputs.skip == 'false' + env: + GH_TOKEN: ${{ github.token }} + BRANCH: ${{ steps.version.outputs.branch }} + NEXT: ${{ steps.version.outputs.next }} + RELEASED: ${{ needs.release.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" + + # A previous attempt may have pushed this deterministic bot branch and then failed + # before creating its PR. Fetch it so force-with-lease can replace only that known tip. + git fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" || true + git checkout -B "$BRANCH" + git add package.json + git commit -m "bump version to $NEXT" + git push --force-with-lease --set-upstream origin "$BRANCH" + + # This requires "Allow GitHub Actions to create and approve pull requests" in the + # repository's Actions settings; no long-lived personal access token is needed. + gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "Bump version to $NEXT" \ + --body "Released $RELEASED. Bumps the app version to $NEXT for the next release cycle."