Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
uv.lock text eol=lf
requirements.lock text eol=lf
docker/locks/*.lock text eol=lf
# constraints.lock was MISSING here (`git check-attr text -- constraints.lock` -> unspecified), so under
# core.autocrlf=true it checks out CRLF — precisely the drift this stanza exists to prevent. Harmless so
# far only because git's clean filter normalizes before `git diff`, which is also why export sync must be
# verified with `git diff` and never a raw `diff` (the latter reports a phantom whole-file difference).
constraints.lock text eol=lf
# The PEP 735 CI-toolchain exports (ADR 0034 §3) — same DEP-1 diff gate, same LF requirement.
ci/locks/*.lock text eol=lf
.mefor-hooks/** text eol=lf
# Shell scripts are executed on the Linux CI runners; force LF so a contributor's
# core.autocrlf=true can never commit CRLF (which breaks `#!/usr/bin/env bash\r`).
Expand Down
7 changes: 6 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ updates:
# pyproject.toml + uv.lock and REGENERATES uv.lock IN its PRs (version updates GA 2025-03,
# security updates GA 2025-12) — the old "pip" ecosystem updated requirements/pyproject but NOT
# uv.lock. It still does not re-derive the EXPORTED locks (requirements.lock + docker/locks/* +
# constraints.lock), which the DEP-1 gate in security.yml byte-diffs;
# constraints.lock + ci/locks/* — SIX artifacts), which the DEP-1 gate in security.yml byte-diffs;
# .github/workflows/dependabot-lock-resync.yml re-exports those on the Dependabot branch so the
# gate stays green.
#
# UNVERIFIED, and it cannot be verified before the next weekly run: whether this ecosystem enumerates
# PEP 735 `[dependency-groups]` (the hash-pinned CI toolchain, ADR 0034 §3) at all. If it does not,
# those pins go stale silently — which is why security.yml's pip-audit step audits ci/locks/*.lock
# directly, turning "silently stale" into a red gate within ~24h via the daily cron.
- package-ecosystem: "uv"
directory: "/"
schedule:
Expand Down
23 changes: 16 additions & 7 deletions .github/workflows/dependabot-lock-resync.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
name: Dependabot lock resync

# Re-exports the FOUR committed "uv export" artifacts (requirements.lock +
# Re-exports the SIX committed "uv export" artifacts (requirements.lock +
# docker/locks/requirements-core.lock + docker/locks/requirements-sqlserver.lock + the HASHLESS
# constraints.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them back
# to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the lockfile
# is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export.
# constraints.lock + the two PEP 735 CI-toolchain locks ci/locks/ci-scanners.lock and
# ci/locks/ci-quality.lock) on a Dependabot PR that touched uv.lock / pyproject.toml, and commits them
# back to the PR branch so the DEP-1 drift gate (security.yml -> pip-audit job, step "Check the
# lockfile is in sync with pyproject (DEP-1)") goes green WITHOUT a human re-export.
#
# WHY: the native "uv" Dependabot ecosystem regenerates uv.lock + pyproject.toml in its PR, but NOT
# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + the four "uv export"s and
# the exported lock artifacts. The DEP-1 gate re-runs "uv lock --check" + all six "uv export"s and
# "git diff --exit-code"s the result, so a Dependabot uv PR would otherwise leave the exports stale
# and red the gate. This workflow runs the IDENTICAL commands and pushes the refreshed exports onto
# the Dependabot branch.
#
# The two ci/locks/*.lock artifacts are what makes the CI toolchain hash-pinned rather than merely
# version-pinned (ADR 0034 §3). They are ALSO what keeps it fresh: a hash-pinned toolchain outside this
# resync machinery would rot into a pinned, stale, unpatched toolchain — worse posture than floating.
#
# KEEP THIS LIST IN LOCKSTEP WITH security.yml's DEP-1 step. A file the gate diffs but this job does
# not re-export is un-fixable by the bot: the resync pushes an incomplete set, DEP-1 re-exports the
# full set, finds the missing one stale, and the PR stays red with no human-free path to green.
Expand Down Expand Up @@ -128,6 +133,10 @@ jobs:
# The hashless constraint export every CI install pins against (`--constraint constraints.lock`,
# ci.yml). Same lock, --no-hashes; omitted here it would stay stale and red DEP-1 forever.
uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock
# The CI TOOLCHAIN locks (PEP 735 dependency groups, ADR 0034 §3). Same lockstep rule as the
# four above: the gate diffs them, so the bot must re-export them or the PR has no path green.
uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock
uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock

- name: Commit and push the resynced locks
if: steps.creds.outputs.present == 'true'
Expand All @@ -139,11 +148,11 @@ jobs:
set -euo pipefail
git config user.name 'dependabot[bot]'
git config user.email '49699333+dependabot[bot]@users.noreply.github.com'
if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock; then
if git diff --quiet -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock; then
echo 'Exported lock files already in sync; nothing to push.'
exit 0
fi
git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock
git add requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock
git commit -m 'chore(deps): resync exported lock files (DEP-1)'
# Push with the persisted App-token credential (NOT GITHUB_TOKEN). An App-token push emits a
# synchronize event so the required checks (DEP-1, ci.yml) re-run on the new commit; a
Expand Down
44 changes: 31 additions & 13 deletions .github/workflows/quality-advisory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,26 @@ jobs:
- name: Install project + coverage tools
run: |
uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole
# NEITHER package is in constraints.lock, so the --constraint above is a no-op for them and both
# previously floated to whatever PyPI served that day. diff-cover is now pinned exactly: the
# inline-annotation surface depends on `--format github-annotations:<level>` and on adjacent-line
# coalescing in GitHubAnnotationsReportGenerator. Deliberately NOT in pyproject.toml -- these are
# CI-only tools, and adding them would trip the DEP-1 lock-sync gate and force four re-exports.
uv pip install --system --constraint constraints.lock pytest-cov "diff-cover==10.4.1"
# HASH-PINNED from the CI toolchain lock (ADR 0034 §3). These tools now DO live in
# pyproject.toml ([dependency-groups].ci-quality, where each pin's rationale sits) and DEP-1
# re-exports this lock by design -- superseding the note that used to sit here claiming they
# were deliberately kept out of pyproject to avoid tripping that gate.
#
# `python -m pip`, not `uv pip install --system`, for ONE honest reason: every other hashed
# install in this repo is a pip install, and keeping one spelling means one thing for the
# guards in tests/test_ci_venv_pinning.py to match. Same setup-python interpreter either way.
#
# NOT because uv cannot: `uv pip install --require-hashes` exists (env UV_REQUIRE_HASHES), and
# setup-uv is already in this job -- the earlier claim that "--require-hashes is pip's
# contract" was simply wrong. NOR for Scorecard's benefit: its parser keys on
# pip/pip3/`python -m pip`, so a `uv pip` line generates NO finding at all, and converting one
# can only ADD parseable surface -- it cannot close an alert. This line is posture-neutral to
# Scorecard and is here for uniformity.
#
# Safe against the editable install above: the group lock and constraints.lock agree on all 10
# shared packages (pytest and pytest-timeout among them), so this cannot re-point what the
# coverage run executes under.
python -m pip install --require-hashes -r ci/locks/ci-quality.lock
- name: Tests under coverage (advisory - never fails)
continue-on-error: true
env:
Expand Down Expand Up @@ -428,13 +442,17 @@ jobs:
- name: Install project + mutmut
run: |
uv pip install --system --constraint constraints.lock -e ".[dev,harness,fhir,dicom,x12,xml,webauthn]" -e packaging/messagefoundry-webconsole
# mutmut 3, pinned. `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM
# cache (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from
# run 30248096425. pytest-timeout is REQUIRED: mutmut 3 always passes `--timeout`/
# `--timeout-method` to pytest, and without the plugin every test invocation dies with an
# unrecognised-argument error that surfaces only as BadTestExecutionCommandsException.
# Neither package is in constraints.lock, so the --constraint above does not pin them.
uv pip install --system "mutmut==3.6.0" pytest-timeout
# mutmut 3 + pytest-timeout, HASH-PINNED from the CI toolchain lock (ADR 0034 §3). Both live in
# pyproject.toml's [dependency-groups].ci-quality, which records why each is load-bearing:
# `mutmut<3` resolved to 2.5.1, which CRASHES on Python 3.14 in its pony-ORM cache
# (`cannot pickle 'itertools.count'`) before generating a single mutant -- verified from run
# 30248096425; and pytest-timeout is REQUIRED because mutmut 3 always passes `--timeout`/
# `--timeout-method` to pytest, so without the plugin every invocation dies with an
# unrecognised-argument error surfacing only as BadTestExecutionCommandsException.
# `python -m pip` for UNIFORMITY with every other hashed install in this repo, not because uv
# cannot do it (`uv pip install --require-hashes` exists) and not for Scorecard (a `uv pip`
# line generates no finding at all) -- see the coverage job's install step for the full note.
python -m pip install --require-hashes -r ci/locks/ci-quality.lock
- name: Mutation-test a bounded scope (advisory - never fails)
continue-on-error: true
env:
Expand Down
44 changes: 35 additions & 9 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ jobs:
# same lock, so a fresh `uv pip install -e ".[extras]"` cannot drift to a newer upstream
# release. Re-export it here so it can never fall out of step with uv.lock.
uv export --all-extras --no-emit-project --no-hashes --format requirements.txt -o constraints.lock
git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock
# The CI TOOLCHAIN locks (ADR 0034 §3): PEP 735 `[dependency-groups]`, so the scanners and the
# quality tools flow through uv.lock and are consumed HASH-PINNED. Non-default groups, so they
# stay out of the four exports above (and out of the SBOM / image locks / audited runtime).
uv export --only-group ci-scanners --format requirements.txt -o ci/locks/ci-scanners.lock
uv export --only-group ci-quality --format requirements.txt -o ci/locks/ci-quality.lock
git diff --exit-code -- requirements.lock docker/locks/requirements-core.lock docker/locks/requirements-sqlserver.lock constraints.lock ci/locks/ci-scanners.lock ci/locks/ci-quality.lock
- name: Install from the hashed lockfile (DEP-1)
run: |
# --require-hashes enforces a hash for every requirement (the lockfile carries them): a
Expand All @@ -89,13 +94,33 @@ jobs:
/tmp/lockcheck/bin/pip install --require-hashes -r requirements.lock
- name: Audit the locked dependencies (DEP-1)
run: |
# PINNED. This job exists to prove nothing unpinned enters the tree, and it was installing
# its own auditor with a bare `pip install pip-audit` — dependency intake that none of the
# lockfiles it checks actually covers. `==` also makes the audit reproducible: an unpinned
# auditor can change its advisory database handling between two runs of the same commit.
python -m pip install --upgrade pip "pip-audit==2.10.1"
# HASH-PINNED from the lock, not merely `==`-pinned: a version pin does not satisfy Scorecard
# PinnedDependenciesID, and the transitive closure floated regardless. Rationale for each pin
# lives in pyproject.toml's [dependency-groups]. THIS step's `--upgrade pip` bootstrap is gone
# rather than pinned — the lock hash-pins `pip` itself (a pip-audit → pip-api dependency).
# Scoped deliberately, because the unqualified claim was FALSE for this job: the DEP-1 step
# above still runs `pip install --upgrade pip "uv=="` in this same interpreter two steps
# earlier, and that unverified pip does the load-bearing work (the six exports + the diff
# gate); the line below then DOWNGRADES pip to the locked version after the fact. So this
# job's posture improves for the AUDITOR, not for the exporter. The `uv` bootstrap is circular
# by construction (ADR 0034 section 3's residuals) and is why `pip` stays registered in
# SECURITY_YML_ACCEPTED_UNPINNED. The claim holds unqualified in the bandit job and zizmor.yml.
python -m pip install --require-hashes -r ci/locks/ci-scanners.lock
# Audit the committed, pinned set — reproducible (vs auditing a fresh latest-resolve).
pip-audit -r requirements.lock --desc
# Audit the TOOLCHAIN locks too. NOT bookkeeping: hash-pinning makes the toolchain STICKY, so
# without this a CVE in a pinned scanner is invisible to every gate — the "pinned, stale,
# unpatched is worse than floating" failure ADR 0034 section 3 names. `--ignore-vuln <ID>` is
# the escape hatch for a triaged advisory, exactly as for the lockfile audit above.
#
# CONSEQUENCE, stated rather than discovered later: this is a REQUIRED context, so a CVE in
# mutmut's or diff-cover's closure reds the merge gate over an ADVISORY tool. That is not a new
# posture — requirements.lock is exported `--all-extras`, so ruff, mypy and pytest already do
# exactly this — but it does add 40 distributions to the blocking set (23 from ci-scanners, 17
# from ci-quality; measured). Zero advisories at the time of writing. If the owner would rather
# the advisory half not block, move the ci-quality line alone to its own step.
pip-audit -r ci/locks/ci-scanners.lock --desc
pip-audit -r ci/locks/ci-quality.lock --desc
# ANTI-SLOPSQUAT. Deliberately a STEP in this already-REQUIRED job rather than a new context: it
# blocks today with no branch-protection change, the same reasoning as the ledger-gate backstop
# in ci.yml. (A hard-failing job that is NOT a required context does not stop auto-merge — it
Expand Down Expand Up @@ -300,9 +325,10 @@ jobs:
python-version: "3.14"
- name: Scan source for insecure patterns
run: |
# Pin bandit: an unpinned upgrade once silently changed `# nosec` parsing (1.9.x requires
# space-separated test IDs) and broke a green branch. Bump deliberately, not implicitly.
python -m pip install --upgrade pip "bandit==1.9.4"
# HASH-PINNED from the lock (ADR 0034 §3); bandit's version is the findings baseline of a
# blocking gate — the `# nosec`-parsing incident behind that pin is recorded beside the pin
# itself, in pyproject.toml's [dependency-groups].ci-scanners. Bump there, then re-export.
python -m pip install --require-hashes -r ci/locks/ci-scanners.lock
# Skips are reviewed-safe project idioms. The subprocess sites (B603/B607) are annotated
# per-line with `# nosec` at the call instead of a wholesale skip, so a NEW unreviewed
# subprocess call is still flagged (review low-27):
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ jobs:
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.14"
- name: Install zizmor (pinned)
- name: Install zizmor (hash-pinned from the CI toolchain lock)
run: |
# Pin deliberately; verify the tag at https://github.com/zizmorcore/zizmor/releases if it 404s.
python -m pip install --upgrade pip "zizmor==1.5.2"
# HASH-PINNED from the lock (ADR 0034 §3). The version lives in pyproject.toml's
# [dependency-groups].ci-scanners with its rationale; bump it there, then re-export.
python -m pip install --require-hashes -r ci/locks/ci-scanners.lock
# actionlint FIRST: it catches the class zizmor structurally cannot. An invalid `${{ }}` anywhere
# in a `run:` body -- comments included -- aborts workflow COMPILATION, so no jobs are created and
# every required context silently never appears (the PR looks stuck, not red). zizmor parses the
Expand Down
Loading
Loading