gowalk-cicd 1.0.81 → 1.0.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -118,7 +118,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
118
118
  (`IOS_DISTRIBUTION_P12_BASE64`, `IOS_DISTRIBUTION_CERT_META_BASE64`,
119
119
  `IOS_DISTRIBUTION_CERT_REGISTRY_BASE64`), and the Android keystore, properties
120
120
  and Play service account into an ephemeral runner checkout.
121
- Tracked `creds/` files remain a backward-compatible fallback.
121
+ Tracked `creds/` files remain a backward-compatible read fallback. Refreshed signing
122
+ files stay ephemeral and are never staged; `persist-signing-cache` is ignored.
123
+ Version source staged before signing is preserved separately with its original index bytes.
122
124
  - Never revoke a distribution certificate automatically, including legacy caches.
123
125
  `certificate-cap-policy` accepts only `fail`; a full cap requires registry reconciliation.
124
126
  - Keep preinstalled Android SDK/NDKs. Every Gradle test/build and SDK download runs
@@ -158,6 +160,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
158
160
  errors still fail the job. Never use `continue-on-error` or a timeout around all SDK setup
159
161
  to hide an optional cache failure. Do not alter cache keys or disable caches as part of
160
162
  this timeout handling.
163
+ - Gradle caches use explicit checksum-pinned restore/save actions, each optional and bounded
164
+ to two minutes. Preserve setup-java's key inputs and absolute cache paths to reuse existing
165
+ caches. Java setup, builds and store uploads remain mandatory; save runs after delivery.
166
+ The cache-only preloader also admits the pinned `restore-only`/`save-only` entrypoints.
161
167
  - GitHub artifact uploads have their own checksum-pinned `artifact_env.cjs` process scope.
162
168
  It reuses the cache transport's approved HTTPS request guard, then removes inherited proxy
163
169
  variables only inside the official `upload-artifact` process. Unknown programs/origins refuse;
package/README.md CHANGED
@@ -62,9 +62,10 @@ the reusable Apple Distribution identity:
62
62
  All three distribution-identity values are base64 so their exact bytes—and the
63
63
  registry digests over those bytes—survive the secret round trip. The workflow
64
64
  materializes them with mode `0600` only in the ephemeral runner
65
- checkout. Secret-backed signing files are never staged or committed by the
66
- composite action. The distribution identity files come from the signing
67
- registry provisioner; pass their paths to the secret tooling without printing
65
+ checkout. Refreshed signing files are never staged or committed by the
66
+ composite action, including when a legacy workflow passes `persist-signing-cache: true`.
67
+ That input is retained as an ignored compatibility input. The distribution identity
68
+ files come from the signing registry provisioner; pass their paths to the secret tooling without printing
68
69
  their contents.
69
70
 
70
71
  Legacy private repositories can still provide the API key at:
@@ -1 +1 @@
1
- 1.0.81
1
+ 1.0.83
package/action/action.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # Note: this file is ~800 lines, above the project's 400-line composite-
2
2
  # action guideline. The size pre-dates the cert-caching feature; the
3
- # signing + commit-back step, the AI-metadata phases, and the
3
+ # signing preparation, the AI-metadata phases, and the
4
4
  # export / upload chunk are candidates for extraction into separate
5
5
  # scripts under ``scripts/`` (similar to how prepare_signing.py /
6
6
  # cert_factory.py / keychain.py / profile_io.py split the cert +
@@ -59,10 +59,10 @@ inputs:
59
59
  default: "fail"
60
60
  persist-signing-cache:
61
61
  description: >
62
- Commit refreshed signing files back to the default branch. Set to
63
- 'false' when credentials were materialized from encrypted secrets.
62
+ DEPRECATED (ignored). Signing files remain in the ephemeral checkout;
63
+ the action never stages refreshed credentials, including legacy caches.
64
64
  required: false
65
- default: "true"
65
+ default: "false"
66
66
  upload:
67
67
  description: "Set to 'false' to stop after producing the IPA"
68
68
  required: false
@@ -509,70 +509,11 @@ runs:
509
509
  set -e
510
510
  echo "SIGNING_PREP_EXIT=$signing_exit" >> "$GITHUB_ENV"
511
511
  if [ "$signing_exit" -ne 0 ]; then
512
- echo "::error::Signing preparation failed; preserving any generated cache before failing."
512
+ echo "::error::Signing preparation failed; preserving staged source before failing."
513
513
  fi
