From cb9f91b815497ce081ebee5a9b0554399e5f634c Mon Sep 17 00:00:00 2001 From: YuYu1015 Date: Mon, 12 Oct 2026 01:09:50 +0800 Subject: [PATCH] ci(release): publish no snapshot for a push with nothing user-facing Every push to main published a snapshot: a GitHub pre-release, a Play upload and a TestFlight build. So a ci:, docs:, test: or refactor: push went out to testers as a build whose whole note was "no user-facing changes", and spent a store build number on it. A push now publishes one only when a commit since the last tag carries a changelog entry: exactly the case where the snapshot note has something to say. The commits it skips are not lost; the next push that does bring an entry runs its note from the same tag. A run started by hand still publishes, and so does a v* tag. android.yml and ios.yml built pull requests only, on the grounds that release.yml builds every push to main; that no longer holds, and a chore(deps) bump pushed straight to main would have been compiled by nothing. They now also run for a push to main that publishes nothing. They ask about the push's own commits, which always fall inside the range release.yml asks about, so a push is built by release.yml, by them, or at worst by both, never by neither. Asking release.yml's own question instead would not be safe here: its tag is pushed at the end of a run, and a run queued behind another sees a tag the push did not. All of them ask through tool/release/notes.sh --any, which reads entries with the same expression the note does, so they cannot disagree on what counts as user-facing. --- .github/workflows/android.yml | 37 +++++++++++++++++++++++++++++++---- .github/workflows/ios.yml | 37 +++++++++++++++++++++++++++++++---- .github/workflows/release.yml | 30 +++++++++++++++++++++++++--- README.md | 2 +- tool/release/notes.sh | 27 ++++++++++++++++++++++--- 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 16ad09aca..de4c6d32c 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,11 +1,15 @@ name: Build Android -# Pull requests only. A push to `main` is handled by release.yml, -# which builds the signed, uploadable artifact — running this as well would -# spend a second runner producing an unsigned one nobody installs. +# Pull requests, and the pushes to `main` that publish no snapshot. A push +# that does is handled by release.yml, which builds the signed, uploadable +# artifact — running this as well would spend a second runner producing an +# unsigned one nobody installs. One that does not would otherwise be built by +# nothing at all. on: pull_request: branches: ["main"] + push: + branches: ["main"] jobs: changes: @@ -14,8 +18,14 @@ jobs: contents: read outputs: android: ${{ steps.filter.outputs.android }} + release: ${{ steps.release.outputs.builds }} steps: - uses: actions/checkout@v5 + with: + # A push reads its own commit messages below: full history, without + # the file contents nothing here looks at. + fetch-depth: ${{ github.event_name == 'push' && '0' || '1' }} + filter: ${{ github.event_name == 'push' && 'blob:none' || '' }} - uses: dorny/paths-filter@v4 id: filter with: @@ -26,9 +36,28 @@ jobs: - 'lib/**' - 'pubspec.yaml' + # Whether release.yml builds this push, for the job below. It publishes + # a snapshot only when a commit since the last tag carries a changelog + # entry; asked here of the push's own commits, which are always within + # that range. So "yes" here is always "yes" there, and a push this + # answers "no" for is built here — at worst by both, never by neither. + - name: Does release.yml build this push + id: release + if: github.event_name == 'push' + env: + BEFORE: ${{ github.event.before }} + run: | + if bash tool/release/notes.sh --any "$BEFORE..$GITHUB_SHA"; then + echo "builds=true" >> "$GITHUB_OUTPUT" + echo "this push brings a changelog entry: release.yml builds it" + else + echo "builds=false" >> "$GITHUB_OUTPUT" + echo "no changelog entry in this push: no snapshot, so it is built here" + fi + Android: needs: changes - if: ${{ needs.changes.outputs.android == 'true' }} + if: ${{ needs.changes.outputs.android == 'true' && needs.changes.outputs.release != 'true' }} runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index c1bfce532..a68e3c1e3 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -1,8 +1,10 @@ name: Build iOS -# Pull requests only. A push to `main` is handled by release.yml, -# which builds the signed, uploadable artifact — running this as well would -# spend a second runner producing an unsigned one nobody installs. +# Pull requests, and the pushes to `main` that publish no snapshot. A push +# that does is handled by release.yml, which builds the signed, uploadable +# artifact — running this as well would spend a second runner producing an +# unsigned one nobody installs. One that does not would otherwise be built by +# nothing at all. # # Every pull request, not only the ones targeting `main`. A stacked branch # opens its pull request against the layer below it, so `branches: [main]` @@ -11,6 +13,8 @@ name: Build iOS # commits reached review that way without Swift being built once. on: pull_request: + push: + branches: [main] # One in-flight run per ref; a new push cancels the previous run. This matters # more now that the trigger is every pull request: restacking a five-deep chain @@ -26,8 +30,14 @@ jobs: contents: read outputs: ios: ${{ steps.filter.outputs.ios }} + release: ${{ steps.release.outputs.builds }} steps: - uses: actions/checkout@v5 + with: + # A push reads its own commit messages below: full history, without + # the file contents nothing here looks at. + fetch-depth: ${{ github.event_name == 'push' && '0' || '1' }} + filter: ${{ github.event_name == 'push' && 'blob:none' || '' }} - uses: dorny/paths-filter@v4 id: filter with: @@ -38,9 +48,28 @@ jobs: - 'lib/**' - 'pubspec.yaml' + # Whether release.yml builds this push, for the job below. It publishes + # a snapshot only when a commit since the last tag carries a changelog + # entry; asked here of the push's own commits, which are always within + # that range. So "yes" here is always "yes" there, and a push this + # answers "no" for is built here — at worst by both, never by neither. + - name: Does release.yml build this push + id: release + if: github.event_name == 'push' + env: + BEFORE: ${{ github.event.before }} + run: | + if bash tool/release/notes.sh --any "$BEFORE..$GITHUB_SHA"; then + echo "builds=true" >> "$GITHUB_OUTPUT" + echo "this push brings a changelog entry: release.yml builds it" + else + echo "builds=false" >> "$GITHUB_OUTPUT" + echo "no changelog entry in this push: no snapshot, so it is built here" + fi + iOS: needs: changes - if: ${{ needs.changes.outputs.ios == 'true' }} + if: ${{ needs.changes.outputs.ios == 'true' && needs.changes.outputs.release != 'true' }} runs-on: macos-latest permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 49c18e524..255e32e8d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,7 @@ -# Every commit on main is a snapshot; every tag is a release. +# A push to main that brings a changelog entry is a snapshot; every tag is a +# release. A push whose snapshot note would be empty — no commit since the last +# tag carrying an entry — publishes nothing, and android.yml and ios.yml build it +# instead. Run by hand, a snapshot is published regardless. # # The three values a build carries come from `tool/release/version.sh` and nowhere # else — no human edits a version, and `pubspec.yaml`'s stays a placeholder for @@ -30,6 +33,7 @@ jobs: code: ${{ steps.ordinal.outputs.code }} date: ${{ steps.v.outputs.date }} prerelease: ${{ steps.v.outputs.prerelease }} + publish: ${{ steps.changelog.outputs.publish }} steps: # Full history: the label counts this week's commits and the train reads # the newest tag. A shallow clone has neither. @@ -70,8 +74,8 @@ jobs: # # **Equal** on a tag means this commit was already published as a # snapshot. That is not an accident, it is the normal shape of a release: - # every push to main publishes, so by the time anyone can tag a commit it - # has already spent its number, and both uploads below run for a snapshot + # every push to main with a changelog entry publishes, so by the time + # anyone can tag a commit it has usually already spent its number, and both uploads below run for a snapshot # exactly as they do for a release. `v26.1` only shipped because its # commit never went out as a snapshot; on `v26.2` (`d2b4664b`, carrying # both `26w35g` and `v26.2`) the tag run computed the snapshot's own @@ -118,8 +122,28 @@ jobs: fi echo "code=$CODE" >> "$GITHUB_OUTPUT" + # Nothing a user would see: the snapshot's note would say so and nothing + # else, and the stores would get a build that changes nothing. A push to + # main only — a tag is a release, and run by hand, someone asked for one. + # The next push that does bring an entry publishes these commits too, + # since its note runs from the same tag. + - name: Anything to publish + id: changelog + env: + EVENT: ${{ github.event_name }} + run: | + if [ "$GITHUB_REF_TYPE" = branch ] && [ "$EVENT" = push ] && + ! bash tool/release/notes.sh --any; then + echo "publish=false" >> "$GITHUB_OUTPUT" + echo "::notice::no commit since $(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo 'the first') carries a changelog entry — no snapshot; android.yml and ios.yml build this push" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + build: needs: version + # `publish`, which needs this, is skipped with it. + if: needs.version.outputs.publish == 'true' strategy: # Independent: one store failing must not stop the other shipping. fail-fast: false diff --git a/README.md b/README.md index 4421a3bf1..de7973783 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ DPIP(Disaster Prevention Information Platform)是臺灣本土團隊開發的 | **多語言** | 介面支援 11 種語言 | > [!NOTE] -> 這個儲存庫正在進行架構重寫(feature-first 分層,見 [ARCHITECTURE.md](ARCHITECTURE.md))。`main` 上的每一次提交都會發布一個快照版本,快照未經完整審查。想要穩定版本請從商店安裝。 +> 這個儲存庫正在進行架構重寫(feature-first 分層,見 [ARCHITECTURE.md](ARCHITECTURE.md))。推上 `main` 的變更裡有使用者看得到的修改時,會發布一個快照版本,快照未經完整審查。想要穩定版本請從商店安裝。 ## 資料來源 diff --git a/tool/release/notes.sh b/tool/release/notes.sh index 6f007de85..5d1962733 100755 --- a/tool/release/notes.sh +++ b/tool/release/notes.sh @@ -37,11 +37,25 @@ # changed under them. # # Usage: tool/release/notes.sh