99 (a "phantom" group that cannot start any backend, e.g. the old
1010 mysql91 / mysql92 stubs, gr and non-gr)
1111
12- B. Does a group that DOES have a real dbdeployer infra lack a GitHub
13- Actions caller workflow (.github/workflows/CI-<group>.yml), i.e. it
14- can run locally via run-tests-isolated.bash but never runs in CI?
12+ B. Can any GitHub Actions workflow actually select the group, i.e. does
13+ it run in CI at all -- or only locally via run-tests-isolated.bash?
14+
15+ A group counts as wired if a CI-<group>.yml caller exists, OR its name
16+ appears in any workflow on this branch or on origin/GH-Actions (where
17+ the reusable half of every pair lives), OR it belongs to a dynamically
18+ discovered family. Groups with no infras.lst are checked too: needing no
19+ backend does not mean needing no workflow. Checking only for a
20+ CI-<group>.yml filename on this branch is what let no-infra-g1 sit
21+ unwired since it was created.
1522
1623Group -> infra resolution mirrors ensure-infras.bash:
1724 BASE_GROUP = <group> with a trailing -g<N>/_g<N> stripped; infra names
2229
2330Severity: WARN-ONLY by default -- this never reds CI on its own. Known,
2431tracked coverage gaps live in ALLOWLIST_NO_WORKFLOW so only *newly*
25- introduced infra-backed groups without a workflow are called out as NEW.
32+ introduced groups without a workflow are called out as NEW.
2633Pass --strict to turn phantom-infra and NEW missing-workflow findings into
2734a non-zero exit (for a future opt-in enforcement step).
2835
3441import json
3542import os
3643import re
44+ import subprocess
3745import sys
3846
3947SCRIPT_DIR = os .path .dirname (os .path .abspath (__file__ ))
5563 "mysql95-binlog" ,
5664 "mysqlx-soak" ,
5765 "pgsql17-repl" ,
66+ # --- pre-existing debt surfaced when the check was widened -------------
67+ # These were invisible while the linter only inspected infra-backed groups
68+ # and only looked for a CI-<group>.yml filename. They are unwired on both
69+ # branches, i.e. tests registered in them do not run in CI. Allowlisted so
70+ # the linter reports only genuinely NEW gaps; trim as workflows land.
71+ "mysql-auto_increment_delay_multiplex=0" ,
72+ "mysql-multiplexing=false" ,
73+ "mysql-query_digests=0" ,
74+ "mysql-query_digests_keep_comment=1" ,
75+ "mysql91-gr" ,
76+ "mysql92-gr" ,
77+ "mysqlx-e2e" ,
78+ "pgsql-repl" ,
79+ "todo" ,
80+ # NOTE: 'no-infra' is deliberately NOT allowlisted. It is a real, currently
81+ # unwired group (5 tests, incl. reg_test_5363_admin_monitor_caching_sha2-t)
82+ # and finding it is what prompted widening this check. Add CI-no-infra-g1.yml
83+ # (+ ci-no-infra-g1.yml@GH-Actions) rather than silencing it here.
5884}
5985
6086
@@ -90,28 +116,111 @@ def read_infra_names(group):
90116 return None
91117
92118
93- def workflow_exists (group ):
119+ # Group families whose CI wiring is generated at run time rather than written
120+ # out as a CI-<group>.yml, so a static name lookup cannot see them.
121+ # cluster_sim_* -> CI-cluster-simulator.yml builds its matrix from
122+ # `test/infra/control/cluster-simulator-ci.bash discover`,
123+ # which selects every groups.json entry starting with this
124+ # prefix. Adding such a group wires it up automatically.
125+ DYNAMIC_DISCOVERY_PREFIXES = ("cluster_sim_" ,)
126+
127+
128+ def _local_workflow_blob ():
129+ """Filenames + contents of .github/workflows on the current branch."""
130+ parts = []
131+ wf_dir = os .path .join (REPO_ROOT , ".github" , "workflows" )
132+ for root , _dirs , files in os .walk (wf_dir ):
133+ for name in files :
134+ parts .append (name )
135+ try :
136+ with open (os .path .join (root , name ), encoding = "utf-8" , errors = "replace" ) as f :
137+ parts .append (f .read ())
138+ except OSError :
139+ pass
140+ return "\n " .join (parts )
141+
142+
143+ def _gh_actions_workflow_blob ():
144+ """Filenames + contents of .github/workflows on origin/GH-Actions.
145+
146+ Returns None when the ref is unavailable (shallow clone, no remote, no git).
147+ The caller must then skip the workflow check rather than report false gaps:
148+ the reusable half of every workflow pair lives on that branch, so without it
149+ we cannot tell a genuinely unwired group from one wired only over there.
150+ """
151+ try :
152+ listing = subprocess .run (
153+ ["git" , "ls-tree" , "-r" , "--name-only" , "origin/GH-Actions" , ".github/workflows" ],
154+ cwd = REPO_ROOT , capture_output = True , text = True , timeout = 60 , check = False ,
155+ )
156+ if listing .returncode != 0 :
157+ return None
158+ files = listing .stdout .split ()
159+ if not files :
160+ return None
161+ parts = ["\n " .join (files )]
162+ for path in files :
163+ blob = subprocess .run (
164+ ["git" , "show" , f"origin/GH-Actions:{ path } " ],
165+ cwd = REPO_ROOT , capture_output = True , text = True , timeout = 60 , check = False ,
166+ )
167+ if blob .returncode == 0 :
168+ parts .append (blob .stdout )
169+ return "\n " .join (parts )
170+ except (OSError , subprocess .SubprocessError ):
171+ return None
172+
173+
174+ def workflow_covers (group , local_blob , gh_blob ):
175+ """True when this group can actually be selected by some CI workflow.
176+
177+ A group is wired up if ANY of these hold:
178+ 1. a caller file is named after it (.github/workflows/CI-<group>.yml)
179+ 2. its name appears anywhere in a workflow on this branch -- covers
180+ groups selected by an env/matrix entry rather than a dedicated file,
181+ e.g. 'TAP_GROUP: mysqlx-tsan-g1' inside a larger workflow
182+ 3. its name appears in a workflow on origin/GH-Actions (the reusable half)
183+ 4. it belongs to a family discovered dynamically (see
184+ DYNAMIC_DISCOVERY_PREFIXES)
185+
186+ Checking only (1) is what let no-infra-g1 go unnoticed in both directions:
187+ it has no dedicated caller, and matching names in file *contents* is needed
188+ to avoid flagging the matrix-driven groups that are genuinely wired.
189+ """
190+ if group .startswith (DYNAMIC_DISCOVERY_PREFIXES ):
191+ return True
94192 for ext in ("yml" , "yaml" ):
95- path = os .path .join (REPO_ROOT , ".github" , "workflows" , f"CI-{ group } .{ ext } " )
96- if os .path .isfile (path ):
193+ if os .path .isfile (os .path .join (REPO_ROOT , ".github" , "workflows" , f"CI-{ group } .{ ext } " )):
97194 return True
195+ if group in local_blob :
196+ return True
197+ if gh_blob is not None and group in gh_blob :
198+ return True
98199 return False
99200
100201
101- def classify_group (group ):
202+ def classify_group (group , local_blob , gh_blob , check_workflows = True ):
102203 """Return (missing_infras, workflow_state) for one group.
103204
104205 missing_infras: list of infra names referenced but absent on disk.
105- workflow_state: None if not infra-backed or a workflow exists;
106- "new" / "known" when a real infra has no workflow.
206+ workflow_state: None when a workflow covers the group, the group is a
207+ phantom, or workflow checking is disabled;
208+ "new" / "known" when nothing in CI can select the group.
209+
210+ NOTE: an absent or empty infras.lst does NOT exempt a group. Such a group
211+ still needs a workflow to ever run in CI -- it simply needs no backend.
212+ Exempting them is exactly why no-infra-g1 was never reported despite having
213+ no CI wiring on either branch since it was created.
107214 """
108215 concrete = read_infra_names (group ) or []
109216 missing = [
110217 n for n in concrete
111218 if not os .path .isdir (os .path .join (REPO_ROOT , "test" , "infra" , n ))
112219 ]
113220 workflow_state = None
114- if concrete and not missing and not workflow_exists (group ):
221+ # A phantom group cannot start its backend, so demanding a workflow for it
222+ # would just be noise on top of the phantom finding.
223+ if check_workflows and not missing and not workflow_covers (group , local_blob , gh_blob ):
115224 workflow_state = "known" if base_group (group ) in ALLOWLIST_NO_WORKFLOW else "new"
116225 return missing , workflow_state
117226
@@ -139,11 +248,21 @@ def lint_coverage(groups_path, strict=False):
139248 with open (groups_path , encoding = "utf-8" ) as f :
140249 data = json .load (f )
141250
251+ local_blob = _local_workflow_blob ()
252+ gh_blob = _gh_actions_workflow_blob ()
253+ check_workflows = gh_blob is not None
254+ if not check_workflows :
255+ print ("NOTE origin/GH-Actions is not available (shallow clone or missing "
256+ "remote); skipping the missing-workflow check. The reusable half of "
257+ "every workflow pair lives on that branch, so without it any finding "
258+ "would be a guess. Run `git fetch origin GH-Actions` to enable it." ,
259+ file = sys .stderr )
260+
142261 phantom = [] # (group, missing_infra)
143262 missing_wf_new = []
144263 missing_wf_known = []
145264 for group in sorted (collect_groups (data )):
146- missing , wf_state = classify_group (group )
265+ missing , wf_state = classify_group (group , local_blob , gh_blob , check_workflows )
147266 phantom .extend ((group , infra ) for infra in missing )
148267 if wf_state == "new" :
149268 missing_wf_new .append (group )
@@ -154,14 +273,15 @@ def lint_coverage(groups_path, strict=False):
154273 print (f"WARN [phantom-infra] group '{ group } ': infras.lst references "
155274 f"missing infra 'test/infra/{ infra } '" , file = sys .stderr )
156275 for group in missing_wf_new :
157- print (f"WARN [missing-workflow:NEW] group '{ group } ' has a dbdeployer "
158- f"infra but no .github/workflows/CI-{ group } .yml -- it will never "
159- f"run in GitHub Actions. Add the caller (+ ci-{ group } .yml@GH-Actions) "
160- f"or, if intentional, add '{ base_group (group )} ' to ALLOWLIST_NO_WORKFLOW." ,
276+ print (f"WARN [missing-workflow:NEW] group '{ group } ' is not selectable by "
277+ f"any workflow on this branch or on origin/GH-Actions -- tests "
278+ f"registered in it never run in CI. Add the caller "
279+ f".github/workflows/CI-{ group } .yml (+ ci-{ group } .yml@GH-Actions), or "
280+ f"if intentional add '{ base_group (group )} ' to ALLOWLIST_NO_WORKFLOW." ,
161281 file = sys .stderr )
162282 for group in missing_wf_known :
163- print (f"note [missing-workflow:known] group '{ group } ' has infra but no "
164- f"workflow (allowlisted family '{ base_group (group )} ')" )
283+ print (f"note [missing-workflow:known] group '{ group } ' has no workflow "
284+ f"(allowlisted family '{ base_group (group )} ')" )
165285
166286 print (
167287 f"\n group coverage lint: { len (collect_groups (data ))} groups | "
0 commit comments