gowalk-cicd 1.0.131 → 1.0.133

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.131
1
+ 1.0.133
@@ -67,13 +67,17 @@ def commands(script: str) -> list[list[str]] | None:
67
67
  current = []
68
68
  target = False
69
69
  for token in tokens:
70
- if target: # the file a redirect writes, not a command
70
+ if target:
71
71
  target = False
72
72
  continue
73
73
  if token.startswith("#"):
74
74
  break
75
75
  if token and set(token) <= OPERATOR:
76
- if set(token) & REDIRECT: # a redirect belongs to the command it follows
76
+ if set(token) & REDIRECT:
77
+ # Only the deferred uploader's effects belong to the removed operation.
78
+ # A redirected builtin/assignment can produce another required build input.
79
+ if not uploads_symbols(current):
80
+ return None
77
81
  target = True
78
82
  continue
79
83
  if current:
@@ -0,0 +1,30 @@
1
+ """An upload-only rewrite must preserve commands that also produce other build inputs."""
2
+ import copy
3
+ import unittest
4
+
5
+ import prepare_crashlytics_build as build
6
+
7
+
8
+ class PhaseEffectTests(unittest.TestCase):
9
+ def test_redirected_builtins_are_build_work_even_without_another_executable(self):
10
+ for command in ('echo public > Generated.swift', 'echo enabled >> build.env',
11
+ ': > generated.config', 'BUILD=release > generated.config'):
12
+ with self.subTest(command=command):
13
+ script = command + '\nupload-symbols -gsp app.plist -p ios symbols'
14
+ self.assertFalse(build.dedicated(script))
15
+ document = {'objects': {'phase': {'isa': 'PBXShellScriptBuildPhase',
16
+ 'name': 'Crashlytics and generated input', 'shellScript': script}}}
17
+ original = copy.deepcopy(document)
18
+ with self.assertRaisesRegex(ValueError, 'shares the build phase'):
19
+ build.rewrite(document)
20
+ self.assertEqual(document, original)
21
+
22
+ def test_supported_generated_guards_remain_upload_only(self):
23
+ self.assertTrue(build.dedicated(
24
+ 'if [ -f "$PODS_ROOT/FirebaseCrashlytics/upload-symbols" ]; then\n'
25
+ '"$PODS_ROOT/FirebaseCrashlytics/upload-symbols" -gsp app.plist -p ios symbols\n'
26
+ 'else echo "warning: upload-symbols absent"; fi'))
27
+
28
+
29
+ if __name__ == '__main__':
30
+ unittest.main()
@@ -1 +1 @@
1
- 1.0.131
1
+ 1.0.133
@@ -1,12 +1,33 @@
1
- """Run one Android compiler with bounded private output and fixed failure signals."""
1
+ """Run one Android compiler with bounded private output and fixed failure signals.
2
+
3
+ A signal alone does not locate a failure. ``signals":["dependency_resolution",
4
+ "tls_verification"]`` was the WHOLE record of a 15-minute Android build on task
5
+ 1183 (run 34362765661): the session could not tell which artifact would not
6
+ resolve or whose certificate was rejected, so its only move was to run the same
7
+ build again. Child output stays unpublished — it carries the store proxy's
8
+ credentials, and GitHub masks only the exact registered secret — so the detail
9
+ is EXTRACTED and SANITIZED instead: bare hostnames and artifact coordinates,
10
+ never a URL, never userinfo, never a free-text line.
11
+ """
2
12
  import argparse
3
13
  import json
14
+ import re
4
15
  import subprocess
5
16
  import sys
6
17
  import threading
7
18
  import time
8
19
 
9
20
  LIMIT = 65536
