Keep your fork in sync without the merge headaches.
Patchlane is an npm CLI that automates maintaining forked repositories with custom patches. It rebuilds an integration branch from upstream, reapplies your patch branches, publishes the result for CI, and then promotes the exact tested commit onto your fork branch automatically.
Install it from npm and run it locally or inside your own workflows:
npx patchlane sync --upstream-owner=kubernetes --upstream-repo=kubernetes --patch-refs="patch/product,patch/sync"If you want AI coding agents to help maintain a Patchlane fork, install the repo-managed skills into the standard .agents/skills folder:
npx patchlane agents- Rebuild – Patchlane creates a fresh integration branch from an upstream branch or release tag.
- Apply patches – Your configured patch branches are applied sequentially.
- Fail fast – If a patch conflicts, the workflow stops and reports which patch failed and why.
- Publish – The rebuilt branch is force-pushed to
sync_branchand its commit SHA is recorded. - Run CI – Your fork's CI runs on
sync_branchand validates the result. - Promote – A second step force-with-lease updates
base_branchonly ifsync_branchstill points at the tested SHA.
- A forked repository on GitHub.
permissions: contents: writein your workflow so the defaultGITHUB_TOKENcan push branches.
Organize your fork-specific changes into logical patch branches and push them to your fork:
git checkout -b patch/product
git checkout -b patch/sync
git checkout -b patch/ciCreate .github/workflows/sync-upstream.yml in your fork:
name: Sync Upstream Integration
on:
schedule:
- cron: '0 10 * * *'
workflow_dispatch:
inputs:
no_push:
description: 'Build the sync branch locally but do not push'
type: boolean
default: false
permissions:
contents: write
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run patchlane sync
run: npx patchlane@latest sync
env:
UPSTREAM_OWNER: kubernetes
UPSTREAM_REPO: kubernetes
BASE_BRANCH: main
SYNC_BRANCH: sync/integration
PATCH_REFS: |
patch/product
patch/sync
patch/ci
NO_PUSH: ${{ inputs.no_push || false }}Create .github/workflows/fork-ci.yml in your fork. It must run on sync_branch pushes so the promotion workflow receives the tested head_sha:
name: Fork CI
on:
pull_request:
push:
branches:
- main
- sync/integration
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Replace this with your fork's actual CI checks."Create .github/workflows/promote-tested-sync.yml in your fork:
name: Promote Tested Sync Branch
on:
workflow_run:
workflows: ['Fork CI']
types: [completed]
permissions:
contents: write
jobs:
promote:
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'sync/integration'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Run patchlane promote
run: npx patchlane@latest promote
env:
BASE_BRANCH: main
SYNC_BRANCH: sync/integration
EXPECTED_SYNC_SHA: ${{ github.event.workflow_run.head_sha }}Trigger the sync workflow manually with no_push: true first to verify your patches apply cleanly.
You can run Patchlane directly via npx without cloning the repository:
# Install or update Patchlane-managed agent skills
npx patchlane agents
# Sync (rebuild integration branch)
npx patchlane sync \
--upstream-owner=kubernetes \
--upstream-repo=kubernetes \
--patch-refs="patch/product,patch/sync,patch/ci" \
--base-branch=main \
--sync-branch=sync/integration \
--no-push
# Promote (after CI passes)
npx patchlane promote \
--expected-sync-sha=abc123 \
--base-branch=main \
--sync-branch=sync/integrationEvery CLI flag also falls back to an environment variable of the same name (e.g. --upstream-owner → UPSTREAM_OWNER).
patchlane agents downloads the latest Patchlane-maintained skills from GitHub and installs them into .agents/skills in the current repository. Re-running it updates the managed Patchlane skill folders in place while leaving unrelated custom skills alone.
Options:
--dirsets the destination folder. Default:.agents/skills--refpulls skills from a specific Patchlane git ref. Default:main
| Option / Env Var | Required | Default | Description |
|---|---|---|---|
upstream_owner |
✅ | — | GitHub owner/org of the upstream repository |
upstream_repo |
✅ | — | Upstream repository name |
patch_refs |
✅ | — | Comma- or newline-delimited list of patch branches (applied in order) |
base_branch |
— | main |
Fork branch later promoted by the promotion workflow |
upstream_ref |
— | main |
Upstream branch when not using releases |
release_selector |
— | latest |
latest, prerelease, regex, or blank for upstream_ref |
sync_branch |
— | sync/integration |
Published generated branch name |
dry_run |
— | false |
Validate patches and test whether they apply cleanly without creating the sync branch |
no_push |
— | false |
Build the sync branch locally but do not push |
origin_remote_name |
— | origin |
Name of the fork remote |
upstream_remote_name |
— | upstream |
Name of the upstream remote |
upstream_remote_url |
— | inferred | URL of the upstream remote (inferred from owner/repo if omitted) |
When running in a GitHub Actions environment, Patchlane writes the following outputs to GITHUB_OUTPUT:
| Output | Description |
|---|---|
sync_branch |
The generated branch that was built |
sync_sha |
The commit SHA published to sync_branch |
failed_bookmark |
First patch that failed to apply |
failed_commit |
Commit at the head of the failed patch |
conflicted_paths |
Files with conflicts |
applied_refs |
Successfully applied patches |
status |
dry_run, no_push, published, unchanged, missing_patch, conflicted, or invalid_patch |
| Option / Env Var | Required | Default | Description |
|---|---|---|---|
expected_sync_sha |
✅ | — | Tested commit SHA that must still be the current sync_branch head |
base_branch |
— | main |
Fork branch promoted to the tested sync commit |
sync_branch |
— | sync/integration |
Generated branch that already passed CI |
origin_remote_name |
— | origin |
Name of the fork remote |
| Output | Description |
|---|---|
promoted_sha |
Commit SHA promoted onto base_branch |
status |
promoted, stale_sync, or promotion_failed |
patch_refs accepts comma- or newline-delimited branch names. Use commas for workflow_dispatch inputs (the GitHub UI handles single-line text more reliably) and newlines for committed YAML:
# Good for workflow_dispatch inputs
patch_refs: patch/product, patch/sync, patch/ci
# Good for committed workflow files
patch_refs: |
patch/product
patch/sync
patch/ci- Keep patches focused – Each patch branch should address a single concern.
- Order matters – Put foundational patches first (e.g.,
patch/cibeforepatch/product). - Store workflows on patches – Your fork's CI and sync workflows should live on patch branches, not the promoted base branch.
- Treat the base branch as generated output – Avoid direct commits on
base_branch; put fork-owned changes onpatch/*. - Test locally first – Use
no_push: trueto validate before letting automation push.
npm install
npm testThis builds the TypeScript and runs the integration harness with mocked git operations.
MIT