gowalk-cicd 1.0.185 → 1.0.187

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
@@ -5,6 +5,12 @@ optional backend deploy. It supports native Swift/SwiftUI iOS projects, native K
5
5
  Android projects, and Flutter apps. Provision encrypted repository secrets, validate a
6
6
  task candidate, and promote its release intent. Store builds and uploads run only in GitHub Actions.
7
7
 
8
+ Tests must run locally, never in GitHub Actions. Run only the focused tests needed for the
9
+ changed behavior before pushing. The iOS and Android actions never run tests or Flutter analysis;
10
+ legacy `run-tests`, `test-command`, and `test-destination` inputs are accepted but ignored,
11
+ including `run-tests: 'true'`. Build, signing, upload, and delivery health checks remain in Actions.
12
+ See [local validation](docs/LOCAL_VALIDATION.md) for focused execution, receipts and CI verification.
13
+
8
14
  App-specific workflow settings live in `.gowalk-cicd.yml`. The installer migrates supported
9
15
  existing settings once and regenerates managed workflows without replacing custom behavior.
10
16
  See [configuration](docs/CONFIGURATION.md).
@@ -108,7 +114,7 @@ download segment. A slow segment becomes a cache miss and normal SDK setup
108
114
  continues normally; SDK setup errors still fail the
109
115
  job. This bounds individual cache segments, not the total cache or SDK download,
110
116
  and keeps existing cache keys. A successfully installed SDK is saved before app
111
- checks run, so a later test/build failure does not force another cold SDK download.
117
+ builds run, so a later build failure does not force another cold SDK download.
112
118
  Failed setup never saves an SDK cache; pub dependencies retain their job-end save.
113
119
  Pull-request runs (`refs/pull/*`, which includes every candidate build) restore these caches
114
120
  but never save them; task branches and the default branch still save. When the caller names
@@ -789,7 +795,6 @@ configuration for either:
789
795
  |---|---|---|
790
796
  | Detected by | `pubspec.yaml` at the repo root | `settings.gradle(.kts)` + `gradlew` at the root or under `android/` |
791
797
  | App module | `android/app` | the one module applying `com.android.application` (version-catalog aliases are resolved; a module named `app` wins a tie against a wear/automotive sibling) |
792
- | Tests (`run-tests`) | `flutter analyze` + `flutter test` | `gradlew test` (all variants, all modules) |
793
798
  | Build | `flutter build appbundle --release --obfuscate --split-debug-info` | `<module>:bundleRelease` |
