Skip to content

km 1.10.1: hold the bump branch only while its head is the workflow's own - #47

Merged
pasrom merged 2 commits into
mainfrom
release/1.10.1
Nov 8, 2026
Merged

pasrom merged 2 commits into
mainfrom
release/1.10.1

Conversation

@pasrom

@pasrom pasrom commented Nov 7, 2026

Copy link
Copy Markdown
Owner

What

A security fix for the team template bump-mounted-pins, and the release that ships it.

When a mount fails to resolve and the open pull request's head moves that mount, --hold-if-moved leaves the bump branch untouched, so the workflow's proposed move is not lost over a failure that may pass. In 1.10.0 the hold applied to any head, including one that --branch-prs rejects (not exactly one commit ahead of main, or touching files other than gitlinks). So a commit pushed onto the bump branch by hand survived under the workflow's pull request whenever a mount failed at the same time.

Fix

The workflow now passes --hold-if-moved only when open and seen are both true. seen is the result --branch-prs already returns in the same step: a pull request had this head, ahead_by == 1 against main, and the compare lists a non-empty set of files that are all submodule paths. No second compare call is made. Any other head is regenerated, and the failing mount stays at main's pin until it resolves again.

moves() keeps its fail-safe (True when the gitlink comparison raises). Its input is now only a head that passed that validation, and the step fetches the branch with --depth 2, so the parent is always present. The fail-safe only fires on something unexpected, and it then preserves nothing but the workflow's own one-commit proposal.

The script's docstring documents that --hold-if-moved takes a head --branch-prs accepted. SHIPPED lists the new hashes of both templates next to the 1.10.0 ones, so a brain with an unchanged 1.10.0 copy is still recognised and gets replaced by km upgrade.

The second commit bumps the version to 1.10.1 (src/km/__init__.py, .claude-plugin/plugin.json, the README examples), as earlier releases did.

Tests

  • New smoke case: a commit pushed by hand on top of the workflow's commit moves a mount that then fails, while another mount moves. The branch is regenerated (changed=true problems=1). Without the seen gate it reads changed=false problems=1.
  • Existing case kept: the workflow's own valid head moves a failing mount, so nothing is pushed (changed=false problems=1). With the hold removed entirely it reads changed=true problems=1.
  • The smoke test's stand-in script no longer moves a mount that fails, matching the real resolve(). Without that, the new case would have passed for the wrong reason. The hold cases now use a second mount that does move.
  • All tests/*_smoke.sh pass. The resolve step needs bash 4, so mounted_pins_smoke.sh was also run under bash 5 in a python:3.12-slim container, with both mutations checked there.
  • A live check against the GitHub compare API confirmed that a submodule-only commit lists the gitlink path under files with ahead_by: 1, so seen is reachable for the workflow's own commits.

Review

/simplify (reuse, simplification, efficiency, altitude): applied the loop form of branch_with and shortened the duplicated comments and the help text. Efficiency and altitude found nothing. Altitude agreed the workflow is the right place for the check: only that step has both the pull request lookup and the compare result, and a local check in the script (REF^ == main) would disable the hold whenever main moves after the bump commit was built.

/code-review:

  • Applied: the stand-in moved a failing mount, which the real script never does, so the new case passed for the wrong reason. Fixed as described above.
  • Applied: the commit message no longer claims the regeneration drops nothing. A move of the failing mount that the workflow had proposed is dropped until the mount resolves again, then proposed again. That is accepted: keeping it would mean trusting part of a branch someone else pushed to.
  • Refuted by a live API check: the concern that the compare API might omit gitlink entries, which would leave seen always false.
  • Not applied, script-side enforcement of the precondition: the script runs under the read-only token of the mounted repositories and has no compare result for this repository. Enforcing it there would need a second compare call, or a local ancestry check that breaks once main moves. The only caller is the workflow, and the new smoke case pins the wiring.
  • Not applied, residual noted: when a rejected head is on the branch and nothing at all moves (every mount up to date, or the only mount failing), the step exits with changed=false and the hand-pushed commit stays under the pull request. That was already so in 1.10.0, independently of the hold. Regenerating then would mean closing the pull request or deleting the branch, which is a larger behaviour change. Setup step 5 in the workflow header ("Require approval of the most recent reviewable push") keeps such a commit from being merged without someone else's review.

Also left as is: a single hand-pushed commit that moves only gitlinks and is one commit ahead of main passes --branch-prs, as it already did for the "already proposes these pins" check. Setup step 5 is the mitigation there too.

… own

bump-mounted-pins leaves the open pull request as it is when a mount
fails that the branch head moves (--hold-if-moved), so the proposed
move is not lost over a passing failure. The hold applied to any head,
including one --branch-prs rejects: a commit pushed by hand onto the
branch then survived under the workflow's pull request on every day a
mount failed.

The hold now also needs the head that --branch-prs accepted (seen: one
commit on main changing gitlinks only), the result the step already
has. Any other head is regenerated and the failing mount stays at
main's pin until it resolves again; a move of it the workflow had
proposed comes back then. The script documents that --hold-if-moved
takes such a head, and SHIPPED lists both templates' new hashes next
to the old ones.

The smoke test's stand-in script no longer moves a mount that fails,
as the real one does not, so the hold cases run with a second mount
that does move.
A team brain's bump-mounted-pins workflow holds its open pull request
over a failing mount only while the branch head is the workflow's own
commit; a commit pushed onto the branch by hand is regenerated away
instead of staying under the workflow's pull request. km upgrade
replaces an unchanged 1.10.0 copy of the workflow and its script.
Without the bump no brain or plugin install would see the fix.
@pasrom
pasrom merged commit f577a64 into main Nov 8, 2026
4 checks passed
@pasrom
pasrom deleted the release/1.10.1 branch November 8, 2026 07:14
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.

1 participant