gowalk-cicd 1.0.177 → 1.0.178

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/README.md CHANGED
@@ -524,6 +524,10 @@ remain readable by the panel. Masked fields are never reconstructed and credenti
524
524
  Protected vhost failures add `backend_vhost_failed` (`gowalk-cicd/backend-vhost-failed.v1`) with fixed
525
525
  brace-free `operation`, `stage`, `reason`, `child_exit` and `signals`; see `docs/BACKEND_EDGE.md`.
526
526
  It preserves the existing refusal and records only bounded observed facts, never private child output.
527
+ Edge health failures add `backend_health_failed` (`gowalk-cicd/backend-health-failed.v1`) with fixed
528
+ brace-free `probe`, `reason`, `curl_exit`, `http_status` and `attempt`; see `docs/BACKEND_EDGE.md`.
529
+ Only the final failed probe's bounded observations appear, with no URLs, bodies, headers or credentials.
530
+ Ingress refusal, strict TLS, exact-build acceptance, retry bounds and the original failed result remain.
527
531
  An interrupted locked Pod fetch can retry once within the existing 900-second install budget.
528
532
  `native_pods_retry` (`gowalk-cicd/native-pods-retry.v1`) reports `attempt:2` and `reason:partial_transfer`;
529
533
  successful preparation reports `pod_attempts`. Dependency locks, relay and TLS checks remain enforced.
@@ -9,6 +9,25 @@ import time
9
9
  from edge_inputs import load, TOKEN_HEADER
10
10
  from edge_vhost import NGINX_DIR, paths, recorded_domain
11
11
 
12
+ PROBES = frozenset({"configuration", "origin_missing_token", "origin_invalid_token", "origin_authorized",
13
+ "legacy_identity", "legacy", "public"})
14
+ REASONS = frozenset({"unclassified", "path_invalid", "request_failed", "http_refused", "json_invalid",
15
+ "body_shape", "build_missing", "build_mismatch"})
16
+
17
+
18
+ def report(probe, reason, *, curl_exit=None, http_status=None, attempt=None):
19
+ probe = probe if type(probe) is str and probe in PROBES else "unclassified"
20
+ reason = reason if type(reason) is str and reason in REASONS else "unclassified"
21
+ code = str(curl_exit) if type(curl_exit) is int and -255 <= curl_exit <= 255 else "none"
22
+ status = str(http_status) if type(http_status) is int and 100 <= http_status <= 599 else "none"
23
+ count = str(attempt) if type(attempt) is int and 1 <= attempt <= 15 else "none"
24
+ message = ("::error title=backend_health_failed::gowalk-cicd/backend-health-failed.v1 "
25
+ "probe={} reason={} curl_exit={} http_status={} attempt={}\n")
26
+ try:
27
+ sys.stderr.write(message.format(probe, reason, code, status, count))
28
+ except (OSError, ValueError):
29
+ pass # Diagnostic delivery must not replace the original refusal.
30
+
12
31
 
13
32
  def request(url, *, headers=None, run=subprocess.run):
14
33
  # curl retains its strict TLS/time bounds. Private headers and bounded response
@@ -27,46 +46,89 @@ def request(url, *, headers=None, run=subprocess.run):
27
46
  return result.returncode, int(status) if status.isdigit() else 0, data
28
47
 
29
48
 
30
- def healthy(url, sha="", *, headers=None, fetch=request, sleep=time.sleep):
49
+ def response_reason(code, status, body, sha):
50
+ if code != 0:
51
+ return "request_failed"
52
+ if not 200 <= status < 300:
53
+ return "http_refused"
54
+ if sha:
55
+ try:
56
+ value = json.loads(body)
57
+ except ValueError:
58
+ return "json_invalid"
59
+ if not isinstance(value, dict):
60
+ return "body_shape"
61
+ if "build_sha" not in value:
62
+ return "build_missing"
63
+ if value.get("build_sha") != sha:
64
+ return "build_mismatch"
65
+ return None
66
+
67
+
68
+ def healthy(url, sha="", *, headers=None, fetch=request, sleep=time.sleep, probe="public"):
31
69
  for attempt in range(15):
70
+ code = status = None
71
+ reason = "unclassified"
32
72
  try:
33
73
  code, status, body = fetch(url, headers=headers)
34
- valid = code == 0 and 200 <= status < 300
35
- if valid and sha:
36
- value = json.loads(body)
37
- valid = isinstance(value, dict) and value.get("build_sha") == sha
38
- if valid:
74
+ reason = response_reason(code, status, body, sha)
75
+ if reason is None:
39
76
  return
40
77
  except (OSError, ValueError, subprocess.SubprocessError):
41
- pass
78
+ reason = "request_failed" if code is None else "unclassified"
42
79
  if attempt != 14:
43
80
  sleep(2)
81
+ report(probe, reason, curl_exit=code, http_status=status, attempt=15)
44
82
  raise ValueError("backend_public_health_failed")
45
83
 
46
84
 
