A GitHub Action and supporting tooling for synchronizing files across Hanakai repositories in the hanami, dry-rb and rom-rb organizations.
In repo-sync.yml, we define groups of repos and files to sync. A simplified example:
dry:
repos:
- dry-rb/dry-auto_inject
- dry-rb/dry-cli
files:
- [templates/gem/.github/workflows/ci.yml.tpl, .github/workflows/ci.yml]
- [templates/gem/.rubocop.yml, .rubocop.yml]
- [templates/gem/gemspec.rb.tpl, "{{ .name.gem }}.gemspec"]
- [templates/gem/README.md.tpl, README.md]This config is used by our main .github/workflows/repo-sync.yml GitHub Actions workflow to sync these files to each repo.
When this action runs on the main branch, it:
- Checks out each repo.
- Validates the repo’s
repo-sync.ymlagainst the configured JSON schema. - For each file entry, copies the source file to the destination path within the repo.
- If the source file has a
.tplextension, evaluate the the source file as a text/template file using thegomplateCLI tool. - The values from
repo-sync.ymlare available within the template. - Destination filenames may also use the template syntax.
- If the source file has a
- Commits and pushes the changes directly to each repo’s main branch.
When this action runs for a PR, it:
- Syncs the repos as above, but in a
ci/repo-sync-preview-[PR_NUMBER]branch, so you can preview the changes. - Receives CI statuses from each repo and displays them as a comment on the PR (see
repo-sync-preview.ymlandaggregate-preview-status.yml).
Together, these should help you have confidence in your changes before you merge.
GitHub Action (repo-sync-action/)
A containerized GitHub action that runs the file sync. This is simple by design: just a Bash script gluing together a range of CLI tools.
entrypoint.sh manages the high-level flow, with most of the logic kept in functions.sh, allowing for reuse in local testing.
Local sync (local-sync/ via bin/local-sync)
A script to test the file sync against local checkouts of repos. This allows for fast and easy development of templates and sync logic, avoiding the hassle of waiting for CI and the risk of unexpected changes to real repositories.
To provide a faithful reproduction of the GitHub Action, this script also runs via Docker and invokes the same internal logic from the action’s functions.sh
Template library (templates/)
The templates we sync across our repos.
Currently, this is a single set of templates for our standard Ruby gem repositories. In future, we may expand the template library to cover different repo archetypes.
RuboCop config (rubocop/rubocop.yml)
A shared RuboCop config used across our repos.
This is kept here as a convenience, and is referenced directly by the RuboCop configs in each repo, which happen to be synced from templates/gem/.rubocop.yml.
For day-to-day changes, manage the sync config at repo-sync.yml. Add to the repos and files lists as needed.
repos should be a list of GitHub repo paths:
repos:
- hanami/hanami
- dry-rb/dry-operation
- rom-rb/rom-sqlTo use a non-default branch, specify the branch name after an @ delimiter:
repos:
- hanami/hanami@unstablefiles should be a list of [<source>, <destination>] array pairs:
files:
- [templates/gem/.github/workflows/ci.yml.tpl, .github/workflows/ci.yml]
- [templates/gem/.rubocop.yml, .rubocop.yml]Entire folders may be synced (though for our purposes, it’s unlikely we’ll need this). Specify folders with a trailing slash:
files:
- [templates/gem/some-folder/, another-folder/]Source files come from this repo, and destination files are created or updated in each target repo listed in repos. File paths are all relative to the root of each repo.
You can use template syntax to name destination files using data from each repo’s own repo-sync.yml. See template authoring for more details on this syntax.
files:
- [templates/gem/gemspec.rb.tpl, {{ .name.gem }}.gemspec]The action runs on:
- Pushes to the main branch (syncs directly to target repos)
- Pull requests (creates preview branches for testing)
- Manual triggers
Note
Later, we should add a daily scheduled run to trigger files changes in response to repo-sync.yml changs in each repo. Alternatively, we could sync a dedicated workflow to each repo that triggers a sync in this repo reponse to repo-sync.yml being updated.
We set these lower-level action parameters in .github/workflows/repo-sync-job.yml.
| Parameter | Required | Description |
|---|---|---|
REPOSITORIES |
Yes | List of repositories to sync files to (formatted as owner/repo or owner/repo@branch) |
FILES |
Yes | File mappings in source=destination format |
REPO_SYNC_SCHEMA_PATH |
Yes | Path to JSON schema for validation |
TOKEN |
Yes | GitHub access token with "repo" scope, plus "workflow" scope if managing Actions-related files |
GIT_EMAIL |
No | Git commit email (default committer is "github-actions[bot]") |
GIT_USERNAME |
No | Git commit username (default committer is "github-actions[bot]") |
PR_NUMBER |
No | Pull request number (automatically provided for PR events) |
PR_EVENT_TYPE |
No | PR event type (automatically provided for PR events) |
PREVIEW_BRANCH_PREFIX |
No | Prefix for preview branch names (default: ci/repo-sync-preview) |
Templates with .tpl extensions are are evaluated as Go text/template files using the gomplate CLI tool.
templates/gem/gemspec.rb.tpl is our most complex template so far, and a helpful example of what’s possible:
Gem::Specification.new do |spec|
spec.name = "{{ .name.gem }}"
spec.authors = ["{{ join .gemspec.authors "\", \"" }}"]
spec.email = ["{{ join .gemspec.email "\", \"" }}"]
spec.license = "MIT"
spec.version = {{ .name.constant }}::VERSION.dup
spec.summary = "{{ .gemspec.summary }}"
{{ if .gemspec.description }}{{ if gt (len .gemspec.description) 100 }}spec.description = <<~TEXT
{{ .gemspec.description | strings.TrimSpace | strings.TrimSuffix "\n" | strings.Indent 4 }}
TEXT{{ else }}spec.description = "{{ .gemspec.description }}"{{ end }}{{ else }}spec.description = spec.summary{{ end }}
spec.homepage = "{{ .gemspec.homepage }}"
spec.files = Dir["{{ join $file_globs "\", \"" }}"]
spec.bindir = "bin"
# ...
end
Values like .name.gem and .gemspec.summary come from each repo’s repo-sync.yml file. For example:
name:
gem: hanami-view
gemspec:
summary: "A super cool view rendering system"repo-sync.yml is validated according to a JSON schema (at templates/repo-sync-schema.json), which should be updated as new values are required.
Functions like if, eq, len, default, join, etc. are available from:
To test file sync locally, first make a local clone of a target repo. Then run bin/local-sync:
bin/local-sync --org <org-name> /path/to/repoThe --org parameter is required and specifies which organization's job configuration to use from the workflow file. Available options are:
dry- for gems from Dry ecosystemhanami- for gems building Hanami framework
After this, you can verify the changes by running git diff in the target repo.
By default, the templates/repo-sync-schema.json JSON schema is used. If you want to use a different schema file, use --schema:
bin/local-sync --schema templates/another-schema.json /path/to/repoThe local sync runs in a Docker container defined by local-sync/Dockerfile. If you’re developing the tool itself, you can force the container to rebuild with --rebuild:
bin/local-sync --rebuild /path/to/repoTo debug the container, enter an interactive shell with --shell:
bin/local-sync --shell /path/to/repo- Clone a target repo for testing
- Make changes to templates or action code
- Test locally:
bin/local-sync /path/to/repo - Verify changes:
cd /path/to/test/repo && git diff - Commit and push the changes to trigger the GitHub Action and sync files to the real repositories on GitHub.
The repo-sync-action is originally adapted from Kevin Brashears’ github-action-file-sync. Thank you, Kevin!