gowalk-cicd 1.0.158 → 1.0.160

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 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 frozen release intent. After source, signing, account transport,
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 intent binds committed source and effective organization/repository variables, with repository
52
- precedence. A changed source input or compiled variable refuses before builds; prepare a new intent after the repair. Native iOS
53
- and Android directories are scoped to their own platform. Unknown inputs, embedded editors,
54
- assets, scripts, dependency locks and backend-generated resources remain in the conservative
55
- source fingerprint. Documents remain inputs because an app may bundle them. There is no blanket
56
- editor/backend exclusion from the input proof. These conservative hashes do not add push triggers.
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. Rerun the whole workflow so admission and platform jobs share
71
- one run attempt; a failed-jobs-only rerun with an older admission receipt refuses.
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 frozen release intent. Adopting an action-directory
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
 
@@ -1 +1 @@
1
- 1.0.158
1
+ 1.0.160
@@ -1,10 +1,10 @@
1
- /* Frozen mobile build inputs. Unknown source paths remain inputs, including embedded editors. */
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.v1';
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, and share the projection with the freeze CLI.
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'], variables = {}) {
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
- // An intent is a freeze. Declaring a platform whose build inputs are missing
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, configuration_sha256: configuration(variables),
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.configuration_sha256 !== configuration(variables)) throw Error('mobile_release_configuration_changed');
102
- if (manifest.schema !== SCHEMA || !manifest.platforms || !Object.keys(manifest.platforms).length ||
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
- for (const [p, sha] of Object.entries(manifest.platforms)) {
106
- if (fingerprint(p) !== sha) throw Error('mobile_release_intent_stale');
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(`::error title=mobile_release_refused::${code}`);
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.158
1
+ 1.0.160
@@ -1 +1 @@
1
- 1.0.158
1
+ 1.0.160
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 frozen mobile release intent on the task branch
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.158",
3
+ "version": "1.0.160",
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
- "CLAUDE.md",
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
+ ]
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 frozen intent through a PR.');
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
- // Freeze committed source and the repository variables the workflow will actually read.
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
- const execute = (command, args) => execFileSync(command, args, {
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
  }
@@ -23,7 +23,7 @@ concurrency:
23
23
 
24
24
  jobs:
25
25
  release:
26
- name: Admit frozen mobile release
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.