Skip to content

Commit e1146e1

Browse files
committed
ci(lint): detect TAP groups that no workflow can select
lint_group_coverage.py already answered "does this group have a CI workflow", but two narrowing assumptions meant it could not see a whole class of gap: 1. classify_group() only considered infra-backed groups: if concrete and not missing and not workflow_exists(group): A group whose infras.lst is empty or absent was exempt by construction. But needing no backend does not mean needing no workflow -- such a group still needs one to ever run in CI. 2. workflow_exists() only looked for a .github/workflows/CI-<group>.yml filename on this branch. That misses groups selected by an env or matrix entry inside a larger workflow (e.g. 'TAP_GROUP: mysqlx-tsan-g1'), and it cannot see the reusable half of a workflow pair, which lives on GH-Actions. Together these hid no-infra-g1: created in ffb610f (2026-03-19), it has an empty infras.lst and no wiring on either branch, so its 5 tests have never run in CI -- and the linter meant to catch exactly this stayed silent. A group is now considered wired if any of: a CI-<group>.yml caller exists; its name appears in a workflow on this branch; its name appears in a workflow on origin/GH-Actions; or it belongs to a dynamically discovered family. cluster_sim_* is the one such family today -- CI-cluster-simulator.yml builds its matrix from `cluster-simulator-ci.bash discover`, which selects every groups.json entry with that prefix, so those groups are wired without any static reference. When origin/GH-Actions is unavailable (shallow clone, no remote), the workflow check is skipped with an explicit NOTE rather than reporting gaps it cannot verify. CI-lint-groups-json.yml now fetches that ref at --depth=1, since actions/checkout fetches a single branch and the check would otherwise skip itself silently. Widening the check surfaced 21 further pre-existing unwired groups. They are allowlisted as tracked debt so the linter reports only genuinely NEW gaps. no-infra-g1 is deliberately NOT allowlisted: it is real and actionable, and is now the single NEW finding. Still warn-only (exit 0); --strict exits 1 for a future enforcement step.
1 parent 89a29ec commit e1146e1

2 files changed

Lines changed: 153 additions & 22 deletions

File tree

.github/workflows/CI-lint-groups-json.yml

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@ jobs:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- uses: actions/checkout@v4
13+
- name: Fetch GH-Actions branch
14+
# The reusable half of every workflow pair lives on GH-Actions, so the
15+
# coverage lint needs that ref to tell an unwired group from one wired
16+
# only over there. actions/checkout fetches a single branch, so without
17+
# this the check finds no ref and skips itself silently. --depth=1 is
18+
# enough: the lint only reads the tree, never history.
19+
run: |
20+
git fetch --no-tags --depth=1 origin \
21+
+refs/heads/GH-Actions:refs/remotes/origin/GH-Actions \
22+
|| echo "::warning::could not fetch GH-Actions; the workflow-coverage check will skip"
1323
- name: Lint groups.json format
1424
run: python3 test/tap/groups/lint_groups_json.py
1525
- name: Check every TAP source is registered in groups.json
1626
run: python3 test/tap/groups/check_groups.py --source
1727
- name: Check group infra/workflow coverage (warn-only)
18-
# Warns when a group references a missing infra (phantom) or has a
19-
# real dbdeployer infra but no GitHub Actions workflow. Known gaps
20-
# are allowlisted; only NEW infra-backed groups without a workflow
21-
# are flagged. Warn-only here (no --strict) so it never reds CI.
28+
# Warns when a group references a missing infra (phantom), or when no
29+
# workflow on either branch can select the group at all -- meaning
30+
# tests registered in it never run in CI. Known gaps are allowlisted;
31+
# only NEW ones are flagged. Warn-only here (no --strict) so it never
32+
# reds CI.
2233
run: python3 test/tap/groups/lint_group_coverage.py

test/tap/groups/lint_group_coverage.py

Lines changed: 138 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,16 @@
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
1623
Group -> infra resolution mirrors ensure-infras.bash:
1724
BASE_GROUP = <group> with a trailing -g<N>/_g<N> stripped; infra names
@@ -22,7 +29,7 @@
2229
2330
Severity: WARN-ONLY by default -- this never reds CI on its own. Known,
2431
tracked 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.
2633
Pass --strict to turn phantom-infra and NEW missing-workflow findings into
2734
a non-zero exit (for a future opt-in enforcement step).
2835
@@ -34,6 +41,7 @@
3441
import json
3542
import os
3643
import re
44+
import subprocess
3745
import sys
3846

3947
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
@@ -55,6 +63,24 @@
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"\ngroup coverage lint: {len(collect_groups(data))} groups | "

0 commit comments

Comments
 (0)