utils/dep_freshness/tests/test_workflow.py
Krzysztof kuhy Rudnicki 98213cbc40
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
Check the toolchain versions CI actually installs, not just the ones declared
`.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
2026-08-28 15:04:00 +02:00

84 lines
2.7 KiB
Python

"""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)