gowalk-cicd 1.0.122 → 1.0.124
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/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.124
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Retry one positively identified
|
|
1
|
+
"""Retry one positively identified transient Pod fetch fault within the existing install deadline."""
|
|
2
2
|
import json
|
|
3
3
|
import re
|
|
4
4
|
import sys
|
|
@@ -14,6 +14,11 @@ TERMINAL = re.compile(
|
|
|
14
14
|
r"repository not found|checksum|compatible versions|deployment mode|"
|
|
15
15
|
r"returned error:\s*(?:401|403|407)\b|\bcurl(?:\s+(?:51|60)\b|:\s*\((?:51|60)\))", re.I)
|
|
16
16
|
PARTIAL = re.compile(r"\bcurl(?:\s+18\b|:\s*\(18\))", re.I)
|
|
17
|
+
# CocoaPods reports a timed-out CDN spec fetch as its own sentence and never as a curl
|
|
18
|
+
# code, so the partial-transfer evidence above cannot see it: "CDN: trunk URL couldn't be
|
|
19
|
+
# downloaded: <url> Response: Timeout was reached". A timeout leaves the successfully
|
|
20
|
+
# fetched specs cached, so the second attempt resumes rather than starting over.
|
|
21
|
+
TIMED_OUT = re.compile(r"Response:\s*Timeout was reached|\bcurl(?:\s+28\b|:\s*\(28\))", re.I)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def interrupted_transfer(body: str) -> bool:
|
|
@@ -21,6 +26,17 @@ def interrupted_transfer(body: str) -> bool:
|
|
|
21
26
|
text in body.lower() for text in ("early eof", "transfer closed", "bytes of body are still expected")))
|
|
22
27
|
|
|
23
28
|
|
|
29
|
+
def timed_out(body: str) -> bool:
|
|
30
|
+
return bool(TIMED_OUT.search(body) and not TERMINAL.search(body))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def retryable(body: str) -> str:
|
|
34
|
+
"""Name the transient class this failure belongs to, or "" when it is not one."""
|
|
35
|
+
if interrupted_transfer(body):
|
|
36
|
+
return "partial_transfer"
|
|
37
|
+
return "transient_timeout" if timed_out(body) else ""
|
|
38
|
+
|
|
39
|
+
|
|
24
40
|
def install(argv: list[str], folder, log, snapshot: dict, *, timeout: float = 900) -> tuple[int, int]:
|
|
25
41
|
deadline = time.monotonic() + timeout
|
|
26
42
|
for attempt in (1, 2):
|
|
@@ -35,9 +51,10 @@ def install(argv: list[str], folder, log, snapshot: dict, *, timeout: float = 90
|
|
|
35
51
|
log.seek(0)
|
|
36
52
|
log.truncate()
|
|
37
53
|
log.write(body.encode())
|
|
38
|
-
|
|
54
|
+
reason = retryable(body)
|
|
55
|
+
if code == 0 or attempt == 2 or not reason:
|
|
39
56
|
return code, attempt
|
|
40
|
-
payload = {"schema": "gowalk-cicd/native-pods-retry.v1", "attempt": 2, "reason":
|
|
57
|
+
payload = {"schema": "gowalk-cicd/native-pods-retry.v1", "attempt": 2, "reason": reason}
|
|
41
58
|
print("::notice title=native_pods_retry::" + json.dumps(payload, separators=(",", ":")),
|
|
42
59
|
file=sys.stderr, flush=True)
|
|
43
60
|
raise AssertionError("unreachable")
|
|
@@ -12,6 +12,10 @@ import native_pods_retry
|
|
|
12
12
|
import test_native_pods
|
|
13
13
|
|
|
14
14
|
PARTIAL = "error: RPC failed; curl 18 transfer closed with outstanding read data remaining\nfatal: early EOF"
|
|
15
|
+
# Verbatim from ios-zuper-sms Mobile Deploy 34346560202, after the diagnostic's own URL
|
|
16
|
+
# redaction — the form `retryable` actually sees.
|
|
17
|
+
TIMEOUT = "\n".join(["CDN: trunk URL couldn't be downloaded: [URL redacted] "
|
|
18
|
+
"Response: Timeout was reached"] * 30)
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class RetryTests(unittest.TestCase):
|
|
@@ -81,6 +85,42 @@ class RetryTests(unittest.TestCase):
|
|
|
81
85
|
self.assertEqual(self.fixture.run_cli()[0], 1)
|
|
82
86
|
self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 1)
|
|
83
87
|
|
|
88
|
+
def test_timed_out_cdn_fetch_recovers_within_the_same_deadline(self):
|
|
89
|
+
self.script([TIMEOUT])
|
|
90
|
+
code, result = self.fixture.run_cli()
|
|
91
|
+
self.assertEqual((code, result["pod_attempts"]), (0, 2))
|
|
92
|
+
attempts = json.loads((self.root / "attempts.json").read_text())
|
|
93
|
+
self.assertEqual(len(attempts), 2)
|
|
94
|
+
for attempt in attempts:
|
|
95
|
+
self.assertEqual(attempt["argv"], ["install", "--deployment", "--no-repo-update"])
|
|
96
|
+
self.assertTrue(attempt["proxy"].startswith("http://127.0.0.1:"))
|
|
97
|
+
self.assertEqual((self.root / "Podfile.lock").read_text(), "fixture-lock")
|
|
98
|
+
|
|
99
|
+
def test_repeat_timeout_stops_after_second_attempt(self):
|
|
100
|
+
self.script([TIMEOUT, TIMEOUT, TIMEOUT])
|
|
101
|
+
code, result = self.fixture.run_cli()
|
|
102
|
+
self.assertEqual((code, result["code"]), (1, "native_pods_install_failed"))
|
|
103
|
+
self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 2)
|
|
104
|
+
|
|
105
|
+
def test_terminal_evidence_dominates_a_timeout(self):
|
|
106
|
+
for failure in ("certificate verify failed", "The requested URL returned error: 403",
|
|
107
|
+
"checksum mismatch", "curl: (60) unknown trust failure"):
|
|
108
|
+
with self.subTest(failure=failure):
|
|
109
|
+
(self.root / "attempts.json").unlink(missing_ok=True)
|
|
110
|
+
self.script([TIMEOUT + "\n" + failure])
|
|
111
|
+
self.assertEqual(self.fixture.run_cli()[0], 1)
|
|
112
|
+
self.assertEqual(len(json.loads((self.root / "attempts.json").read_text())), 1)
|
|
113
|
+
|
|
114
|
+
def test_each_transient_class_is_named_and_unrelated_waits_are_not_retried(self):
|
|
115
|
+
self.assertEqual(native_pods_retry.retryable(TIMEOUT), "transient_timeout")
|
|
116
|
+
self.assertEqual(native_pods_retry.retryable(PARTIAL), "partial_transfer")
|
|
117
|
+
self.assertEqual(native_pods_retry.retryable("curl: (28) Operation timed out"),
|
|
118
|
+
"transient_timeout")
|
|
119
|
+
for body in ("the timeout was generous", "Response: 404 Not Found",
|
|
120
|
+
"waiting for the lock", "unknown network error"):
|
|
121
|
+
with self.subTest(body=body):
|
|
122
|
+
self.assertEqual(native_pods_retry.retryable(body), "")
|
|
123
|
+
|
|
84
124
|
def test_one_deadline_and_no_guess_from_generic_eof(self):
|
|
85
125
|
for body in ("fatal: early EOF", "curl 180 failure", "unknown network error"):
|
|
86
126
|
self.assertFalse(native_pods_retry.interrupted_transfer(body))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.124
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.124
|
package/package.json
CHANGED
package/src/install.mjs
CHANGED
|
@@ -127,6 +127,70 @@ function copyAction(source, destination, repoRoot, dryRun) {
|
|
|
127
127
|
return { dest, existed, fileCount };
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
// A workflow overwrite is wholesale: the template replaces the consumer's file.
|
|
131
|
+
// Anything that repo had tuned for itself is gone, and until now silently — the
|
|
132
|
+
// only thing standing between an app and a broken deploy was somebody reading
|
|
133
|
+
// the diff. obd-scanner adopted twice in one morning and both times the
|
|
134
|
+
// template's `runtime-env: ${{ secrets.BACKEND_RUNTIME_ENV }}` replaced an
|
|
135
|
+
// inline runtime env for a secret that repo does not define; adopting it as
|
|
136
|
+
// written would have deployed an empty environment (2026-09-09).
|
|
137
|
+
//
|
|
138
|
+
// So say what changed. This is a receipt, not a merge: the template still wins,
|
|
139
|
+
// and the adopting session is told exactly which of its own bindings to put
|
|
140
|
+
// back. Workflows are committed files, so nothing here is secret material —
|
|
141
|
+
// values are truncated only to keep the report readable.
|
|
142
|
+
const BINDING = /^\s*([A-Za-z0-9_.-]+):\s*(\S.*?)\s*$/;
|
|
143
|
+
const REPORT_LIMIT = 20;
|
|
144
|
+
|
|
145
|
+
function bindings(text) {
|
|
146
|
+
// Every distinct `key: value` pair, not one per key. Keys like
|
|
147
|
+
// `timeout-minutes` appear many times across jobs and an app tunes a
|
|
148
|
+
// particular one; keying by name alone reported the first occurrence and
|
|
149
|
+
// silently missed exactly the binding that had been changed.
|
|
150
|
+
const found = new Map();
|
|
151
|
+
for (const line of String(text).split('\n')) {
|
|
152
|
+
const match = BINDING.exec(line);
|
|
153
|
+
if (!match) continue;
|
|
154
|
+
const [, key, value] = match;
|
|
155
|
+
if (!found.has(key)) found.set(key, new Set());
|
|
156
|
+
found.get(key).add(value);
|
|
157
|
+
}
|
|
158
|
+
return found;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function short(value) {
|
|
162
|
+
const text = String(value).replace(/\s+/g, ' ').trim();
|
|
163
|
+
return text.length > 80 ? text.slice(0, 77) + '...' : text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function changedBindings(before, after) {
|
|
167
|
+
const old = bindings(before), fresh = bindings(after);
|
|
168
|
+
const changes = [];
|
|
169
|
+
for (const [key, values] of old) {
|
|
170
|
+
const now = fresh.get(key);
|
|
171
|
+
for (const value of values) {
|
|
172
|
+
if (now && now.has(value)) continue; // this exact pair survived
|
|
173
|
+
changes.push({ key, before: value, after: now ? [...now].join(' | ') : null });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return changes;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function reportOverwrite(relative, before, after) {
|
|
180
|
+
const changes = changedBindings(before, after);
|
|
181
|
+
if (!changes.length) return changes;
|
|
182
|
+
console.log(` ${relative}: ${changes.length} binding(s) this repo had set were replaced by the template.`);
|
|
183
|
+
console.log(' Restore the ones this app owns before committing:');
|
|
184
|
+
for (const change of changes.slice(0, REPORT_LIMIT)) {
|
|
185
|
+
console.log(` ${change.key}: ${short(change.before)}`
|
|
186
|
+
+ (change.after === null ? ' -> (removed)' : ` -> ${short(change.after)}`));
|
|
187
|
+
}
|
|
188
|
+
if (changes.length > REPORT_LIMIT) {
|
|
189
|
+
console.log(` ...and ${changes.length - REPORT_LIMIT} more; read the full diff.`);
|
|
190
|
+
}
|
|
191
|
+
return changes;
|
|
192
|
+
}
|
|
193
|
+
|
|
130
194
|
function copyWorkflow(repoRoot, dryRun) {
|
|
131
195
|
const dest = join(repoRoot, WORKFLOW_DEST_REL);
|
|
132
196
|
const existed = existsSync(dest);
|
|
@@ -135,8 +199,10 @@ function copyWorkflow(repoRoot, dryRun) {
|
|
|
135
199
|
return { dest, existed };
|
|
136
200
|
}
|
|
137
201
|
ensureDir(dirname(dest));
|
|
202
|
+
const previous = existed ? readFileSync(dest, 'utf8') : '';
|
|
138
203
|
cpSync(WORKFLOW_TEMPLATE, dest, { force: true });
|
|
139
204
|
console.log(` ${existed ? 'Overwrote' : 'Created'} ${WORKFLOW_DEST_REL}`);
|
|
205
|
+
if (existed) reportOverwrite(WORKFLOW_DEST_REL, previous, readFileSync(dest, 'utf8'));
|
|
140
206
|
return { dest, existed };
|
|
141
207
|
}
|
|
142
208
|
|
|
@@ -148,8 +214,10 @@ function copyBackendWorkflow(repoRoot, dryRun) {
|
|
|
148
214
|
return;
|
|
149
215
|
}
|
|
150
216
|
ensureDir(dirname(dest));
|
|
217
|
+
const previous = existed ? readFileSync(dest, 'utf8') : '';
|
|
151
218
|
cpSync(BACKEND_WORKFLOW_TEMPLATE, dest, { force: true });
|
|
152
219
|
console.log(` ${existed ? 'Overwrote' : 'Created'} ${BACKEND_WORKFLOW_DEST_REL}`);
|
|
220
|
+
if (existed) reportOverwrite(BACKEND_WORKFLOW_DEST_REL, previous, readFileSync(dest, 'utf8'));
|
|
153
221
|
}
|
|
154
222
|
|
|
155
223
|
function writeVersionMarkers(repoRoot, dryRun) {
|
|
@@ -244,8 +312,12 @@ export async function runInstall({ dryRun = false } = {}) {
|
|
|
244
312
|
console.log(` [dry-run] ${webWorkflowExisted ? 'overwrite' : 'create'} ${WEB_WORKFLOW_DEST_REL}`);
|
|
245
313
|
} else {
|
|
246
314
|
ensureDir(dirname(webWorkflowDest));
|
|
315
|
+
const webWorkflowPrevious = webWorkflowExisted ? readFileSync(webWorkflowDest, 'utf8') : '';
|
|
247
316
|
cpSync(WEB_WORKFLOW_TEMPLATE, webWorkflowDest, { force: true });
|
|
248
317
|
console.log(` ${webWorkflowExisted ? 'Overwrote' : 'Created'} ${WEB_WORKFLOW_DEST_REL}`);
|
|
318
|
+
if (webWorkflowExisted) {
|
|
319
|
+
reportOverwrite(WEB_WORKFLOW_DEST_REL, webWorkflowPrevious, readFileSync(webWorkflowDest, 'utf8'));
|
|
320
|
+
}
|
|
249
321
|
}
|
|
250
322
|
}
|
|
251
323
|
|