gowalk-cicd 1.0.110 → 1.0.111
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 +4 -0
- package/README.md +15 -0
- package/action/.daemux-version +1 -1
- package/android-action/.daemux-version +1 -1
- package/android-action/play-upload/diagnostics.cjs +73 -0
- package/android-action/play-upload/process.cjs +9 -3
- package/android-action/play-upload/run.cjs +5 -2
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -369,6 +369,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
369
369
|
refusal permits a review hold. Unknown/transport/post-commit uncertainty does not replay.
|
|
370
370
|
The existing signed AAB, proxy, track/status/rollout and notes survive one bounded retry;
|
|
371
371
|
only a successful hold emits `play_review_pending`. Keep raw child output out of receipts.
|
|
372
|
+
Each attempt also emits brace-free `play_upload_diagnostic` with fixed observed progress,
|
|
373
|
+
process outcome and error signals from bounded private stdout/stderr line buffers.
|
|
374
|
+
Preserve the v1 upload result and retry rules; diagnostics never authorize replay or prove
|
|
375
|
+
an operation completed. See README for the independent diagnostic schema and field vocabulary.
|
|
372
376
|
- The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
|
|
373
377
|
`setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
|
|
374
378
|
gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
|
package/README.md
CHANGED
|
@@ -360,6 +360,7 @@ the situation and whose **message** is one line of JSON carrying a versioned
|
|
|
360
360
|
| --- | --- | --- |
|
|
361
361
|
| `play_review_pending` | `gowalk-cicd/play-review-pending.v1` | `package`, `track`, `version_code` |
|
|
362
362
|
| `play_upload_result` | `gowalk-cicd/play-upload.v1` | `ok`, `code`, `attempts`, `review_pending`, upstream identity |
|
|
363
|
+
| `play_upload_diagnostic` | `gowalk-cicd/play-upload-diagnostic.v1` | Fixed process evidence; see below |
|
|
363
364
|
| `store_version_locked` | `gowalk-cicd/store-version-locked.v1` | `version`, `state`, `build_number` |
|
|
364
365
|
| `firebase_symbols_pending` | `gowalk-cicd/firebase-symbols-pending.v1` | `platform`, `app_id`, `source_sha`, `artifact`, `file`, `sha256`, `status` |
|
|
365
366
|
| `mobile_upload_reused` | `gowalk-cicd/mobile-upload-reused.v1` | `platform`, `fingerprint`, `run_id`, `run_attempt`, `source_sha`, `deployment_id`, `next_action` |
|
|
@@ -460,6 +461,20 @@ successful review-hold retry emits the existing `play_review_pending` receipt.
|
|
|
460
461
|
Ordinary retry success never misreports an unsubmitted release. The wrapper does
|
|
461
462
|
not depend on check-run annotations becoming visible while its job is running.
|
|
462
463
|
|
|
464
|
+
Every completed uploader attempt also emits `play_upload_diagnostic`, independently of the
|
|
465
|
+
unchanged v1 result. Its brace-free message starts with `gowalk-cicd/play-upload-diagnostic.v1`
|
|
466
|
+
and carries these space-separated fields in order: `attempt=<1|2>`, `phase=<phase>`,
|
|
467
|
+
`exit_code=<0-255|unknown>`, `signal=<signal>`, `error_count=<count|unknown>`,
|
|
468
|
+
`cancelled=<true|false>`, `commit_reported=<true|false>`, `signals=<signals>`.
|
|
469
|
+
The phase is the last observed official progress line: `startup`, `edit_create`, `tracks_read`,
|
|
470
|
+
`bundle_upload`, `track_update` or `edit_commit`. It does not prove that operation completed.
|
|
471
|
+
The signal is `none`, `SIGTERM`, `SIGINT`, `SIGKILL` or `other`; observed error signals are
|
|
472
|
+
comma-separated `connection_reset`, `connection_refused`, `connection_timeout`, `dns_failure`,
|
|
473
|
+
`tls_verification`, `output_truncated`, or `none`. Each private stdout/stderr line buffer is
|
|
474
|
+
bounded to 16 KiB. No provider message, path, URL, track or credential is republished.
|
|
475
|
+
These observations never authorize a retry. Missing commit output does not prove no mutation,
|
|
476
|
+
and a reported commit does not prove review submission. Unknown failures remain unclassified.
|
|
477
|
+
|
|
463
478
|
An expired/deleted edit does not identify its cause. Another edit opened with the
|
|
464
479
|
same API user/package can invalidate it; avoid competing read edits while uploading.
|
|
465
480
|
An external actor is not proven by an expired-edit error alone. Ambiguous outcomes
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.111
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.111
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Observe only fixed progress/error patterns; never retain provider text in a receipt.
|
|
2
|
+
const phases = new Map([
|
|
3
|
+
['Creating a new Edit for this release', 'edit_create'],
|
|
4
|
+
['Committing the Edit', 'edit_commit'],
|
|
5
|
+
]);
|
|
6
|
+
const prefixes = new Map([
|
|
7
|
+
["Validating tracks: '", 'tracks_read'], ['Uploading ', 'bundle_upload'],
|
|
8
|
+
['::debug::Creating release for:', 'track_update'],
|
|
9
|
+
]);
|
|
10
|
+
const tlsCodes = ['CERT_HAS_EXPIRED', 'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
|
|
11
|
+
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY', 'ERR_TLS_CERT_ALTNAME_INVALID'];
|
|
12
|
+
const patterns = new Map([
|
|
13
|
+
['connection_reset', /\bECONNRESET\b|\bsocket hang up\b/i],
|
|
14
|
+
['connection_refused', /\bECONNREFUSED\b/],
|
|
15
|
+
['connection_timeout', /\b(?:ETIMEDOUT|ESOCKETTIMEDOUT|ECONNABORTED)\b/],
|
|
16
|
+
['dns_failure', /\b(?:ENOTFOUND|EAI_AGAIN)\b/],
|
|
17
|
+
['tls_verification', new RegExp(`\\b(?:${tlsCodes.join('|')})\\b`)],
|
|
18
|
+
]);
|
|
19
|
+
const knownPhases = new Set(['startup', ...phases.values(), ...prefixes.values()]);
|
|
20
|
+
const knownSignals = new Set([...patterns.keys(), 'output_truncated']);
|
|
21
|
+
const limit = 16384;
|
|
22
|
+
|
|
23
|
+
function create() {
|
|
24
|
+
return {phase: 'startup', signals: new Set(), pending: {stdout: '', stderr: ''},
|
|
25
|
+
discard: {stdout: false, stderr: false}};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function line(state, stream, value) {
|
|
29
|
+
if (stream === 'stdout') {
|
|
30
|
+
if (phases.has(value)) state.phase = phases.get(value);
|
|
31
|
+
for (const [prefix, phase] of prefixes) if (value.startsWith(prefix)) state.phase = phase;
|
|
32
|
+
if (!value.startsWith('::error')) return;
|
|
33
|
+
value = value.replace(/%0D/g, '\r').replace(/%0A/g, '\n').replace(/%25/g, '%');
|
|
34
|
+
}
|
|
35
|
+
for (const [signal, pattern] of patterns) if (pattern.test(value)) state.signals.add(signal);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function consume(state, stream, chunk) {
|
|
39
|
+
state.pending[stream] += chunk;
|
|
40
|
+
let end;
|
|
41
|
+
while ((end = state.pending[stream].indexOf('\n')) !== -1) {
|
|
42
|
+
const value = state.pending[stream].slice(0, end).replace(/\r$/, '');
|
|
43
|
+
state.pending[stream] = state.pending[stream].slice(end + 1);
|
|
44
|
+
if (state.discard[stream]) state.discard[stream] = false;
|
|
45
|
+
else if (value.length <= limit) line(state, stream, value);
|
|
46
|
+
else state.signals.add('output_truncated');
|
|
47
|
+
}
|
|
48
|
+
if (state.pending[stream].length > limit) {
|
|
49
|
+
state.pending[stream] = ''; state.discard[stream] = true; state.signals.add('output_truncated');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function snapshot(state) {
|
|
54
|
+
return {phase: state.phase, signals: [...state.signals].sort()};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function annotation(attempt, result) {
|
|
58
|
+
const diagnostic = result.diagnostic || {};
|
|
59
|
+
const phase = knownPhases.has(diagnostic.phase) ? diagnostic.phase : 'startup';
|
|
60
|
+
const signals = Array.isArray(diagnostic.signals)
|
|
61
|
+
? [...new Set(diagnostic.signals.filter(value => knownSignals.has(value)))].sort() : [];
|
|
62
|
+
const exit = Number.isInteger(result.status) && result.status >= 0 && result.status <= 255
|
|
63
|
+
? result.status : 'unknown';
|
|
64
|
+
const signal = ['SIGTERM', 'SIGINT', 'SIGKILL'].includes(result.signal)
|
|
65
|
+
? result.signal : result.signal ? 'other' : 'none';
|
|
66
|
+
const errors = Number.isSafeInteger(result.errors) && result.errors >= 0 ? result.errors : 'unknown';
|
|
67
|
+
return '::notice title=play_upload_diagnostic::gowalk-cicd/play-upload-diagnostic.v1'
|
|
68
|
+
+ ` attempt=${attempt === 2 ? 2 : 1} phase=${phase} exit_code=${exit} signal=${signal}`
|
|
69
|
+
+ ` error_count=${errors} cancelled=${result.cancelled === true} commit_reported=${result.committed === true}`
|
|
70
|
+
+ ` signals=${signals.join(',') || 'none'}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {create, consume, snapshot, annotation};
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const {spawn} = require('node:child_process');
|
|
3
3
|
const {readFileSync, writeFileSync} = require('node:fs');
|
|
4
4
|
const {join} = require('node:path');
|
|
5
|
+
const diagnostics = require('./diagnostics.cjs');
|
|
5
6
|
const messages = new Map([
|
|
6
7
|
['This edit has expired, please create a new Edit.', 'edit_expired'],
|
|
7
8
|
['This Edit has been deleted.', 'edit_deleted'],
|
|
@@ -40,6 +41,7 @@ function execute(program, env, directory, timeout) {
|
|
|
40
41
|
const output = join(directory, 'output'), exports = join(directory, 'environment');
|
|
41
42
|
writeFileSync(output, '', {mode: 0o600}); writeFileSync(exports, '', {mode: 0o600});
|
|
42
43
|
const state = {pending: '', errors: 0, code: 'unclassified_failure', ambiguous: false};
|
|
44
|
+
const diagnostic = diagnostics.create();
|
|
43
45
|
const child = spawn(process.execPath, [program], {
|
|
44
46
|
env: {...env, GITHUB_OUTPUT: output, GITHUB_ENV: exports}, stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
47
|
});
|
|
@@ -50,15 +52,19 @@ function execute(program, env, directory, timeout) {
|
|
|
50
52
|
};
|
|
51
53
|
const timer = setTimeout(stop, timeout);
|
|
52
54
|
process.on('SIGTERM', stop); process.on('SIGINT', stop);
|
|
53
|
-
child.stdout.setEncoding('utf8'); child.stdout.on('data', chunk =>
|
|
54
|
-
|
|
55
|
+
child.stdout.setEncoding('utf8'); child.stdout.on('data', chunk => {
|
|
56
|
+
consume(state, chunk); diagnostics.consume(diagnostic, 'stdout', chunk);
|
|
57
|
+
});
|
|
58
|
+
child.stderr.setEncoding('utf8');
|
|
59
|
+
child.stderr.on('data', chunk => diagnostics.consume(diagnostic, 'stderr', chunk));
|
|
55
60
|
child.on('error', () => { state.ambiguous = true; });
|
|
56
61
|
child.on('close', (status, signal) => {
|
|
57
62
|
clearTimeout(timer); clearTimeout(force); process.off('SIGTERM', stop); process.off('SIGINT', stop);
|
|
58
63
|
consume(state, '\n');
|
|
64
|
+
diagnostics.consume(diagnostic, 'stdout', '\n'); diagnostics.consume(diagnostic, 'stderr', '\n');
|
|
59
65
|
const committed = /(?:^|\n)committedEditId(?:=|<<)/.test(readFileSync(output, 'utf8'));
|
|
60
66
|
resolve({status, signal, cancelled, committed, errors: state.errors,
|
|
61
|
-
code: state.ambiguous ? 'unclassified_failure' : state.code});
|
|
67
|
+
code: state.ambiguous ? 'unclassified_failure' : state.code, diagnostic: diagnostics.snapshot(diagnostic)});
|
|
62
68
|
});
|
|
63
69
|
});
|
|
64
70
|
}
|
|
@@ -4,9 +4,10 @@ const {join} = require('node:path');
|
|
|
4
4
|
const source = require('./source.cjs');
|
|
5
5
|
const processRunner = require('./process.cjs');
|
|
6
6
|
const owner = require('./owner.cjs');
|
|
7
|
+
const diagnostics = require('./diagnostics.cjs');
|
|
7
8
|
|
|
8
9
|
async function upload(program, env, directory, execute = processRunner.execute, timeout = 3600000,
|
|
9
|
-
cancelled = () => false) {
|
|
10
|
+
cancelled = () => false, report = () => {}) {
|
|
10
11
|
const deadline = Date.now() + timeout;
|
|
11
12
|
let mode = 'false';
|
|
12
13
|
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
@@ -14,6 +15,7 @@ async function upload(program, env, directory, execute = processRunner.execute,
|
|
|
14
15
|
const target = join(directory, String(attempt)); mkdirSync(target, {mode: 0o700});
|
|
15
16
|
const result = await execute(program, {...env, INPUT_CHANGESNOTSENTFORREVIEW: mode}, target,
|
|
16
17
|
Math.max(1, deadline - Date.now()));
|
|
18
|
+
report(attempt, result);
|
|
17
19
|
const reviewPending = mode === 'true';
|
|
18
20
|
if (result.status === 0 && !result.signal && !result.cancelled && result.errors === 0) {
|
|
19
21
|
return {ok: true, code: reviewPending ? 'review_pending' : 'uploaded', attempts: attempt,
|
|
@@ -39,7 +41,8 @@ async function main() {
|
|
|
39
41
|
directory = mkdtempSync(join(env.RUNNER_TEMP, 'gowalk-play-upload-'));
|
|
40
42
|
const program = await source.prepare(directory);
|
|
41
43
|
receipt = await owner.withOwner(env, env.INPUT_PACKAGENAME, cancelled =>
|
|
42
|
-
upload(program, env, directory, processRunner.execute, 3600000, cancelled
|
|
44
|
+
upload(program, env, directory, processRunner.execute, 3600000, cancelled,
|
|
45
|
+
(attempt, result) => console.log(diagnostics.annotation(attempt, result))));
|
|
43
46
|
} catch (error) {
|
|
44
47
|
const allowed = ['assigned_proxy_required', 'runner_context_required', 'uploader_integrity_refused',
|
|
45
48
|
'uploader_source_unavailable', ...owner.codes];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.111
|