gowalk-cicd 1.0.102 → 1.0.104

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
@@ -204,6 +204,8 @@ node /path/to/gowalk-cicd/bin/cli.mjs
204
204
  secret. The action writes it to host-side `.runtime.env` with mode 0600 and
205
205
  supplies it to Compose after the generated `.env`; never print its content.
206
206
  Backend failures emit `backend_deploy_failed` with the fixed runner/host phase and exit code.
207
+ Its message uses the exact schema/phase/exit envelope documented in README, without JSON braces
208
+ that multiline secret masking can erase. Never reconstruct masked fields or include arbitrary text.
207
209
  Native bundle/Pods failures emit `native_prepare_failed` with fixed refusal codes and observed
208
210
  signals. Unknown signals remain unclassified; never copy child output or commands into annotations.
209
211
  - The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
@@ -262,6 +264,11 @@ node /path/to/gowalk-cicd/bin/cli.mjs
262
264
  Both Flutter configuration and the Pods helper use that bundle in their child process,
263
265
  preserving Podfile.lock and the parent job environment. Deliver the workflow wrapper and
264
266
  action helper together; action-only updates cannot repair earlier custom configuration steps.
267
+ - Locked Pod installation retries once only when its own attempt reports curl 18 and a partial-transfer
268
+ symptom. Both attempts share the existing 900-second budget and recheck Podfile, Podfile.lock and
269
+ selected Ruby bundle inputs. TLS/auth/permission, checksum, version and deployment errors never retry.
270
+ The assigned relay, deployment flags and completed-workspace guard remain mandatory; caches are retained.
271
+ `native_pods_retry` publishes only the fixed attempt/reason, and success reports `pod_attempts`.
265
272
  - Version stamping is per-build-system: Flutter takes `--build-number`, Gradle
266
273
  has no equivalent, so `set_gradle_version()` rewrites the literal in the
267
274
  checkout the way the iOS action patches the pbxproj. Never commit that edit.
package/README.md CHANGED
@@ -336,6 +336,13 @@ Native bundle and Pods failures emit `native_prepare_failed` with schema
336
336
  Backend failures emit `backend_deploy_failed` with schema `gowalk-cicd/backend-deploy-failed.v1`,
337
337
  the runner/host `phase` and `exit_code`. These receipts preserve the failed result and exclude
338
338
  private command output, commands, hosts and runtime credentials. Empty signals are unclassified.
339
+ The backend message is `gowalk-cicd/backend-deploy-failed.v1 phase=<phase> exit_code=<code>`:
340
+ fixed fields without JSON braces, which GitHub can mask when a multiline secret has brace-only lines.
341
+ Consumers validate the complete envelope, the known phase and exit code 1–255; older JSON messages
342
+ remain readable by the panel. Masked fields are never reconstructed and credential masking stays enabled.
343
+ An interrupted locked Pod fetch can retry once within the existing 900-second install budget.
344
+ `native_pods_retry` (`gowalk-cicd/native-pods-retry.v1`) reports `attempt:2` and `reason:partial_transfer`;
345
+ successful preparation reports `pod_attempts`. Dependency locks, relay and TLS checks remain enforced.
339
346
 
340
347
  SDK transfers also emit `flutter_download_progress` notices with schema
341
348
  `gowalk-cicd/flutter-download-progress.v1`. Each carries `phase`, `event`, `elapsed_ms`
@@ -1 +1 @@
1
- 1.0.102
1
+ 1.0.104
@@ -18,9 +18,9 @@ import native_pods_process
18
18
  from native_prepare_evidence import report
19
19
 
20
20
 
21
- def command(argv: list[str], cwd: Path, log) -> int:
21
+ def command(argv: list[str], cwd: Path, log, *, timeout: float = 900) -> int:
22
22
  try:
23
- return native_pods_process.command(argv, cwd, log)
23
+ return native_pods_process.command(argv, cwd, log, timeout=timeout)
24
24
  except native_pods_process.Interrupted:
25
25
  raise guard.Refused("native_pods_operation_interrupted") from None