21
+ #: At most this many extracted facts per kind — a bounded record, not a log.
22
+ MAX_DETAILS = 8
23
+ #: A host in a URL, with any ``user:password@`` userinfo dropped by the pattern
24
+ #: itself so a credential can never reach the capture group.
25
+ _URL_HOST = re.compile(r"\bhttps?://(?:[^/@\s]*@)?([A-Za-z0-9.-]+\.[A-Za-z]{2,})")
26
+ #: Gradle/Maven coordinates: group:artifact:version, which carry no secrets.
27
+ _COORDINATE = re.compile(r"\b([A-Za-z][\w.-]*(?::[\w.+-]+){2})\b")
28
+ #: Hosts a certificate complaint names, e.g. "No subject alternative DNS name
29
+ #: matching pub.dev found" or "unable to find valid certification path to <host>".
30
+ _CERT_HOST = re.compile(r"(?:matching|for|to)\s+([a-z0-9-]+(?:\.[a-z0-9-]+)+)", re.I)
10
31
  SIGNALS = {
11
32
  "tls_verification": ("PKIX path building failed", "CERTIFICATE_VERIFY_FAILED", "certificate verify failed"),
12
33
  "dependency_resolution": ("Could not resolve all files", "Could not resolve all artifacts", "Could not GET '"),
@@ -29,6 +50,39 @@ def signals(output: bytes) -> list[str]:
29
50
  return sorted(name for name, patterns in SIGNALS.items() if any(pattern in text for pattern in patterns))
30
51
 
31
52
 
53
+ def _bounded(values) -> list[str]:
54
+ """Deduplicate, order and cap — a record the session can act on."""
55
+ seen = []
56
+ for value in values:
57
+ if value not in seen:
58
+ seen.append(value)
59
+ if len(seen) >= MAX_DETAILS:
60
+ break
61
+ return sorted(seen)
62
+
63
+
64
+ def details(output: bytes, found: list[str]) -> dict:
65
+ """Sanitized facts that LOCATE the signals, extracted from the private tail.
66
+
67
+ Only pattern captures leave this function: hostnames and dependency
68
+ coordinates. The raw text, any URL, and anything shaped like userinfo stay
69
+ in the buffer that is discarded with the process.
70
+ """
71
+ text = output.decode("utf-8", errors="replace")
72
+ out = {}
73
+ if "dependency_resolution" in found or "tls_verification" in found:
74
+ hosts = _bounded(_URL_HOST.findall(text))
75
+ if "tls_verification" in found:
76
+ hosts = _bounded(hosts + _CERT_HOST.findall(text))
77
+ if hosts:
78
+ out["hosts"] = hosts
79
+ if "dependency_resolution" in found:
80
+ coordinates = _bounded(_COORDINATE.findall(text))
81
+ if coordinates:
82
+ out["artifacts"] = coordinates
83
+ return out
84
+
85
+
32
86
  def emit(title: str, level: str, **fields) -> None:
33
87
  print(f"::{level} title={title}::" + json.dumps(fields, separators=(",", ":")), flush=True)
34
88
 
@@ -60,8 +114,10 @@ def run(phase: str, command: list[str], *, interval: float = 60) -> int:
60
114
  if not reader.is_alive():
61
115
  child.stdout.close()
62
116
  if code:
117
+ retained = bytes(tail)
118
+ found = signals(retained)
63
119
  emit("android_build_failed", "error", schema="gowalk-cicd/android-build-failed.v1",
64
- phase=phase, exit_code=code, signals=signals(bytes(tail)))
120
+ phase=phase, exit_code=code, signals=found, **details(retained, found))
65
121
  return code if code >= 0 else 128 - code
66
122
 
67
123
 
@@ -103,6 +103,46 @@ class BuildDiagnosticsTests(unittest.TestCase):
103
103
  self.assertIn('"signals":["r8_missing_classes"]', result.stdout)
104
104
  self.assertNotIn("private-class", result.stdout + result.stderr)
105
105
 
106
+ def test_dependency_failure_names_hosts_and_artifacts_without_credentials(self):
107
+ """A signal alone cannot be acted on; the record must LOCATE the failure.
108
+
109
+ Task 1183's run 34362765661 recorded only the two signal names for a
110
+ 15-minute build, so the session could only run it again.
111
+ """
112
+ code, rows, output = self.invoke(
113
+ "import sys; print(\"Could not resolve all files for configuration ':app:releaseRuntimeClasspath'\");"
114
+ "print('Could not resolve com.google.firebase:firebase-analytics:21.5.0');"
115
+ "print(\"Could not GET 'https://proxyuser:sup3rSecret@residential.exit.test:8080/maven2/x.pom'\");"
116
+ "print('PKIX path building failed: unable to find valid certification path to dl.google.com');"
117
+ "sys.exit(1)"
118
+ )
119
+ self.assertEqual(code, 1)
120
+ row = rows[0]
121
+ self.assertEqual(row["signals"], ["dependency_resolution", "tls_verification"])
122
+ self.assertIn("dl.google.com", row["hosts"])
123
+ self.assertIn("residential.exit.test", row["hosts"])
124
+ self.assertIn("com.google.firebase:firebase-analytics:21.5.0", row["artifacts"])
125
+ for secret in ("sup3rSecret", "proxyuser", "maven2", "x.pom"):
126
+ self.assertNotIn(secret, output)
127
+
128
+ def test_extracted_detail_is_bounded_and_ordered(self):
129
+ text = "".join(f"Could not GET 'https://host{n}.test/a'\n" for n in range(40)).encode()
130
+ found = ["dependency_resolution"]
131
+ hosts = diagnostics.details(text, found)["hosts"]
132
+ self.assertEqual(len(hosts), diagnostics.MAX_DETAILS)
133
+ self.assertEqual(hosts, sorted(hosts))
134
+
135
+ def test_a_signal_that_needs_no_locating_adds_no_detail(self):
136
+ code, rows, _ = self.invoke(
137
+ "import sys; print('Missing classes detected while running R8'); sys.exit(3)")
138
+ self.assertEqual(code, 3)
139
+ self.assertEqual(set(rows[0]), {"schema", "phase", "exit_code", "signals"})
140
+
141
+ def test_userinfo_never_reaches_the_record_even_without_a_signal_match(self):
142
+ detail = diagnostics.details(b"Could not GET 'https://u:p@only.test/x'", ["dependency_resolution"])
143
+ self.assertEqual(detail["hosts"], ["only.test"])
144
+ self.assertNotIn("p@", json.dumps(detail))
145
+
106
146
 
107
147
  if __name__ == "__main__":
108
148
  unittest.main()
@@ -1 +1 @@
1
- 1.0.131
1
+ 1.0.133
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.131",
3
+ "version": "1.0.133",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {