Skip to content

raven-actions/repo-files-sync

Repository files navigation

πŸ” Repo Files Sync Action

GitHub - marketplace GitHub - release GitHub - ci GitHub - license Codecov


⚠️ This is a heavily modified fork project of the repo-file-sync-action, which looks like is stale.

Keep files like Action workflows or entire directories in sync between multiple repositories.

πŸ‘‹ Introduction

With repo-files-sync you can sync files, like workflow .yml files, configuration files or whole directories between repositories or branches. It works by running a GitHub Action in your main repository every time you push something to that repo. The action will use a sync.yml config file to figure out which files it should sync where. If it finds a file which is out of sync it will open a pull request in the target repository with the changes.

πŸš€ Features

  • Keep GitHub Actions workflow files in sync across all your repositories
  • Sync any file or a whole directory to as many repositories as you want
  • Easy configuration for any use case
  • Create a pull request in the target repo so you have the last say on what gets merged
  • Filter directory syncs using glob patterns (include, exclude)
  • Optionally delete orphaned files in the target (deleteOrphaned / DELETE_ORPHANED)
  • Automatically label pull requests to integrate with other actions like automerge-action
  • Assign users to the pull request
  • Request reviews globally or per group (users and teams)
  • Sync using a GitHub App installation token (GitHub-verified commits)
  • Optional fork-based workflow (push to forks, open PRs upstream)
  • Render Jinja-style templates and use variables thanks to Nunjucks

πŸ“š Usage

Workflow

Create a .yml file in your .github/workflows folder (you can find more info about the structure in the GitHub Docs):

.github/workflows/sync.yml

name: Sync

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  sync:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Run Files Sync
        uses: raven-actions/repo-files-sync@v1
        with:
          GH_PAT: ${{ secrets.GH_PAT }}

Token