26
26
  except (OSError, RuntimeError, subprocess.SubprocessError):
@@ -14,6 +14,7 @@ import tempfile
14
14
 
15
15
  import native_dependency_guard as guard
16
16
  import native_bundle
17
+ import native_pods_retry
17
18
  from native_bundle import command
18
19
  from native_pods_diagnostics import refuse
19
20
  from native_prepare_evidence import report
@@ -51,9 +52,12 @@ def prepare(root: Path, container: str, proxy_helper: Path) -> dict:
51
52
  initial = container or str(guard.selected(root, "", True).relative_to(root))
52
53
  guard.check(root, initial, before_pods=True)
53
54
  prefix, bundle_snapshot = native_bundle.prepare(root, folder, proxy_helper, log)
54
- if command([sys.executable, str(proxy_helper), "--", *prefix, "pod", "install",
55
- "--deployment", "--no-repo-update"],
56
- folder, log):
55
+ snapshot = {**bundle_snapshot, folder / "Podfile.lock": digest,
56
+ folder / "Podfile": hashlib.sha256((folder / "Podfile").read_bytes()).hexdigest()}
57
+ code, attempts = native_pods_retry.install(
58
+ [sys.executable, str(proxy_helper), "--", *prefix, "pod", "install", "--deployment", "--no-repo-update"],
59
+ folder, log, snapshot)
60
+ if code:
57
61
  refuse("native_pods_install_failed", log, guard.Refused)
58
62
  native_bundle.unchanged(bundle_snapshot)
59
63
  if digest != hashlib.sha256((folder / "Podfile.lock").read_bytes()).hexdigest():
@@ -65,7 +69,7 @@ def prepare(root: Path, container: str, proxy_helper: Path) -> dict:
65
69
  raise guard.Refused("native_project_selection_required")
66
70
  final = str(workspaces[0].relative_to(root))
67
71
  result = guard.check(root, final)
68
- return {**result, "pods": "installed", "lock_sha256": digest, "workspace": final}
72
+ return {**result, "pods": "installed", "pod_attempts": attempts, "lock_sha256": digest, "workspace": final}
69
73
 
70
74
 
71
75
  def interrupted(*_args) -> None:
@@ -0,0 +1,43 @@
1
+ """Retry one positively identified interrupted Pod fetch within the existing install deadline."""
2
+ import json
3
+ import re
4
+ import sys
5
+ import tempfile
6
+ import time
7
+
8
+ import native_bundle
9
+ import native_dependency_guard as guard
10
+ from native_pods_diagnostics import diagnostic
11
+
12
+ TERMINAL = re.compile(
13
+ r"certificate|SSL peer|TLS|authentication|unauthorized|forbidden|permission denied|"
14
+ r"repository not found|checksum|compatible versions|deployment mode|"
15
+ r"returned error:\s*(?:401|403|407)\b|\bcurl(?:\s+(?:51|60)\b|:\s*\((?:51|60)\))", re.I)
16
+ PARTIAL = re.compile(r"\bcurl(?:\s+18\b|:\s*\(18\))", re.I)
17
+
18
+
19
+ def interrupted_transfer(body: str) -> bool:
20
+ return bool(PARTIAL.search(body) and not TERMINAL.search(body) and any(
21
+ text in body.lower() for text in ("early eof", "transfer closed", "bytes of body are still expected")))
22
+
23
+
24
+ def install(argv: list[str], folder, log, snapshot: dict, *, timeout: float = 900) -> tuple[int, int]:
25
+ deadline = time.monotonic() + timeout
26
+ for attempt in (1, 2):
27
+ native_bundle.unchanged(snapshot)
28
+ remaining = deadline - time.monotonic()
29
+ if remaining <= 0:
30
+ raise guard.Refused("native_pods_operation_interrupted")
31
+ with tempfile.TemporaryFile() as child_log:
32
+ code = native_bundle.command(argv, folder, child_log, timeout=remaining)
33
+ body = diagnostic(child_log)
34
+ native_bundle.unchanged(snapshot)
35
+ log.seek(0)
36
+ log.truncate()
37
+ log.write(body.encode())
38
+ if code == 0 or attempt == 2 or not interrupted_transfer(body):
39
+ return code, attempt
40
+ payload = {"schema": "gowalk-cicd/native-pods-retry.v1", "attempt": 2, "reason": "partial_transfer"}
41
+ print("::notice title=native_pods_retry::" + json.dumps(payload, separators=(",", ":")),
42
+ file=sys.stderr, flush=True)
43
+ raise AssertionError("unreachable")
@@ -11,6 +11,7 @@ SIGNALS = {
11
11
  "tls_verification": ("certificate verify failed", "CERTIFICATE_VERIFY_FAILED"),
12
12
  "connection_timeout": ("Connection timed out", "execution expired", "Read timed out"),
13
13
  "dns_resolution": ("Could not resolve host", "getaddrinfo"),
14
+ "partial_transfer": ("curl 18", "curl: (18)"),
14
15
  "disk_full": ("No space left on device",),
15
16
  }
