diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 81d883b..3f572ec 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -142,6 +142,17 @@ jobs:
with:
dry-run: true
+ test-template-sync:
+ name: "[Test] Template Sync - Dry Run"
+ uses: ./.github/workflows/template-sync.yaml
+ permissions:
+ contents: write
+ pull-requests: write
+ with:
+ source-repo-path: devantler-tech/gitops-tenant-template
+ use-app-token: false
+ dry-run: true
+
ci-required-checks:
name: "CI - Required Checks"
runs-on: ubuntu-latest
@@ -160,6 +171,7 @@ jobs:
test-scan-for-todo-comments,
test-sync-cluster-policies,
test-update-agent-skills,
+ test-template-sync,
]
permissions: {}
if: ${{ always() }}
@@ -181,3 +193,4 @@ jobs:
${{ needs.test-scan-for-todo-comments.result }}
${{ needs.test-sync-cluster-policies.result }}
${{ needs.test-update-agent-skills.result }}
+ ${{ needs.test-template-sync.result }}
diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml
new file mode 100644
index 0000000..811c7e5
--- /dev/null
+++ b/.github/workflows/template-sync.yaml
@@ -0,0 +1,130 @@
+name: 🔄 Template Sync
+
+on:
+ workflow_call:
+ inputs:
+ source-repo-path:
+ description: >-
+ The `owner/repo` path of the upstream template repository to sync from
+ (passed to `actions-template-sync` as `source_repo_path`). When
+ `use-app-token` is set, the App token is auto-scoped to read this repo
+ as well, so a PRIVATE template under the same owner works out of the
+ box; a cross-owner private template needs a custom token with read
+ access to it.
+ required: true
+ type: string
+ upstream-branch:
+ description: Branch of the template repository to sync from
+ required: false
+ type: string
+ default: main
+ pr-title:
+ description: >-
+ Title of the sync PR. Defaults to a Conventional-Commit `chore:` title
+ because every consumer squash-merges on the PR title into its changelog.
+ required: false
+ type: string
+ default: "chore: sync changes from the upstream template"
+ pr-commit-msg:
+ description: Commit message for the sync PR
+ required: false
+ type: string
+ default: "chore: sync changes from the upstream template"
+ pr-labels:
+ description: Comma-separated labels applied to the sync PR
+ required: false
+ type: string
+ default: dependencies,automation
+ pr-branch-name-prefix:
+ description: Prefix for the branch the sync PR is opened from
+ required: false
+ type: string
+ default: chore/template-sync
+ template-sync-ignore-file-path:
+ description: >-
+ Path to the ignore file listing consumer-owned files that must NOT be
+ overwritten by the template (same format as `.gitignore`).
+ required: false
+ type: string
+ default: .templatesyncignore
+ dry-run:
+ description: "Skip the sync and PR creation (validate the workflow interface only)"
+ required: false
+ default: false
+ type: boolean
+ use-app-token:
+ description: >-
+ When `true` (the default), open the sync PR with a GitHub App token
+ (minted from the `APP_ID` variable and the `APP_PRIVATE_KEY` secret)
+ instead of the default `GITHUB_TOKEN`. A PR opened with `GITHUB_TOKEN`
+ does NOT trigger the caller's `on: pull_request`/`push` CI runs, so its
+ required checks never report and it stays blocked; an App token avoids
+ this. Set to `false` to fall back to `GITHUB_TOKEN`.
+ required: false
+ default: true
+ type: boolean
+ secrets:
+ APP_PRIVATE_KEY:
+ description: >-
+ GitHub App private key, required when `use-app-token` is `true` (the
+ default). Paired with the `APP_ID` repository/organization variable to
+ mint an App token for opening the sync PR.
+ required: false
+
+permissions: {}
+
+jobs:
+ template-sync:
+ name: Template sync
+ if: ${{ !inputs.dry-run }}
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: 🧮 Resolve template repo name
+ id: template
+ if: ${{ inputs.use-app-token }}
+ shell: bash
+ env:
+ SOURCE_REPO_PATH: ${{ inputs.source-repo-path }}
+ run: echo "name=${SOURCE_REPO_PATH##*/}" >> "$GITHUB_OUTPUT"
+
+ - name: 🔑 Generate GitHub App token
+ id: app-token
+ if: ${{ inputs.use-app-token }}
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
+ with:
+ app-id: ${{ vars.APP_ID }}
+ private-key: ${{ secrets.APP_PRIVATE_KEY }}
+ owner: ${{ github.repository_owner }}
+ # Scope the token to exactly the caller repo (push the sync branch +
+ # open the PR) AND the upstream template repo (clone it — required when
+ # the template is PRIVATE, harmless when it is public). Assumes both
+ # live under the same owner; a cross-owner private template needs its
+ # own read-scoped token instead.
+ repositories: ${{ github.event.repository.name }},${{ steps.template.outputs.name }}
+ permission-contents: write
+ permission-pull-requests: write
+
+ - name: 📑 Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ # actions-template-sync needs full history to compute and merge the
+ # template diff. It handles its own git auth via `github_token`, so the
+ # checkout does not need to persist credentials.
+ fetch-depth: 0
+ persist-credentials: false
+ token: ${{ steps.app-token.outputs.token || github.token }}
+
+ - name: 🔄 Sync from template
+ uses: AndreasAugustin/actions-template-sync@8a0f668b83c32a0f673353086d74f12b8853d4f5 # v2.5.3
+ with:
+ source_repo_path: ${{ inputs.source-repo-path }}
+ upstream_branch: ${{ inputs.upstream-branch }}
+ github_token: ${{ steps.app-token.outputs.token || github.token }}
+ pr_title: ${{ inputs.pr-title }}
+ pr_commit_msg: ${{ inputs.pr-commit-msg }}
+ pr_labels: ${{ inputs.pr-labels }}
+ pr_branch_name_prefix: ${{ inputs.pr-branch-name-prefix }}
+ template_sync_ignore_file_path: ${{ inputs.template-sync-ignore-file-path }}
diff --git a/README.md b/README.md
index d577831..6b5a208 100644
--- a/README.md
+++ b/README.md
@@ -322,6 +322,51 @@ jobs:
+### 🔄 Template Sync
+
+
+Click to expand
+
+[.github/workflows/template-sync.yaml](.github/workflows/template-sync.yaml) keeps a repository in sync with an upstream template repository via [AndreasAugustin/actions-template-sync](https://github.com/AndreasAugustin/actions-template-sync), opening a PR with any incoming template changes. List the files this repository *owns* (and that must never be overwritten by the template) in a `.templatesyncignore` file at the repo root — everything else the template ships is kept in sync.
+
+#### Usage
+
+```yaml
+on:
+ schedule:
+ - cron: "0 6 * * 1"
+ workflow_dispatch:
+
+jobs:
+ template-sync:
+ uses: devantler-tech/reusable-workflows/.github/workflows/template-sync.yaml@{ref} # ref
+ with:
+ source-repo-path: devantler-tech/gitops-tenant-template
+ secrets:
+ APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
+```
+
+By default the sync PR is opened with a GitHub App token (`use-app-token: true`) so it triggers the caller's CI; this needs the `APP_ID` variable and the `APP_PRIVATE_KEY` secret. Set `use-app-token: false` to fall back to `GITHUB_TOKEN` (the PR then will not trigger `on: pull_request` checks).
+
+#### Secrets and Inputs
+
+| Key | Type | Default | Required | Description |
+|----------------------------------|-----------------|--------------------------------------------------|----------|-----------------------------------------------------------------------------|
+| `APP_PRIVATE_KEY` | Secret | - | When `use-app-token` | GitHub App private key (paired with the `APP_ID` variable) |
+| `source-repo-path` | Input (string) | - | Yes | `owner/repo` of the upstream template to sync from |
+| `upstream-branch` | Input (string) | `main` | No | Branch of the template repository to sync from |
+| `pr-title` | Input (string) | `chore: sync changes from the upstream template` | No | Title of the sync PR (Conventional-Commit by default) |
+| `pr-commit-msg` | Input (string) | `chore: sync changes from the upstream template` | No | Commit message for the sync PR |
+| `pr-labels` | Input (string) | `dependencies,automation` | No | Comma-separated labels for the sync PR |
+| `pr-branch-name-prefix` | Input (string) | `chore/template-sync` | No | Prefix for the branch the sync PR is opened from |
+| `template-sync-ignore-file-path` | Input (string) | `.templatesyncignore` | No | Path to the file listing consumer-owned (non-synced) files |
+| `use-app-token` | Input (boolean) | `true` | No | Open the sync PR with a GitHub App token so it triggers the caller's CI |
+| `dry-run` | Input (boolean) | `false` | No | Skip the sync and PR creation (validate workflow interface only) |
+
+> **Note:** The calling workflow runs the sync job with `contents: write` and `pull-requests: write` (declared by the reusable workflow).
+
+
+
### 🔄 Update Agent Skills