gowalk-cicd 1.0.180 → 1.0.182
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/README.md +13 -0
- package/action/scripts/set_app_store_whats_new.py +7 -12
- package/android-action/play-upload/action.yml +5 -0
- package/android-action/play-upload/diagnostics.cjs +5 -2
- package/android-action/play-upload/evidence.cjs +30 -0
- package/android-action/play-upload/evidence_schema.cjs +71 -0
- package/android-action/play-upload/process.cjs +2 -1
- package/android-action/play-upload/provider_evidence.cjs +51 -0
- package/android-action/play-upload/run.cjs +12 -2
- package/package.json +1 -1
- package/templates/deploy.yml +11 -0
- package/templates/history.json.gz +0 -0
package/README.md
CHANGED
|
@@ -652,6 +652,19 @@ bounded to 16 KiB. No provider message, path, URL, track or credential is republ
|
|
|
652
652
|
These observations never authorize a retry. Missing commit output does not prove no mutation,
|
|
653
653
|
and a reported commit does not prove review submission. Unknown failures remain unclassified.
|
|
654
654
|
|
|
655
|
+
The wrapper also writes validated `gowalk-cicd/play-upload-evidence.v1` JSON outside its
|
|
656
|
+
private upload directory: `attempt-1.json`, optional `attempt-2.json`, and `terminal.json`.
|
|
657
|
+
Each record binds the GitHub run/attempt, source SHA, compiled version code (the optional
|
|
658
|
+
`versionCode` input), and immutable uploader identity. Missing bindings are null, never inferred.
|
|
659
|
+
Attempt records add observed HTTP status/RPC codes and exact known message classes from complete,
|
|
660
|
+
bounded error lines; raw provider prose, paths, URLs and identifiers are discarded. The pinned
|
|
661
|
+
uploader sometimes emits only an error message, so missing status/code fields remain empty.
|
|
662
|
+
These fields do not change retry policy or the original result/diagnostic annotations.
|
|
663
|
+
The action exports `diagnostic-path`; the managed deploy template retains its JSON as
|
|
664
|
+
`play-upload-diagnostics-<run_id>-<run_attempt>` on success or failure. Adopt both action and
|
|
665
|
+
template to retain artifacts. Failed evidence validation/storage emits a fixed refusal and fails
|
|
666
|
+
the job without replaying provider work or changing its result.
|
|
667
|
+
|
|
655
668
|
Package-owner acquisition rechecks an occupied record within one 60-second wait budget before
|
|
656
669
|
provider work. Every read retains the binding and identity checks; cancellation interrupts the wait.
|
|
657
670
|
Time passing never authorizes clearing an owner. A known CI owner still requires exact terminal
|
|
@@ -22,7 +22,8 @@ Non-fatal by design: any failure is reported as a ::warning:: and the action
|
|
|
22
22
|
continues. The TestFlight upload itself has already succeeded by this point.
|
|
23
23
|
|
|
24
24
|
Environment:
|
|
25
|
-
ASC_KEY_ID, ASC_ISSUER_ID
|
|
25
|
+
ASC_KEY_ID, ASC_ISSUER_ID -- App Store Connect API identity
|
|
26
|
+
ASC_KEY_PATH / ASC_KEY_P8_PATH -- existing or prepared key-file path
|
|
26
27
|
MARKETING_VERSION -- e.g. "1.2.3"
|
|
27
28
|
APP_STORE_VERSION_ID -- appStoreVersion id (REUSE or CREATE)
|
|
28
29
|
APP_STORE_WHATS_NEW_FILE -- path to file holding release notes
|
|
@@ -228,16 +229,10 @@ def _update_all_localizations(
|
|
|
228
229
|
token: str, version_id: str, version: str, whats_new: str, seed_locale: str,
|
|
229
230
|
only_if_empty: bool = False,
|
|
230
231
|
) -> int:
|
|
231
|
-
"""
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
When ``only_if_empty`` is set the text is a fallback backstop: any
|
|
236
|
-
localization that already has a non-blank whatsNew (e.g. notes a human
|
|
237
|
-
entered) is left untouched, and only empty ones are filled. This lets the
|
|
238
|
-
step run unconditionally without clobbering good content, while still
|
|
239
|
-
guaranteeing no locale ships with an empty "What's New" (which App Store
|
|
240
|
-
Connect rejects as "This field is required")."""
|
|
232
|
+
"""Write localized notes, creating one seed when no localizations exist.
|
|
233
|
+
Return the count written. ``only_if_empty`` preserves existing non-blank
|
|
234
|
+
notes and fills the empty text App Store Connect requires before review.
|
|
235
|
+
"""
|
|
241
236
|
locs = get_json(
|
|
242
237
|
f"/appStoreVersions/{version_id}/appStoreVersionLocalizations", token
|
|
243
238
|
)
|
|
@@ -306,7 +301,7 @@ def main() -> int:
|
|
|
306
301
|
token = make_jwt(
|
|
307
302
|
_require_env("ASC_KEY_ID"),
|
|
308
303
|
_require_env("ASC_ISSUER_ID"),
|
|
309
|
-
|
|
304
|
+
os.environ.get("ASC_KEY_PATH", "").strip() or _require_env("ASC_KEY_P8_PATH"),
|
|
310
305
|
)
|
|
311
306
|
|
|
312
307
|
ver = get_json(f"/appStoreVersions/{version_id}", token)
|
|
@@ -26,11 +26,16 @@ inputs:
|
|
|
26
26
|
mobileReleaseIntentScript:
|
|
27
27
|
description: Committed mobile_release.cjs path, invoked after acquiring the package owner and before upload.
|
|
28
28
|
default: ''
|
|
29
|
+
versionCode:
|
|
30
|
+
description: Exact compiled version code for sanitized upload diagnostics only.
|
|
31
|
+
default: ''
|
|
29
32
|
outputs:
|
|
30
33
|
review-pending:
|
|
31
34
|
description: True only after a successful explicitly requested review-hold retry.
|
|
32
35
|
receipt:
|
|
33
36
|
description: Sanitized versioned result with attempt count and upstream identity.
|
|
37
|
+
diagnostic-path:
|
|
38
|
+
description: Runner-owned directory with closed per-attempt and terminal diagnostic JSON.
|
|
34
39
|
runs:
|
|
35
40
|
using: node24
|
|
36
41
|
main: run.cjs
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Observe only fixed progress/error patterns; never retain provider text in a receipt.
|
|
2
|
+
const provider = require('./provider_evidence.cjs');
|
|
2
3
|
const phases = new Map([
|
|
3
4
|
['Creating a new Edit for this release', 'edit_create'],
|
|
4
5
|
['Committing the Edit', 'edit_commit'],
|
|
@@ -22,10 +23,11 @@ const limit = 16384;
|
|
|
22
23
|
|
|
23
24
|
function create() {
|
|
24
25
|
return {phase: 'startup', signals: new Set(), pending: {stdout: '', stderr: ''},
|
|
25
|
-
discard: {stdout: false, stderr: false}};
|
|
26
|
+
discard: {stdout: false, stderr: false}, provider: provider.create()};
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function line(state, stream, value) {
|
|
30
|
+
provider.observe(state.provider, value);
|
|
29
31
|
if (stream === 'stdout') {
|
|
30
32
|
if (phases.has(value)) state.phase = phases.get(value);
|
|
31
33
|
for (const [prefix, phase] of prefixes) if (value.startsWith(prefix)) state.phase = phase;
|
|
@@ -70,4 +72,5 @@ function annotation(attempt, result) {
|
|
|
70
72
|
+ ` signals=${signals.join(',') || 'none'}`;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
module.exports = {create, consume, snapshot, annotation
|
|
75
|
+
module.exports = {create, consume, snapshot, annotation,
|
|
76
|
+
providerSnapshot: state => provider.snapshot(state.provider)};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// This separate, sanitized directory survives cleanup of the private uploader output.
|
|
2
|
+
const {mkdtempSync, writeFileSync} = require('node:fs');
|
|
3
|
+
const {isAbsolute, join} = require('node:path');
|
|
4
|
+
const records = require('./evidence_schema.cjs');
|
|
5
|
+
|
|
6
|
+
function begin(env) {
|
|
7
|
+
const binding = records.context(env);
|
|
8
|
+
let directory = '', failed = false, recorded = 0;
|
|
9
|
+
try {
|
|
10
|
+
if (!isAbsolute(env.RUNNER_TEMP || '') || /[\x00-\x1f\x7f]/.test(env.RUNNER_TEMP)) throw new Error();
|
|
11
|
+
directory = mkdtempSync(join(env.RUNNER_TEMP, 'gowalk-play-evidence-'));
|
|
12
|
+
} catch { failed = true; }
|
|
13
|
+
const save = (name, value) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!directory || !records.valid(value)) throw new Error();
|
|
16
|
+
writeFileSync(join(directory, name), JSON.stringify(value) + '\n', {mode: 0o600, flag: 'wx'});
|
|
17
|
+
} catch { failed = true; }
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
attempt(number, result) {
|
|
21
|
+
save(`attempt-${number}.json`, records.attempt(binding, number, result));
|
|
22
|
+
recorded += 1;
|
|
23
|
+
},
|
|
24
|
+
terminal(result) { save('terminal.json', records.terminal(binding, result, recorded)); },
|
|
25
|
+
get path() { return failed ? '' : directory; },
|
|
26
|
+
get failed() { return failed; },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {begin};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const provider = require('./provider_evidence.cjs');
|
|
2
|
+
const source = require('./source.cjs');
|
|
3
|
+
const schema = 'gowalk-cicd/play-upload-evidence.v1';
|
|
4
|
+
const phases = new Set(['startup', 'edit_create', 'tracks_read', 'bundle_upload', 'track_update', 'edit_commit']);
|
|
5
|
+
const signals = new Set(['connection_reset', 'connection_refused', 'connection_timeout', 'dns_failure',
|
|
6
|
+
'tls_verification', 'output_truncated']);
|
|
7
|
+
const processSignals = new Set(['none', 'SIGTERM', 'SIGINT', 'SIGKILL', 'other']);
|
|
8
|
+
const codes = new Set(['uploaded', 'review_pending', 'edit_expired', 'edit_deleted', 'review_hold_required',
|
|
9
|
+
'unclassified_failure', 'upload_cancelled', 'upload_outcome_uncertain', 'uploader_failed',
|
|
10
|
+
'runner_context_required', 'uploader_integrity_refused', 'uploader_source_unavailable',
|
|
11
|
+
'mobile_release_intent_unconfirmed', ...require('./owner.cjs').codes]);
|
|
12
|
+
const keys = (value, names) => value && typeof value === 'object' && !Array.isArray(value)
|
|
13
|
+
&& Object.keys(value).sort().join(',') === names.split(',').sort().join(',');
|
|
14
|
+
const integer = (value, minimum, maximum = Number.MAX_SAFE_INTEGER) =>
|
|
15
|
+
Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
16
|
+
const nullable = (value, check) => value === null || check(value);
|
|
17
|
+
|
|
18
|
+
function context(env) {
|
|
19
|
+
const positive = value => /^[1-9][0-9]*$/.test(value || '') && integer(Number(value), 1) ? Number(value) : null;
|
|
20
|
+
return {run_id: positive(env.GITHUB_RUN_ID), run_attempt: positive(env.GITHUB_RUN_ATTEMPT),
|
|
21
|
+
source_sha: /^[0-9a-f]{40}$/.test(env.GITHUB_SHA || '') ? env.GITHUB_SHA : null,
|
|
22
|
+
version_code: positive(env.INPUT_VERSIONCODE || env.ANDROID_BUILD_NUMBER)};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function base(binding, kind) {
|
|
26
|
+
return {schema, kind, ...binding, upstream_revision: source.revision, upstream_sha256: source.digest};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function attempt(binding, number, result) {
|
|
30
|
+
const diagnostic = result.diagnostic || {};
|
|
31
|
+
const observed = Array.isArray(diagnostic.signals) ? diagnostic.signals.filter(value => signals.has(value)) : [];
|
|
32
|
+
return {...base(binding, 'attempt'), attempt: number,
|
|
33
|
+
phase: phases.has(diagnostic.phase) ? diagnostic.phase : 'startup',
|
|
34
|
+
exit_code: integer(result.status, 0, 255) ? result.status : null,
|
|
35
|
+
signal: processSignals.has(result.signal) ? result.signal : result.signal ? 'other' : 'none',
|
|
36
|
+
error_count: integer(result.errors, 0) ? result.errors : null,
|
|
37
|
+
cancelled: result.cancelled === true, commit_reported: result.committed === true,
|
|
38
|
+
transport_signals: [...new Set(observed)].sort(),
|
|
39
|
+
provider: provider.valid(result.provider) ? result.provider : provider.snapshot(provider.create())};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function terminal(binding, result, recorded) {
|
|
43
|
+
return {...base(binding, 'terminal'), recorded_attempts: recorded, result: {
|
|
44
|
+
ok: result.ok === true, code: codes.has(result.code) ? result.code : 'uploader_failed',
|
|
45
|
+
attempts: integer(result.attempts, 0, 2) ? result.attempts : 0,
|
|
46
|
+
review_pending: result.review_pending === true}};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valid(value) {
|
|
50
|
+
const fields = 'schema,kind,run_id,run_attempt,source_sha,version_code,upstream_revision,upstream_sha256';
|
|
51
|
+
if (!value || value.schema !== schema || value.upstream_revision !== source.revision
|
|
52
|
+
|| value.upstream_sha256 !== source.digest || !nullable(value.run_id, item => integer(item, 1))
|
|
53
|
+
|| !nullable(value.run_attempt, item => integer(item, 1))
|
|
54
|
+
|| !nullable(value.version_code, item => integer(item, 1))
|
|
55
|
+
|| !nullable(value.source_sha, item => typeof item === 'string' && /^[0-9a-f]{40}$/.test(item))) return false;
|
|
56
|
+
if (value.kind === 'terminal') return keys(value, fields + ',recorded_attempts,result')
|
|
57
|
+
&& integer(value.recorded_attempts, 0, 2) && keys(value.result, 'ok,code,attempts,review_pending')
|
|
58
|
+
&& typeof value.result.ok === 'boolean' && codes.has(value.result.code)
|
|
59
|
+
&& integer(value.result.attempts, 0, 2) && typeof value.result.review_pending === 'boolean';
|
|
60
|
+
return value.kind === 'attempt' && keys(value, fields
|
|
61
|
+
+ ',attempt,phase,exit_code,signal,error_count,cancelled,commit_reported,transport_signals,provider')
|
|
62
|
+
&& integer(value.attempt, 1, 2) && phases.has(value.phase) && processSignals.has(value.signal)
|
|
63
|
+
&& nullable(value.exit_code, item => integer(item, 0, 255))
|
|
64
|
+
&& nullable(value.error_count, item => integer(item, 0)) && typeof value.cancelled === 'boolean'
|
|
65
|
+
&& typeof value.commit_reported === 'boolean' && Array.isArray(value.transport_signals)
|
|
66
|
+
&& value.transport_signals.length <= signals.size
|
|
67
|
+
&& new Set(value.transport_signals).size === value.transport_signals.length
|
|
68
|
+
&& [...value.transport_signals].every(item => signals.has(item)) && provider.valid(value.provider);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = {schema, context, attempt, terminal, valid};
|
|
@@ -53,7 +53,8 @@ function execute(program, env, directory, timeout) {
|
|
|
53
53
|
diagnostics.consume(diagnostic, 'stdout', '\n'); diagnostics.consume(diagnostic, 'stderr', '\n');
|
|
54
54
|
const committed = /(?:^|\n)committedEditId(?:=|<<)/.test(readFileSync(output, 'utf8'));
|
|
55
55
|
resolve({status, signal, cancelled, committed, errors: state.errors,
|
|
56
|
-
code: state.ambiguous ? 'unclassified_failure' : state.code, diagnostic: diagnostics.snapshot(diagnostic)
|
|
56
|
+
code: state.ambiguous ? 'unclassified_failure' : state.code, diagnostic: diagnostics.snapshot(diagnostic),
|
|
57
|
+
provider: diagnostics.providerSnapshot(diagnostic)});
|
|
57
58
|
});
|
|
58
59
|
});
|
|
59
60
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Closed observations only. Missing provider fields are never inferred from message text.
|
|
2
|
+
const http = new Set([400, 401, 402, 403, 404, 405, 408, 409, 410, 412, 413, 414, 415,
|
|
3
|
+
416, 417, 422, 428, 429, 500, 501, 502, 503, 504]);
|
|
4
|
+
const rpc = new Set(['CANCELLED', 'UNKNOWN', 'INVALID_ARGUMENT', 'DEADLINE_EXCEEDED', 'NOT_FOUND',
|
|
5
|
+
'ALREADY_EXISTS', 'PERMISSION_DENIED', 'RESOURCE_EXHAUSTED', 'FAILED_PRECONDITION', 'ABORTED',
|
|
6
|
+
'OUT_OF_RANGE', 'UNIMPLEMENTED', 'INTERNAL', 'UNAVAILABLE', 'DATA_LOSS', 'UNAUTHENTICATED']);
|
|
7
|
+
const messages = new Map([
|
|
8
|
+
['This edit has expired, please create a new Edit.', 'edit_expired'],
|
|
9
|
+
['This Edit has been deleted.', 'edit_deleted'],
|
|
10
|
+
['Changes cannot be sent for review automatically. Please set the query parameter changesNotSentForReview to true. '
|
|
11
|
+
+ 'Once committed, the changes in this edit can be sent for review from the Google Play Console UI.',
|
|
12
|
+
'review_hold_required'],
|
|
13
|
+
['Unknown error occurred.', 'uploader_unknown_error'],
|
|
14
|
+
['New edit has no ID, cannot continue.', 'edit_id_missing'],
|
|
15
|
+
]);
|
|
16
|
+
const classes = new Set(messages.values());
|
|
17
|
+
|
|
18
|
+
function create() {
|
|
19
|
+
return {http: new Set(), rpc: new Set(), classes: new Set()};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function observe(state, value) {
|
|
23
|
+
const match = /^::error(?: [^\r\n]+)?::(.*)$/.exec(value) || /^Error: (.*)$/.exec(value);
|
|
24
|
+
if (!match) return;
|
|
25
|
+
const text = match[1].replace(/%0D/g, '\r').replace(/%0A/g, '\n').replace(/%25/g, '%');
|
|
26
|
+
if (messages.has(text)) state.classes.add(messages.get(text));
|
|
27
|
+
const status = /^Request failed with status code ([45]\d\d)(?:\.?$|\.\s)/.exec(text);
|
|
28
|
+
if (status && http.has(Number(status[1]))) state.http.add(Number(status[1]));
|
|
29
|
+
let body;
|
|
30
|
+
try { body = JSON.parse(text); } catch { return; }
|
|
31
|
+
const error = body?.error;
|
|
32
|
+
if (!error || typeof error !== 'object' || Array.isArray(error) || !http.has(error.code)) return;
|
|
33
|
+
state.http.add(error.code);
|
|
34
|
+
if (rpc.has(error.status)) state.rpc.add(error.status);
|
|
35
|
+
if (messages.has(error.message)) state.classes.add(messages.get(error.message));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function snapshot(state) {
|
|
39
|
+
return {http_statuses: [...state.http].sort((a, b) => a - b), rpc_codes: [...state.rpc].sort(),
|
|
40
|
+
message_classes: [...state.classes].sort()};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function valid(value) {
|
|
44
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)
|
|
45
|
+
|| Object.keys(value).sort().join(',') !== 'http_statuses,message_classes,rpc_codes') return false;
|
|
46
|
+
return [['http_statuses', http], ['rpc_codes', rpc], ['message_classes', classes]].every(([key, choices]) =>
|
|
47
|
+
Array.isArray(value[key]) && value[key].length <= choices.size
|
|
48
|
+
&& new Set(value[key]).size === value[key].length && [...value[key]].every(item => choices.has(item)));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {create, observe, snapshot, valid};
|
|
@@ -6,6 +6,7 @@ const processRunner = require('./process.cjs');
|
|
|
6
6
|
const owner = require('./owner.cjs');
|
|
7
7
|
const diagnostics = require('./diagnostics.cjs');
|
|
8
8
|
const releaseIntent = require('./release_intent.cjs');
|
|
9
|
+
const evidence = require('./evidence.cjs');
|
|
9
10
|
|
|
10
11
|
async function upload(program, env, directory, execute = processRunner.execute, timeout = 3600000,
|
|
11
12
|
cancelled = () => false, report = () => {}) {
|
|
@@ -33,9 +34,10 @@ async function upload(program, env, directory, execute = processRunner.execute,
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
async function main() {
|
|
36
|
-
let directory, receipt;
|
|
37
|
+
let directory, receipt, records;
|
|
37
38
|
try {
|
|
38
39
|
const env = {...process.env};
|
|
40
|
+
records = evidence.begin(env);
|
|
39
41
|
if (!env.PLAY_EDIT_BINDING) throw new Error('play_owner_binding_required');
|
|
40
42
|
env.PLAY_EDIT_PRINCIPAL_SHA256 = require('./owner_principal.cjs').read(env.INPUT_SERVICEACCOUNTJSON);
|
|
41
43
|
if (!env.RUNNER_TEMP || !env.GITHUB_OUTPUT) throw new Error('runner_context_required');
|
|
@@ -45,7 +47,9 @@ async function main() {
|
|
|
45
47
|
if (cancelled()) return {ok: false, code: 'upload_cancelled', attempts: 0, review_pending: false};
|
|
46
48
|
await releaseIntent.begin(env);
|
|
47
49
|
return upload(program, env, directory, processRunner.execute, 3600000, cancelled,
|
|
48
|
-
(attempt, result) =>
|
|
50
|
+
(attempt, result) => {
|
|
51
|
+
records.attempt(attempt, result); console.log(diagnostics.annotation(attempt, result));
|
|
52
|
+
});
|
|
49
53
|
});
|
|
50
54
|
} catch (error) {
|
|
51
55
|
if (owner.codes.includes(error.message)) console.log(owner.annotation(error.message, undefined,
|
|
@@ -61,13 +65,19 @@ async function main() {
|
|
|
61
65
|
} finally { if (directory) rmSync(directory, {recursive: true, force: true}); }
|
|
62
66
|
receipt = {schema: 'gowalk-cicd/play-upload.v1', ...receipt, upstream_revision: source.revision,
|
|
63
67
|
upstream_sha256: source.digest};
|
|
68
|
+
records?.terminal(receipt);
|
|
64
69
|
const encoded = JSON.stringify(receipt);
|
|
65
70
|
if (process.env.GITHUB_OUTPUT) {
|
|
66
71
|
appendFileSync(process.env.GITHUB_OUTPUT, `review-pending=${receipt.review_pending}\nreceipt=${encoded}\n`);
|
|
72
|
+
appendFileSync(process.env.GITHUB_OUTPUT, `diagnostic-path=${records?.path || ''}\n`);
|
|
67
73
|
}
|
|
68
74
|
// The panel parses a success receipt even when a multi-line secret masks its outer braces.
|
|
69
75
|
console.log(`::${receipt.ok ? 'notice' : 'error'} title=play_upload_result::${encoded}`);
|
|
70
76
|
if (!receipt.ok) process.exitCode = 1;
|
|
77
|
+
if (records?.failed) {
|
|
78
|
+
console.log('::error title=play_upload_evidence_failed::Sanitized upload evidence could not be retained.');
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
module.exports = {upload, main};
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -658,6 +658,17 @@ jobs:
|
|
|
658
658
|
userFraction: >-
|
|
659
659
|
${{ vars.GOOGLE_PLAY_STATUS == 'inProgress' && (vars.GOOGLE_PLAY_USER_FRACTION || '0.2') || '' }}
|
|
660
660
|
whatsNewDirectory: ${{ hashFiles('distribution/whatsnew/**') != '' && 'distribution/whatsnew' || '' }}
|
|
661
|
+
versionCode: ${{ steps.android.outputs.build-number || steps.reuse.outputs.build-number }}
|
|
662
|
+
|
|
663
|
+
- name: Retain sanitized Play upload diagnostics
|
|
664
|
+
if: ${{ always() && steps.play-upload.outputs.diagnostic-path != '' }}
|
|
665
|
+
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
|
666
|
+
env:
|
|
667
|
+
NODE_OPTIONS: --require "${{ github.workspace }}/.github/actions/android-app/scripts/artifact_env.cjs"
|
|
668
|
+
with:
|
|
669
|
+
name: play-upload-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}
|
|
670
|
+
path: ${{ steps.play-upload.outputs.diagnostic-path }}/*.json
|
|
671
|
+
if-no-files-found: error
|
|
661
672
|
|
|
662
673
|
# The retry landed the bundle without submitting it. That is a fact for
|
|
663
674
|
- name: Record confirmed Android upload
|
|
Binary file
|