Skip to main content
A sync hub is one repository that every other repository in your organisation takes its rules and shared files from. With smartcloud, the hub holds two things:
  • a preset: the settings every repository shares, such as labels, title rules, review rules and the default branch ruleset;
  • a folder of templates: files every repository should have, such as LICENSE, SECURITY.md, dependabot.yml and the smartcloud workflow itself.
Each repository’s settings file says “use the hub” in two lines. From then on, a change made once in the hub reaches every repository: preset changes on each repository’s next run, and template changes as a pull request someone reviews and merges. Why you would want one:
  • No drift. Forty repositories stop holding forty slightly different copies of the same files.
  • One review for a policy change. Tightening a rule is one pull request in the hub, not one per repository.
  • Rules that stick. A repository can add to the preset but not weaken it, and a pull request that edits a synced file by hand fails a check.
This guide builds a hub for an organisation called my-org. Replace my-org with your organisation’s login throughout. It takes about an hour, most of it writing your own templates. Resnovas runs its own repositories this way from Resnovas/.github, which is a useful full-size example once you have the basics working.
New to the words used here? Preset, GitHub App, ruleset and restricted run are in the glossary. The Sync and Presets pages describe each feature on its own; this guide puts them together.

What you will build

What happens once it is set up:
  1. Someone changes the preset or a template in my-org/.github and it merges to main.
  2. Each repository runs its smartcloud workflow: on a push to its default branch, on a schedule, or when run by hand.
  3. Every run reads the preset, so a preset change applies straight away.
  4. The sync part of the run renders every template with that repository’s values and compares the result with its files. If anything differs, it opens or updates one pull request, chore(sync): sync files from my-org/.github.
  5. A maintainer reviews and merges it like any other pull request.

Before you start

You need:
  • to be an owner of the organisation, to create the app and organisation secrets;
  • the GitHub CLI signed in (gh auth login), to check things from your machine;
  • Node 24 or later, to run the smartcloud CLI (npx @resnovas/smartcloud).
Decide one thing first: should the hub be public? Public is simpler and is what Resnovas uses. The steps below work either way.

Step 1: create the hub repository

1

Create my-org/.github

Create a repository named exactly .github in your organisation (New repository, owner my-org, name .github). The name is not required by smartcloud, but GitHub gives a public .github repository special jobs, such as default community health files and the organisation profile (profile/README.md), so it is the natural home.
2

Add the folders

Create smartcloud/ for the preset and templates/ for the shared files. You will fill both in the next steps.

Step 2: create a GitHub App for the organisation

The workflow token every Actions run gets cannot read another private repository, cannot push changes to workflow files, and cannot change repository settings. Sync and settings need all three, so the hub setup acts as a GitHub App.
1

Create the app

Follow Create a GitHub App in your organisation’s Settings > Developer settings > GitHub Apps. Name it something like my-org-bot; its commits and pull requests appear as my-org-bot[bot]. It needs Contents, Pull requests and Workflows (read and write) for sync, and Administration (read and write) for settings.
2

Install it on all repositories

On the app’s page, Install App > my-org > All repositories, so new repositories are covered without a second step.
3

Store its credentials once, for the whole organisation

In the organisation’s Settings > Secrets and variables > Actions:
  • Variables tab: SMARTCLOUD_APP_ID, the app’s ID.
  • Secrets tab: SMARTCLOUD_APP_PRIVATE_KEY, the whole .pem file from Generate a private key. Give it access to all repositories.
Every repository’s workflow mints a short-lived token from these. Pull requests from forks and Dependabot get no secrets, so they fall back to the workflow token and run restricted: smartcloud skips what it cannot do and says so in the job summary, rather than failing.

Step 3: write the preset

Create smartcloud/base.yml in the hub. This is the organisation’s shared settings file: everything in it applies to every repository that extends it, and is locked there (Add, never change).
my-org/.github/smartcloud/base.yml
The sync section is what makes this a hub: The Recommended setups page has a fuller preset with commits, reviews and security settings.

Step 4: write the templates

