chore: integrate shared quick checks for E2E validation

This commit is contained in:
overtrue
2026-09-05 18:59:07 +08:00
4 changed files with 326 additions and 164 deletions
+116
View File
@@ -0,0 +1,116 @@
# Copyright 2024 RustFS Team
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Quick Checks
description: Run the shared compile-free RustFS quality checks.
runs:
using: composite
steps:
- name: Install quality tools
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2
with:
tool: |
ripgrep@15.2.0
shellcheck@0.11.0
- name: Install actionlint
shell: bash
run: |
actionlint_dir="$(mktemp -d "${RUNNER_TEMP}/actionlint.XXXXXX")"
curl --fail --location --silent --show-error \
--output "$actionlint_dir/actionlint.tar.gz" \
https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz
echo "8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8 $actionlint_dir/actionlint.tar.gz" | sha256sum --check --status
tar -xzf "$actionlint_dir/actionlint.tar.gz" -C "$actionlint_dir" actionlint
rm "$actionlint_dir/actionlint.tar.gz"
echo "$actionlint_dir" >> "$GITHUB_PATH"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check workflow syntax and shell scripts
shell: bash
run: shellcheck --version && actionlint
- name: Check code formatting
shell: bash
run: cargo fmt --all --check
- name: Check unsafe code allowances
shell: bash
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
shell: bash
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
shell: bash
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
shell: bash
run: ./scripts/check_logging_guardrails.sh
- name: Check error other(format!) ratchet
shell: bash
run: ./scripts/check_error_other_format_ratchet.sh
- name: Check tokio io-uring feature guard
shell: bash
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
shell: bash
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
shell: bash
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
shell: bash
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
shell: bash
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
shell: bash
run: ./scripts/check_embedded_secrets.sh
- name: Check test wiring
shell: bash
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/test_e2e_binary.py
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/test_security_workflow.py
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
shell: bash
run: ./scripts/check_no_planning_docs.sh
- name: Check CI paths stay in sync
shell: bash
run: ./scripts/check_ci_paths_sync.sh
- name: Check io_uring lane --lib precondition
shell: bash
run: ./scripts/check_uring_lane_lib_only.sh
+6 -90
View File
@@ -12,24 +12,10 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# Companion to ci.yml for required status checks. # Reports the existing required checks for paths excluded by ci.yml.
# # Mixed PRs can trigger both workflows; their Quick Checks jobs use one shared
# ci.yml skips docs-only pull requests via paths-ignore, but the branch ruleset # action to keep validation coverage aligned. Keep this paths list in sync with
# requires a check named "Test and Lint" — without this workflow a docs-only PR # ci.yml's pull_request.paths-ignore via scripts/check_ci_paths_sync.sh.
# would wait on it forever. This workflow triggers on exactly the paths ci.yml
# ignores and reports success under the same job name. Mixed PRs trigger both
# workflows and the real check still gates: a required check with any failing
# run blocks the merge.
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/troubleshooting-required-status-checks#handling-skipped-but-required-checks
#
# "Quick Checks" is mirrored here ahead of the ruleset change that will make it
# required too (rustfs/backlog#1599). Until that change lands this job is
# inert; mirroring it first is what lets the ruleset change happen without
# stranding docs-only PRs on a check nobody reports.
#
# Keep the paths list below in sync with the pull_request paths-ignore list
# in ci.yml, and keep the quick-checks steps below byte-identical to the
# quick-checks job in ci.yml.
name: Continuous Integration (docs only) name: Continuous Integration (docs only)
@@ -59,19 +45,6 @@ permissions:
contents: read contents: read
jobs: jobs:
# Deliberately NOT a bare `echo`. Once "Quick Checks" becomes a required
# check, ci.yml gates every expensive job behind it, so a mixed PR reports
# two check runs with this name: the real one (45-51s) and this companion.
# GitHub has no written contract for how it picks between same-named
# required check runs ("latest wins" vs "any failure blocks"), so instead of
# relying on ordering we make both runs execute the same commands against
# the same merge ref — their conclusions are then necessarily identical and
# the choice does not matter. Keep these steps byte-identical to the
# quick-checks job in ci.yml (a guard script that asserts this, and the paths
# sync below, is tracked in rustfs/backlog#1603).
#
# For a genuinely docs-only PR this adds no strictness (no code changed, so
# fmt and the guards always pass) and costs ~50s of ubuntu-latest.
quick-checks: quick-checks:
name: Quick Checks name: Quick Checks
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -82,65 +55,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Install ripgrep - name: Run shared quick checks
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2 uses: ./.github/actions/quick-checks
with:
tool: ripgrep@15.2.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt --all --check
- name: Check unsafe code allowances
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
run: ./scripts/check_logging_guardrails.sh
- name: Check tokio io-uring feature guard
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
run: ./scripts/check_embedded_secrets.sh
- name: Check test wiring
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/test_e2e_binary.py
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/test_security_workflow.py
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Check CI paths stay in sync
run: ./scripts/check_ci_paths_sync.sh
- name: Check io_uring lane --lib precondition
run: ./scripts/check_uring_lane_lib_only.sh
test-and-lint: test-and-lint:
name: Test and Lint name: Test and Lint
+3 -68
View File
@@ -100,12 +100,7 @@ jobs:
- name: Typos check with custom config file - name: Typos check with custom config file
uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master
# Fast, compile-free checks that fail early so contributors get feedback in # Fail early with compile-free checks shared with docs-only CI.
# ~1 minute instead of waiting for the full test job.
#
# These steps are mirrored byte-for-byte in ci-docs-only.yml so that a mixed
# PR, which reports two check runs named "Quick Checks", cannot get one red
# and one green. Edit both jobs together.
quick-checks: quick-checks:
name: Quick Checks name: Quick Checks
if: github.event_name != 'pull_request' || github.event.action != 'closed' if: github.event_name != 'pull_request' || github.event.action != 'closed'
@@ -117,68 +112,8 @@ jobs:
with: with:
persist-credentials: false persist-credentials: false
- name: Install ripgrep - name: Run shared quick checks
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2 uses: ./.github/actions/quick-checks
with:
tool: ripgrep@15.2.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt
- name: Check code formatting
run: cargo fmt --all --check
- name: Check unsafe code allowances
run: ./scripts/check_unsafe_code_allowances.sh
- name: Check layered dependencies
run: ./scripts/check_layer_dependencies.sh
- name: Check architecture migration rules
run: ./scripts/check_architecture_migration_rules.sh
- name: Check logging guardrails
run: ./scripts/check_logging_guardrails.sh
- name: Check error other(format!) ratchet
run: ./scripts/check_error_other_format_ratchet.sh
- name: Check tokio io-uring feature guard
run: ./scripts/check_no_tokio_io_uring.sh
- name: Check extension schema boundaries
run: ./scripts/check_extension_schema_boundaries.sh
- name: Check body-cache whitelist guard
run: ./scripts/check_body_cache_whitelist.sh
- name: Check s3s footprint ratchet
run: ./scripts/check_s3s_footprint.sh
- name: Check cryptographic capability wording
run: ./scripts/check_fips_wording.sh
- name: Check no embedded secret material
run: ./scripts/check_embedded_secrets.sh
- name: Check test wiring
run: |
python3 ./scripts/check_test_wiring.py --self-test
python3 ./scripts/test_e2e_binary.py
python3 ./scripts/check_scheduled_validation_freshness.py --self-test
python3 ./scripts/test_security_workflow.py
python3 ./scripts/check_test_wiring.py
- name: Check no planning docs committed
run: ./scripts/check_no_planning_docs.sh
- name: Check CI paths stay in sync
run: ./scripts/check_ci_paths_sync.sh
- name: Check io_uring lane --lib precondition
run: ./scripts/check_uring_lane_lib_only.sh
test-and-lint: test-and-lint:
name: Test and Lint name: Test and Lint
+201 -6
View File
@@ -5,7 +5,9 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import os
import re import re
import subprocess
import sys import sys
import tempfile import tempfile
import tomllib import tomllib
@@ -481,18 +483,20 @@ def yaml_block(lines: list[str], key: str, indent: int) -> list[str] | None:
return lines[start:end] return lines[start:end]
def workflow_step_block(job_lines: list[str], action: str) -> tuple[int, list[str]] | None: def workflow_step_block(
job_lines: list[str], value: str, key: str = "uses", indent: int = 6
) -> tuple[int, list[str]] | None:
uses_index = next( uses_index = next(
( (
index index
for index, line in enumerate(job_lines) for index, line in enumerate(job_lines)
if ( if (
line.split("#", 1)[0].strip() == f"- uses: {action}" line.split("#", 1)[0].strip() == f"- {key}: {value}"
and len(line) - len(line.lstrip()) == 6 and len(line) - len(line.lstrip()) == indent
) )
or ( or (
line.split("#", 1)[0].strip() == f"uses: {action}" line.split("#", 1)[0].strip() == f"{key}: {value}"
and len(line) - len(line.lstrip()) == 8 and len(line) - len(line.lstrip()) == indent + 2
) )
), ),
None, None,
@@ -520,6 +524,67 @@ def workflow_step_block(job_lines: list[str], action: str) -> tuple[int, list[st
return start, job_lines[start:end] return start, job_lines[start:end]
def yaml_scalar_continues(lines: list[str], index: int, indent: int) -> bool:
following = next(
(line for line in lines[index + 1:] if line.strip() and not line.lstrip().startswith("#")), None
)
return following is not None and len(following) - len(following.lstrip()) > indent
def check_quick_checks(root: Path) -> list[str]:
errors: list[str] = []
bypass_key = r'''(?:if|continue-on-error|needs|"if"|"continue-on-error"|"needs"|'if'|'continue-on-error'|'needs')\s*:'''
for name in ("ci.yml", "ci-docs-only.yml"):
relative = f".github/workflows/{name}"
path = root / relative
job = yaml_block(path.read_text().splitlines(), "quick-checks", 2) if path.is_file() else None
if job is None:
errors.append(f"{relative}: missing Quick Checks job")
continue
conditions = [index for index, line in enumerate(job) if re.match(rf"^ {bypass_key}", line)]
expected = ["if: github.event_name != 'pull_request' || github.event.action != 'closed'"] if name == "ci.yml" else []
if [job[index].strip() for index in conditions] != expected or any(
yaml_scalar_continues(job, index, 4) for index in conditions
):
errors.append(f"{relative}: Quick Checks job must not add dependencies, bypass failures, or change its event condition")
checkout = workflow_step_block(job, "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0")
action = workflow_step_block(job, "./.github/actions/quick-checks")
if checkout is None or action is None:
errors.append(f"{relative}: Quick Checks requires checkout and the shared quick-checks action")
continue
if checkout[0] >= action[0]:
errors.append(f"{relative}: checkout must run before shared Quick Checks")
if " persist-credentials: false" not in checkout[1]:
errors.append(f"{relative}: Quick Checks checkout must disable persisted credentials")
for step in (checkout, action):
if any(re.match(rf"^\s+(?:- )?{bypass_key}", line) for line in step[1]):
errors.append(f"{relative}: Quick Checks checkout and shared action must run without bypasses")
relative = ".github/actions/quick-checks/action.yml"
path = root / relative
runs = yaml_block(path.read_text().splitlines(), "runs", 0) if path.is_file() else None
if runs is None or " using: composite" not in runs:
errors.append(f"{relative}: missing composite action")
return errors
steps = yaml_block(runs, "steps", 2) or []
for command in ("shellcheck --version && actionlint", "./scripts/check_error_other_format_ratchet.sh"):
step = workflow_step_block(steps, command, key="run", indent=4)
if step is None:
errors.append(f"{relative}: missing direct execution of {command}")
continue
if " shell: bash" not in step[1] or any(
re.match(rf"^\s+(?:- )?{bypass_key}", line) for line in step[1]
):
errors.append(f"{relative}: {command} must use bash without a condition or continue-on-error")
run_index = next(
index for index, line in enumerate(step[1])
if line.split("#", 1)[0].rstrip() in (f" run: {command}", f" - run: {command}")
)
if yaml_scalar_continues(step[1], run_index, 6):
errors.append(f"{relative}: {command} must remain a single-line run scalar")
return errors
def alert_step_errors( def alert_step_errors(
job_lines: list[str], job_lines: list[str],
expected_action_if: str | None, expected_action_if: str | None,
@@ -820,10 +885,139 @@ def validate(root: Path) -> list[str]:
errors.extend(check_workflow_readiness(root)) errors.extend(check_workflow_readiness(root))
errors.extend(check_profile_definitions(root)) errors.extend(check_profile_definitions(root))
errors.extend(check_scheduled_alerts(root)) errors.extend(check_scheduled_alerts(root))
errors.extend(check_quick_checks(root))
return errors return errors
class SelfTests(unittest.TestCase): class SelfTests(unittest.TestCase):
def test_quick_checks_rejects_caller_and_execution_bypasses(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
caller = (
"jobs:\n quick-checks:\n steps:\n"
" - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0\n"
" with:\n persist-credentials: false\n"
" - uses: ./.github/actions/quick-checks\n"
)
action = (
"runs:\n using: composite\n steps:\n"
" - uses: taiki-e/install-action@pinned\n"
" with:\n tool: actionlint@1.7.12\n"
" - name: Lint workflows\n shell: bash\n run: shellcheck --version && actionlint\n"
" - name: Error format ratchet\n shell: bash\n"
" run: ./scripts/check_error_other_format_ratchet.sh\n"
)
sources = {
".github/workflows/ci.yml": caller.replace(
" steps:", " if: github.event_name != 'pull_request' || github.event.action != 'closed'\n steps:"
),
".github/workflows/ci-docs-only.yml": caller,
".github/actions/quick-checks/action.yml": action,
}
for relative, source in sources.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(source)
self.assertEqual(check_quick_checks(root), [])
for relative in (".github/workflows/ci.yml", ".github/workflows/ci-docs-only.yml"):
source = sources[relative]
mutations = {
"different action": source.replace("./.github/actions/quick-checks", "./.github/actions/other"),
"conditional call": source + " if: false\n",
"ignored call failure": source + " continue-on-error: true\n",
"conditional checkout": source.replace(" with:", " if: false\n with:"),
"ignored job failure": source.replace(" steps:", " continue-on-error: true\n steps:"),
"changed job condition": (
source.replace("github.event_name != 'pull_request' || github.event.action != 'closed'", "false")
if relative.endswith("/ci.yml") else source.replace(" steps:", " if: false\n steps:")
),
"persisted credentials": source.replace("persist-credentials: false", "persist-credentials: true"),
"late checkout": source.replace(" - uses: ./.github/actions/quick-checks\n", "").replace(
" steps:\n", " steps:\n - uses: ./.github/actions/quick-checks\n"
),
"missing job": source.replace(" quick-checks:", " other-checks:"),
}
for key in ("'if' : false", '"if": false', "'continue-on-error': true", '"continue-on-error" : true'):
mutations[f"quoted call {key}"] = source + f" {key}\n"
mutations[f"quoted checkout {key}"] = source.replace(" with:", f" {key}\n with:")
job_source = source.replace(
" if: github.event_name != 'pull_request' || github.event.action != 'closed'\n", ""
) if "if" in key else source
mutations[f"quoted job {key}"] = job_source.replace(" steps:", f" {key}\n steps:")
for dependency in ("needs: prerequisite", "needs: [prerequisite]", "needs:\n - prerequisite", "'needs' : [prerequisite]", '"needs": [prerequisite]'):
for condition in ("false", "true"):
prerequisite = f"\n prerequisite:\n if: {condition}\n runs-on: ubuntu-latest\n steps:\n - run: exit 1\n"
mutations[f"job dependency {dependency} if {condition}"] = source.replace(" steps:", f" {dependency}\n steps:") + prerequisite
if relative.endswith("/ci.yml"):
for separator in ("", "\n", " # continued condition\n"):
mutations[f"continued job condition {separator!r}"] = source.replace(
" steps:", f"{separator} && false\n steps:"
)
for case, mutated in mutations.items():
with self.subTest(path=relative, case=case):
(root / relative).write_text(mutated)
self.assertTrue(check_quick_checks(root))
(root / relative).write_text(source)
relative = ".github/actions/quick-checks/action.yml"
mutations = {
"not composite": action.replace("using: composite", "using: node24"),
"only installed actionlint": action.replace("run: shellcheck --version && actionlint", "run: echo actionlint"),
"missing shellcheck preflight": action.replace("shellcheck --version && ", ""),
"missing ratchet": action.replace("run: ./scripts/check_error_other_format_ratchet.sh", "run: echo skipped"),
"swallowed lint failure": action.replace("&& actionlint", "&& actionlint || true"),
"swallowed ratchet failure": action.replace("ratchet.sh", "ratchet.sh || true"),
"conditional lint": action.replace("run: shellcheck", "if: false\n run: shellcheck"),
"ignored ratchet failure": action.replace("run: ./scripts/", "continue-on-error: true\n run: ./scripts/"),
"non-failing shell": action.replace("shell: bash", "shell: bash {0}"),
"run text in step name": action.replace(
"name: Lint workflows", "name: |\n run: shellcheck --version && actionlint"
).replace("\n run: shellcheck --version && actionlint\n", "\n run: shellcheck --version && actionlint\n || true\n"),
}
for command in ("shellcheck --version && actionlint", "./scripts/check_error_other_format_ratchet.sh"):
for key in ("'if' : false", '"if": false', "'continue-on-error': true", '"continue-on-error" : true'):
mutations[f"quoted {command} {key}"] = action.replace(f"run: {command}", f"{key}\n run: {command}")
for separator in ("", "\n", " # continued command\n"):
mutations[f"continued {command} {separator!r}"] = action.replace(
f"run: {command}\n", f"run: {command}\n{separator} || true\n"
)
for case, mutated in mutations.items():
with self.subTest(case=case):
(root / relative).write_text(mutated)
self.assertTrue(check_quick_checks(root))
(root / relative).unlink()
self.assertTrue(check_quick_checks(root))
def test_quick_checks_commands_propagate_failure(self) -> None:
runs = yaml_block((ROOT / ".github/actions/quick-checks/action.yml").read_text().splitlines(), "runs", 0)
steps = yaml_block(runs or [], "steps", 2) or []
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
(root / "scripts").mkdir()
commands = ("shellcheck", "actionlint", "./scripts/check_error_other_format_ratchet.sh")
for failing in commands:
with self.subTest(command=failing):
run = "shellcheck --version && actionlint" if failing != commands[-1] else failing
step = workflow_step_block(steps, run, key="run", indent=4)
self.assertIsNotNone(step)
run_index = next(index for index, line in enumerate(step[1]) if line.startswith(" run:"))
self.assertFalse(yaml_scalar_continues(step[1], run_index, 6))
body = step[1][run_index].removeprefix(" run: ")
for command in commands:
shim = root / command
shim.write_text(f"#!/bin/sh\nexit {17 if command == failing else 0}\n")
shim.chmod(0o755)
result = subprocess.run(
["bash", "--noprofile", "--norc", "-e", "-o", "pipefail", "-c", body],
cwd=root, env=dict(os.environ, PATH=f"{root}{os.pathsep}{os.environ['PATH']}"),
capture_output=True, text=True,
)
self.assertEqual(result.returncode, 17, result.stderr)
def test_validate_includes_quick_checks(self) -> None:
error = "Quick Checks wiring regression"
with mock.patch(__name__ + ".check_quick_checks", return_value=[error]):
self.assertIn(error, validate(ROOT))
def test_core_gate_rejects_missing_ignored_filtered_and_corrupt_inputs(self) -> None: def test_core_gate_rejects_missing_ignored_filtered_and_corrupt_inputs(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) root = Path(tmp)
@@ -1058,6 +1252,7 @@ class SelfTests(unittest.TestCase):
mock.patch(__name__ + ".check_profile_definitions", return_value=[]), mock.patch(__name__ + ".check_profile_definitions", return_value=[]),
mock.patch(__name__ + ".check_ilm_build_budget", return_value=[]), mock.patch(__name__ + ".check_ilm_build_budget", return_value=[]),
mock.patch(__name__ + ".check_scheduled_alerts", return_value=[]), mock.patch(__name__ + ".check_scheduled_alerts", return_value=[]),
mock.patch(__name__ + ".check_quick_checks", return_value=[]),
): ):
self.assertEqual(len(validate(root)), 1) self.assertEqual(len(validate(root)), 1)
@@ -1498,7 +1693,7 @@ def main() -> int:
for error in errors: for error in errors:
print(f"ERROR: {error}", file=sys.stderr) print(f"ERROR: {error}", file=sys.stderr)
return 1 return 1
print("OK: e2e modules, runner selection, fuzz matrices, profiles, and scheduled alerts are wired") print("OK: e2e modules, runner selection, fuzz matrices, profiles, scheduled alerts, and Quick Checks are wired")
return 0 return 0