Skip to content

ci: let Relock commit its lock onto any base branch - #483

Merged
hellices merged 3 commits into
mainfrom
fix/481-relock-propose-base
Oct 25, 2026
Merged

hellices merged 3 commits into
mainfrom
fix/481-relock-propose-base

Conversation

@hellices

@hellices hellices commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #481

Summary

The Relock workflow's propose job downloaded the verified uv.lock over its sparse checkout of main, then switched to the tested base revision with git checkout -b "$branch" "$BASE_SHA". When the base's uv.lock differs from main's, git refuses to carry that local change across:

error: Your local changes to the following files would be overwritten by checkout:
	uv.lock

Every relock onto such a branch therefore failed after all its verification. Runs 40155755520 and 40158406513 hit it on #482's branch, and both locks had to be committed by hand. Earlier runs all used base=main, whose tip is the tested revision, so the switch changed nothing.

  • The fix: the artifact now downloads to $RUNNER_TEMP/relocked-lock, outside the checkout.
    • The host check (with main's validator) and the digest match run on it there, before the token is exposed, as before.
    • Only after the base revision is checked out is it copied in.
    • The file about to be staged is then matched to the digest once more, before git add --sparse uv.lock.
  • Unchanged: the trust anchor (the validator still comes from main), the write-token boundary, and the branch-from-BASE_SHA rule.
  • Docs: the Relock section of docs/dev/quality-gates.md now says the base can be any branch and describes the second digest match.

Verification

  • The failure, reproduced: tests/test_lockfile.py::test_the_verified_lock_is_committed_onto_any_base runs the job's own steps against a temporary repository: a bare origin, a sparse checkout of main, the artifact placed where download-artifact puts it, and a stub gh. It is parametrized over three bases: main itself, a branch that shares main's lock, and a branch with its own lock. Before the change, the last case failed with CI's exact error and the first two passed, matching real runs.
  • Now: all three push the verified bytes, and nothing else, onto the tested revision.
  • The gh stub (after review) records each call's exact arguments and accepts only gh auth setup-git and gh pr create. The test asserts the whole call sequence, so a workflow that also ran gh pr merge would fail it, whether or not the call is guarded with || true. Both forms were checked by mutation. The pr create options must be exactly --base, --head, --title and --body, in that order, with the expected values, so an added --repo or a stray argument also fails the test.
  • Second digest match: test_the_committed_bytes_are_the_bytes_that_were_verified now also pins it between the copy and git add. Removing that match, or moving it before the copy, fails the test with a message naming the problem.
  • Mutation: putting the artifact back over the checkout fails the new test with CI's exact error.
  • Platform: the new test is skipped on Windows, because the workflow runs only on Linux runners. Elsewhere it uses GNU sha256sum, falling back to shasum -a 256 (same options) where the system one is BSD's.
  • zizmor: uvx zizmor .github/workflows/relock.yml reports no findings, with the same 4 suppressed as main's copy.

Run locally on macOS with Python 3.13 and the frozen lock. Each step ran directly with the project's environment rather than through make, because this machine cannot sync from PyPI:

Command Result
pytest -p no:tach tests/test_lockfile.py tests/test_docs_links.py tests/test_docs_build_config.py tests/test_ci_workflow.py 161 passed
python scripts/check_source_size.py passed
ruff check src/ tests/ and ruff format --check src/ tests/ passed; 659 files already formatted
SKIP=mypy,source-size pre-commit run --files <changed files> passed
mypy no issues in 659 source files
tach check all modules validated
pytest -n 8 -q 13,758 passed, 25 skipped

The workflow itself only runs on dispatch. The next Relock run on a non-main base will be the first end-to-end use.

This PR's CI on the final head (6a63a41f, hosted runners, four xdist workers) passed every required check: pre-commit, security (including zizmor), dependency-review, analyze, and:

Job Result
test (3.11) passed, 13m11s
test (3.12) passed, 16m52s
test (3.13) passed, 13m44s
windows-test passed, 11m19s (the new test is skipped there)

Checklist

  • This closes an accepted issue in this repository that is assigned to me.
  • Tests cover the change, written to fail first where behavior changed.
  • make check passes locally.
  • User-facing docs and the README match the change, or nothing user-facing changed.
  • No security invariant is weakened: writes still need a keystroke approval
    and a fail-closed audit entry, and sensitive reads are still masked.

The propose job downloaded the verified lock over its sparse checkout of
main, then switched to the tested base revision. When the base's uv.lock
differs from main's, git refuses to carry that local change across
("Your local changes ... would be overwritten by checkout"), so every
relock onto such a branch failed after all its verification. Runs
40155755520 and 40158406513 hit it on the bundle branch, and their locks
had to be committed by hand (#481). Earlier runs all used base=main,
whose tip is the tested revision, so the switch changed nothing.

The artifact now downloads to $RUNNER_TEMP/relocked-lock, outside the
checkout. The host check and the digest match run on it there, with
main's validator, before the token is exposed, as before. Only after the
base revision is checked out is it copied in, and the file about to be
staged is matched to the digest once more.

test_the_verified_lock_is_committed_onto_any_base runs the job's own
steps against a sparse checkout of main in a temporary repository, with
a stub gh. It covers three bases: main itself, a branch that shares
main's lock, and a branch with its own lock. The last failed with CI's
exact error before the change, and all three now push the verified
bytes, and nothing else, onto the tested revision.
test_the_committed_bytes_are_the_bytes_that_were_verified now also pins
the second digest match between the copy and `git add`; removing it or
moving it before the copy fails that test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings October 25, 2026 05:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Required CI completion remains unconfirmed for this write-enabled workflow change.

No open issues 🎉

What changed in this PR

Fixes #481 by allowing Relock to commit its verified lock onto a base branch whose uv.lock differs from main’s, without changing the existing validation or token boundaries.

Changes:

  • Downloads the artifact outside the checkout, copies it after switching to the tested revision, and rechecks its digest before staging.
  • Adds regression coverage for three base-branch scenarios and the copy/check/stage order.
  • Documents support for arbitrary base branches and the second digest check.
File Description
tests/​test_lockfile.py Adds base-branch regression tests and digest-order assertions.
docs/​dev/​quality-gates.md Explains artifact placement and validation before staging.
.github/​workflows/​relock.yml Defers copying the verified lock until after checkout of the tested base.

Review effort: Balanced

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이슈 1건을 인라인 코멘트로 남겼습니다.

Comment thread tests/test_lockfile.py

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused fix addresses the checkout failure, preserves validation boundaries, and includes regression coverage with no unresolved findings.

No open issues 🎉

Review effort: Balanced

The stub recorded every gh call and always succeeded, and the test only
looked for the one `pr create` line. So a workflow that also ran, say,
`gh pr merge "$branch" --squash || true` would pass while doing exactly
what the repository forbids. The stub now writes each call's exact argv,
NUL-separated, to its own numbered file, and accepts only `gh auth
setup-git` and `gh pr create`; any other call fails the step and stays
on record. The test asserts the whole call sequence, not one line.

Injecting `gh pr merge "$branch" --squash || true` after the push now
fails the call-set assertion, and the unguarded form fails the step with
"unexpected gh call"; both passed before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

No blocking code issues were identified, but required test and analysis checks for the current head are still running.

No open issues 🎉

Review effort: Balanced

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이슈 1건을 인라인 코멘트로 남겼습니다.

Comment thread tests/test_lockfile.py
The stub accepted any `pr create`, and the test compared only the first
four arguments after it, so appending `--repo other/target` (or a stray
argument) to the workflow's call kept the recorded prefix and the call
count intact and passed, while the real pull request would target
another repository. The test now requires the options to be exactly
--base, --head, --title and --body, in that order, pairs them, and
checks each value: the base, the pushed branch, the relock title, and a
body that names the tested revision. Comparing the whole argv would copy
the body's prose into the test, so the option names pin the shape and
the values pin what matters.

Adding `--repo other/target` or a positional argument to the call now
fails the test; both passed before.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Required test and analysis checks on the current revision are still running, so the green-check prerequisite remains unconfirmed.

No open issues 🎉

Review effort: Balanced

@my-reviewer-agent my-reviewer-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

검토 범위에서 보고할 이슈가 없습니다.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The focused checkout-order fix preserves validation and credential isolation, with regression coverage for the affected scenarios and no unresolved findings.

No open issues 🎉

Review effort: Balanced

@hellices
hellices merged commit 1df98ce into main Oct 25, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relock: propose fails when the base branch's uv.lock differs from main's

2 participants