Every file under templates/ is placed at the same path in each repository: templates/SECURITY.md becomes SECURITY.md, and templates/.github/dependabot.yml becomes .github/dependabot.yml. A template can be synced whole or in part.

Placeholders: the parts that differ per repository

Write {{KEY}} wherever a value differs, for example templates/SECURITY.md:
templates/SECURITY.md
Keys are upper case letters, digits and underscores, starting with a letter (ORG_NAME, RESPONSE_DAYS). Anything else in double braces is left alone, so GitHub Actions expressions such as ${{ github.token }} in a workflow template are safe. Where values come from:
  • The preset’s sync.values hold what is the same everywhere. They are locked: a repository cannot change them.
  • A repository’s own sync.values add keys the preset does not set. That is how one repository says RESPONSE_DAYS: '2' while the rest take the default.

Managed blocks: files a repository can add to

A template synced whole replaces the repository’s file every time. That suits LICENSE or SECURITY.md. For files each repository needs to extend, such as dependabot.yml, mark the part the hub owns:
templates/.github/dependabot.yml
  • Lines between house:managed:begin and house:managed:end belong to the hub. Every sync rewrites them.
  • Everything else belongs to the repository, and sync never touches it. house:local marks where its own lines go.
  • The marker names are fixed (they are not changed to your organisation’s name), and they only count on a comment line: # in YAML, TOML and CODEOWNERS, // in JSON with comments, <!-- in Markdown.
The Sync page lists the conflicts smartcloud warns about, such as a local key that redefines a synced one.

The two templates that tie it together

Two templates make the whole thing self-sustaining. The first is each repository’s settings file, whose managed block says “use the hub” and leaves the rest to the repository:
templates/.github/smartcloud.yml
The second is the workflow that runs smartcloud, so a fix to the workflow also reaches every repository:
templates/.github/workflows/smartcloud.yml
The schedule (0 7 * * 1, Mondays at 07:00 UTC) is how often each repository checks for template changes without a push. Pin third-party actions to a full commit SHA in production. Add the rest of your shared files the same way: LICENSE (with {{COPYRIGHT_YEAR}} and {{COPYRIGHT_HOLDER}}), CODE_OF_CONDUCT.md, issue forms under .github/ISSUE_TEMPLATE/, a pull request template. An executable template, such as a script, makes its synced file executable too.

Step 5: let the hub sync itself

The hub is a repository too, and it should follow its own rules. Copy the two templates from step 4 into the hub’s own .github/ folder, exactly as they are, and commit everything to main.
  • The hub extends its own preset. Reading a preset in the same repository always works, whatever the token.
  • Its sync renders templates/ into the hub itself, so its own .github/smartcloud.yml, workflow and LICENSE stay in step with what every other repository receives.
  • The edit check is off in the hub, because changing templates is its job: a pull request that edits templates/ never fails SYNC there.
Run the workflow once by hand (Actions > smartcloud > Run workflow). The job summary lists the labels it created, the settings it applied and, if any rendered file differs from the hub’s own copy, the sync pull request it opened.

Step 6: connect a repository

For each repository that should follow the hub:
1

Add the two files

Copy templates/.github/smartcloud.yml to the repository’s .github/smartcloud.yml, and templates/.github/workflows/smartcloud.yml to .github/workflows/smartcloud.yml. Everything else arrives with the first sync.
2

Add what the preset leaves open

Below the house:local line, add this repository’s own settings: its CI job, environments, extra labels, and any template it keeps its own copy of:
.github/smartcloud.yml
3

Check it from your machine

  • validate reads the preset and prints Built from: my-org/.github/smartcloud/base.yml@main, .github/smartcloud.yml. A value that conflicts with the preset fails here, before it can fail a run.
  • doctor checks the token can read the hub, and that SMARTCLOUD_APP_ID and SMARTCLOUD_APP_PRIVATE_KEY exist for the repository.
  • sync --out writes every file the first sync would propose into /tmp/api-sync, with its status (added, updated, unchanged), and lists conflicts. A missing placeholder value shows up here.
4

Commit and run

