ci: wire the existing linters into CI as advisory checks - #626
Merged
Merged
Conversation
The Makefile has carried `vermin`, `pylint`, `mypy` and `bandit` targets all along and no workflow ran any of them, so nothing noticed when their output changed. (`isort` does appear in cron-vendor.yml, but as a formatter that rewrites the generated constants, not as a check.) Requested by the owner off the back of the CONTRIBUTING refresh in 4ecac90, which established that the local linters are not a CI gate; no issue tracks it. - .github/workflows/lint.yml: new workflow, one job on Python 3.14, running all four tools as advisory steps. Measured on 15189ab before choosing that: bandit 8 findings (7 medium, 1 low, 0 high); mypy 115 errors in 39 files of 496 checked; vermin 105 files flagged; pylint 5902 messages (80 E, 4767 W, 519 R, 541 C). None of the four is clean, so none can block today -- a job that is red the day it lands teaches everyone to scroll past red, which costs more than the checks are worth. Each step writes its count to the run summary instead, and promoting one to blocking is deleting its `continue-on-error` line. The header records the promotion order and why pylint is not in it: 4358 of its 4767 warnings are `unused-argument`, `redefined-builtin` and `super-init-not-called`, all inherent to the schema DSL. - One job rather than four, and one interpreter rather than a matrix: the tools read the source, so five runs would yield five copies of the same findings, and four jobs would pay for four checkouts and four `.[all]` installs to run four cheap tools. 3.14 matches cron-vendor.yml and is the version the recorded counts came from, so a count that moves means the code moved. - Triggers are `pull_request`, a Saturday 06:00 schedule and `workflow_dispatch` -- deliberately not `push: [main]`, which the other validation workflows here use. Those gate something; these gate nothing yet, so a second advisory run per merge, on a commit that already got one on its pull request, would add no information while competing for runners that pull requests here already queue behind 21 checks for ten minutes at a time. The weekly schedule covers main's own drift, which is all the push trigger would have added. - Makefile: the four flag sets become variables and `pipenv run` becomes `$(RUN)`, so the workflow runs `make pylint RUN=` rather than restating the flags. That leaves exactly one definition of each tool's flag set, so local and CI cannot drift into "clean on my machine, red in CI". Verified the expanded commands are byte-identical to the literals they replace. New `vermin-ci` target because `vermin` redirects into temp/ and hands the file to an editor, so its exit status is the editor's and a run that found violations still succeeds; `vermin-ci` writes to stdout and lets the failure propagate, off the same `$(VERMIN_FLAGS)`. No linter configuration was weakened to make anything pass: every tool runs the flags the Makefile already used, and `vermin.ini` is picked up from the repository root locally and in CI alike. Two findings are reported rather than fixed, since a lint fix does not belong in a CI wiring change. vermin puts the code's real floor at 3.11 (`enum.StrEnum` and `enum.show_flag_values`, with 86 modules importing `typing_extensions`) while `vermin.ini` targets 3.6 and `pyproject.toml` still declares `requires-python = ">=3.6, <4"` -- the declared floor and the actual one are five releases apart, and that is why vermin cannot block. And mypy finds `Optional`, `Protocol` and `Any` used in string annotations that are never imported, at pcapkit/protocols/schema/internet/ipv6_route.py:271 and :136 and pcapkit/utilities/logging.py:350; latent, because `cast()`'s first argument is a string Python never evaluates. No changelog entry: CI and developer tooling, not user-visible. Verified `python util/changelog_md.py --check` still exits 0. No tests run -- nothing here touches the package.
1 task done
JarryShaw
added a commit
that referenced
this pull request
Sep 22, 2026
…ed (#656) The *Coding style* section still told contributors that none of the linters is run by the pull-request workflows, which #626 made false when it added `.github/workflows/lint.yml`. Saying only that they now run would mislead in the other direction, so the replacement leads with the part that decides what a contributor should expect: all four steps carry `continue-on-error: true`, so a red linter cannot fail a pull request. It also keeps `isort` out of that group -- isort appears in `cron-vendor.yml` as a formatter over the regenerated constants, not as a check -- and records the `make vermin` trap, whose exit status is the viewer's rather than vermin's, which is why `make vermin-ci` is the target CI runs. Three smaller corrections found while checking the rest of the file against the tree: * The README has no *Testing* section any more. Its only mention of testing is a row in the *Documentation* table linking to `docs/source/testing.rst`, which is where the test commands moved. * The `Changelog drift` job's triggers are scoped to `main` -- `push` and `pull_request` both carry `branches: [main]` -- rather than firing on every push and pull request in the repository. * "The exception stops at the root" overstated the Markdown boundary: the three issue and pull-request templates under `.github/` are Markdown for the same GitHub-rendering reason the root files are. No changelog entry, deliberately: none of this is user-visible, and a bullet here would collide with the changelog consolidation currently in flight.
This was referenced Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Makefile has carried
vermin,pylint,mypyandbandittargets all along and noworkflow ran any of them, so nothing noticed when their output changed. (
isortdoes appear incron-vendor.yml, but as a formatter that rewrites the generated constants, not as a check.)Requested by the owner off the back of the CONTRIBUTING refresh in 4ecac90, which established
that the local linters are not a CI gate. No issue tracks it.
Measured first, then designed
Every tool was run against this branch's tree at the flag sets the Makefile already uses, on the
repo venv's Python 3.14.7. Counts as of
15189abff:vermin.initargets 3.6None of the four is clean, so none can block today. A job that is red the day it lands teaches
everyone to scroll past red, which costs more than the checks are worth. Each step is
continue-on-error: trueand writes its count to the run summary instead; promoting one isdeleting that one line.
Promotion order, recorded in the workflow header:
B104"binding to all interfaces" onaddress: ... = '0.0.0.0'default arguments in the MH/MIP option builders, which bind nosocket. This package already uses
# nosec— 16 findings are suppressed that way today — sotriaging 8 comments is the whole job.
unused-ignore, i.e.# type: ignorecomments that are no longerneeded. Deleting those is risk-free and cuts a fifth of the count.
unused-argument(2931),redefined-builtin(808) andsuper-init-not-called(619), all threeinherent to the schema DSL — protocol fields are legitimately named
next/type/id, and theif TYPE_CHECKING: def __init__(...)stubs exist precisely so as not to callsuper().__init__. Narrowing to--enable=E,Fdoes not rescue it either: that is still 80errors, mostly
unsubscriptable-object(40) andno-member(22) false positives off the samemetaprogramming.
Shape and cost
One new workflow rather than a job bolted onto an existing one, following CodeQL's precedent that
static analysis stands on its own — adding it to
unit-tests.ymlwould couple linting to thatworkflow's
workflow_callgate and let a lint finding blockcron-vendor's version bump.One job, one interpreter. The tools read the source, so a matrix would produce five copies of
the same findings, and four separate jobs would pay for four checkouts and four
.[all]installsto run four cheap tools. Python 3.14 matches
cron-vendor.ymland is the version the counts abovecame from, so a count that moves means the code moved rather than the interpreter.
Triggers are
pull_request, a Saturday 06:00 schedule, andworkflow_dispatch— deliberatelynot
push: [main], which the other validation workflows use. Those gate something; these gatenothing yet, so a second advisory run per merge, on a commit that already got one on its pull
request, adds no information while competing for runners that pull requests here already queue
behind 21 checks for ten minutes at a time. The weekly schedule covers main's own drift, which is
all the push trigger would have added. 06:00 Saturday is an hour clear of CodeQL (02:00), Python
Compatibility (04:00) and Vendor Update (10:00).
Local and CI run the same flags, by construction
The four flag sets became Makefile variables and
pipenv runbecame$(RUN), so the workflowruns
make pylint RUN=rather than restating the flags. That leaves exactly one definition ofeach flag set, so the two cannot drift into "clean on my machine, red in CI". Verified the
expanded commands are byte-identical to the literals they replace (
make -n pylint RUN=diffedagainst the previous recipe string).
vermin.iniis picked up from the repository root in bothplaces.
verminneeded a second recipe: the existing target redirects intotemp/and hands the file toan editor, so its exit status is the editor's and a run that found violations still succeeds. The
new
vermin-ciwrites to stdout and lets the failure propagate, off the same$(VERMIN_FLAGS).The original
vermintarget is behaviourally unchanged.No linter configuration was weakened to make anything pass. Nothing was narrowed, no rule
disabled, no severity floor raised.
Found and deliberately not fixed
Neither belongs in a CI wiring change, and
pcapkit/**is untouched by this PR.minimum at 3.11 —
enum.StrEnumandenum.show_flag_valuesare 3.11 members, and 86modules import
typing_extensions— whilevermin.inisetstargets = 3.6andpyproject.tomlstill declaresrequires-python = ">=3.6, <4". That mismatch is exactly whyvermin exits non-zero, so it is the thing standing between vermin and a blocking job. Resolving
it either way (raise
requires-python, or raisetargetsto match it) is a user-visiblepackaging decision and wants its own change.
name-defined:Optionalatpcapkit/protocols/schema/internet/ipv6_route.py:271,Protocolat the same file's
:136, andAnyatpcapkit/utilities/logging.py:350. Latent rather thanlive, because
cast()'s first argument and aTYPE_CHECKING-guarded__init__stub'sannotations are strings Python never evaluates — but they would break any runtime annotation
evaluation, and
ipv6_route.py:271sits two lines from anunreachablefinding in the samefunction.
Verification
make -nfor all four targets in both forms; CI-form expansions diffed byte-for-byte againstthe recipes they replace.
make bandit RUN=andmake vermin-ci RUN=run end-to-end with the tools onPATH: 8 findingsand 105 files respectively, matching the direct runs, each exiting non-zero, and
vermin-ciconfirmed not to write
temp/vermin.txt.continue-on-errorstructure asserted.python util/changelog_md.py --checkexits 0.No changelog entry: CI and developer tooling, not user-visible. No tests run — nothing here
touches the package.