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 +10 -0
- package/README.md +33 -23
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/play-upload/action.yml +33 -0
- package/android-action/play-upload/process.cjs +78 -0
- package/android-action/play-upload/run.cjs +55 -0
- package/android-action/play-upload/source.cjs +45 -0
- package/android-action/scripts/test_early_play_workflow.py +6 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +13 -53
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
|
-
|
|
232
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.78
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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
|
-
|
|
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.
|
|
1
|
+
1.0.78
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -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
|
-
#
|
|
475
|
-
#
|
|
476
|
-
#
|
|
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:
|
|
487
|
+
uses: ./.github/actions/android-app/play-upload
|
|
496
488
|
env:
|
|
497
|
-
|
|
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: ${{
|
|
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 }}
|