Commit the two files on a branch, open a pull request and merge it. The merge is a push to the default branch, which starts the first full run.

Step 7: the first sync pull request

Within a minute of that push, the repository has a pull request:
  • Title: chore(sync): sync files from my-org/.github.
  • From the org/sync branch, by my-org-bot[bot], labelled org-sync by the preset’s labelling rule.
  • Body: every file, marked as added, updated or made executable.
  • Commits are signed by GitHub, because they are made through the API with the app’s token, so they pass a ruleset that requires signed commits.
What happens to files the repository already had:
  • A file synced whole is replaced by the template. Review the diff: if the repository needs its own version, add the path to sync.exclude and the next sync drops it from the pull request.
  • A file with a managed block keeps the repository’s content: the first time, the old content is kept, commented out, at the house:local line, so nothing is lost silently. Move back anything you still need as real lines, and delete the rest.
Review it like any other pull request and merge it. From then on, each sync pull request contains only what changed in the hub since. The same run also applies the preset’s settings and creates its labels; the job summary lists every change.

Step 8: roll out a change to every repository

This is what the hub is for. Say you want every repository’s Dependabot to check npm weekly.
1

Change the hub in a pull request

Edit templates/.github/dependabot.yml (or smartcloud/base.yml, for a rule change) on a branch of my-org/.github, say npm-updates.
2

Preview it against a real repository

Point a local config at your branch and render one repository’s files:
preview.yml
The output lists which files would change in my-org/api, and any conflict with that repository’s own lines. For a preset change, run smartcloud validate in a few repositories with the preset reference in their file pointed at your branch, to catch a repository whose own settings would now conflict with it.
3

Merge it

Merge the pull request in the hub. Because every repository reads @main:
  • a preset change applies on each repository’s next run of any kind;
  • a template change arrives as a sync pull request on each repository’s next push to its default branch, its next scheduled run, or a manual run.
4

Roll it out now instead of waiting for the schedule

Start the workflow in every repository that has it:
Each repository opens or updates its sync pull request. Merge them as they pass.
For a risky change, roll out in stages: keep @main for most repositories, and let a few pilot repositories extend and sync from a branch or tag such as @next. Once they are happy, merge next into main. Pinning a tag (@v3) everywhere instead means nothing changes until you move the tag.

Everyday rules for repositories

  • Add, never change. Below house:local, add new labels and rules and the keys the preset leaves open. A different value for something the preset sets fails the run with cannot change "...": it is set by .... To change a shared value, change the preset.
  • Edit templates in the hub, not their copies. A pull request that edits a synced file, or the managed block of one, fails the smartcloud / sync check with SYNC (a warning for a maintainer). The next sync would put the content back anyway.
  • Opt out of a file with sync.exclude, never by deleting it: deleting a synced file fails the check too.
  • Never push to the sync branch. It is force-updated on every run.

Common problems

The run had only the workflow token. Check the app is installed on the repository, and that SMARTCLOUD_APP_ID and SMARTCLOUD_APP_PRIVATE_KEY are visible to it (organisation secrets can be limited to selected repositories). smartcloud doctor --repo my-org/name names what is missing. Pull requests from forks and Dependabot are always restricted; that is expected.
A template uses a placeholder that neither the preset’s nor the repository’s sync.values sets. Add it to the preset if it is the same everywhere, to the repository if it differs, or write {{SECURITY_EMAIL:-a default}} in the template.
The app lacks the Workflows permission, which sync needs to push .github/workflows/smartcloud.yml. Add it on the app’s page; each organisation owner then accepts the new permission on the installation.
The hub is private and the pull request came from a fork or Dependabot, whose token cannot read it. Make the hub public, or accept that those pull requests are checked without the preset. Keep the repository’s own CI check required in the ruleset next to smartcloud for them.
The repository’s file sets a different value for something the preset sets. Remove it or restate it exactly, or change the preset for everyone. See Add, never change.
The change is inside a file synced whole, or inside a managed block. Move it below the house:local line, or add the file to sync.exclude if the repository should own it outright.
Last modified on September 28, 2026