From 733557c47a43c9f1b4ffde77cd0fdd29d790856b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 30 May 2026 00:50:55 +0200 Subject: [PATCH 1/2] feat: add template-sync reusable workflow Add a `template-sync.yaml` reusable workflow that keeps a repository in sync with an upstream template repository via AndreasAugustin/actions-template-sync (SHA-pinned, v2.5.3), opening a PR with incoming template changes. It mints a GitHub App token by default (use-app-token: true) so the sync PR triggers the caller's CI, defaults the PR title/commit to a Conventional-Commit `chore:` so consumers' squash-merge changelogs stay clean, and reads consumer-owned paths from `.templatesyncignore`. Wire a `[Test] Template Sync - Dry Run` job into ci.yaml and document the workflow in README.md. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yaml | 13 +++ .github/workflows/template-sync.yaml | 117 +++++++++++++++++++++++++++ README.md | 45 +++++++++++ 3 files changed, 175 insertions(+) create mode 100644 .github/workflows/template-sync.yaml 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..8124f1c --- /dev/null +++ b/.github/workflows/template-sync.yaml @@ -0,0 +1,117 @@ +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`). The source + repository must be readable with the token in use — a public template + needs no extra access; a private template requires the App token (see + `use-app-token`) to be installation-scoped to read 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: 🔑 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 }} + # Scope the token to least privilege — exactly what the sync needs + # (push the sync branch + open the PR) — instead of inheriting the + # App installation's blanket permissions. + 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
From 93e629a76422abc4af9afe5feaff6b263db6a323 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 30 May 2026 01:51:37 +0200 Subject: [PATCH 2/2] fix: scope the app token to read the upstream template repo A token from create-github-app-token without owner/repositories is scoped to the caller repo only, so it cannot clone a PRIVATE source-repo-path in a separate template repo (the sync would fail before opening a PR). Scope the token to the caller repo AND the template repo (same owner) so private templates work; public templates are unaffected. Addresses the Copilot review note on the template-sync workflow. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/template-sync.yaml | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml index 8124f1c..811c7e5 100644 --- a/.github/workflows/template-sync.yaml +++ b/.github/workflows/template-sync.yaml @@ -6,10 +6,11 @@ on: 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`). The source - repository must be readable with the token in use — a public template - needs no extra access; a private template requires the App token (see - `use-app-token`) to be installation-scoped to read it. + (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: @@ -81,6 +82,14 @@ jobs: 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 }} @@ -88,9 +97,13 @@ jobs: with: app-id: ${{ vars.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - # Scope the token to least privilege — exactly what the sync needs - # (push the sync branch + open the PR) — instead of inheriting the - # App installation's blanket permissions. + 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