From c8724a34dd0a9b03d6e86e2daab0aa31314b844a Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 20:56:13 +0200 Subject: [PATCH 1/2] Let the listing go up when the pictures did not `listing` named `screenshot-set` in a plain `needs:`, so a screenshot run that wedged an emulator did not cost the release its pictures - it skipped the job outright and cost the release its store copy and this version's release notes as well. The bundle went up, the tag and the draft were written, and the one job that tells play what the release says never ran. The lane was already written for the other outcome: `stage_screenshots` answers "no screenshots under fastlane/framed - writing the listing text only" and hands `skip_upload_screenshots` on to supply, which leaves the pictures the store has where they are. Nothing ever reached it. So `screenshot-set` supplies the job rather than gating it. `!cancelled()` is what takes the implicit `success()` off the `needs`, and `needs.upload.result` puts back the half that has to hold: a listing names a version code, and there is no version code until the bundle is up. Checked on a branch of its own rather than reasoned about, both ways round, since a `needs:` behaving other than assumed is what this commit is about: - `screenshot-set` failing -> `listing` runs, the fetch step is skipped, the warning is written, and `needs.screenshot-set.result` reads `failure`. - `upload` failing -> `listing` is skipped, as before. The warning is there because the alternative is a release finishing green over a listing still showing the pictures of some earlier version. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UBGYtacAAsAiA19UJGvdXb --- .github/workflows/release.yml | 24 ++++++++++++++++++++---- CLAUDE.md | 9 ++++++--- README.md | 3 ++- fastlane/Fastfile | 2 +- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 50a6452900b3..aee85a39409b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ name: release # the two play bundles to the internal track, writes their listings and records what went # out; foss rides on the github release. Split into jobs so that a half uploaded release # is repairable: "Re-run failed jobs" retries one upload against the bundle already built -# and signed, and a wedged emulator costs the release its pictures and not its binary. +# and signed, and a wedged emulator costs the release its pictures and nothing else. # # No tag triggers anything and none is written before an upload; build/ is # written afterwards, and the v tag only when the drafted release is published, @@ -329,7 +329,7 @@ jobs: # The bundles alone. What the store says about them is the listing job below, so # that a screenshot run that wedged an emulator costs the release its pictures - # and not its binary. + # and neither its binary nor its copy. # # A job per flavor rather than a loop, so one half can be re-run alone. fail-fast # off for the same reason @@ -384,9 +384,15 @@ jobs: # Its own job because the listing is editable for as long as the release is on # the internal track, while a bundle cannot be uploaded twice - and because it # is the half that waits on the emulators. + # + # `screenshot-set` supplies this job, it does not gate it: without the pictures + # the lane writes the listing text and this version's notes and leaves what the + # store has where it is, which is what `stage_screenshots` is written to do. + # Behind a plain `needs:` that never got to run - a wedged emulator skipped the + # job outright and cost the release its copy along with its pictures. listing: needs: [upload, screenshot-set] - if: ${{ !inputs.dry_run }} + if: ${{ !cancelled() && !inputs.dry_run && needs.upload.result == 'success' }} runs-on: ubuntu-24.04 strategy: fail-fast: false @@ -408,13 +414,23 @@ jobs: given: ${{ inputs.version }} run: .github/scripts/resolve-version.py --input "$given" --dry-run "$dry_run" - # where the lane looks for them, and the same set both apps are given + # where the lane looks for them, and the same set both apps are given. Only + # when there is a set: the artifact is written with if-no-files-found: error, + # so a screenshot-set that passed is the same answer as one that exists - name: fetch the screenshots + if: ${{ needs.screenshot-set.result == 'success' }} uses: actions/download-artifact@v8 with: name: framed path: fastlane/framed + # said out loud, since the release otherwise finishes green over a listing + # still showing the pictures of some earlier version + - name: say there are no pictures this time + if: ${{ needs.screenshot-set.result != 'success' }} + run: | + echo "::warning::no screenshots for this release - the listing goes up with its text alone, and the store keeps the pictures it already has" + - name: play store credentials env: GOOGLE_PLAY_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }} diff --git a/CLAUDE.md b/CLAUDE.md index bb0c64d7235b..71bc6cc9a609 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,9 +82,12 @@ the test can already reach. into the test. The underscore in the name is not a slip either: `frame-screenshots.py` and the generator import it, and a dash cannot be imported. -The release runs the two devices on a runner each, checks the halves together, and only -then writes the listing - which is a job behind the bundle upload, so a wedged emulator -costs the release its pictures and not its binary. +The release runs the two devices on a runner each and checks the halves together, then +writes the listing. That job is behind the bundle upload but **not gated on the pictures**: +without them the lane writes the listing text and the release notes alone and leaves what +the store has, so a wedged emulator costs the release its pictures and nothing else. Do not +put the screenshots back into a plain `needs:` - that is what made a failed capture take the +copy down with it. ## Build diff --git a/README.md b/README.md index 856664b290c2..5c148fb221f1 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,8 @@ icon and the feature graphic are still not uploaded; see `fastlane/metadata/READ Separate from the bundle upload on purpose: a bundle cannot go up twice, while the listing stays editable for as long as the release sits on the internal track - and the listing is the half that waits on the emulators. A screenshot run that wedges costs the release its -pictures, not its binary. +pictures and nothing else: the listing still goes up, with its text and this version's +release notes, over the screenshots the store already has. The run says so as a warning. `fastlane android listingPro` and `listingLite` send the listing without a bundle, which is how a typo is fixed: Play refuses a version code twice, so repairing the words should diff --git a/fastlane/Fastfile b/fastlane/Fastfile index b6f1cc78848c..71c07b9f459c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -200,7 +200,7 @@ platform :android do # The bundle alone. What the store says about it goes up separately, in # uploadListing, so that a screenshot run that wedged an emulator costs the - # release its pictures and not its binary - and so a rejected word can be + # release its pictures and nothing else - and so a rejected word can be # rewritten and pushed again without touching a bundle play refuses twice. private_lane :uploadBundle do |options| flavor = options[:flavor] From c2b99ed086465e1a4d389364d560517b9b67c663 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 21:07:39 +0200 Subject: [PATCH 2/2] Say what a repaired capture does, since finishing without pictures invites the question "Re-run failed jobs" re-runs what failed and everything downstream, so a repaired screenshot job carries `screenshot-set` and `listing` with it and the pictures go up in a second edit against the same version code - which the internal track allows for as long as the release sits on it. Checked on a branch of its own, both halves of it: the listing really does run again and reads `screenshot-set` as `success`, and the device that passed the first time is not photographed twice - its artifact carries over from the earlier attempt and the two halves merge as they would have. The job list reports `run_attempt` 2 for jobs that never re-executed, so that field is not the thing to read. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01UBGYtacAAsAiA19UJGvdXb --- .github/workflows/release.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aee85a39409b..32447820af66 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -390,6 +390,13 @@ jobs: # store has where it is, which is what `stage_screenshots` is written to do. # Behind a plain `needs:` that never got to run - a wedged emulator skipped the # job outright and cost the release its copy along with its pictures. + # + # Finishing without them does not spend the release's one chance at them. + # "Re-run failed jobs" re-runs what failed and everything downstream, so a + # repaired capture brings `screenshot-set` and this job with it and the pictures + # go up in a second edit - which is the whole reason the listing is its own job. + # The device that did pass is not run again; its artifact carries over from the + # attempt before, and the two halves merge as they would have. listing: needs: [upload, screenshot-set] if: ${{ !cancelled() && !inputs.dry_run && needs.upload.result == 'success' }}