From ca41f666915c3529094f89f0de3c3f45c7ceb43c Mon Sep 17 00:00:00 2001 From: LiChenyang-GZ <1169281314@qq.com> Date: Sat, 10 Oct 2026 03:22:11 +1100 Subject: [PATCH 1/3] chore(release): deploy the prod backend before store review Button 1 now deploys the API from the submitted commit (waits for approval on the prod environment) and health-checks it, so the reviewer and TestFlight use the matching backend. Button 2 (renamed Prod - Go live) only raises the minimum supported version and records the release. Adds the add-first, remove-later rule to the backend-changes, database-migrations and pr-review skills. --- .claude/skills/backend-changes/SKILL.md | 18 ++++++- .claude/skills/database-migrations/SKILL.md | 17 +++++++ .claude/skills/pr-review/SKILL.md | 2 + .github/workflows/release-prod-app.yml | 44 +++++++++++++---- .github/workflows/release-prod-backend.yml | 52 ++++----------------- .github/workflows/tag-release.yml | 4 +- 6 files changed, 84 insertions(+), 53 deletions(-) diff --git a/.claude/skills/backend-changes/SKILL.md b/.claude/skills/backend-changes/SKILL.md index d8b65ad7d..e565325a5 100644 --- a/.claude/skills/backend-changes/SKILL.md +++ b/.claude/skills/backend-changes/SKILL.md @@ -19,4 +19,20 @@ Features live under `Modules//`: Commands// Queries// Controllers/ Domain/{Entities,Services}/ Events/ Enums/ Services/ DependencyInjection.cs -``` \ No newline at end of file +``` + +## Keep the live app working: add first, remove later + +The production API is deployed **before** the new app goes to store review, so the previous app talks to the new API until the next force update. Every backend change must keep it working. + +**Breaking — never in the same release as the change that replaces it:** + +- **Response fields** removed, renamed, or changed in type or meaning. +- **Requests**: a query param or body field renamed or removed, or a new **required** one the old app doesn't send. +- **Endpoints** deleted or moved. +- **Enum values** renamed, removed or renumbered (adding one at the end is fine). +- **Validation or behaviour**: stricter rules, or a different result for the same request. + +**How to change something anyway:** add the new one next to the old one, keep the old one working, and mark it `// Remove after force update to `. A later release removes it, once the minimum supported version is past every app that used it. Optional new params with a default are fine; so is a new endpoint or field. + +To check what the live app uses, look at the mobile code at the latest `submitted/*` tag (`git grep submitted/ -- blotztask-mobile`), not at `main`. diff --git a/.claude/skills/database-migrations/SKILL.md b/.claude/skills/database-migrations/SKILL.md index 6831f9a0f..d52bd5173 100644 --- a/.claude/skills/database-migrations/SKILL.md +++ b/.claude/skills/database-migrations/SKILL.md @@ -28,3 +28,20 @@ Do NOT execute `dotnet ef migrations add` or `dotnet ef database update`. Always - PascalCase, verb-first, describing the schema change — e.g. `AddLabelRelationToTaskItem`, `CreateTaskItemTable`, `RemoveSeedData`. - Avoid vague names like `Update1` or `Fix`. Older entries like `createDuedateToTaskItem` are inconsistent — don't follow that casing. + +## Add-only within a release + +Migrations run in the production deploy, before the new app goes to review, with no down step. After one runs: + +- **The previous app keeps working.** It talks to the new API straight away (see the backend-changes skill, "add first, remove later"). +- **The previous API still runs on the new schema**, so a rollback to the last tag works. + +So within one release a migration may only **add**: new tables, new nullable columns (or columns with a default), new indexes. These wait for a later release, after the old code that used them is gone from production: + +- `DropTable`, `DropColumn` +- `RenameColumn`, `RenameTable` +- `AlterColumn` that changes type, length or nullability of a column the old code reads or writes + +Moving data to a new shape: add the new table or column and copy the data in this migration (`migrationBuilder.Sql(...)`), keep the old one, drop it in a later release. + +When you explain the migration to the user (step 1 above), say whether it is add-only. If it isn't, say which part must move to a later release. diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index 4620af79d..ff03939fc 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -18,6 +18,8 @@ argument-hint: "[PR number, PR URL, or branch; optional, defaults to current bra 5. For Blotz-specific review, pay extra attention when the PR touches: auth/user scoping, mobile-backend DTO contract changes, date/timezone handling, recurring tasks, AI generation, AI quota usage, review reports, notifications, and EF/database changes. + **Backward compatibility is a major finding.** The production API ships before the new app is reviewed, so the previous app talks to it straight away. Flag any change the live app would notice: an endpoint, DTO field, request param, enum value or column removed, renamed or retyped, or a new required param. Check against the mobile code at the latest `submitted/*` tag, not `main`. Fix: keep the old one next to the new one, remove it in a later release (backend-changes skill). + 6. **Decide whether re-testing is worth it — don't re-test by default.** Read the PR's `Verified:` line first. - Clear proof, and a simple change (`L1`/`L2`) → trust it; don't re-run anything. - Risky change (`L3`/`L4`), or the proof is thin, missing, or doesn't cover what the diff changes → re-run **only** the relevant tests: backend `cd blotztask-test && dotnet test --filter `. A real-device run is expensive — ask the reviewer before starting one. diff --git a/.github/workflows/release-prod-app.yml b/.github/workflows/release-prod-app.yml index 12a454b4e..5d5bf5748 100644 --- a/.github/workflows/release-prod-app.yml +++ b/.github/workflows/release-prod-app.yml @@ -11,11 +11,11 @@ name: Prod - Submit app to the stores # upload lands as a draft (`releaseStatus: "draft"`) that only goes out when # someone starts the rollout in the console. # -# It deploys no backend on purpose. Store review sits in the middle of a -# production release, so by the time it passes, main has usually moved on. -# Button 2 (Prod - Ship backend) deploys the API from the `submitted/` -# tag once the release is live in the stores, which is what keeps the API and the -# binary users installed on one commit. +# Then it deploys the production API from the same commit, before the build is +# sent for review. The deploy waits for approval on the `prod` environment. +# +# Button 2 (Prod - Go live) raises the minimum supported version and records the +# release, once the new version is live in both stores. on: workflow_dispatch: @@ -166,8 +166,8 @@ jobs: ref: main # This tag is the whole point of button 1. It pins the commit that became the - # binaries now waiting in the store consoles, so that button 2 can deploy the - # API from it days later instead of from whatever main has become by then. + # binaries now waiting in the store consoles, so that button 2 can record the + # release against it days later instead of whatever main has become by then. # # Forced because a rejected review is resubmitted under the same version, and # the tag should follow the code that was actually submitted last. @@ -178,7 +178,7 @@ jobs: git tag -f "$TAG" "$GITHUB_SHA" git push --force origin "refs/tags/$TAG" echo "### Tagged \`$TAG\` at \`$GITHUB_SHA\`" >> "$GITHUB_STEP_SUMMARY" - echo "Next: submit for review in App Store Connect, then once the release is live in both stores run **Prod - Ship backend** on this tag." >> "$GITHUB_STEP_SUMMARY" + echo "Next: approve the API deploy below. Once it is healthy, log in once with the prod build on TestFlight and check the task list loads, then submit for review. Once the release is live in **both** stores, run **Prod - Go live** on this tag." >> "$GITHUB_STEP_SUMMARY" - uses: actions/download-artifact@v4 with: @@ -201,3 +201,31 @@ jobs: Tagged as `submitted/${{ needs.version.outputs.new_version }}`. Merge to keep `eas.json` on `main` in step with what is in the stores. + + deploy-api: + name: Deploy the API from the submitted commit + needs: tag-and-record + uses: ./.github/workflows/deploy-prod-api.yml + secrets: inherit + permissions: + id-token: write + contents: read + + verify-api: + name: Health check + needs: deploy-api + runs-on: ubuntu-latest + steps: + - name: Check the production API is healthy + working-directory: . + run: | + set -euo pipefail + BODY=$(curl -fsS --retry 12 --retry-delay 15 --retry-all-errors \ + https://app-blotz-task-api-prod.azurewebsites.net/health) + echo "/health returned: $BODY" + if [ "$BODY" != "Healthy" ]; then + echo "::error::production API is not healthy" + exit 1 + fi + echo "### Production API deployed and healthy" >> "$GITHUB_STEP_SUMMARY" + echo "Log in once with the prod build on TestFlight, then submit for review." >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/release-prod-backend.yml b/.github/workflows/release-prod-backend.yml index fa65a6ca3..0b05356ca 100644 --- a/.github/workflows/release-prod-backend.yml +++ b/.github/workflows/release-prod-backend.yml @@ -1,17 +1,13 @@ -name: Prod - Ship backend +name: Prod - Go live # Button 2 of the two-button production release. # -# Run this AFTER you have pressed release in App Store Connect / started the -# rollout in Play Console AND confirmed the new version is actually live. It -# deploys the production API, points the in-app update prompt at the new version, -# and records the release in GitHub. +# Run this on the `submitted/` tag AFTER you have pressed release in App +# Store Connect AND started the rollout in Play Console, and confirmed the new +# version is actually live in BOTH stores. It points the in-app update prompt at +# the new version and records the release in GitHub. # -# Deploying after the stores publish leaves a short window - the length of this -# run - where a freshly updated app talks to the old API. The rule that keeps -# that harmless: if an app change needs a new endpoint, ship the endpoint ahead -# of the release with a plain "Prod - Build and deploy WebApi" run, the way the -# badges equip endpoint went out before the app that used it. +# It deploys nothing: button 1 already deployed the API from this tag. on: workflow_dispatch: @@ -49,7 +45,7 @@ jobs: set -euo pipefail VERSION="${GITHUB_REF_NAME#submitted/}" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "### Shipping the backend for \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" + echo "### Going live with \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" echo "From \`${{ github.ref_name }}\` (\`${{ github.sha }}\`), not from main." >> "$GITHUB_STEP_SUMMARY" - uses: actions/checkout@v4 @@ -89,37 +85,9 @@ jobs: echo "$PLATFORM build number: ${BUILD:-not found}" done - deploy-api: - name: Deploy the API from the tag - needs: meta - uses: ./.github/workflows/deploy-prod-api.yml - secrets: inherit - permissions: - id-token: write - contents: read - - verify: - name: Health check - needs: deploy-api - runs-on: ubuntu-latest - steps: - # Retries ride out the App Service warm-up after the deploy - a cold .NET - # start can take a couple of minutes. /health also covers the database, - # which matters because the deploy runs EF migrations right before it. - - name: Check the production API is healthy - run: | - set -euo pipefail - BODY=$(curl -fsS --retry 12 --retry-delay 15 --retry-all-errors \ - https://app-blotz-task-api-prod.azurewebsites.net/health) - echo "/health returned: $BODY" - if [ "$BODY" != "Healthy" ]; then - echo "::error::production API is not healthy" - exit 1 - fi - update-prompt: name: Point the update prompt at the new version - needs: [meta, verify] + needs: meta runs-on: ubuntu-latest permissions: id-token: write @@ -189,8 +157,8 @@ jobs: tag-release: name: Record the release - # Last, so a release is only written down once the API is healthy and the - # update prompt is actually serving the new version. + # Last, so a release is only written down once the update prompt is actually + # serving the new version. # # TODO: fold tag-release.yml into this job once the two-button flow has # survived a few real releases. It is ~30 lines whose whole job is one diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 7de0bda7d..cd807d67a 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -1,8 +1,8 @@ name: Tag Release # Records a SHIPPED release (iOS + Android, which ship together) in GitHub. -# Normally called by release-prod-backend.yml (button 2), once the production API -# has been deployed from the release tag and answered its health check. +# Normally called by release-prod-backend.yml (button 2), once the release is +# live in both stores and the update prompt serves the new version. # workflow_dispatch stays for recording a release by hand. # # It creates a `release/` tag + a published GitHub Release whose notes are From 53851e9cdcac0bcb94f63848607468ae9c3a2e1c Mon Sep 17 00:00:00 2001 From: LiChenyang-GZ <1169281314@qq.com> Date: Sun, 11 Oct 2026 20:07:24 +1100 Subject: [PATCH 2/3] chore(release): drop button 2, record the release with Tag Release by hand The minimum supported version is raised by hand in Azure, per platform, as each store goes live. Tag Release now requires the commit and reads the version from a submitted/ tag. --- .github/workflows/release-prod-app.yml | 11 +- .github/workflows/release-prod-backend.yml | 183 --------------------- .github/workflows/tag-release.yml | 46 ++---- 3 files changed, 22 insertions(+), 218 deletions(-) delete mode 100644 .github/workflows/release-prod-backend.yml diff --git a/.github/workflows/release-prod-app.yml b/.github/workflows/release-prod-app.yml index 5d5bf5748..ab4162980 100644 --- a/.github/workflows/release-prod-app.yml +++ b/.github/workflows/release-prod-app.yml @@ -1,6 +1,6 @@ name: Prod - Submit app to the stores -# Button 1 of the two-button production release. +# Production release. # # Builds the production iOS + Android apps from main and uploads them - the iOS # build to App Store Connect, the Android bundle to the Play production track - @@ -14,8 +14,9 @@ name: Prod - Submit app to the stores # Then it deploys the production API from the same commit, before the build is # sent for review. The deploy waits for approval on the `prod` environment. # -# Button 2 (Prod - Go live) raises the minimum supported version and records the -# release, once the new version is live in both stores. +# After that, by hand: raise each platform's minimum supported version in Azure +# once that store is live, and run Tag Release on the `submitted/` tag +# once both are. on: workflow_dispatch: @@ -166,7 +167,7 @@ jobs: ref: main # This tag is the whole point of button 1. It pins the commit that became the - # binaries now waiting in the store consoles, so that button 2 can record the + # binaries now waiting in the store consoles, so that Tag Release can record the # release against it days later instead of whatever main has become by then. # # Forced because a rejected review is resubmitted under the same version, and @@ -178,7 +179,7 @@ jobs: git tag -f "$TAG" "$GITHUB_SHA" git push --force origin "refs/tags/$TAG" echo "### Tagged \`$TAG\` at \`$GITHUB_SHA\`" >> "$GITHUB_STEP_SUMMARY" - echo "Next: approve the API deploy below. Once it is healthy, log in once with the prod build on TestFlight and check the task list loads, then submit for review. Once the release is live in **both** stores, run **Prod - Go live** on this tag." >> "$GITHUB_STEP_SUMMARY" + echo "Next: approve the API deploy below. Once it is healthy, log in once with the prod build on TestFlight and check the task list loads, then submit for review. Raise each platform's minimum supported version in Azure once that store is live. Once both are live, run **Tag Release** with commit \`$TAG\`." >> "$GITHUB_STEP_SUMMARY" - uses: actions/download-artifact@v4 with: diff --git a/.github/workflows/release-prod-backend.yml b/.github/workflows/release-prod-backend.yml deleted file mode 100644 index 0b05356ca..000000000 --- a/.github/workflows/release-prod-backend.yml +++ /dev/null @@ -1,183 +0,0 @@ -name: Prod - Go live - -# Button 2 of the two-button production release. -# -# Run this on the `submitted/` tag AFTER you have pressed release in App -# Store Connect AND started the rollout in Play Console, and confirmed the new -# version is actually live in BOTH stores. It points the in-app update prompt at -# the new version and records the release in GitHub. -# -# It deploys nothing: button 1 already deployed the API from this tag. - -on: - workflow_dispatch: - -concurrency: - group: release-prod-backend - cancel-in-progress: false - -jobs: - meta: - name: Check the tag and collect build numbers - runs-on: ubuntu-latest - outputs: - version: ${{ steps.parse.outputs.version }} - ios_build: ${{ steps.builds.outputs.ios_build }} - android_build: ${{ steps.builds.outputs.android_build }} - steps: - # The dropdown accepts branches too. Running from main is exactly the mistake - # this button exists to prevent, so refuse anything that is not a release tag. - - name: Refuse anything but a submitted/* tag - run: | - set -euo pipefail - if [ "${{ github.ref_type }}" != "tag" ]; then - echo "::error::Run this on the submitted/ tag, not on ${{ github.ref_name }}." - exit 1 - fi - case "${{ github.ref_name }}" in - submitted/*) ;; - *) echo "::error::${{ github.ref_name }} is not a submitted/* tag."; exit 1 ;; - esac - - - name: Read the version out of the tag name - id: parse - run: | - set -euo pipefail - VERSION="${GITHUB_REF_NAME#submitted/}" - echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "### Going live with \`$VERSION\`" >> "$GITHUB_STEP_SUMMARY" - echo "From \`${{ github.ref_name }}\` (\`${{ github.sha }}\`), not from main." >> "$GITHUB_STEP_SUMMARY" - - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: 22.x - - - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - # Build numbers only decorate the release title, so this must never be able to - # fail the run - a change in the EAS output shape should cost a nice title, not - # a deploy. They used to be copied by hand out of App Store Connect. - - name: Look up the build numbers on EAS - id: builds - continue-on-error: true - env: - EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} - VERSION: ${{ steps.parse.outputs.version }} - working-directory: blotztask-mobile - run: | - set -uo pipefail - for PLATFORM in ios android; do - BUILD=$(eas build:list \ - --platform "$PLATFORM" \ - --status finished \ - --limit 20 \ - --json \ - --non-interactive 2>/dev/null \ - | jq -r --arg v "$VERSION" \ - 'map(select(.appVersion == $v)) | .[0].appBuildVersion // empty' \ - || true) - echo "${PLATFORM}_build=$BUILD" >> "$GITHUB_OUTPUT" - echo "$PLATFORM build number: ${BUILD:-not found}" - done - - update-prompt: - name: Point the update prompt at the new version - needs: meta - runs-on: ubuntu-latest - permissions: - id-token: write - contents: read - # Required: the AZURE_* secrets live on this environment, and the managed - # identity's federated credential is scoped to - # `repo:sol-wizard/Blotz-Task-App:environment:prod` - a job without it gets a - # token Entra ID will not accept. - environment: - name: "prod" - steps: - - name: Azure login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_GHA_MANAGED_IDENTITY_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_GHA_MANAGED_IDENTITY_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - # MinimumSupportedVersion follows LatestVersion, which is what production - # already carries. Every release so far has changed the API in ways older - # builds cannot talk to, so the wall goes up with the release: from here the - # only client that can reach the API is the one it was deployed for. - # - # This is safe only because the workflow runs AFTER the stores have - # published - the users it locks out can all install the version it demands. - # Move this step earlier and it becomes a wall with nothing behind it. - # - # To ship a release without forcing the update, lower - # MinimumSupportedVersion in the portal afterwards. - # - # `appsettings set` adds and updates only the settings named here; it leaves - # everything else on the App Service alone. - - name: Point the update prompt at ${{ needs.meta.outputs.version }} - run: | - az webapp config appsettings set \ - --name app-blotz-task-api-prod \ - --resource-group rg-blotz-task-prod \ - --settings \ - AppVersion__Ios__LatestVersion=${{ needs.meta.outputs.version }} \ - AppVersion__Ios__MinimumSupportedVersion=${{ needs.meta.outputs.version }} \ - AppVersion__Android__LatestVersion=${{ needs.meta.outputs.version }} \ - AppVersion__Android__MinimumSupportedVersion=${{ needs.meta.outputs.version }} \ - --output none - - - name: azure logout - if: always() - run: az logout - - # Writing app settings restarts the App Service, so this waits out the cold - # start and confirms the policy the app will actually read - a reachable API - # is not proof that the new configuration took. - - name: Check the served policy - run: | - set -euo pipefail - BODY=$(curl -fsS --retry 12 --retry-delay 15 --retry-all-errors \ - https://app-blotz-task-api-prod.azurewebsites.net/api/app-version) - echo "$BODY" | jq . - EXPECTED="${{ needs.meta.outputs.version }}" - for FIELD in .ios.latestVersion .ios.minimumSupportedVersion \ - .android.latestVersion .android.minimumSupportedVersion; do - SERVED=$(echo "$BODY" | jq -r "$FIELD") - if [ "$SERVED" != "$EXPECTED" ]; then - echo "::error::API is serving $FIELD = $SERVED, expected $EXPECTED" - exit 1 - fi - done - - tag-release: - name: Record the release - # Last, so a release is only written down once the update prompt is actually - # serving the new version. - # - # TODO: fold tag-release.yml into this job once the two-button flow has - # survived a few real releases. It is ~30 lines whose whole job is one - # `gh release create`, and this is the only caller that matters - its own - # workflow_dispatch is a rarely used manual fallback. Kept separate for now - # because it is proven code and none of this has run against a real release - # yet; rewriting it would buy one fewer file at the cost of re-verifying - # something that already works. - needs: [meta, update-prompt] - uses: ./.github/workflows/tag-release.yml - permissions: - contents: write - with: - # Passed explicitly rather than left blank. Tag Release falls back to reading - # eas.json, and the commit this tag points at still carries the OLD version - - # button 1 bumps the version inside its own run and only writes it back to - # main through a pull request afterwards. - ios_version: ${{ needs.meta.outputs.version }} - android_version: ${{ needs.meta.outputs.version }} - ios_build: ${{ needs.meta.outputs.ios_build }} - android_build: ${{ needs.meta.outputs.android_build }} - commit: ${{ github.sha }} diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index cd807d67a..66d568e9f 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -1,9 +1,8 @@ name: Tag Release # Records a SHIPPED release (iOS + Android, which ship together) in GitHub. -# Normally called by release-prod-backend.yml (button 2), once the release is -# live in both stores and the update prompt serves the new version. -# workflow_dispatch stays for recording a release by hand. +# Run it by hand on the `submitted/` tag once the release is live in +# both stores. # # It creates a `release/` tag + a published GitHub Release whose notes are # auto-generated from every PR merged since the previous release. This published @@ -11,40 +10,23 @@ name: Tag Release # It builds and submits nothing, and needs no Expo/App Store credentials. on: - workflow_call: - inputs: - ios_version: - type: string - required: false - ios_build: - type: string - required: false - android_version: - type: string - required: false - android_build: - type: string - required: false - commit: - type: string - required: false workflow_dispatch: inputs: ios_version: - description: "iOS version shipped (blank = read from eas.json production.ios)" + description: "iOS version shipped (blank = the version in a submitted/ tag)" required: false ios_build: description: "iOS build number from App Store Connect (e.g. 139)" required: false android_version: - description: "Android version shipped (blank = read from eas.json production.android)" + description: "Android version shipped (blank = the version in a submitted/ tag)" required: false android_build: description: "Android version code / build from Play Console" required: false commit: - description: "What was built - a commit SHA, branch, or tag such as submitted/1.3.1 (blank = latest main)" - required: false + description: "What was built - normally the submitted/ tag, e.g. submitted/1.4.1" + required: true permissions: contents: write @@ -57,18 +39,22 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - ref: ${{ inputs.commit || 'main' }} + ref: ${{ inputs.commit }} - name: Resolve versions and tag id: meta run: | + set -euo pipefail + COMMIT="${{ inputs.commit }}" + TAG_V="" + case "$COMMIT" in submitted/*) TAG_V="${COMMIT#submitted/}" ;; esac IOS_V="${{ inputs.ios_version }}" - if [ -z "$IOS_V" ]; then - IOS_V=$(jq -r '.build.production.ios.env.APP_VERSION' blotztask-mobile/eas.json) - fi + IOS_V="${IOS_V:-$TAG_V}" AND_V="${{ inputs.android_version }}" - if [ -z "$AND_V" ]; then - AND_V=$(jq -r '.build.production.android.env.APP_VERSION' blotztask-mobile/eas.json) + AND_V="${AND_V:-$TAG_V}" + if [ -z "$IOS_V" ] || [ -z "$AND_V" ]; then + echo "::error::Give both versions, or run on a submitted/ tag." + exit 1 fi { echo "ios_v=$IOS_V" From e9f56da37bb823c93d42e0122b6782290a027d46 Mon Sep 17 00:00:00 2001 From: LiChenyang-GZ <1169281314@qq.com> Date: Sun, 18 Oct 2026 18:48:34 +1100 Subject: [PATCH 3/3] chore(release): check the live release's tag, name LatestVersion in the manual steps --- .claude/skills/backend-changes/SKILL.md | 2 +- .claude/skills/pr-review/SKILL.md | 2 +- .github/workflows/release-prod-app.yml | 12 +++++++----- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.claude/skills/backend-changes/SKILL.md b/.claude/skills/backend-changes/SKILL.md index e565325a5..e7426459a 100644 --- a/.claude/skills/backend-changes/SKILL.md +++ b/.claude/skills/backend-changes/SKILL.md @@ -35,4 +35,4 @@ The production API is deployed **before** the new app goes to store review, so t **How to change something anyway:** add the new one next to the old one, keep the old one working, and mark it `// Remove after force update to `. A later release removes it, once the minimum supported version is past every app that used it. Optional new params with a default are fine; so is a new endpoint or field. -To check what the live app uses, look at the mobile code at the latest `submitted/*` tag (`git grep submitted/ -- blotztask-mobile`), not at `main`. +To check what the live app uses, look at the mobile code for the version in the latest published GitHub Release (`gh release view --json name -q .name`, e.g. "Release release/2026-10-17 — iOS 1.4.0 (161) · Android 1.4.0 (105)"), then `git grep submitted/ -- blotztask-mobile`. Not `main`, and not simply the newest `submitted/*` tag: that tag is made at upload, so it can be a build still in review that no user has yet. Tag Release only publishes the release once both stores are live. diff --git a/.claude/skills/pr-review/SKILL.md b/.claude/skills/pr-review/SKILL.md index ff03939fc..7a8c70c3b 100644 --- a/.claude/skills/pr-review/SKILL.md +++ b/.claude/skills/pr-review/SKILL.md @@ -18,7 +18,7 @@ argument-hint: "[PR number, PR URL, or branch; optional, defaults to current bra 5. For Blotz-specific review, pay extra attention when the PR touches: auth/user scoping, mobile-backend DTO contract changes, date/timezone handling, recurring tasks, AI generation, AI quota usage, review reports, notifications, and EF/database changes. - **Backward compatibility is a major finding.** The production API ships before the new app is reviewed, so the previous app talks to it straight away. Flag any change the live app would notice: an endpoint, DTO field, request param, enum value or column removed, renamed or retyped, or a new required param. Check against the mobile code at the latest `submitted/*` tag, not `main`. Fix: keep the old one next to the new one, remove it in a later release (backend-changes skill). + **Backward compatibility is a major finding.** The production API ships before the new app is reviewed, so the previous app talks to it straight away. Flag any change the live app would notice: an endpoint, DTO field, request param, enum value or column removed, renamed or retyped, or a new required param. Check against the mobile code at the `submitted/` tag of the latest published GitHub Release (`gh release view --json name -q .name`), not `main` and not the newest `submitted/*` tag, which may still be in review. Fix: keep the old one next to the new one, remove it in a later release (backend-changes skill). 6. **Decide whether re-testing is worth it — don't re-test by default.** Read the PR's `Verified:` line first. - Clear proof, and a simple change (`L1`/`L2`) → trust it; don't re-run anything. diff --git a/.github/workflows/release-prod-app.yml b/.github/workflows/release-prod-app.yml index ab4162980..93a454da2 100644 --- a/.github/workflows/release-prod-app.yml +++ b/.github/workflows/release-prod-app.yml @@ -14,9 +14,11 @@ name: Prod - Submit app to the stores # Then it deploys the production API from the same commit, before the build is # sent for review. The deploy waits for approval on the `prod` environment. # -# After that, by hand: raise each platform's minimum supported version in Azure -# once that store is live, and run Tag Release on the `submitted/` tag -# once both are. +# After that, by hand, once each store is live: in Azure set that platform's +# `AppVersion____LatestVersion` to the new version (every release, it +# drives the optional "update available" prompt), and its +# `AppVersion____MinimumSupportedVersion` too if this release forces +# the update. Run Tag Release on the `submitted/` tag once both are live. on: workflow_dispatch: @@ -166,7 +168,7 @@ jobs: fetch-depth: 0 ref: main - # This tag is the whole point of button 1. It pins the commit that became the + # This tag is the whole point of this workflow. It pins the commit that became the # binaries now waiting in the store consoles, so that Tag Release can record the # release against it days later instead of whatever main has become by then. # @@ -179,7 +181,7 @@ jobs: git tag -f "$TAG" "$GITHUB_SHA" git push --force origin "refs/tags/$TAG" echo "### Tagged \`$TAG\` at \`$GITHUB_SHA\`" >> "$GITHUB_STEP_SUMMARY" - echo "Next: approve the API deploy below. Once it is healthy, log in once with the prod build on TestFlight and check the task list loads, then submit for review. Raise each platform's minimum supported version in Azure once that store is live. Once both are live, run **Tag Release** with commit \`$TAG\`." >> "$GITHUB_STEP_SUMMARY" + echo "Next: approve the API deploy below. Once it is healthy, log in once with the prod build on TestFlight and check the task list loads, then submit for review. Once each store is live, set that platform's \`AppVersion____LatestVersion\` in Azure to the new version, and its \`MinimumSupportedVersion\` too if this release forces the update. Once both are live, run **Tag Release** with commit \`$TAG\`." >> "$GITHUB_STEP_SUMMARY" - uses: actions/download-artifact@v4 with: