Skip to content

Delivery Method

via controls how file changes are delivered to the target repository.

Commits directly to the default branch. All file changes are bundled into a single atomic commit.

spec:
via: push
commit_message: "ci: sync shared files" # optional, auto-generated if omitted

Use this for low-risk files (LICENSE, CODEOWNERS, security policies) or routine syncs of already-reviewed templates.

Creates a branch, commits all files, and opens a pull request for review before merging.

spec:
via: pull_request
commit_message: "ci: sync shared files" # optional, auto-generated if omitted
branch: gh-infra/sync-shared # optional, see below
pr_title: "Sync shared files" # optional, defaults to commit_message
pr_body: | # optional, supports Markdown
Automated file sync by gh-infra.
Updates CI workflows and shared config.

Use this when changes need review — for example, CI workflows or Dockerfiles that could break builds.

If a pull request already exists for the branch, gh-infra updates it instead of creating a new one.

Without branch, the branch is gh-infra/sync-<owner>-<name>: <name> is the repository for File and metadata.name for FileSet. An unnamed FileSet uses its sorted repository names joined by +, e.g. gh-infra/sync-babarot-gomi+gh-infra.

Each apply resets the branch to the default branch before committing, so two pull_request resources that would use the same branch on the same repository are rejected by validate, plan, and apply. This happens when both set the same branch, or neither sets one and they share a name: two File manifests for one repository, or unnamed FileSets that list the same repositories. Set a distinct branch, or a distinct metadata.name on a FileSet.

Field Used by Default Description
commit_message both auto Commit message for the sync commit
branch pull_request gh-infra/sync-<owner>-<name> Branch name for the pull request (see Pull request branch)
pr_title pull_request value of commit_message Pull request title
pr_body pull_request auto Pull request body (supports Markdown)

Set executable on a file entry to control its executable bit, for example for git hooks and scripts:

spec:
files:
- path: .githooks/pre-commit
source: ./templates/pre-commit
executable: true
Value Result
true The file is committed with mode 100755
false The file is committed with mode 100644, clearing an existing executable bit
(omitted) An existing file keeps its current mode; a new file is created as 100644

plan compares the mode as well as the content, so adding executable: true to a file whose content is already in sync shows a mode change (mode 100644 → 100755). With reconcile: create_only, the mode is applied only when the file is created.

Only regular files can be marked: a symlink or submodule at the path is reported as an error. Executable files cannot be created in an empty repository, because the Contents API used there cannot set a mode; create the first commit without them.

commit_message, pr_title, and pr_body accept <% %> expressions, rendered per target repository at apply time. plan and validate also render them, so a template error is reported before anything is written:

Variable Value
<% .Repo.Name %>, <% .Repo.Owner %>, <% .Repo.FullName %> Target repository, as in Templating
<% .Source.URL %> Value of the GH_INFRA_SOURCE_URL environment variable, or empty if unset

.Vars is not available in messages. A multi-line commit_message is split Git-style: the first line becomes the commit headline and the rest becomes the body. When pr_title is omitted, only the headline is used as the title.

Link each sync commit back to the change that triggered it by setting GH_INFRA_SOURCE_URL in CI, for example to ${{ github.event.head_commit.url }} in a workflow that applies on push (see Apply on merge). plan and validate render the messages with an empty URL to check them. Wrap the line in if so nothing is emitted when the variable is unset:

spec:
commit_message: |-
ci: sync CI workflow for <% .Repo.Name %>
<% if .Source.URL %>Source: <% .Source.URL %><% end %>

For repositories with no commits yet, gh-infra falls back to the Contents API. Each file becomes a separate commit. With via: pull_request there is no branch to open a pull request from, so the files are pushed to the default branch; plan shows via push for that repository, including one the same apply creates. See GraphQL Commit API vs Contents API for details.