Skip to main content
Most organisations want the same files in every repository: a licence, a code of conduct, issue forms, a Dependabot config, shared workflows. Copying them by hand works once, then the copies drift. The sync feature keeps them in step. You keep the master copies, called templates, in one directory of one repository. smartcloud renders them for each repository that syncs from it and opens a pull request whenever a file is out of date. It also fails pull requests that edit synced content by hand, so a change is made once, in the templates, and reaches every repository. A file can be synced whole, or only in part: a managed block lets the template own some lines while the repository adds its own around them.
Setting this up for a whole organisation? Your organisation’s sync hub walks through it end to end: the hub repository, the preset, the templates, the app, the first sync pull request and rolling out a change.

Turn it on

1

Put your templates in a repository

Create a directory of templates, for example templates/ in your organisation’s .github repository. Each file is placed at the same path in the target repository, so templates/.github/dependabot.yml becomes .github/dependabot.yml. Write {{KEY}} wherever a value differs per repository.
2

Give smartcloud a token that can read the templates and push workflows

The workflow token cannot read another private repository or change files under .github/workflows, so sync needs a GitHub App token. A personal access token can sync too, but GitHub never lets one create check runs, so the smartcloud checks cannot be published with it. The app needs Contents, Pull requests and Workflows (read and write) on the target repository and Contents (read) on the template repository; Contents (read) also covers downloading a repository’s archive, which is how the templates are read. Settings shows how to create the app and pass its token as GITHUB_TOKEN.
3

Run on the right events

Syncing runs on schedule, workflow_dispatch and push. The edit check runs on pull request events. The workflow in Getting started has all of them.
4

Add a sync section

.github/smartcloud.yml
5

Preview the result

smartcloud sync --out <dir> renders the templates into a local directory so you can see what would be proposed, and a run with dryRun: true lists the files without opening a pull request. Then run the workflow by hand: a pull request titled chore(sync): sync files from my-org/.github appears.

Options

Complete example

.github/smartcloud.yml

How it works

Syncing

On schedule, workflow_dispatch and push events, smartcloud:
  1. Reads every template in source, leaving out excluded paths.
  2. Renders each one: {{KEY}} placeholders are replaced from values, and {{REPOSITORY}} is always the repository’s owner/name. A placeholder with no value fails the sync (<file>: no value for {{KEY}}) rather than leaving a blank. To make a value optional, give the placeholder a default: {{PROJECT_TYPE:-none}} becomes none when no PROJECT_TYPE is set, and the value when one is. This suits a value each repository may set for itself, in its own sync.values, but need not. Excluded templates are not rendered, so a repository that keeps its own copy need not supply that template’s values.
  3. Merges each rendered template into the matching file on the default branch: a file without a managed block is replaced whole, and a file with one has only its block replaced.
  4. Opens, or updates, one pull request from branch into the default branch with every file that changed, titled chore(sync): sync files from owner/repo. Its body lists each file as added, updated or made executable. An executable template makes its file executable.
The commit is made through the GitHub API as the token’s own identity, and signed off as that identity. GitHub signs it when the token is the workflow token or a GitHub App token, so it passes a ruleset that requires signed commits. Setting SMARTCLOUD_COMMITTER_NAME and SMARTCLOUD_COMMITTER_EMAIL commits and signs off as that identity instead, which GitHub does not sign. When nothing is out of date, no pull request is opened.

How the files are read

A sync reads the templates and the repository’s current files from one download each, so it costs the same handful of GitHub requests however many templates the source holds:
  1. The source ref (main, a tag, or a commit) is resolved to a commit, one request, so the run reads one consistent snapshot.
  2. The repository archive at that commit is downloaded once (GitHub’s tarball endpoint, which redirects to a download that carries no token), and the files under the template directory are taken from it, with their execute bits.
  3. The repository’s own default branch is read the same way: its commit is resolved, its archive downloaded once, and only the synced paths kept.
An archive over 64 MB (compressed) is not read. Where an archive cannot be read, because it is over that size, the token cannot see it, or GitHub cannot serve it, that side is read the old way instead: its tree is listed and each synced file fetched on its own, and the run’s log carries a warning, sync: could not read the templates as one archive, or the repository, with the reason. The result is the same either way, only the request count differs. There is no option for this: the fallback is automatic. The edit check reads only the templates a pull request touches, so it lists the template directory first and then fetches up to two touched templates one by one, or every touched template from one archive when there are more, whichever costs fewer requests.

Managed blocks

