Skip to main content

Sync files from a source repo to multiple destination repos

Project description

path-sync

PyPI GitHub codecov Docs

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-separated path:section_id pairs 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 identifier
  • OK_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

Skipping Files per Destination

Use skip_file_patterns to exclude files for specific destinations. Patterns match against the destination path (after dest_path remapping):

paths:
  - src_path: scripts/
    dest_path: tools/  # remapped in destination
destinations:
  - name: dest1
    dest_path_relative: ../dest1
    skip_file_patterns:
      - "tools/internal/*"   # matches destination path, not src
      - "*.test.py"
      - "docs/draft.md"

Patterns use fnmatch syntax (* matches any characters, ? matches single character).

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
  - src_path: scripts/
    exclude_file_patterns:
      - "*.pyc"
      - "test_*.py"
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]
    skip_file_patterns:
      - "scripts/internal/*"
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 (see path options below)
destinations Target repos with sync settings
header_config Comment style per extension (has defaults)
pr_defaults PR title, labels, reviewers, assignees

Path options:

Field Description
src_path Source file, directory, or glob pattern (required)
dest_path Destination path (defaults to src_path)
sync_mode sync (default), replace, or scaffold
exclude_dirs Directory names to skip (defaults: __pycache__, .git, .venv, etc.)
exclude_file_patterns Filename patterns to skip, supports globs (*.pyc, test_*.py)

Destination options:

Field Description
name Destination identifier (required)
repo_url Repo URL for cloning if not found locally
dest_path_relative Path to destination repo relative to source (required)
copy_branch Branch for sync (defaults to sync/{config_name})
default_branch Default branch to compare against (defaults to main)
skip_sections Map of {dest_path: [section_ids]} to preserve locally
skip_file_patterns Patterns to skip for this destination (matches dest path, fnmatch syntax)

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 uses sync/{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

Dependency Updates

The dep-update command runs dependency updates across multiple repositories. It clones repos, runs update commands, verifies changes, and creates PRs.

Quick Start

# Create config at .github/myconfig.dep.yaml (see example below)
# Then run:
path-sync dep-update -n myconfig

# Preview without creating PRs
path-sync dep-update -n myconfig --dry-run

# Filter specific destinations
path-sync dep-update -n myconfig -d repo1,repo2

Dep Config Reference

Config file: .github/{name}.dep.yaml

name: uv-deps
from_config: python-template  # references .github/python-template.src.yaml for destinations
exclude_destinations:
  - path-sync  # skip self

updates:
  - command: uv lock --upgrade
  - workdir: packages/sub  # optional subdirectory
    command: uv lock --upgrade

verify:
  on_fail: skip  # default strategy: skip, fail, warn
  steps:
    - run: uv sync
    - run: just fmt
      commit:
        message: "chore: format after update"
        add_paths: [".", "!uv.lock"]  # ! prefix excludes
      on_fail: warn
    - run: just test

pr:
  branch: deps/uv-lock-update
  title: "chore(deps): update uv.lock"
  labels: [dependencies]
  reviewers: []  # optional
  assignees: []  # optional
  auto_merge: true
Field Description
from_config Source config name for destination list
include_destinations Only process these destinations
exclude_destinations Skip these destinations
updates Commands to run (in order)
verify.on_fail Default failure strategy: skip, fail, warn
verify.steps Verification commands with optional commit/on_fail
pr.auto_merge Enable GitHub auto-merge after PR creation

CLI Flags

Flag Description
-n, --name Config name (required)
-d, --dest Filter destinations (comma-separated)
--work-dir Clone directory for repos without dest_path_relative
--dry-run Preview without creating PRs
--skip-verify Skip verification steps
--pr-reviewers Override PR reviewers (comma-separated)
--pr-assignees Override PR assignees (comma-separated)

Failure Strategies

  • skip: Skip PR for this repo, continue with others (default)
  • fail: Stop all processing immediately
  • warn: Create PR anyway with warning in body

Per-step on_fail overrides the verify-level default.

GitHub Actions

name: dep-update
on:
  schedule:
    - cron: "0 6 * * 1"  # Weekly Monday
  workflow_dispatch:

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uvx path-sync dep-update -n myconfig
        env:
          GH_TOKEN: ${{ secrets.GH_PAT }}

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

path_sync-0.5.0.tar.gz (20.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

path_sync-0.5.0-py3-none-any.whl (29.1 kB view details)

Uploaded Python 3

File details

Details for the file path_sync-0.5.0.tar.gz.

File metadata

  • Download URL: path_sync-0.5.0.tar.gz
  • Upload date:
  • Size: 20.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for path_sync-0.5.0.tar.gz
Algorithm Hash digest
SHA256 efbfbe82b18a15a1428cf2008083f5a741c69216eee05594174a6f1ab773d949
MD5 557f68085010c05e9e0cd1d885dca301
BLAKE2b-256 43c632e9e1f3176a053d0339a81dab6099c1ce0054a2b5782f361f5fce641b17

See more details on using hashes here.

Provenance

The following attestation bundles were made for path_sync-0.5.0.tar.gz:

Publisher: release.yaml on EspenAlbert/path-sync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file path_sync-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: path_sync-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 29.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for path_sync-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 257a994c456a3a890b75af4955df1a528116d3c0283054b6d2a27c3c3e8aa9ea
MD5 0f1655aa72b205f0d8c3639424b47970
BLAKE2b-256 4544cc7095710d58c0399f285102a01a776accb664d89edc960e0aa9e2757fb9

See more details on using hashes here.

Provenance

The following attestation bundles were made for path_sync-0.5.0-py3-none-any.whl:

Publisher: release.yaml on EspenAlbert/path-sync

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page