Skip to content

Commit eeb7fd4

Browse files
authored
validate: ignore cla-bot status when gating release builds (#21)
The release workflow calls check_ci_status() to refuse builds when upstream CI is red. cla-bot reports verification/cla-signed via the legacy commit-status API and that status is permanently red on every direct-mem / hint backport branch in e2b-dev/firecracker because the backports carry commits authored by upstream maintainers we don't have a CLA for (ilstam, ShadowCurse, JackThomson2, Manciukic, zulinx86). Those contributors aren't going to sign our CLA, so the status will stay red and we still need to ship those builds. Filter IGNORED_STATUS_CONTEXTS out of the combined-status response and recompute the rollup. Real CI failures alongside the CLA failure still block (test covers it). IGNORED_CHECK_NAMES gives a parallel knob for the Checks API if a check-run-based bot ever ends up in the same spot.
1 parent 86f1b7d commit eeb7fd4

2 files changed

Lines changed: 133 additions & 10 deletions

File tree

scripts/test_validate.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,62 @@ def test_ci_skipped_checks_count_as_success(self):
355355
success, message = check_ci_status(SAMPLE_COMMIT_SHA)
356356
assert success is True
357357

358+
def test_ci_ignored_cla_status_does_not_block(self):
359+
"""cla-bot's verification/cla-signed status is permanently red on
360+
external-contributor backport branches; filtering it out should let
361+
the build proceed when it's the only failure."""
362+
with patch("validate.gh_api") as mock_api:
363+
mock_api.side_effect = [
364+
{
365+
"state": "failure",
366+
"total_count": 2,
367+
"statuses": [
368+
{"context": "verification/cla-signed", "state": "error"},
369+
{"context": "buildkite/firecracker", "state": "success"},
370+
],
371+
},
372+
{"total_count": 0, "check_runs": []},
373+
]
374+
success, message = check_ci_status(SAMPLE_COMMIT_SHA)
375+
assert success is True
376+
assert "passed" in message
377+
378+
def test_ci_ignored_cla_status_alone_falls_through(self):
379+
"""If the CLA status is the only one and we filter it out, the
380+
rollup is empty → fall back to the no-checks 'proceed anyway' path."""
381+
with patch("validate.gh_api") as mock_api:
382+
mock_api.side_effect = [
383+
{
384+
"state": "failure",
385+
"total_count": 1,
386+
"statuses": [
387+
{"context": "verification/cla-signed", "state": "error"},
388+
],
389+
},
390+
{"total_count": 0, "check_runs": []},
391+
]
392+
success, message = check_ci_status(SAMPLE_COMMIT_SHA)
393+
assert success is True
394+
395+
def test_ci_other_failure_still_blocks_when_cla_also_failed(self):
396+
"""Real CI failure must still block even when the CLA status is also
397+
failing — the filter must not mask non-ignored failures."""
398+
with patch("validate.gh_api") as mock_api:
399+
mock_api.side_effect = [
400+
{
401+
"state": "failure",
402+
"total_count": 2,
403+
"statuses": [
404+
{"context": "verification/cla-signed", "state": "error"},
405+
{"context": "buildkite/firecracker", "state": "failure"},
406+
],
407+
},
408+
{"total_count": 0, "check_runs": []},
409+
]
410+
success, message = check_ci_status(SAMPLE_COMMIT_SHA)
411+
assert success is False
412+
assert "failed" in message
413+
358414

359415
class TestGenerateBuildMatrix:
360416
"""Tests for generate_build_matrix function."""

scripts/validate.py

Lines changed: 77 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,27 +154,88 @@ def resolve_tag_and_commit(
154154
return "", "", "Either tag or commit_hash must be provided"
155155

156156

157+
# IGNORED_STATUS_CONTEXTS lists legacy commit-status contexts that should not
158+
# block a release build even when failing. Keep the set tiny and well-justified.
159+
#
160+
# verification/cla-signed: cla-bot fails on the upstream firecracker fork
161+
# whenever a backport branch carries commits authored by upstream maintainers
162+
# we don't have a CLA for (e.g. ilstam, ShadowCurse, JackThomson2). Those
163+
# contributors won't ever sign our CLA, so the status is permanently red on
164+
# every direct-mem / hint backport branch — we still want to ship those builds.
165+
IGNORED_STATUS_CONTEXTS = frozenset({"verification/cla-signed"})
166+
167+
# IGNORED_CHECK_NAMES is the equivalent for the Checks API (apps that file a
168+
# check-run rather than a legacy status). Empty today; mirror IGNORED_STATUS_CONTEXTS
169+
# if a check-run-based bot ever ends up in the same situation.
170+
IGNORED_CHECK_NAMES = frozenset()
171+
172+
173+
def _rollup_status(statuses: list[dict]) -> tuple[str, int]:
174+
"""Compute (state, count) over the statuses list, mirroring how GitHub's
175+
combined-status endpoint rolls up: any failure → failure, else any pending
176+
→ pending, else any success → success, else unknown.
177+
"""
178+
if not statuses:
179+
return "unknown", 0
180+
states = {s.get("state") for s in statuses}
181+
if "failure" in states or "error" in states:
182+
return "failure", len(statuses)
183+
if "pending" in states:
184+
return "pending", len(statuses)
185+
if "success" in states:
186+
return "success", len(statuses)
187+
return "unknown", len(statuses)
188+
189+
157190
def check_ci_status(commit_hash: str, repo: str = "e2b-dev/firecracker") -> tuple[bool, str]:
158191
"""
159192
Check CI status for a commit.
160193
161194
Returns (success, message).
162195
"""
163-
# Check commit status API
196+
# Check commit status API. Filter out IGNORED_STATUS_CONTEXTS and recompute
197+
# the rollup so a single permanently-red status (e.g. cla-bot on
198+
# external-contributor backport branches) doesn't block release builds.
164199
status_response = gh_api(f"/repos/{repo}/commits/{commit_hash}/status")
165200
if not status_response:
166-
status_response = {"state": "unknown", "total_count": 0}
167-
168-
status = status_response.get("state", "unknown")
169-
status_count = status_response.get("total_count", 0)
201+
status_response = {"state": "unknown", "total_count": 0, "statuses": []}
202+
203+
raw_statuses = status_response.get("statuses", []) or []
204+
ignored_status_contexts = [
205+
s.get("context") for s in raw_statuses
206+
if s.get("context") in IGNORED_STATUS_CONTEXTS
207+
]
208+
filtered_statuses = [
209+
s for s in raw_statuses
210+
if s.get("context") not in IGNORED_STATUS_CONTEXTS
211+
]
212+
if ignored_status_contexts:
213+
status, status_count = _rollup_status(filtered_statuses)
214+
print(
215+
f"Status API: ignoring contexts {sorted(set(ignored_status_contexts))} "
216+
f"→ rollup state={status}, count={status_count}",
217+
file=sys.stderr,
218+
)
219+
else:
220+
status = status_response.get("state", "unknown")
221+
status_count = status_response.get("total_count", 0)
222+
print(f"Status API: state={status}, count={status_count}", file=sys.stderr)
170223

171-
# Check check-runs API
224+
# Check check-runs API. Same filter for IGNORED_CHECK_NAMES.
172225
check_response = gh_api(f"/repos/{repo}/commits/{commit_hash}/check-runs")
173226
if not check_response:
174227
check_response = {"total_count": 0, "check_runs": []}
175228

176-
check_count = check_response.get("total_count", 0)
177-
check_runs = check_response.get("check_runs", [])
229+
raw_check_runs = check_response.get("check_runs", []) or []
230+
ignored_check_names = [
231+
cr.get("name") for cr in raw_check_runs
232+
if cr.get("name") in IGNORED_CHECK_NAMES
233+
]
234+
check_runs = [
235+
cr for cr in raw_check_runs
236+
if cr.get("name") not in IGNORED_CHECK_NAMES
237+
]
238+
check_count = len(check_runs)
178239

179240
# Determine check conclusion
180241
if check_count == 0:
@@ -188,8 +249,14 @@ def check_ci_status(commit_hash: str, repo: str = "e2b-dev/firecracker") -> tupl
188249
else:
189250
check_conclusion = "unknown"
190251

191-
print(f"Status API: state={status}, count={status_count}", file=sys.stderr)
192-
print(f"Check-runs API: conclusion={check_conclusion}, count={check_count}", file=sys.stderr)
252+
if ignored_check_names:
253+
print(
254+
f"Check-runs API: ignoring {sorted(set(ignored_check_names))} "
255+
f"→ conclusion={check_conclusion}, count={check_count}",
256+
file=sys.stderr,
257+
)
258+
else:
259+
print(f"Check-runs API: conclusion={check_conclusion}, count={check_count}", file=sys.stderr)
193260

194261
if status == "failure" or check_conclusion == "failure":
195262
return False, f"CI failed for commit {commit_hash} - refusing to build"

0 commit comments

Comments
 (0)