gowalk-cicd 1.0.82 → 1.0.84

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
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.82
1
+ 1.0.84
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.
@@ -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.82
1
+ 1.0.84
@@ -39,11 +39,14 @@ class Relay:
39
39
  self.ready.set()
40
40
  if self.server:
41
41
  self.server.close()
42
- self.loop.run_until_complete(self.server.wait_closed())
42
+ # Python 3.12+ waits for active clients in Server.wait_closed().
43
+ # Their handlers own the connections, so cancel and drain them first.
43
44
  tasks = asyncio.all_tasks(self.loop)
44
45
  for task in tasks:
45
46
  task.cancel()
46
47
  self.loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
48
+ if self.server:
49
+ self.loop.run_until_complete(self.server.wait_closed())
47
50
  self.loop.close()
48
51
 
49
52
  async def _upstream(self, request: bytes):
@@ -0,0 +1,44 @@
1
+ """Open client sockets must not keep the command's owned relay alive on Python 3.12+."""
2
+ import asyncio
3
+ import socket
4
+ import unittest
5
+
6
+ import jvm_proxy_relay as relay
7
+ from test_jvm_proxy_relay import upstream
8
+
9
+
10
+ async def accepted():
11
+ while not any(task.get_coro().__name__ == "_handle" for task in asyncio.all_tasks()):
12
+ await asyncio.sleep(0.001)
13
+
14
+
15
+ class RelayCleanupTests(unittest.TestCase):
16
+ def test_shutdown_cancels_open_clients_before_waiting_for_server_connections(self):
17
+ with upstream() as (proxy, _seen):
18
+ for connected in (False, True):
19
+ with self.subTest(established_tunnel=connected):
20
+ owned = relay.Relay(proxy)
21
+ owned.thread.start()
22
+ self.assertTrue(owned.ready.wait(5))
23
+ client = socket.create_connection(("127.0.0.1", owned.port), timeout=5)
24
+ try:
25
+ if connected:
26
+ client.sendall(b"CONNECT unresolvable.invalid:443 HTTP/1.1\r\nHost: ignored\r\n\r\n")
27
+ self.assertIn(b"200", client.recv(4096))
28
+ else:
29
+ # Synchronize on acceptance while deliberately leaving the request unfinished.
30
+ asyncio.run_coroutine_threadsafe(accepted(), owned.loop).result(timeout=5)
31
+ owned.loop.call_soon_threadsafe(owned.loop.stop)
32
+ owned.thread.join(timeout=2)
33
+ self.assertFalse(owned.thread.is_alive(), "relay cleanup waits on a client it must close")
34
+ self.assertTrue(owned.loop.is_closed())
35
+ self.assertEqual(client.recv(4096), b"")
36
+ with self.assertRaises(OSError):
37
+ socket.create_connection(("127.0.0.1", owned.port), timeout=0.2)
38
+ finally:
39
+ client.close()
40
+ owned.thread.join(timeout=5)
41
+
42
+
43
+ if __name__ == "__main__":
44
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.82
1
+ 1.0.84
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
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.