gowalk-cicd 1.0.113 → 1.0.115
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 +4 -0
- package/README.md +2 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/native_bundle.py +1 -1
- package/action/scripts/native_pods_diagnostics.py +15 -1
- package/action/scripts/native_pods_retry.py +2 -2
- package/action/scripts/test_native_pods.py +4 -0
- package/action/scripts/test_native_pods_diagnostics.py +27 -0
- package/action/scripts/test_native_pods_retry.py +2 -1
- package/action/scripts/test_native_pods_timeout.py +34 -0
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -214,6 +214,10 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
214
214
|
that multiline secret masking can erase. Never reconstruct masked fields or include arbitrary text.
|
|
215
215
|
Native bundle/Pods failures emit `native_prepare_failed` with fixed refusal codes and observed
|
|
216
216
|
signals. Unknown signals remain unclassified; never copy child output or commands into annotations.
|
|
217
|
+
Timeout and cancellation retain the existing bounded sanitized diagnostic after owned-child cleanup;
|
|
218
|
+
preserve the failure code, install deadline and retry fences.
|
|
219
|
+
Within the bounded capture, retain the CocoaPods primary exception alongside the tail when a long
|
|
220
|
+
stack or secondary issue-search failure hides it. Redact before selecting the bounded excerpt.
|
|
217
221
|
- The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
|
|
218
222
|
new app before GitHub registers `deploy-backend.yml` on the default branch.
|
|
219
223
|
Default-branch deploys continue through the separate backend workflow.
|
package/README.md
CHANGED
|
@@ -380,6 +380,8 @@ Native bundle and Pods failures emit `native_prepare_failed` with schema
|
|
|
380
380
|
Backend failures emit `backend_deploy_failed` with schema `gowalk-cicd/backend-deploy-failed.v1`,
|
|
381
381
|
the runner/host `phase` and `exit_code`. These receipts preserve the failed result and exclude
|
|
382
382
|
private command output, commands, hosts and runtime credentials. Empty signals are unclassified.
|
|
383
|
+
Timeout and cancellation preserve the bounded, sanitized child diagnostic after owned-process cleanup;
|
|
384
|
+
observed signals do not authorize another attempt or change the failed result.
|
|
383
385
|
The backend message is `gowalk-cicd/backend-deploy-failed.v1 phase=<phase> exit_code=<code>`:
|
|
384
386
|
fixed fields without JSON braces, which GitHub can mask when a multiline secret has brace-only lines.
|
|
385
387
|
Consumers validate the complete envelope, the known phase and exit code 1–255; older JSON messages
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.115
|
|
@@ -22,7 +22,7 @@ def command(argv: list[str], cwd: Path, log, *, timeout: float = 900) -> int:
|
|
|
22
22
|
try:
|
|
23
23
|
return native_pods_process.command(argv, cwd, log, timeout=timeout)
|
|
24
24
|
except native_pods_process.Interrupted:
|
|
25
|
-
|
|
25
|
+
refuse("native_pods_operation_interrupted", log, guard.Refused)
|
|
26
26
|
except (OSError, RuntimeError, subprocess.SubprocessError):
|
|
27
27
|
raise guard.Refused("native_pods_process_unverified") from None
|
|
28
28
|
|
|
@@ -23,6 +23,20 @@ def values(environment: dict[str, str]) -> set[str]:
|
|
|
23
23
|
return {value for value in found if value}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def excerpt(text: str) -> str:
|
|
27
|
+
lines = text.splitlines()
|
|
28
|
+
tail = "\n".join(lines[-30:])[-4000:]
|
|
29
|
+
start = next((index for index in range(len(lines) - 1, -1, -1)
|
|
30
|
+
if lines[index].strip() == "### Error"), None)
|
|
31
|
+
if start is None:
|
|
32
|
+
return tail
|
|
33
|
+
primary = "\n".join(lines[start:start + 6])[:1200]
|
|
34
|
+
if primary in tail:
|
|
35
|
+
return tail
|
|
36
|
+
tail = "\n".join(lines[-(29 - len(primary.splitlines())):])[-(3995 - len(primary)):]
|
|
37
|
+
return primary + "\n...\n" + tail
|
|
38
|
+
|
|
39
|
+
|
|
26
40
|
def diagnostic(log, environment: dict[str, str] | None = None) -> str:
|
|
27
41
|
log.flush()
|
|
28
42
|
log.seek(0, os.SEEK_END)
|
|
@@ -44,7 +58,7 @@ def diagnostic(log, environment: dict[str, str] | None = None) -> str:
|
|
|
44
58
|
text = re.sub(r"\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+", "[token redacted]", text)
|
|
45
59
|
text = re.sub(r"https?://[^\s\"'<>]+", "[URL redacted]", text)
|
|
46
60
|
text = re.sub(r"[A-Za-z0-9_+/=-]{32,}", "[long value redacted]", text)
|
|
47
|
-
return
|
|
61
|
+
return excerpt(text)
|
|
48
62
|
|
|
49
63
|
|
|
50
64
|
def refuse(code: str, log, exception):
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
|
|
8
8
|
import native_bundle
|
|
9
9
|
import native_dependency_guard as guard
|
|
10
|
-
from native_pods_diagnostics import diagnostic
|
|
10
|
+
from native_pods_diagnostics import diagnostic, refuse
|
|
11
11
|
|
|
12
12
|
TERMINAL = re.compile(
|
|
13
13
|
r"certificate|SSL peer|TLS|authentication|unauthorized|forbidden|permission denied|"
|
|
@@ -27,7 +27,7 @@ def install(argv: list[str], folder, log, snapshot: dict, *, timeout: float = 90
|
|
|
27
27
|
native_bundle.unchanged(snapshot)
|
|
28
28
|
remaining = deadline - time.monotonic()
|
|
29
29
|
if remaining <= 0:
|
|
30
|
-
|
|
30
|
+
refuse("native_pods_operation_interrupted", log, guard.Refused)
|
|
31
31
|
with tempfile.TemporaryFile() as child_log:
|
|
32
32
|
code = native_bundle.command(argv, folder, child_log, timeout=remaining)
|
|
33
33
|
body = diagnostic(child_log)
|
|
@@ -142,6 +142,7 @@ class PodsTests(unittest.TestCase):
|
|
|
142
142
|
self.locked()
|
|
143
143
|
script = self.root / "pod"
|
|
144
144
|
script.write_text('#!' + sys.executable + '\nimport os,time\nfrom pathlib import Path\n'
|
|
145
|
+
'print("Downloading dependencies\\nRead timed out\\nPRIVATE_SENTINEL", flush=True)\n'
|
|
145
146
|
'Path("owned-child").write_text(str(os.getpid()))\ntime.sleep(30)\n')
|
|
146
147
|
script.chmod(0o700)
|
|
147
148
|
self.env["PATH"] = str(self.root) + os.pathsep + self.env["PATH"]
|
|
@@ -155,8 +156,11 @@ class PodsTests(unittest.TestCase):
|
|
|
155
156
|
output, errors = child.communicate(timeout=10)
|
|
156
157
|
self.assertTrue((self.root / "owned-child").exists())
|
|
157
158
|
self.assertEqual(json.loads(output)["code"], "native_pods_operation_interrupted")
|
|
159
|
+
self.assertIn("Downloading dependencies", json.loads(output).get("diagnostic", ""))
|
|
160
|
+
self.assertNotIn("PRIVATE_SENTINEL", output + errors)
|
|
158
161
|
self.assertTrue(errors.startswith("::error title=native_prepare_failed::"))
|
|
159
162
|
self.assertEqual(json.loads(errors.strip().split("::", 2)[2])["code"], "native_pods_operation_interrupted")
|
|
163
|
+
self.assertEqual(json.loads(errors.strip().split("::", 2)[2])["signals"], ["connection_timeout"])
|
|
160
164
|
owned_pid = int((self.root / "owned-child").read_text())
|
|
161
165
|
deadline = time.monotonic() + 5
|
|
162
166
|
while time.monotonic() < deadline:
|
|
@@ -37,3 +37,30 @@ class DiagnosticsTests(unittest.TestCase):
|
|
|
37
37
|
self.assertLessEqual(len(result.splitlines()), 30)
|
|
38
38
|
pem = b"-----BEGIN PRIVATE KEY-----\nPRIVATE_PEM_VALUE\n-----END PRIVATE KEY-----\n"
|
|
39
39
|
self.assertNotIn("PRIVATE_PEM_VALUE", diagnostic(io.BytesIO(pem), {}))
|
|
40
|
+
|
|
41
|
+
def test_cocoapods_primary_exception_survives_long_stack_and_secondary_failure(self):
|
|
42
|
+
body = ('Analyzing dependencies\n### Error\n\n```\n'
|
|
43
|
+
'FixturePodError - cannot install dependency 12.3\n'
|
|
44
|
+
'Authorization: Bearer fixture-private-value\n'
|
|
45
|
+
'https://fixture-user:fixture-password@example.invalid/private\n')
|
|
46
|
+
body += ''.join(f'/ruby/gems/example/file_{index}.rb:12:in helper_{index}\n' for index in range(80))
|
|
47
|
+
body += '```\nTEMPLATE END\nSearching for inspections failed: secondary query refused\n'
|
|
48
|
+
result = diagnostic(io.BytesIO(body.encode()), {})
|
|
49
|
+
self.assertIn('FixturePodError - cannot install dependency 12.3', result)
|
|
50
|
+
self.assertIn('secondary query refused', result)
|
|
51
|
+
self.assertNotIn('fixture-private-value', result)
|
|
52
|
+
self.assertNotIn('fixture-password', result)
|
|
53
|
+
self.assertNotIn('example.invalid', result)
|
|
54
|
+
self.assertLessEqual(len(result), 4000)
|
|
55
|
+
self.assertLessEqual(len(result.splitlines()), 30)
|
|
56
|
+
self.assertEqual(diagnostic(io.BytesIO(result.encode()), {}), result)
|
|
57
|
+
|
|
58
|
+
def test_primary_exception_survives_character_bound_as_well_as_line_bound(self):
|
|
59
|
+
body = '### Error\n\n```\nFixturePodError - initial failure\n'
|
|
60
|
+
body += ('frame with many ordinary path components ' * 8 + '\n') * 28
|
|
61
|
+
body += 'secondary cleanup failure\n'
|
|
62
|
+
result = diagnostic(io.BytesIO(body.encode()), {})
|
|
63
|
+
self.assertIn('FixturePodError - initial failure', result)
|
|
64
|
+
self.assertIn('secondary cleanup failure', result)
|
|
65
|
+
self.assertLessEqual(len(result), 4000)
|
|
66
|
+
self.assertLessEqual(len(result.splitlines()), 30)
|
|
@@ -95,6 +95,7 @@ class RetryTests(unittest.TestCase):
|
|
|
95
95
|
self.assertEqual([c.kwargs["timeout"] for c in command.call_args_list], [899, 200])
|
|
96
96
|
command.reset_mock()
|
|
97
97
|
with patch.object(native_pods_retry.time, "monotonic", side_effect=[0, 1, 901]):
|
|
98
|
-
with self.assertRaisesRegex(guard.Refused, "native_pods_operation_interrupted"):
|
|
98
|
+
with self.assertRaisesRegex(guard.Refused, "native_pods_operation_interrupted") as caught:
|
|
99
99
|
native_pods_retry.install(["fixture"], self.root, log, {})
|
|
100
100
|
self.assertEqual(command.call_count, 1)
|
|
101
|
+
self.assertEqual(caught.exception.diagnostic, PARTIAL)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Keep a real timed-out Pod child's sanitized evidence after its private log closes."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
import tempfile
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import native_dependency_guard as guard
|
|
10
|
+
import native_pods_retry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@unittest.skipUnless(os.name == "posix", "owned POSIX process groups")
|
|
14
|
+
class TimeoutTests(unittest.TestCase):
|
|
15
|
+
def test_timeout_retains_bounded_diagnostic_without_retrying_or_leaving_child(self):
|
|
16
|
+
with tempfile.TemporaryDirectory(prefix="native-pods-timeout-") as folder:
|
|
17
|
+
root = Path(folder)
|
|
18
|
+
script = ('import os,time; from pathlib import Path; '
|
|
19
|
+
'Path("attempts").open("a").write("attempt\\n"); '
|
|
20
|
+
'print("x" * 20000, flush=True); '
|
|
21
|
+
'print("Downloading dependencies\\nRead timed out\\nPRIVATE_SENTINEL", flush=True); '
|
|
22
|
+
'Path("child").write_text(str(os.getpid())); time.sleep(30)')
|
|
23
|
+
with tempfile.TemporaryFile() as log, patch.dict(os.environ, {"TEST_TOKEN": "PRIVATE_SENTINEL"}):
|
|
24
|
+
with self.assertRaisesRegex(guard.Refused, "native_pods_operation_interrupted") as caught:
|
|
25
|
+
native_pods_retry.install([sys.executable, "-c", script], root, log, {}, timeout=1)
|
|
26
|
+
self.assertEqual((root / "attempts").read_text(), "attempt\n")
|
|
27
|
+
with self.assertRaises(ProcessLookupError):
|
|
28
|
+
os.kill(int((root / "child").read_text()), 0)
|
|
29
|
+
body = getattr(caught.exception, "diagnostic", "")
|
|
30
|
+
self.assertIn("Downloading dependencies", body)
|
|
31
|
+
self.assertIn("Read timed out", body)
|
|
32
|
+
self.assertNotIn("PRIVATE_SENTINEL", body)
|
|
33
|
+
self.assertLessEqual(len(body), 4000)
|
|
34
|
+
self.assertLessEqual(len(body.splitlines()), 30)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.115
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.115
|