4187798fd780b07b8183dd42909e5c6201acf3d9
Sync-Origin: forward/main/2026-05-28T04:24:11Z
josh-sync
Bidirectional monorepo ↔ subrepo sync via josh-proxy. Supports multiple sync targets from a single config.
Quick Start
1. Add config
Create .josh-sync.yml in your monorepo root:
schema_version: 2
josh:
proxy_url: "https://josh.example.com"
monorepo_path: "org/monorepo"
monorepo_url: "git@gitea.example.com:org/monorepo.git"
monorepo_auth: "ssh" # optional, default "https"
targets:
- name: "billing"
subfolder: "services/billing"
subrepo_url: "git@gitea.example.com:ext/billing.git"
subrepo_auth: "ssh"
branches:
main: main
exclude: # files excluded from subrepo (optional)
- ".monorepo/"
bot:
name: "josh-sync-bot"
email: "josh-sync-bot@example.com"
trailer: "Josh-Sync-Origin"
2. Add CI workflows
Copy from examples/ and customize paths/branches:
# .gitea/workflows/josh-sync-forward.yml
- uses: https://your-gitea.example.com/org/josh-sync@v1
with:
direction: forward
env:
SYNC_BOT_USER: ${{ secrets.SYNC_BOT_USER }}
SYNC_BOT_TOKEN: ${{ secrets.SYNC_BOT_TOKEN }}
SUBREPO_SSH_KEY: ${{ secrets.SUBREPO_SSH_KEY }}
3. Local dev (Nix)
Add josh-sync as a flake input, then:
{ inputs, ... }: {
imports = [ inputs.josh-sync.devenvModules.default ];
}
Run josh-sync preflight to validate your setup.
Documentation
- Setup Guide — Step-by-step: prerequisites, importing existing subrepos, CI workflows, file exclusion, and troubleshooting
- Configuration Reference — Full
.josh-sync.ymlfield documentation - Architecture Decision Records — Design rationale and trade-offs
- Changelog — Version history
Versioning
josh-sync follows semver:
| Bump | Meaning | Action required |
|---|---|---|
| Patch (1.x.y) | Bug fixes only | Safe to upgrade |
| Minor (1.y.0) | New optional config fields, new commands, new action inputs | Safe to upgrade; new features are opt-in |
| Major (y.0.0) | Removed or renamed config fields, CLI flags, action inputs, env vars, or state format | Read the Breaking Changes section in the changelog before upgrading |
CI workflows pin to a floating major tag (e.g. @v1). A breaking change bumps the major version and moves the tag — @v1 workflows keep working until you explicitly update to @v2.
CLI
josh-sync sync [--forward|--reverse] [--force] [--target NAME[,NAME]] [--branch BRANCH]
josh-sync preflight
josh-sync import <target>
josh-sync adopt <target> [--restart]
josh-sync reset <target>
josh-sync onboard <target> [--restart]
josh-sync migrate-pr <target> [PR#...] [--all]
josh-sync status
josh-sync state show <target> [branch]
josh-sync state reset <target> [branch]
How It Works
- Forward sync (mono → subrepo): pushes directly if clean, creates conflict PR if not. Uses
--force-with-leasefor safety. - Reverse sync (subrepo → mono): creates a PR by default. Add
--forceto bypass the PR and push directly to the mono branch (destructive). - Adoption: existing active subrepos can be connected to josh-sync with a normal fast-forward merge commit, preserving old subrepo history and avoiding force-push.
- File exclusion:
excludepatterns are embedded inline in the josh-proxy URL. Excluded files exist only in the monorepo. - Filter reconciliation: Changing the exclude list auto-creates a merge commit that connects old and new histories — no force-push needed.
- Loop prevention:
Josh-Sync-Origin:git trailer filters out bot commits. - State tracking: orphan branch
josh-sync-statestores JSON per target/branch.
Dependencies
bash >=4, git, curl, jq, yq (mikefarah/yq v4+), openssh, rsync
The Nix flake bundles all dependencies automatically.
License
MIT
Languages
Shell
97.6%
Nix
1.5%
Makefile
0.9%