gowalk-cicd 1.0.76 → 1.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -172,6 +172,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
172
172
  - The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
173
173
  new app before GitHub registers `deploy-backend.yml` on the default branch.
174
174
  Default-branch deploys continue through the separate backend workflow.
175
+ - Mobile pushes exclude durable `.factory/progress` records and the separately deployed
176
+ backend/server/Compose paths. A mixed commit with any native/shared input still builds;
177
+ explicit workflow dispatch remains available. Keep backend deployment's own path filters
178
+ intact, and do not broadly exclude assets, root build settings or unknown app layouts.
175
179
  - Domain-backed backend deploys fail closed on certificate issuance and the
176
180
  public HTTPS health probe. `select_certbot_account.sh` chooses one existing
177
181
  Let's Encrypt ACME v2 account deterministically so multi-account hosts stay
@@ -267,6 +271,12 @@ node /path/to/gowalk-cicd/bin/cli.mjs
267
271
  version and metadata was skipped), and `firebase_symbols_pending` (the dSYM
268
272
  archive awaits the session's proxied console upload). The README lists the schemas; add a
269
273
  field by bumping the version.
274
+ - `android-action/play-upload/` verifies unchanged SHA/checksum-pinned official uploader
275
+ bytes before running Node24. It classifies immediate private child output, never live
276
+ GitHub annotations: expired/deleted retries normally; only the exact cannot-auto-submit
277
+ refusal permits a review hold. Unknown/transport/post-commit uncertainty does not replay.
278
+ The existing signed AAB, proxy, track/status/rollout and notes survive one bounded retry;
279
+ only a successful hold emits `play_review_pending`. Keep raw child output out of receipts.
270
280
  - The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
271
281
  `setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
272
282
  gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
package/README.md CHANGED
@@ -228,8 +228,8 @@ already at or below those numbers are left untouched. Linux only.
228
228
 
229
229
  ### When Play will not accept a release for review
230
230
 
231
- Once an app has changes that Google requires a person to submit — most often
232
- after a policy rejection — the API refuses to commit them:
231
+ Some Play changes require a separate console review submission. The ordinary API
232
+ commit then returns this explicit refusal:
233
233
 
234
234
  ```
235
235
  Changes cannot be sent for review automatically. Please set the query parameter
@@ -237,10 +237,10 @@ changesNotSentForReview to true. Once committed, the changes in this edit can be
237
237
  sent for review from the Google Play Console UI.
238
238
  ```
239
239
 
240
- That fails the whole upload, so the bundle you just built never reaches Play at
241
- all. The retry re-uploads with `changesNotSentForReview`, which lands the bundle
242
- and leaves it waiting, then records that fact as a typed check-run annotation on
243
- the job (see [Check-run annotations](#check-run-annotations-the-panel-reads)):
240
+ The uncommitted edit has not published the release. Only this exact refusal lets
241
+ the upload wrapper retry the same signed artifact with `changesNotSentForReview`.
242
+ A successful retry records the separate review submission that remains, using a
243
+ typed check-run annotation (see [Check-run annotations](#check-run-annotations-the-panel-reads)):
244
244
 
245
245
  ```
246
246
  ::notice title=play_review_pending::{"schema":"gowalk-cicd/play-review-pending.v1",
@@ -289,6 +289,7 @@ the situation and whose **message** is one line of JSON carrying a versioned
289
289
  | title | schema | fields |
290
290
  | --- | --- | --- |
291
291
  | `play_review_pending` | `gowalk-cicd/play-review-pending.v1` | `package`, `track`, `version_code` |
292
+ | `play_upload_result` | `gowalk-cicd/play-upload.v1` | `ok`, `code`, `attempts`, `review_pending`, upstream identity |
292
293
  | `store_version_locked` | `gowalk-cicd/store-version-locked.v1` | `version`, `state`, `build_number` |
293
294
  | `firebase_symbols_pending` | `gowalk-cicd/firebase-symbols-pending.v1` | `platform`, `app_id`, `source_sha`, `artifact`, `file`, `sha256`, `status` |
294
295
  | `apple_build_upload_failed` (error) | `gowalk-cicd/apple-build-upload-failed.v2` | `category`, `stage`, `receipt_available`, receipt identifiers (`cfBundleVersion`, `upload_id`, `file_id`, `state`, `provider_status`, `provider_code`, `provider_pointer`), `action` |
@@ -329,23 +330,32 @@ emits the `play_preflight_failed` error annotation with schema
329
330
  values and raw network errors are omitted. A cleanup failure also records the known
330
331
  edit ID and `cleanup_required: true` so recovery targets that edit.
331
332
 
332
- A Play edit is a short-lived server-side object, and an AAB upload that runs
333
- long enough outlives one:
334
-
335
- ```
336
- ##[error]This edit has expired, please create a new Edit.
337
- ##[error]This Edit has been deleted.
338
- ```
339
-
340
- Neither says anything about the build, which is signed and retained by that
341
- point. The upload step therefore runs with `continue-on-error` and a second
342
- attempt follows when the first fails — a fresh edit succeeds. Both attempts
343
- failing still fails the job.
344
-
345
- One caveat this makes explicit: **nothing else should hold an edit open on the
346
- same package while a deploy runs.** A script that polls `edits.insert` /
347
- `edits.delete` to inspect track state will invalidate the edit the upload is
348
- using, and the deploy fails for a reason that has nothing to do with the app.
333
+ The upload wrapper at `android-action/play-upload/` runs the unchanged official
334
+ `r0adkll/upload-google-play` revision `e738b9dd8f2476ea806d921b64aacd24f34515a5` in
335
+ Node24. Its fixed GitHub download is checked against the recorded size and SHA256
336
+ before execution; an absent or changed program refuses upload. Both proxy spellings
337
+ and bypass variables are explicitly set for only that provider subprocess.
338
+
339
+ The wrapper permits at most two attempts within one hour, reusing the existing
340
+ signed bundle, track, status, rollout fraction and release notes:
341
+
342
+ | First result | Recovery |
343
+ | --- | --- |
344
+ | Exact expired/deleted-edit refusal | Fresh edit with the original normal review behavior |
345
+ | Exact cannot-auto-submit refusal quoted above | Fresh edit with the explicit review-hold flag |
346
+ | Missing, multiple, unknown, transport or post-commit-uncertain failure | Fail without replay |
347
+
348
+ Raw uploader output and exception text are private to the wrapper. It emits a
349
+ `play_upload_result` annotation with schema `gowalk-cicd/play-upload.v1`, a fixed
350
+ code, attempt count, review-pending boolean and verified upstream identity. Only a
351
+ successful review-hold retry emits the existing `play_review_pending` receipt.
352
+ Ordinary retry success never misreports an unsubmitted release. The wrapper does
353
+ not depend on check-run annotations becoming visible while its job is running.
354
+
355
+ An expired/deleted edit does not identify its cause. Another edit opened with the
356
+ same API user/package can invalidate it; avoid competing read edits while uploading.
357
+ An external actor is not proven by an expired-edit error alone. Ambiguous outcomes
358
+ require a fresh authoritative inventory/track readback before another mutation.
349
359
 
350
360
  ### Runner disk space
351
361
 
@@ -1 +1 @@
1
- 1.0.76
1
+ 1.0.78
@@ -1 +1 @@
1
- 1.0.76
1
+ 1.0.78
@@ -0,0 +1,33 @@
1
+ name: Publish the signed Play bundle with classified recovery
2
+ description: Run the unchanged pinned official uploader; retry only definitive supported refusals.
3
+ inputs:
4
+ serviceAccountJson:
5
+ description: Path to the provisioned service account file.
6
+ required: true
7
+ packageName:
8
+ description: App package identifier.
9
+ required: true
10
+ releaseFiles:
11
+ description: Existing signed APK or AAB paths, comma separated.
12
+ required: true
13
+ tracks:
14
+ description: Destination tracks.
15
+ required: true
16
+ status:
17
+ description: Requested release status.
18
+ default: completed
19
+ userFraction:
20
+ description: Optional staged rollout fraction.
21
+ whatsNewDirectory:
22
+ description: Existing localized release notes directory.
23
+ inAppUpdatePriority:
24
+ description: In-app update priority.
25
+ default: '0'
26
+ outputs:
27
+ review-pending:
28
+ description: True only after a successful explicitly requested review-hold retry.
29
+ receipt:
30
+ description: Sanitized versioned result with attempt count and upstream identity.
31
+ runs:
32
+ using: node24
33
+ main: run.cjs
@@ -0,0 +1,78 @@
1
+ // Fixed classifications only: raw child output and exceptions never reach logs or receipts.
2
+ const {spawn} = require('node:child_process');
3
+ const {readFileSync, writeFileSync} = require('node:fs');
4
+ const {join} = require('node:path');
5
+ const messages = new Map([
6
+ ['This edit has expired, please create a new Edit.', 'edit_expired'],
7
+ ['This Edit has been deleted.', 'edit_deleted'],
8
+ ['Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true. '
9
+ + 'Once committed, the changes in this edit can be sent for review from the Google Play Console UI.',
10
+ 'review_hold_required'],
11
+ ]);
12
+
13
+ function environment(env) {
14
+ let proxy;
15
+ try { proxy = new URL(env.GOOGLE_STORE_PROXY_URL); } catch { throw new Error('assigned_proxy_required'); }
16
+ const authority = env.GOOGLE_STORE_PROXY_URL.split('://')[1]?.split(/[/?#]/)[0] || '';
17
+ if (!['http:', 'https:'].includes(proxy.protocol) || !proxy.hostname || !/:\d+$/.test(authority)
18
+ || /[\r\n\0]/.test(env.GOOGLE_STORE_PROXY_URL)) throw new Error('assigned_proxy_required');
19
+ const value = env.GOOGLE_STORE_PROXY_URL;
20
+ return {...env, HTTP_PROXY: value, HTTPS_PROXY: value, http_proxy: value, https_proxy: value,
21
+ NO_PROXY: '', no_proxy: '', ALL_PROXY: '', all_proxy: ''};
22
+ }
23
+
24
+ function consume(state, chunk) {
25
+ state.pending += chunk;
26
+ let end;
27
+ while ((end = state.pending.indexOf('\n')) !== -1) {
28
+ const line = state.pending.slice(0, end).replace(/\r$/, ''); state.pending = state.pending.slice(end + 1);
29
+ if (line.startsWith('::error::')) {
30
+ state.errors += 1;
31
+ const message = line.slice(9).replace(/%0D/g, '\r').replace(/%0A/g, '\n').replace(/%25/g, '%');
32
+ state.code = messages.get(message) || 'unclassified_failure';
33
+ } else if (line.startsWith('::error ')) { state.errors += 1; state.ambiguous = true; }
34
+ }
35
+ if (state.pending.length > 16384) { state.pending = ''; state.ambiguous = true; }
36
+ }
37
+
38
+ function execute(program, env, directory, timeout) {
39
+ return new Promise(resolve => {
40
+ const output = join(directory, 'output'), exports = join(directory, 'environment');
41
+ writeFileSync(output, '', {mode: 0o600}); writeFileSync(exports, '', {mode: 0o600});
42
+ const state = {pending: '', errors: 0, code: 'unclassified_failure', ambiguous: false};
43
+ const child = spawn(process.execPath, [program], {
44
+ env: {...env, GITHUB_OUTPUT: output, GITHUB_ENV: exports}, stdio: ['ignore', 'pipe', 'pipe'],
45
+ });
46
+ let cancelled = false, force;
47
+ const stop = () => {
48
+ cancelled = true; child.kill('SIGTERM');
49
+ if (!force) force = setTimeout(() => child.kill('SIGKILL'), 2000);
50
+ };
51
+ const timer = setTimeout(stop, timeout);
52
+ process.on('SIGTERM', stop); process.on('SIGINT', stop);
53
+ child.stdout.setEncoding('utf8'); child.stdout.on('data', chunk => consume(state, chunk));
54
+ child.stderr.resume();
55
+ child.on('error', () => { state.ambiguous = true; });
56
+ child.on('close', (status, signal) => {
57
+ clearTimeout(timer); clearTimeout(force); process.off('SIGTERM', stop); process.off('SIGINT', stop);
58
+ consume(state, '\n');
59
+ const committed = /(?:^|\n)committedEditId(?:=|<<)/.test(readFileSync(output, 'utf8'));
60
+ resolve({status, signal, cancelled, committed, errors: state.errors,
61
+ code: state.ambiguous ? 'unclassified_failure' : state.code});
62
+ });
63
+ });
64
+ }
65
+
66
+ function retry(result) {
67
+ if (result.status !== 1 || result.signal || result.cancelled || result.committed || result.errors !== 1) return null;
68
+ if (['edit_expired', 'edit_deleted'].includes(result.code)) return 'false';
69
+ return result.code === 'review_hold_required' ? 'true' : null;
70
+ }
71
+
72
+ function failure(result) {
73
+ if (result.cancelled) return 'upload_cancelled';
74
+ if (result.committed || result.signal || result.status === null) return 'upload_outcome_uncertain';
75
+ return result.status === 1 && result.errors === 1 ? result.code : 'unclassified_failure';
76
+ }
77
+
78
+ module.exports = {environment, execute, consume, retry, failure};
@@ -0,0 +1,55 @@
1
+ // Development delivery tooling; no uploader/runtime code enters the product binary.
2
+ const {appendFileSync, mkdtempSync, mkdirSync, rmSync} = require('node:fs');
3
+ const {join} = require('node:path');
4
+ const source = require('./source.cjs');
5
+ const processRunner = require('./process.cjs');
6
+
7
+ async function upload(program, env, directory, execute = processRunner.execute, timeout = 3600000) {
8
+ const deadline = Date.now() + timeout;
9
+ let mode = 'false';
10
+ for (let attempt = 1; attempt <= 2; attempt++) {
11
+ const target = join(directory, String(attempt)); mkdirSync(target, {mode: 0o700});
12
+ const result = await execute(program, {...env, INPUT_CHANGESNOTSENTFORREVIEW: mode}, target,
13
+ Math.max(1, deadline - Date.now()));
14
+ const reviewPending = mode === 'true';
15
+ if (result.status === 0 && !result.signal && !result.cancelled && result.errors === 0) {
16
+ return {ok: true, code: reviewPending ? 'review_pending' : 'uploaded', attempts: attempt,
17
+ review_pending: reviewPending};
18
+ }
19
+ const next = processRunner.retry(result);
20
+ if (attempt === 2 || next === null || Date.now() >= deadline) {
21
+ return {ok: false, code: processRunner.failure(result),
22
+ attempts: attempt, review_pending: false};
23
+ }
24
+ mode = next;
25
+ console.log(`Google Play upload: retry=${result.code} changes_not_sent_for_review=${mode}`);
26
+ }
27
+ }
28
+
29
+ async function main() {
30
+ let directory, receipt;
31
+ try {
32
+ const env = processRunner.environment(process.env);
33
+ if (!env.RUNNER_TEMP || !env.GITHUB_OUTPUT) throw new Error('runner_context_required');
34
+ directory = mkdtempSync(join(env.RUNNER_TEMP, 'gowalk-play-upload-'));
35
+ receipt = await upload(await source.prepare(directory), env, directory);
36
+ } catch (error) {
37
+ const allowed = ['assigned_proxy_required', 'runner_context_required', 'uploader_integrity_refused',
38
+ 'uploader_source_unavailable'];
39
+ receipt = {ok: false, code: allowed.includes(error.message) ? error.message : 'uploader_failed',
40
+ attempts: 0, review_pending: false};
41
+ } finally { if (directory) rmSync(directory, {recursive: true, force: true}); }
42
+ receipt = {schema: 'gowalk-cicd/play-upload.v1', ...receipt, upstream_revision: source.revision,
43
+ upstream_sha256: source.digest};
44
+ const encoded = JSON.stringify(receipt);
45
+ if (process.env.GITHUB_OUTPUT) {
46
+ appendFileSync(process.env.GITHUB_OUTPUT, `review-pending=${receipt.review_pending}\nreceipt=${encoded}\n`);
47
+ }
48
+ console.log(`::${receipt.ok ? 'notice' : 'error'} title=play_upload_result::${encoded}`);
49
+ if (!receipt.ok) process.exitCode = 1;
50
+ }
51
+
52
+ module.exports = {upload, main};
53
+ if (require.main === module) void main().catch(() => {
54
+ process.stderr.write('play_upload_receipt_failed\n'); process.exitCode = 1;
55
+ });
@@ -0,0 +1,45 @@
1
+ // Execute unchanged official uploader bytes, pinned independently of the source transport.
2
+ const https = require('node:https');
3
+ const {createHash} = require('node:crypto');
4
+ const {writeFileSync} = require('node:fs');
5
+ const {join} = require('node:path');
6
+ const revision = 'e738b9dd8f2476ea806d921b64aacd24f34515a5';
7
+ const digest = '15b27f8937f30f20d81bb585c4ad7ec7032e84491be51ac4dda330767a287f6b';
8
+ const source = `https://raw.githubusercontent.com/r0adkll/upload-google-play/${revision}/lib/index.js`;
9
+
10
+ function verify(bytes) {
11
+ if (bytes.length !== 1405863 || createHash('sha256').update(bytes).digest('hex') !== digest) {
12
+ throw new Error('uploader_integrity_refused');
13
+ }
14
+ return bytes;
15
+ }
16
+
17
+ function download() {
18
+ // Native HTTPS reaches only this fixed GitHub origin; no provider transport is changed.
19
+ return new Promise((resolve, reject) => {
20
+ const request = https.get(source, {timeout: 30000, agent: new https.Agent({proxyEnv: {}})}, response => {
21
+ if (response.statusCode !== 200) {
22
+ response.resume(); reject(new Error('uploader_source_unavailable')); return;
23
+ }
24
+ const chunks = []; let size = 0;
25
+ response.on('data', chunk => {
26
+ size += chunk.length;
27
+ if (size > 1405863) request.destroy(new Error('uploader_integrity_refused'));
28
+ else chunks.push(chunk);
29
+ });
30
+ response.on('end', () => resolve(Buffer.concat(chunks)));
31
+ response.on('error', () => reject(new Error('uploader_source_unavailable')));
32
+ });
33
+ request.on('timeout', () => request.destroy(new Error('uploader_source_unavailable')));
34
+ request.on('error', () => reject(new Error('uploader_source_unavailable')));
35
+ });
36
+ }
37
+
38
+ async function prepare(directory) {
39
+ const bytes = verify(await download());
40
+ const target = join(directory, 'official-uploader.cjs');
41
+ writeFileSync(target, bytes, {flag: 'wx', mode: 0o400});
42
+ return target;
43
+ }
44
+
45
+ module.exports = {revision, digest, source, verify, prepare};
@@ -12,7 +12,12 @@ WORKFLOW = (ROOT / "templates/deploy.yml").read_text()
12
12
 
13
13
  def readiness_step() -> str:
14
14
  start = WORKFLOW.index(" - name: Check Google Play API readiness")
15
- finish = WORKFLOW.index(" # Play edits", start)
15
+ body = WORKFLOW.index(" run: |\n", start) + len(" run: |\n")
16
+ finish = body
17
+ for line in WORKFLOW[body:].splitlines(keepends=True):
18
+ if line.strip() and not line.startswith(" "):
19
+ break
20
+ finish += len(line)
16
21
  return WORKFLOW[start:finish]
17
22
 
18
23
 
@@ -1 +1 @@
1
- 1.0.76
1
+ 1.0.78
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.76",
3
+ "version": "1.0.78",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,6 +6,13 @@ on:
6
6
  - '**/*.md'
7
7
  - '.gitignore'
8
8
  - 'creds/**'
9
+ # Durable task records and independently deployed backend sources do not
10
+ # change a native binary. Mixed commits still run when any mobile input changes.
11
+ - '.factory/progress/**'
12
+ - 'backend/**'
13
+ - 'server/**'
14
+ - 'docker-compose.yml'
15
+ - '.github/workflows/deploy-backend.yml'
9
16
  - '.github/actions/swift-app/**'
10
17
  - '.github/actions/android-app/**'
11
18
  workflow_dispatch:
@@ -471,32 +478,15 @@ jobs:
471
478
  python3 .github/actions/android-app/scripts/play_preflight.py \
472
479
  --package "$PACKAGE_NAME" \
473
480
  --service-account "$SERVICE_ACCOUNT"
474
- # Play edits are short-lived server-side objects, and an AAB upload that runs
475
- # long — a 100-200 MB bundle, a slow runner, or anything else holding an edit
476
- # open on the same package — outlives one. The failures read as
477
- # "This edit has expired, please create a new Edit." or "This Edit has been
478
- # deleted." Neither says anything about the build, which is already signed and
479
- # retained by this point; a second attempt opens a fresh edit and succeeds.
480
- # Note this also means nothing else should be creating edits against these
481
- # packages while a deploy runs — an external poller that opens and deletes an
482
- # edit will invalidate the one this step is holding.
483
- # The upload action is JavaScript, so its proxy can only be the step's own
484
- # upper-case env keys below. Node clients fall back to a lower-case no_proxy
485
- # when NO_PROXY is empty, and GitHub refuses both spellings in one env map, so
486
- # the ambient lower-case bypass is cleared through GITHUB_ENV instead.
487
- - name: Clear ambient lower-case proxy bypass
488
- if: ${{ steps.mode.outputs.mode == 'local' && steps.play.outputs.ready == 'true' }}
489
- shell: bash
490
- run: python3 .github/actions/android-app/scripts/play_store_proxy.py --github-env
481
+ # The wrapper verifies the unchanged pinned official uploader and classifies
482
+ # its immediate result. Only explicit expired/deleted edits retry normally;
483
+ # only Play's explicit review-hold refusal changes the review flag.
491
484
  - name: Upload Android release to Google Play
492
485
  id: play-upload
493
- continue-on-error: true
494
486
  if: ${{ steps.mode.outputs.mode == 'local' && steps.play.outputs.ready == 'true' }}
495
- uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
487
+ uses: ./.github/actions/android-app/play-upload
496
488
  env:
497
- HTTPS_PROXY: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
498
- HTTP_PROXY: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
499
- NO_PROXY: '' # one spelling only: GitHub rejects env keys that differ by case
489
+ GOOGLE_STORE_PROXY_URL: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
500
490
  with:
501
491
  serviceAccountJson: ${{ steps.android.outputs.play-service-account }}
502
492
  packageName: ${{ steps.android.outputs.package-name }}
@@ -507,36 +497,6 @@ jobs:
507
497
  ${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
508
498
  whatsNewDirectory: ${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
509
499
 
510
- # The retry serves two different failures, and both need a fresh edit:
511
- # * the edit expired mid-upload (a large bundle on a slow runner), and
512
- # * Play refused to auto-submit: "Changes cannot be sent for review
513
- # automatically. Please set changesNotSentForReview to true." Play returns
514
- # this once an app has changes a human must submit — after a policy
515
- # rejection, for instance — and it fails the whole upload, so the built
516
- # bundle never lands at all.
517
- - name: Retry Google Play upload without auto-submit
518
- id: play-upload-retry
519
- if: >-
520
- ${{ steps.mode.outputs.mode == 'local'
521
- && steps.play.outputs.ready == 'true'
522
- && steps.play-upload.outcome == 'failure' }}
523
- uses: r0adkll/upload-google-play@e738b9dd8f2476ea806d921b64aacd24f34515a5 # v1.1.5
524
- env:
525
- HTTPS_PROXY: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
526
- HTTP_PROXY: ${{ secrets.GOOGLE_STORE_PROXY_URL }}
527
- NO_PROXY: '' # one spelling only: GitHub rejects env keys that differ by case
528
- with:
529
- serviceAccountJson: ${{ steps.android.outputs.play-service-account }}
530
- packageName: ${{ steps.android.outputs.package-name }}
531
- releaseFiles: ${{ steps.android.outputs.bundle-path }}
532
- tracks: ${{ vars.GOOGLE_PLAY_TRACK || 'internal' }}
533
- status: ${{ vars.GOOGLE_PLAY_STATUS || 'completed' }}
534
- userFraction: >-
535
- ${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
536
- whatsNewDirectory: >-
537
- ${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
538
- changesNotSentForReview: true
539
-
540
500
  # The retry landed the bundle without submitting it. That is a fact for
541
501
  # the panel that merged this commit, not a chore for a person: the panel
542
502
  # reads this typed check-run annotation off the job (title + JSON with a
@@ -544,7 +504,7 @@ jobs:
544
504
  # step that presses "Send changes for review". Normal after a policy
545
505
  # rejection; the ordinary first upload submits and this never fires.
546
506
  - name: Record that Play holds this release for a review submission
547
- if: ${{ always() && steps.play-upload-retry.outcome == 'success' }}
507
+ if: ${{ steps.play-upload.outcome == 'success' && steps.play-upload.outputs.review-pending == 'true' }}
548
508
  shell: bash
549
509
  env:
550
510
  PACKAGE: ${{ steps.android.outputs.package-name }}