gowalk-cicd 1.0.121 → 1.0.123

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.
@@ -1 +1 @@
1
- 1.0.121
1
+ 1.0.123
@@ -1 +1 @@
1
- 1.0.121
1
+ 1.0.123
@@ -88,7 +88,16 @@ def call(phase: str, operation, *, retry: bool = False, edit_id: str | None = No
88
88
  else:
89
89
  if not retry or getattr(result, "status_code", None) not in RETRY_STATUSES:
90
90
  if accepted is not None and result.status_code not in accepted:
91
- fail(phase, "provider_refused", attempt, status=result.status_code, edit_id=edit_id)
91
+ # A transient upstream status is not a refusal, even where the
92
+ # phase may not replay it: `edit_create` never repeats an
93
+ # ambiguous insert, so a 503 there ends the build — and calling
94
+ # that "provider_refused" sent the next session hunting for a
95
+ # configuration Google never objected to (Foundy, 2026-09-09,
96
+ # run 34345638488). The remedy for one is to dispatch again;
97
+ # for the other it is to change something first.
98
+ code = ("provider_unavailable" if result.status_code in RETRY_STATUSES
99
+ else "provider_refused")
100
+ fail(phase, code, attempt, status=result.status_code, edit_id=edit_id)
92
101
  return result
93
102
  if attempt == limit:
94
103
  fail(phase, "provider_unavailable", attempt, status=result.status_code, edit_id=edit_id)
@@ -63,6 +63,26 @@ class PreflightTransportTests(unittest.TestCase):
63
63
  self.assertIn('"edit_id": "123"', str(caught.exception))
64
64
  self.assertEqual(operation.call_count, 3)
65
65
 
66
+ def test_a_transient_status_is_unavailable_even_where_the_phase_cannot_replay(self):
67
+ """`edit_create` must not repeat an ambiguous insert, but naming its 503 a
68
+ refusal told the next session to go and fix a configuration Google never
69
+ objected to. The status decides the word; the retry policy is unchanged."""
70
+ operation = mock.Mock(return_value=mock.Mock(status_code=503))
71
+ with self.assertRaises(SystemExit) as caught:
72
+ transport.call("edit_create", operation, accepted=(200, 404), retry_connect=True)
73
+ self.assertIn('"code": "provider_unavailable"', str(caught.exception))
74
+ self.assertIn('"status": 503', str(caught.exception))
75
+ self.assertIn('"attempts": 1', str(caught.exception))
76
+ operation.assert_called_once()
77
+ self.sleep.assert_not_called()
78
+
79
+ def test_a_real_refusal_keeps_its_own_word(self):
80
+ operation = mock.Mock(return_value=mock.Mock(status_code=403))
81
+ with self.assertRaises(SystemExit) as caught:
82
+ transport.call("edit_create", operation, accepted=(200, 404), retry_connect=True)
83
+ self.assertIn('"code": "provider_refused"', str(caught.exception))
84
+ self.assertIn('"status": 403', str(caught.exception))
85
+
66
86
  def test_refusal_after_retry_records_the_actual_attempt_in_a_safe_annotation(self):
67
87
  operation = mock.Mock(side_effect=[mock.Mock(status_code=503), mock.Mock(status_code=403)])
68
88
  with self.assertRaises(SystemExit) as caught:
@@ -1 +1 @@
1
- 1.0.121
1
+ 1.0.123
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.121",
3
+ "version": "1.0.123",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
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