reusable GitHub workflows and tooling for bidirectional synchronization between a private repository and a public repository.
the private repo may contain internal-only code that must never appear in the public repo. repo-sync handles stripping that code automatically and creating sync PRs in both directions.
when a commit merges to the default branch of either repo, a sync PR is created in the other repo:
- private → public: a clean snapshot of the private repo is generated (with all internal code stripped), and the diff is applied to the public repo.
- public → private: the public commit is cherry-picked into the private repo as-is.
sync PRs are managed as a stack -- each new sync PR is based on the previous one, so conflicts queue up naturally and each PR shows only a single commit's changes. clean sync PRs auto-merge; conflicted ones get an agent-proposed resolution and a human reviewer.
for full details, see docs/PRD.md and docs/TECH-DESIGN.md.
two mechanisms for keeping code out of the public repo:
any directory named private (at any depth) is excluded entirely. this is the simplest option for fully-private modules.
for inline private code within otherwise-public files:
fn my_func() {
// !repo-sync: private-start
println!("this code exists only in our private repo");
// !repo-sync: private-end
println!("this code is public");
}the marker lines and everything between them are stripped. markers must be properly paired (every private-start needs a private-end in the same file) and cannot be nested.
before integrating, ensure the consuming repos have:
- a GitHub App ("sync bot") installed on both repos (and on the
repo-syncrepo itself) withcontents:write,pull_requests:write,workflows:write, andmetadata:readpermissions. store the App ID and private key as repo secrets (REPO_SYNC_APP_ID,REPO_SYNC_APP_PRIVATE_KEY) in both repos. the reusable workflows generate short-lived installation tokens internally. - a second GitHub App ("approver bot") with
contents:write,pull_requests:write, andmetadata:readpermissions. this app handles approval and conflict resolution for sync PRs — a separate identity is needed because GitHub does not allow a PR's author to approve it. store asREPO_SYNC_APPROVER_APP_IDandREPO_SYNC_APPROVER_APP_PRIVATE_KEY. - auto-merge enabled as a repo-level setting.
- squash merge as the merge strategy for PRs, configured to preserve the PR description in the commit message.
- branch protection rules on
repo-sync/*branches, so only the sync workflow's token can create or push to them. - required PR approvals on the default branch. the approver bot approves clean (conflict-free) sync PRs automatically; conflict-resolved PRs require human approval.
the bootstrap script creates the initial public repo from the private repo:
./scripts/bootstrap.sh \
--private-repo warpdotdev/warp-internal \
--public-repo warpdotdev/warp-public \
--token "$GITHUB_TOKEN"this:
- queries the private repo's default branch and uses it for the public repo (both must match)
- generates a clean snapshot of the private repo at
HEAD(stripping allprivate/dirs and!repo-syncmarker regions) - pushes the snapshot as the initial commit to the public repo
- checks that the public repo has no existing commits (refuses to overwrite)
- sets watermark tags in both repos so the sync workflows know where to start
the token must have contents:write, pull_requests:write, and workflows:write on both repos (the workflows scope is needed because the repo may contain .github/workflows/ files). you can generate one by creating a GitHub App installation token (recommended) or using a fine-grained PAT for one-time use.
add marker validation to the private repo's CI so developers catch issues before merging:
# .github/workflows/validate-markers.yml
name: validate markers
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: warpdotdev/repo-sync/actions/validate-markers@mainthis validates that all !repo-sync markers are properly paired, not nested, and that no symlinks exist in the repo.
add the sync workflow to both repos. see examples/consuming-repo-sync.yml for a complete example. the key pieces are:
# .github/workflows/repo-sync.yml
name: repo-sync
on:
push:
branches: [main] # triggers sync creation
pull_request:
types: [closed, opened, synchronize, edited, labeled]
branches: [main] # triggers restack + approve
schedule:
- cron: "*/15 * * * *" # triggers escalation checks
# store REPO_SYNC_APP_ID and REPO_SYNC_APPROVER_APP_ID as variables,
# and their private keys as secrets.
jobs:
sync:
if: github.event_name == 'push'
uses: warpdotdev/repo-sync/.github/workflows/sync.yml@main
with:
public_repo: warpdotdev/warp-public
private_repo: warpdotdev/warp-internal
app_id: ${{ vars.REPO_SYNC_APP_ID }}
secrets:
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
restack:
if: >-
(github.event_name == 'pull_request' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'repo-sync/')) ||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'repo-sync:needs-restack' && startsWith(github.event.pull_request.head.ref, 'repo-sync/'))
uses: warpdotdev/repo-sync/.github/workflows/restack.yml@main
with:
public_repo: warpdotdev/warp-public
private_repo: warpdotdev/warp-internal
app_id: ${{ vars.REPO_SYNC_APP_ID }}
secrets:
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}
approve:
if: github.event_name == 'pull_request' && github.event.action != 'closed' && startsWith(github.event.pull_request.head.ref, 'repo-sync/')
uses: warpdotdev/repo-sync/.github/workflows/approve.yml@main
with:
public_repo: warpdotdev/warp-public
private_repo: warpdotdev/warp-internal
approver_app_id: ${{ vars.REPO_SYNC_APPROVER_APP_ID }}
secrets:
approver_app_private_key: ${{ secrets.REPO_SYNC_APPROVER_APP_PRIVATE_KEY }}
escalation:
if: github.event_name == 'schedule'
uses: warpdotdev/repo-sync/.github/workflows/escalation.yml@main
with:
escalate_to: "@oncall-client-primary"
escalate_after: "1h"
public_repo: warpdotdev/warp-public
private_repo: warpdotdev/warp-internal
app_id: ${{ vars.REPO_SYNC_APP_ID }}
secrets:
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}the same workflow file works in both repos -- the workflows derive which repo is which by comparing github.repository against the private_repo input.
after setup, test by merging a small change in each direction:
- private → public: merge a commit to the private repo that modifies public code. a sync PR should appear in the public repo within a few minutes.
- public → private: merge a commit to the public repo. a sync PR should appear in the private repo.
- internal-only change: merge a commit that only modifies code inside
private/dirs or!repo-syncmarkers. no sync PR should be created.
when a sync PR reaches the bottom of the stack and has merge conflicts, the approval workflow invokes an Oz agent to propose a resolution. a human reviewer is always requested for sign-off on conflict-resolved PRs. if the agent fails, the PR is assigned to a human without a proposed resolution.
if the reviewer doesn't respond within the configured timeout (default: 1 hour), the PR is escalated to the configured team (default: @oncall-client-primary).
for operational procedures and failure remediation, see docs/RUNBOOK.md.
.agents/skills/ # oz agent skill definitions
conflict-resolution/ # generic merge conflict resolution
pr-description/ # PR description generation (private→public)
.github/workflows/ # reusable GitHub Actions workflows
sync.yml # sync PR creation
restack.yml # post-merge restacking
approve.yml # approval + conflict resolution
escalation.yml # cron: timeout, CI failure, stuck stack
actions/
validate-markers/ # CI validation composite action
docker/
pr-description/ # Dockerfile for agent isolation
docs/ # design documents
PRD.md # product requirements
TECH-DESIGN.md # technical design
DECISIONS.md # decision log
VALIDATION.md # test cases
examples/
consuming-repo-sync.yml # example consuming repo workflow
scripts/
bootstrap.sh # one-time bootstrap script
src/repo_sync/ # python package
strip/ # stripping tool + shared marker library
stack/ # stack management + trailer parsing
workflows/ # workflow orchestration logic
tests/ # pytest test suite
after stripping, some generated files (e.g., Cargo.lock) may reference crates or modules that were removed by the strip step. to handle this, repo-sync supports optional fixup scripts that run after stripping but before the sync diff is computed.
fixup scripts are configured per-direction via workflow inputs:
sync:
uses: warpdotdev/repo-sync/.github/workflows/sync.yml@main
with:
public_repo: warpdotdev/warp-public
private_repo: warpdotdev/warp-internal
app_id: ${{ vars.REPO_SYNC_APP_ID }}
private_to_public_fixup_script: scripts/post-strip-fixup.sh
secrets:
app_private_key: ${{ secrets.REPO_SYNC_APP_PRIVATE_KEY }}script contract:
- the script receives the working directory (the stripped snapshot) as its sole argument.
- it must exit 0 on success; a non-zero exit is treated as a permanent sync failure.
- for private→public sync, the script runs on both the current and parent snapshots so the diff is computed from two consistently-fixed-up trees.
example fixup script that regenerates Cargo.lock after stripping:
#!/bin/bash
set -e
cd "$1"
cargo generate-lockfilesupported directions:
private_to_public_fixup_script-- runs after stripping, before diff computation. fully supported.public_to_private_fixup_script-- config surface exists but execution is not yet implemented.
- both repos must use the same default branch (e.g., both use
main). the bootstrap script enforces this, and the sync workflows assume it. - symlinks are not supported. any symlink in the repo will cause the stripping tool to error. this is a fail-closed safety measure -- symlinks could potentially bypass
private/directory exclusion. the CI validation action also checks for this.
- docs/PRD.md -- product requirements and high-level behavior
- docs/TECH-DESIGN.md -- technical design and implementation details
- docs/DECISIONS.md -- decision log with alternatives and justifications
- docs/VALIDATION.md -- comprehensive test cases
- docs/RUNBOOK.md -- operational procedures for failure scenarios