Reject release jobs failed in earlier run attempts

This commit is contained in:
SAPHID committed 2026-10-27 02:39:01 +00:00
1 parent 36cef1096a
commit dab13fa876
4 files changed
+38 -16

No files matched your search

+1 -1
View File
@@ -51,7 +51,7 @@ count. So a build reaches people only when you publish it, after testing it.
The script checks that the tag is on GitHub, that exact-commit main CI
(including e2e), native amd64/ARM64 Decky runtime checks, and the tagged
release workflow are green with no failed,
cancelled, skipped or running jobs, and that all ten installers
cancelled, skipped or running jobs in any run attempt, and that all ten installers
are attached, each with the SHA-256 digest GitHub records. It also checks
that the draft does not begin with a `WITHDRAWN` notice, so the preserved
v0.4.48 draft cannot accidentally be offered again. It checks
+18 -12
View File
@@ -77,19 +77,25 @@ def successful_jobs(run, required):
attempt = run.get("run_attempt")
if not isinstance(attempt, int) or attempt < 1:
raise GateError(f"run {run['id']} has no valid attempt number")
data = api(f"actions/runs/{run['id']}/attempts/{attempt}/jobs?per_page=100")
jobs = data.get("jobs") or []
if data.get("total_count") != len(jobs):
raise GateError(f"run {run['id']} has more jobs than the gate inspected")
names = [job.get("name") for job in jobs]
if len(names) != len(set(names)) or not required.issubset(names):
latest_jobs = None
for number in range(1, attempt + 1):
data = api(f"actions/runs/{run['id']}/attempts/{number}/jobs?per_page=100")
jobs = data.get("jobs") or []
if data.get("total_count") != len(jobs):
raise GateError(f"run {run['id']} attempt {number} has more jobs than the gate inspected")
names = [job.get("name") for job in jobs]
if not jobs or len(names) != len(set(names)):
raise GateError(f"run {run['id']} attempt {number} has missing or duplicate jobs")
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
if bad:
raise GateError(f"run {run['id']} attempt {number} has unfinished, failed, cancelled, or skipped jobs: {bad}")
latest_jobs = jobs
names = [job.get("name") for job in latest_jobs]
if not required.issubset(names):
missing = sorted(required.difference(names))
raise GateError(f"run {run['id']} has missing or duplicate jobs: {missing}")
bad = [f"{job.get('name')}: {job.get('status')}/{job.get('conclusion')}"
for job in jobs if job.get("status") != "completed" or job.get("conclusion") != "success"]
if bad:
raise GateError(f"run {run['id']} has unfinished, failed, cancelled, or skipped jobs: {bad}")
print(f"gate passed: {run.get('html_url')} ({len(jobs)} jobs)")
raise GateError(f"run {run['id']} has missing jobs in its latest attempt: {missing}")
print(f"gate passed: {run.get('html_url')} ({len(latest_jobs)} jobs, {attempt} attempts)")
def main(tag):
+8 -3
View File
@@ -91,17 +91,22 @@ elif method == "GET" and endpoint.startswith(repo + "actions/workflows/"):
is_main = workflow != "release.yml"
run_id = {"checks.yml": 501, "release.yml": 502, "decky-runtime.yml": 503}[workflow]
run = {"id": run_id, "head_sha": gate.get("main_sha" if is_main else "release_sha", commit),
"head_branch": branch, "event": "push", "run_attempt": 1,
"head_branch": branch, "event": "push", "run_attempt": gate.get("release_attempts", 1) if run_id == 502 else 1,
"status": gate.get("decky_status" if run_id == 503 else ("main_status" if is_main else "release_status"), "completed"),
"conclusion": gate.get("decky_conclusion" if run_id == 503 else ("main_conclusion" if is_main else "release_conclusion"), "success"),
"html_url": "https://github.com/saphid/frame-control/actions/runs/%s" % run_id}
print(json.dumps({"workflow_runs": [run]}))
elif method == "GET" and endpoint.startswith(repo + "actions/runs/"):
run_id = endpoint[len(repo + "actions/runs/"):].split("/")[0]
parts = endpoint[len(repo + "actions/runs/"):].split("/")
run_id = parts[0]
attempt = int(parts[2])
names = {"501": main_jobs, "502": release_jobs, "503": decky_jobs}[run_id]
names = [name for name in names if name != gate.get("omit_job")]
jobs = [{"name": name, "status": "completed",
"conclusion": gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job") else "success"}
"conclusion": (gate.get("prior_bad_job_conclusion", "failure")
if run_id == "502" and attempt == 1 and name == gate.get("prior_bad_job")
else gate.get("bad_job_conclusion", "success") if name == gate.get("bad_job")
else "success")}
for name in names]
print(json.dumps({"total_count": len(jobs), "jobs": jobs}))
elif method == "GET" and endpoint.startswith(repo + "releases?"):
+11
View File
@@ -134,6 +134,17 @@ class PublishRelease(unittest.TestCase):
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_a_prior_cancelled_installer_after_successful_rerun(self):
gates = {"release_attempts": 2,
"prior_bad_job": "build (macos-latest, dist, app/dist/*.dmg app/dist/*.zip)",
"prior_bad_job_conclusion": "cancelled"}
p, log, upload = self.run_script([draft()], "v9.8.7", gates=gates)
self.assertNotEqual(p.returncode, 0)
self.assertIn("attempt 1", p.stderr)
self.assertIn("cancelled", p.stderr)
self.assertIsNone(upload)
self.assertFalse(any("-X" in call for call in log))
def test_refuses_missing_or_nonexact_gate_results(self):
for gates in ({"omit_job": "e2e"}, {"main_sha": "c" * 40},
{"decky_conclusion": "failure"}, {"release_conclusion": "failure"},