Skip to content

ci: rewrite the upstream-sync bot: real merge, new branch, open a PR, never approve - #21

Merged
solderzzc merged 1 commit into
mainfrom
ci/upstream-sync-bot-v2
Oct 6, 2026
Merged

solderzzc merged 1 commit into
mainfrom
ci/upstream-sync-bot-v2

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Proposed changes

Rewrites the "🔄 Auto-Sync Upstream" workflow (upstream-sync.yml). The bot may still open a PR on its own, but it never approves, reviews or merges anything.

Why

The old job had three defects (from its run logs and from reading it):

  • It ran git checkout upstream/main -- ., which overwrites the whole tree with upstream. Merging its PR would have wiped every fork patch (TurboQuant, SSD streaming, the scoped-device compile fix, ...).
  • It force-pushed the shared branch sync/upstream-latest every day.
  • A failing gh pr create was hidden by an || echo fallback, so a run could stay green without opening a PR.

What the bot does now

The logic moved into .github/scripts/upstream-sync.sh (the same file is proposed for SharpAI/mlx-swift-lm); the workflow only sets env vars and runs it.

  • Fetches origin and ml-explore/mlx-swift. If this fork already contains upstream, or a sync PR is already open, the run ends green and does nothing. A sync PR open for 7+ days adds a warning.
  • Merges upstream into a scratch worktree as a real two-parent merge (--no-ff, no -X ours/theirs, no cherry-picks).
  • On conflicts nothing is pushed. The run ends red and lists the conflicted files (log + step summary). A person resolves them, like in Sync upstream ml-explore/mlx-swift (mlx v0.32.2, mlx-c ebc88f1) via real merge #20.
  • Keeps this fork's .github/workflows and checks that the merge commit leaves it identical to main (the default token cannot push workflow changes).
  • Pushes only a new branch bot/upstream-<upstream sha7>-<main sha7> with a plain git push: never forced, never an existing branch. If the branch already exists it must hold the same merge, otherwise the run ends red.
  • Opens the PR with gh pr create: SWIFTLM_PR_TOKEN first (CI starts normally), then GITHUB_TOKEN (CI does not start until someone closes and reopens the PR). If neither can, the run ends red with a pre-filled "open PR" link. A PR a person closed is not reopened or duplicated.
  • The PR body lists the commits and files, flags changes to Package.swift and Source/Cmlx, and reports an advisory swiftc -parse of the changed Swift files. The "I have read this PR description" box is left unchecked.

What it never does

  • No approve, review, merge or auto-merge, and no comment, label or edit on any PR. The only PR call in the script is gh pr create.
  • No force-push, and no write to a branch it did not just create.
  • SWIFTLM_PR_TOKEN is passed to the gh pr create process only.

Note: the repo setting "Allow GitHub Actions to create and approve pull requests" is a single checkbox and cannot separate creating from approving. It is currently on, so the guarantee above comes from the script, not from the setting.

What to expect after merging

  • Source/Cmlx/mlx and Source/Cmlx/mlx-c are vendored as plain trees here (upstream uses submodule gitlinks), so an upstream bump of either one will conflict and end the run red. It needs a manual sync like Sync upstream ml-explore/mlx-swift (mlx v0.32.2, mlx-c ebc88f1) via real merge #20; the conflict report says so.
  • This fork is currently up to date with upstream (b4f7684), so the first real run should end green without a PR.
  • sync/upstream-latest and sync/upstream-0.32.3-merge are not touched by the new job and can be deleted.
  • To see what a run would do without pushing or opening anything: Actions → Auto-Sync Upstream → Run workflow → dry_run.

Tests

The test harness is not part of this PR; it lives outside the repo.

  • Harness against local bare "origin"/"upstream" repos with a stubbed gh: 4336 checks in 26 scenarios, 0 failures (clean merge, conflicts, workflow-only conflicts, rejected push, existing branch, PR already open / closed earlier, token failures, hostile file names, dry run, cleanup). It ran on macOS with bash 3.2 only.
  • A grep-based guard over the script and workflow (no approve/review/merge call, no force, no token leak) and its own self-test (217 checks) pass.
  • Dry run against the real remotes (nothing written): this fork is up to date with upstream, so it would do nothing.
  • Not run: bash 5.2 / Linux, a real Actions run, and a real gh pr create (mocked in the harness). The first real run should be a dry_run dispatch.

Checklist

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

AI usage

  • I have read this PR description in full and approve it as my own, and it
    accurately describes the code changes.
  • AI usage disclosure: this PR was prepared with Claude Code (Claude Sonnet 5.5): the script, the workflow, the test harness and this description. It still needs human review before merging.

🤖 Generated with Claude Code

… never approve

The old job force-pushed the shared sync/upstream-latest branch, committed
conflict markers (or, in mlx-swift, overwrote the tree with upstream), and
masked PR-creation failures with `|| echo`.

The job now runs .github/scripts/upstream-sync.sh:
- merges mlx-swift's upstream main into a scratch worktree as a real two-parent
  merge (no -X ours/theirs); conflicts are never pushed, the run ends red with
  a report instead;
- restores the fork's .github/workflows byte for byte and checks the result;
- pushes ONLY a new branch bot/upstream-<up7>-<base7> (never an existing one,
  never forced) and opens a PR with `gh pr create`, SWIFTLM_PR_TOKEN first so
  CI starts, then GITHUB_TOKEN; if neither can, it ends red with a pre-filled
  compare link;
- never approves, reviews, merges, comments on or edits a PR, and does not
  enable auto-merge;
- stays quiet (green) while a sync PR is open, and never reopens a branch
  whose PR was closed.

A dry run (the default outside GitHub Actions, or the workflow_dispatch
dry_run input) pushes nothing and opens nothing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@solderzzc
solderzzc merged commit 3a3589c into main Oct 6, 2026
8 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.

1 participant