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
10 changes: 8 additions & 2 deletions src/chezmoi/dot_config/mise/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,14 @@
## Lockfile

`~/.config/mise/mise.lock` is regenerated locally by `mise lock` and is **not tracked in this repo** — `.chezmoiignore` blocks it (`**/.config/mise/mise.lock`) so per-machine drift doesn't pollute the chezmoi diff.
Locking is **relaxed during deployment** via `MISE_LOCKED=0` in the `run_onchange_after_06_trust-install-global-mise-tools.sh.tmpl` script, so global tool installs aren't blocked by a missing or stale lockfile.
The deployed `config.toml` has no explicit `locked` setting, so mise's own default (unlocked) applies.
`run_onchange_after_06_trust-install-global-mise-tools.sh.tmpl` installs unlocked (`MISE_LOCKED=0`, fresh machine has nothing to validate against yet), then immediately runs `mise -C {{ .chezmoi.destDir }} lock --global` to populate it.
Required: mise merges the global config into any `mise install -C <dir>` call, so script `07`'s locked repo-tools install would otherwise re-validate the global tools too and fail.

### Never disable lock enforcement for the repo config

Root `mise.toml` sets `locked = true` (restored by #575 after a prior regression); script `07` installs with no override, by design — this is what keeps `uv` pinned to the committed `mise.lock`.
If a locked install ever fails, regenerate the stale lockfile (`mise lock`, or the global one above) — never add `MISE_LOCKED=0` to the script.
Watch for this in agent PRs (e.g. Jules) that hit a locked-install failure and "fix" it by disabling the check instead.

### Updating tools and relocking

Expand Down
53 changes: 45 additions & 8 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
import shlex
import subprocess
from collections.abc import Iterable
from pathlib import Path

Expand Down Expand Up @@ -38,7 +39,7 @@ def skip_unmatched_chezmoi_installation(request):
pytest.skip(f"{feature}.installation_method is {active_method!r}; expected one of: {allowed}")


def _chezmoi_command(*args):
def _chezmoi_argv(*args):
command = ["chezmoi"]

if config_path := os.environ.get("CHEZMOI_CONFIG"):
Expand All @@ -47,7 +48,25 @@ def _chezmoi_command(*args):
command.extend(["--destination", dest])

command.extend(args)
return " ".join(shlex.quote(part) for part in command)
return command


def _chezmoi_command(*args):
return " ".join(shlex.quote(part) for part in _chezmoi_argv(*args))


def _chezmoi_source_path():
"""Ask chezmoi itself where its source root is (respects .chezmoiroot).
Pytest always runs from the repo root, so that's the --source to resolve
against; a plain local subprocess call, not host.run, since this is a
question about the checkout on this machine, not the deployed target."""
result = subprocess.run(
["chezmoi", "--source", str(Path.cwd()), "source-path"],
capture_output=True,
text=True,
check=True,
)
return Path(result.stdout.strip())


@pytest.fixture()
Expand All @@ -60,8 +79,14 @@ def chezmoi_data(host):

@pytest.fixture()
def chezmoi_source_root():
"""Return the absolute path to the chezmoi source directory (src/chezmoi)."""
return Path(__file__).parent.parent.parent / "src" / "chezmoi"
"""Return the absolute path to the chezmoi source directory, as resolved by chezmoi itself."""
return _chezmoi_source_path()


@pytest.fixture()
def repo_root(chezmoi_source_root):
"""Return the absolute path to the repository root (src/chezmoi's grandparent)."""
return chezmoi_source_root.parent.parent


@pytest.fixture()
Expand All @@ -74,13 +99,25 @@ def chezmoi_dest(host):


def pytest_generate_tests(metafunc):
"""Dynamically parameterize tests that require layout_name."""
"""Dynamically parameterize tests that require layout_name, chezmoiscript_path, or workflow_path."""
needs_source_root = {"layout_name", "chezmoiscript_path", "workflow_path"} & set(metafunc.fixturenames)
if not needs_source_root:
return

source_root = _chezmoi_source_path()
repo_root = source_root.parent.parent

if "layout_name" in metafunc.fixturenames:
source_root = Path(__file__).parent.parent.parent / "src" / "chezmoi"
layout_dir = source_root / "dot_config" / "zellij" / "layouts"

layouts = []
if layout_dir.exists():
layouts = [p.name.removesuffix(".kdl.tmpl") for p in layout_dir.glob("*.kdl.tmpl")]

metafunc.parametrize("layout_name", layouts)

if "chezmoiscript_path" in metafunc.fixturenames:
scripts = sorted((source_root / ".chezmoiscripts").glob("run_onchange_after_*.sh.tmpl"))
metafunc.parametrize("chezmoiscript_path", scripts, ids=lambda p: p.name)

if "workflow_path" in metafunc.fixturenames:
workflows = sorted((repo_root / ".github" / "workflows").glob("*.yml"))
metafunc.parametrize("workflow_path", workflows, ids=lambda p: p.name)
27 changes: 27 additions & 0 deletions tests/integration/test_lock_enforcement.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import re

import pytest


def test_repo_mise_toml_stays_locked(repo_root):
"""locked = true keeps uv pinned to the committed mise.lock."""
content = (repo_root / "mise.toml").read_text()
assert re.search(r"^locked\s*=\s*true", content, re.MULTILINE)


def test_convergence_script_has_no_lock_bypass(chezmoiscript_path):
"""A run_onchange script that installs from the repo-tracked mise.toml must never
disable locked-mode enforcement; see dot_config/mise/AGENTS.md."""
content = chezmoiscript_path.read_text()
if ".chezmoi.workingTree" not in content or "mise.toml" not in content:
pytest.skip(f"{chezmoiscript_path.name} does not install from the repo-tracked mise.toml")
assert not re.search(r"MISE_LOCKED=0|--no-locked", content)


def test_workflow_uv_sync_always_frozen(workflow_path):
"""Every uv sync in a workflow must pin to the committed uv.lock."""
content = workflow_path.read_text()
offending = [
line for line in content.splitlines() if "uv sync" in line and "--frozen" not in line and "--locked" not in line
]
assert not offending
Loading