From c3791feb2e8217b186388a809b1d0e3f284d3e63 Mon Sep 17 00:00:00 2001 From: "Mr. Gintaras Kazlauskas" Date: Tue, 3 Nov 2026 16:25:35 +0200 Subject: [PATCH 1/3] chore: introduce locked mise toolchain Pin repository tools and add isolated Linux installation verification while preserving existing checks and deployment workflows. --- .github/workflows/mise-toolchain.yml | 31 ++++++++++++++++++++++++++++ MISE-ADOPTION.md | 31 ++++++++++++++++++++++++++++ mise.lock | 22 ++++++++++++++++++++ mise.toml | 11 ++++++++++ 4 files changed, 95 insertions(+) create mode 100644 .github/workflows/mise-toolchain.yml create mode 100644 MISE-ADOPTION.md create mode 100644 mise.lock create mode 100644 mise.toml diff --git a/.github/workflows/mise-toolchain.yml b/.github/workflows/mise-toolchain.yml new file mode 100644 index 0000000..08be08d --- /dev/null +++ b/.github/workflows/mise-toolchain.yml @@ -0,0 +1,31 @@ +name: mise toolchain + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + mise-toolchain: + name: mise toolchain (Linux x64) + runs-on: ubuntu-24.04 + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install verified mise and locked tools + uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 + with: + version: 2026.10.25 + sha256: 5d362ecc578dc7c146679b841dda949d6472cbf86ff5635db7afdca1b6b3fd87 + install_args: --locked + cache: false + env: false + - name: Print selected tool versions + run: mise run toolchain + - name: Confirm installation did not change configuration + run: git diff --exit-code -- mise.toml mise.lock diff --git a/MISE-ADOPTION.md b/MISE-ADOPTION.md new file mode 100644 index 0000000..a9063e1 --- /dev/null +++ b/MISE-ADOPTION.md @@ -0,0 +1,31 @@ +# mise adoption: .github + +Status: review proposal. Linux x64 locked installation and version diagnostics passed locally; CI execution and application compatibility remain to be reviewed. Base commit: `48b96656c6779788cf84444fc0973f9697d18dcb`. + +This file introduces exact runtime selections and a diagnostic task only. Python and Node patch versions were resolved from their official release indexes on 2026-11-03; they need application compatibility testing. Terraform selections preserve this repository's existing CI selection. ShellCheck 0.11.0 installs successfully; lint compatibility still requires the existing repository checks. + +## First use + +1. Install an approved, verified, exact mise release once on your workstation. +2. Read `mise.toml` before trusting it. Repository configuration and tasks can execute code. +3. Run `mise trust` in this repository only after review. +4. Run `mise install --locked` to download the declared tools. +5. Run `mise run toolchain` to inspect versions. Success proves version availability, not application correctness. +6. Follow this repository's existing dependency setup in an isolated environment. mise does not replace Python requirements, npm package locks, Terraform provider locks, system packages, or database services. +7. Run the existing repository tests and required CI checks. Do not rename required check contexts or enable deployment as part of installation. +8. The supplied `mise.lock` was generated by mise 2026.10.25 for Linux x64 and installed successfully. Additional platforms require generated locks and validation before support is claimed. +9. Migrate CI in a separate reviewed change using the same configuration. Pin the mise installer action to a reviewed full commit SHA and the mise binary to an exact version; retain all existing gates and permissions. + +## Boundaries + +No automatic hooks, environment-file loading, production credentials, deployment tasks, or cross-repository downloads are added. `mise run toolchain` is diagnostic only. The additive mise-toolchain workflow verifies locked installation on Linux x64. Existing application/deployment workflows are unchanged and do not yet use mise. The new smoke check is not configured as a required branch-protection check. Package dependencies and additional CLI tools still require their own inventory and pinning. + +## Evidence inspected + +- `.github/workflows/ci-base.yml` +- `.github/workflows/compliance-base.yml` +- `.github/workflows/security-scan.yml` + +## Installer provenance + +The smoke workflow pins mise-action to its reviewed commit and mise to 2026.10.25, verifies the extracted Linux x64 binary SHA-256, disables shared caching, and uses a read-only token on a hosted runner. The archive digest was checked against the release API before deriving the binary digest. This is integrity verification, not an independent publisher identity audit. diff --git a/mise.lock b/mise.lock new file mode 100644 index 0000000..fb0455d --- /dev/null +++ b/mise.lock @@ -0,0 +1,22 @@ +# @generated - this file is auto-generated by `mise lock` https://mise.jdx.dev/dev-tools/mise-lock.html + +lockfile_version = 3 + +[[tools.node]] +version = "22.23.4" +backend = "core:node" +specifiers = ["22.23.4"] + +[tools.node."platforms.linux-x64"] +checksum = "sha256:b8492284c76a54baf58d61668dd0e41ee0fc98adabf9503d30f05d08f55b4406" +url = "https://nodejs.org/dist/v22.23.4/node-v22.23.4-linux-x64.tar.gz" + +[[tools.python]] +version = "3.12.16" +backend = "core:python" +specifiers = ["3.12.16"] + +[tools.python."platforms.linux-x64"] +checksum = "sha256:ce4f6d0a6cfdb3bed6e44c2f9affd85943c76a42e25ad3a259002f31d6fb8a20" +url = "https://github.com/astral-sh/python-build-standalone/releases/download/20261026/cpython-3.12.16+20261026-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz" +provenance = "github-attestations" diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..3f7d01f --- /dev/null +++ b/mise.toml @@ -0,0 +1,11 @@ +# Initial local toolchain adoption; see MISE-ADOPTION.md for validation limits. +[tools] +python = "3.12.16" +node = "22.23.4" + +[tasks.toolchain] +description = "Print selected tool versions; this is not a compliance gate" +run = [ + "python --version", + "node --version", +] From 13068a4dea3b94af5e83a6e68669c74800b6c2ae Mon Sep 17 00:00:00 2001 From: Gintaras Kazlauskas Date: Tue, 3 Nov 2026 18:51:54 +0200 Subject: [PATCH 2/3] fix(tools): bound mise config discovery and make lockfile mode a repo property (ADR-0010 M-01, M-02) Adds .miserc.toml with ceiling_paths set to the parent of the repository root, and [tool_config] locked = true to mise.toml, plus a four-leg refusal test in the mise workflow. jolarca-control proved the same pattern green on run 41677451440; this repository's own run is the evidence for this repository. Why each file: mise loads config from the filesystem root down to the cwd and merges [tools] per tool, so an unbounded search lets a stray parent mise.toml add tools that CI never sees -- the same commit resolving differently per machine. The setting cannot live under [settings] in mise.toml because mise reads .miserc.toml first, and the ceiling must name the parent because a ceiling directory is not searched itself. locked = true makes strict lockfile mode a property of this repository rather than of the CI job's --locked flag. The guard reads the declared tool set out of mise.toml instead of naming versions, so one file is correct across the fleet. Each negative leg is paired with a positive one, because a guard asserting only absence also passes when the command printed nothing. Limits, in MISE-ADOPTION.md: Linux x64 only, CI only, no secrets, not a required check; a project .miserc.toml cannot ignore global or system config, so ADR-0010 M-04 stays open; ceiling_paths fails open if the template does not render, silently, and leg 2 is what turns the declaration into a control. --- .github/workflows/mise-toolchain.yml | 141 ++++++++++++++++++++++++++- .miserc.toml | 30 ++++++ MISE-ADOPTION.md | 45 +++++++++ mise.toml | 7 ++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 .miserc.toml diff --git a/.github/workflows/mise-toolchain.yml b/.github/workflows/mise-toolchain.yml index 08be08d..352049a 100644 --- a/.github/workflows/mise-toolchain.yml +++ b/.github/workflows/mise-toolchain.yml @@ -1,9 +1,9 @@ name: mise toolchain on: - pull_request: push: branches: [main] + pull_request: permissions: contents: read @@ -29,3 +29,142 @@ jobs: run: mise run toolchain - name: Confirm installation did not change configuration run: git diff --exit-code -- mise.toml mise.lock + + # ADR-0010 M-01 and M-02. mise.toml binds the repository; this job binds the claim. + # A declaration nobody has watched fail is a hypothesis, so every leg below has an + # opposite leg: a guard that only asserts absence also passes when the command + # printed nothing at all. + # + # Portable by construction. Nothing here names a tool or a version: the declared + # set is read out of mise.toml, so the same file is correct in every repository + # that adopts this pattern. Two measured reasons it is written this way: + # * mise abbreviates HOME in `mise config` output, so a grep for + # "$GITHUB_WORKSPACE/mise.toml" matches nothing while that very file is loaded. + # The first version of this job failed for exactly that reason, and its failure + # output was the proof of the inheritance. Assertions therefore match on the + # fixture's own tool name and on the versions the repository declares. + # * jolarca-control's tests/test_ci_gates_are_real.py forbids a shell fallback in + # any run step, and it named four steps here. Handing a status to a fallback + # reports an unreadable input as a clean scan, so every step tests status with + # `if` and exits 1 when it cannot read its own input. + mise-config-scope: + name: mise config scope guard (Linux x64) + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install verified mise and locked tools + uses: jdx/mise-action@5228313ee0372e111a38da051671ca30fc5a96db # v3 + with: + version: 2026.10.25 + sha256: 5d362ecc578dc7c146679b841dda949d6472cbf86ff5635db7afdca1b6b3fd87 + install_args: --locked + cache: false + env: false + - name: Show what mise loaded and what it trusts + run: | + set -euo pipefail + mise --version + echo '--- mise trust --show ---' + if ! mise trust --show; then + echo 'note: trust listing exited non-zero' + fi + echo '--- mise config ---' + if ! mise config; then + echo 'note: config listing exited non-zero' + fi + - name: Plant a decoy config one directory above the repository + run: | + set -euo pipefail + parent="$(dirname "$GITHUB_WORKSPACE")" + printf '[tools]\nbidi-decoy = "9.9.9"\n' > "$parent/mise.toml" + echo "planted: $parent/mise.toml" + ls -l "$parent" + - name: Leg 1 - unbounded search MUST see the decoy (so leg 2 is not vacuous) + env: + MISE_CEILING_PATHS: "" + run: | + set -euo pipefail + if ! out="$(mise config 2>&1)"; then + printf '%s\n' "$out" + echo '::error::mise config could not be read; the guard refuses to grade an input it cannot see' + exit 1 + fi + printf '%s\n' "$out" + if ! printf '%s' "$out" | grep -q 'bidi-decoy'; then + echo '::error::the decoy is invisible even with no ceiling, so this runner proves nothing about ceiling_paths' + exit 1 + fi + echo 'OK: with no ceiling mise discovers the parent config and adopts its tool.' + - name: Leg 2 - the committed .miserc.toml MUST hide the decoy and keep every declared pin + run: | + set -euo pipefail + if ! out="$(mise config 2>&1)"; then + printf '%s\n' "$out" + echo '::error::mise config could not be read' + exit 1 + fi + printf '%s\n' "$out" + if printf '%s' "$out" | grep -q 'bidi-decoy'; then + echo '::error::ceiling_paths failed open: mise still loads a config above the repository' + exit 1 + fi + if ! vers="$(mise ls --current 2>&1)"; then + printf '%s\n' "$vers" + echo '::error::mise ls --current could not be read' + exit 1 + fi + printf '%s\n' "$vers" + declared="$(awk '/^\[tools\]/{f=1; next} /^\[/{f=0} f && /=/ { + gsub(/[ "]/, ""); split($0, a, "="); if (a[2] ~ /^[0-9]/) print a[1] " " a[2] }' mise.toml)" + if [ -z "$declared" ]; then + echo '::error::mise.toml declares no tools, so a silent refusal would be read as a bound holding' + exit 1 + fi + printf 'declared tools:\n%s\n' "$declared" + while read -r name version; do + if [ -z "$name" ]; then + continue + fi + if ! printf '%s' "$vers" | grep -qF "$version"; then + echo "::error::$name $version is declared but not selected, so this repository's config is not in effect" + exit 1 + fi + echo "OK: $name $version is the selected version." + done <<< "$declared" + echo 'OK: decoy excluded and every declared pin is the one mise actually selects.' + - name: Leg 3 - plain "mise install" succeeds from the committed lockfile + run: | + set -euo pipefail + mise install + echo 'OK: no --locked flag was passed anywhere and the lock still governed resolution. Leg 4 is meaningful.' + - name: Leg 4 - with no lockfile, plain "mise install" MUST be refused + run: | + set -euo pipefail + work="$(mktemp -d)" + cp mise.toml .miserc.toml "$work"/ + cd "$work" + if ! mise trust .; then + echo 'note: mise trust exited non-zero on the scratch copy' + fi + first="$(awk '/^\[tools\]/{f=1; next} /^\[/{f=0} f && /=/ { + gsub(/[ "]/, ""); split($0, a, "="); if (a[2] ~ /^[0-9]/) print a[1]; exit }' mise.toml)" + if [ -z "$first" ]; then + echo '::error::no tool to test, so this leg would prove nothing' + exit 1 + fi + echo "testing: mise install $first with mise.lock deliberately absent" + data="$(mktemp -d)" + if out="$(MISE_DATA_DIR="$data" mise install "$first" 2>&1)"; then + printf '%s\n' "$out" + echo "::error::plain mise install ACCEPTED $first with no lockfile present - locked mode is not a property of the repository" + exit 1 + fi + printf '%s\n' "$out" + if ! printf '%s' "$out" | grep -qi 'lock'; then + echo '::error::install failed, but the refusal does not name the lockfile, so the cause is unrelated and this leg proves nothing' + exit 1 + fi + echo 'OK: refused, and the refusal names the lockfile.' diff --git a/.miserc.toml b/.miserc.toml new file mode 100644 index 0000000..21a9217 --- /dev/null +++ b/.miserc.toml @@ -0,0 +1,30 @@ +# Bounds mise's config discovery to this repository. Closes ADR-0010 M-01. +# +# WHY THIS FILE AND NOT [settings] IN mise.toml +# mise reads .miserc.toml BEFORE any mise.toml. Setting ceiling_paths under +# [settings] in mise.toml has no effect on config discovery, which is precisely +# the mistake a reader of mise.toml alone would make. +# +# WHY THE PARENT, NOT THE REPO ROOT +# The ceiling directory is not searched itself: with a ceiling of /home/user, +# /home/user/projects/app/mise.toml loads but /home/user/mise.toml does not. +# config_root is the directory holding this file, so dirname(config_root) is the +# repo's parent. Naming the repo root would stop mise loading this repository's +# own mise.toml -- a bound that passes by disabling the thing it guards. +# +# WHY THIS FAILS OPEN, AND WHAT ACTUALLY ENFORCES IT +# If the template does not render, mise uses the file's raw, unrendered content as +# a literal path. That path matches nothing, discovery stays unbounded, and no +# warning is printed because mise reads this before logging starts. So this file +# is a declaration, not a control. The `mise config scope guard` job in +# .github/workflows/mise-toolchain.yml is the control: it plants a decoy config in +# the parent directory and fails the run if mise loads it. +# +# WHAT IT CANNOT BOUND +# A project .miserc.toml cannot ignore global or system config -- documented, not +# inferred from this repo's silence. So ~/.config/mise/config.toml and /etc/mise +# still merge into every project here, and env/get_env() are empty in a project +# miserc so this file cannot even read the environment to compensate. That is +# ADR-0010 M-04: the remaining half of the bound is a machine baseline, and it is +# not enforced by anything in this repository. +ceiling_paths = ["{{ config_root | dirname }}"] diff --git a/MISE-ADOPTION.md b/MISE-ADOPTION.md index a9063e1..6707ca8 100644 --- a/MISE-ADOPTION.md +++ b/MISE-ADOPTION.md @@ -29,3 +29,48 @@ No automatic hooks, environment-file loading, production credentials, deployment ## Installer provenance The smoke workflow pins mise-action to its reviewed commit and mise to 2026.10.25, verifies the extracted Linux x64 binary SHA-256, disables shared caching, and uses a read-only token on a hosted runner. The archive digest was checked against the release API before deriving the binary digest. This is integrity verification, not an independent publisher identity audit. + +## Repository-level bounds added 2026-11-03 (ADR-0010 M-01 and M-02) + +Two files the original proposal did not carry: + +- `.miserc.toml` bounds mise's config discovery to this repository by setting + `ceiling_paths` to the **parent** of the repo root. mise reads `.miserc.toml` + before any `mise.toml`, so the same setting under `[settings]` in `mise.toml` has + no effect on discovery at all. A ceiling directory is not searched itself, which + is why the value names the parent: naming this directory would stop mise loading + the very config the bound exists to protect. +- `mise.toml` gains `[tool_config] locked = true`, making strict lockfile mode a + property of this repository rather than of one CI invocation. + `install_args: --locked` in the workflow binds the job; a developer running plain + `mise install` was never bound by it. `[tool_config]` is not merged and covers + only tools declared in this config root, so it does nothing about tools inherited + from a parent directory -- which is why it has to ship with `.miserc.toml`, not + instead of it. + +`mise-config-scope` in the workflow is a four-leg refusal test, each negative paired +with a positive, because a guard that only asserts absence also passes when the +command printed nothing: + +1. with no ceiling, `mise config` must list a decoy planted one directory above the + checkout and adopt its tool -- proving the next leg is testing something; +2. with the committed `.miserc.toml`, the decoy must be absent **and** every version + this repository declares must still be the one mise actually selects; +3. plain `mise install`, no `--locked` anywhere, must succeed from the committed + lockfile; +4. the same plain install must be refused when `mise.lock` is absent. + +The job reads the declared tool set out of `mise.toml` instead of naming versions, +so one file is correct in every repository that adopts the pattern. + +### Limits, stated rather than implied + +Linux x64 only, in CI, on a hosted runner with `permissions: contents: read`, no +secrets and no deployment path. It is not a required status check. A project +`.miserc.toml` **cannot** ignore global or system config, so `~/.config/mise` and +`/etc/mise` still merge into every project on the machine (ADR-0010 M-04 stays +open). `ceiling_paths` **fails open**: an unrendered template becomes a literal path +matching nothing, silently, because mise reads the file before logging starts -- leg 2 +is what turns the declaration into a control, and deleting that leg deletes the +control. The printed verdicts on this pull request's checks are the evidence; a green +badge is not, and nothing here certifies any version as vulnerability-free. diff --git a/mise.toml b/mise.toml index 3f7d01f..70eb432 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,11 @@ # Initial local toolchain adoption; see MISE-ADOPTION.md for validation limits. +# Strict lockfile mode for the tools declared in this config root (ADR-0010 M-02). +# install_args: --locked in the workflow binds the job, not the repository; a plain +# `mise install` was never bound by it. [tool_config] is not merged and reaches only +# tools declared here, so .miserc.toml must ship with it, not after it. +[tool_config] +locked = true + [tools] python = "3.12.16" node = "22.23.4" From 4f6e0daf4257a8cdac21003a0f5474f050479d04 Mon Sep 17 00:00:00 2001 From: Gintaras Kazlauskas Date: Tue, 3 Nov 2026 18:57:01 +0200 Subject: [PATCH 3/3] fix(ci): keep the scope guard under the strictest yamllint limit in the fleet Propagating the guard broke `ci` in jolarca-data and jolarca-legal, and only in those two, which is the whole point: their ci job runs yamllint with line-length max 140, while jolarca-control's .yamllint allows 160. The file I wrote passed locally at 160 and failed at 140 on line 167 (141 characters). Proven as mine, not pre-existing, by the A/B on the same workflow: jolarca-legal ci was success at 61102be5 and failure at c13a8c4f with only my commit between. Three message lines rewrapped; the file's longest line is now 119 characters, which passes 140 and 120 as well as 160. Verified before pushing with yamllint at all three limits plus each repository's own .yamllint. This is ADR-0010's own argument applied to the toolchain guard: one file is judged by different rules in different repositories, so the portable artifact has to be written to the strictest limit in the fleet rather than to the local one. --- .github/workflows/mise-toolchain.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/mise-toolchain.yml b/.github/workflows/mise-toolchain.yml index 352049a..0900dc9 100644 --- a/.github/workflows/mise-toolchain.yml +++ b/.github/workflows/mise-toolchain.yml @@ -94,7 +94,7 @@ jobs: fi printf '%s\n' "$out" if ! printf '%s' "$out" | grep -q 'bidi-decoy'; then - echo '::error::the decoy is invisible even with no ceiling, so this runner proves nothing about ceiling_paths' + echo '::error::decoy invisible even with no ceiling: this runner cannot test ceiling_paths' exit 1 fi echo 'OK: with no ceiling mise discovers the parent config and adopts its tool.' @@ -159,12 +159,12 @@ jobs: data="$(mktemp -d)" if out="$(MISE_DATA_DIR="$data" mise install "$first" 2>&1)"; then printf '%s\n' "$out" - echo "::error::plain mise install ACCEPTED $first with no lockfile present - locked mode is not a property of the repository" + echo "::error::mise install ACCEPTED $first with no lockfile: locked mode is not a repo property" exit 1 fi printf '%s\n' "$out" if ! printf '%s' "$out" | grep -qi 'lock'; then - echo '::error::install failed, but the refusal does not name the lockfile, so the cause is unrelated and this leg proves nothing' + echo '::error::install failed for a reason other than the lockfile, so this leg proves nothing' exit 1 fi echo 'OK: refused, and the refusal names the lockfile.'