Skip to content

Commit 69e48c9

Browse files
committed
release workflow fixes
1 parent 5cae0b4 commit 69e48c9

2 files changed

Lines changed: 69 additions & 46 deletions

File tree

.github/workflows/release.yml

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -227,30 +227,68 @@ jobs:
227227
--environment production
228228
229229
- name: Verify deployment
230+
env:
231+
API_HOST: openhardwaremanager.blackdune-e38fce01.westus3.azurecontainerapps.io
232+
EXPECTED_VERSION: ${{ needs.validate.outputs.version }}
233+
# Lifespan startup (storage + MATCHING_EAGER_INIT up to 120s) can exceed
234+
# the old 20×5s window; use liveness (no storage fingerprint) and tolerate
235+
# empty/non-JSON gateway responses during revision rollout.
230236
run: |
231-
for i in $(seq 1 20); do
232-
ACTUAL=$(curl -sf https://openhardwaremanager.blackdune-e38fce01.westus3.azurecontainerapps.io/health \
233-
| python3 -c "import sys,json; print(json.load(sys.stdin)['version'])" || true)
234-
[ "$ACTUAL" = "${{ needs.validate.outputs.version }}" ] && break
235-
sleep 5
236-
done
237-
echo "deployed version=$ACTUAL (expected ${{ needs.validate.outputs.version }})"
238-
test "$ACTUAL" = "${{ needs.validate.outputs.version }}"
237+
python3 - <<'PY'
238+
import json
239+
import os
240+
import sys
241+
import time
242+
import urllib.error
243+
import urllib.request
244+
245+
host = os.environ["API_HOST"]
246+
expected = os.environ["EXPECTED_VERSION"]
247+
url = f"https://{host}/health/liveness"
248+
actual = None
249+
250+
for attempt in range(1, 49):
251+
try:
252+
with urllib.request.urlopen(url, timeout=30) as resp:
253+
body = json.load(resp)
254+
actual = body.get("version")
255+
if actual == expected:
256+
print(f"deployed version={actual} (expected {expected})")
257+
sys.exit(0)
258+
print(f"attempt {attempt}/48: version={actual!r} (waiting for {expected!r})")
259+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError) as exc:
260+
print(f"attempt {attempt}/48: not ready ({type(exc).__name__})")
261+
time.sleep(5)
262+
263+
print(f"deployed version={actual!r} (expected {expected!r})")
264+
sys.exit(1)
265+
PY
239266
240267
- name: Verify storage fingerprint (drift + emptiness gate)
241268
# Assert the live app is actually pointed at the container the repo
242269
# declares (config/environments/production.toml) AND that it is non-empty.
243270
# Emptiness is a deploy-gate failure (never promote a release onto an
244271
# empty/mis-pointed prod container) even though it is only a startup warning.
272+
env:
273+
API_HOST: openhardwaremanager.blackdune-e38fce01.westus3.azurecontainerapps.io
245274
run: |
246275
python3 - <<'PY'
247-
import json, sys, tomllib, urllib.request
276+
import json, sys, time, tomllib, urllib.error, urllib.request, os
248277
249-
host = "openhardwaremanager.blackdune-e38fce01.westus3.azurecontainerapps.io"
278+
host = os.environ["API_HOST"]
250279
with open("config/environments/production.toml", "rb") as f:
251280
expected = tomllib.load(f).get("azure_storage_container", "")
252-
with urllib.request.urlopen(f"https://{host}/health", timeout=15) as r:
253-
fp = json.load(r).get("storage", {})
281+
282+
fp = {}
283+
for attempt in range(1, 13):
284+
try:
285+
with urllib.request.urlopen(f"https://{host}/health", timeout=30) as r:
286+
fp = json.load(r).get("storage", {})
287+
if fp.get("container") is not None:
288+
break
289+
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as exc:
290+
print(f"attempt {attempt}/12: /health not ready ({type(exc).__name__})")
291+
time.sleep(5)
254292
255293
container, okh, okw = fp.get("container"), fp.get("okh_count"), fp.get("okw_count")
256294
print(f"live container={container!r} expected={expected!r} okh={okh} okw={okw}")
@@ -273,7 +311,7 @@ jobs:
273311
publish-frontend:
274312
name: Publish frontend multi-arch to Docker Hub
275313
runs-on: ubuntu-latest
276-
needs: [validate]
314+
needs: [validate, test, docker-smoke]
277315
if: >-
278316
github.event_name == 'push' ||
279317
(github.event_name == 'workflow_dispatch' && inputs.dry_run == false)
@@ -323,7 +361,7 @@ jobs:
323361
deploy-frontend-azure:
324362
name: Deploy frontend to Azure Container Apps
325363
runs-on: ubuntu-latest
326-
needs: [validate, publish-frontend]
364+
needs: [validate, publish, publish-frontend]
327365
if: github.event_name == 'push'
328366
environment: production
329367
permissions:

tests/unit/test_probes.py

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ def _probe_assumes_api_reachable():
2525
yield
2626

2727

28-
def _cfg(**options) -> HarnessConfig:
28+
def _cfg(module: str = "probe_match", **options) -> HarnessConfig:
29+
"""Probe module config for unit tests (no live API required)."""
30+
options.setdefault("skip_if_unreachable", False)
2931
return HarnessConfig(
3032
modules={
31-
"probe_match": ModuleConfig(enabled=True, options=options),
33+
module: ModuleConfig(enabled=True, options=options),
3234
}
3335
)
3436

@@ -95,23 +97,17 @@ def test_probe_latency_exceeds_error_slo(mock_api):
9597

9698
mock_api.return_value = HttpResult("GET", "u", 200, 15000, body={"items": []})
9799
mod = ProbeLatencyLoop(
98-
HarnessConfig(
99-
modules={
100-
"probe_latency": ModuleConfig(
101-
enabled=True,
102-
options={
103-
"checks": [
104-
{
105-
"name": "slow",
106-
"method": "GET",
107-
"path": "/okh?page=1",
108-
"warn_ms": 1000,
109-
"error_ms": 5000,
110-
}
111-
]
112-
},
113-
)
114-
}
100+
_cfg(
101+
"probe_latency",
102+
checks=[
103+
{
104+
"name": "slow",
105+
"method": "GET",
106+
"path": "/okh?page=1",
107+
"warn_ms": 1000,
108+
"error_ms": 5000,
109+
}
110+
],
115111
)
116112
)
117113
report = mod.run()
@@ -126,14 +122,7 @@ def test_probe_cache_flags_ineffective_paths(mock_api):
126122
HttpResult("GET", "u", 200, 950, body={}),
127123
]
128124
mod = ProbeCacheLoop(
129-
HarnessConfig(
130-
modules={
131-
"probe_cache": ModuleConfig(
132-
enabled=True,
133-
options={"probe_paths": ["/okh?page=1&page_size=5"]},
134-
)
135-
}
136-
)
125+
_cfg("probe_cache", probe_paths=["/okh?page=1&page_size=5"]),
137126
)
138127
report = mod.run()
139128
assert not report.ok
@@ -162,11 +151,7 @@ def test_probe_okh_files_flags_relative_paths(mock_api):
162151
},
163152
),
164153
]
165-
mod = ProbeOkhFilesLoop(
166-
HarnessConfig(
167-
modules={"probe_okh_files": ModuleConfig(enabled=True, options={})}
168-
)
169-
)
154+
mod = ProbeOkhFilesLoop(_cfg("probe_okh_files"))
170155
report = mod.run()
171156
assert not report.ok
172157
assert any("not API-proxied" in f.title for f in report.findings)

0 commit comments

Comments
 (0)