diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index b55dcfb..4dc576c 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -43,4 +43,8 @@ jobs: global-json-file: global.json - name: 📦 Restore dependencies - run: dotnet restore Example.slnx + # Bare `dotnet restore` resolves the repo root's single solution file, + # so this workflow stays template-owned and placeholder-independent: it + # keeps working after an instance renames Example.slnx, and template + # syncs can keep propagating hardening/version bumps to it. + run: dotnet restore diff --git a/.github/workflows/template-sync.yaml b/.github/workflows/template-sync.yaml new file mode 100644 index 0000000..a7ff858 --- /dev/null +++ b/.github/workflows/template-sync.yaml @@ -0,0 +1,36 @@ +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. Outside devantler-tech the org-provided App credentials are + # absent and the scheduled run would only fail — so personal-account + # instances are off by default and opt in by supplying their OWN GitHub App + # (one that can open PRs): the APP_PRIVATE_KEY secret plus the APP_CLIENT_ID + # repository variable (the reusable workflow mints its token from that + # variable/secret pair), and setting the repository variable + # TEMPLATE_SYNC_ENABLED=true. + if: >- + github.repository != 'devantler-tech/dotnet-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 + # 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/dotnet-template + secrets: + APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }} diff --git a/.templatesyncignore b/.templatesyncignore new file mode 100644 index 0000000..0fd76bf --- /dev/null +++ b/.templatesyncignore @@ -0,0 +1,66 @@ +# .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/dotnet-template, never by editing it in an instance. +# Template-owned plumbing includes the +# .github/workflows/{ci,publish,release,template-sync,todos,copilot-setup-steps} +# workflows, the shared configs (.editorconfig, .gitattributes, +# .pre-commit-config.yaml), and the CLAUDE.md/GEMINI.md shims. (ci.yaml is +# deliberately template-owned for devantler-tech instances — they all share the +# same ruleset-injected build/test workflows and this required-checks +# aggregation, so CI fixes must propagate. An instance OUTSIDE devantler-tech +# does not inherit those org rulesets and must replace ci.yaml with real +# build/test jobs — after doing so, add `.github/workflows/ci.yaml` to this +# file, which the instance owns, so a later sync cannot revert it.) + +# --- Identity & docs (instance-specific) --- +AGENTS.md +README.md +LICENSE +.github/CODEOWNERS +.claude/skills/maintain/SKILL.md + +# --- Release / dependency config the instance tailors --- +.releaserc +.gitignore +.github/dependabot.yaml + +# --- Toolchain floor (instance-owned so SDK and TFMs move in lockstep) --- +# AGENTS.md's toolchain-floor policy requires global.json's SDK floor and every +# to move together. The project trees below are ignored, so a +# synced global.json bump would move the SDK floor WITHOUT its matching TFMs — +# a lockstep violation delivered downstream. The instance owns the whole +# toolchain upgrade instead (the .NET parity to go-template, where the +# instance-owned go.mod carries the Go toolchain floor). +global.json + +# --- The instance's own solution and code --- +# The solution, projects, and code are renamed from Example.* by +# scripts/rename-placeholders.sh at scaffold time and evolve with the instance; +# a sync must never clobber them — nor re-introduce the template's Example.slnx +# after the rename. src/ and tests/ are ignored WHOLESALE so a template file +# added later at a path the instance already uses can never touch instance code. +Example.slnx +src/ +tests/ + +# --- 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 7b159c5..23c244e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ - `AGENTS.md` — the canonical, cross-tool project instructions. - `CLAUDE.md` / `GEMINI.md` — exact one-line `@AGENTS.md` shims; never copy guidance into them. - `.editorconfig` — formatting and analyzer rules enforced at build. -- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `validate-scaffold.yaml` (template-repo-only gate that exercises the agent shims and scaffold-rename script — no-ops downstream), `publish.yaml` (publishes the NuGet library on `v*` tags via the reusable `publish-dotnet-library` workflow), `release.yaml`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. +- `.github/workflows/` — `ci.yaml` (required-checks aggregation on PRs/merge queue), `validate-scaffold.yaml` (template-repo-only gate that exercises the agent shims and scaffold-rename script — no-ops downstream), `publish.yaml` (publishes the NuGet library on `v*` tags via the reusable `publish-dotnet-library` workflow), `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`, `sync-labels.yaml`, `todos.yaml`, and `copilot-setup-steps.yml`. - `.pre-commit-config.yaml` — a [pre-commit](https://pre-commit.com) config with a local `dotnet-format` hook that runs `dotnet format` on staged C# changes (opt in with `pre-commit install`); `.releaserc` — semantic-release configuration. - `scripts/validate-agent-shims.test.sh` — hermetic structural check that both tool-specific shims contain exactly `@AGENTS.md` plus one newline. Run with `sh scripts/validate-agent-shims.test.sh`; CI runs it via `validate-scaffold.yaml`. diff --git a/README.md b/README.md index 3cb8709..e24cb7f 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,39 @@ dotnet test Run the script with no argument to derive a PascalCase name from your `origin` GitHub remote. It leaves the **Use this template** links above and the maintenance docs untouched; review the result with `git diff`. (Prefer to rename by hand? Repoint `Example` across the `.slnx`, `src/`, `tests/`, and README references yourself.) +## 🔄 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/publish/release workflows, the shared configs + (`.editorconfig`, `.gitattributes`, `.pre-commit-config.yaml`), 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 solution and code (the `.slnx`, all of `src/` + and `tests/`), the toolchain floor (`global.json` — the SDK floor and every + `` must move in lockstep, so the whole upgrade is yours), + identity and docs (`README.md`, `AGENTS.md`, `LICENSE`, `CODEOWNERS`), and + the configs you tailor (`.releaserc`, `.gitignore`, `dependabot.yaml`). +- **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. In devantler-tech +instances it works out of the box (the org provides the App credentials); an +instance elsewhere is off by default — opt in by supplying your own GitHub App +(one allowed to open PRs in your repository): add its private key as the +`APP_PRIVATE_KEY` secret, its client ID as the `APP_CLIENT_ID` repository +variable, and set the repository variable `TEMPLATE_SYNC_ENABLED=true`. Note +that outside devantler-tech the synced `ci.yaml` is only an empty required-check +aggregator (the real build/test workflows are injected by devantler-tech org +rulesets): replace it with your own CI and add `.github/workflows/ci.yaml` to +your `.templatesyncignore` so later syncs preserve your version. + ## 📝 Usage ### Add a project to the solution