Sync files from a source repo to multiple destination repos
Project description
path-sync
Sync files from a source repo to multiple destination repos.
Overview
Problem: You have shared config files (linter rules, CI templates, editor settings) that should be consistent across multiple repositories. Manual copying leads to drift.
Solution: path-sync provides one-way file syncing with clear ownership:
| Term | Definition |
|---|---|
| SRC | Source repository containing the canonical files |
| DEST | Destination repository receiving synced files |
| Header | Comment added to synced files marking them as managed |
| Section | Marked region within a file for partial syncing |
Key behaviors:
- SRC owns synced content; DEST should not edit it
- Files with headers are updated on each sync
- Remove a header to opt-out (file becomes DEST-owned)
- Orphaned files (removed from SRC) are deleted in DEST
Installation
# From PyPI
uvx path-sync --help
# Or install in project
uv pip install path-sync
Quick Start
1. Bootstrap a source config
path-sync boot -n myconfig -d ../dest-repo1 -d ../dest-repo2 -p '.cursor/**/*.mdc'
Creates .github/myconfig.src.yaml with auto-detected git remote and destinations.
2. Copy files to destinations
path-sync copy -n myconfig
By default, prompts before each git operation. See Usage Scenarios for common patterns.
| Flag | Description |
|---|---|
-d dest1,dest2 |
Filter specific destinations |
--dry-run |
Preview without writing (requires existing repos) |
-y, --no-prompt |
Skip confirmations (for CI) |
--local |
No git ops after sync (no commit/push/PR) |
--no-checkout |
Skip branch switching (assumes already on correct branch) |
--checkout-from-default |
Reset to origin/default before sync |
--no-pr |
Push but skip PR creation |
--force-overwrite |
Overwrite files even if header removed (opted out) |
--detailed-exit-code |
Exit 0=no changes, 1=changes, 2=error |
--skip-orphan-cleanup |
Skip deletion of orphaned synced files |
--pr-title |
Override PR title (supports {name}, {dest_name}) |
--pr-labels |
Comma-separated PR labels |
--pr-reviewers |
Comma-separated PR reviewers |
--pr-assignees |
Comma-separated PR assignees |
3. Validate (run in dest repo)
uvx path-sync validate-no-changes -b main
Options:
-b, --branch- Default branch to compare against (default: main)--skip-sections- Comma-separatedpath:section_idpairs to skip (e.g.,justfile:coverage)
Usage Scenarios
| Scenario | Command |
|---|---|
| Interactive sync | copy -n cfg |
| CI fresh sync | copy -n cfg --checkout-from-default -y |
| Local preview | copy -n cfg --dry-run |
| Local test files | copy -n cfg --local |
| Already on branch | copy -n cfg --no-checkout |
| Push, manual PR | copy -n cfg --no-pr -y |
| Force opted-out | copy -n cfg --force-overwrite |
Interactive prompt behavior: Declining the checkout prompt syncs files but skips commit/push/PR (same as --local). Use --no-checkout when you're already on the correct branch and want to proceed with git operations.
Section Markers
For partial file syncing (e.g., justfile, pyproject.toml), wrap sections with markers:
# === DO_NOT_EDIT: path-sync default ===
lint:
ruff check .
# === OK_EDIT ===
DO_NOT_EDIT: path-sync {id}- Start of managed section with identifierOK_EDIT- End marker (content below is editable)
During sync, only content within markers is replaced. Destination can have extra sections.
Use skip_sections in destination config to exclude specific sections from sync:
destinations:
- name: dest1
dest_path_relative: ../dest1
skip_sections:
justfile: [coverage] # keep local coverage recipe
Config Reference
Source config (.github/{name}.src.yaml):
name: cursor
src_repo_url: https://github.com/user/src-repo
schedule: "0 6 * * *"
paths:
- src_path: .cursor/**/*.mdc
- src_path: templates/justfile
dest_path: justfile
destinations:
- name: dest1
repo_url: https://github.com/user/dest1
dest_path_relative: ../dest1
# copy_branch: sync/cursor # defaults to sync/{config_name}
default_branch: main
skip_sections:
justfile: [coverage]
| Field | Description |
|---|---|
name |
Config identifier |
src_repo_url |
Source repo URL (auto-detected from git remote) |
schedule |
Cron for scheduled sync workflow |
paths |
Files/globs to sync (src_path required, dest_path optional) |
destinations |
Target repos with sync settings |
header_config |
Comment style per extension (has defaults) |
pr_defaults |
PR title, labels, reviewers, assignees |
Header Format
Synced files have a header comment identifying the source config:
# path-sync copy -n myconfig
Comment style is extension-aware:
| Extension | Format |
|---|---|
.py, .sh, .yaml |
# path-sync copy -n {name} |
.go, .js, .ts |
// path-sync copy -n {name} |
.md, .mdc, .html |
<!-- path-sync copy -n {name} --> |
Remove this header to opt-out of future syncs for that file.
GitHub Actions
Source repo workflow
Create .github/workflows/path_sync_copy.yaml:
name: path-sync copy
on:
schedule:
- cron: "0 6 * * *"
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uvx path-sync copy -n myconfig --checkout-from-default -y
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
Destination repo validation
Create .github/workflows/path_sync_validate.yaml:
name: path-sync validate
on:
push:
branches-ignore:
- main
- sync/**
pull_request:
branches:
- main
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v5
- run: uvx path-sync validate-no-changes -b main
Validation skips automatically when:
- On a
sync/*branch (path-sync usessync/{config_name}by default) - On the default branch (comparing against itself)
The workflow triggers exclude these branches too, reducing unnecessary CI runs.
PAT Requirements
Create a Fine-grained PAT at https://github.com/settings/tokens?type=beta
| Permission | Scope |
|---|---|
| Contents | Read/write (push branches) |
| Pull requests | Read/write (create PRs) |
| Workflows | Read/write (if syncing .github/workflows/) |
| Metadata | Read (always required) |
Add as repository secret: GH_PAT
Common Errors
| Error | Fix |
|---|---|
HTTP 404: Not Found |
Add repo to PAT's repository access |
HTTP 403: Resource not accessible |
Add Contents + Pull requests permissions |
GraphQL: Resource not accessible |
Use GH_PAT, not GITHUB_TOKEN |
HTTP 422: Required status check |
Exclude sync/* from branch protection |
Alternatives Considered
| Tool | Why Not |
|---|---|
| repo-file-sync-action | No local CLI, no validation |
| Copier | Merge-based (conflicts), no multi-dest |
| Cruft | Patch-based, single dest |
Why path-sync:
- One SRC to many DEST repos
- Local CLI + CI support
- Section-level sync for shared files
- Validation enforced across repos
- Clear ownership (no merge conflicts)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file path_sync-0.3.1.tar.gz.
File metadata
- Download URL: path_sync-0.3.1.tar.gz
- Upload date:
- Size: 15.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00c3bb787bb331a46b01f53e5d171f7cbc77d4a427c3cfa61d35d41dab197bab
|
|
| MD5 |
9471ba7b5a12084f05d1ce86c42eeff2
|
|
| BLAKE2b-256 |
1cd97e054dfa09184632b7596e3c6ad18c2b108469c3d295336749c281ae8087
|
Provenance
The following attestation bundles were made for path_sync-0.3.1.tar.gz:
Publisher:
release.yaml on EspenAlbert/path-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
path_sync-0.3.1.tar.gz -
Subject digest:
00c3bb787bb331a46b01f53e5d171f7cbc77d4a427c3cfa61d35d41dab197bab - Sigstore transparency entry: 821222097
- Sigstore integration time:
-
Permalink:
EspenAlbert/path-sync@26c7f59b4ff28b5632d4e9c4f05e6db7e7ddc31e -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/EspenAlbert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@26c7f59b4ff28b5632d4e9c4f05e6db7e7ddc31e -
Trigger Event:
push
-
Statement type:
File details
Details for the file path_sync-0.3.1-py3-none-any.whl.
File metadata
- Download URL: path_sync-0.3.1-py3-none-any.whl
- Upload date:
- Size: 20.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d4acffabc632162f108abbe8ba35ee49a62cc53de8a651556676b79323b6154a
|
|
| MD5 |
31563e0f398bdd70cec75089ed1cbde7
|
|
| BLAKE2b-256 |
f3cf4e8ce0a626f394c7ce75aa544958c8babcdbc3ff11a2f0f3334516b96f66
|
Provenance
The following attestation bundles were made for path_sync-0.3.1-py3-none-any.whl:
Publisher:
release.yaml on EspenAlbert/path-sync
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
path_sync-0.3.1-py3-none-any.whl -
Subject digest:
d4acffabc632162f108abbe8ba35ee49a62cc53de8a651556676b79323b6154a - Sigstore transparency entry: 821222100
- Sigstore integration time:
-
Permalink:
EspenAlbert/path-sync@26c7f59b4ff28b5632d4e9c4f05e6db7e7ddc31e -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/EspenAlbert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@26c7f59b4ff28b5632d4e9c4f05e6db7e7ddc31e -
Trigger Event:
push
-
Statement type: