Skip to content

Add the repin bot - #56

Merged
danielhanchen merged 1 commit into
masterfrom
repin-bot
Aug 3, 2026
Merged

Add the repin bot#56
danielhanchen merged 1 commit into
masterfrom
repin-bot

Conversation

@danielhanchen

Copy link
Copy Markdown
Member

Four nightlies failed in a week, every one of them a pinned PR that merged yesterday and stopped merging today because the base tag moved under it. Preflight (#53) tells us that a few hours early. This does the other half: the merge itself.

What it does

On a preflight failure, for each pin in scripts/unsloth/pr-set.json:

  • merge the current base tag into the pin's branch
  • resolve conflicts that are provably pure add/add, and only those
  • push the merged branches and open a PR moving the pins

It opens the PR and stops. It does not merge it.

What it refuses to do

The pin file exists so that only reviewed code ships, so the bot is written to be unable to widen it:

  • Branches we do not own. pwilkin/llama.cpp (model: add Kimi-K3 text model ggml-org/llama.cpp#26185) gets a report line, never a push. Only unslothai/* and danielhanchen/* are writable.
  • Pins the author has moved past. If the branch head is ahead of the pin, repinning would pull in commits nobody reviewed. It reports and stops. All four pins are currently at their branch head, so this costs nothing today.
  • Conflicts it cannot prove are safe. See below.

The add/add rule

scripts/unsloth/additive_merge.py re-reads the conflict in diff3 style and resolves a hunk only when the merge base section is empty, which means both sides added text where there was none and neither edited the other's. That is exactly the recurring case LLM_ARCH_FOO: fallthrough collision. It additionally refuses when the two sides share a line, because that is one change made twice rather than two independent additions.

Anything else, including the common case of two PRs each appending a term to the same if condition, is left conflicted and reported. It never picks a side.

Decisions are made for every conflicted file before any of them is written, so a refusal on the second file cannot leave the first one half-resolved.

Verification

scripts/unsloth/test_additive_merge.py builds real git conflicts and covers the resolve, the refusals, the partial-write guard and the dry run. 14 assertions, all passing.

The one that matters is the replay of the actual 08-02 breakage: base b10229, Inkling pinned at 02142bbc, colliding with upstream's new LLM_ARCH_DEEPSEEK4. The resolver reproduces the resolution I made by hand in 1e6f9e4a byte for byte.

Ran end to end against the live repos on today's base b10236:

pin outcome
ggml-org#24423 clean merge, repinned
ggml-org#25731 clean merge, repinned
unslothai#48 1 add/add hunk resolved (src/llama-arch.cpp, DEEPSEEK4/INKLING), repinned
ggml-org#26185 not ours, reported

REPIN_TOKEN

The plan was for the bot to work without a workflow-scoped token. It cannot: pushing to danielhanchen/llama.cpp is cross-repo, which GITHUB_TOKEN cannot do at all, and the merges carry upstream's own .github/workflows changes, which needs workflow write. Dropping those paths from the merge would leave the branch permanently diverged there, which is worse.

So it needs a REPIN_TOKEN secret with contents and workflow write on unslothai/llama.cpp and danielhanchen/llama.cpp. Until that exists the bot still runs every step except the push, and files the report as an issue, so nothing here is blocked on the secret. Note it only ever pushes to feature branches and to its own proposal branch; master is only ever touched through a PR a human merges.

@danielhanchen
danielhanchen merged commit bdff306 into master Aug 3, 2026
@danielhanchen
danielhanchen deleted the repin-bot branch August 3, 2026 13:14

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e3fecc99b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

repin:
name: Repin against the current base tag
# A preflight that passed has nothing to fix.
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'failure' }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Trigger the bot from the reported preflight failure

The automatic path never runs for the pin conflicts it is intended to repair. .github/workflows/unsloth-pin-preflight.yml records a status=failure output and opens an issue, but neither that step nor prebuilt-alert exits nonzero, so the workflow conclusion remains success; this condition therefore skips the repin job on every detected merge conflict. Trigger based on the preflight's reported status or make the preflight workflow actually fail.

Useful? React with 👍 / 👎.

Comment thread scripts/unsloth/repin.py
Comment on lines +139 to +140
m = run(["git", "-c", "merge.conflictStyle=diff3", *git_id, "merge", "--no-ff",
"--no-edit", "-m", f"Merge {base} into {head_ref}", f"refs/tags/{base}"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve against the cumulative pin tree

Each branch is merged only with the base tag, whereas the checked preflight merges pins sequentially and reports a failure against the base plus all earlier pins. If two pinned PRs conflict with each other but each merges cleanly with the base, this code creates new SHAs without resolving the actual conflict and can report a successful proposal that still fails the nightly. The repin operation must reproduce the cumulative merge topology used by the preflight.

Useful? React with 👍 / 👎.

+ ", ".join(sorted(shared)[:3])
)
# Upstream first, then ours: the same order a human repin produces.
return list(theirs) + list(ours)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refuse semantically ambiguous insertion conflicts

An empty diff3 base and disjoint line text do not prove that two insertions can safely be concatenated. For example, branches that insert mode = 1; and mode = 2; at the same location satisfy these checks, but the fixed theirs-then-ours order silently selects one behavior; mutually exclusive control-flow additions have the same problem. Since the workflow commits and pushes this resolution directly to owned branches, the resolver should restrict automation to a structurally validated pattern rather than unioning every insertion-only conflict.

Useful? React with 👍 / 👎.

Comment on lines +69 to +70
set -uo pipefail
python3 scripts/unsloth/repin.py \

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve failures from the repin process

Because this step enables -u and pipefail but not -e, a nonzero exit from repin.py does not fail the step: the subsequent jq reads can also fail, and the final successful echo leaves steps.repin.outcome as success. The Report step explicitly relies on that outcome to detect crashes, so malformed input or an unexpected runtime error is instead reported as a successful run with no actionable alert.

Useful? React with 👍 / 👎.

Comment on lines +128 to +130
git push -q --force \
"https://x-access-token:${REPIN_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" \
"HEAD:refs/heads/${REPIN_BRANCH}" 2>&1 | sed "s/${REPIN_TOKEN}/***/g"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Stop after a failed proposal-branch push

If this push fails because of token permissions, branch protection, or a transient remote error, pipefail exposes the failure but the absence of set -e lets execution continue. The workflow can then edit or create a PR against a missing or stale unsloth/auto-repin branch and finally emit mode=pushed, causing the Report step to declare success even though the proposed pin changes were never published.

Useful? React with 👍 / 👎.

Comment on lines +76 to +77
CHANGED="$(jq -r '.changed' "${RUNNER_TEMP}/repin.json")"
BLOCKED="$(jq -r '[.results[] | select(.action == "conflict" or .action == "third-party")] | length' "${RUNNER_TEMP}/repin.json")"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Count refusal skips as blocked

The blocked count excludes every skip, even when skipping is the condition that caused preflight to fail and explicitly requires human action. For example, a required pin whose PR was closed, a force-pushed-away commit, or a branch head that moved past the reviewed SHA is returned as action="skip"; with no automatic changes this produces changed=0, blocked=0, and a successful Report status instead of alerting that the pin remains unusable. Expected no-op cases and refusal/error skips need distinct classifications.

Useful? React with 👍 / 👎.

Comment thread scripts/unsloth/repin.py
Comment on lines +114 to +116
repo = work / f"r{pin['num']}"
run(["git", "clone", "-q", "--filter=blob:none", "--no-checkout",
f"https://github.com/{pin['src']}.git", str(repo)])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Include the source repository in scratch paths

Scratch clone names use only the PR number, although pr-set.json permits PRs from both ggml-org/llama.cpp and unslothai/llama.cpp. If both repositories have a pinned PR with the same number, the first clone creates r<N> and the second clone fails because that directory already exists, so a valid pin is skipped solely due to a cross-repository number collision. Incorporate the source owner or repository into the directory name.

Useful? React with 👍 / 👎.

Comment on lines +103 to +110
while read -r path head_repo head_ref new_sha; do
echo "pushing ${new_sha:0:10} to ${head_repo}:${head_ref}"
# No -x anywhere in this step; the URL carries the token.
if ! git -C "$path" push \
"https://x-access-token:${REPIN_TOKEN}@github.com/${head_repo}.git" \
"HEAD:refs/heads/${head_ref}" 2>&1 | sed "s/${REPIN_TOKEN}/***/g"; then
FAILED="${FAILED} ${head_repo}:${head_ref}"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve successful SHAs after partial branch pushes

Branch pushes occur sequentially, but any later failure causes the workflow to exit before proposing any pin updates. Branches pushed earlier in the loop remain advanced; on the next run their API head no longer equals the old pinned SHA, so repin_one refuses them and the bot has lost the new SHA needed to update pr-set.json. A transient failure on one repository can therefore strand successfully resolved pins and require manual recovery; retain and propose successful push results or avoid leaving them unrecorded.

Useful? React with 👍 / 👎.

Comment thread scripts/unsloth/repin.py
Comment on lines +103 to +106
if not head_repo.startswith(OWNED):
out["action"] = "third-party"
out["note"] = f"`{head_repo}` is not ours; ask the author to merge master"
return out

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refuse pushes to owned default branches

The only authorization check is ownership of the head repository; it does not verify that head_ref is a disposable feature branch. If an owned PR is opened from unslothai/llama.cpp:master or danielhanchen/llama.cpp:master, the later push fast-forwards that default branch directly with the generated merge commit, despite the workflow's stated guarantee that master is touched only through a human-reviewed PR. Compare the head ref with the repository's default branch or use an explicit branch allowlist before permitting the push.

Useful? React with 👍 / 👎.

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