In order for the Action to access your repositories you have to specify a Personal Access token as the value for GH_PAT (GITHUB_TOKEN will not work). The PAT needs the full repo scope (#31).

It is recommended to set the token as a Repository Secret.

Alternatively, you can provide the token of a GitHub App Installation via the GH_INSTALLATION_TOKEN input. You can obtain such token for example via this action. Tokens from apps have the advantage that they provide more granular access control.

The app needs to be configured for each repo you want to sync to, and have the Contents read & write and Metadata read-only permission. If you want to use PRs (default setting) you additionally need Pull requests read & write access, and to sync workflow files you need Workflows read & write access.

If using an installation token you are required to provide the GIT_EMAIL and GIT_USERNAME input.

Sync configuration

The last step is to create a .yml file in the .github folder of your repository and specify what file(s) to sync to which repositories:

.github/sync.yml

user/repository:
  - .github/workflows/test.yml
  - .github/workflows/lint.yml

user/repository2:
  - source: workflows/stale.yml
    dest: .github/workflows/stale.yml

More info on how to specify what files to sync where below.

Versioning

To always use the latest version of the action add the latest tag to the action name like this:

uses: raven-actions/repo-files-sync@latest

If you want to make sure that your workflow doesn't suddenly break when a new major version is released, use the v1 tag instead (recommended usage):

uses: raven-actions/repo-files-sync@v1

With the v1 tag you will always get the latest non-breaking version which will include potential bug fixes in the future. If you use a specific version, make sure to regularly check if a new version is available, or enable Dependabot.

βš™οΈ Action Inputs

Here are all the inputs repo-files-sync takes:

Key Value Required Default
GH_PAT Your Personal Access token GH_PAT or GH_INSTALLATION_TOKEN required N/A
GH_INSTALLATION_TOKEN Token from a GitHub App installation GH_PAT or GH_INSTALLATION_TOKEN required N/A
CONFIG_PATH Path to the sync configuration file No .github/sync.yml
INLINE_CONFIG Inline YAML configuration (alternative to CONFIG_PATH) No N/A
IS_FINE_GRAINED Labels the GH_PAT as a fine grained token No false
PR_LABELS Labels which will be added to the pull request. Set to false to turn off No sync
ASSIGNEES Users to assign to the pull request No N/A
REVIEWERS Users to request a review of the pull request from No N/A
TEAM_REVIEWERS Teams to request a review of the pull request from No N/A
COMMIT_PREFIX Prefix for commit message and pull request title No πŸ”„
COMMIT_BODY Commit message body. Will be appended to commit message, separated by two line returns. No ''
PR_BODY Additional content to add in the PR description. No ''
ORIGINAL_MESSAGE Use original commit message instead. Only works if the file(s) were changed and the action was triggered by pushing a single commit. No false
COMMIT_AS_PR_TITLE Use first line of the commit message as PR title. Only works if ORIGINAL_MESSAGE is true and working. No false
COMMIT_EACH_FILE Commit each file seperately No true
GIT_EMAIL The e-mail address used to commit the synced files Only when using installation token the email of the PAT used
GIT_USERNAME The username used to commit the synced files Only when using installation token the username of the PAT used
OVERWRITE_EXISTING_PR Overwrite any existing Sync PR with the new changes No true
BRANCH_PREFIX Specify a different prefix for the new branch in the target repo No repo-sync/SOURCE_REPO_NAME
TMP_DIR The working directory where all git operations will be done No tmp-${ Date.now().toString() }
DRY_RUN Run everything except that nothing will be pushed No false
SKIP_CLEANUP Skips removing the temporary directory. Useful for debugging No false
SKIP_PR Skips creating a Pull Request and pushes directly to the default branch No false
DELETE_ORPHANED Global default for deleting orphaned files in target repositories (used when file-level deleteOrphaned is not set) No false
FORK A Github account username. Changes will be pushed to a fork of target repos on this account. No false

Input behavior notes

  • DRY_RUN: true runs the full sync logic, but does not push any changes.
  • SKIP_PR: true pushes changes directly to the target repo's default branch (no PR).
  • SKIP_CLEANUP: true keeps the working directory (TMP_DIR) on the runner for debugging.

Outputs

The action sets the pull_request_urls output to the URLs of any created Pull Requests. It will be an array of URLs to each PR, e.g. '["https://github.com/username/repository/pull/number", "..."]'.

πŸ› οΈ Sync Configuration

In order to tell repo-files-sync what files to sync where, you have to create a sync.yml file in the .github directory of your main repository (see action-inputs on how to change the location).

πŸ’‘ Tip: For IDE validation and autocompletion, add this comment at the top of your sync.yml:

# yaml-language-server: $schema=https://raw.githubusercontent.com/raven-actions/repo-files-sync/main/sync.schema.json

The top-level key should be used to specify the target repository in the format username/repository-name@branch, after that you can list all the files you want to sync to that individual repository:

user/repo:
  - path/to/file.txt
user/repo2@develop:
  - path/to/file2.txt

Inline configuration

Instead of using a configuration file, you can provide the sync configuration directly in your workflow using INLINE_CONFIG:

- name: Run Files Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    INLINE_CONFIG: |
      user/repo:
        - LICENSE
        - .github/workflows/ci.yml
      user/repo2:
        - source: src/
          dest: lib/

This is useful for simple configurations or when you want to keep everything in one file.

There are multiple ways to specify which files to sync to each individual repository.

List individual file(s)

The easiest way to sync files is the list them on a new line for each repository:

user/repo:
  - .github/workflows/build.yml
  - LICENSE
  - .gitignore

Different destination path/filename(s)

Using the dest option you can specify a destination path in the target repo and/or change the filename for each source file:

user/repo:
  - source: workflows/build.yml
    dest: .github/workflows/build.yml
  - source: LICENSE.md
    dest: LICENSE

Sync entire directories

You can also specify entire directories to sync:

user/repo:
  - source: workflows/
    dest: .github/workflows/

Exclude certain files when syncing directories

Using the exclude key you can specify files you want to exclude when syncing entire directories (#26).

user/repo:
  - source: workflows/
    dest: .github/workflows/
    exclude: |
      node.yml
      lint.yml

Note

  • exclude patterns are glob patterns and are relative to the source directory.
  • You can also paste full paths like workflows/lint.yml; they will be normalized back to a path relative to source.

Include or exclude files when syncing directories

Use include to limit which files are synced and exclude to skip files. Both accept newline-separated glob patterns relative to the source path. These filters are also respected when deleteOrphaned is enabled, so excluded files are not removed.

Precedence (directory sync):

  1. If include is set, only matching files are considered.
  2. Then exclude removes files from that set.
user/repo:
  - source: workflows/
    include: |
      **/*.yml
      **/*.yaml
    exclude: |
      **/legacy/**

Note

Pattern matching uses minimatch (same glob style as many JS tooling ecosystems).

Don't replace existing file(s)

By default if a file already exists in the target repository, it will be replaced. You can change this behaviour by setting the replace option to false.

For single files, replace: false will skip syncing if the destination file already exists:

user/repo:
  - source: .github/workflows/lint.yml
    replace: false

For directories, replace: false is applied file-by-file:

  • Files that already exist at the destination are not overwritten.
  • New files are still copied.
  • Existing extra files in the destination are only removed if deleteOrphaned: true.
user/repo:
  - source: workflows/
    dest: .github/workflows/
    replace: false

Using templates

You can render templates before syncing by using the Jinja-style template syntax. It will be compiled using Nunjucks and the output written to the specific file(s) or folder(s).

Nunjucks supports variables and blocks among other things. To enable, set the template field to a context dictionary, or in case of no variables, true:

user/repo:
  - source: src/README.md
    template:
      user:
        name: Raven-Actions
        handle: @raven-actions

In the source file you can then use these variables like this:

# README.md

Created by {{ user.name }} ({{ user.handle }})

Result:

# README.md

Created by Raven-Actions (@raven-actions)

You can also use extends with a relative path to inherit other templates. Take a look at Nunjucks template syntax for more info.

user/repo:
  - source: .github/workflows/child.yml
    template: true
# child.yml
{% extends './parent.yml' %}

{% block some_block %}
This is some content
{% endblock %}

Built-in template variables

Every Nunjucks template context automatically includes a repo object with information about the target repository:

Variable Description Example
repo.url Full HTTPS URL https://github.com/user/repo
repo.fullName Host + owner + name github.com/user/repo
repo.uniqueName Full name with branch github.com/user/repo@main
repo.host Host name github.com
repo.user Owner/organization user
repo.name Repository name repo
repo.branch Target branch main

This is useful for bulk templating across multiple repositories:

group:
  repos: |
    user/repo1
    user/repo2
  files:
    - source: templates/README.md
      dest: README.md
      template: true
<!-- templates/README.md -->
# {{ repo.name }}

Repository: [{{ repo.fullName }}]({{ repo.url }})
Maintained by: {{ repo.user }}

Delete orphaned files

With the deleteOrphaned option you can choose to delete files in the target repository if they are deleted in the source repository. The option defaults to false and works for both directories and individual files.

If you want to enable this globally for all file entries, set the action input DELETE_ORPHANED: true. Individual file entries can still override it with deleteOrphaned: true|false.

user/repo:
  - source: workflows/
    dest: .github/workflows/
    deleteOrphaned: true

For single files, if the source file no longer exists and deleteOrphaned is true, the destination file will be removed:

user/repo:
  - source: config.json
    dest: config.json
    deleteOrphaned: true

Sync the same files to multiple repositories

Instead of repeating yourself listing the same files for multiple repositories, you can create a group:

group:
  repos: |
    user/repo
    user/repo1
  files:
    - source: workflows/build.yml
      dest: .github/workflows/build.yml
    - source: LICENSE.md
      dest: LICENSE

You can create multiple groups like this:

group:
  # first group
  - files:
      - source: workflows/build.yml
        dest: .github/workflows/build.yml
      - source: LICENSE.md
        dest: LICENSE
    repos: |
      user/repo1
      user/repo2

  # second group
  - files:
      - source: configs/dependabot.yml
        dest: .github/dependabot.yml
    repos: |
      user/repo3
      user/repo4

Group-level reviewers

You can specify reviewers at the group level to override the global REVIEWERS setting for specific groups:

group:
  repos: |
    user/repo1
    user/repo2
  files:
    - source: rules/
      dest: .cursor/rules/
  reviewers:
    - username1
    - username2

This will automatically request a review from the specified users when PRs are created for repositories in this group.

Note

Group-level reviewers override the global REVIEWERS input for that group.

Branch suffix for groups

When syncing different file sets to the same repository, use branchSuffix to create unique PR branches:

group:
  - repos: |
      user/repo
    files:
      - source: config/
        dest: config/
    branchSuffix: config-sync

  - repos: |
      user/repo
    files:
      - source: workflows/
        dest: .github/workflows/
    branchSuffix: workflow-sync

The branchSuffix value is also appended to the PR title so parallel syncs are easy to distinguish.

Overwriting existing PRs and branches

If OVERWRITE_EXISTING_PR is true (default), the action will try to reuse the same sync branch/PR each run for a given target repo + branch + branchSuffix.

If OVERWRITE_EXISTING_PR is false, the action will create a new branch for each run instead (timestamp suffix). This is useful when you want to keep previous sync PRs open, or avoid non-fast-forward push failures when the existing sync branch has been modified.

Note

The action does not force-push. If the remote branch moved, the push can fail with a non-fast-forward error.

Syncing branches

You can also sync different branches from the same or different repositories (#51). For example, a repository named foo/bar with branch main, and sync.yml contents:

group:
  repos: |
    foo/bar@de
    foo/bar@es
    foo/bar@fr
  files:
    - source: .github/workflows/
      dest: .github/workflows/

Here all files in .github/workflows/ will be synced from the main branch to the branches de/es/fr.

πŸ“– Examples

Here are a few examples to help you get started!

Basic Example

.github/sync.yml

user/repository:
  - LICENSE
  - .gitignore

Sync all workflow files

This example will keep all your .github/workflows files in sync across multiple repositories:

.github/sync.yml

group:
  repos: |
    user/repo1
    user/repo2
  files:
    - source: .github/workflows/
      dest: .github/workflows/

Custom labels

By default repo-files-sync will add the sync label to every PR it creates. You can turn this off by setting PR_LABELS to false, or specify your own labels:

.github/workflows/sync.yml

- name: Run GitHub File Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    PR_LABELS: |
      file-sync
      automerge

Assign a user to the PR

You can tell repo-files-sync to assign users to the PR with ASSIGNEES:

.github/workflows/sync.yml

- name: Run GitHub File Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    ASSIGNEES: raven-actions

Request a PR review

You can tell repo-files-sync to request a review of the PR from users with REVIEWERS and from teams with TEAM_REVIEWERS:

.github/workflows/sync.yml

- name: Run GitHub File Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    REVIEWERS: |
      raven-actions
      raven-actions-bot
    TEAM_REVIEWERS: engineering

Custom GitHub Enterprise Host

If your target repository is hosted on a GitHub Enterprise Server you can specify a custom host name like this:

.github/workflows/sync.yml

https://custom.host/user/repo:
  - path/to/file.txt

# or in a group

group:
  - files:
      - source: path/to/file.txt
        dest: path/to/file.txt
    repos: |
      https://custom.host/user/repo

Note: The key has to start with http to indicate that you want to use a custom host.

Different branch prefix

By default all new branches created in the target repo will be in the this format: repo-sync/SOURCE_REPO_NAME/SOURCE_BRANCH_NAME, with the SOURCE_REPO_NAME being replaced with the name of the source repo and SOURCE_BRANCH_NAME with the name of the source branch.

If your repo name contains invalid characters, like a dot (#32), you can specify a different prefix for the branch (the text before /SOURCE_BRANCH_NAME):

.github/workflows/sync.yml

uses: raven-actions/repo-files-sync@v1
with:
    GH_PAT: ${{ secrets.GH_PAT }}
    BRANCH_PREFIX: custom-branch

The new branch will then be custom-branch/SOURCE_BRANCH_NAME.

You can use SOURCE_REPO_NAME in your custom branch prefix as well and it will be replaced with the actual repo name

Custom commit body

You can specify a custom commit body. This will be appended to the commit message, separated by two new lines. For example:

.github/workflows/sync.yml

- name: Run GitHub File Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    COMMIT_BODY: "Change-type: patch"

The above example would result in a commit message that looks something like this:

πŸ”„ synced local '<filename>' with remote '<filename>'

Change-type: patch

Add content to the PR body

You can add more content to the PR body with the PR_BODY option. For example:

.github/workflows/sync.yml

- name: Run GitHub File Sync
  uses: raven-actions/repo-files-sync@v1
  with:
    GH_PAT: ${{ secrets.GH_PAT }}
    PR_BODY: This is your custom PR Body

It will be added below the first line of the body and above the list of changed files. The above example would result in a PR body that looks something like this:

synced local file(s) with GITHUB_REPOSITORY.

This is your custom PR Body

β–Ά Changed files

---

This PR was created automatically by the repo-files-sync workflow run xxx.

Fork and pull request workflow

If you do not wish to grant this action write access to target repositories, you can specify a bot/user Github acccount that you do have access to with the FORK parameter.

A fork of each target repository will be created on this account, and all changes will be pushed to a branch on the fork, instead of upstream. Pull requests will be opened from the forks to target repositories.

Note: while you can open pull requests to target repositories without write access, some features, like applying labels, are not possible.

uses: raven-actions/repo-files-sync@v1
with:
    GH_PAT: ${{ secrets.GH_PAT }}
    FORK: file-sync-bot

Advanced sync config

Here's how I keep common files in sync across my repositories. The main repository raven-actions/.workflows contains all the files I want to sync and the repo-files-sync Action which runs on every push.

Using groups I can specify which file(s) should be synced to which repositories:

.github/sync.yml

group:
  # dependabot files
  - files:
      - source: configs/dependabot.yml
        dest: .github/dependabot.yml
      - source: workflows/dependencies/dependabot.yml
        dest: .github/workflows/dependabot.yml
    repos: |
      raven-actions/do-spaces-action
      raven-actions/running-at
      raven-actions/spaces-cli
      raven-actions/metadata-scraper
      raven-actions/ejs-serve
      raven-actions/feedback-js
      raven-actions/drkmd.js

  # GitHub Sponsors config
  - files:
      - source: configs/FUNDING.yml
        dest: .github/FUNDING.yml
    repos: |
      raven-actions/do-spaces-action
      raven-actions/running-at
      raven-actions/spaces-cli
      raven-actions/qrgen
      raven-actions/metadata-scraper
      raven-actions/ejs-serve
      raven-actions/feedback-js
      raven-actions/drkmd.js

  # Semantic release
  - files:
      - source: workflows/versioning/release-scheduler.yml
        dest: .github/workflows/release-scheduler.yml
      - source: workflows/versioning/release.yml
        dest: .github/workflows/release.yml
      - source: configs/release.config.js
        dest: release.config.js
    repos: |
      raven-actions/do-spaces-action
      raven-actions/metadata-scraper
      raven-actions/feedback-js
      raven-actions/drkmd.js

  # Stale issues workflow
  - files:
      - source: workflows/issues/stale.yml
        dest: .github/workflows/stale.yml
    repos: |
      raven-actions/do-spaces-action
      raven-actions/running-at
      raven-actions/spaces-cli
      raven-actions/qrgen
      raven-actions/metadata-scraper
      raven-actions/ejs-serve
      raven-actions/feedback-js
      raven-actions/drkmd.js

  # Lint CI workflow
  - files:
      - source: workflows/node/lint.yml
        dest: .github/workflows/lint.yml
    repos: |
      raven-actions/do-spaces-action
      raven-actions/running-at
      raven-actions/spaces-cli
      raven-actions/metadata-scraper
      raven-actions/ejs-serve
      raven-actions/feedback-js
      raven-actions/drkmd.js

  # MIT License
  - files:
      - source: LICENSE
        dest: LICENSE
    repos: |
      raven-actions/do-spaces-action
      raven-actions/running-at
      raven-actions/spaces-cli
      raven-actions/qrgen
      raven-actions/metadata-scraper
      raven-actions/ejs-serve
      raven-actions/feedback-js
      raven-actions/drkmd.js

πŸ—οΈ Project changes - fork vs source

  • Added INLINE_CONFIG input to supply the sync config inline (no .github/sync.yml required).
  • Added directory filtering via include (allowlist) and exclude (denylist) using glob patterns (minimatch).
  • Added global DELETE_ORPHANED default and extended deleteOrphaned to work for single files as well as directories.
  • Added group-level reviewers (overrides global REVIEWERS) and branchSuffix to support multiple independent sync PRs per target repo.
  • Templates: always inject a built-in repo object into the Nunjucks context (host/user/name/branch/url, etc.) for easier bulk templating.
  • Improved path/pattern normalization (accepts full paths and normalizes them relative to source; consistent / handling across OSes).

πŸ‘₯ Contributing

Contributions to the project are welcome! Please follow Contributing Guide.

πŸ›‘οΈ License

This project is distributed under the terms of the MIT license.

About

πŸ” Keep files in sync between multiple repositories.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors