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
74 changes: 0 additions & 74 deletions .github/actions/install-mdtablefix/action.yml

This file was deleted.

2 changes: 0 additions & 2 deletions .github/actions/windows-gate-cache/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,6 @@ runs:
~/.cargo/bin
~/.local/bin
.chocolatey-cache
.mdtablefix-build
key: ${{ steps.keys.outputs.tools }}

- name: Restore Whitaker suite
Expand Down Expand Up @@ -198,7 +197,6 @@ runs:
~/.cargo/bin
~/.local/bin
.chocolatey-cache
.mdtablefix-build
key: ${{ steps.keys.outputs.tools }}

- name: Save compiler cache
Expand Down
12 changes: 7 additions & 5 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,14 @@ jobs:
- name: Show Ninja version
run: ninja --version
- name: Install mdtablefix
# Required by `make check-fmt`. No prebuilt release exists for this
# platform, so the action compiles it once per cache generation into
# `.mdtablefix-build`, which the tool cache owns.
uses: ./.github/actions/install-mdtablefix
# Required by `make check-fmt`. mdtablefix 0.5.1 publishes a Windows
# x86_64 archive, so this lane installs a prebuilt binary like every
# other. It compiled the tool from source until then, which was this
# repository's last source-build exception.
uses: >-
leynos/shared-actions/.github/actions/install-mdtablefix@c5a54701c8603a0fa756a6b34c49bc2af75a6c11
with:
build-dir: ${{ github.workspace }}/.mdtablefix-build
version: ${{ env.MDTABLEFIX_VERSION }}
- name: Setup uv
# `make check-fmt` runs the uv-driven Ruff format check on every
# platform, so the Windows job needs uv on the PATH as well.
Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,15 @@ env:
# Single source of truth for the cargo-nextest pin, declared once at
# workflow scope so the AGENTS.md `sed` extraction yields one version.
NEXTEST_VERSION: '0.9.133'
# Pins for the Python lint and typecheck gates. Make's `?=` assignments
# yield to the environment, so these choose the versions.
# tests/workflow_contracts/python_toolchain_sync_test.py holds them equal
# to the Makefile defaults.
# Pins for the Python lint and typecheck gates. Make's `?=` assignments yield
# to the environment, so these choose the versions, and
# tests/workflow_contracts/python_toolchain_sync_test.py holds them equal to
# the Makefile defaults.
RUFF_VERSION: '0.16.4'
TY_VERSION: '0.0.74'
PYTHON_BASELINE: '3.14'
# `make check-fmt` shells out to mdtablefix. Pin it once so both formatter
# jobs produce the same canonical output.
MDTABLEFIX_VERSION: '0.5.0'
# `make check-fmt` shells out to mdtablefix; pinned once so both lanes agree.
MDTABLEFIX_VERSION: '0.5.1'
NETSUKE_RUNNER_IMAGE: ubuntu2404 # carried in every compiled-tool cache key

jobs:
Expand Down Expand Up @@ -139,10 +138,11 @@ jobs:
- name: Show Ninja version
run: ninja --version
- name: Install mdtablefix
uses: >-
leynos/shared-actions/.github/actions/install-mdtablefix@c5a54701c8603a0fa756a6b34c49bc2af75a6c11
Comment thread
coderabbitai[bot] marked this conversation as resolved.
# The gate cache owns `~/.local/bin`, so a warm run finds it present.
uses: ./.github/actions/install-mdtablefix
with:
build-dir: ${{ github.workspace }}/.mdtablefix-build
version: ${{ env.MDTABLEFIX_VERSION }}
- name: Setup uv
# Before Format: the uv-driven Ruff, Pylint, and df12 gates need it.
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990
Expand Down Expand Up @@ -307,7 +307,7 @@ jobs:
# tests/workflow_contracts/ci_windows_job_test.py holds them equal.
with:
nextest-version: '0.9.133'
mdtablefix-version: '0.5.0'
mdtablefix-version: '0.5.1'
python-baseline: '3.14'

