gowalk-cicd 1.0.158 → 1.0.161
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 +19 -10
- package/action/.daemux-version +1 -1
- package/action/scripts/mobile_inputs.cjs +23 -17
- package/action/scripts/mobile_release.cjs +3 -1
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/bin/cli.mjs +1 -1
- package/docs/CONFIGURATION.md +8 -6
- package/package.json +2 -2
- package/remedies.json +32 -0
- package/src/config/validation-events.mjs +28 -46
- package/src/config/validation.mjs +2 -4
- package/src/install.mjs +1 -1
- package/src/release.mjs +2 -32
- package/templates/deploy.yml +3 -1
- package/templates/history.json.gz +0 -0
- package/CLAUDE.md +0 -476
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Ready task PRs build shipping binaries alongside validation; the protected merge
|
|
|
34
34
|
binaries through the existing store upload paths. See [candidate artifact reuse](docs/CANDIDATE_ARTIFACTS.md)
|
|
35
35
|
for identity checks, native dependency caches, version handling and adoption of custom workflows.
|
|
36
36
|
|
|
37
|
-
Mobile delivery requires an explicit
|
|
37
|
+
Mobile delivery requires an explicit release intent. After source, signing, account transport,
|
|
38
38
|
and integration readiness pass, run on the task branch:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
@@ -48,12 +48,17 @@ Merge the exact validated candidate through the repository's protected PR. Only
|
|
|
48
48
|
web, documentation and tooling landings do not trigger a store build. Backend and web retain
|
|
49
49
|
their own workflows. Web compilation no longer starts just because a plugin version marker changed.
|
|
50
50
|
|
|
51
|
-
The
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
The CLI reads no GitHub variables. Its v2 manifest contains only `schema` and a `platforms`
|
|
52
|
+
map: source hashes make each changed-source release declaration a file change, but never freeze
|
|
53
|
+
admission. A push changing the intent admits the pushed tree for its declared platforms. Other
|
|
54
|
+
events, including manual dispatch, admit only the tree of the last intent-changing commit.
|
|
55
|
+
Candidate repairs included in the protected merge therefore need no re-freeze. A dispatch of a
|
|
56
|
+
later landing still refuses. Existing v1 intents remain accepted.
|
|
57
|
+
|
|
58
|
+
Upload and binary reuse identities use the checkout actually built plus its current build variables.
|
|
59
|
+
Native iOS and Android directories are scoped to their platform. Unknown inputs, embedded editors,
|
|
60
|
+
assets, scripts, dependency locks, documents and backend-generated resources remain in the
|
|
61
|
+
conservative source fingerprint. These hashes add no push triggers and do not gate admission.
|
|
57
62
|
|
|
58
63
|
The gate records each platform in GitHub deployment receipts, scoped to that input fingerprint.
|
|
59
64
|
A confirmed upload is reused on subsequent attempts and emits `mobile_upload_reused` with the
|
|
@@ -67,8 +72,12 @@ receipt and read back its exact version with the account-pinned store tooling. R
|
|
|
67
72
|
upload when necessary. After positive upload readback, reconcile the named GitHub deployment to
|
|
68
73
|
`success` with description `uploaded`, retaining the provider evidence in the app task. Never clear
|
|
69
74
|
an uncertain receipt based on elapsed time or a missing response. A terminal attempt that failed
|
|
70
|
-
before the upload marker may retry.
|
|
71
|
-
|
|
75
|
+
before the upload marker may retry. An earlier admission attempt of the same run, source and
|
|
76
|
+
platform supports failed-job retries while preserving the required upload phase.
|
|
77
|
+
|
|
78
|
+
`remedies.json` contains failure recovery guidance keyed by `schema`, `code` and `fixed_in`.
|
|
79
|
+
Each `remedy` is at most 4 KB. `mobile_release_refused` emits
|
|
80
|
+
`gowalk-cicd/mobile-release-refused.v1` with its fixed failure `code`; success notices carry no remedy.
|
|
72
81
|
|
|
73
82
|
Mobile runs serialize without cancelling an in-flight upload. Manual dispatch obeys the same
|
|
74
83
|
intent and receipts; it cannot silently rebuild an already-uploaded input. Feature-branch dispatch
|
|
@@ -1160,7 +1169,7 @@ on a verified task branch (under both `.github/actions/swift-app/` and
|
|
|
1160
1169
|
| Candidates | Action updates and later version changes have separate verified receipts |
|
|
1161
1170
|
| Failure mode | npm discovery remains optional; unconfirmed source preservation fails the job |
|
|
1162
1171
|
|
|
1163
|
-
The shipped `deploy.yml` watches only the
|
|
1172
|
+
The shipped `deploy.yml` watches only the release intent. Adopting an action-directory
|
|
1164
1173
|
update on its own therefore does not launch another mobile upload. Review and adopt the template
|
|
1165
1174
|
as well as the action directories to enable this behavior in an existing consumer.
|
|
1166
1175
|
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.161
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Release declarations and build identities. Unknown paths remain inputs, including embedded editors. */
|
|
2
2
|
'use strict';
|
|
3
3
|
const {execFileSync} = require('node:child_process');
|
|
4
4
|
const {createHash} = require('node:crypto');
|
|
5
5
|
const fs = require('node:fs');
|
|
6
6
|
const INTENT = '.factory/releases/mobile.json';
|
|
7
|
-
const SCHEMA = 'gowalk-cicd/mobile-release.
|
|
7
|
+
const SCHEMA = 'gowalk-cicd/mobile-release.v2';
|
|
8
8
|
const git = (...args) => execFileSync('git', args, {encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore']});
|
|
9
9
|
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
10
10
|
|
|
@@ -21,7 +21,7 @@ function input(name, platform) {
|
|
|
21
21
|
|
|
22
22
|
// Named vars read by templates/deploy.yml, except the rotating Play ownership
|
|
23
23
|
// lease: it authorizes provider access but never changes a build. Keep the list
|
|
24
|
-
// checked against the template
|
|
24
|
+
// checked against the template. Configuration changes affect reuse, never release admission.
|
|
25
25
|
const BUILD_VARIABLES = [
|
|
26
26
|
'BACKEND_API_DOMAIN', 'BACKEND_APP_NAME', 'BACKEND_DEPLOY_HOST', 'BACKEND_HEALTH_PATH',
|
|
27
27
|
'BACKEND_INGRESS_REQUIRED', 'BACKEND_PUBLIC_BASE_URL', 'BACKEND_PUBLIC_HEALTH_URL',
|
|
@@ -76,20 +76,15 @@ function iosPodsCommitted() {
|
|
|
76
76
|
return PODS.every(path => tracked.has(path));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
function create(platforms = ['ios', 'android']
|
|
79
|
+
function create(platforms = ['ios', 'android']) {
|
|
80
80
|
if (['main', 'master', ''].includes(git('branch', '--show-current').trim())) {
|
|
81
81
|
throw Error('mobile_release_task_branch_required');
|
|
82
82
|
}
|
|
83
83
|
const dirty = git('status', '--porcelain', '-z', '--untracked-files=all').split('\0').filter(Boolean);
|
|
84
84
|
if (dirty.some(row => row.slice(3) !== INTENT)) throw Error('mobile_release_source_not_committed');
|
|
85
|
-
//
|
|
86
|
-
// spends the whole freeze on a job that cannot start: CI requires the
|
|
87
|
-
// committed lockfile and refuses Xcode-managed package downloads, so the iOS
|
|
88
|
-
// job fails in native preparation and the fix needs another intent. Across
|
|
89
|
-
// eight rebuild tasks on 2026-09-09 there were 35 Mobile Deploy runs and 3
|
|
90
|
-
// green conclusions, all three on the one repository committing this pair.
|
|
85
|
+
// CI requires the committed CocoaPods pair for resolved Flutter iOS plugins.
|
|
91
86
|
if (platforms.includes('ios') && !iosPodsCommitted()) throw Error('mobile_release_ios_pods_uncommitted');
|
|
92
|
-
const manifest = {schema: SCHEMA,
|
|
87
|
+
const manifest = {schema: SCHEMA,
|
|
93
88
|
platforms: Object.fromEntries(platforms.map(p => [p, fingerprint(p)]))};
|
|
94
89
|
fs.mkdirSync('.factory/releases', {recursive: true});
|
|
95
90
|
fs.writeFileSync(INTENT, JSON.stringify(manifest, null, 2) + '\n');
|
|
@@ -98,13 +93,24 @@ function create(platforms = ['ios', 'android'], variables = {}) {
|
|
|
98
93
|
|
|
99
94
|
function read(variables = {}) {
|
|
100
95
|
const manifest = JSON.parse(fs.readFileSync(INTENT, 'utf8'));
|
|
101
|
-
if (manifest.
|
|
102
|
-
|
|
96
|
+
if (![SCHEMA, 'gowalk-cicd/mobile-release.v1'].includes(manifest.schema) ||
|
|
97
|
+
!manifest.platforms || !Object.keys(manifest.platforms).length ||
|
|
103
98
|
Object.entries(manifest.platforms).some(([p, sha]) =>
|
|
104
99
|
!['ios', 'android'].includes(p) || !/^[a-f0-9]{64}$/.test(sha))) throw Error('mobile_release_intent_invalid');
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
// Stored hashes make a later release declaration a file change, not an admission fence.
|
|
101
|
+
return {schema: manifest.schema, configuration_sha256: configuration(variables),
|
|
102
|
+
platforms: Object.fromEntries(Object.keys(manifest.platforms).map(p => [p, fingerprint(p)]))};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function admit() {
|
|
106
|
+
const latest = git('log', '--first-parent', '-1', '--format=%H', '--', INTENT).trim();
|
|
107
|
+
if (!latest) throw Error('mobile_release_intent_invalid');
|
|
108
|
+
const tree = git('rev-parse', 'HEAD^{tree}').trim();
|
|
109
|
+
const declared = git('rev-parse', `${latest}^{tree}`).trim();
|
|
110
|
+
const changed = git('diff-tree', '--root', '--diff-merges=first-parent', '--no-commit-id', '--name-only',
|
|
111
|
+
'-r', 'HEAD', '--', INTENT).trim();
|
|
112
|
+
if (!(process.env.GITHUB_EVENT_NAME === 'push' && changed) && tree !== declared) {
|
|
113
|
+
throw Error('mobile_release_intent_stale');
|
|
107
114
|
}
|
|
108
|
-
return manifest;
|
|
109
115
|
}
|
|
110
|
-
module.exports = {INTENT, SCHEMA, BUILD_VARIABLES, buildVariables, input, fingerprint, create, read, configuration};
|
|
116
|
+
module.exports = {INTENT, SCHEMA, BUILD_VARIABLES, buildVariables, input, fingerprint, create, read, configuration, admit};
|
|
@@ -38,6 +38,7 @@ async function reusable(record, expected) {
|
|
|
38
38
|
|
|
39
39
|
async function admit(platforms = 'both') {
|
|
40
40
|
if (!['both', 'ios', 'android'].includes(platforms)) throw Error('mobile_release_platform_invalid');
|
|
41
|
+
inputs.admit();
|
|
41
42
|
const manifest = inputs.read(JSON.parse(process.env.MOBILE_RELEASE_VARIABLES || '{}'));
|
|
42
43
|
if (process.env.MOBILE_CANDIDATE_BUILD === 'true') return candidatePlatforms(manifest, platforms);
|
|
43
44
|
const selected = {};
|
|
@@ -115,7 +116,8 @@ async function main(args) {
|
|
|
115
116
|
}
|
|
116
117
|
if (require.main === module) main(process.argv.slice(2)).catch(error => {
|
|
117
118
|
const code = /^mobile_release_[a-z0-9_]+$/.test(error.message) ? error.message : 'mobile_release_unavailable';
|
|
118
|
-
console.error(
|
|
119
|
+
console.error('::error title=mobile_release_refused::' +
|
|
120
|
+
JSON.stringify({schema: 'gowalk-cicd/mobile-release-refused.v1', code}));
|
|
119
121
|
process.exitCode = 1;
|
|
120
122
|
});
|
|
121
123
|
module.exports = {owner, reusable, admit, phase, candidatePlatforms};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.161
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.161
|
package/bin/cli.mjs
CHANGED
|
@@ -62,7 +62,7 @@ What it writes:
|
|
|
62
62
|
.github/workflows/deploy-web.yml - Flutter web release workflow when web/ exists
|
|
63
63
|
|
|
64
64
|
Options:
|
|
65
|
-
release [ios|android|both] Prepare a
|
|
65
|
+
release [ios|android|both] Prepare a mobile release intent on the task branch
|
|
66
66
|
--dry-run Print what would happen without writing files
|
|
67
67
|
-v, --version Show version number
|
|
68
68
|
-h, --help Show this help message
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -76,12 +76,14 @@ inferred safe from a custom step name. Candidate misses never wait or reserve a
|
|
|
76
76
|
|
|
77
77
|
Separate custom workflows remain app-owned. Recognized native dependency jobs
|
|
78
78
|
receive optional lock/toolchain-keyed cache restore/save inside their existing job.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
An app-owned workflow whose push trigger is a plain `branches` list admitting `task/**`
|
|
80
|
+
validates each candidate on that push, which runs on the exact head: the installer deletes its
|
|
81
|
+
duplicate `pull_request` trigger, and a trailing `!task/**` written by earlier releases, in one
|
|
82
|
+
write. Job names, commands and custom checks are unchanged. A push with `branches-ignore`,
|
|
83
|
+
`paths` or `paths-ignore`, or one that does not admit `task/**`, is left as it is; such
|
|
84
|
+
workflows retain separate push/PR concurrency groups, so neither check cancels the other.
|
|
85
|
+
An upgrade also repairs the shared group emitted by 1.0.142–143. Separate platforms and
|
|
86
|
+
repositories keep their existing parallelism.
|
|
85
87
|
|
|
86
88
|
`.gowalk-cicd.lock.json` records the template and generated-file digests. Commit it
|
|
87
89
|
with the configuration and rendered workflows. It is migration state, not a CI job.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gowalk-cicd",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.161",
|
|
4
4
|
"description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"!backend-action/**/*.py[cod]",
|
|
21
21
|
"templates/",
|
|
22
22
|
"README.md",
|
|
23
|
-
"
|
|
23
|
+
"remedies.json",
|
|
24
24
|
"docs/GRADLE_CACHE.md",
|
|
25
25
|
"docs/CANDIDATE_ARTIFACTS.md",
|
|
26
26
|
"docs/BACKEND_EDGE.md",
|
package/remedies.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
4
|
+
"code": "mobile_release_phase_owner_mismatch",
|
|
5
|
+
"fixed_in": "1.0.126",
|
|
6
|
+
"remedy": "Inspect the release helper the workflow actually executes, including its immutable action snapshot. Before 1.0.126, a failed-jobs-only retry could retain an earlier admission attempt and refuse after rebuilding. If its receipt still says building, rerun the whole original workflow or adopt the released helper through a checked candidate. From 1.0.126 an earlier attempt of the same run, source and platform is supported, while begin-upload still requires building. A foreign run, source or platform remains refused. Preserve explicit task pins and app-specific inputs; never patch consumer ownership checks or relabel an ambiguous receipt."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"schema": "gowalk-cicd/play-owner.v1",
|
|
10
|
+
"code": "play_owner_busy",
|
|
11
|
+
"fixed_in": "1.0.132",
|
|
12
|
+
"remedy": "Read the package edit owner's current state through supported tooling and preserve unrelated work. The 1.0.132 package includes the 1.0.131 repair that records Android upload intent inside the acquired Play owner: adopt its action and workflow together, using mobileReleaseIntentScript instead of a separate Record Android upload intent step. A new owner refusal then leaves the release receipt building. Older workflows may already have marked uploading even though no provider call was attempted. That existing ambiguity still needs exact provider readback and supported recovery with the retained binary; neither elapsed time nor an owner refusal permits clearing the marker."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
16
|
+
"code": "mobile_release_upload_phase_unconfirmed",
|
|
17
|
+
"fixed_in": "1.0.132",
|
|
18
|
+
"remedy": "Read the original run, attempt and deployment receipt before retrying. A completed uploaded receipt is reused with its original evidence; finish its metadata and submission work. An uploading receipt is ambiguous even when a later owner refusal reports no provider attempt. Adopt the 1.0.132 action and workflow together to place future Android intent writes inside acquired Play ownership. For an existing receipt, prove the exact provider build/version or use supported retained-binary recovery; never relabel it building by hand or clear ownership on elapsed time."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
22
|
+
"code": "mobile_release_http_500",
|
|
23
|
+
"fixed_in": "1.0.147",
|
|
24
|
+
"remedy": "A GitHub receipt POST can persist uploading and still return HTTP 500 before the uploader executes; this proves neither an Apple upload nor a capacity limit. From 1.0.147 the helper reconciles its uniquely marked write by bounded readback without replaying the POST. Preserve the original run, attempt, binary and receipt if it still fails. recover_upload_intent.cjs owner/repo deployment-id plan|live restores an iOS intent only after fresh proof that its exact failed HTTP 500 intent step preceded an explicitly skipped uploader. Inspect the plan and live readback, then retry the original failed job. An executed uploader or uncertain evidence keeps the guard closed."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"schema": "gowalk-cicd/mobile-release-refused.v1",
|
|
28
|
+
"code": "mobile_release_intent_stale",
|
|
29
|
+
"fixed_in": "1.0.160",
|
|
30
|
+
"remedy": "From 1.0.160 a push changing .factory/releases/mobile.json admits the pushed tree for its declared platforms; upload and binary reuse keys come from the built tree and current build variables. Other events, including workflow_dispatch and newer-head retries, require the built tree to equal the last intent-changing commit's tree. Prepare and commit a new release intent on a task branch, land it through the required candidate checks, and follow that deployment. A dispatch of later source cannot substitute for the approved release. Legacy v1 intents are accepted; their stored source/configuration hashes no longer gate admission."
|
|
31
|
+
}
|
|
32
|
+
]
|
|
@@ -1,56 +1,38 @@
|
|
|
1
|
-
//
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
1
|
+
// A task/** push already validates the exact candidate head, so a duplicate PR trigger is deleted.
|
|
3
2
|
const REF = '${{ github.ref }}';
|
|
4
3
|
const COALESCED = '${{ github.event.pull_request.head.repo.full_name == github.repository'
|
|
5
4
|
+ ' && github.event.pull_request.head.ref || github.ref_name }}';
|
|
5
|
+
const FACTORY_EXCLUSION = '!task/**';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
// Repair the shared push/PR group emitted by 1.0.142–143.
|
|
8
|
+
function normalizeGroup(doc, concurrency) {
|
|
9
|
+
if (concurrency?.['cancel-in-progress'] !== true || typeof concurrency.group !== 'string') return false;
|
|
10
|
+
const group = concurrency.group.replace(COALESCED, REF);
|
|
11
|
+
const recognized = group.includes('${{ github.workflow }}') || group === `flutter-ci-${REF}`;
|
|
12
|
+
if (group === concurrency.group || !group.includes(REF) || !recognized) return false;
|
|
13
|
+
doc.setIn(['concurrency', 'group'], group);
|
|
14
|
+
return true;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return
|
|
17
|
+
// Only a plain branch list whose last pattern for task refs admits them all; anything else is left alone.
|
|
18
|
+
function candidateBranches(push) {
|
|
19
|
+
if (!push || typeof push !== 'object' || Array.isArray(push)) return null;
|
|
20
|
+
if (['branches-ignore', 'paths', 'paths-ignore'].some(key => Object.hasOwn(push, key))) return null;
|
|
21
|
+
const values = push.branches;
|
|
22
|
+
if (!Array.isArray(values) || values.some(value => typeof value !== 'string')) return null;
|
|
23
|
+
const branches = values.at(-1) === FACTORY_EXCLUSION ? values.slice(0, -1) : values;
|
|
24
|
+
const admitted = branches.findLastIndex(value => value === 'task/**' || value === '**');
|
|
25
|
+
if (admitted < 0 || branches.slice(admitted + 1).some(value => value.startsWith('!'))) return null;
|
|
26
|
+
return branches;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
function
|
|
28
|
-
if (push !== null && (typeof push !== 'object' || Array.isArray(push))) return false;
|
|
29
|
-
const filters = push || {};
|
|
30
|
-
const field = Object.hasOwn(filters, 'branches') ? 'branches' : 'branches-ignore';
|
|
31
|
-
if (!Object.hasOwn(filters, field) && ('tags' in filters || 'tags-ignore' in filters)) return false;
|
|
32
|
-
const values = filters[field] || [];
|
|
33
|
-
if (!Array.isArray(values) || values.some(value => typeof value !== 'string')) return false;
|
|
34
|
-
if ('branches' in filters && 'branches-ignore' in filters) return false;
|
|
35
|
-
const exclusion = field === 'branches' ? '!task/**' : 'task/**';
|
|
36
|
-
if (values.at(-1) === exclusion) return false;
|
|
37
|
-
doc.setIn(['on', 'push'], doc.createNode({ ...filters, [field]: [...values, exclusion] }));
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function upgradeEvents(doc, value, branch) {
|
|
29
|
+
export function upgradeEvents(doc, value) {
|
|
42
30
|
const events = value.on;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
let changed = group !== original;
|
|
51
|
-
if (changed) doc.setIn(['concurrency', 'group'], group);
|
|
52
|
-
if (coversCandidates(events.pull_request, branch, value.jobs)) {
|
|
53
|
-
changed = excludeFactoryPush(doc, events.push) || changed;
|
|
54
|
-
}
|
|
55
|
-
return changed;
|
|
31
|
+
if (!events || !Object.hasOwn(events, 'push') || !Object.hasOwn(events, 'pull_request')) return false;
|
|
32
|
+
const grouped = normalizeGroup(doc, value.concurrency);
|
|
33
|
+
const branches = candidateBranches(events.push);
|
|
34
|
+
if (!branches) return grouped;
|
|
35
|
+
if (branches.length < events.push.branches.length) doc.deleteIn(['on', 'push', 'branches', branches.length]);
|
|
36
|
+
doc.deleteIn(['on', 'pull_request']);
|
|
37
|
+
return true;
|
|
56
38
|
}
|
|
@@ -3,7 +3,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { readYaml, MigrationRepair } from './yaml.mjs';
|
|
5
5
|
import { upgradeReuse } from './validation-reuse.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { upgradeEvents } from './validation-events.mjs';
|
|
7
7
|
const CACHE = 'caa296126883cff596d87d8935842f9db880ef25';
|
|
8
8
|
const PREFIX = '.github/actions/swift-app/scripts/';
|
|
9
9
|
|
|
@@ -54,12 +54,10 @@ export function planValidation(root, managed) {
|
|
|
54
54
|
if (!existsSync(directory)) return [];
|
|
55
55
|
const writes = [];
|
|
56
56
|
const files = readdirSync(directory).filter(file => /\.ya?ml$/.test(file) && !managed.includes(file));
|
|
57
|
-
// Another app-owned workflow may depend on this workflow's push evidence.
|
|
58
|
-
const branch = files.length === 1 ? defaultBranch(root) : '';
|
|
59
57
|
for (const file of files) {
|
|
60
58
|
const source = readFileSync(join(directory, file), 'utf8');
|
|
61
59
|
const { doc, value } = readYaml(source, file);
|
|
62
|
-
const events = upgradeEvents(doc, value
|
|
60
|
+
const events = upgradeEvents(doc, value);
|
|
63
61
|
const reused = upgradeReuse(doc, value, file);
|
|
64
62
|
const cached = upgradeCaches(doc, doc.toJS({ maxAliasCount: 100 }), file);
|
|
65
63
|
if (cached || events || reused) {
|
package/src/install.mjs
CHANGED
|
@@ -167,7 +167,7 @@ function printSummary() {
|
|
|
167
167
|
console.log(' GOOGLE_PLAY_SERVICE_ACCOUNT_JSON');
|
|
168
168
|
console.log(' The workflow materializes them with mode 0600 only on its ephemeral runner.');
|
|
169
169
|
console.log(' Existing checkout credentials remain a backward-compatible fallback.');
|
|
170
|
-
console.log(' Validate committed task source, run gowalk-cicd release, and merge its
|
|
170
|
+
console.log(' Validate committed task source, run gowalk-cicd release, and merge its intent through a PR.');
|
|
171
171
|
console.log('');
|
|
172
172
|
console.log('Re-run this installer anytime to update the vendored action:');
|
|
173
173
|
console.log(' npx --yes gowalk-cicd');
|
package/src/release.mjs
CHANGED
|
@@ -1,36 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
import {execFileSync} from 'node:child_process';
|
|
1
|
+
// Declare the platforms ready to ship from committed task source; no remote configuration read.
|
|
3
2
|
import inputs from '../action/scripts/mobile_inputs.cjs';
|
|
4
3
|
|
|
5
|
-
export function variables(repository, execute) {
|
|
6
|
-
const info = JSON.parse(execute('gh', ['api', `repos/${repository}`]));
|
|
7
|
-
if (info.full_name !== repository || !['User', 'Organization'].includes(info.owner?.type)) {
|
|
8
|
-
throw Error('mobile_release_repository_unverified');
|
|
9
|
-
}
|
|
10
|
-
const scopes = info.owner.type === 'Organization' ? ['organization-variables', 'variables'] : ['variables'];
|
|
11
|
-
const result = {};
|
|
12
|
-
for (const scope of scopes) {
|
|
13
|
-
const pages = JSON.parse(execute('gh', ['api', `repos/${repository}/actions/${scope}?per_page=100`,
|
|
14
|
-
'--paginate', '--slurp']));
|
|
15
|
-
if (!Array.isArray(pages) || !pages.length || pages.some(page => !Array.isArray(page?.variables))) {
|
|
16
|
-
throw Error('mobile_release_configuration_unverified');
|
|
17
|
-
}
|
|
18
|
-
for (const row of pages.flatMap(page => page.variables)) {
|
|
19
|
-
if (!row || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(row.name) || typeof row.value !== 'string') {
|
|
20
|
-
throw Error('mobile_release_configuration_unverified');
|
|
21
|
-
}
|
|
22
|
-
result[row.name] = row.value;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return inputs.buildVariables(result);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
4
|
export function release(platform) {
|
|
29
|
-
|
|
30
|
-
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 60000,
|
|
31
|
-
}).trim();
|
|
32
|
-
const remote = execute('git', ['remote', 'get-url', 'origin']);
|
|
33
|
-
const match = /^(?:git@github\.com:|https:\/\/github\.com\/)([\w.-]+\/[\w.-]+?)(?:\.git)?$/.exec(remote);
|
|
34
|
-
if (!match) throw Error('mobile_release_repository_unverified');
|
|
35
|
-
return inputs.create(platform === 'both' ? ['ios', 'android'] : [platform], variables(match[1], execute));
|
|
5
|
+
return inputs.create(platform === 'both' ? ['ios', 'android'] : [platform]);
|
|
36
6
|
}
|
package/templates/deploy.yml
CHANGED
|
@@ -23,7 +23,7 @@ concurrency:
|
|
|
23
23
|
|
|
24
24
|
jobs:
|
|
25
25
|
release:
|
|
26
|
-
name: Admit
|
|
26
|
+
name: Admit mobile release
|
|
27
27
|
if: ${{ github.ref_name == github.event.repository.default_branch || inputs.candidate-build }}
|
|
28
28
|
runs-on: ubuntu-24.04
|
|
29
29
|
timeout-minutes: 5
|
|
@@ -32,6 +32,8 @@ jobs:
|
|
|
32
32
|
android: ${{ steps.intent.outputs.android }}
|
|
33
33
|
steps:
|
|
34
34
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
|
|
35
|
+
with:
|
|
36
|
+
fetch-depth: 0
|
|
35
37
|
- name: Verify release intent and prior upload receipts
|
|
36
38
|
id: intent
|
|
37
39
|
env:
|
|
Binary file
|
package/CLAUDE.md
DELETED
|
@@ -1,476 +0,0 @@
|
|
|
1
|
-
# gowalk-cicd — Development Guide
|
|
2
|
-
|
|
3
|
-
## Purpose
|
|
4
|
-
|
|
5
|
-
npm package that distributes iOS TestFlight, Android Google Play, and Flutter
|
|
6
|
-
web release workflows.
|
|
7
|
-
Consumer runs `npx gowalk-cicd` and gets:
|
|
8
|
-
|
|
9
|
-
- `.github/actions/swift-app/` — vendored composite action
|
|
10
|
-
- `.github/actions/android-app/` — vendored Android action (Flutter or Gradle)
|
|
11
|
-
- `.github/workflows/deploy.yml` — dual-platform workflow
|
|
12
|
-
- `.github/workflows/deploy-web.yml` — Flutter web release artifact workflow
|
|
13
|
-
(only when `web/index.html` exists)
|
|
14
|
-
|
|
15
|
-
Re-running the installer updates it. `gowalk-cicd release [ios|android|both]` creates the
|
|
16
|
-
explicit source-and-configuration intent; it does not build or upload. Keep this surface small.
|
|
17
|
-
|
|
18
|
-
## Structure
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
gowalk-cicd/ ← repo root IS the gowalk-cicd package
|
|
22
|
-
├── action/ ← iOS action — canonical source of truth
|
|
23
|
-
│ ├── action.yml
|
|
24
|
-
│ └── scripts/*.py
|
|
25
|
-
├── android-action/ ← Android action — canonical source of truth
|
|
26
|
-
│ ├── action.yml ← branches on project kind (flutter | gradle)
|
|
27
|
-
│ └── scripts/*.py
|
|
28
|
-
├── bin/cli.mjs ← CLI entrypoint (parses flags, calls runInstall)
|
|
29
|
-
├── src/install.mjs ← copy logic, gitignore-check, summary print
|
|
30
|
-
├── templates/deploy.yml ← managed dual-platform workflow template
|
|
31
|
-
├── templates/deploy-web.yml ← Flutter web workflow copied conditionally
|
|
32
|
-
├── backend-action/ ← backend deploy action + remote host script
|
|
33
|
-
├── templates/deploy-backend.yml ← backend workflow copied when compose exists
|
|
34
|
-
├── scripts/auto-version.mjs ← patch-bump-from-npm used by the publish workflow
|
|
35
|
-
├── .github/workflows/publish.yml ← auto-publish to npm on push to main
|
|
36
|
-
├── README.md ← consumer docs
|
|
37
|
-
├── CLAUDE.md ← this file (for AI agents)
|
|
38
|
-
└── package.json
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Key invariant: action directories are canonical sources
|
|
42
|
-
|
|
43
|
-
The composite actions under `action/` and `android-action/` are the source of
|
|
44
|
-
truth. Edit them directly here; consumer copies are generated by the installer.
|
|
45
|
-
|
|
46
|
-
There is no upstream copy to sync from. This package was split out of the
|
|
47
|
-
daemux-plugins monorepo, where `action/` used to be a vendored mirror of
|
|
48
|
-
`.github/actions/ios-native-testflight/` refreshed by a `sync-action.mjs`
|
|
49
|
-
step. That relationship no longer applies — the sync script and the
|
|
50
|
-
vendoring concept are gone.
|
|
51
|
-
|
|
52
|
-
## Publishing
|
|
53
|
-
|
|
54
|
-
Auto-publish on every push to `main` (the entire repo IS the package). The
|
|
55
|
-
workflow (`.github/workflows/publish.yml`) serializes version selection through
|
|
56
|
-
publication; PR checks use separate cancellable groups. This prevents concurrent
|
|
57
|
-
main runs from choosing the same npm patch version. The workflow:
|
|
58
|
-
|
|
59
|
-
1. Runs `scripts/auto-version.mjs .` which queries npm for the current
|
|
60
|
-
published version of `gowalk-cicd` and bumps the patch. A first publish
|
|
61
|
-
keeps the seed version from `package.json`.
|
|
62
|
-
2. Stamps the new version into both action directories (consumed by the
|
|
63
|
-
consumer-side autoupdate check) before publishing.
|
|
64
|
-
3. Runs `npm publish --access public` using npm Trusted Publishing. The new
|
|
65
|
-
package name requires one bootstrap publish via `NPM_TOKEN` before npm can
|
|
66
|
-
attach the `publish.yml` trusted-publisher relationship; the workflow
|
|
67
|
-
supplies that token as a fallback and uses OIDC after trust is configured.
|
|
68
|
-
|
|
69
|
-
Manual publish (from repo root):
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
npm publish --access public
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## Testing locally
|
|
76
|
-
|
|
77
|
-
From a scratch directory:
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
mkdir /tmp/scratch && cd /tmp/scratch && git init
|
|
81
|
-
node /path/to/gowalk-cicd/bin/cli.mjs --dry-run
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
Or install into a real app repo after `npm install` in this source checkout
|
|
85
|
-
(updates action directories and renders workflows from `.gowalk-cicd.yml`):
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
cd ~/path/to/app-project
|
|
89
|
-
node /path/to/gowalk-cicd/bin/cli.mjs
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Conventions
|
|
93
|
-
|
|
94
|
-
- Prefer Node built-ins. `yaml` parses and edits app configuration/workflows during installation;
|
|
95
|
-
`update-notifier` supplies the optional update banner. Neither adds an app CI job.
|
|
96
|
-
- `.gowalk-cicd.yml` owns app settings and workflow extensions. The generated workflow digests
|
|
97
|
-
and template baselines live in `.gowalk-cicd.lock.json`. Plan all migrations before any writes;
|
|
98
|
-
never overwrite a conflicting app customization. See `docs/CONFIGURATION.md`.
|
|
99
|
-
Refresh `templates/history.json.gz` with `node scripts/build-workflow-history.mjs` when templates change.
|
|
100
|
-
- Never hardcode paths — use `fileURLToPath(import.meta.url)` and derive
|
|
101
|
-
from `__dirname`.
|
|
102
|
-
- Keep the CLI surface minimal: `--dry-run`, `-v`, `-h`. Resist adding flags.
|
|
103
|
-
- `.gitignore` in the consumer repo is git-tracked. Never auto-edit it.
|
|
104
|
-
Detect and warn, tell the user to remove lines manually.
|
|
105
|
-
- Never repeat an `env:` key in another case (`NO_PROXY` beside `no_proxy`): GitHub
|
|
106
|
-
compares env keys case-insensitively and rejects the whole workflow or action file.
|
|
107
|
-
`test/env-keys.test.mjs` scans the templates and actions. Where a step genuinely needs
|
|
108
|
-
both spellings of a variable, set them inside the step's own child process, never in YAML.
|
|
109
|
-
- Composite action steps cannot use workflow-only `timeout-minutes`. Native cache restores
|
|
110
|
-
retain their segment timeout and the caller's job deadline; workflow-level cache steps
|
|
111
|
-
can retain their two-minute step limits. `test/composite-timeouts.test.mjs` guards this schema boundary.
|
|
112
|
-
- Store API calls belong in GitHub Actions. Local tests may build/sign but must
|
|
113
|
-
never connect to App Store Connect or Google Play.
|
|
114
|
-
- **GitHub Actions reaches Apple and Google directly, and must never be given a store
|
|
115
|
-
proxy.** The account-pinned residential exit exists so that the panel and the runner
|
|
116
|
-
Macs are not the hosts contacting the stores; a GitHub-hosted runner is neither.
|
|
117
|
-
No workflow, action or helper here may read `APPLE_STORE_PROXY_URL`,
|
|
118
|
-
`GOOGLE_STORE_PROXY_URL`, or set `HTTP(S)_PROXY` for a build tool — the job-wide
|
|
119
|
-
transport this replaced billed every SDK, pub, Gradle and CocoaPods byte to a metered
|
|
120
|
-
residential exit. Apple IPA uploads use `action/scripts/upload_build.py`, not native upload tools.
|
|
121
|
-
It retains provider IDs/local SHA-256 and verifies matching IPA identity before resume.
|
|
122
|
-
Finalization sends `uploaded:true` as Apple's documented BuildUpload flow does; the
|
|
123
|
-
IPA endpoint rejected the optional SHA_256 checksum attribute despite the generic
|
|
124
|
-
schema advertising it. Completed-file reuse requires the content-derived filename
|
|
125
|
-
and size; any checksum returned by Apple is verified against the local file.
|
|
126
|
-
- Signing material should use encrypted Actions secrets. `deploy.yml`
|
|
127
|
-
materializes `ASC_KEY_P8` + its ID/issuer, the Apple distribution identity
|
|
128
|
-
(`IOS_DISTRIBUTION_P12_BASE64`, `IOS_DISTRIBUTION_CERT_META_BASE64`,
|
|
129
|
-
`IOS_DISTRIBUTION_CERT_REGISTRY_BASE64`), and the Android keystore, properties
|
|
130
|
-
and Play service account into an ephemeral runner checkout.
|
|
131
|
-
Tracked `creds/` files remain a backward-compatible read fallback. Refreshed signing
|
|
132
|
-
files stay ephemeral and are never staged; `persist-signing-cache` is ignored.
|
|
133
|
-
Signing preparation never issues a distribution certificate on a cache miss: the ephemeral
|
|
134
|
-
job cannot retain its new private key. Require a supplied usable identity, preserve failed
|
|
135
|
-
material for reconciliation and report `apple_signing_identity_required` without provider writes.
|
|
136
|
-
Version source staged before signing is preserved separately with its original index bytes.
|
|
137
|
-
- App Group identifiers come from each archive target's entitlements. `app_groups.py`
|
|
138
|
-
checks exact values in cached and fresh profiles; a fresh mismatch fails before profile
|
|
139
|
-
installation with `apple_app_groups_required`. Register/assign groups through the scoped
|
|
140
|
-
Apple account console before retrying CI. Never invent an ASC group operation, infer an
|
|
141
|
-
identifier from the bundle name, or accept a wildcard/unrelated group as a matching grant.
|
|
142
|
-
- Never revoke a distribution certificate automatically, including legacy caches.
|
|
143
|
-
`certificate-cap-policy` accepts only `fail`; a full cap requires registry reconciliation.
|
|
144
|
-
A certificate POST's HTTP 409 alone does not prove a full cap. `certificate_failure.py`
|
|
145
|
-
emits fixed reason/code evidence in `apple_certificate_rejected`; raw provider details stay private.
|
|
146
|
-
Certificate creation sends the complete PEM CSR in `csrContent`, including its framing and newlines.
|
|
147
|
-
- Keep preinstalled Android SDK/NDKs. Gradle tests/builds, SDK downloads, pub, analysis,
|
|
148
|
-
localization and Flutter bootstrap all run with the job's own environment and reach their
|
|
149
|
-
providers directly. Do not reintroduce a relay, a wrapper shell or a proxy variable for
|
|
150
|
-
them: those existed only to force traffic through an account exit CI no longer uses.
|
|
151
|
-
- `android-action/flutter-setup/` owns Flutter setup. It forwards the official action's
|
|
152
|
-
inputs/cache identities while isolating restore/save from provider transport. Its step-local
|
|
153
|
-
`NODE_OPTIONS` preloader (`android-action/scripts/cache_*.cjs`) also protects CocoaPods cache
|
|
154
|
-
steps and verifies the exact pinned Actions cache entrypoint bytes and HTTPS
|
|
155
|
-
GitHub cache/Azure blob destinations before the cache process runs. Unknown code or
|
|
156
|
-
origins refuse. The preloader stays scoped to that one process: never export it through
|
|
157
|
-
`GITHUB_ENV`. The preloader snapshot under `RUNNER_TEMP` also covers official post/save after
|
|
158
|
-
older consumer autoupdate changes the action directory. The installer delivers this development
|
|
159
|
-
tooling for native iOS repositories too; it does not require an Android app.
|
|
160
|
-
- Flutter manifest/archive GET recovery is scoped to the two pinned setup invocations through `BASH_ENV`.
|
|
161
|
-
It allows four attempts within 60 seconds for manifests or 20 minutes for archives, with an 8 GiB archive bound. Interrupted archives resume private bytes with HTTP Range; an origin ignoring
|
|
162
|
-
Range permits a bounded full restart. Only complete JSON or an archive matching the selected official
|
|
163
|
-
manifest's SHA-256 enters action outputs. A transient origin refusal (408, 429, 500, 502, 503,
|
|
164
|
-
504) shares that retry budget. Authentication/certificate refusals remain terminal.
|
|
165
|
-
`flutter_download_failed` emits fixed transport codes and retained-byte counts, never curl stderr.
|
|
166
|
-
Archive progress emits retained byte counts every minute and safe results after each attempt;
|
|
167
|
-
the transfer deadline leaves room for a failure receipt before a 30-minute validation job ends.
|
|
168
|
-
With SDK caching enabled, failed archive transfers preserve public bytes in a job-owned,
|
|
169
|
-
digest-scoped directory. Separate optional cache restore/save steps retain them between runs;
|
|
170
|
-
they never expose partial bytes as an installed SDK. Successful or corrupt completed archives
|
|
171
|
-
are removed from that partial cache. Full SDK cache identities stay unchanged.
|
|
172
|
-
- Web Release selects bundled renderer resources with `--no-web-resources-cdn` inside
|
|
173
|
-
the existing proxied build command. This is not a font or integration offline guarantee;
|
|
174
|
-
preserve app-authored font declarations, bootstrap configuration and other build inputs.
|
|
175
|
-
- Flutter setup scopes `SEGMENT_DOWNLOAD_TIMEOUT_MINS=1` to the composite action's optional
|
|
176
|
-
SDK/pub cache restores. This bounds each cache segment, not the whole cache or SDK download.
|
|
177
|
-
A cache timeout becomes a cache miss; normal proxied SDK setup remains mandatory and its
|
|
178
|
-
errors still fail the job. Never use `continue-on-error` or a timeout around all SDK setup
|
|
179
|
-
to hide an optional cache failure. Do not alter cache keys or disable caches as part of
|
|
180
|
-
this timeout handling.
|
|
181
|
-
- Flutter SDK caching restores first and saves immediately after successful setup, before
|
|
182
|
-
consumer tests/builds can fail and suppress a combined cache action's success-only post hook.
|
|
183
|
-
Save only on a cache miss with caching enabled, a real successful install and verified cache
|
|
184
|
-
scope. Preserve resolved keys/paths and the official pinned restore/save programs. Failed or
|
|
185
|
-
dry-run setup never publishes an SDK cache; pub dependency caching retains its job-end behavior.
|
|
186
|
-
- Exact `flutter-version` inputs may reuse previously resolved public release metadata before SDK
|
|
187
|
-
cache lookup. Metadata is scoped to platform, origin, channel, architecture and full version;
|
|
188
|
-
URL/pin/schema/release fields are checked before reuse. Blank, wildcard and version-file inputs
|
|
189
|
-
resolve online. Missing or invalid metadata falls back to the required proxied GET; missing
|
|
190
|
-
account transport still refuses even with a warm cache. SDK keys and checksum checks stay unchanged.
|
|
191
|
-
- Gradle caches use checksum-pinned restore/save actions, each optional and bounded to two minutes.
|
|
192
|
-
Native Pods/Bundler caches use the committed locks and selected Ruby/Xcode/architecture in the existing job.
|
|
193
|
-
Preserve setup-java's complete keys, inputs and absolute paths; restore the same OS/architecture prefix.
|
|
194
|
-
Failed releases save distinct partial keys unless cancelled; completed keys require success. Builds remain required.
|
|
195
|
-
Only pinned cache entrypoints get cache transport; see [cache behavior](docs/GRADLE_CACHE.md) before adoption.
|
|
196
|
-
- GitHub artifact uploads have their own checksum-pinned `artifact_env.cjs` process scope.
|
|
197
|
-
`mobile-candidate.yml` builds only trusted ready task PRs, using the existing release workflow in
|
|
198
|
-
candidate mode. Candidate mode cannot publish or mutate a marketing-version slot. Main promotes
|
|
199
|
-
only verified matching binaries with current credentials and retains the original build/symbol identity;
|
|
200
|
-
a miss uses the normal build without waiting. Preserve successful sibling artifacts across reruns.
|
|
201
|
-
See `docs/CANDIDATE_ARTIFACTS.md`; never turn this optimization into a new factory stage or resource reservation.
|
|
202
|
-
It reuses the cache transport's approved HTTPS request guard inside the official
|
|
203
|
-
`upload-artifact` process. Unknown programs/origins refuse; artifact failures remain job failures.
|
|
204
|
-
The iOS action reads this helper from its immutable `SWIFT_APP_ACTION` snapshot. Its shared
|
|
205
|
-
`cache_scope.cjs` and `artifact_env.cjs` copies match the Android distribution byte for byte,
|
|
206
|
-
enforced by `test/artifact-wiring.test.mjs`; update both when changing that common guard.
|
|
207
|
-
Update existing consumer workflow artifact steps as well as action directories on adoption.
|
|
208
|
-
- Backend layout selection belongs in `backend-action/layout.cjs`, reused by the installer and both
|
|
209
|
-
workflow entrypoints. Support the four standard Compose names without source renames. Multiple
|
|
210
|
-
matches require explicit existing `backend-dir`/`compose-file` configuration before writes or SSH.
|
|
211
|
-
Keep the chosen basename through transfer and remote `COMPOSE_FILE`; stale host files must not win
|
|
212
|
-
Docker's implicit filename precedence. Root source directories remain unsupported. Cover isolated
|
|
213
|
-
consumer installs, action validation/transfer and remote command selection in focused tests.
|
|
214
|
-
- Backend runtime credentials use the single encrypted `BACKEND_RUNTIME_ENV`
|
|
215
|
-
secret. The action writes it to host-side `.runtime.env` with mode 0600 and
|
|
216
|
-
supplies it to Compose after the generated `.env`; never print its content.
|
|
217
|
-
Backend failures emit `backend_deploy_failed` with the fixed runner/host phase and exit code.
|
|
218
|
-
Its message uses the exact schema/phase/exit envelope documented in README, without JSON braces
|
|
219
|
-
that multiline secret masking can erase. Never reconstruct masked fields or include arbitrary text.
|
|
220
|
-
Native bundle/Pods failures emit `native_prepare_failed` with fixed refusal codes and observed
|
|
221
|
-
signals. Unknown signals remain unclassified; never copy child output or commands into annotations.
|
|
222
|
-
Timeout and cancellation retain the existing bounded sanitized diagnostic after owned-child cleanup;
|
|
223
|
-
preserve the failure code, install deadline and retry fences.
|
|
224
|
-
Within the bounded capture, retain the CocoaPods primary exception alongside the tail when a long
|
|
225
|
-
stack or secondary issue-search failure hides it. Redact before selecting the bounded excerpt.
|
|
226
|
-
- The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
|
|
227
|
-
new app before GitHub registers `deploy-backend.yml` on the default branch.
|
|
228
|
-
Default-branch deploys continue through the separate backend workflow.
|
|
229
|
-
- Mobile pushes exclude durable `.factory/progress` records and the separately deployed
|
|
230
|
-
backend/server/Compose paths, backend action, docs, marketing, account-recovery scripts,
|
|
231
|
-
tests and the standard validation/web workflows. Backend action changes trigger only backend
|
|
232
|
-
deployment; its package version marker alone does not. A mixed commit with any native/shared input still builds;
|
|
233
|
-
explicit workflow dispatch remains available. Keep backend deployment's own path filters
|
|
234
|
-
intact, and do not broadly exclude assets, root build settings or unknown app layouts.
|
|
235
|
-
- Domain-backed backend deploys fail closed on certificate issuance and the
|
|
236
|
-
public HTTPS health probe. The probe requires a completed 2xx response with normal TLS
|
|
237
|
-
verification; a redirect, including one to the same URL, is not healthy. Each attempt
|
|
238
|
-
has bounded connection/total time and retains only curl exit and HTTP status diagnostics.
|
|
239
|
-
`select_certbot_account.sh` chooses one existing
|
|
240
|
-
Let's Encrypt ACME v2 account deterministically so multi-account hosts stay
|
|
241
|
-
non-interactive; legacy v1 account directories are not valid candidates.
|
|
242
|
-
`ensure_certificate.sh` preserves failed inventory status and retries only Certbot's
|
|
243
|
-
explicit lock-busy response (six attempts, 75 seconds total backoff). Every retry reads
|
|
244
|
-
inventory before issuance; absence needs recognized complete SAN records or Certbot's explicit
|
|
245
|
-
no-certificate sentinel. Empty/malformed output and arbitrary errors never mean absence. Raw command
|
|
246
|
-
output stays in an owned private temporary directory removed on exit. Do not remove
|
|
247
|
-
Certbot locks, stop other processes, force renewal or bypass the public HTTPS health gate.
|
|
248
|
-
Docker port changes update only the managed nginx upstream, preserving existing TLS directives.
|
|
249
|
-
Existing certificate inventory is followed by domain-scoped `certbot install`, so a legacy
|
|
250
|
-
HTTP-only vhost also recovers without reissuance. Installation shares the bounded lock-only retry.
|
|
251
|
-
- Optional backend edge delivery keeps `api-domain` as infrastructure and verifies the separate
|
|
252
|
-
`public-health-url` against exact health JSON `build_sha`. `public-base-url` supplies
|
|
253
|
-
`BACKEND_PUBLIC_BASE_URL` to Compose interpolation; app services consume it for generated URLs.
|
|
254
|
-
`ingress-required` requires a private `ingress-token` before deployment. Protect only the distinct
|
|
255
|
-
`backend-<app>-edge.conf`; preserve a legacy vhost's TLS/public access and update both managed
|
|
256
|
-
ports. A missing token/settings cannot downgrade an already protected app. Keep tokens in private
|
|
257
|
-
stdin/file transport, strip the ingress header before app forwarding, and validate real nginx in
|
|
258
|
-
focused tests. Managed locations forward WebSocket upgrades with a domain-scoped Connection map
|
|
259
|
-
and disable response buffering; verify a real frame exchange and a first SSE event before upstream
|
|
260
|
-
completion. Preserve legacy deployments that have not opted in and existing failure phases.
|
|
261
|
-
Remote helpers support Python 3.8 and newer; avoid newer syntax and standard-library APIs.
|
|
262
|
-
Run the edge suite with `python3.8 -m unittest discover -s backend-action -p 'test_edge*.py'`;
|
|
263
|
-
CI repeats it with Python 3.8, including the real nginx checks, before publication.
|
|
264
|
-
- The Android action serves two build systems. `android_config.project_kind()`
|
|
265
|
-
is the only place that decides which; every downstream step branches on the
|
|
266
|
-
`project_kind` output rather than re-sniffing the repo. Flutter wins the tie
|
|
267
|
-
because a Flutter app also carries `android/settings.gradle`.
|
|
268
|
-
- Both Android release compilers run through `android_build_diagnostics.py`.
|
|
269
|
-
It retains at most 64 KiB of private child output and publishes
|
|
270
|
-
only fixed signal names, phase and exit status in `android_build_failed`. An empty signal
|
|
271
|
-
list is unclassified, never a guessed cause; matching signals are observations, not a retry
|
|
272
|
-
decision. Minute progress notices replace raw compiler output. Preserve the actual exit,
|
|
273
|
-
command arguments, signing and symbol checks.
|
|
274
|
-
- Offline delivery preflight recognizes compiled single-size icons through `delivery_icons.py`.
|
|
275
|
-
Require matching 1024 `Icon Image` and `MultiSized Image` index records per declared device idiom;
|
|
276
|
-
do not treat an arbitrary 1024 image as coverage or weaken missing-family checks.
|
|
277
|
-
- iOS plist stamping reads complete JSON build settings through `resolve_info_plist.py`,
|
|
278
|
-
preserving Xcode's exit status and bounded sanitized stderr. Select exactly the configured
|
|
279
|
-
bundle's application target and resolve paths from that target's source root; never take
|
|
280
|
-
the first dependency plist or stop reading Xcode output early. The encryption step reuses
|
|
281
|
-
the validated source path. Missing/ambiguous source plists fail; positively generated
|
|
282
|
-
plists retain the existing build-setting version path. Queries disable signing and package
|
|
283
|
-
updates, retain inherited proxies, and never enable provisioning network operations.
|
|
284
|
-
- Native Xcode validation and archives first check the selected project/workspace graph with
|
|
285
|
-
`native_dependency_guard.py ROOT --container PATH`. Xcode-managed Swift packages remain refused:
|
|
286
|
-
they resolve outside the committed lockfile the guard exists to enforce. The same app session preserves
|
|
287
|
-
source and uses the vendor's official CocoaPods distribution or verified local XCFrameworks.
|
|
288
|
-
`native_pods.py` installs a committed, unchanged `Podfile.lock` with `pod install --deployment`,
|
|
289
|
-
then checks the completed workspace. No Podfile means no dependency download at all.
|
|
290
|
-
Initial local lock resolution may use the
|
|
291
|
-
explicit `--before-pods --initial-pods-lock` guard; CI never uses that development exception.
|
|
292
|
-
These checks cover the declared native dependency graph, not arbitrary custom build-script traffic.
|
|
293
|
-
`native_bundle.py` honors the selected project's committed Gemfile/lock, falling back to
|
|
294
|
-
the repository root; missing gems install with a frozen bundle.
|
|
295
|
-
Both Flutter configuration and the Pods helper use that bundle in their child process,
|
|
296
|
-
preserving Podfile.lock and the parent job environment. Deliver the workflow wrapper and
|
|
297
|
-
action helper together; action-only updates cannot repair earlier custom configuration steps.
|
|
298
|
-
Flutter `--config-only` can query Xcode and install Pods before returning. In the workflow,
|
|
299
|
-
generate Flutter inputs with `pub get`, run the locked `native_pods.py` preparation and
|
|
300
|
-
completed graph guard, then configure the release with its existing obfuscation/define flags.
|
|
301
|
-
The graph guard still applies when there is no Podfile. Preserve this order in custom CI.
|
|
302
|
-
- Locked Pod installation retries once only when its own attempt reports curl 18 and a partial-transfer
|
|
303
|
-
symptom. Both attempts share the existing 900-second budget and recheck Podfile, Podfile.lock and
|
|
304
|
-
selected Ruby bundle inputs. TLS/auth/permission, checksum, version and deployment errors never retry.
|
|
305
|
-
The assigned relay, deployment flags and completed-workspace guard remain mandatory; caches are retained.
|
|
306
|
-
`native_pods_retry` publishes only the fixed attempt/reason, and success reports `pod_attempts`.
|
|
307
|
-
- Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
|
|
308
|
-
has no equivalent, so `set_gradle_version()` rewrites the literal in the
|
|
309
|
-
checkout the way the iOS action patches the pbxproj. Never commit that edit.
|
|
310
|
-
The rewrite is best-effort — a module that computes its versionCode has no
|
|
311
|
-
literal to rewrite — so the Gradle build additionally applies the resolved
|
|
312
|
-
values through the Variant API (`scripts/version_override.init.gradle`,
|
|
313
|
-
passed as `--init-script`). That override, not the rewrite, is what makes the
|
|
314
|
-
number authoritative. Do NOT reach for `-Pandroid.injected.version.code`:
|
|
315
|
-
those properties were removed in AGP 7.3 and are silently ignored by every
|
|
316
|
-
AGP 8.x app in the fleet.
|
|
317
|
-
- Dart obfuscation is mandatory for Flutter apps and has no opt-out — do not
|
|
318
|
-
add an input, repo variable or config key for it. Both Flutter release
|
|
319
|
-
builds carry `--obfuscate --split-debug-info`: `flutter build appbundle` in
|
|
320
|
-
`android-action/action.yml`, and the `flutter build ios --config-only` call
|
|
321
|
-
in `templates/deploy.yml`, which is where the iOS build is obfuscated
|
|
322
|
-
(`--config-only` writes `DART_OBFUSCATION` / `SPLIT_DEBUG_INFO` into
|
|
323
|
-
`ios/Flutter/Generated.xcconfig`; the Flutter build phase inside the
|
|
324
|
-
`xcodebuild archive` reads them). The flags are one feature: `--obfuscate`
|
|
325
|
-
refuses to run without `--split-debug-info`, and the symbol files it writes
|
|
326
|
-
are the only way to read a stack trace from that build, so they are always
|
|
327
|
-
retained as an artifact (`android-symbols-*`, `ios-symbols-*`) and a build
|
|
328
|
-
that wrote none fails. Symbols are written under `RUNNER_TEMP`, never into
|
|
329
|
-
the checkout, so nothing that commits the working tree can pick them up.
|
|
330
|
-
Native Gradle, Swift and Flutter web builds are untouched (web has no such
|
|
331
|
-
flag). Wiring is pinned by `android-action/scripts/test_flutter_obfuscation.py`
|
|
332
|
-
and `test/install.test.mjs`.
|
|
333
|
-
- Whatever an obfuscation flag needs to be safe must travel in the same file
|
|
334
|
-
as the flag. The consumer-side autoupdate re-vendors the action directories
|
|
335
|
-
but can never push `deploy.yml`, so "new action + old workflow" is the
|
|
336
|
-
fleet's normal state. That is why the Android action uploads
|
|
337
|
-
`android-symbols-*` itself instead of leaving it to the workflow, and why
|
|
338
|
-
the iOS flags and the `ios-symbols-*` step sit together in `deploy.yml`.
|
|
339
|
-
The iOS retain step runs on `!cancelled()` because the swift-app action keeps
|
|
340
|
-
going after the TestFlight upload (metadata, "What's New"): a failure there
|
|
341
|
-
must not discard the symbols of a build that is already live. Symbol
|
|
342
|
-
artifacts carry no `retention-days` so the repository's setting governs and
|
|
343
|
-
can be raised to cover a release's life.
|
|
344
|
-
- Firebase Crashlytics traffic leaves the runner directly, like every other provider
|
|
345
|
-
request in CI. Native iOS upload-symbols uses NSURLSession, so it is never executed. Before
|
|
346
|
-
archiving, dedicated Crashlytics upload phases in the ephemeral Xcode projects
|
|
347
|
-
are deferred; mixed/unidentified upload phases refuse the archive for repair.
|
|
348
|
-
CI retains the archive's dSYMs as a ZIP with UUIDs, source SHA and SHA-256, then
|
|
349
|
-
emits `firebase_symbols_pending` (`gowalk-cicd/firebase-symbols-pending.v1`).
|
|
350
|
-
The app session downloads that exact artifact and uploads it through its
|
|
351
|
-
account-pinned Firebase console, reading processing back before completing the
|
|
352
|
-
delivery request. This requires no human handoff. A green CI run alone is not
|
|
353
|
-
evidence that the deferred symbols reached Firebase. Android retains its Dart
|
|
354
|
-
symbols and uploads them before the store bundle.
|
|
355
|
-
- Crashlytics buildtools bootstrap runs before Flutter compilation. `crashlytics_bootstrap.py`
|
|
356
|
-
pins the official JAR version, byte size and SHA-256, admits only verified dependency-cache
|
|
357
|
-
bytes, and bounds GET recovery to three attempts within three minutes. Java receives
|
|
358
|
-
that verified file through `CRASHLYTICS_LOCAL_JAR`; changing the Firebase CLI pin must check
|
|
359
|
-
its JAR version/override contract. TLS, authorization and checksum errors never retry.
|
|
360
|
-
`firebase_symbols_bootstrap_failed` carries fixed typed evidence, never private curl output.
|
|
361
|
-
- Play readiness and AAB/APK version inventory share one prebuild edit. Retry OAuth
|
|
362
|
-
transport, inventory reads and cleanup of that known edit; never replay an ambiguous
|
|
363
|
-
`edits.insert`. Edit creation retries at most three times only for typed connection-timeout
|
|
364
|
-
or typed connection failure evidence that no provider request was sent. TLS verification
|
|
365
|
-
failures, provider responses and lost origin responses do not authorize a retry.
|
|
366
|
-
Only an explicit package-404 allows first-release defaults; failed
|
|
367
|
-
inventory is not an empty account. The action exposes `play-api-ready` so current
|
|
368
|
-
workflows skip the redundant postbuild check. Error annotations carry safe
|
|
369
|
-
phase/category evidence and any cleanup edit ID without raw provider exceptions.
|
|
370
|
-
- The iOS action reads every script and prompt from the snapshot it takes of
|
|
371
|
-
itself in its first step (`$SWIFT_APP_ACTION`, under `RUNNER_TEMP`), never
|
|
372
|
-
from `${{ github.action_path }}` after that step. The plugin self-update
|
|
373
|
-
(`autoupdate_check.sh`) runs right after the snapshot and preserves its own source candidate,
|
|
374
|
-
before the version/signing/archive steps, so a red deploy still receives the
|
|
375
|
-
release that fixes it. `autoupdate_stage.py` installs and preserves that update in one
|
|
376
|
-
task-owned detached worktree, then removes it. The active build's HEAD, index, dirty files
|
|
377
|
-
and action manifests stay unchanged: GitHub reparses local composites for post hooks using
|
|
378
|
-
cached step IDs, so a script snapshot alone cannot protect a changed manifest's step count.
|
|
379
|
-
`test_action_step_order.py` pins both. A new script reference in
|
|
380
|
-
`action/action.yml` therefore uses `"$SWIFT_APP_ACTION/scripts/..."`.
|
|
381
|
-
- Delivery dependencies may remain after a green job; each is a typed
|
|
382
|
-
check-run annotation (`::notice title=<name>::<one-line JSON with a versioned
|
|
383
|
-
schema>`), never prose in a `::warning::`: `play_review_pending`
|
|
384
|
-
(`templates/deploy.yml`, Play refused auto-submit and the bundle landed
|
|
385
|
-
unsubmitted) and `store_version_locked` (`action/action.yml`, the
|
|
386
|
-
`TESTFLIGHT_ONLY` decision of `manage_marketing_version.py`: an App Store
|
|
387
|
-
version is under review, so the build shipped to TestFlight under that
|
|
388
|
-
version and metadata was skipped), and `firebase_symbols_pending` (the dSYM
|
|
389
|
-
archive awaits the session's proxied console upload). The README lists the schemas; add a
|
|
390
|
-
field by bumping the version.
|
|
391
|
-
- `android-action/play-upload/` verifies unchanged SHA/checksum-pinned official uploader
|
|
392
|
-
bytes before running Node24. It classifies immediate private child output, never live
|
|
393
|
-
GitHub annotations: expired/deleted retries normally; only the exact cannot-auto-submit
|
|
394
|
-
refusal permits a review hold. Unknown/transport/post-commit uncertainty does not replay.
|
|
395
|
-
The existing signed AAB, track/status/rollout and notes survive one bounded retry;
|
|
396
|
-
only a successful hold emits `play_review_pending`. Keep raw child output out of receipts.
|
|
397
|
-
Each attempt also emits brace-free `play_upload_diagnostic` with fixed observed progress,
|
|
398
|
-
process outcome and error signals from bounded private stdout/stderr line buffers.
|
|
399
|
-
Preserve the v1 upload result and retry rules; diagnostics never authorize replay or prove
|
|
400
|
-
an operation completed. See README for the independent diagnostic schema and field vocabulary.
|
|
401
|
-
- The JDK is chosen by `scripts/select_jdk.py`, called from the workflow before
|
|
402
|
-
`setup-java`. The `JAVA_VERSION` repo variable always wins; otherwise Flutter
|
|
403
|
-
gets 17 and native Gradle gets 21, except that a positively-detected Kotlin
|
|
404
|
-
Gradle plugin below 1.9.20 drops to 17 (older kapt cannot run under JDK 21).
|
|
405
|
-
Detection logic belongs in that script, where it is unit-tested — not inline
|
|
406
|
-
in the workflow template, where nothing can test it.
|
|
407
|
-
|
|
408
|
-
- CI-generated source is preserved by `source_maintenance.py` on a deterministic
|
|
409
|
-
`task/ci-maintenance-*` branch, with remote parent/tree/SHA readback. It never changes
|
|
410
|
-
the build HEAD, rebases a dirty checkout, force-pushes or pushes to the default branch.
|
|
411
|
-
Only managed action/version source is accepted; credential-shaped staging refuses.
|
|
412
|
-
`source_maintenance_pending` identifies the exact source for the same app session's
|
|
413
|
-
checked PR. Unconfirmed preservation is a job failure; a receipt is not a completed merge.
|
|
414
|
-
- Play preflight, version inventory and the pinned uploader share the panel's package edit owner.
|
|
415
|
-
The binding's nonsecret `principal_sha256` must match the selected service account's
|
|
416
|
-
`client_email` fingerprint before provider work. A mismatch requires supported CI-secret
|
|
417
|
-
provisioning; never accept the account ID or a caller-supplied fingerprint alone.
|
|
418
|
-
`APP_ROBOT_PLAY_EDIT_BINDING` is panel-provisioned repository data; workflows pass it as
|
|
419
|
-
`PLAY_EDIT_BINDING` with the repo-local GitHub token and `contents:write`/`actions:read`.
|
|
420
|
-
`play-upload/owner_run.cjs` protects Python entrypoints; `run.cjs` holds the same owner across
|
|
421
|
-
its permitted upload retry. An active owner gets a single 60-second acquisition wait budget;
|
|
422
|
-
each retry revalidates the record, cancellation interrupts the wait, and an expired budget
|
|
423
|
-
never clears an owner. Missing/retired bindings and unknown write outcomes still refuse;
|
|
424
|
-
only exact terminal run/attempt/source proof permits CI-owner recovery.
|
|
425
|
-
Both entrypoints emit brace-free `play_edit_owner` errors with fixed codes and optional child
|
|
426
|
-
exit evidence, preserving the existing upload receipt and refusal. Never reconstruct masked text.
|
|
427
|
-
Other packages stay parallel. The GitHub coordination request is fixed-origin.
|
|
428
|
-
Update workflow wiring as well as action directories.
|
|
429
|
-
|
|
430
|
-
- Mobile Deploy watches `.factory/releases/mobile.json`, produced from committed task source and
|
|
431
|
-
the named build-variable allow-list shared by freeze and runtime in `mobile_inputs.cjs`.
|
|
432
|
-
The list follows `templates/deploy.yml`; rotating Play ownership leases stay excluded.
|
|
433
|
-
Unknown inputs and embedded editors remain in its source proof; native trees
|
|
434
|
-
are platform-scoped. Backend/web deployments remain independent. Confirmed store uploads are
|
|
435
|
-
recorded immediately in repository deployment receipts; retries reuse their prior-run evidence.
|
|
436
|
-
An uncertain upload never starts another binary automatically. Preserve its signed artifact,
|
|
437
|
-
complete exact provider readback/recovery through scoped tooling, then reconcile its receipt.
|
|
438
|
-
`action/scripts/recover_upload_intent.cjs owner/repo deployment-id plan|live` repairs only
|
|
439
|
-
an iOS intent whose exact terminal run has one failed HTTP 500 intent step and an explicitly
|
|
440
|
-
skipped uploader in the complete GitHub job log. It refuses later run attempts and changed
|
|
441
|
-
evidence, appends a read-back building receipt, and never uploads or calls Apple. Retry the
|
|
442
|
-
original failed job afterward; preserve the recovery proof separately.
|
|
443
|
-
The local Android workflow supplies `mobileReleaseIntentScript` to the upload wrapper instead
|
|
444
|
-
of marking `uploading` before acquiring Play ownership. A refused owner leaves the release
|
|
445
|
-
record at `building`; the wrapper records intent inside its acquired lease before provider
|
|
446
|
-
dispatch. Adopt both the action and workflow wiring; action-only updates preserve older
|
|
447
|
-
workflows' intent handling. Never clear an existing `uploading` marker on an owner refusal.
|
|
448
|
-
Actions without `MOBILE_RELEASE_DEPLOYMENT_ID` preserve legacy consumer behavior. All new
|
|
449
|
-
receipt helper reads are fixed-origin GitHub requests; Apple/Google still use their assigned exits.
|
|
450
|
-
|
|
451
|
-
## Scoped backend diagnosis
|
|
452
|
-
|
|
453
|
-
Backend network preparation and `operation: network-diagnostics` follow `docs/BACKEND_NETWORKS.md`.
|
|
454
|
-
Keep allocation inside verified existing daemon pool bases, preserve all existing and authored networks,
|
|
455
|
-
and hold the host allocation lock only through creation/readback. Never restart/reconfigure Docker,
|
|
456
|
-
prune networks, or invent a free private range. Remote modules and source facts remain Python 3.8 compatible.
|
|
457
|
-
Tests include the explicit CI-only six-project Docker coexistence proof; ordinary local tests contact no daemon.
|
|
458
|
-
|
|
459
|
-
The existing backend action accepts `operation: identity-diagnostics` with a UTC `diagnostic-since`
|
|
460
|
-
from the last24hours and exact `diagnostic-service` (default`api`). It performs no deploy, rsync,
|
|
461
|
-
Compose mutation or environment read. `identity_diagnostics.py` uses the existing CI SSH binding,
|
|
462
|
-
selects exactly one running container by Compose project/service, rechecks both labels and reads
|
|
463
|
-
at most1000recent log lines within a1MiB/30second bound. Raw output stays on the host; only the
|
|
464
|
-
fixed `firebase_identity_refused` stage/reason allowlist leaves it. No match means no observed
|
|
465
|
-
label, not successful authentication. Dispatch this action through the app's existing backend
|
|
466
|
-
workflow after a protected package release/adoption; never run hand-SSH or expose raw logs.
|
|
467
|
-
|
|
468
|
-
The diagnostic reader accepts the product-independent standard WARNING logger envelope, discards its
|
|
469
|
-
logger name and rejects trailing fields. Structured remote failure codes survive exit1; SSH transport,
|
|
470
|
-
read bounds and scope refusals have a closed vocabulary. Unknown exceptions remain unclassified.
|
|
471
|
-
|
|
472
|
-
The same validated envelope is retained in the dedicated `backend-identity-diagnostics-<job>-<attempt>`
|
|
473
|
-
artifact on success and classified reader failure. Download `result.json` from that exact run;
|
|
474
|
-
GitHub secret masking can alter structural braces in logs. The artifact contains only schema,
|
|
475
|
-
ok, fixed failure and fixed stage/reason records. Raw host output, configuration and identities
|
|
476
|
-
never enter it. Its pinned upload uses the same process-scoped artifact transport as mobile actions.
|