A template without markers is synced whole. A template with a managed block is extendable: smartcloud only replaces the lines between the markers and keeps everything the repository adds around them.
.github/dependabot.yml
Markers count only on comment lines: # in YAML, TOML and CODEOWNERS, // in JSON with comments (such as editor settings), and <!-- in Markdown. A document that merely mentions a marker in its text is not split. The first time a file the repository already had is adopted, its previous content is kept, commented out, at the house:local line (or at the end), so nothing is lost silently. The Resnovas .github/smartcloud.yml is itself synced this way: the extends line sits in the managed block, and each repository adds its own config below # house:local. See Presets. Local additions that conflict with the synced rules are reported as warnings when syncing:
  • local rules after a block that must come last, as in CODEOWNERS, where the last matching rule wins (“local rules after the managed block would override it; move them above the block”);
  • a local YAML top-level key, issue or discussion form field id, or workflow job that redefines a synced one;
  • a local JSON entry that reuses a synced label, name or id;
  • a Dependabot update that duplicates a synced one (in dependabot.yml or dependabot.yaml, one entry per directory in directories).

The edit check

On pull request events, unless check is false, smartcloud flags edits to synced content with the rule id SYNC: Each is an error for contributors and a warning for maintainers and the owner (see maintainerLevel). Change synced content in the source repository instead; a conflicting local rule is fixed in the repository itself. A conflict already on the default branch is left to the sync’s warning, so it does not fail unrelated pull requests or the sync’s own. Bringing a file in line with the latest templates is always allowed. Only the synced files the pull request touches are read: the files it lists as changed, and those the base has and its head lacks, since GitHub lists a renamed file by its new path alone. Pull requests in the source repository itself are not checked, since they legitimately change the templates. Findings link to GOVERNANCE.md#synced-files under links.policyBase.

What you will see on GitHub

  • A sync pull request from branch, titled chore(sync): sync files from owner/repo, listing every file added, updated or made executable. It is updated in place on later runs until it is merged.
  • A smartcloud / sync check run on the commit. On a sync run it carries conflict warnings; on a pull request it fails when a contributor edited synced content, and is neutral for a maintainer’s own edit.
  • The report comment on a pull request, listing each SYNC finding with the file and a link to your governance page.
  • The job summary, listing each file proposed and the pull request number.

Forks and restricted runs

Sync needs a token that can read the source and push workflow files, so a restricted run skips the whole feature, syncing and the edit check alike, and the job summary says “cross-repository sync needs a token that can read the source and push workflow files”. That covers runs with only the workflow token, pull requests from forks, and Dependabot’s runs and pull requests. The one exception: a pull request run given a read-only houseToken still runs the edit check, which only reads the templates in .github; see Tokens and access.
Because the edit check is skipped on a fork’s pull request, a reviewer should look for edits to synced files there by hand. The next scheduled sync after merging proposes putting the synced content back.

In the house preset

The Resnovas house preset syncs from Resnovas/.github/templates@main on the branch house/sync, with check: true and the house values: ORG_NAME, LEGAL_HOLDER, COPYRIGHT_YEAR, PACKAGE_SCOPE, CONDUCT_CONTACT, COVERAGE_MIN, SPONSORS and the OWNERS_* CODEOWNERS handles. A repository adds sync.exclude for the templates it keeps its own copy of, and may add maintainerLevel; it cannot change source, branch, check or a value the preset sets.

Troubleshooting

The run had only the workflow token, or came from a fork or Dependabot. Pass a GitHub App token or a personal access token with repo and workflow as GITHUB_TOKEN. smartcloud doctor reports a classic token without the workflow scope.
A template uses a placeholder with no entry in sync.values. Add the value, or exclude the template if this repository keeps its own copy.
source is not in the form owner/repo/path@ref. The path must name a directory inside the repository; . and .. segments are rejected.
The sync force-updates its branch, so it must not be the branch the pull request merges into. Choose another branch, or leave it at smartcloud/sync.
The pull request changed content the templates own. Revert that change and make it in the template repository instead. For a file with a managed block, move your change below the house:local line. If this repository should own the file outright, add it to sync.exclude.
A warning in the run’s log, not a failure: the run read that side one file at a time instead, which costs more requests. The reason follows in brackets. ValidationFailed means the archive is over 64 MB compressed, or was not a valid archive; NotFound or Forbidden means the token could not see the repository’s archive. The sync’s result is unchanged.
The token cannot push files under .github/workflows. Give the app Workflows (read and write), or add the workflow scope to a classic token.
Last modified on September 28, 2026