16
17
  ALL_REQUIRED = frozenset({"pod_lock_drift", "gem_missing"})
@@ -0,0 +1,100 @@
1
+ """Exercise interrupted installs through the actual CLI and relay, plus the shared deadline."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import sys
6
+ import tempfile
7
+ import unittest
8
+ from unittest.mock import patch
9
+
10
+ import native_dependency_guard as guard
11
+ import native_pods_retry
12
+ import test_native_pods
13
+
14
+ PARTIAL = "error: RPC failed; curl 18 transfer closed with outstanding read data remaining\nfatal: early EOF"
15
+
16
+
17
+ class RetryTests(unittest.TestCase):
18
+ def setUp(self):
19
+ self.fixture = test_native_pods.PodsTests()
20
+ self.fixture.setUp()
21
+ self.addCleanup(self.fixture.doCleanups)
22
+ self.fixture.locked()
23
+ self.root = self.fixture.root
24
+
25
+ def script(self, failures, *, change_lock=False):
26
+ configuration = json.dumps({"failures": failures, "change_lock": change_lock})
27
+ script = self.root / "pod"
28
+ script.write_text('#!' + sys.executable + '\nimport json,os,sys\nfrom pathlib import Path\n'
29
+ 'config=json.loads(' + repr(configuration) + ')\n'
30
+ 'p=Path("attempts.json");rows=json.loads(p.read_text()) if p.exists() else []\n'
31
+ 'rows.append({"argv":sys.argv[1:],"proxy":os.environ["HTTPS_PROXY"],'
32
+ '"bypass":os.environ["NO_PROXY"]});p.write_text(json.dumps(rows))\n'
33
+ 'if len(rows)<=len(config["failures"]):\n'
34
+ ' if config["change_lock"]: Path("Podfile.lock").write_text("changed")\n'
35
+ ' print(config["failures"][len(rows)-1],file=sys.stderr);raise SystemExit(7)\n'
36
+ 'p=Path("App.xcworkspace");p.mkdir(exist_ok=True)\n'
37
+ '(p/"contents.xcworkspacedata").write_text('
38
+ '\'<Workspace><FileRef location="group:App.xcodeproj"/></Workspace>\')\n')
39
+ script.chmod(0o700)
40
+ self.fixture.env["PATH"] = str(self.root) + os.pathsep + self.fixture.env["PATH"]
41
+ self.fixture.env["NO_PROXY"] = "*"
42
+
43
+ def test_partial_transfer_recovers_with_same_lock_flags_and_private_relay(self):
44
+ self.script([PARTIAL])
45
+ code, result = self.fixture.run_cli()
46
+ self.assertEqual((code, result["pod_attempts"]), (0, 2))
47
+ attempts = json.loads((self.root / "attempts.json").read_text())
48
+ self.assertEqual(len(attempts), 2)
49
+ for attempt in attempts:
50
+ self.assertEqual(attempt["argv"], ["install", "--deployment", "--no-repo-update"])
51
+ self.assertTrue(attempt["proxy"].startswith("http://127.0.0.1:"))
52
+ self.assertEqual(attempt["bypass"], "")
53
+ self.assertEqual((self.root / "Podfile.lock").read_text(), "fixture-lock")
54
+
55
+ def test_repeat_partial_failure_stops_after_second_attempt(self):
56
+ self.script([PARTIAL, PARTIAL, PARTIAL])
57
+ code, result = self.fixture.run_cli()
58
+ self.assertEqual((code, result["code"]), (1, "native_pods_install_failed"))
59
+ self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 2)
60
+
61
+ def test_changed_lock_refuses_before_any_retry(self):
62
+ self.script([PARTIAL], change_lock=True)
63
+ code, result = self.fixture.run_cli()
64
+ self.assertEqual((code, result["code"]), (1, "native_bundle_inputs_changed"))
65
+ self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 1)
66
+
67
+ def test_unknown_second_failure_does_not_inherit_first_attempt_classification(self):
68
+ self.script([PARTIAL, "unclassified failure", PARTIAL])
69
+ code, result = self.fixture.run_cli()
70
+ self.assertEqual(code, 1)
71
+ self.assertEqual(result["diagnostic"], "unclassified failure")
72
+ self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 2)
73
+
74
+ def test_terminal_evidence_dominates_partial_transfer(self):
75
+ for failure in ("certificate verify failed", "authentication failed", "permission denied",
76
+ "checksum mismatch", "could not find compatible versions", "deployment mode",
77
+ "The requested URL returned error: 407", "curl: (60) unknown trust failure"):
78
+ with self.subTest(failure=failure):
79
+ (self.root / "attempts.json").unlink(missing_ok=True)
80
+ self.script([PARTIAL + "\n" + failure])
81
+ self.assertEqual(self.fixture.run_cli()[0], 1)
82
+ self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 1)
83
+
84
+ def test_one_deadline_and_no_guess_from_generic_eof(self):
85
+ for body in ("fatal: early EOF", "curl 180 failure", "unknown network error"):
86
+ self.assertFalse(native_pods_retry.interrupted_transfer(body))
87
+ self.assertTrue(native_pods_retry.interrupted_transfer("curl: (18) transfer closed"))
88
+ def child(_argv, _folder, log, *, timeout):
89
+ log.write(PARTIAL.encode())
90
+ return 7
91
+ with tempfile.TemporaryFile() as log, patch.object(native_pods_retry.native_bundle, "command",
92
+ side_effect=child) as command:
93
+ with patch.object(native_pods_retry.time, "monotonic", side_effect=[0, 1, 700]):
94
+ self.assertEqual(native_pods_retry.install(["fixture"], self.root, log, {}), (7, 2))
95
+ self.assertEqual([c.kwargs["timeout"] for c in command.call_args_list], [899, 200])
96
+ command.reset_mock()
97
+ with patch.object(native_pods_retry.time, "monotonic", side_effect=[0, 1, 901]):
98
+ with self.assertRaisesRegex(guard.Refused, "native_pods_operation_interrupted"):
99
+ native_pods_retry.install(["fixture"], self.root, log, {})
100
+ self.assertEqual(command.call_count, 1)
@@ -1 +1 @@
1
- 1.0.102
1
+ 1.0.104
@@ -1 +1 @@
1
- 1.0.102
1
+ 1.0.104
@@ -8,8 +8,9 @@ backend_report_failure() {
8
8
  remote_setup|initialize_environment|compose_up|discover_port|local_health|configure_vhost|certificate|public_health) ;;
9
9
  *) phase=unclassified ;;
10
10
  esac
11
- printf '::error title=backend_deploy_failed::{"schema":"gowalk-cicd/backend-deploy-failed.v1",'\
12
- '"phase":"%s","exit_code":%s}\n' "$phase" "$status"
11
+ # Multiline JSON secrets can make GitHub mask standalone braces. Keep fixed evidence readable.
12
+ printf '::error title=backend_deploy_failed::gowalk-cicd/backend-deploy-failed.v1 phase=%s exit_code=%s\n' \
13
+ "$phase" "$status"
13
14
  return "$status"
14
15
  }
15
16
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.102",
3
+ "version": "1.0.104",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {