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
Onschedule, workflow_dispatch and push events, smartcloud:
- Reads every template in
source, leaving outexcluded paths. - Renders each one:
{{KEY}}placeholders are replaced fromvalues, and{{REPOSITORY}}is always the repository’sowner/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}}becomesnonewhen noPROJECT_TYPEis set, and the value when one is. This suits a value each repository may set for itself, in its ownsync.values, but need not. Excluded templates are not rendered, so a repository that keeps its own copy need not supply that template’s values. - 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.
- Opens, or updates, one pull request from
branchinto the default branch with every file that changed, titledchore(sync): sync files from owner/repo. Its body lists each file as added, updated or made executable. An executable template makes its file executable.
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:- The source ref (
main, a tag, or a commit) is resolved to a commit, one request, so the run reads one consistent snapshot. - 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.
- 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.
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,nameorid; - a Dependabot update that duplicates a synced one (in
dependabot.ymlordependabot.yaml, one entry per directory indirectories).
The edit check
On pull request events, unlesscheck 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, titledchore(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 / synccheck 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
SYNCfinding 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-onlyhouseToken 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 fromResnovas/.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
Sync was skipped: restricted access
Sync was skipped: restricted access
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.LICENSE: no value for {{HOLDER}}
LICENSE: no value for {{HOLDER}}
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.sync.source must be owner/repo/path@ref
sync.source must be owner/repo/path@ref
source is not in the form owner/repo/path@ref. The path must name a directory inside the repository; . and
.. segments are rejected.sync.branch is the default branch
sync.branch is the default branch
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.A pull request fails with SYNC: edits a synced file
A pull request fails with SYNC: edits a synced file
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.sync: could not read the templates as one archive
sync: could not read the templates as one archive
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.Refusing to allow a GitHub App to create or update workflow
Refusing to allow a GitHub App to create or update workflow
The token cannot push files under
.github/workflows. Give the app Workflows (read and write), or add the
workflow scope to a classic token.