85
+ def origin_refusal(url, headers, probe, fetch):
86
+ try:
87
+ code, status, _body = fetch(url, headers=headers)
88
+ except (OSError, ValueError, subprocess.SubprocessError):
89
+ report(probe, "request_failed", attempt=1)
90
+ raise
91
+ if code != 0 or status != 403:
92
+ report(probe, "request_failed" if code != 0 else "http_refused",
93
+ curl_exit=code, http_status=status, attempt=1)
94
+ raise ValueError("backend_origin_ingress_unverified")
95
+
96
+
47
97
  def verify(config, app, domain, health, *, nginx_dir=NGINX_DIR, fetch=request, sleep=time.sleep):
48
- value = load(config)
98
+ try:
99
+ value = load(config)
100
+ except (OSError, ValueError, subprocess.SubprocessError):
101
+ report("configuration", "unclassified")
102
+ raise
49
103
  if not health.startswith("/") or any(character in health for character in "\r\n?#"):
104
+ report("configuration", "path_invalid")
50
105
  raise ValueError("backend_health_path_invalid")
51
106
  origin_url = f"https://{domain}{health}"
52
107
  if value["ingress_required"] == "true":
53
- for headers in (None, {TOKEN_HEADER: "invalid-backend-ingress-token"}):
54
- code, status, _body = fetch(origin_url, headers=headers)
55
- if code != 0 or status != 403:
56
- raise ValueError("backend_origin_ingress_unverified")
108
+ for probe, headers in (("origin_missing_token", None),
109
+ ("origin_invalid_token", {TOKEN_HEADER: "invalid-backend-ingress-token"})):
110
+ origin_refusal(origin_url, headers, probe, fetch)
57
111
  healthy(origin_url, value["expected_build_sha"], headers={TOKEN_HEADER: value["ingress_token"]},
58
- fetch=fetch, sleep=sleep)
59
- legacy = recorded_domain(paths(config, app, nginx_dir)[0])
112
+ fetch=fetch, sleep=sleep, probe="origin_authorized")
113
+ try:
114
+ legacy = recorded_domain(paths(config, app, nginx_dir)[0])
115
+ except (OSError, ValueError, subprocess.SubprocessError):
116
+ report("legacy_identity", "unclassified")
117
+ raise
60
118
  if legacy:
61
- healthy(f"https://{legacy}{health}", fetch=fetch, sleep=sleep)
119
+ healthy(f"https://{legacy}{health}", fetch=fetch, sleep=sleep, probe="legacy")
62
120
  public = value["public_health_url"] or origin_url
63
121
  healthy(public, value["expected_build_sha"], fetch=fetch, sleep=sleep)
64
122
  return {"ok": True, "public_health_verified": True, "build_sha": value["expected_build_sha"],
65
123
  "ingress_verified": value["ingress_required"] == "true"}
66
124
 
67
125
 
68
- if __name__ == "__main__":
126
+ def main():
69
127
  try:
70
128
  print(json.dumps(verify(*sys.argv[1:5])))
71
129
  except (OSError, ValueError, subprocess.SubprocessError):
72
130
  sys.exit("backend_edge_health_failed")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
@@ -57,3 +57,19 @@ They are observed patterns, not a unique cause or retry permission. No raw error
57
57
  token or child output is included. A failed restoration adds a separate rollback refusal while keeping
58
58
  the original failure visible; absence of that extra line alone does not prove successful restoration.
59
59
  The generic `backend_edge_vhost_refused`, changed/unchanged stdout, ingress, TLS and deploy gates remain.
60
+
61
+ Health refusals add the fixed stderr annotation `backend_health_failed` with schema
62
+ `gowalk-cicd/backend-health-failed.v1`. Its brace-free fields are `probe`, `reason`, `curl_exit`,
63
+ `http_status` and `attempt`. Probes are configuration, origin_missing_token, origin_invalid_token,
64
+ origin_authorized, legacy_identity, legacy and public; unknown metadata becomes unclassified.
65
+ Reasons are unclassified, path_invalid, request_failed, http_refused, json_invalid, body_shape,
66
+ build_missing and build_mismatch. No URL, response body, build identity, header, credential, path,
67
+ exception text or subprocess output enters the annotation. Curl exit is an observed integer from
68
+ -255 to 255, HTTP status from 100 to 599 and attempt from 1 to 15; other values become `none`.
69
+ An exhausted health check emits only its final attempt's facts, clearing any previous observations.
70
+ These facts identify the failed probe, not a unique cause or permission to retry or bypass it.
71
+ Missing-token and invalid-token checks remain single requests requiring completed 403 responses.
72
+ Healthy-origin, retained-legacy and public checks retain 15 attempts with two seconds between them,
73
+ strict TLS, existing curl time/body bounds and exact build checks. A successful check emits no
74
+ failure annotation and returns the existing result. Original exceptions and the generic CLI refusal
75
+ `backend_edge_health_failed` with exit 1 remain unchanged, including when diagnostic delivery fails.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.177",
3
+ "version": "1.0.178",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {