mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-13 16:30:59 +01:00
Check the toolchain versions CI actually installs, not just the ones declared
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
`.fvmrc` is a declaration nothing in this fleet reads. `subosito/flutter-action`
takes its own `flutter-version:`, and that pin is what the runner installs, so
the two drift in silence and the failure mode is the worst kind: green
locally, red in CI, with no diff to explain it.
kuhylog is the live case. Its runner is on Flutter **3.44.9**, which predates
the `@awaitNotRequired` annotation, so `unawaited(Navigator.push(...))` is
`unnecessary_unawaited` on 3.47.2 here and `discarded_futures` there -- the two
SDKs demand opposite code and CI went red on a commit that is clean locally.
The fleet is spread across 3.44.6, 3.44.8, 3.47.0 and 3.47.1 with no one
tracking it.
So a workflow's `flutter-version:` / `node-version:` is now a dependency like
any other, judged against the same latest stable. That needs no separate
"these must agree" rule: two values both required to equal latest already
agree. A channel name and a `${{ }}` expression are skipped -- one is not a
version, the other resolves at run time and guessing is worse than silence.
Found real drift on its first run: both TypeScript workflows here pinned
`node-version: 24`, a floating major that lets two runs of the same commit
install different Node. Now 24.20.0.
321 tests, 100% branch coverage on every production module.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRykabMwPgDzFHcARoqpaK
This commit is contained in:
parent
7a682a46f3
commit
98213cbc40
2
.github/workflows/ts-core-tests.yml
vendored
2
.github/workflows/ts-core-tests.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: "24.20.0"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
2
.github/workflows/web-ui-tests.yml
vendored
2
.github/workflows/web-ui-tests.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
node-version: "24.20.0"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@ -56,4 +56,5 @@ repos:
|
||||
(pubspec\.(yaml|lock)|pyproject\.toml|requirements.*\.txt|uv\.lock
|
||||
|package\.json|package-lock\.json|pnpm-lock\.yaml
|
||||
|Cargo\.(toml|lock)|go\.(mod|sum)
|
||||
|\.fvmrc|\.nvmrc|\.python-version)$
|
||||
|\.fvmrc|\.nvmrc|\.python-version
|
||||
|\.github/workflows/.*\.ya?ml)$
|
||||
|
||||
@ -14,7 +14,9 @@ import subprocess
|
||||
|
||||
from dep_freshness._tables import EXCLUDED_DIRS, MANIFEST_GLOBS, REQUIREMENTS_PATTERN
|
||||
from dep_freshness.models import Dep
|
||||
from dep_freshness.parsers import fvm, golang, javascript, pubspec, python, rust
|
||||
from dep_freshness.parsers import (
|
||||
fvm, golang, javascript, pubspec, python, rust, workflow,
|
||||
)
|
||||
|
||||
_REQUIREMENTS = re.compile(REQUIREMENTS_PATTERN)
|
||||
|
||||
@ -31,7 +33,11 @@ _BY_NAME = {
|
||||
|
||||
|
||||
def is_manifest(path: Path) -> bool:
|
||||
return path.name in MANIFEST_GLOBS or bool(_REQUIREMENTS.match(path.name))
|
||||
return (
|
||||
path.name in MANIFEST_GLOBS
|
||||
or bool(_REQUIREMENTS.match(path.name))
|
||||
or workflow.is_workflow(path)
|
||||
)
|
||||
|
||||
|
||||
def parse_manifest(path: Path) -> list[Dep]:
|
||||
@ -41,6 +47,8 @@ def parse_manifest(path: Path) -> list[Dep]:
|
||||
return parser(path)
|
||||
if _REQUIREMENTS.match(path.name):
|
||||
return python.parse_requirements(path)
|
||||
if workflow.is_workflow(path):
|
||||
return workflow.parse(path)
|
||||
return []
|
||||
|
||||
|
||||
|
||||
70
dep_freshness/parsers/workflow.py
Normal file
70
dep_freshness/parsers/workflow.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""`.github/workflows/*.yml` -> the toolchain versions CI actually builds with.
|
||||
|
||||
`.fvmrc` declares the SDK, but nothing in this fleet reads it: `subosito/
|
||||
flutter-action` takes its own `flutter-version:`, and that pin is what the
|
||||
runner really installs. The two drift silently, and the failure mode is the
|
||||
worst kind -- green locally, red in CI, with no diff to explain it. kuhylog
|
||||
went red on `discarded_futures` for exactly this reason: the runner was on
|
||||
Flutter 3.44.9, which predates the `@awaitNotRequired` annotation that makes
|
||||
the same `unawaited(...)` call a violation on 3.47.2.
|
||||
|
||||
So the CI pin is a dependency like any other, judged against the same latest
|
||||
stable. Keeping both honest that way needs no separate "these must agree"
|
||||
rule: two values both required to equal latest already agree.
|
||||
|
||||
A `${{ ... }}` expression is skipped -- it resolves at run time, and guessing
|
||||
is worse than saying nothing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from dep_freshness._tables import TOOLCHAIN
|
||||
from dep_freshness.models import Dep
|
||||
from dep_freshness.versions import exact_pin
|
||||
|
||||
WORKFLOW_DIR = ".github/workflows"
|
||||
|
||||
# `flutter-version: 3.47.2`, `node-version: '24.18.0'`, quoted or not.
|
||||
_PIN = re.compile(
|
||||
r"""^\s*(?P<tool>flutter|node)-version:\s*["']?(?P<value>[^"'#\s]+)["']?"""
|
||||
)
|
||||
# A channel name is not a version, and `.fvmrc` already reports that case.
|
||||
_NOT_A_VERSION = frozenset({"stable", "beta", "master", "main", "dev", "lts", "latest"})
|
||||
|
||||
|
||||
def is_workflow(path: Path) -> bool:
|
||||
"""True for a file inside a repo's `.github/workflows` directory."""
|
||||
return (
|
||||
path.suffix in (".yml", ".yaml")
|
||||
and path.parent.name == "workflows"
|
||||
and path.parent.parent.name == ".github"
|
||||
)
|
||||
|
||||
|
||||
def parse(path: Path) -> list[Dep]:
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return []
|
||||
deps: list[Dep] = []
|
||||
for number, line in enumerate(text.splitlines(), start=1):
|
||||
match = _PIN.match(line)
|
||||
if not match:
|
||||
continue
|
||||
value = match.group("value")
|
||||
if value.startswith("${{") or value in _NOT_A_VERSION:
|
||||
continue
|
||||
deps.append(
|
||||
Dep(
|
||||
ecosystem=TOOLCHAIN,
|
||||
name=match.group("tool"),
|
||||
constraint=value,
|
||||
path=path,
|
||||
line=number,
|
||||
pinned=exact_pin(value),
|
||||
)
|
||||
)
|
||||
return deps
|
||||
@ -17,4 +17,5 @@
|
||||
(pubspec\.(yaml|lock)|pyproject\.toml|requirements.*\.txt|uv\.lock
|
||||
|package\.json|package-lock\.json|pnpm-lock\.yaml
|
||||
|Cargo\.(toml|lock)|go\.(mod|sum)
|
||||
|\.fvmrc|\.nvmrc|\.python-version)$
|
||||
|\.fvmrc|\.nvmrc|\.python-version
|
||||
|\.github/workflows/.*\.ya?ml)$
|
||||
|
||||
83
dep_freshness/tests/test_workflow.py
Normal file
83
dep_freshness/tests/test_workflow.py
Normal file
@ -0,0 +1,83 @@
|
||||
"""The toolchain versions a CI runner actually installs.
|
||||
|
||||
`.fvmrc` is a declaration nothing in this fleet reads; `subosito/flutter-action`
|
||||
takes its own `flutter-version:`, and that is what the runner gets. When the
|
||||
two disagree the failure is green-locally / red-in-CI with no diff to explain
|
||||
it -- kuhylog's runner was on Flutter 3.44.9, which predates the
|
||||
`@awaitNotRequired` annotation, so the same code was a violation there and
|
||||
clean here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dep_freshness import discover
|
||||
from dep_freshness.parsers import workflow
|
||||
from dep_freshness.tests.conftest import write
|
||||
|
||||
CI = """\
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
verify:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
flutter-version: 3.44.9
|
||||
channel: stable
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.18.0'
|
||||
"""
|
||||
|
||||
|
||||
def _path(repo):
|
||||
return repo / ".github/workflows/ci.yml"
|
||||
|
||||
|
||||
def test_a_workflow_pin_is_a_toolchain_dependency(repo):
|
||||
deps = {
|
||||
d.name: d for d in workflow.parse(write(repo, ".github/workflows/ci.yml", CI))
|
||||
}
|
||||
assert deps["flutter"].pinned == "3.44.9"
|
||||
assert deps["flutter"].line == 12
|
||||
assert deps["node"].pinned == "24.18.0"
|
||||
assert deps["node"].ecosystem == "toolchain"
|
||||
|
||||
|
||||
def test_a_channel_name_is_not_a_version(repo):
|
||||
body = CI.replace("flutter-version: 3.44.9", "flutter-version: stable")
|
||||
names = [
|
||||
d.name for d in workflow.parse(write(repo, ".github/workflows/ci.yml", body))
|
||||
]
|
||||
assert "flutter" not in names, "a channel tells the gate nothing to compare"
|
||||
|
||||
|
||||
def test_a_runtime_expression_is_skipped_rather_than_guessed(repo):
|
||||
body = CI.replace("flutter-version: 3.44.9", "flutter-version: ${{ env.FLUTTER }}")
|
||||
names = [
|
||||
d.name for d in workflow.parse(write(repo, ".github/workflows/ci.yml", body))
|
||||
]
|
||||
assert "flutter" not in names
|
||||
|
||||
|
||||
def test_an_unreadable_workflow_yields_nothing(repo):
|
||||
assert workflow.parse(repo / ".github/workflows/gone.yml") == []
|
||||
|
||||
|
||||
def test_only_files_under_github_workflows_count(repo):
|
||||
assert workflow.is_workflow(_path(repo))
|
||||
assert workflow.is_workflow(repo / ".github/workflows/ci.yaml")
|
||||
assert not workflow.is_workflow(repo / ".github/ci.yml")
|
||||
assert not workflow.is_workflow(repo / "workflows/ci.yml")
|
||||
assert not workflow.is_workflow(repo / ".github/workflows/notes.md")
|
||||
|
||||
|
||||
def test_discovery_routes_a_workflow_to_the_workflow_parser(repo):
|
||||
path = write(repo, ".github/workflows/ci.yml", CI)
|
||||
assert discover.is_manifest(path)
|
||||
assert {d.name for d in discover.parse_manifest(path)} == {"flutter", "node"}
|
||||
assert path in discover.find_manifests(repo)
|
||||
Loading…
Reference in New Issue
Block a user