794
799
  | Toolchain installed | Flutter + JDK 17 | JDK 21, or 17 on an old Kotlin — see [Choosing the JDK](#choosing-the-jdk) |
795
800
 
@@ -1051,7 +1056,6 @@ schemes, pass inputs in `.github/workflows/deploy.yml`:
1051
1056
  with:
1052
1057
  scheme: MyAppRelease
1053
1058
  bundle-id: com.example.myapp
1054
- run-tests: 'false'
1055
1059
  uses-non-exempt-encryption: 'false'
1056
1060
  ```
1057
1061
 
@@ -1067,9 +1071,9 @@ common ones:
1067
1071
  | `team-id` | Override the auto-detected team ID |
1068
1072
  | `app-store-apple-id` | Numeric ASC app ID (override auto-lookup) |
1069
1073
  | `certificate-cap-policy` | Deprecated and ignored: CI never creates or revokes certificates. |
1070
- | `run-tests` | `false` to skip the simulator test stage |
1074
+ | `run-tests`, `test-command`, `test-destination` | Deprecated and ignored: tests run locally only. |
1071
1075
  | `uses-non-exempt-encryption` | Value for `ITSAppUsesNonExemptEncryption` |
1072
- | `archive` | `false` to build-only (PR runs without secrets) |
1076
+ | `archive` | `false` to skip archive, export, and upload |
1073
1077
  | `upload` | `false` to archive but not upload to TestFlight |
1074
1078
  | `manage-app-store-version` | `false` to upload without creating/editing the App Store release slot |
1075
1079
  | `app-store-whats-new` | Inline "What's New" text (overrides files) |
package/action/action.yml CHANGED
@@ -69,9 +69,8 @@ inputs:
69
69
  default: "false"
70
70
  archive:
71
71
  description: >
72
- Set to 'false' to skip archive + export + upload entirely. Useful for
73
- PR / branch runs that only need to build and test without code signing
74
- secrets. When 'false', `upload` is ignored.
72
+ Set to 'false' to skip archive + export + upload entirely.
73
+ When 'false', `upload` is ignored.
75
74
  required: false
76
75
  default: "true"
77
76
  manage-app-store-version:
@@ -82,21 +81,15 @@ inputs:
82
81
  required: false
83
82
  default: "true"
84
83
  run-tests:
85
- description: "Set to 'false' to skip the simulator test stage"
84
+ description: DEPRECATED (ignored). Tests run locally, never in GitHub Actions.
86
85
  required: false
87
- default: ""
86
+ default: "false"
88
87
  test-command:
89
- description: >
90
- Shell command used for the simulator test stage. Defaults to running
91
- `bash Scripts/run-tests.sh` when that file exists, otherwise
92
- `xcodebuild test` against the chosen scheme on a booted simulator with
93
- code-signing disabled. Override to run a custom runner.
88
+ description: DEPRECATED (ignored). Custom test commands are never executed.
94
89
  required: false
95
90
  default: ""
96
91
  test-destination:
97
- description: >
98
- Destination passed to the default `xcodebuild test` invocation when
99
- `test-command` is empty and no `Scripts/run-tests.sh` is present.
92
+ description: DEPRECATED (ignored). Simulator tests run locally only.
100
93
  required: false
101
94
  default: ""
102
95
  uses-non-exempt-encryption:
@@ -276,9 +269,6 @@ runs:
276
269
  INPUT_USES_NON_EXEMPT: ${{ inputs.uses-non-exempt-encryption }}
277
270
  INPUT_WHATS_NEW: ${{ inputs.app-store-whats-new }}
278
271
  INPUT_LOCALE: ${{ inputs.app-store-locale }}
279
- INPUT_RUN_TESTS: ${{ inputs.run-tests }}
280
- INPUT_TEST_COMMAND: ${{ inputs.test-command }}
281
- INPUT_TEST_DESTINATION: ${{ inputs.test-destination }}
282
272
  run: |
283
273
  # read_config.py runs FIRST (it derives every other value), so it must
284
274
  # install its full dep set inline. All later steps reuse these.
@@ -320,48 +310,6 @@ runs:
320
310
  key: ${{ steps.native_cache_scope.outputs.key }}
321
311
  path: ${{ steps.native_cache_scope.outputs.paths }}
322
312
 
323
- - name: Run simulator tests
324
- if: ${{ (inputs.run-tests || env.CFG_RUN_TESTS) == 'true' && inputs.candidate-binary == '' }}
325
- shell: bash
326
- env:
327
- PROJECT: ${{ inputs.project || env.CFG_PROJECT }}
328
- WORKSPACE: ${{ inputs.workspace || env.CFG_WORKSPACE }}
329
- SCHEME: ${{ inputs.scheme || env.CFG_SCHEME }}
330
- CONFIGURATION: ${{ inputs.configuration || env.CFG_CONFIGURATION }}
331
- TEST_COMMAND: ${{ inputs.test-command || env.CFG_TEST_COMMAND }}
332
- TEST_DESTINATION: >-
333
- ${{ inputs.test-destination || env.CFG_TEST_DESTINATION
334
- || 'platform=iOS Simulator,name=iPhone 15' }}
335
- run: |
336
- set -o pipefail
337
- python3 "$SWIFT_APP_ACTION/scripts/native_dependency_guard.py" "$GITHUB_WORKSPACE" \
338
- --container "${WORKSPACE:-$PROJECT}"
339
- if [ -n "$TEST_COMMAND" ]; then
340
- echo "Running custom test command: $TEST_COMMAND"
341
- bash -c "$TEST_COMMAND"
342
- elif [ -f "Scripts/run-tests.sh" ]; then
343
- echo "Running Scripts/run-tests.sh"
344
- bash Scripts/run-tests.sh
345
- else
346
- if [ -n "$WORKSPACE" ]; then
347
- PROJ_ARGS=(-workspace "$WORKSPACE")
348
- else
349
- PROJ_ARGS=(-project "$PROJECT")
350
- fi
351
- echo "Running xcodebuild test on $TEST_DESTINATION"
352
- xcodebuild \
353
- "${PROJ_ARGS[@]}" \
354
- -scheme "$SCHEME" \
355
- -configuration "$CONFIGURATION" \
356
- -destination "$TEST_DESTINATION" \
357
- -disableAutomaticPackageResolution -skipPackageUpdates \
358
- CODE_SIGNING_ALLOWED=NO \
359
- CODE_SIGNING_REQUIRED=NO \
360
- CODE_SIGN_IDENTITY="" \
361
- DEVELOPMENT_TEAM="" \
362
- test
363
- fi
364
-
365
313
  - name: Stage ASC API key for authenticated REST uploads
366
314
  if: ${{ inputs.archive == 'true' }}
367
315
  shell: bash
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ // Tests run on the owned workspace; Actions verifies an authenticated exact-source result only.
3
+ const { spawnSync } = require('node:child_process');
4
+ const { mkdirSync, rmSync, writeFileSync } = require('node:fs');
5
+ const { dirname, relative, resolve, isAbsolute } = require('node:path');
6
+ const { SCHEMA, CONTEXT, command, identity, readReceipt, sameSource, github, expectedAuthor,
7
+ verifyStatus, digest } = require('./local_validation_core.cjs');
8
+
9
+ function localOnly() {
10
+ if (process.env.GITHUB_ACTIONS === 'true') throw new Error('tests_forbidden_in_github_actions');
11
+ }
12
+
13
+ function run(args) {
14
+ localOnly();
15
+ if (args[0] !== '--receipt' || args[2] !== '--' || !args[3]) throw new Error('receipt_and_command_required');
16
+ const path = resolve(args[1]);
17
+ const root = command('git', ['rev-parse', '--show-toplevel']);
18
+ const nested = relative(root, path);
19
+ if (nested !== '..' && !nested.startsWith('../') && !isAbsolute(nested)) {
20
+ throw new Error('receipt_must_be_outside_checkout');
21
+ }
22
+ const source = identity();
23
+ rmSync(path, { force: true });
24
+ const argv = args.slice(3);
25
+ const result = spawnSync(argv[0], argv.slice(1), { stdio: 'inherit' });
26
+ if (result.error || result.status !== 0) throw new Error('local_validation_command_failed');
27
+ sameSource(source, identity());
28
+ const receipt = { schema: SCHEMA, ...source, command_sha256: digest(argv) };
29
+ mkdirSync(dirname(path), { recursive: true });
30
+ writeFileSync(path, JSON.stringify(receipt) + '\n', { mode: 0o600 });
31
+ console.log(JSON.stringify({ ok: true, result: 'local_validation_passed', ...receipt }));
32
+ }
33
+
34
+ function publish(args) {
35
+ localOnly();
36
+ if (args[0] !== '--receipt' || args.length !== 2) throw new Error('receipt_required');
37
+ const receipt = readReceipt(args[1]);
38
+ sameSource(receipt, identity());
39
+ const commit = github(`repos/${receipt.repository}/git/commits/${receipt.head_sha}`);
40
+ if (commit.tree.sha !== receipt.tree_sha) throw new Error('remote_tree_mismatch');
41
+ const status = github(`repos/${receipt.repository}/statuses/${receipt.head_sha}`, {
42
+ state: 'success', context: CONTEXT, description: `v1:${receipt.tree_sha}:${receipt.command_sha256}`,
43
+ });
44
+ verifyStatus(status, receipt.tree_sha, github('user').login);
45
+ console.log(JSON.stringify({ ok: true, result: 'local_validation_published', ...receipt }));
46
+ }
47
+
48
+ function check(args) {
49
+ localOnly();
50
+ if (args[0] !== '--receipt' || args[2] !== '--sha' || args.length !== 4) throw new Error('receipt_and_sha_required');
51
+ const receipt = readReceipt(args[1]);
52
+ if (receipt.head_sha !== args[3]) throw new Error('receipt_head_mismatch');
53
+ sameSource(receipt, identity());
54
+ console.log(JSON.stringify({ ok: true, result: 'local_validation_current', ...receipt }));
55
+ }
56
+
57
+ async function verify(args) {
58
+ if (args[0] !== '--sha' || args.length !== 2 || !/^[a-f0-9]{40}$/.test(args[1])) {
59
+ throw new Error('explicit_head_sha_required');
60
+ }
61
+ const sha = args[1], repository = process.env.GITHUB_REPOSITORY;
62
+ const author = expectedAuthor(sha, repository);
63
+ const tree = github(`repos/${repository}/git/commits/${sha}`).tree.sha;
64
+ if (command('git', ['rev-parse', 'HEAD^{tree}']) !== tree) throw new Error('checkout_tree_mismatch');
65
+ let status;
66
+ for (let attempt = 0; attempt <= 6; attempt += 1) {
67
+ // The combined /status summary omits creator; full records authenticate the issuer.
68
+ const statuses = github(`repos/${repository}/commits/${sha}/statuses?per_page=100`);
69
+ status = statuses.find(item => item.context === CONTEXT);
70
+ if (status || attempt === 6) break;
71
+ await new Promise(resolve => setTimeout(resolve, 5000));
72
+ }
73
+ verifyStatus(status, tree, author);
74
+ console.log(JSON.stringify({ ok: true, result: 'local_validation_verified', head_sha: sha, tree_sha: tree }));
75
+ }
76
+
77
+ async function main() {
78
+ const [operation, ...args] = process.argv.slice(2);
79
+ const actions = { run, check, publish, verify };
80
+ if (!Object.hasOwn(actions, operation)) throw new Error('run_check_publish_or_verify_required');
81
+ await actions[operation](args);
82
+ }
83
+
84
+ main().catch(error => {
85
+ const code = /^[a-z][a-z0-9_]+$/.test(error.message) ? error.message : 'invalid_local_validation_input';
86
+ console.error(`local_validation_refused: ${code}`);
87
+ process.exitCode = 1;
88
+ });
@@ -0,0 +1,84 @@
1
+ const { createHash } = require('node:crypto');
2
+ const { execFileSync } = require('node:child_process');
3
+ const { readFileSync } = require('node:fs');
4
+
5
+ const SCHEMA = 'gowalk-cicd/local-validation.v1';
6
+ const CONTEXT = 'local-validation';
7
+
8
+ function command(program, args) {
9
+ try {
10
+ return execFileSync(program, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15000 }).trim();
11
+ }
12
+ catch { throw new Error(`${program}_command_failed`); }
13
+ }
14
+
15
+ function repositoryFromRemote(remote, ssh = args => command('ssh', args)) {
16
+ const https = remote.match(/^https:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
17
+ if (https) return https[1];
18
+ const scp = remote.match(/^(?:([\w.-]+)@)?([A-Za-z0-9][\w.-]*):([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
19
+ const url = remote.match(/^ssh:\/\/(?:([\w.-]+)@)?([A-Za-z0-9][\w.-]*)(?::(22))?\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/);
20
+ const [user, host, repository] = scp ? scp.slice(1) : url ? [url[1], url[2], url[4]] : [];
21
+ if (!repository) throw new Error('github_repository_required');
22
+ if (host === 'github.com' && user === 'git') return repository;
23
+ const config = Object.fromEntries(ssh(['-G', ...(user ? ['-l', user] : []), host])
24
+ .split('\n').map(line => line.trim().split(/\s+/, 2)));
25
+ if (config.hostname !== 'github.com' || config.user !== 'git' || config.port !== '22') {
26
+ throw new Error('github_repository_required');
27
+ }
28
+ return repository;
29
+ }
30
+
31
+ function identity() {
32
+ const repository = repositoryFromRemote(command('git', ['remote', 'get-url', 'origin']));
33
+ if (command('git', ['status', '--porcelain'])) throw new Error('clean_checkout_required');
34
+ return { repository, head_sha: command('git', ['rev-parse', 'HEAD']),
35
+ tree_sha: command('git', ['rev-parse', 'HEAD^{tree}']) };
36
+ }
37
+
38
+ function readReceipt(path) {
39
+ const receipt = JSON.parse(readFileSync(path, 'utf8'));
40
+ if (receipt.schema !== SCHEMA || !/^[\w.-]+\/[\w.-]+$/.test(receipt.repository)
41
+ || !/^[a-f0-9]{40}$/.test(receipt.head_sha) || !/^[a-f0-9]{40}$/.test(receipt.tree_sha)
42
+ || !/^[a-f0-9]{64}$/.test(receipt.command_sha256)) throw new Error('invalid_local_validation_receipt');
43
+ return receipt;
44
+ }
45
+
46
+ function sameSource(receipt, source) {
47
+ for (const field of ['repository', 'head_sha', 'tree_sha']) {
48
+ if (receipt[field] !== source[field]) throw new Error('local_validation_source_changed');
49
+ }
50
+ }
51
+
52
+ function github(path, fields = {}) {
53
+ const args = ['api', path];
54
+ for (const [key, value] of Object.entries(fields)) args.push('-f', `${key}=${value}`);
55
+ return JSON.parse(command('gh', args));
56
+ }
57
+
58
+ function expectedAuthor(sha, repository, event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'))) {
59
+ if (process.env.GITHUB_REPOSITORY !== repository) throw new Error('repository_mismatch');
60
+ if (event.pull_request) {
61
+ const pr = event.pull_request;
62
+ if (pr.head.repo.full_name !== repository || pr.head.sha !== sha) throw new Error('same_repository_head_required');
63
+ return pr.user.login;
64
+ }
65
+ if (process.env.GITHUB_EVENT_NAME === 'workflow_dispatch') {
66
+ if (process.env.GITHUB_SHA !== sha || event.repository?.full_name !== repository) {
67
+ throw new Error('dispatch_head_required');
68
+ }
69
+ return event.sender.login;
70
+ }
71
+ if (event.after !== sha || process.env.GITHUB_SHA !== sha) throw new Error('push_head_required');
72
+ return event.sender.login;
73
+ }
74
+
75
+ function verifyStatus(status, tree, author) {
76
+ if (!status || status.state !== 'success' || status.creator?.login !== author
77
+ || !new RegExp(`^v1:${tree}:[a-f0-9]{64}$`).test(status.description)) {
78
+ throw new Error('successful_author_local_validation_required');
79
+ }
80
+ }
81
+
82
+ module.exports = { SCHEMA, CONTEXT, command, repositoryFromRemote, identity, readReceipt, sameSource,
83
+ github, expectedAuthor, verifyStatus,
84
+ digest: args => createHash('sha256').update(JSON.stringify(args)).digest('hex') };
@@ -25,11 +25,9 @@ inputs:
25
25
  required: false
26
26
  default: ""
27
27
  run-tests:
28
- description: >-
29
- Run the project's analyzer and unit tests before the release build
30
- (flutter analyze + flutter test, or the Gradle module's unit tests)
28
+ description: DEPRECATED (ignored). Tests and analysis run locally, never in GitHub Actions.
31
29
  required: false
32
- default: "true"
30
+ default: "false"
33
31
  dart-defines:
34
32
  description: >-
35
33
  Compile-time Dart defines for a Flutter app, one `KEY=VALUE` per line
@@ -110,23 +108,6 @@ runs:
110
108
  shell: bash
111
109
  run: flutter pub get
112
110
 
113
- - name: Analyze Flutter app
114
- if: ${{ steps.config.outputs.project_kind == 'flutter' && inputs.run-tests == 'true' }}
115
- shell: bash
116
- run: flutter analyze
117
-
118
- # `flutter test` hard-fails with "Test directory \"test\" not found" when a repo
119
- # has no tests, which is not a real failure. Skip instead of blowing up the deploy.
120
- - name: Test Flutter app
121
- if: ${{ steps.config.outputs.project_kind == 'flutter' && inputs.run-tests == 'true' }}
122
- shell: bash
123
- run: |
124
- if [ -d test ]; then
125
- flutter test --no-pub
126
- else
127
- echo "No test/ directory; skipping flutter test."
128
- fi
129
-
130
111
  # Dart obfuscation is mandatory for Flutter apps: every release build is
131
112
  # compiled with --obfuscate, and there is deliberately no input or repo
132
113
  # variable that switches it off. --split-debug-info is not optional
@@ -217,20 +198,6 @@ runs:
217
198
  # additionally overrides them through the Variant API so the resolved
218
199
  # versionCode holds even when there is no literal — see
219
200
  # scripts/version_override.init.gradle.
220
- # `test` is the aggregate task ("Run unit tests for all variants") and is the
221
- # only one guaranteed to exist: AGP only creates testXUnitTest tasks for the
222
- # variants it enables, and AGP 9 disables the release unit test variant by
223
- # default, so `:app:testReleaseUnitTest` is simply absent in many projects.
224
- # Running it unqualified also mirrors `flutter test`, which covers the whole
225
- # package rather than one module.
226
- - name: Test Android app
227
- if: ${{ steps.config.outputs.project_kind == 'gradle' && inputs.run-tests == 'true' }}
228
- shell: bash
229
- working-directory: ${{ steps.config.outputs.gradle_root || '.' }}
230
- run: |
231
- chmod +x ./gradlew
232
- ./gradlew test --console=plain --stacktrace
233
-
234
201
  # ANDROID_BUILD_NUMBER is already in the environment: resolve_android.py
235
202
  # exported it through GITHUB_ENV. ANDROID_BUILD_NAME is only set when the
236
203
  # caller actually pinned a build-name — empty means "leave versionName
package/bin/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync } from 'node:fs';
4
+ import { spawnSync } from 'node:child_process';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { dirname, join } from 'node:path';
6
7
  const __filename = fileURLToPath(import.meta.url);
@@ -8,6 +9,11 @@ const __dirname = dirname(__filename);
8
9
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
9
10
 
10
11
  const args = process.argv.slice(2);
12
+ if (args[0] === 'validate') {
13
+ const helper = join(__dirname, '../action/scripts/local_validation.cjs');
14
+ const result = spawnSync(process.execPath, [helper, ...args.slice(1)], { stdio: 'inherit' });
15
+ process.exit(result.status ?? 1);
16
+ }
11
17
  if (args[0] === 'release') {
12
18
  const platform = args[1] || 'both';
13
19
  if (args.length > 2 || !['ios', 'android', 'both'].includes(platform)) {
@@ -51,6 +57,7 @@ What it writes:
51
57
 
52
58
  Options:
53
59
  release [ios|android|both] Prepare a mobile release intent on the task branch
60
+ validate run|check|publish|verify Run local focused checks or verify their authenticated source receipt
54
61
  --dry-run Print what would happen without writing files
55
62
  -v, --version Show version number
56
63
  -h, --help Show this help message
@@ -1,7 +1,7 @@
1
1
  # Native validation and delivery time
2
2
 
3
- Keep routine push-to-verified delivery within 2–5 minutes. Preserve every required check and
4
- coverage assertion. Measure regressions and fix the slow stage in the same task; lowering a
3
+ Keep routine push-to-verified delivery within 2–5 minutes. Preserve required delivery checks and
4
+ verify the local candidate evidence. Measure regressions and fix the slow stage in the same task; lowering a
5
5
  timeout, skipping a failed test or calling a transferred binary production-ready is not a fix.
6
6
  Cold toolchain setup and provider processing are recorded separately; neither is hidden from
7
7
  total wall time or billed runner time. Compile one frozen candidate and promote its verified
@@ -37,61 +37,17 @@ version and all helper hashes, writes `DEST/release.json`, and outputs `scripts`
37
37
  `version`, and `integrity`. Scripts retain their package-relative directory layout. Neither mode
38
38
  reads credentials or connects to a store. The receipt schema is `gowalk-cicd/native-tooling.v1`.
39
39
 
40
- ## Reuse native checks only with positive proof
41
-
42
- The application owns a reviewed JSON configuration, normally `.factory/native-validation.json`:
43
-
44
- ```json
45
- {
46
- "schema": 1,
47
- "input_paths": ["Sources", "Resources", "Tests", "UITests", "project.yml", "App.xcodeproj"],
48
- "workflow": ".github/workflows/ios-ci.yml",
49
- "jobs": [{"name": "Native tests", "steps": ["Run all native tests", "Verify full coverage"]}],
50
- "context_keys": ["package_version", "package_integrity", "xcode", "sdk", "devices", "backend"],
51
- "external_context_keys": ["backend"]
52
- }
53
- ```
54
-
55
- Use `input_paths: ["."]` for unknown project layouts; include every source, test, asset, script,
56
- lockfile, configuration and resource consumed by a reviewed narrower suite. `.github/`, the
57
- validated configuration and receipt helper bytes always participate. Symlinks, submodules and
58
- dirty selected inputs cannot establish reusable proof. Generated temporary configurations are
59
- supported; their canonical contents participate just like a committed configuration.
60
-
61
- Pass `NATIVE_VALIDATION_CONTEXT` as a JSON object whose keys match `context_keys`, with nonempty
62
- string values read from actual execution state. Include the resolved package version/integrity,
63
- full selected Xcode build, SDK/runtime, device matrix and consumed variables. A floating runner
64
- label is not a toolchain identity; describe the actual Mac before lookup when necessary.
65
-
66
- Every external key requires a fresh `sha256:<64 hex characters>` witness for the exact backend
67
- deployment/flow revision or canonical relevant response. An origin URL is not an external-state
68
- witness. Missing proof means full validation. For suites with inherently changing external data,
69
- run those integration checks afresh outside the reusable portion. Unknown custom scripts or
70
- undeclared network dependencies keep full coverage; never guess that they are unchanged.
71
-
72
- ```bash
73
- node "$SCRIPTS/native_validation.cjs" lookup .factory/native-validation.json
74
- # After every declared test and coverage check succeeded:
75
- node "$SCRIPTS/native_validation.cjs" record .factory/native-validation.json
76
- ```
77
-
78
- `lookup` needs repository `actions:read`, `GITHUB_TOKEN`, and a checkout containing the prior
79
- producer commit. Its outputs are `reused`, `reuse-reason`, `fingerprint`, `artifact-name` and,
80
- on a hit, `source-run`/`source-sha`. Missing external context returns a miss without a fingerprint.
81
- No completed match or unavailable API proof runs full coverage immediately; it never waits for
82
- another workflow. Missing local producer history is also a safe miss.
83
-
84
- `record` runs inside the producing job after tests, writing `receipt-path` and `artifact-name`.
85
- Retain that single `native-validation.json` with the pinned `actions/upload-artifact`, seven-day
86
- retention and the existing artifact process scope. For matrix suites, a collector records only
87
- after verifying every shard; configure every exact required job and successful step. A later
88
- lookup verifies the completed successful same-repository run, matching workflow, every named
89
- job/step, immutable artifact digest and an independently recomputed producer source tree. The
90
- receipt contains hashes and run identity, never context values or credentials.
91
-
92
- Keep the existing required `task/**` push check. A cheap admission may reuse prior success only
93
- under this proof; its required final check must require either that proof or successful fresh
94
- coverage. Do not replace the required push check with an unrelated PR merge-ref status.
40
+ ## Validate locally, verify the source receipt in Actions
41
+
42
+ Automated tests never run in GitHub Actions, including self-hosted jobs. Run only impacted checks
43
+ in the owned workspace and publish an authenticated receipt for the frozen clean candidate.
44
+ [Local validation](LOCAL_VALIDATION.md) defines the `gowalk-cicd validate` commands and proof.
45
+ Actions verifies that source proof and keeps build, signing, artifact, upload and health checks.
46
+ Existing custom app workflows require explicit migration: the installer does not infer which
47
+ parts of an arbitrary shell script are tests or rewrite mixed build/test execution blindly.
48
+ The older native-validation artifact reuse helpers remain for compatibility; they do not authorize
49
+ new test execution in Actions. Preserve required check names while replacing their execution with
50
+ local proof verification, and describe that behavior honestly in the job steps.
95
51
 
96
52
  ## Cache work is part of delivery cost
97
53
 
@@ -0,0 +1,61 @@
1
+ # Local validation and Actions integrity
2
+
3
+ Automated tests run in the owned workspace, never in GitHub Actions, including self-hosted
4
+ jobs. Select only the focused checks affected by the change. Do not repeat a full regression
5
+ on every push, on merge, or before publication. Actions still builds, signs, uploads, checks
6
+ artifacts and verifies deployment health; those operations are not automated test suites.
7
+
8
+ Commit the complete candidate first. Use a durable receipt path outside the checkout:
9
+
10
+ ```bash
11
+ npx --yes gowalk-cicd@1.0.186 validate run --receipt "$TASK_OUTPUT/local-validation.json" -- \
12
+ node --test test/changed-behavior.test.mjs
13
+ npx --yes gowalk-cicd@1.0.186 validate check --receipt "$TASK_OUTPUT/local-validation.json" --sha "$HEAD_SHA"
14
+ git push origin HEAD
15
+ npx --yes gowalk-cicd@1.0.186 validate publish --receipt "$TASK_OUTPUT/local-validation.json"
16
+ ```
17
+
18
+ `TASK_OUTPUT` is task-owned durable storage; `HEAD_SHA` is the candidate's `git rev-parse HEAD`.
19
+ The local command is executed directly, with the supplied arguments. Use the project's existing
20
+ focused runner when several checks belong together. `run` refuses Actions and a dirty checkout,
21
+ removes an old receipt at that path, and writes a new one only after the command exits successfully
22
+ and HEAD, repository, tree and checkout cleanliness remain unchanged. `check` verifies that result
23
+ without executing commands or contacting GitHub. `publish` checks it again, reads the remote commit
24
+ tree and posts an authenticated `local-validation` commit status. GitHub authentication uses `gh`.
25
+
26
+ The receipt schema is `gowalk-cicd/local-validation.v1`, with `repository`, `head_sha`, `tree_sha`
27
+ and `command_sha256`. The command digest is SHA-256 of the actual argument vector encoded as JSON.
28
+ The full committed tree includes dependency lockfiles and configuration. This proof does not claim
29
+ to describe untracked installed dependencies or the whole local machine. The status description is
30
+ `v1:<tree SHA-1>:<command SHA-256>`; receipts and statuses contain no command text or child logs.
31
+
32
+ Actions verifies the explicitly selected head, using the helper vendored by the installer:
33
+
34
+ ```yaml
35
+ permissions:
36
+ contents: read
37
+ statuses: read
38
+ steps:
39
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10
40
+ - name: Verify local candidate validation
41
+ env:
42
+ GH_TOKEN: ${{ github.token }}
43
+ VALIDATION_HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
44
+ run: node .github/actions/swift-app/scripts/local_validation.cjs verify --sha "$VALIDATION_HEAD"
45
+ ```
46
+
47
+ `verify` executes no tests. A PR requires same-repository head identity and a successful status
48
+ issued by its author; a push requires its exact event SHA and sender. Manual workflow dispatch verifies
49
+ the selected repository, explicit SHA against `GITHUB_SHA`, and the dispatch sender. The checked-out tree must equal
50
+ that head's tree, including on a PR merge ref. If main advanced and changed the merge tree, update
51
+ the task branch and validate the new candidate locally. Missing, failed, wrong-author or mismatched
52
+ proof refuses. A missing status gets six five-second retries for publication immediately after push.
53
+
54
+ The CLI also accepts `gowalk-cicd validate verify --sha ...`. The installed script and its sibling
55
+ `local_validation_core.cjs` avoid fetching another package in app jobs. Legacy composite inputs
56
+ `run-tests`, `test-command` and `test-destination` are accepted but ignored; composites execute no tests.
57
+ Custom app workflows and required status names need explicit migration. The installer preserves
58
+ unknown app-authored execution rather than guessing how to split an arbitrary mixed build/test script.
59
+
60
+ For this package, `pr.yml` verifies local proof, release version and the npm archive. Main publication
61
+ packages and publishes that checked artifact without npm lifecycle hooks or a repeated regression.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.185",
3
+ "version": "1.0.187",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,7 +42,8 @@
42
42
  "docs/BACKEND_SSH_GATE.md",
43
43
  "docs/BACKEND_NATIVE.md",
44
44
  "docs/CONFIGURATION.md",
45
- "docs/CI_PERFORMANCE.md"
45
+ "docs/CI_PERFORMANCE.md",
46
+ "docs/LOCAL_VALIDATION.md"
46
47
  ],
47
48
  "keywords": [
48
49
  "ios",
Binary file
@@ -37,7 +37,6 @@ jobs:
37
37
  runs-on: ${{ vars.ANDROID_RUNNER || 'ubuntu-24.04' }}
38
38
  timeout-minutes: 20
39
39
  continue-on-error: true
40
- cache-mode: write
41
40
  concurrency:
42
41
  group: warm-mobile-dependencies-android
43
42
  cancel-in-progress: false
@@ -203,7 +202,6 @@ jobs:
203
202
  runs-on: ${{ fromJSON(needs.plan.outputs.ios).runner }}
204
203
  timeout-minutes: 20
205
204
  continue-on-error: true
206
- cache-mode: write
207
205
  concurrency:
208
206
  group: warm-mobile-dependencies-ios
209
207
  cancel-in-progress: false