From 62f5439da88d3dd2ca95788a48c8132b6a7d970f Mon Sep 17 00:00:00 2001 From: Jiangzhou He Date: Thu, 22 Oct 2026 13:44:43 -0700 Subject: [PATCH 1/3] check_ci_conformance: flag required checks from tier-skipped matrix jobs A matrix job skipped by the fast tier reports one check under its unexpanded name, so a required `name (value)` check from it never arrives and every pull request waits. cocoindex-code-plus requires two such checks. Co-Authored-By: Claude Opus 5.5 --- CI.md | 5 +- .../apply_rulesets.cpython-314.pyc | Bin 8788 -> 8788 bytes .../check_ci_conformance.cpython-314.pyc | Bin 14978 -> 15598 bytes ...pply_rulesets.cpython-314-pytest-9.1.1.pyc | Bin 4711 -> 4711 bytes ...i_conformance.cpython-314-pytest-9.1.1.pyc | Bin 18445 -> 21217 bytes scripts/check_ci_conformance.py | 44 +++++++++++------- scripts/test_check_ci_conformance.py | 19 +++++++- 7 files changed, 49 insertions(+), 19 deletions(-) diff --git a/CI.md b/CI.md index ca7d447..409fc3b 100644 --- a/CI.md +++ b/CI.md @@ -57,7 +57,10 @@ workflow has all of them. Every other workflow that runs on pull requests has parts 2 and 3, the `ready_for_review` type, and the `stacked` type when it filters on branches. Every workflow that reports a required check triggers on `merge_group`, or -the queue waits for that check until it times out. +the queue waits for that check until it times out. Require `ci-gate`, plus +checks from jobs that run in every tier, such as `fast-check`. A matrix job +that the tier skips reports one check under its unexpanded name, so a +required `name (value)` check from it never arrives. ## The merge queue diff --git a/scripts/__pycache__/apply_rulesets.cpython-314.pyc b/scripts/__pycache__/apply_rulesets.cpython-314.pyc index 6421b77d1fe8c188427d9ab032c1154b2ccb8102..493dbb2ed1e7c99d846d7ab6b50be36db99c175e 100644 GIT binary patch delta 19 ZcmccOa>a#9n~#@^0SNR8H*$F?0RS?51W^D0 delta 19 ZcmccOa>a#9n~#@^0SIOkY~=D%0su7g1iAnK diff --git a/scripts/__pycache__/check_ci_conformance.cpython-314.pyc b/scripts/__pycache__/check_ci_conformance.cpython-314.pyc index f31f7bf324ebbcc3e1851e651c09fe51fab2bb6c..4dccc298ceb88e2ab5f3c2d4195b27f7450349ea 100644 GIT binary patch delta 2684 zcmZuzZERE589v9?-yhf^&X@Dyn>bByo0%pi3_}RCkiZr~5;wkqVepK7otQfI9qC(6fqk+7HlvEJ&4B7O6rN{lT;!n^b9zJaBu5sv@=@m8LCOKvB18+Iy~T zO0^wJ&w20b^YMPa=W_0Eb30zzVKXBb@8^^YjcvDggsQQ3Ena<4KQs+oasa?vL#&K; zG58LHHU>igig79`M<%0Lk(^+>VaBtcC((3lDw0U1fG@^Li2k)9fsfPITzH-C6V^{k;NX02V z<5MCp5iyaRS^lFI@ zXyRcEoh+r2S(vgUDS0&|RhgDDld_|O)Us7OBkJ*ybaZ%HjFc`WKZSo({r3W~fc|W) zTJ~J;yw=}^aEDg7woX^6MG82&5)rp;(3~nSh*pxpqQJTY|%Kl%YGFNfTs6J3) z0r`-m5s(87;6W`y5+`5>3TZ>SkUppNp`2zg0bH%19mTLu9rS|EN%JlKAy91w&LBdV zIZ7Q9!k)4N4`9jAS>{f{eNAWxyd-d=hBEWHNifhuRdv07geC;P<|H??4M88y+(Z&iO=BXIjQRutrVxIJY#-Dv1%2!+)YVOJf%W88O9 zyov2|qoB492}b(Ewpu=35)RDLf`Yo?Dt2*DKWuBQEc&R;MMtU(h6f;i`NH|ig$BV` z3crdr-~0lRjA537>VPj(kKtR@88NyGCWK!fONhT-p zGtp!=nV$5i=}&*-zvO%T%%vb6a|<{CZ~YdCHZaiw8b^ad*~L2r4gJ2Z1|xdg;ljJ= z?G|rsyrpbQO&CmA8l`3P&%97kHUNLmq6Hmz3ey6#aA* zQf*eTW^!36mz8}9P>7s{%_nbZ)SOha^A<-~ZYemT)*P8#-@)^0I+BYBH6Z21ioMFo zDUr*8molHg=ks}fQ0T{bBaUVc3($p^r# zjF7qf`~^{=WlcvIh4f zwS?`0_*+0<*sB(fKdf%NYS`4H`dv?TI*0jDUFS0QDew8E;ih3#{|kGO@6GeQAM_O6 zee3SN2k!oBmSz1zSJ$dga0T*?z_PyJ2s|+$n`=Xl4&!6EcxY_>&{#frs(9$M%D*Zs z4nK1DZ|ab3-t}2s=Y|fMZ2v%T*R~ZooS#~qOCvuXS;?$U-Z}#z5jcSXn@`{;2-}A7 zBdc>;kF54Z|7JN{wm1lzCI6EDC*NdkBW(4+CadR*=a0}(}x22||4)gmR z0Qc&g3}3bY(tqn;rE@R53O)Mn3v--~+=ivJ-Q{JD_vq&Tn`i{SQEwE=NwtUXM zzhv+h6?%^!USuST^ino3(-4pqQXnO!32PMOLk7$$WR3ly$w2&z=}SqHNs~PNytm~j zJ5({np%TZk88SzXvu`tdN3mCvD68r7^{Wfs4pW(#w3=Cpp;CnU_V$=}K=7+pe|K$m z??18qSfQ!!f%V`fhw8`h6FptocT?C_5069=xonPzkqBXhjyz@X2?H&hLH@<4-vcO| zJSQvq?3@H;o@g1H^=0xO2I`woReqf=1?p>5W3Wexs%c5*9rSKs7k1LW1SV_PbM2`a U9u!Y~)c{=(4jU(FbbsrA0gGy*fB*mh delta 2201 zcmZ8jZERCj7(S=Bx9ir9j(yPXi?&Rm-A9qFW9SAB2nb9D`zj`kb+xikQ+)p?2j*>chBz3Ta2|PQ?Uy?qs z6u&FO@3Ny2HNI1gvt9h0%k7pyr{bMS@_?9V;S9owo@tyeXTvXk+f!Izf(-&8;;f6} zP9ISy?-5=6^+$4e(CsbiA!LsMZm8%MVW&O<0`C=zxX+T!ujS_R8y0hMr$I3W==QA4 zmid`C=i2S)T0y)1= z_IXXL0%W!i!H2L3mqb;GM&jCt2r-K&UQrLIX3kv04&csL5%44x1F+NugDnC{E6^@6 z(N=h@yWw*11PL&opTc}H!BZ6}Opa!%%)t)FmsFSu_ys;3;a z&P$&sz?gPwu_irw>9H_|ra7SiV=q>MO8 zK&+$f)J~%2fRSI%t0*>XEyNSsg{^t+|E``{Qo?QdStA<==@45DEt}JKCJc&L0R}rs zLQn+75J}6RjUe;Z{fl`_+MS3%{kg(&EG`Ylk$0iED<->!BsL%g2bi2tTp1qD5m5F0 zJRbxInej1zIzs5|Gaxk4CE!GobdrtW(C&qebQx%I^yQ}=>@YxW6^;aGCVayrDMEd? z)7H0>3Q{jbD1#`Gis*+tl? zvsVJ)_aynLyk^q2eZ0KpSa`Bw%hz?c8n#__zd^?vwvAHICO3`Sw*O#vPFgxghmX0> ztM90j=)^>2$9eU<`u6;rQ~4Q+D6m+ zwsq9_Ss}m|`K5@prgfD6XI%3AY;>}{0_=nXFD+WO` zvP4v3DgIrxN4yS-{i(mJKc}nS1W_#j!0!Ag&7DCw6xOG7^}*Wwuz&=arL(W5W__<2 zX*b_rw^OW!nzQh`we`-zfaM8;m9!Vt3jVp$$q)$0 zV*ateo|f=m{JXfvmiuH6bl~pQnTy-YG9NjicSCP3{}q?KRI~I+Y8bj^JR--yM_G<* z^hq|rN9zwzjTfzqn(%#Nz5L+H75r31BK6720&2O5y4wgZ^P3HunvbCPB0>cMMvq}h zXhKwqF??9rB?Jtd&IQdNt6G*6dm+N)82gIvTD7^3{Pac1{!8Mu#X&^+R-) z(DE4ljnG_~&IrWOM(>%qvGH8hw0>VO*q?w$O9}=V)*!o!@Fl`u2=@@K^0CI!)elhA z{~hR_e~mwNSJCtI^VEov9vr=>unN*KFmX?lhgx}C(?AitT@ HH@W`-QjICt diff --git a/scripts/__pycache__/test_apply_rulesets.cpython-314-pytest-9.1.1.pyc b/scripts/__pycache__/test_apply_rulesets.cpython-314-pytest-9.1.1.pyc index 36ff4d33630d7a949f1f06463df173ff43bb231c..642c181a74ab3d4af5b8b7c8dfdea32bb8e55abd 100644 GIT binary patch delta 19 ZcmaE^@?3>Wn~#@^0SNR8H*&=a0RS-m1Udi! delta 19 ZcmaE^@?3>Wn~#@^0SIOkY~+d+0su301fu`| diff --git a/scripts/__pycache__/test_check_ci_conformance.cpython-314-pytest-9.1.1.pyc b/scripts/__pycache__/test_check_ci_conformance.cpython-314-pytest-9.1.1.pyc index afea302daa12860b8ab84a8b30d0eef8593edba2..95d3b17675db5d4e903e55be78b687970894ed02 100644 GIT binary patch delta 3058 zcmaKuS!^3s6ozN)@g8qYvvfEZ_g0bM86! z-nsUCGjF^}-+zzx?DV*u6ns9&70%!DyydBBki6 z=SQ@j_+Z5cxUBJUfnkRBSBg=7>Wt>Y6ze}pi}ZlS$T;H*(`_cr6HQ@CG!NK}G0xOR z>m$q{oqdtB>^n|T*^%sppq`R~6}M6>oqctX+O^rJgPeKJu^-&*`=X7r8QDEHdh`Ys zD09}pW_AN-jni!PnVz_*jnQgZ6Kfu*dfV2ZMEupXU0I4ol}A=DSm)dg%Ae61o0(-; zi;?11P9Czt;G^A*N_dmQXDpPe!&L^bN*%DjK(iLsss-GPaaF8MizF;YB*N^nmaal{ zv3AzMxgvCVH2dYLno^EoaaG0~jTC1sZe+Z4xf}bPaJe=8_I^j%1v~Xk*^P46rA5v< zj7Y>(zP#vWt5`SZuGrLrbvNZP=4gb|Ft&QCR(b5NJvvAkzk7sQvw)ZNuwKr)b^*~h zu*sO9Y^u#LRn17MN>dPQR{UNl#5-lA7DV5ZspRMCa3S=}p}@-}Xzm{Vau$<^?+J}r6&9nFLy zBU({W>AH-ofC1&cH`yuU+0=qurk+qPx({HEy0~5I{|DwTBh1_LEFGpfstn&W;M-J& zZ`!yWMKj?0FvqC)ekscr@ucC~l+9M)`!&w4;`?3ET7p@RPH+A8R}||egt$e8cv~6M zfYAn_*MoRH11Q!ZUrz9d-J!g4NP~T&0v=-9szbV&VeLAti~O#jwUc$QPR_ZOTiK7+ z1*6M28lbhMcpPhJ?NDiL>$jI(na*bA%3%k?R_UNFqPqgrfN9plRaNX~W!?R_OZzFK zD}uV_nCYpc22Qg+&a-v}(ff2#=e@={Mh~R+>vT3jkF-WgBe8+rWS;dSsS68EA=4f> zJIRw#J~2v!oOsT8UKo{dZb+q=cTY7 z4huP1%*j%CBsVdE6b;HG#|PDHNhV2|ADwOw%m4>TdI}72S{@TJEf;tqrPZwj@w& z%AOE%xi0_r2Fe#)YVKGHb}mYb{4MVhS;D4Vi*&Z!7bME=7bEDtf6hnJ4bJ};6qA15dyD_lscjvti5wmNaZH@1LR*Az}H|Y Vugo30;QyxyU%8(&Agk5<{{VYs#B~4w delta 872 zcmY+C&rcIU6vy{%cbC#_mtSHb{Ak4(x&o!OXbNqypsfT$j1wt|L5YFl56~{NMdTp( z5BP)3g_~SV@P_f?QIC4iszgnRFRZtoAo-|02{P;#^cGfxiI9KtQhst~g68MuFVppB}d(`z1GtUqtpb85s-<#Cr-1$O&OE#BmuH zfMcw-N3@mUA`phTV>u~4BnWe(?RHwoz{Qv#Gz)CR8*CuHoE!t9jrGLD9?7mCB5|>0 zZ;<^Rt^;2(Ujbm4~|JPGx&hJQxb8B%Yko4gX~;#_M)!u=Pj)Dp)|@TWyF5 zFkA_sXNbxVx{9SUcT=9}3udv)I#jCjHi}!K2RX{_okOP`nCnDzA;O4m4mOaDYxFtV z2**Px_p&=#e6F)IB$Ysg&j7+^G~7N%tTm_AR@wm-eFWQ*B#| zzg1&gKJjiW@vyP2Ss%6K z?jLZyvE(%D7QBI0^KIzO?EL9hA>0-9BQQxdjwJOV diff --git a/scripts/check_ci_conformance.py b/scripts/check_ci_conformance.py index db5ce37..05e8561 100644 --- a/scripts/check_ci_conformance.py +++ b/scripts/check_ci_conformance.py @@ -108,19 +108,32 @@ def fail(message: str) -> None: return findings -def check_name_prefixes(doc: dict[Any, Any]) -> list[str]: - """Return the check-name prefixes the workflow's jobs report under.""" - prefixes = [] - for job_id, job in (doc.get("jobs") or {}).items(): - name = _text(job.get("name")) or job_id - prefixes.append(name.split("${{")[0].strip()) - return prefixes - - -def check_required_contexts(contexts: list[str], merge_group_docs: list[dict[Any, Any]]) -> list[str]: - """Return the required contexts that no merge_group workflow reports.""" - prefixes = [p for doc in merge_group_docs for p in check_name_prefixes(doc)] - return [c for c in contexts if not any(p and c.startswith(p) for p in prefixes)] +def _check_name_prefix(job_id: str, job: dict[str, Any]) -> str: + """The prefix of the check names a job reports under.""" + return (_text(job.get("name")) or job_id).split("${{")[0].strip() + + +def required_check_problems(contexts: list[str], merge_group_docs: list[dict[Any, Any]]) -> list[str]: + """Explain each required context that would leave a pull request or merge group waiting.""" + problems = [] + for context in contexts: + jobs = [ + (job_id, job) + for doc in merge_group_docs + for job_id, job in (doc.get("jobs") or {}).items() + if (prefix := _check_name_prefix(job_id, job)) and context.startswith(prefix) + ] + if not jobs: + problems.append(f"Required check {context!r} comes from no workflow that triggers on merge_group, " + "so the merge queue would wait for it forever.") + continue + # A skipped matrix job reports one check under its unexpanded name, so + # a required `name (value)` check never arrives on a fast-tier run. + if all((job.get("strategy") or {}).get("matrix") and "outputs.full" in _text(job.get("if")) + for _, job in jobs): + problems.append(f"Required check {context!r} comes from matrix job {jobs[0][0]}, which the fast tier " + f"skips; a skipped matrix job never reports that name. Require {GATE_JOB} instead.") + return problems def required_contexts(repo: str) -> list[str] | None: @@ -170,9 +183,8 @@ def main() -> int: findings.append(Finding(str(directory), f"Exactly one workflow must define {GATE_JOB}; found {len(gate_files)}.")) if args.repo: contexts = required_contexts(args.repo) - for context in check_required_contexts(contexts or [], merge_group_docs): - findings.append(Finding(str(directory), f"Required check {context!r} comes from no workflow that " - "triggers on merge_group, so the merge queue would wait for it forever.")) + for problem in required_check_problems(contexts or [], merge_group_docs): + findings.append(Finding(str(directory), problem)) annotate = os.environ.get("GITHUB_ACTIONS") == "true" for finding in findings: diff --git a/scripts/test_check_ci_conformance.py b/scripts/test_check_ci_conformance.py index c229f4b..a71721b 100644 --- a/scripts/test_check_ci_conformance.py +++ b/scripts/test_check_ci_conformance.py @@ -4,7 +4,7 @@ import yaml -from check_ci_conformance import check_required_contexts, check_workflow +from check_ci_conformance import check_workflow, required_check_problems TEMPLATE = pathlib.Path(__file__).parent.parent / "workflow-templates" / "ci.yml" @@ -109,4 +109,19 @@ def test_required_contexts_match_job_names_and_matrix_prefixes() -> None: runs-on: x """) contexts = ["ci-gate", "pre-commit (3.11)", "rust-checks (ubuntu-latest)", "fast-check"] - assert check_required_contexts(contexts, [doc]) == ["fast-check"] + problems = required_check_problems(contexts, [doc]) + assert len(problems) == 1 and "'fast-check'" in problems[0] + + +def test_required_check_from_tier_skipped_matrix_job() -> None: + doc = yaml.safe_load(""" +on: [merge_group] +jobs: + pre-commit: + needs: changes + if: needs.changes.outputs.full == 'true' + strategy: {matrix: {python-version: ['3.11']}} + runs-on: x +""") + problems = required_check_problems(["pre-commit (3.11)"], [doc]) + assert len(problems) == 1 and "Require ci-gate instead" in problems[0] From d4121c6616c2dd797ca838f43c2ea14439c493eb Mon Sep 17 00:00:00 2001 From: Jiangzhou He Date: Thu, 22 Oct 2026 14:07:39 -0700 Subject: [PATCH 2/3] Review fixes: status functions, ruleset pagination, untrack __pycache__ - check_ci_conformance: a job whose `if` uses cancelled() or failure() runs when the job it needs is skipped, so it does not inherit the draft skip; only always() was recognized. - apply_rulesets: `gh api --paginate` prints one JSON array per page; `--slurp` wraps them so more than one page parses. - The kit's first commit tracked scripts/__pycache__; untrack it and ignore __pycache__/. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + .../__pycache__/apply_rulesets.cpython-314.pyc | Bin 8788 -> 0 bytes .../check_ci_conformance.cpython-314.pyc | Bin 15598 -> 0 bytes ..._apply_rulesets.cpython-314-pytest-9.1.1.pyc | Bin 4711 -> 0 bytes ..._ci_conformance.cpython-314-pytest-9.1.1.pyc | Bin 21217 -> 0 bytes scripts/apply_rulesets.py | 5 ++++- scripts/check_ci_conformance.py | 6 +++++- scripts/test_check_ci_conformance.py | 6 ++++++ 8 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .gitignore delete mode 100644 scripts/__pycache__/apply_rulesets.cpython-314.pyc delete mode 100644 scripts/__pycache__/check_ci_conformance.cpython-314.pyc delete mode 100644 scripts/__pycache__/test_apply_rulesets.cpython-314-pytest-9.1.1.pyc delete mode 100644 scripts/__pycache__/test_check_ci_conformance.cpython-314-pytest-9.1.1.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/scripts/__pycache__/apply_rulesets.cpython-314.pyc b/scripts/__pycache__/apply_rulesets.cpython-314.pyc deleted file mode 100644 index 493dbb2ed1e7c99d846d7ab6b50be36db99c175e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8788 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 diff --git a/scripts/__pycache__/check_ci_conformance.cpython-314.pyc b/scripts/__pycache__/check_ci_conformance.cpython-314.pyc deleted file mode 100644 index 4dccc298ceb88e2ab5f3c2d4195b27f7450349ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15598 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 aiHGjjlO@dkt bool: def sync_ruleset(org: str, path: pathlib.Path, check: bool) -> bool: want = json.loads(path.read_text()) - existing = {r["name"]: r["id"] for r in gh(f"orgs/{org}/rulesets", "--paginate")} + # `--slurp` wraps the pages in one array; `--paginate` alone prints one + # JSON array per page, which is not one JSON document. + pages = gh(f"orgs/{org}/rulesets", "--paginate", "--slurp") + existing = {r["name"]: r["id"] for page in pages for r in page} if want["name"] in existing: have = gh(f"orgs/{org}/rulesets/{existing[want['name']]}") found = differences(want, have) diff --git a/scripts/check_ci_conformance.py b/scripts/check_ci_conformance.py index 05e8561..2f435df 100644 --- a/scripts/check_ci_conformance.py +++ b/scripts/check_ci_conformance.py @@ -78,7 +78,11 @@ def fail(message: str) -> None: "with cancel-in-progress, must cancel a pull request's superseded runs.") for job_id, job in jobs.items(): condition = _text(job.get("if")) - inherits_skip = bool(job.get("needs")) and "always()" not in condition + # A job that needs a skipped job is skipped too, unless a status + # function in its `if` overrides the implicit success() check. + inherits_skip = bool(job.get("needs")) and not any( + f in condition for f in ("always()", "cancelled()", "failure()") + ) if "pull_request.draft" not in condition and not inherits_skip: fail(f"Job {job_id} must skip drafts: add `github.event.pull_request.draft != true` to its if.") diff --git a/scripts/test_check_ci_conformance.py b/scripts/test_check_ci_conformance.py index a71721b..9ae316c 100644 --- a/scripts/test_check_ci_conformance.py +++ b/scripts/test_check_ci_conformance.py @@ -125,3 +125,9 @@ def test_required_check_from_tier_skipped_matrix_job() -> None: """) problems = required_check_problems(["pre-commit (3.11)"], [doc]) assert len(problems) == 1 and "Require ci-gate instead" in problems[0] + + +def test_status_function_overrides_inherited_draft_skip() -> None: + doc = template() + doc["jobs"]["test"]["if"] = "!cancelled()" + assert any("Job test must skip drafts" in m for m in messages(doc)) From 2380ee15709c0a00dca0707681d9de7a016c8a49 Mon Sep 17 00:00:00 2001 From: Jiangzhou He Date: Thu, 22 Oct 2026 14:18:34 -0700 Subject: [PATCH 3/3] Review fixes: required-workflow triggers, stack diffs, required-check matching - ci-conformance: a ruleset runs a required workflow only on opened, synchronize and reopened and ignores `types`, so a draft marked ready got no new run of the skipped job. Run it on drafts too. - The starter workflow skips the path filter for a stack layer: on a pull request the filter lists the layer's own files, but the top layer's run stands in for the whole stack. CI.md says to diff stack layers against the stack base. - check_ci_conformance: a required check matches a job only by its name, a matrix leg `name (...)` or a reusable call `name / ...` (a `test` job no longer satisfies `test-e2e`); tier skips propagate through `needs`; a skipped reusable-workflow call is flagged like a skipped matrix job; the short form `concurrency: ` no longer crashes the check. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci-conformance.yml | 8 +++- CI.md | 10 +++- scripts/check_ci_conformance.py | 72 +++++++++++++++++++--------- scripts/test_check_ci_conformance.py | 53 ++++++++++++++++++++ workflow-templates/ci.yml | 5 +- 5 files changed, 122 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-conformance.yml b/.github/workflows/ci-conformance.yml index fdd8950..b0df89b 100644 --- a/.github/workflows/ci-conformance.yml +++ b/.github/workflows/ci-conformance.yml @@ -4,9 +4,13 @@ name: CI conformance # requires this workflow on every repository whose ci-tier property is # merge-queue. It runs from this repository, on the target repository's pull # requests and merge groups, and checks the target's workflows against CI.md. +# +# A ruleset runs a required workflow on the default activity types only +# (opened, synchronize, reopened, and the merge group's checks_requested) and +# ignores `types` and other filters. So the job runs on drafts too: a draft +# marked ready gets no new event, and the check is cheap. on: pull_request: - types: [opened, synchronize, reopened, ready_for_review, stacked] merge_group: concurrency: @@ -19,7 +23,7 @@ permissions: jobs: ci-conformance: # This repository has no ci-gate of its own; self-test.yml covers it. - if: github.event.pull_request.draft != true && github.repository != 'cocoindex-io/.github' + if: github.repository != 'cocoindex-io/.github' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/CI.md b/CI.md index 409fc3b..2966c00 100644 --- a/CI.md +++ b/CI.md @@ -49,7 +49,11 @@ workflow has all of them. in its `if`, or needs a job that has it. 4. **A `changes` job** runs `cocoindex-io/.github/actions/ci-mode@v1` (it needs `checks: read`) and the path filter. Expensive jobs run when - `full == 'true'` and the filter output is not `'false'`. + `full == 'true'` and the filter output is not `'false'`. The filter is + skipped for a stack layer, and so is any other diff against the pull + request's base branch: a stack layer's base is the layer below it, but + the top layer's run stands in for the whole stack. Diff a stack layer + against `github.event.pull_request.stack.base.ref`. 5. **A `ci-gate` job** with `if: always()` that needs every other job. It is the one check to require. A skipped job reports success, so a required check that can be skipped by a failure would let the failure merge. @@ -124,6 +128,10 @@ repository's own rulesets. the Change Management Policy asks that the change be announced. - Check a repository locally: `uv run --with pyyaml scripts/check_ci_conformance.py PATH --repo OWNER/NAME`. +- A ruleset runs a required workflow only on `opened`, `synchronize` and + `reopened` pull request events and on merge groups, and ignores the + workflow's `types` and filters. `ci-conformance` therefore runs on drafts + too. ## Changing this kit diff --git a/scripts/check_ci_conformance.py b/scripts/check_ci_conformance.py index 2f435df..dac65e7 100644 --- a/scripts/check_ci_conformance.py +++ b/scripts/check_ci_conformance.py @@ -25,6 +25,9 @@ CI_MODE_ACTION = "cocoindex-io/.github/actions/ci-mode@" GATE_JOB = "ci-gate" GITHUB_ACTIONS_APP_ID = 15368 +# Status functions in a job's `if` replace the implicit success() check, so +# the job runs even when a job it needs was skipped. +STATUS_FUNCTIONS = ("always()", "cancelled()", "failure()") class Finding(NamedTuple): @@ -71,8 +74,11 @@ def fail(message: str) -> None: if (has_gate or branch_filtered) and "stacked" not in types: fail("pull_request.types must list stacked: a stack layer whose opened event arrives before its " "stack exists misses the branch filter and runs no CI.") - group = _text((doc.get("concurrency") or {}).get("group")) - cancel = (doc.get("concurrency") or {}).get("cancel-in-progress") + concurrency = doc.get("concurrency") or {} + if isinstance(concurrency, str): # `concurrency: ` is the short form + concurrency = {"group": concurrency} + group = _text(concurrency.get("group")) + cancel = concurrency.get("cancel-in-progress") if "pull_request.number" not in group or cancel in (None, False): fail("A workflow-level concurrency group keyed on github.event.pull_request.number, " "with cancel-in-progress, must cancel a pull request's superseded runs.") @@ -80,9 +86,7 @@ def fail(message: str) -> None: condition = _text(job.get("if")) # A job that needs a skipped job is skipped too, unless a status # function in its `if` overrides the implicit success() check. - inherits_skip = bool(job.get("needs")) and not any( - f in condition for f in ("always()", "cancelled()", "failure()") - ) + inherits_skip = bool(job.get("needs")) and not any(f in condition for f in STATUS_FUNCTIONS) if "pull_request.draft" not in condition and not inherits_skip: fail(f"Job {job_id} must skip drafts: add `github.event.pull_request.draft != true` to its if.") @@ -112,31 +116,55 @@ def fail(message: str) -> None: return findings -def _check_name_prefix(job_id: str, job: dict[str, Any]) -> str: - """The prefix of the check names a job reports under.""" - return (_text(job.get("name")) or job_id).split("${{")[0].strip() +def _needs(job: dict[str, Any]) -> list[str]: + needs = job.get("needs") or [] + return [needs] if isinstance(needs, str) else list(needs) + + +def _tier_skipped_jobs(jobs: dict[str, Any]) -> set[str]: + """Jobs a fast-tier run skips: those whose `if` reads the tier, and those that need one.""" + skipped = {job_id for job_id, job in jobs.items() if "outputs.full" in _text(job.get("if"))} + grew = True + while grew: + grew = False + for job_id, job in jobs.items(): + if (job_id not in skipped and any(n in skipped for n in _needs(job)) + and not any(f in _text(job.get("if")) for f in STATUS_FUNCTIONS)): + skipped.add(job_id) + grew = True + return skipped + + +def _reports(job_id: str, job: dict[str, Any], context: str) -> bool: + """Whether the job reports `context`: as its name, a matrix leg `name (…)`, or a reusable workflow's `name / …`.""" + name = _text(job.get("name")) or job_id + if "${{" in name: + return context.startswith(name.split("${{")[0]) + return context == name or context.startswith((f"{name} (", f"{name} / ")) def required_check_problems(contexts: list[str], merge_group_docs: list[dict[Any, Any]]) -> list[str]: """Explain each required context that would leave a pull request or merge group waiting.""" problems = [] for context in contexts: - jobs = [ - (job_id, job) - for doc in merge_group_docs - for job_id, job in (doc.get("jobs") or {}).items() - if (prefix := _check_name_prefix(job_id, job)) and context.startswith(prefix) - ] - if not jobs: + producers = [] # (job id, skipped by the fast tier, the name it reports when skipped) + for doc in merge_group_docs: + jobs: dict[str, Any] = doc.get("jobs") or {} + skipped = _tier_skipped_jobs(jobs) + producers += [ + (job_id, job_id in skipped, _text(job.get("name")) or job_id) + for job_id, job in jobs.items() + if _reports(job_id, job, context) + ] + if not producers: problems.append(f"Required check {context!r} comes from no workflow that triggers on merge_group, " "so the merge queue would wait for it forever.") - continue - # A skipped matrix job reports one check under its unexpanded name, so - # a required `name (value)` check never arrives on a fast-tier run. - if all((job.get("strategy") or {}).get("matrix") and "outputs.full" in _text(job.get("if")) - for _, job in jobs): - problems.append(f"Required check {context!r} comes from matrix job {jobs[0][0]}, which the fast tier " - f"skips; a skipped matrix job never reports that name. Require {GATE_JOB} instead.") + # A skipped job reports one check under its own name: a skipped matrix + # job doesn't expand into its legs, and a skipped reusable-workflow + # call reports none of the called workflow's jobs. + elif all(skipped and context != name for _, skipped, name in producers): + problems.append(f"Required check {context!r} comes from job {producers[0][0]}, which the fast tier " + f"skips; a skipped job reports only its own name. Require {GATE_JOB} instead.") return problems diff --git a/scripts/test_check_ci_conformance.py b/scripts/test_check_ci_conformance.py index 9ae316c..0ceb1a6 100644 --- a/scripts/test_check_ci_conformance.py +++ b/scripts/test_check_ci_conformance.py @@ -131,3 +131,56 @@ def test_status_function_overrides_inherited_draft_skip() -> None: doc = template() doc["jobs"]["test"]["if"] = "!cancelled()" assert any("Job test must skip drafts" in m for m in messages(doc)) + + +def test_short_form_concurrency() -> None: + doc = template() + doc["concurrency"] = "ci-${{ github.event.pull_request.number }}" + assert any("cancel-in-progress" in m for m in messages(doc)) + + +def test_required_check_match_is_exact_or_expanded() -> None: + doc = yaml.safe_load(""" +on: [merge_group] +jobs: + test: {runs-on: x} +""") + problems = required_check_problems(["test-e2e"], [doc]) + assert len(problems) == 1 and "no workflow that triggers on merge_group" in problems[0] + + +def test_required_check_from_tier_skipped_reusable_workflow() -> None: + doc = yaml.safe_load(""" +on: [merge_group] +jobs: + changes: {runs-on: x} + build-test: + name: Build & Test + needs: changes + if: needs.changes.outputs.full == 'true' && needs.changes.outputs.build-test != 'false' + uses: ./.github/workflows/_test.yml +""") + problems = required_check_problems(["Build & Test / build-test (ubuntu-latest)", "changes"], [doc]) + assert len(problems) == 1 and "Build & Test / build-test" in problems[0] + + +def test_required_check_from_job_skipped_through_needs() -> None: + doc = yaml.safe_load(""" +on: [merge_group] +jobs: + changes: {runs-on: x} + build: + needs: changes + if: needs.changes.outputs.full == 'true' + runs-on: x + test: + needs: build + strategy: {matrix: {os: [a]}} + runs-on: x + report: + needs: test + if: always() + runs-on: x +""") + problems = required_check_problems(["test (a)", "report", "build"], [doc]) + assert len(problems) == 1 and "'test (a)'" in problems[0] diff --git a/workflow-templates/ci.yml b/workflow-templates/ci.yml index 30f010e..056b824 100644 --- a/workflow-templates/ci.yml +++ b/workflow-templates/ci.yml @@ -49,10 +49,13 @@ jobs: - id: mode uses: cocoindex-io/.github/actions/ci-mode@v1 # A skipped filter leaves its outputs empty, and an empty output runs - # the job: dispatched and scheduled runs test everything. + # the job: dispatched and scheduled runs test everything. So does a + # stack's top layer: on a pull request the filter lists the files the + # layer itself changed, but its run stands in for the whole stack. - id: filter if: >- steps.mode.outputs.full == 'true' + && !github.event.pull_request.stack && contains(fromJSON('["pull_request", "push", "merge_group"]'), github.event_name) uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 with: