Skip to content

Commit 79ed60a

Browse files
authored
ci(fix): fixing cyclic dependency in check ci status (#151)
Signed-off-by: Nelesh Singla <117123879+nsingla@users.noreply.github.com>
1 parent 7455b34 commit 79ed60a

3 files changed

Lines changed: 103 additions & 7 deletions

File tree

.github/scripts/ci_checks/ci_checks.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,22 @@ def get_check_runs(self, repo: str, head_sha: str) -> dict:
4242
return json.loads(result.stdout)
4343

4444
def get_own_check_run_id(self, repo: str, head_sha: str, check_name: str) -> int:
45-
"""Return the ID of the check run matching *check_name*."""
45+
"""Return the ID of the check run matching *check_name*.
46+
47+
Prefers an in-progress instance over completed ones to avoid
48+
misidentifying a stale run when multiple runs share the same name.
49+
Falls back to the first completed run if none is in-progress.
50+
"""
4651
data = self.get_check_runs(repo, head_sha)
52+
fallback: int | None = None
4753
for cr in data.get("check_runs", []):
4854
if cr["name"] == check_name:
49-
return cr["id"]
55+
if cr["status"] == "in_progress":
56+
return cr["id"]
57+
if fallback is None:
58+
fallback = cr["id"]
59+
if fallback is not None:
60+
return fallback
5061
raise ChecksError(f"Check run '{check_name}' not found")
5162

5263

.github/scripts/ci_checks/tests/test_ci_checks.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,17 @@ def get_check_runs(self, repo: str, head_sha: str) -> dict:
7272
return response
7373

7474
def get_own_check_run_id(self, repo: str, head_sha: str, check_name: str) -> int:
75-
"""Return a fixed check run ID by scanning check_runs_responses."""
75+
"""Return a fixed check run ID, preferring the in-progress instance."""
7676
if self._check_runs_responses:
77+
fallback: int | None = None
7778
for cr in self._check_runs_responses[0].get("check_runs", []):
7879
if cr["name"] == check_name:
79-
return cr["id"]
80+
if cr["status"] == "in_progress":
81+
return cr["id"]
82+
if fallback is None:
83+
fallback = cr["id"]
84+
if fallback is not None:
85+
return fallback
8086
raise ChecksError(f"Check run '{check_name}' not found")
8187

8288

@@ -389,6 +395,69 @@ def test_empty_check_runs_retries_then_fails(self):
389395
wait_for_checks(gh, "owner/repo", "abc123", check_run_id=999, delay=0, retries=2, interval=5)
390396
assert gh.get_check_runs.call_count == 2
391397

398+
def test_stale_completed_run_with_same_name_excluded_by_ignore(self):
399+
"""A stale completed check_ci_status run is excluded via ignore_checks."""
400+
gh = MagicMock(spec=GhClient)
401+
gh.get_check_runs.return_value = json.loads(
402+
_api_response(
403+
_make_check_run(800, "check_ci_status", "completed", "failure"),
404+
_make_check_run(999, "check_ci_status", "in_progress"),
405+
_make_check_run(100, "lint", "completed", "success"),
406+
)
407+
)
408+
wait_for_checks(
409+
gh,
410+
"owner/repo",
411+
"abc123",
412+
check_run_id=999,
413+
delay=0,
414+
retries=1,
415+
interval=0,
416+
ignore_checks=frozenset({"check_ci_status"}),
417+
)
418+
419+
def test_stale_failed_run_causes_false_failure_without_ignore(self):
420+
"""Without ignore_checks, a stale failed run with the same name causes failure."""
421+
gh = MagicMock(spec=GhClient)
422+
gh.get_check_runs.return_value = json.loads(
423+
_api_response(
424+
_make_check_run(800, "check_ci_status", "completed", "failure"),
425+
_make_check_run(999, "check_ci_status", "in_progress"),
426+
_make_check_run(100, "lint", "completed", "success"),
427+
)
428+
)
429+
with pytest.raises(ChecksError, match="check_ci_status"):
430+
wait_for_checks(
431+
gh,
432+
"owner/repo",
433+
"abc123",
434+
check_run_id=999,
435+
delay=0,
436+
retries=1,
437+
interval=0,
438+
)
439+
440+
def test_concurrent_in_progress_run_excluded_by_ignore(self):
441+
"""Two concurrent in-progress runs with same name don't deadlock when using ignore_checks."""
442+
gh = MagicMock(spec=GhClient)
443+
gh.get_check_runs.return_value = json.loads(
444+
_api_response(
445+
_make_check_run(998, "check_ci_status", "in_progress"),
446+
_make_check_run(999, "check_ci_status", "in_progress"),
447+
_make_check_run(100, "lint", "completed", "success"),
448+
)
449+
)
450+
wait_for_checks(
451+
gh,
452+
"owner/repo",
453+
"abc123",
454+
check_run_id=999,
455+
delay=0,
456+
retries=1,
457+
interval=0,
458+
ignore_checks=frozenset({"check_ci_status"}),
459+
)
460+
392461
def test_many_check_runs_all_evaluated(self):
393462
"""All check runs are evaluated, not just the first page worth."""
394463
gh = MagicMock(spec=GhClient)
@@ -446,14 +515,25 @@ def test_get_own_check_run_id_finds_matching_check(self, mock_run):
446515
assert client.get_own_check_run_id("owner/repo", "abc123", "check_ci_status") == 200
447516

448517
@patch("ci_checks.ci_checks.subprocess.run")
449-
def test_get_own_check_run_id_returns_first_match(self, mock_run):
450-
"""get_own_check_run_id returns the first matching ID when duplicates exist."""
518+
def test_get_own_check_run_id_prefers_in_progress(self, mock_run):
519+
"""get_own_check_run_id prefers the in-progress run over a completed one."""
451520
response = _api_response(
452521
_make_check_run(200, "check_ci_status", "completed", "success"),
453522
_make_check_run(300, "check_ci_status", "in_progress"),
454523
)
455524
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=response)
456525
client = GhClient()
526+
assert client.get_own_check_run_id("owner/repo", "abc123", "check_ci_status") == 300
527+
528+
@patch("ci_checks.ci_checks.subprocess.run")
529+
def test_get_own_check_run_id_falls_back_to_completed(self, mock_run):
530+
"""get_own_check_run_id falls back to completed run when none is in-progress."""
531+
response = _api_response(
532+
_make_check_run(200, "check_ci_status", "completed", "success"),
533+
_make_check_run(300, "check_ci_status", "completed", "success"),
534+
)
535+
mock_run.return_value = subprocess.CompletedProcess(args=[], returncode=0, stdout=response)
536+
client = GhClient()
457537
assert client.get_own_check_run_id("owner/repo", "abc123", "check_ci_status") == 200
458538

459539
@patch("ci_checks.ci_checks.subprocess.run")

.github/workflows/ci-checks.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,13 @@ on:
66
pull_request_target:
77
types: [opened, synchronize, reopened, labeled]
88

9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.label.name || 'ci' }}
11+
cancel-in-progress: true
12+
913
jobs:
1014
check_ci_status:
15+
if: github.event.action != 'labeled' || github.event.label.name == 'ok-to-test'
1116
runs-on: ubuntu-24.04
1217
timeout-minutes: 10
1318
permissions:
@@ -39,7 +44,7 @@ jobs:
3944
--author-login "$AUTHOR_LOGIN" \
4045
--head-sha "$HEAD_SHA" \
4146
--check-name "check_ci_status" \
42-
--ignore-checks "Agent" \
47+
--ignore-checks "Agent,check_ci_status" \
4348
--delay 5 \
4449
--retries 10 \
4550
--polling-interval 5 \

0 commit comments

Comments
 (0)