From 1d1e2061deaa5ad0f7c8c6e490136fc29897ce30 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 13:20:39 +0000 Subject: [PATCH 1/2] SRE-805: Validate mise lockfile in CI Add a 'Mise lockfile' job to the Lint workflow that catches broken, stale, or wrong-architecture .config/mise/mise.lock entries: - .github/scripts/validate-mise-lock.py runs offline checks: config.toml pins match locked versions, required platforms are covered, no platform entry points at an asset for a different OS/arch, and no two platforms share a non-universal asset. - 'mise install --locked --dry-run' natively fails on tools missing from the lockfile or lacking pre-resolved URLs for the platform. - A real 'mise install --locked' of URL-locked tools downloads and sha256-verifies the locked artifacts on linux-x64 and linux-arm64. The job skips gracefully while mise.lock is absent on main, and the known wrong-arch yq macos-arm64 entry produced by 'mise lock' upstream is allowlisted as a warning with a tracking comment. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EcBiQCfeizMQJubtqXkiMs --- .github/scripts/validate-mise-lock.py | 221 ++++++++++++++++++++++++++ .github/workflows/lint.yml | 77 ++++++++- 2 files changed, 297 insertions(+), 1 deletion(-) create mode 100755 .github/scripts/validate-mise-lock.py diff --git a/.github/scripts/validate-mise-lock.py b/.github/scripts/validate-mise-lock.py new file mode 100755 index 00000000000..a84943a9195 --- /dev/null +++ b/.github/scripts/validate-mise-lock.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +"""Validate `.config/mise/mise.lock` without network access. + +Checks that the lockfile is consistent with `.config/mise/config.toml` and +internally sane. `mise` itself has no `mise lock --check` (as of 2026.7.0), +and a locked `mise install` only validates entries for the platform it runs +on, so this script covers the cross-platform failure modes: + +1. Staleness: every tool pinned in `config.toml` has a lockfile entry whose + version matches the pin. +2. Coverage: every lockfile entry that carries platform data has an entry for + each platform developers and CI rely on (see REQUIRED_PLATFORMS). +3. Cross-platform sanity: a platform entry's download URL must not point at + an asset built for a different architecture or operating system (e.g. a + `darwin_amd64` asset under a `macos-arm64` key), and two different + platforms must not share the same asset unless it is explicitly universal. + +Exits non-zero on failures. Known upstream bugs can be allowlisted in +KNOWN_BAD_ENTRIES; matching findings are downgraded to warnings. +""" + +from __future__ import annotations + +import re +import sys +import tomllib +from pathlib import Path +from urllib.parse import urlparse + +CONFIG_PATH = Path(".config/mise/config.toml") +LOCK_PATH = Path(".config/mise/mise.lock") + +# Platforms that must be present for every tool that has platform data in the +# lockfile. Extra platforms (e.g. macos-x64) are validated but not required. +REQUIRED_PLATFORMS = frozenset({"linux-x64", "linux-arm64", "macos-arm64"}) + +# Substrings in an asset filename that contradict the platform key's +# architecture. Only unambiguous markers are listed to avoid false positives. +ARCH_CONFLICTS = { + "x64": ("arm64", "aarch64", "aarch_64", "aarch-64", "armv6", "armv7"), + "arm64": ("amd64", "x86_64", "x86-64", "i686", "i386"), +} + +# Substrings in an asset filename that contradict the platform key's OS. +OS_CONFLICTS = { + "linux": ("darwin", "macos", "osx", "apple", "windows", "win32", "win64", ".exe"), + "macos": ("linux", "musl", "windows", "win32", "win64", ".exe"), +} + +# Asset name markers for genuinely platform-independent artifacts, exempt +# from the duplicate-URL check. +UNIVERSAL_MARKERS = ("universal", "noarch", "any", "all") + +# (tool, platform, exact URL) entries excused from cross-platform sanity +# checks. Findings on these entries are reported as warnings instead of +# failures. Remove an entry once the underlying bug is fixed and the +# lockfile has been regenerated with the correct asset. +KNOWN_BAD_ENTRIES = { + # `mise lock` (2026.7.0) resolves the darwin_amd64 asset for yq's + # macos-arm64 entry, mirroring the macos-x64 entry byte-for-byte, even + # though a native yq_darwin_arm64 asset exists in the release. Found in + # hashintel/hash#9000; reported upstream to jdx/mise. Intel-mac binaries + # run under Rosetta 2, so this is a performance bug, not a breakage. + ( + "yq", + "macos-arm64", + "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_darwin_amd64", + ), +} + +failures: list[str] = [] +warnings: list[str] = [] + + +def report(message: str, *, known_bad: bool) -> None: + if known_bad: + warnings.append(message) + else: + failures.append(message) + + +def is_known_bad(tool: str, platform: str, url: str | None) -> bool: + return (tool, platform, url) in KNOWN_BAD_ENTRIES + + +def version_matches(pin: str, locked: str) -> bool: + """Match the way mise resolves pins: exact, or pin as version prefix.""" + return locked == pin or locked.startswith(f"{pin}.") + + +def asset_name(url: str) -> str: + return urlparse(url).path.rsplit("/", 1)[-1].lower() + + +def check_staleness(config: dict, lock_tools: dict) -> None: + for tool, spec in config.get("tools", {}).items(): + pin = spec["version"] if isinstance(spec, dict) else spec + if not isinstance(pin, str): + continue + entries = lock_tools.get(tool) + if not entries: + report( + f"{tool}: pinned to {pin} in {CONFIG_PATH} but missing from " + f"{LOCK_PATH} — run `mise lock` and commit the result", + known_bad=False, + ) + continue + locked_versions = [e.get("version", "") for e in entries] + if not any(version_matches(pin, v) for v in locked_versions): + report( + f"{tool}: pinned to {pin} in {CONFIG_PATH} but locked at " + f"{', '.join(locked_versions)} — run `mise lock` and commit " + f"the result", + known_bad=False, + ) + + +def platform_entries(entry: dict) -> dict[str, dict]: + """Extract `platforms.` sub-tables from one lockfile tool entry.""" + result = {} + for key, value in entry.items(): + if key.startswith("platforms.") and isinstance(value, dict): + result[key.removeprefix("platforms.")] = value + return result + + +def check_platform_coverage(tool: str, platforms: dict[str, dict]) -> None: + missing = REQUIRED_PLATFORMS - platforms.keys() + if missing: + report( + f"{tool}: lockfile is missing platform entries for " + f"{', '.join(sorted(missing))} — run " + f"`mise lock --platform {','.join(sorted(REQUIRED_PLATFORMS))}`", + known_bad=False, + ) + + +def check_asset_sanity(tool: str, platforms: dict[str, dict]) -> None: + for platform, info in platforms.items(): + url = info.get("url") + if not url: + continue + os_key, _, arch_key = platform.partition("-") + name = asset_name(url) + tokens = set(re.split(r"[^a-z0-9]+", name)) + + conflicts = [ + marker + for marker in ARCH_CONFLICTS.get(arch_key, ()) + if marker in name + ] + # Short markers like "x64" are only checked as whole tokens since + # they are substrings of unrelated words. + if arch_key == "arm64" and "x64" in tokens: + conflicts.append("x64") + conflicts += [ + marker for marker in OS_CONFLICTS.get(os_key, ()) if marker in name + ] + if conflicts: + report( + f"{tool}: platforms.{platform} URL points at an asset for a " + f"different platform (matched {', '.join(sorted(set(conflicts)))}): {url}", + known_bad=is_known_bad(tool, platform, url), + ) + + # Two different platforms sharing one asset is the signature of the + # wrong-asset bug class, unless the asset is explicitly universal. + by_url: dict[str, list[str]] = {} + for platform, info in sorted(platforms.items()): + if info.get("url"): + by_url.setdefault(info["url"], []).append(platform) + for url, keys in by_url.items(): + if len(keys) < 2: + continue + if any(marker in set(re.split(r"[^a-z0-9]+", asset_name(url))) for marker in UNIVERSAL_MARKERS): + continue + report( + f"{tool}: platforms {', '.join(keys)} share the same asset, " + f"which is not marked universal: {url}", + known_bad=any(is_known_bad(tool, key, url) for key in keys), + ) + + +def main() -> int: + if not LOCK_PATH.is_file(): + print(f"{LOCK_PATH} not found — nothing to validate") + return 0 + + with CONFIG_PATH.open("rb") as f: + config = tomllib.load(f) + with LOCK_PATH.open("rb") as f: + lock = tomllib.load(f) + + lock_tools: dict[str, list[dict]] = lock.get("tools", {}) + + check_staleness(config, lock_tools) + for tool, entries in lock_tools.items(): + for entry in entries: + platforms = platform_entries(entry) + if not platforms: + # Backends such as cargo:/npm:/pipx: build or fetch through + # their own package manager and carry no per-platform URLs. + continue + check_platform_coverage(tool, platforms) + check_asset_sanity(tool, platforms) + + for message in warnings: + print(f"::warning title=mise.lock (known upstream bug)::{message}") + for message in failures: + print(f"::error title=mise.lock::{message}") + + checked = sum(1 for entries in lock_tools.values() for e in entries) + print( + f"Checked {checked} lockfile entries: " + f"{len(failures)} failure(s), {len(warnings)} known-bad warning(s)" + ) + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c8b85c97272..e68433c4017 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -397,9 +397,81 @@ jobs: title: "SQLFluff Lint" input: "annotations.json" + mise-lock: + name: Mise lockfile (${{ matrix.runner }}) + permissions: + contents: read + strategy: + fail-fast: false + matrix: + runner: [ubuntu-24.04, ubuntu-24.04-arm] + runs-on: ${{ matrix.runner }} + steps: + - name: Checkout source code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # The lockfile does not exist on `main` yet (it lands with #9000), so + # skip gracefully when it is absent instead of failing. + - name: Check for lockfile + id: lockfile + run: echo "exists=$([[ -f .config/mise/mise.lock ]] && echo true || echo false)" | tee -a $GITHUB_OUTPUT + + # Offline consistency checks: the lockfile matches the `config.toml` + # pins, covers all required platforms, and no platform entry points at + # an asset built for a different OS/architecture (e.g. a darwin_amd64 + # asset under a macos-arm64 key). + - name: Validate lockfile consistency + if: steps.lockfile.outputs.exists == 'true' + run: python3 .github/scripts/validate-mise-lock.py + + - name: Install mise + if: steps.lockfile.outputs.exists == 'true' + uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 + with: + install: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # `--locked` makes mise fail if any configured tool is missing from the + # lockfile or, for URL-locked backends, has no pre-resolved URL for + # this platform. `--dry-run` validates without downloading. + - name: Validate lockfile covers configured tools + if: steps.lockfile.outputs.exists == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: mise install --locked --dry-run + + # Actually install the tools that have pre-resolved URLs for this + # platform, so the locked URLs are downloaded and their sha256 + # checksums and sizes are verified by mise. This catches a broken or + # tampered lockfile entry before the (slower) Docker builds do. + - name: Install URL-locked tools + if: steps.lockfile.outputs.exists == 'true' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TOOLS=$(python3 - <<'EOF' + import platform + import tomllib + + arch = {"x86_64": "x64", "aarch64": "arm64"}[platform.machine()] + key = f"platforms.linux-{arch}" + with open(".config/mise/mise.lock", "rb") as f: + tools = tomllib.load(f).get("tools", {}) + print(" ".join(sorted({ + name + for name, entries in tools.items() + for entry in entries + if entry.get(key, {}).get("url") + }))) + EOF + ) + echo "Installing: $TOOLS" + mise install --locked $TOOLS + passed: name: Linting passed - needs: [setup, package, global] + needs: [setup, package, global, mise-lock] if: always() runs-on: ubuntu-latest steps: @@ -412,6 +484,9 @@ jobs: - name: Check global results run: | [[ ${{ needs.global.result }} =~ success|skipped ]] + - name: Check mise lockfile results + run: | + [[ ${{ needs.mise-lock.result }} =~ success|skipped ]] - name: Notify Slack on failure uses: rtCamp/action-slack-notify@c58b60ee33df2229ed2d2eed86eeaf7e6c527c5a From 057b5014376f8f11b5f66441915be15763bdeb9a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 13:29:15 +0000 Subject: [PATCH 2/2] Drop custom lockfile script in favor of mise-native validation only Per review: no Python validator for the lockfile. The mise-lock job now relies solely on mise itself: - 'mise install --locked --dry-run' fails on tools missing from the lockfile or lacking pre-resolved URLs for the current platform. - 'mise install --locked ' downloads the locked URLs and verifies sha256 checksums and sizes natively. The wrong-arch asset class (e.g. the upstream yq macos-arm64 bug) is intentionally no longer checked in CI. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EcBiQCfeizMQJubtqXkiMs --- .github/scripts/validate-mise-lock.py | 221 -------------------------- .github/workflows/lint.yml | 39 +---- 2 files changed, 8 insertions(+), 252 deletions(-) delete mode 100755 .github/scripts/validate-mise-lock.py diff --git a/.github/scripts/validate-mise-lock.py b/.github/scripts/validate-mise-lock.py deleted file mode 100755 index a84943a9195..00000000000 --- a/.github/scripts/validate-mise-lock.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -"""Validate `.config/mise/mise.lock` without network access. - -Checks that the lockfile is consistent with `.config/mise/config.toml` and -internally sane. `mise` itself has no `mise lock --check` (as of 2026.7.0), -and a locked `mise install` only validates entries for the platform it runs -on, so this script covers the cross-platform failure modes: - -1. Staleness: every tool pinned in `config.toml` has a lockfile entry whose - version matches the pin. -2. Coverage: every lockfile entry that carries platform data has an entry for - each platform developers and CI rely on (see REQUIRED_PLATFORMS). -3. Cross-platform sanity: a platform entry's download URL must not point at - an asset built for a different architecture or operating system (e.g. a - `darwin_amd64` asset under a `macos-arm64` key), and two different - platforms must not share the same asset unless it is explicitly universal. - -Exits non-zero on failures. Known upstream bugs can be allowlisted in -KNOWN_BAD_ENTRIES; matching findings are downgraded to warnings. -""" - -from __future__ import annotations - -import re -import sys -import tomllib -from pathlib import Path -from urllib.parse import urlparse - -CONFIG_PATH = Path(".config/mise/config.toml") -LOCK_PATH = Path(".config/mise/mise.lock") - -# Platforms that must be present for every tool that has platform data in the -# lockfile. Extra platforms (e.g. macos-x64) are validated but not required. -REQUIRED_PLATFORMS = frozenset({"linux-x64", "linux-arm64", "macos-arm64"}) - -# Substrings in an asset filename that contradict the platform key's -# architecture. Only unambiguous markers are listed to avoid false positives. -ARCH_CONFLICTS = { - "x64": ("arm64", "aarch64", "aarch_64", "aarch-64", "armv6", "armv7"), - "arm64": ("amd64", "x86_64", "x86-64", "i686", "i386"), -} - -# Substrings in an asset filename that contradict the platform key's OS. -OS_CONFLICTS = { - "linux": ("darwin", "macos", "osx", "apple", "windows", "win32", "win64", ".exe"), - "macos": ("linux", "musl", "windows", "win32", "win64", ".exe"), -} - -# Asset name markers for genuinely platform-independent artifacts, exempt -# from the duplicate-URL check. -UNIVERSAL_MARKERS = ("universal", "noarch", "any", "all") - -# (tool, platform, exact URL) entries excused from cross-platform sanity -# checks. Findings on these entries are reported as warnings instead of -# failures. Remove an entry once the underlying bug is fixed and the -# lockfile has been regenerated with the correct asset. -KNOWN_BAD_ENTRIES = { - # `mise lock` (2026.7.0) resolves the darwin_amd64 asset for yq's - # macos-arm64 entry, mirroring the macos-x64 entry byte-for-byte, even - # though a native yq_darwin_arm64 asset exists in the release. Found in - # hashintel/hash#9000; reported upstream to jdx/mise. Intel-mac binaries - # run under Rosetta 2, so this is a performance bug, not a breakage. - ( - "yq", - "macos-arm64", - "https://github.com/mikefarah/yq/releases/download/v4.53.2/yq_darwin_amd64", - ), -} - -failures: list[str] = [] -warnings: list[str] = [] - - -def report(message: str, *, known_bad: bool) -> None: - if known_bad: - warnings.append(message) - else: - failures.append(message) - - -def is_known_bad(tool: str, platform: str, url: str | None) -> bool: - return (tool, platform, url) in KNOWN_BAD_ENTRIES - - -def version_matches(pin: str, locked: str) -> bool: - """Match the way mise resolves pins: exact, or pin as version prefix.""" - return locked == pin or locked.startswith(f"{pin}.") - - -def asset_name(url: str) -> str: - return urlparse(url).path.rsplit("/", 1)[-1].lower() - - -def check_staleness(config: dict, lock_tools: dict) -> None: - for tool, spec in config.get("tools", {}).items(): - pin = spec["version"] if isinstance(spec, dict) else spec - if not isinstance(pin, str): - continue - entries = lock_tools.get(tool) - if not entries: - report( - f"{tool}: pinned to {pin} in {CONFIG_PATH} but missing from " - f"{LOCK_PATH} — run `mise lock` and commit the result", - known_bad=False, - ) - continue - locked_versions = [e.get("version", "") for e in entries] - if not any(version_matches(pin, v) for v in locked_versions): - report( - f"{tool}: pinned to {pin} in {CONFIG_PATH} but locked at " - f"{', '.join(locked_versions)} — run `mise lock` and commit " - f"the result", - known_bad=False, - ) - - -def platform_entries(entry: dict) -> dict[str, dict]: - """Extract `platforms.` sub-tables from one lockfile tool entry.""" - result = {} - for key, value in entry.items(): - if key.startswith("platforms.") and isinstance(value, dict): - result[key.removeprefix("platforms.")] = value - return result - - -def check_platform_coverage(tool: str, platforms: dict[str, dict]) -> None: - missing = REQUIRED_PLATFORMS - platforms.keys() - if missing: - report( - f"{tool}: lockfile is missing platform entries for " - f"{', '.join(sorted(missing))} — run " - f"`mise lock --platform {','.join(sorted(REQUIRED_PLATFORMS))}`", - known_bad=False, - ) - - -def check_asset_sanity(tool: str, platforms: dict[str, dict]) -> None: - for platform, info in platforms.items(): - url = info.get("url") - if not url: - continue - os_key, _, arch_key = platform.partition("-") - name = asset_name(url) - tokens = set(re.split(r"[^a-z0-9]+", name)) - - conflicts = [ - marker - for marker in ARCH_CONFLICTS.get(arch_key, ()) - if marker in name - ] - # Short markers like "x64" are only checked as whole tokens since - # they are substrings of unrelated words. - if arch_key == "arm64" and "x64" in tokens: - conflicts.append("x64") - conflicts += [ - marker for marker in OS_CONFLICTS.get(os_key, ()) if marker in name - ] - if conflicts: - report( - f"{tool}: platforms.{platform} URL points at an asset for a " - f"different platform (matched {', '.join(sorted(set(conflicts)))}): {url}", - known_bad=is_known_bad(tool, platform, url), - ) - - # Two different platforms sharing one asset is the signature of the - # wrong-asset bug class, unless the asset is explicitly universal. - by_url: dict[str, list[str]] = {} - for platform, info in sorted(platforms.items()): - if info.get("url"): - by_url.setdefault(info["url"], []).append(platform) - for url, keys in by_url.items(): - if len(keys) < 2: - continue - if any(marker in set(re.split(r"[^a-z0-9]+", asset_name(url))) for marker in UNIVERSAL_MARKERS): - continue - report( - f"{tool}: platforms {', '.join(keys)} share the same asset, " - f"which is not marked universal: {url}", - known_bad=any(is_known_bad(tool, key, url) for key in keys), - ) - - -def main() -> int: - if not LOCK_PATH.is_file(): - print(f"{LOCK_PATH} not found — nothing to validate") - return 0 - - with CONFIG_PATH.open("rb") as f: - config = tomllib.load(f) - with LOCK_PATH.open("rb") as f: - lock = tomllib.load(f) - - lock_tools: dict[str, list[dict]] = lock.get("tools", {}) - - check_staleness(config, lock_tools) - for tool, entries in lock_tools.items(): - for entry in entries: - platforms = platform_entries(entry) - if not platforms: - # Backends such as cargo:/npm:/pipx: build or fetch through - # their own package manager and carry no per-platform URLs. - continue - check_platform_coverage(tool, platforms) - check_asset_sanity(tool, platforms) - - for message in warnings: - print(f"::warning title=mise.lock (known upstream bug)::{message}") - for message in failures: - print(f"::error title=mise.lock::{message}") - - checked = sum(1 for entries in lock_tools.values() for e in entries) - print( - f"Checked {checked} lockfile entries: " - f"{len(failures)} failure(s), {len(warnings)} known-bad warning(s)" - ) - return 1 if failures else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e68433c4017..fc226992c3e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -416,14 +416,6 @@ jobs: id: lockfile run: echo "exists=$([[ -f .config/mise/mise.lock ]] && echo true || echo false)" | tee -a $GITHUB_OUTPUT - # Offline consistency checks: the lockfile matches the `config.toml` - # pins, covers all required platforms, and no platform entry points at - # an asset built for a different OS/architecture (e.g. a darwin_amd64 - # asset under a macos-arm64 key). - - name: Validate lockfile consistency - if: steps.lockfile.outputs.exists == 'true' - run: python3 .github/scripts/validate-mise-lock.py - - name: Install mise if: steps.lockfile.outputs.exists == 'true' uses: jdx/mise-action@146a28175021df8ca24f8ee1828cc2a60f980bd5 # v3.5.1 @@ -441,33 +433,18 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: mise install --locked --dry-run - # Actually install the tools that have pre-resolved URLs for this - # platform, so the locked URLs are downloaded and their sha256 - # checksums and sizes are verified by mise. This catches a broken or - # tampered lockfile entry before the (slower) Docker builds do. + # Download the tools that have pre-resolved URLs in the lockfile so + # mise verifies their sha256 checksums and sizes. This catches a broken + # or tampered lockfile entry before the (slower) Docker builds do. + # Keep the list in sync with the tools carrying `platforms.*` entries + # in `mise.lock`; the dry-run step above already fails on tools missing + # from the lockfile. Cargo/npm/pipx tools are excluded, as they install + # through their own package managers and have no locked URLs to verify. - name: Install URL-locked tools if: steps.lockfile.outputs.exists == 'true' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - TOOLS=$(python3 - <<'EOF' - import platform - import tomllib - - arch = {"x86_64": "x64", "aarch64": "arm64"}[platform.machine()] - key = f"platforms.linux-{arch}" - with open(".config/mise/mise.lock", "rb") as f: - tools = tomllib.load(f).get("tools", {}) - print(" ".join(sorted({ - name - for name, entries in tools.items() - for entry in entries - if entry.get(key, {}).get("url") - }))) - EOF - ) - echo "Installing: $TOOLS" - mise install --locked $TOOLS + run: mise install --locked cargo-binstall java just node protoc sentry-cli taplo uv yq passed: name: Linting passed