From 17139005accca84eeac597ed893ce5edb5c1f19e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 17 Jul 2026 08:34:58 +0200 Subject: [PATCH 1/3] feat: adopt weekly template-sync with the tenant-template ownership model Fixes #176 Co-Authored-By: Claude Fable 5 --- .github/workflows/template-sync.yaml | 27 ++++++++++++++ .templatesyncignore | 56 ++++++++++++++++++++++++++++ AGENTS.md | 2 +- README.md | 25 +++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/template-sync.yaml create mode 100644 .templatesyncignore diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml new file mode 100644 index 0000000..90c8ba2 --- /dev/null +++ b/.github/workflows/template-sync.yaml @@ -0,0 +1,27 @@ +name: 🔄 Template Sync + +on: + schedule: + - cron: "0 6 * * 1" # weekly, Monday 06:00 UTC + workflow_dispatch: + +permissions: {} + +jobs: + template-sync: + # Skipped in the template repo itself (it is the sync source); runs in every + # instance created from this template, where it opens a PR with template changes. + if: github.repository != 'devantler-tech/go-template' + # The reusable workflow's job opens the sync PR, so the caller must grant it + # write to contents + pull-requests. A reusable workflow cannot be granted + # more than the caller, and the top-level `permissions: {}` grants none, so + # without this the run fails at startup (reusable-workflow permission check) + # — both here and in every instance where the job actually runs. + permissions: + contents: write + pull-requests: write + uses: devantler-tech/actions/.github/workflows/template-sync.yaml@0f27c13a9785608e0bbf9998777d27761b4de617 # v10.0.2 + with: + source-repo-path: devantler-tech/go-template + secrets: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.templatesyncignore b/.templatesyncignore new file mode 100644 index 0000000..7df4a76 --- /dev/null +++ b/.templatesyncignore @@ -0,0 +1,56 @@ +# .templatesyncignore — files an INSTANCE created from this template OWNS. +# +# Same syntax as .gitignore. The weekly template-sync workflow +# (devantler-tech/actions .github/workflows/template-sync.yaml, using +# AndreasAugustin/actions-template-sync) is skipped in this template repo itself +# and runs in every instance created from it — opening a PR that brings template +# changes downstream. It only ever touches files that exist in this template, and +# it SKIPS anything matched here, so an instance's tailored files are never +# clobbered by an upstream sync. +# +# Everything NOT listed here is TEMPLATE-OWNED shared plumbing kept current in +# every instance by template-sync — change it upstream in +# devantler-tech/go-template, never by editing it in an instance. Template-owned +# plumbing includes .github/workflows/{ci,cd,release,template-sync,todos}.yaml, +# the lint configs (.golangci.yml, .mega-linter.yml, .pre-commit-config.yaml, +# .editorconfig, .gitattributes), the mockery pre-commit hook scripts, and the +# CLAUDE.md/GEMINI.md shims. (ci.yaml is deliberately template-owned here — +# unlike the stack-neutral gitops-tenant-template, every Go instance shares the +# same required-checks aggregation, so CI fixes must propagate.) + +# --- Identity & docs (instance-specific) --- +AGENTS.md +README.md +LICENSE +.github/CODEOWNERS +.claude/skills/maintain/SKILL.md + +# --- Release / dependency / lint-dictionary config the instance tailors --- +.releaserc +.gitignore +.github/dependabot.yaml +cspell.json + +# --- The instance's own Go module and code --- +# The module path, entrypoint, and example packages are repointed by +# scripts/rename-placeholders.sh at scaffold time and evolve with the instance; +# a sync must never clobber them. +go.mod +go.sum +main.go +cmd/ +internal/ +pkg/example/ +pkg/featureflag/ + +# --- Scaffolding machinery (template-only; dead code in a live instance) --- +# These run when a NEW repo is created from this template ("use this template" +# copies everything, so scaffolding still arrives at creation time). A +# scaffolded instance never runs them again — ignoring them stops template-sync +# (re-)introducing them downstream, so an instance that deletes them stays clean. +scripts/rename-placeholders.sh +scripts/rename-placeholders.test.sh +.github/workflows/validate-scaffold.yaml + +# --- This ignore-list itself (the instance curates its own ownership) --- +.templatesyncignore diff --git a/AGENTS.md b/AGENTS.md index 2eac06f..3e67650 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ need. - `AGENTS.md` — the canonical, cross-tool project instructions. - `CLAUDE.md` / `GEMINI.md` — exact one-line `@AGENTS.md` shims; never copy guidance into them. - `.golangci.yml` — golangci-lint v2 config (formatters + `default: all` linters, with a few opt-outs and mock-file exclusions). -- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the agent shims, onboarding script, and pre-commit mockery hook — no-ops downstream), `release.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. +- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `cd.yaml` (GoReleaser release on `v*` tags), `validate-scaffold.yaml` (template-repo-only gate that exercises the agent shims, onboarding script, and pre-commit mockery hook — no-ops downstream), `template-sync.yaml` (skipped in this repo; in instances it opens a weekly PR syncing template-owned plumbing, honouring `.templatesyncignore` — see the README's *Staying current* ownership classes), `release.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. - `.pre-commit-config.yaml` — local pre-commit hooks: `golangci-lint-fmt` (Go formatting) and mock generation (`mockery`, via `.github/scripts/run-mockery.sh`). - `.github/scripts/run-mockery.sh` — the pre-commit mockery hook's entry point; a guarded no-op until the project adds a `.mockery.yml`/`.mockery.yaml`, then runs `mockery` (so a fresh clone's hook stays green while the generation step is already wired). - `.github/scripts/run-mockery.test.sh` — hermetic test for the mockery hook: runs `run-mockery.sh` under a stripped PATH and asserts the three branches (silent no-op without a config, exit 1 + install hint when mockery is absent, exec when present). Run with `sh .github/scripts/run-mockery.test.sh`; CI runs it via `validate-scaffold.yaml`. diff --git a/README.md b/README.md index cd7f814..d72fc84 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,31 @@ script leaves the upstream **Use this template** links above untouched, runs `go mod tidy`, and you can review the result with `git diff`. (Prefer to do it by hand? `go mod edit -module github.com//my-project && go mod tidy`.) +## 🔄 Staying current + +A weekly **template-sync** workflow opens a PR in your repository whenever this +template's shared plumbing changes, so instances never drift from the +portfolio's CI/lint/agent-file conventions. It never touches your code: every +file falls into one of three ownership classes: + +- **Template-owned plumbing** — synced downstream by the weekly PR: the + `.github/workflows/` CI/CD/release workflows, the lint configs + (`.golangci.yml`, `.mega-linter.yml`, `.pre-commit-config.yaml`, + `.editorconfig`, `.gitattributes`), and the `CLAUDE.md`/`GEMINI.md` shims. + Change these upstream in the template, never by hand in an instance. +- **Instance-owned** — listed in [`.templatesyncignore`](.templatesyncignore), + never touched by a sync: your Go module and code (`go.mod`, `go.sum`, + `main.go`, `cmd/`, `internal/`, the example packages), identity and docs + (`README.md`, `AGENTS.md`, `LICENSE`, `CODEOWNERS`), and the configs you + tailor (`.releaserc`, `.gitignore`, `dependabot.yaml`, `cspell.json`). +- **Scaffold-time-only** — the rename script and the template's own + `validate-scaffold.yaml` gate arrive when the repo is created and are ignored + by sync afterwards, so you can delete them and they stay gone. + +The sync workflow no-ops in this template repository itself and needs the +org-provided `APP_PRIVATE_KEY` secret in instances (present by default on +devantler-tech repositories). + ## 📝 Usage ### Add a dependency From 2b8743f00acfd53f2760f10f4ceba9240a4177be Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 17 Jul 2026 08:47:32 +0200 Subject: [PATCH 2/3] fix(sync): guard credential-less instances; widen instance-owned classes (codex review) Co-Authored-By: Claude Fable 5 --- .github/workflows/template-sync.yaml | 10 ++++++++-- .templatesyncignore | 15 ++++++++++----- README.md | 19 +++++++++++-------- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml index 90c8ba2..6bd093f 100644 --- a/.github/workflows/template-sync.yaml +++ b/.github/workflows/template-sync.yaml @@ -10,8 +10,14 @@ permissions: {} jobs: template-sync: # Skipped in the template repo itself (it is the sync source); runs in every - # instance created from this template, where it opens a PR with template changes. - if: github.repository != 'devantler-tech/go-template' + # instance created from this template, where it opens a PR with template + # changes. Outside devantler-tech the org-provided APP_PRIVATE_KEY secret is + # absent and the scheduled run would only fail — so personal-account + # instances are off by default and opt in by adding the secret plus a + # repository variable TEMPLATE_SYNC_ENABLED=true. + if: >- + github.repository != 'devantler-tech/go-template' && + (github.repository_owner == 'devantler-tech' || vars.TEMPLATE_SYNC_ENABLED == 'true') # The reusable workflow's job opens the sync PR, so the caller must grant it # write to contents + pull-requests. A reusable workflow cannot be granted # more than the caller, and the top-level `permissions: {}` grants none, so diff --git a/.templatesyncignore b/.templatesyncignore index 7df4a76..81c2a5a 100644 --- a/.templatesyncignore +++ b/.templatesyncignore @@ -25,23 +25,28 @@ LICENSE .github/CODEOWNERS .claude/skills/maintain/SKILL.md -# --- Release / dependency / lint-dictionary config the instance tailors --- +# --- Release / dependency / lint config the instance tailors --- +# .golangci.yml is instance-owned on purpose: its depguard rule is a strict +# allowlist (stdlib + OpenFeature), so any real project must extend it — a +# synced overwrite would clobber the project's dependency allowlist. .releaserc .gitignore .github/dependabot.yaml +.golangci.yml cspell.json # --- The instance's own Go module and code --- -# The module path, entrypoint, and example packages are repointed by +# The module path, entrypoint, and packages are repointed by # scripts/rename-placeholders.sh at scaffold time and evolve with the instance; -# a sync must never clobber them. +# a sync must never clobber them. pkg/ is ignored WHOLESALE (not just the +# starter packages) so a template file added later at a path the instance +# already uses can never touch instance code. go.mod go.sum main.go cmd/ internal/ -pkg/example/ -pkg/featureflag/ +pkg/ # --- Scaffolding machinery (template-only; dead code in a live instance) --- # These run when a NEW repo is created from this template ("use this template" diff --git a/README.md b/README.md index d72fc84..d719635 100644 --- a/README.md +++ b/README.md @@ -49,22 +49,25 @@ portfolio's CI/lint/agent-file conventions. It never touches your code: every file falls into one of three ownership classes: - **Template-owned plumbing** — synced downstream by the weekly PR: the - `.github/workflows/` CI/CD/release workflows, the lint configs - (`.golangci.yml`, `.mega-linter.yml`, `.pre-commit-config.yaml`, - `.editorconfig`, `.gitattributes`), and the `CLAUDE.md`/`GEMINI.md` shims. + `.github/workflows/` CI/CD/release workflows, the shared lint configs + (`.mega-linter.yml`, `.pre-commit-config.yaml`, `.editorconfig`, + `.gitattributes`), and the `CLAUDE.md`/`GEMINI.md` shims. Change these upstream in the template, never by hand in an instance. - **Instance-owned** — listed in [`.templatesyncignore`](.templatesyncignore), never touched by a sync: your Go module and code (`go.mod`, `go.sum`, - `main.go`, `cmd/`, `internal/`, the example packages), identity and docs + `main.go`, `cmd/`, `internal/`, all of `pkg/`), identity and docs (`README.md`, `AGENTS.md`, `LICENSE`, `CODEOWNERS`), and the configs you - tailor (`.releaserc`, `.gitignore`, `dependabot.yaml`, `cspell.json`). + tailor (`.releaserc`, `.gitignore`, `dependabot.yaml`, `.golangci.yml` — + its depguard allowlist grows with your dependencies — and `cspell.json`). - **Scaffold-time-only** — the rename script and the template's own `validate-scaffold.yaml` gate arrive when the repo is created and are ignored by sync afterwards, so you can delete them and they stay gone. -The sync workflow no-ops in this template repository itself and needs the -org-provided `APP_PRIVATE_KEY` secret in instances (present by default on -devantler-tech repositories). +The sync workflow no-ops in this template repository itself. In devantler-tech +instances it works out of the box (the org provides the `APP_PRIVATE_KEY` +secret); an instance elsewhere is off by default — opt in by adding an +`APP_PRIVATE_KEY` secret (a GitHub App key that can open PRs) and setting the +repository variable `TEMPLATE_SYNC_ENABLED=true`. ## 📝 Usage From ef5131fc6b3a0f9c9fc18c5e8443c96c944ef669 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Fri, 17 Jul 2026 08:56:10 +0200 Subject: [PATCH 3/3] fix(sync): header comment no longer claims .golangci.yml is template-owned (codex round 2) Co-Authored-By: Claude Fable 5 --- .templatesyncignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.templatesyncignore b/.templatesyncignore index 81c2a5a..3d3f3e1 100644 --- a/.templatesyncignore +++ b/.templatesyncignore @@ -12,8 +12,9 @@ # every instance by template-sync — change it upstream in # devantler-tech/go-template, never by editing it in an instance. Template-owned # plumbing includes .github/workflows/{ci,cd,release,template-sync,todos}.yaml, -# the lint configs (.golangci.yml, .mega-linter.yml, .pre-commit-config.yaml, -# .editorconfig, .gitattributes), the mockery pre-commit hook scripts, and the +# the shared lint configs (.mega-linter.yml, .pre-commit-config.yaml, +# .editorconfig, .gitattributes — NOT .golangci.yml, which instances own; see +# below), the mockery pre-commit hook scripts, and the # CLAUDE.md/GEMINI.md shims. (ci.yaml is deliberately template-owned here — # unlike the stack-neutral gitops-tenant-template, every Go instance shares the # same required-checks aggregation, so CI fixes must propagate.)