From 31494119431545e257f70043ba23589c8bb11f16 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 5 Sep 2026 17:37:20 +0800 Subject: [PATCH 1/3] fix(ci): share quick checks and lint workflows --- .github/actions/quick-checks/action.yml | 104 ++++++++++++ .github/workflows/ci-docs-only.yml | 95 +---------- .github/workflows/ci.yml | 70 +------- scripts/check_test_wiring.py | 203 +++++++++++++++++++++++- 4 files changed, 310 insertions(+), 162 deletions(-) create mode 100644 .github/actions/quick-checks/action.yml diff --git a/.github/actions/quick-checks/action.yml b/.github/actions/quick-checks/action.yml new file mode 100644 index 000000000..e3e6ee154 --- /dev/null +++ b/.github/actions/quick-checks/action.yml @@ -0,0 +1,104 @@ +# 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 + actionlint@1.7.12 + shellcheck@0.11.0 + + - 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/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 diff --git a/.github/workflows/ci-docs-only.yml b/.github/workflows/ci-docs-only.yml index 7c6ad22ec..e61db2aff 100644 --- a/.github/workflows/ci-docs-only.yml +++ b/.github/workflows/ci-docs-only.yml @@ -12,24 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Companion to ci.yml for required status checks. -# -# ci.yml skips docs-only pull requests via paths-ignore, but the branch ruleset -# requires a check named "Test and Lint" — without this workflow a docs-only PR -# 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. +# Reports the existing required checks for paths excluded by ci.yml. +# Mixed PRs can trigger both workflows; their Quick Checks jobs use one shared +# action to keep validation coverage aligned. Keep this paths list in sync with +# ci.yml's pull_request.paths-ignore via scripts/check_ci_paths_sync.sh. name: Continuous Integration (docs only) @@ -59,19 +45,6 @@ permissions: contents: read 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: name: Quick Checks runs-on: ubuntu-latest @@ -82,64 +55,8 @@ jobs: with: persist-credentials: false - - name: Install ripgrep - uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2 - 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/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 + - name: Run shared quick checks + uses: ./.github/actions/quick-checks test-and-lint: name: Test and Lint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 171f52380..5250f8bae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,12 +100,7 @@ jobs: - name: Typos check with custom config file uses: crate-ci/typos@37bb98842b0d8c4ffebdb75301a13db0267cef89 # master - # Fast, compile-free checks that fail early so contributors get feedback in - # ~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. + # Fail early with compile-free checks shared with docs-only CI. quick-checks: name: Quick Checks if: github.event_name != 'pull_request' || github.event.action != 'closed' @@ -117,67 +112,8 @@ jobs: with: persist-credentials: false - - name: Install ripgrep - uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2 - 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/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 + - name: Run shared quick checks + uses: ./.github/actions/quick-checks test-and-lint: name: Test and Lint diff --git a/scripts/check_test_wiring.py b/scripts/check_test_wiring.py index 8b1cf1246..fe451220f 100755 --- a/scripts/check_test_wiring.py +++ b/scripts/check_test_wiring.py @@ -5,7 +5,9 @@ from __future__ import annotations import hashlib import json +import os import re +import subprocess import sys import tempfile import tomllib @@ -481,18 +483,20 @@ def yaml_block(lines: list[str], key: str, indent: int) -> list[str] | None: 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( ( index for index, line in enumerate(job_lines) if ( - line.split("#", 1)[0].strip() == f"- uses: {action}" - and len(line) - len(line.lstrip()) == 6 + line.split("#", 1)[0].strip() == f"- {key}: {value}" + and len(line) - len(line.lstrip()) == indent ) or ( - line.split("#", 1)[0].strip() == f"uses: {action}" - and len(line) - len(line.lstrip()) == 8 + line.split("#", 1)[0].strip() == f"{key}: {value}" + and len(line) - len(line.lstrip()) == indent + 2 ) ), 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] +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|"if"|"continue-on-error"|'if'|'continue-on-error')\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 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( job_lines: list[str], expected_action_if: str | None, @@ -820,10 +885,135 @@ def validate(root: Path) -> list[str]: errors.extend(check_workflow_readiness(root)) errors.extend(check_profile_definitions(root)) errors.extend(check_scheduled_alerts(root)) + errors.extend(check_quick_checks(root)) return errors 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:") + 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: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) @@ -1058,6 +1248,7 @@ class SelfTests(unittest.TestCase): mock.patch(__name__ + ".check_profile_definitions", return_value=[]), mock.patch(__name__ + ".check_ilm_build_budget", return_value=[]), mock.patch(__name__ + ".check_scheduled_alerts", return_value=[]), + mock.patch(__name__ + ".check_quick_checks", return_value=[]), ): self.assertEqual(len(validate(root)), 1) @@ -1498,7 +1689,7 @@ def main() -> int: for error in errors: print(f"ERROR: {error}", file=sys.stderr) 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 From e77c6f0ca5191636152a5d443aee7b0761209994 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 5 Sep 2026 17:42:14 +0800 Subject: [PATCH 2/3] fix(ci): install actionlint from its verified release --- .github/actions/quick-checks/action.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/actions/quick-checks/action.yml b/.github/actions/quick-checks/action.yml index e3e6ee154..e6cc59775 100644 --- a/.github/actions/quick-checks/action.yml +++ b/.github/actions/quick-checks/action.yml @@ -23,9 +23,20 @@ runs: with: tool: | ripgrep@15.2.0 - actionlint@1.7.12 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: From a74919db8eb4947d3667452a17f015c638572dd2 Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 5 Sep 2026 18:17:13 +0800 Subject: [PATCH 3/3] fix(ci): reject dependencies on required quick checks --- scripts/check_test_wiring.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/check_test_wiring.py b/scripts/check_test_wiring.py index fe451220f..63de463d3 100755 --- a/scripts/check_test_wiring.py +++ b/scripts/check_test_wiring.py @@ -533,7 +533,7 @@ def yaml_scalar_continues(lines: list[str], index: int, indent: int) -> bool: def check_quick_checks(root: Path) -> list[str]: errors: list[str] = [] - bypass_key = r'''(?:if|continue-on-error|"if"|"continue-on-error"|'if'|'continue-on-error')\s*:''' + 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 @@ -546,7 +546,7 @@ def check_quick_checks(root: Path) -> list[str]: 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 bypass failures or change its event condition") + 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: @@ -944,6 +944,10 @@ class SelfTests(unittest.TestCase): " 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(