514
514
 
515
- # Encrypted signing material is the supported factory path and disables
516
- # the legacy signing-cache staging below. Source maintenance refuses staged
517
- # credential paths; it never pushes them or bypasses branch protection.
518
- # A version bump is preserved separately from the earlier action update.
519
-
520
- - name: Stage refreshed signing identity
521
- # Only stage on push to the default branch — PR runs and feature
522
- # branches must NOT mutate creds/ (they'd race the default branch
523
- # commit and clutter PR diffs). The signing identity is a long-lived
524
- # artifact owned by the canonical branch.
525
- if: ${{ inputs.archive == 'true'
526
- && inputs.persist-signing-cache == 'true'
527
- && github.event_name == 'push'
528
- && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
529
- shell: bash
530
- working-directory: ${{ github.workspace }}
531
- run: |
532
- set -euo pipefail
533
- # Default checkout is shallow (fetch-depth=1). Pushing back to the
534
- # default branch needs a full ref so git can rebase on top of any
535
- # commits that landed since checkout — promote to a full clone.
536
- # Either form may fail on already-deep checkouts; ignore non-zero.
537
- git fetch --unshallow 2>/dev/null \
538
- || git fetch --depth=2147483647 2>/dev/null \
539
- || true
540
- git config user.name 'github-actions[bot]'
541
- git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
542
- # Stage ONLY the specific files prepare_signing.py manages — never
543
- # `creds/` wholesale. The user's ASC .p8 key is already tracked
544
- # (committed separately at setup) so `git add` of new files leaves
545
- # it alone, but `creds/` may also hold notes, build artifacts, or
546
- # other unmanaged files we MUST NOT commit. Each `git add -f`
547
- # bypasses any consumer .gitignore that hides creds/. Missing
548
- # files (shouldn't happen given step ordering, but defensive)
549
- # produce a non-zero exit which we swallow — the commit-step is
550
- # the source of truth for "is there anything to commit".
551
- git add -f -- creds/cert.p12 2>/dev/null || true
552
- git add -f -- creds/cert.meta.json 2>/dev/null || true
553
- git add -f -- creds/cert.registry.json 2>/dev/null || true
554
- git add -f -- creds/profiles.manifest.json 2>/dev/null || true
555
- # The whole profiles/ directory is action-managed (one
556
- # .mobileprovision per signable bundle, GC'd when bundles are
557
- # removed); staging the directory picks up adds + removes in one
558
- # shot. Force-flag overrides any creds/profiles/ ignore rule.
559
- git add -f -- creds/profiles/ 2>/dev/null || true
560
- # Marketing-version auto-bump: NOTHING TO DO HERE.
561
- # version_utils.write_marketing_version() does an immediate
562
- # `git add -f -- <path>` right after the write, which captures
563
- # the post-bump, pre-prepare_signing state in the index.
564
- # prepare_signing.py later mutates the working-tree pbxproj
565
- # for manual-signing settings, but those edits land only in
566
- # the working tree -- the index keeps the version bump alone.
567
- # The marker at $RUNNER_TEMP/swift-app-mv-bumped is for log /
568
- # diagnostics; we surface it here so CI logs show exactly
569
- # which file mmv touched.
570
- bump_marker="$RUNNER_TEMP/swift-app-mv-bumped"
571
- if [ -f "$bump_marker" ]; then
572
- echo "auto-bump: marker present; the following file(s) were"
573
- echo "auto-bumped and pre-staged by version_utils:"
574
- sed 's/^/ /' "$bump_marker"
575
- fi
515
+ # Signing files remain in the ephemeral checkout for archive/export.
516
+ # Only version changes staged before prepare_signing enter source maintenance.
576
517
 
577
518
  - name: Commit + push staged bot changes
578
519
  # Any marketing-version auto-bump the version step staged is preserved.
@@ -5,6 +5,8 @@ const {basename, dirname, join} = require('node:path');
5
5
  const hashes = new Map([
6
6
  ['restore', 'a49301428b4c5477522c00f6464566457ed327d64ae5f80f8c12e762fb2ea9a2'],
7
7
  ['save', 'b969065795aaf5c6d20ba158e910be5d353eefb02eb2785263eb32e13090c066'],
8
+ ['restore-only', '6255afaa3956351b8cfefc1e82f026b4db418a10678a8eaddf3d6c55f81744de'],
9
+ ['save-only', '7fb63f90f06ce6a10f39d40a113f99791bffdedad5394cdad5b4dfaa644559cb'],
8
10
  ]);
9
11
  const artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
10
12
  const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
@@ -9,13 +9,11 @@ if git diff --cached --quiet; then
9
9
  fi
10
10
 
11
11
  staged="$(git diff --cached --name-only)"
12
- has_cert=0
13
12
  has_auto=0
14
13
  has_bump=0
15
14
  # autoupdate_check.sh deliberately does NOT stage .github/workflows/deploy.yml
16
15
  # (GITHUB_TOKEN cannot push workflow-file changes), so we only need to detect
17
16
  # action/ paths to classify the commit as an autoupdate.
18
- if grep -qE '^creds/' <<<"$staged"; then has_cert=1; fi
19
17
  if grep -qE '^\.github/actions/(swift-app|android-app)/' <<<"$staged"; then has_auto=1; fi
20
18
  # marketing-version-auto-bump (mmv_floor_check) writes the bumped value
21
19
  # into the project's source-of-truth (resolved by version_utils):
@@ -34,7 +32,6 @@ fi
34
32
  # Build a subject that lists every concern that actually fired. Avoids a
35
33
  # 2^N case explosion by composing components in a stable order.
36
34
  parts=()
37
- if [[ $has_cert -eq 1 ]]; then parts+=("refresh signing identity"); fi
38
35
  if [[ $has_auto -eq 1 ]]; then parts+=("autoupdate app-ci"); fi
39
36
  if [[ $has_bump -eq 1 ]]; then parts+=("bump MARKETING_VERSION"); fi
40
37
  if [[ ${#parts[@]} -eq 0 ]]; then
@@ -61,9 +61,9 @@ class TestActionStepOrder(unittest.TestCase):
61
61
  self.assertIn("format('refs/heads/{0}', github.event.repository.default_branch)", block)
62
62
  self.assertIn('bash "$BOT_SCRIPTS_DIR/commit_bot_changes.sh"', commit)
63
63
  self.assertIn("GITHUB_REF_NAME: ${{ github.ref_name }}", commit)
64
- # The cert refresh keeps its own, later commit step.
64
+ # Version source is preserved after ephemeral signing preparation.
65
65
  self.assertIn("Commit + push staged bot changes", self.names)
66
- self.assertLess(self.names.index("Stage refreshed signing identity"),
66
+ self.assertLess(self.names.index("Prepare signing (cert + keychain + per-target profiles + pbxproj)"),
67
67
  self.names.index("Commit + push staged bot changes"))
68
68
 
69
69
  def test_every_script_and_prompt_is_read_from_the_snapshot(self) -> None:
@@ -0,0 +1,82 @@
1
+ """Run the action's signing-to-maintenance steps against a protected Git remote."""
2
+ from pathlib import Path
3
+ import subprocess
4
+ import textwrap
5
+
6
+ from source_maintenance_fixture import MaintenanceFixture
7
+ from test_action_step_order import step_block, step_names
8
+
9
+
10
+ ACTION = Path(__file__).resolve().parents[1]
11
+ PREPARE = "Prepare signing (cert + keychain + per-target profiles + pbxproj)"
12
+ RERAISE = "Re-raise signing preparation failure"
13
+ FILES = ("cert.p12", "cert.meta.json", "cert.registry.json", "profiles.manifest.json",
14
+ "profiles/app.mobileprovision")
15
+
16
+
17
+ class SigningSourceBoundaryTests(MaintenanceFixture):
18
+ def setUp(self):
19
+ super().setUp()
20
+ snapshot = self.root / "action"
21
+ scripts = snapshot / "scripts"
22
+ scripts.mkdir(parents=True)
23
+ (scripts / "prepare_signing.py").write_text(
24
+ "from pathlib import Path\nimport os\n"
25
+ f"for name in {FILES!r}:\n"
26
+ " p = Path('creds') / name\n p.parent.mkdir(parents=True, exist_ok=True)\n"
27
+ " p.write_text('synthetic-signing-material')\n"
28
+ "p = Path('project.yml')\np.write_text(p.read_text() + 'signing: ephemeral\\n')\n"
29
+ "raise SystemExit(int(os.environ.get('FIXTURE_SIGNING_EXIT', '0')))\n")
30
+ (self.repo / ".git/info/exclude").write_text("creds/\n")
31
+ self.env.update({"SWIFT_APP_ACTION": str(snapshot), "RUNNER_TEMP": str(self.root),
32
+ "GITHUB_ENV": str(self.root / "environment"),
33
+ "BOT_SCRIPTS_DIR": str(ACTION / "scripts")})
34
+ self.source = (ACTION / "action.yml").read_text()
35
+
36
+ def run_signing_to_maintenance(self):
37
+ names = step_names(self.source)
38
+ selected = names[names.index(PREPARE):names.index(RERAISE)]
39
+ result = None
40
+ for name in selected:
41
+ block = step_block(self.source, name)
42
+ script = textwrap.dedent(block.split(" run: ", 1)[1])
43
+ if script.startswith("|\n"):
44
+ script = textwrap.dedent(script[2:])
45
+ result = subprocess.run(["bash", "-e", "-o", "pipefail", "-c", script],
46
+ cwd=self.repo, env=self.env, capture_output=True, text=True, timeout=15)
47
+ if result.returncode:
48
+ return result
49
+ return result
50
+
51
+ def assert_ephemeral_signing(self):
52
+ for name in FILES:
53
+ self.assertEqual((self.repo / "creds" / name).read_text(), "synthetic-signing-material")
54
+ self.assertEqual(self.git("ls-files", "creds"), "")
55
+ self.assertEqual(self.git("diff", "--cached", "--name-only"), "")
56
+ self.assertEqual(self.git("rev-parse", "HEAD").strip(), self.base)
57
+
58
+ def test_legacy_true_input_keeps_signing_out_of_version_candidate(self):
59
+ # All steps for archive=true/default-branch push/persist-signing-cache=true run.
60
+ self.stage()
61
+ receipt = self.receipt(self.run_signing_to_maintenance())
62
+ self.assertEqual(self.git("diff", "--name-only", self.base, receipt["head_sha"]), "project.yml\n")
63
+ self.assertEqual(self.git("show", receipt["head_sha"] + ":project.yml"), "version: 1.1\n")
64
+ self.assertEqual((self.repo / "project.yml").read_text(), "version: 1.1\nsigning: ephemeral\n")
65
+ self.assert_ephemeral_signing()
66
+
67
+ def test_signing_without_source_changes_needs_no_maintenance_candidate(self):
68
+ result = self.run_signing_to_maintenance()
69
+ self.assertEqual(result.returncode, 0, result.stderr)
70
+ self.assertEqual(len(self.git("ls-remote", "--heads", "origin").splitlines()), 1)
71
+ self.assert_ephemeral_signing()
72
+
73
+ def test_signing_failure_stays_visible_after_source_preservation(self):
74
+ self.stage()
75
+ self.env["FIXTURE_SIGNING_EXIT"] = "7"
76
+ self.receipt(self.run_signing_to_maintenance())
77
+ self.assertIn("SIGNING_PREP_EXIT=7\n", Path(self.env["GITHUB_ENV"]).read_text())
78
+ script = textwrap.dedent(step_block(self.source, RERAISE).split(" run: |\n", 1)[1])
79
+ result = subprocess.run(["bash", "-e", "-c", script], cwd=self.repo,
80
+ env={**self.env, "SIGNING_PREP_EXIT": "7"}, capture_output=True, text=True)
81
+ self.assertEqual(result.returncode, 7)
82
+ self.assert_ephemeral_signing()
@@ -1 +1 @@
1
- 1.0.81
1
+ 1.0.83
@@ -5,6 +5,8 @@ const {basename, dirname, join} = require('node:path');
5
5
  const hashes = new Map([
6
6
  ['restore', 'a49301428b4c5477522c00f6464566457ed327d64ae5f80f8c12e762fb2ea9a2'],
7
7
  ['save', 'b969065795aaf5c6d20ba158e910be5d353eefb02eb2785263eb32e13090c066'],
8
+ ['restore-only', '6255afaa3956351b8cfefc1e82f026b4db418a10678a8eaddf3d6c55f81744de'],
9
+ ['save-only', '7fb63f90f06ce6a10f39d40a113f99791bffdedad5394cdad5b4dfaa644559cb'],
8
10
  ]);
9
11
  const artifactHash = '168c44946cba03564808c19b83a72601e6d1d6f57081dbd2d6e2ea1cedc522f5';
10
12
  const proxyKeys = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy', 'ALL_PROXY', 'all_proxy'];
@@ -1 +1 @@
1
- 1.0.81
1
+ 1.0.83
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -181,8 +181,6 @@ jobs:
181
181
  # is locked in review. Default-branch deploys retain metadata automation.
182
182
  manage-app-store-version: >-
183
183
  ${{ github.ref_name == github.event.repository.default_branch && 'true' || 'false' }}
184
- persist-signing-cache: >-
185
- ${{ steps.ios_credentials.outputs.source == 'encrypted' && 'false' || 'true' }}
186
184
  # Repository variable FIREBASE_APP_ID: the app's Firebase App ID(s),
187
185
  # comma-separated when it ships both platforms. With an iOS id the
188
186
  # action uploads the archive's dSYMs to Crashlytics before TestFlight.
@@ -348,7 +346,35 @@ jobs:
348
346
  with:
349
347
  distribution: temurin
350
348
  java-version: ${{ steps.jdk.outputs.version }}
351
- cache: gradle
349
+ # Keep Java setup mandatory; optional caches have no unbounded post step.
350
+ - name: Retain Gradle cache scope
351
+ id: gradle_scope
352
+ if: ${{ steps.mode.outputs.mode == 'local' }}
353
+ continue-on-error: true
354
+ shell: bash
355
+ run: |
356
+ node .github/actions/android-app/scripts/cache_scope.cjs --snapshot
357
+ node - <<'NODE'
358
+ require('fs').appendFileSync(process.env.GITHUB_OUTPUT,
359
+ `home=${require('os').homedir()}\narch=${process.arch}\n`);
360
+ NODE
361
+ - name: Restore Gradle dependencies
362
+ id: gradle_cache
363
+ if: ${{ steps.mode.outputs.mode == 'local' && steps.gradle_scope.outcome == 'success' }}
364
+ uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25
365
+ continue-on-error: true
366
+ timeout-minutes: 2
367
+ env:
368
+ NODE_OPTIONS: --require "${{ steps.gradle_scope.outputs.preloader }}"
369
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: '1'
370
+ with:
371
+ path: |
372
+ ${{ steps.gradle_scope.outputs.home }}/.gradle/caches
373
+ ${{ steps.gradle_scope.outputs.home }}/.gradle/wrapper
374
+ key: >-
375
+ ${{ format('setup-java-{0}-{1}-gradle-{2}', runner.os, steps.gradle_scope.outputs.arch,
376
+ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'buildSrc/**/Versions.kt',
377
+ 'buildSrc/**/Dependencies.kt', 'gradle/*.versions.toml', '**/versions.properties')) }}
352
378
  - name: Set up Flutter
353
379
  if: ${{ steps.mode.outputs.mode == 'local' && steps.flutter.outputs.enabled == 'true' }}
354
380
  uses: ./.github/actions/android-app/flutter-setup
@@ -518,6 +544,22 @@ jobs:
518
544
  run: |
519
545
  echo "::notice title=play_review_pending::{\"schema\":\"gowalk-cicd/play-review-pending.v1\",\"package\":\"${PACKAGE}\",\"track\":\"${TRACK}\",\"version_code\":\"${VERSION_CODE}\"}"
520
546
 
547
+ - name: Save Gradle dependencies
548
+ if: >-
549
+ ${{ success() && steps.mode.outputs.mode == 'local'
550
+ && steps.gradle_cache.outputs.cache-primary-key != ''
551
+ && steps.gradle_cache.outputs.cache-hit != 'true' }}
552
+ uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25
553
+ continue-on-error: true
554
+ timeout-minutes: 2
555
+ env:
556
+ NODE_OPTIONS: --require "${{ steps.gradle_scope.outputs.preloader }}"
557
+ with:
558
+ path: |
559
+ ${{ steps.gradle_scope.outputs.home }}/.gradle/caches
560
+ ${{ steps.gradle_scope.outputs.home }}/.gradle/wrapper
561
+ key: ${{ steps.gradle_cache.outputs.cache-primary-key }}
562
+
521
563
  # GitHub only registers a newly added workflow after it reaches the default
522
564
  # branch. A brand-new app's feature branch therefore cannot dispatch the
523
565
  # separate Backend Deploy workflow yet. Reuse this already-registered mobile