gowalk-cicd 1.0.114 → 1.0.116

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
@@ -216,6 +216,8 @@ node /path/to/gowalk-cicd/bin/cli.mjs
216
216
  signals. Unknown signals remain unclassified; never copy child output or commands into annotations.
217
217
  Timeout and cancellation retain the existing bounded sanitized diagnostic after owned-child cleanup;
218
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.
219
221
  - The mobile workflow's feature-branch-only `backend-preview` job bootstraps a
220
222
  new app before GitHub registers `deploy-backend.yml` on the default branch.
221
223
  Default-branch deploys continue through the separate backend workflow.
@@ -1 +1 @@
1
- 1.0.114
1
+ 1.0.116
@@ -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 "\n".join(text.splitlines()[-30:])[-4000:]
61
+ return excerpt(text)
48
62
 
49
63
 
50
64
  def refuse(code: str, log, exception):
@@ -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)
@@ -1 +1 @@
1
- 1.0.114
1
+ 1.0.116
@@ -1 +1 @@
1
- 1.0.114
1
+ 1.0.116
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.114",
3
+ "version": "1.0.116",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -581,9 +581,19 @@ jobs:
581
581
  run: |
582
582
  echo "::notice title=play_review_pending::{\"schema\":\"gowalk-cicd/play-review-pending.v1\",\"package\":\"${PACKAGE}\",\"track\":\"${TRACK}\",\"version_code\":\"${VERSION_CODE}\"}"
583
583
 
584
+ # Saved even when the build failed OR the job ran out of time. The cache
585
+ # holds DOWNLOADS, not build outputs: Gradle's `modules-2` store is
586
+ # content-addressed and re-verified on use, and a partial fetch never
587
+ # enters it. `!cancelled()` still skipped exactly the case that matters —
588
+ # a job cancelled at its own `timeout-minutes` — which deadlocked every
589
+ # app's FIRST Android release: a cold build that exceeds the cap is
590
+ # cancelled before this step, so nothing is cached, so the next attempt is
591
+ # cold too. Foundy (2026-09-09) spent 39 of its 45 minutes in
592
+ # `flutter_bundle` with no cache entry for the repository at all, while an
593
+ # app with a warm cache finishes the same job in 13 minutes.
584
594
  - name: Save Gradle dependencies
585
595
  if: >-
586
- ${{ !cancelled() && steps.mode.outputs.mode == 'local'
596
+ ${{ always() && steps.mode.outputs.mode == 'local'
587
597
  && steps.gradle_cache.outputs.cache-primary-key != ''
588
598
  && steps.gradle_cache.outputs.cache-hit != 'true' }}
589
599
  uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25