kani-smoke:
Expand Down
63 changes: 36 additions & 27 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -852,22 +852,28 @@ gate work on a GitHub-hosted runner (leynos/shared-actions#446); and restores
the cache service variables that `mozilla-actions/sccache-action` overwrites.
One SHA across every reference, so a future bump moves them together.

CI installs tools from trusted prebuilt releases only, with one recorded
exception and no others. `setup-rust` verifies the `cargo-binstall` installer
checksum, and every `cargo binstall` call refuses to compile, so a missing
prebuilt release fails the job instead of quietly building the tool.

The remaining exception is `mdtablefix`. Its published releases are fine, but
the crate's binstall metadata sets `bin-dir = "."`, so resolution fails with
"bin-dir configuration provided generates empty source path"
([leynos/mdtablefix#458](https://github.com/leynos/mdtablefix/issues/458)). [`install-mdtablefix`](../.github/actions/install-mdtablefix)
therefore takes the Linux release tarball directly and verifies it against a
SHA-256 pinned in the action and cross-checked against the release's own
`.sha256` sidecar. No Windows binary is published at all, so that lane compiles
the tool once per cache generation into `.mdtablefix-build`, which the Windows
tool cache owns and which never shares compiler output with the product. Both
paths end in an executable-and-version probe, so a stale binary cannot satisfy
a version bump.
CI installs tools from trusted prebuilt releases only, with **no exceptions**.
`setup-rust` verifies the `cargo-binstall` installer checksum, and every
`cargo binstall` call refuses to compile, so a missing prebuilt release fails
the job instead of quietly building the tool.

That rule became absolute with mdtablefix 0.5.1. Until then, this repository
carried its own `install-mdtablefix` action because the crate's binstall
metadata set `bin-dir = "."`, which cargo-binstall rejects
([leynos/mdtablefix#458](https://github.com/leynos/mdtablefix/issues/458)),
and because no Windows archive was published at all
([leynos/mdtablefix#459](https://github.com/leynos/mdtablefix/issues/459)).
The Linux lane took the release tarball against a pinned SHA-256 and the
Windows lane compiled the tool once per cache generation into
`.mdtablefix-build`.

0.5.1 publishes archives for Linux and macOS on both architectures and for
Windows on `x86_64`, with correct metadata, so both formatter lanes now use the
shared
[`install-mdtablefix`](https://github.com/leynos/shared-actions/tree/main/.github/actions/install-mdtablefix)
action and the local one is gone along with its build directory. The shared
action refuses anything earlier than 0.5.1, so an accidental downgrade fails
with a message naming the reason rather than reintroducing source compilation.

`cargo-orthohelp` was the second such exception and is no longer one.
`ortho-config` published no binaries until 0.9.1
Expand All @@ -883,15 +889,18 @@ binary, and this is still the one lane whose save is not restricted to a push on
that installs the tool must be the run that publishes it; the key is
content-addressed by version, so concurrent writers produce identical archives.

Delete the remaining exception when mdtablefix#458 lands, and the rule becomes
absolute. Two contracts hold the line meanwhile.
`tests/workflow_contracts/cache_ownership_test.py` counts the permitted
`cargo install` occurrences rather than pattern-matching them, so a second
source build cannot hide behind the first, and it separately lists
`cargo-orthohelp` in `FORBIDDEN_SOURCE_BUILDS`, checked by name, so a retired
exception cannot return as a newly permitted one.
`tests/workflow_orthohelp_install.rs` requires the release lane to disable
binstall's compile strategy and rejects every `cargo install` naming the tool.
With both exceptions retired, the rule is absolute, and three contracts enforce
it. `tests/workflow_contracts/cache_ownership_test.py` asserts the permitted
set is empty, so reinstating an exception is a deliberate policy change rather
than an edit; it lists both retired tools in `FORBIDDEN_SOURCE_BUILDS`, checked
by name because a count of permitted builds is satisfied by adding a tool back
as a newly permitted entry; and it rejects `cargo install` anywhere in any
workflow or composite action. `tests/workflow_orthohelp_install.rs` requires
the release lane to disable binstall's compile strategy, and
`tests/workflow_contracts/ci_mdtablefix_installer_test.py` requires both
formatter lanes to use the shared action at a version no earlier than 0.5.1,
and the retired local action and its build directory to be absent rather than
merely unused.

The detector behind both parses the command rather than matching its shape. An
option's value is indistinguishable from a crate name without knowing which
Expand Down Expand Up @@ -1168,7 +1177,7 @@ two cannot drift:
```bash
MDTABLEFIX_VERSION="$(sed -n "s/.*MDTABLEFIX_VERSION: '\(.*\)'.*/\1/p" \
.github/workflows/ci.yml)"
cargo binstall --no-confirm --locked \
cargo binstall --no-confirm --locked --disable-strategies compile \
"mdtablefix@$MDTABLEFIX_VERSION"
```

Expand Down Expand Up @@ -1621,7 +1630,7 @@ wrapping; `make markdownlint` then verifies the result.
`make check-fmt` runs the Rust and Python formatter checks, then passes tracked
Markdown files to `scripts/check-markdown-format.sh`. The wrapper skips the
Markdown check when the file list is empty, so the command remains portable
across hosts. The checker requires `mdtablefix` version `0.5.0`, the version
across hosts. The checker requires `mdtablefix` version `0.5.1`, the version
pinned by `MDTABLEFIX_VERSION` in the CI workflow; verify an installation with
`mdtablefix --version`. Run `make test-markdown-format` to exercise the
checker, including its empty-input behaviour, before changing the wrapper.
Expand Down
37 changes: 19 additions & 18 deletions tests/workflow_contracts/cache_contract_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,27 +113,28 @@ def is_build_tree(path: str) -> bool:
("release.yml", "windows-native-recipe-smoke"),
)

#: The remaining documented source-build exception, keyed by the issue that
#: retires it. Counting it by name means a second cannot arrive disguised as
#: another occurrence of this one.
#: No source build is permitted anywhere. The rule used to carry documented
#: exceptions, counted by name so a third could not arrive disguised as a
#: second occurrence of either; both have now been retired because their
#: crates began publishing binaries.
#:
#: `cargo-orthohelp` used to be the other entry, permitted while
#: leynos/ortho-config#479 left the crate with no published binaries. 0.9.1
#: ships checksum-verified archives for every platform this estate targets
#: (leynos/ortho-config#480), so that exception is retired rather than
#: relaxed: the packaging lane now passes `--disable-strategies compile`, and
#: `FORBIDDEN_SOURCE_BUILDS` holds the tool to it.
SOURCE_BUILD_EXCEPTIONS = {
"mdtablefix on Windows (mdtablefix#458)": (
'cargo install --locked "mdtablefix@${MDTABLEFIX_VERSION}"'
),
}
#: `cargo-orthohelp` was permitted while leynos/ortho-config#479 left the crate
#: with no published binaries, and `mdtablefix` on Windows while
#: leynos/mdtablefix#458 left its binstall metadata broken and #459 left the
#: platform without an archive at all. ortho-config 0.9.1 and mdtablefix 0.5.1
#: publish checksum-verified archives for every platform this estate targets,
#: so both lanes now install prebuilt binaries.
#:
#: Kept as an empty mapping rather than deleted: it is the seam a future
#: exception would come back through, and an empty one states that the count
#: is zero rather than leaving the question unasked.
SOURCE_BUILD_EXCEPTIONS: dict[str, str] = {}

#: Tools that once had a source-build exception and must never regain one.
#: Retiring an exception is only durable if something rejects its return; a
#: shrinking count of permitted builds would not, since a reader could add the
#: tool back as a third entry above.
FORBIDDEN_SOURCE_BUILDS = ("cargo-orthohelp",)
#: Checked by name rather than by a count of permitted builds, because a count
#: is satisfied by adding the tool back as a newly permitted entry, which is
#: precisely the regression worth catching.
FORBIDDEN_SOURCE_BUILDS = ("cargo-orthohelp", "mdtablefix")

#: Shared-action steps whose own cache would archive a `target` tree unless
#: the caller owns it. `rust-build-release` forwards the input to its nested
Expand Down
39 changes: 18 additions & 21 deletions tests/workflow_contracts/cache_ownership_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
UBICLOUD_LABELS,
has_single_cache_owner,
)
from source_build_data import is_source_built
from source_build_data import is_source_built, source_build_commands
from workflow_loading import (
job_steps,
load_workflow,
Expand Down Expand Up @@ -352,34 +352,31 @@ def test_workflows_do_not_reintroduce_source_tool_builds_or_stale_providers() ->
for path in sorted({*WORKFLOW_DIR.glob("*.yml"), *WORKFLOW_DIR.glob("*.yaml")})
+ sorted(ACTION_DIR.rglob("action.yml"))
)
# One documented exception remains, and it is counted rather than
# pattern-matched so a second cannot hide behind it.
permitted = {
name: workflow_text.count(text)
for name, text in SOURCE_BUILD_EXCEPTIONS.items()
}
unexpected = {name: count for name, count in permitted.items() if count != 1}
assert not unexpected, (
"each documented source-build exception must appear exactly once, "
f"got {unexpected!r}"
# No exception remains. The count is asserted rather than assumed, so
# adding one back is a deliberate act that fails here first.
assert not SOURCE_BUILD_EXCEPTIONS, (
"every source-build exception has been retired; reinstating one is a "
f"policy change, not an edit, got {SOURCE_BUILD_EXCEPTIONS!r}"
)
# A retired exception has to stay retired, checked by name rather than by
# a shrinking count: a count would be satisfied by adding the tool back as
# another permitted entry above.
# Both retired tools are checked by name. A count of permitted builds
# would not do: it is satisfied by adding a tool back as a newly permitted
# entry, which is precisely the regression worth catching.
returned = [
tool for tool in FORBIDDEN_SOURCE_BUILDS if is_source_built(tool, workflow_text)
]
assert not returned, (
f"{returned!r} had a source-build exception that was retired; these "
"tools publish prebuilt archives and must not be compiled in CI"
)
source_builds = workflow_text.count("cargo install ")
# One extra occurrence is the mdtablefix action's own comment naming the
# command it guards; the exceptions themselves account for the rest.
assert source_builds <= sum(permitted.values()) + 1, (
"CI tools must use trusted prebuilt binaries rather than source "
f"builds; found {source_builds} `cargo install` calls against "
f"{sum(permitted.values())} documented exceptions"
# And nothing else compiles a tool either, which is the general rule the
# two named tools were the last exceptions to. Parsed rather than searched
# for as a substring: `cargo install`, a tab, `cargo +nightly install`
# and a line continuation all run the same command, and a literal search
# for "cargo install " misses every one of them.
compiled = source_build_commands(workflow_text)
assert not compiled, (
"CI tools must come from trusted prebuilt binaries; no workflow or "
f"composite action may run `cargo install`, found {compiled!r}"
)
assert "nscloud-cache-action" not in workflow_text, (
"the Namespace cache volume action must not return"
Expand Down
Loading
Loading