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 +3 -1
- package/README.md +4 -3
- package/action/.daemux-version +1 -1
- package/action/action.yml +7 -66
- package/action/scripts/commit_bot_changes.sh +0 -3
- package/action/scripts/test_action_step_order.py +2 -2
- package/action/scripts/test_signing_source_boundary.py +82 -0
- package/android-action/.daemux-version +1 -1
- package/android-action/scripts/jvm_proxy_relay.py +4 -1
- package/android-action/scripts/test_jvm_proxy_cleanup.py +44 -0
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
- package/templates/deploy.yml +0 -2
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.
|
|
66
|
-
composite action
|
|
67
|
-
|
|
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:
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
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: "
|
|
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
|
|
512
|
+
echo "::error::Signing preparation failed; preserving staged source before failing."
|
|
513
513
|
fi
|
|
514
514
|
|
|
515
|
-
#
|
|
516
|
-
#
|
|
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
|
-
#
|
|
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("
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1
|
+
1.0.84
|
package/package.json
CHANGED
package/templates/deploy.yml
CHANGED
|
@@ -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.
|