From 9782376cf9fae8496bd14371b878babb863d9ac4 Mon Sep 17 00:00:00 2001 From: Elisa Date: Mon, 21 Sep 2026 15:29:21 +0200 Subject: [PATCH 1/3] feat: moved common linting configs and setups to dev-kit --- .editorconfig | 32 +++++ .github/actions/setup-nix/action.yml | 3 +- .github/pull_request_template.md | 6 + .github/workflows/lint.yml | 32 +++++ .github/workflows/release-drafter.yaml | 6 +- .github/workflows/renovate-auto-approve.yml | 4 +- .golangci.yml | 105 ++++++++++++++++ Makefile | 11 ++ README.md | 128 +++++++++++++------- common.mk | 48 ++++++++ docs/CONTRIBUTING.md | 18 +-- docs/NEW_REPO.md | 22 ++-- example/Makefile | 4 - flake.nix | 4 + scripts/envtest-sideload.sh | 49 ++++---- scripts/repo-settings.sh | 18 +-- treefmt.toml | 61 ++++++++++ 17 files changed, 443 insertions(+), 108 deletions(-) create mode 100644 .editorconfig create mode 100644 .github/workflows/lint.yml create mode 100644 .golangci.yml create mode 100644 Makefile create mode 100644 treefmt.toml diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5ebbb8f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,32 @@ +# Shared editor defaults for ODC repositories. +# Bootstrapped from dev-kit by common.mk. Commit a local .editorconfig file to override it. +# +# Kept in sync with treefmt.toml on purpose: shfmt reads this file for shell +# style, so the editor and `make fmt` cannot disagree. + +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.go] +indent_style = tab +indent_size = 4 + +[{Makefile,*.mk}] +indent_style = tab + +[*.md] +trim_trailing_whitespace = false + +[*.{sh,bash}] +switch_case_indent = true +space_redirects = true + +[*.{json,yml,yaml,toml}] +indent_size = 2 diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml index dc759fe..caa1515 100644 --- a/.github/actions/setup-nix/action.yml +++ b/.github/actions/setup-nix/action.yml @@ -7,8 +7,7 @@ inputs: default: opendefensecloud cachix-auth-token: description: >- - Cachix auth token. Leave empty to fall back to a read-only cache, which - is what happens on fork pull requests, where secrets are unavailable. + Cachix auth token. Leave empty to fall back to a read-only cache, which is what happens on fork pull requests, where secrets are unavailable. required: false default: '' cachix-signing-key: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 67577fc..25f634a 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,18 +1,24 @@ ## What + + Closes # ## Why + ## Testing + ## Notes for reviewers + ## Checklist + - [ ] Tests added/updated - [ ] No breaking changes (or upgrade path documented above) - [ ] Readable commit history (squashed and cleaned up as desired) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..92bc1cb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,32 @@ +name: Lint + +permissions: + contents: read + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + lint: + runs-on: ubuntu-24.04 + defaults: + run: + shell: nix develop --command bash -e {0} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + - name: Set up nix with the shared Cachix cache + uses: ./.github/actions/setup-nix + with: + cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }} + cachix-signing-key: ${{ secrets.CACHIX_SIGNING_KEY }} + - name: fmt + run: make fmt + - name: verify formatting + uses: ./.github/actions/diff-check + - name: lint + run: make lint diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 922e5f3..9814ad8 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -10,9 +10,9 @@ on: pull_request: # Only following types are handled by the action, but one can default to all as well types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks - # pull_request_target: - # types: [opened, reopened, synchronize] + # pull_request_target event is required for autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] permissions: contents: read diff --git a/.github/workflows/renovate-auto-approve.yml b/.github/workflows/renovate-auto-approve.yml index 9b50826..49b4fcd 100644 --- a/.github/workflows/renovate-auto-approve.yml +++ b/.github/workflows/renovate-auto-approve.yml @@ -43,9 +43,7 @@ jobs: auto-approve: runs-on: ubuntu-latest if: >- - github.event_name == 'pull_request' && - github.event.pull_request.user.login == inputs.renovate-actor && - (contains(github.event.pull_request.labels.*.name, inputs.automerge-label) || + github.event_name == 'pull_request' && github.event.pull_request.user.login == inputs.renovate-actor && (contains(github.event.pull_request.labels.*.name, inputs.automerge-label) || contains(github.event.pull_request.labels.*.name, inputs.block-label)) steps: - name: Approve, or revoke a prior approval if the block label appears diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ee07b20 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,105 @@ +# Baseline golangci-lint configuration for ODC repositories. +# Bootstrapped from dev-kit by common.mk; commit a local .golangci.yml file to override it. +version: "2" +linters: + default: none + settings: + dogsled: + max-blank-identifiers: 4 + exhaustive: + default-signifies-exhaustive: true + godot: + scope: declarations + capital: false + gosec: + excludes: + - G101 # Look for hardcoded credentials + - G204 # Audit use of command execution + - G306 # Poor file permissions used when writing to a file + modernize: + disable: + - omitzero + nlreturn: + block-size: 2 + staticcheck: + checks: ["all", "-ST1000", "-ST1001", "-ST1003", "-ST1005", "-ST1012", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-QF1001", "-QF1003", "-QF1008"] + exclusions: + generated: lax + presets: [comments, common-false-positives, legacy, std-error-handling] + paths: [third_party, builtin$, examples$] + warn-unused: true + disable: + - nilnil + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - contextcheck + - copyloopvar + - decorder + - dogsled + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + - exhaustive + - exptostd + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - godoclint + - godot + - goheader + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - intrange + - loggercheck + - makezero + - mirror + - misspell + - modernize + - musttag + - nakedret + - nilerr + - nilnil + - nlreturn + - noctx + - nosprintfhostport + - predeclared + - promlinter + - protogetter + - reassign + - sloglint + - staticcheck + - testableexamples + - unconvert + - unparam + - unused + - usestdlibvars + - usetesting + - wastedassign +formatters: + enable: [gci, gofmt] + settings: + gci: + sections: + - standard # Standard section: captures all standard packages. + - default # Default section: contains all imports that could not be matched to another section type. + - localmodule # Local module section: contains all local packages. This section is not present unless explicitly enabled. + - blank # Blank section: contains all blank imports. This section is not present unless explicitly enabled. + - dot # Dot section: contains all dot imports. This section is not present unless explicitly enabled. + custom-order: true + exclusions: + generated: lax + paths: [third_party, builtin$, examples$] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6958b1a --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +DEV_KIT_VERSION := main +DEV_KIT_NO_SELF_UPDATE := 1 + +# The shared configs live in this repository; there is nothing to bootstrap. +DEV_KIT_CONFIGS := + +include common.mk + +# No Go code here, so golangci-lint is not installed and treefmt must not +# insist on it. Every other repository leaves TREEFMT_ARGS empty. +TREEFMT_ARGS := --allow-missing-formatter diff --git a/README.md b/README.md index ac83545..169fa0b 100644 --- a/README.md +++ b/README.md @@ -17,27 +17,67 @@ Copy the files from `example/` into your project and adjust them for your needs. The included `common.mk` provides: -| Target | Description | -| --- | --- | -| `help` | Display all available targets | -| `clean` | Remove the `bin/` directory | -| `mod` | Run `go mod tidy`, `download`, and `verify` | -| `golangci-lint` | Run golangci-lint | -| `shellcheck` | Run shellcheck on shell scripts | -| `scan` | Scan for vulnerabilities using osv-scanner | -| `setup-local-cluster` | Create a Kind cluster for local development | -| `repo-settings` | Reconcile GitHub repository settings | +| Target | Description | +| --- | --- | +| `help` | Display all available targets | +| `clean` | Remove the `bin/` directory | +| `fmt` | Format every file in the repository | +| `lint` | Check formatting, then run shellcheck and, when the repository has a `go.mod`, golangci-lint | +| `fmt-all` | Formatting only (what `fmt` calls) | +| `lint-all` | Fail if any file is not formatted (what `lint` calls) | +| `mod` | Run `go mod tidy`, `download`, and `verify` | +| `golangci-lint` | Run golangci-lint | +| `shellcheck` | Run shellcheck on shell scripts | +| `scan` | Scan for vulnerabilities using osv-scanner | +| `setup-local-cluster` | Create a Kind cluster for local development | +| `repo-settings` | Reconcile GitHub repository settings | | `envtest-binaries-sideload` | Populate the envtest cache from upstream K8s/etcd releases | +### Formatting and linting + +One formatter front-end for the whole repository. `treefmt` drives gofmt (via +`golangci-lint fmt`, so import order stays configured in one place), `yamlfmt`, +`shfmt` and `mdformat`. + +```sh +make fmt # writes +make lint # fails on drift, then shellcheck and golangci-lint (Go repos) +``` + +The Go steps are conditional on a `go.mod` in the repository root: without one, +`lint` skips golangci-lint and `fmt` does not install it. Repositories with no +Go code also set `TREEFMT_ARGS := --allow-missing-formatter` so treefmt does not +insist on a formatter it will never call, and narrow `DEV_KIT_CONFIGS` to drop +`.golangci.yml`. + +`treefmt.toml`, `.golangci.yml` and `.editorconfig` are fetched from dev-kit at +the pinned `DEV_KIT_VERSION` the first time they are needed, and refetched when +that version changes. A repository that commits its own copy of one of these +files keeps it. The fetch rule only fires for a file that is absent. Add the +bootstrapped ones to `.gitignore` next to the existing `common.mk` entry. + +`shfmt` reads its style from `.editorconfig`, so an editor and `make fmt` cannot +disagree about shell scripts. + +Repositories extend the entry points by adding prerequisites rather than +redefining them, which keeps the recipe in `common.mk`: + +```make +fmt: license-headers +lint: license-headers-check +``` + ### Variables -| Variable | Default | Description | -| --- | --- | --- | -| `BUILD_PATH` | `$(shell pwd)` | Base directory for local binaries | -| `LOCALBIN` | `$(BUILD_PATH)/bin` | Directory for installed binaries | -| `OSV_SCANNER_CONFIG` | `./.osv-scanner.toml` | Path to osv-scanner configuration | -| `OS` | `$(shell $(GO) env GOOS)` | Current Operating System | -| `ARCH` | `$(shell $(GO) env GOARCH)` | Current CPU architecture | +| Variable | Default | Description | +| --- | --- | --- | +| `BUILD_PATH` | `$(shell pwd)` | Base directory for local binaries | +| `LOCALBIN` | `$(BUILD_PATH)/bin` | Directory for installed binaries | +| `OSV_SCANNER_CONFIG` | `./.osv-scanner.toml` | Path to osv-scanner configuration | +| `DEV_KIT_CONFIGS` | `.editorconfig .golangci.yml treefmt.toml` | Shared configs to bootstrap; set to empty to opt out | +| `TREEFMT_ARGS` | *(empty)* | Extra treefmt flags, e.g. `--allow-missing-formatter` in repos without Go | +| `OS` | `$(shell $(GO) env GOOS)` | Current Operating System | +| `ARCH` | `$(shell $(GO) env GOARCH)` | Current CPU architecture | Any binary defined in your `tools.lock` is also available as a Make target (e.g. `make $(CONTROLLER_GEN)`). Take a look at the variables defined in @@ -121,18 +161,18 @@ It configures: The repository settings are configurable via make variables (set them in your `Makefile` or pass them on the command line, e.g. `make repo-settings REPO_STATUS_CHECKS='["CI","lint"]' REPO_RULESET_BRANCHES='["release/*"]'`): -| Variable | Default | Description | -| --- | --- | --- | -| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | -| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | -| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | -| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | -| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | -| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | -| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | -| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | -| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | -| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | +| Variable | Default | Description | +| --- | --- | --- | +| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | +| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | +| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | +| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | +| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | +| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | +| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | +| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | +| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | +| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | ### GitHub Actions @@ -140,11 +180,11 @@ The repo ships composite actions that consuming repositories reference directly. Pin them to a SHA with a version comment, as the action-pin check requires. The `<40-char-sha>` placeholders below are not copy-pasteable — substitute real digests: -| Action | Description | -| --- | --- | -| `.github/actions/setup-nix` | Install upstream Nix and enable the shared Cachix cache | -| `.github/actions/get-go-version` | Extract the Go version from `flake.nix` | -| `.github/actions/diff-check` | Fail if a command left uncommitted changes behind | +| Action | Description | +| --- | --- | +| `.github/actions/setup-nix` | Install upstream Nix and enable the shared Cachix cache | +| `.github/actions/get-go-version` | Extract the Go version from `flake.nix` | +| `.github/actions/diff-check` | Fail if a command left uncommitted changes behind | `setup-nix` replaces the two-step Nix installer + Cachix preamble that every job needs before it can use `nix develop` as its shell. Secrets are not visible @@ -167,22 +207,22 @@ jobs: - run: make test ``` -| Input | Default | Description | -| --- | --- | --- | -| `cachix-name` | `opendefensecloud` | Cachix cache to use | -| `cachix-auth-token` | `''` | Auth token; empty falls back to a read-only cache, as on fork PRs | -| `cachix-signing-key` | `''` | Signing key; empty disables pushing to the cache | +| Input | Default | Description | +| --- | --- | --- | +| `cachix-name` | `opendefensecloud` | Cachix cache to use | +| `cachix-auth-token` | `''` | Auth token; empty falls back to a read-only cache, as on fork PRs | +| `cachix-signing-key` | `''` | Signing key; empty disables pushing to the cache | ### Default git hooks The dev shell installs the following git hooks automatically: -| Hook | Stage | Description | -| --- | --- | --- | -| `fmt` | `pre-commit` | Runs `make fmt` | -| `lint` | `pre-commit` | Runs `make lint` | +| Hook | Stage | Description | +| --- | --- | --- | +| `fmt` | `pre-commit` | Runs `make fmt` | +| `lint` | `pre-commit` | Runs `make lint` | | `osv-scanner` | `pre-commit` | Runs `make scan` on dependency file changes (disabled by default — see [Vulnerability scanning](#vulnerability-scanning)) | -| `commitlint` | `commit-msg` | Validates commit messages against [Conventional Commits](https://www.conventionalcommits.org/) (disabled by default) | +| `commitlint` | `commit-msg` | Validates commit messages against [Conventional Commits](https://www.conventionalcommits.org/) (disabled by default) | To enable the `commitlint` hook, set `commitlint.enable = true` in `preCommitHooks` and add a `.commitlintrc.yml` to the project root: diff --git a/common.mk b/common.mk index 9e468d3..e9883b7 100644 --- a/common.mk +++ b/common.mk @@ -34,7 +34,11 @@ HELM ?= helm JQ ?= jq KIND ?= kind KUBECTL ?= kubectl +MDFORMAT ?= mdformat SHELLCHECK ?= shellcheck +SHFMT ?= shfmt +TREEFMT ?= treefmt +YAMLFMT ?= yamlfmt YQ ?= yq # External prerequisites (not managed by flake.nix or tools.lock) @@ -159,6 +163,44 @@ setup-local-cluster: ## Set up a Kind cluster for local development if it does n $(KIND) create cluster --name $(KIND_CLUSTER) $(if $(KIND_CONFIG),--config $(KIND_CONFIG)) ;; \ esac +##@ Formatting and linting + +# Shared configuration shipped from dev-kit. The rule only fires for a file that +# is absent, so a repository that commits its own copy silently overrides it. +DEV_KIT_CONFIGS ?= .editorconfig .golangci.yml treefmt.toml + +$(DEV_KIT_CONFIGS): + @curl --fail -sSL \ + 'https://raw.githubusercontent.com/opendefensecloud/dev-kit/$(DEV_KIT_VERSION)/$@' \ + -o '$@.dev-kit-download' + @mv '$@.dev-kit-download' '$@' + @echo "fetched $@ from dev-kit $(DEV_KIT_VERSION)" >&2 + +# treefmt drives gofmt (via golangci-lint), yamlfmt, shfmt and mdformat from one +# config. LOCALGOBIN goes on PATH so it picks up the pinned golangci-lint from +# tools.lock rather than whatever happens to be installed. Repos without Go code +# skip that install and set TREEFMT_ARGS := --allow-missing-formatter. +TREEFMT_ARGS ?= +_TREEFMT_GO_TOOL := $(if $(wildcard go.mod),$(GOLANGCI_LINT)) + +.PHONY: fmt-all +fmt-all: $(_TREEFMT_GO_TOOL) ## Format every file in the repository + @$(if $(DEV_KIT_CONFIGS),$(MAKE) $(DEV_KIT_CONFIGS) >/dev/null) + @PATH="$(LOCALGOBIN):$$PATH" $(TREEFMT) $(TREEFMT_ARGS) + +.PHONY: lint-all +lint-all: $(_TREEFMT_GO_TOOL) ## Fail if any file is not formatted + @$(if $(DEV_KIT_CONFIGS),$(MAKE) $(DEV_KIT_CONFIGS) >/dev/null) + @PATH="$(LOCALGOBIN):$$PATH" $(TREEFMT) --ci $(TREEFMT_ARGS) + +# Public entry points. These names are what the flake git-hooks and every +# workflow call, so they stay stable. A repository extends them by adding +.PHONY: fmt +fmt: fmt-all ## Format code + +.PHONY: lint +lint: lint-all shellcheck $(if $(wildcard go.mod),golangci-lint) ## Check formatting and run all linters + ##@ Common golang targets .PHONY: mod mod: ## run go mod tidy, download, verify @@ -243,11 +285,17 @@ update-common-mk-bootstrap: ## Rewrite the common.mk: rule in Makefile to the cu # If the remote content differs from the local file, deletes this file so that # Make's include-file-remake mechanism triggers the project's common.mk: rule on # its next restart — picking up the pre-downloaded .common.mk-download file. +# DEV_KIT_NO_SELF_UPDATE=1 disables this. dev-kit's own Makefile sets it: there +# common.mk is the source file, and the staleness check would delete it. _COMMON_MK_SELF_UPDATE := $(shell \ + [ -z '$(DEV_KIT_NO_SELF_UPDATE)' ] || exit 0; \ hash_cmd=$$(command -v sha256sum >/dev/null 2>&1 && echo "sha256sum" || echo "shasum -a 256"); \ stored=$$(cat .common.mk-version 2>/dev/null); \ if [ "$$stored" != "$(DEV_KIT_VERSION)" ]; then \ rm -f .common.mk-checked .common.mk-download; \ + for f in $(DEV_KIT_CONFIGS); do \ + git ls-files --error-unmatch "$$f" >/dev/null 2>&1 || rm -f "$$f"; \ + done; \ elif find .common.mk-checked -mmin -60 2>/dev/null | grep -q .; then \ exit 0; \ fi; \ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d33efa6..3bc9fc5 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -12,17 +12,17 @@ This project follows the [Conventional Commits](https://www.conventionalcommits. ### Allowed Types -| Type | Purpose | +| Type | Purpose | | ---------- | ---------------------------------------------------- | -| `feat` | A new feature | -| `fix` | A bug fix | -| `docs` | Documentation changes | -| `chore` | Maintenance tasks (deps, CI config, etc.) | +| `feat` | A new feature | +| `fix` | A bug fix | +| `docs` | Documentation changes | +| `chore` | Maintenance tasks (deps, CI config, etc.) | | `refactor` | Code changes that neither fix a bug nor add a feature | -| `test` | Adding or updating tests | -| `ci` | CI/CD pipeline changes | -| `perf` | Performance improvements | -| `revert` | Reverting a previous commit | +| `test` | Adding or updating tests | +| `ci` | CI/CD pipeline changes | +| `perf` | Performance improvements | +| `revert` | Reverting a previous commit | ### Examples diff --git a/docs/NEW_REPO.md b/docs/NEW_REPO.md index 95dda7c..65cfeee 100644 --- a/docs/NEW_REPO.md +++ b/docs/NEW_REPO.md @@ -11,7 +11,7 @@ Create the repo on GitHub (via UI or `gh repo create`) under the `opendefenseclo Copy the files from `example/` into your project root and adjust them: - **`flake.nix`** — Set `goVersion`, add extra `packages`, and configure `preCommitHooks` as needed. If your project does not use Go, omit `goVersion`. -- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`). Implement the `fmt` and `lint` targets or disable their pre-commit hooks in `flake.nix`. +- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`). `fmt` and `lint` come from `common.mk`; do not redefine them. To add a repository-specific step, attach it as a prerequisite (e.g. `fmt: license-headers`), which appends without overriding the shared recipe. - **`tools.lock`** — Add any Go tool dependencies your project needs (one per line: ` @`). - **`renovate.json`** — Copy as-is. The custom managers handle `DEV_KIT_VERSION` in your Makefile and entries in `tools.lock`. @@ -48,11 +48,11 @@ See the README for the full list of `REPO_*` variables. The following secrets must be whitelisted for your repository at the organization level (Settings > Secrets and variables > Actions > Repository access): -| Secret | Used by | +| Secret | Used by | | --------------------- | -------------------------------------------------- | -| `ADD_TO_PROJECT_PAT` | `issues-add-to-project` workflow | -| `CACHIX_AUTH_TOKEN` | any job using the `setup-nix` action (see below) | -| `CACHIX_SIGNING_KEY` | any job using the `setup-nix` action (see below) | +| `ADD_TO_PROJECT_PAT` | `issues-add-to-project` workflow | +| `CACHIX_AUTH_TOKEN` | any job using the `setup-nix` action (see below) | +| `CACHIX_SIGNING_KEY` | any job using the `setup-nix` action (see below) | If your project uses private runners, whitelist the repository in the runner group settings (Settings > Actions > Runner groups). @@ -61,13 +61,13 @@ If your project uses private runners, whitelist the repository in the runner gro Copy the relevant workflows from `.github/workflows/` in this repository: -| Workflow | Purpose | +| Workflow | Purpose | | ------------------------------- | ------------------------------------------------------- | -| `conventional-commits.yml` | Validates PR titles and commit messages against Conventional Commits | -| `issues-add-labels.yaml` | Automatically adds `needs-triage` label to new issues | -| `issues-add-to-project.yml` | Adds new issues and PRs to the org project board | -| `release-drafter.yaml` | Drafts release notes from merged PRs | -| `update-action-pins.yml` | Fails the PR if any action is not pinned to a SHA | +| `conventional-commits.yml` | Validates PR titles and commit messages against Conventional Commits | +| `issues-add-labels.yaml` | Automatically adds `needs-triage` label to new issues | +| `issues-add-to-project.yml` | Adds new issues and PRs to the org project board | +| `release-drafter.yaml` | Drafts release notes from merged PRs | +| `update-action-pins.yml` | Fails the PR if any action is not pinned to a SHA | If using release-drafter, also copy `.github/release-drafter.yml` (the config file). diff --git a/example/Makefile b/example/Makefile index df8b07f..0f65231 100644 --- a/example/Makefile +++ b/example/Makefile @@ -18,7 +18,3 @@ cobra: $(COBRA) scan: echo hello - -fmt: # FIXME(user): Implement the fmt target or disable the pre-commit hook for it (flake.nix) - -lint: # FIXME(user): Implement the lint target or disable the pre-commit hook for it (flake.nix) diff --git a/flake.nix b/flake.nix index c8e0571..2dd697a 100644 --- a/flake.nix +++ b/flake.nix @@ -49,8 +49,12 @@ kind kubectl kubernetes-helm + mdformat shellcheck + shfmt + treefmt unzip + yamlfmt yq-go ]; diff --git a/scripts/envtest-sideload.sh b/scripts/envtest-sideload.sh index 05e76fd..ad6c33f 100755 --- a/scripts/envtest-sideload.sh +++ b/scripts/envtest-sideload.sh @@ -5,10 +5,10 @@ # opendefensecloud/solution-arsenal#556 for the rationale (envtest's release # cadence lags K8s releases sporadically; upstream is closed-as-not-planned). # -# dl.k8s.io has all versions but only Linux builds. controller-tools has +# dl.k8s.io has all versions but only Linux builds. controller-tools has # Darwin builds, but not for all versions of Kubernetes and etcd. -# To run a specific version under Darwin might not be possible, -# +# To run a specific version under Darwin might not be possible, +# # So it should: # - check K8S_VERSION, use envtest-setup binaries if available # - if not, fall back to dl.k8s.io(which has no Darwin builds) @@ -35,7 +35,7 @@ K8S_VERSION="${K8S_VERSION#v}" YQ="${YQ:-yq}" # Idempotency — bail out if setup-envtest already finds this version in cache. -if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -i -p path >/dev/null 2>&1; then +if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -i -p path > /dev/null 2>&1; then exit 0 fi @@ -43,9 +43,12 @@ fi # below need it. os=$(uname -s | tr '[:upper:]' '[:lower:]') case "$(uname -m)" in - x86_64) arch=amd64 ;; - aarch64|arm64) arch=arm64 ;; - *) echo "envtest-sideload: unsupported arch $(uname -m)" >&2; exit 1 ;; + x86_64) arch=amd64 ;; + aarch64 | arm64) arch=arm64 ;; + *) + echo "envtest-sideload: unsupported arch $(uname -m)" >&2 + exit 1 + ;; esac # We can only assemble a sideload tarball from upstream sources on linux and @@ -54,10 +57,10 @@ esac # vanilla `setup-envtest use` (the controller-tools index) and only fail when # the requested version isn't there. if [ "$os" != linux ] && [ "$os" != darwin ]; then - if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -p path >/dev/null 2>&1; then + if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -p path > /dev/null 2>&1; then exit 0 fi - cat >&2 <&2 << EOF envtest-sideload: ${os} is supported only via controller-tools' pre-packaged archives, but '${K8S_VERSION}' is not in their index and this script can only sideload upstream binaries on linux and darwin. @@ -73,7 +76,7 @@ fi # pre-packaged darwin archive whenever the requested version is in their index; # only fall through to the source build when it isn't. if [ "$os" = darwin ]; then - if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -p path >/dev/null 2>&1; then + if "$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -p path > /dev/null 2>&1; then exit 0 fi fi @@ -92,7 +95,7 @@ fetch() { # checks them. GNU coreutils ships `sha256sum`; stock macOS ships only # `shasum`. Both accept the same check format on stdin. sha256_check() { - if command -v sha256sum >/dev/null 2>&1; then + if command -v sha256sum > /dev/null 2>&1; then sha256sum -c - else shasum -a 256 -c - @@ -102,8 +105,8 @@ sha256_check() { # Look up the etcd version K8s ships with from its build/dependencies.yaml. # Self-updating per K8s release. deps_url="https://raw.githubusercontent.com/kubernetes/kubernetes/v${K8S_VERSION}/build/dependencies.yaml" -etcd_version=$(fetch "$deps_url" \ - | "$YQ" '.dependencies[] | select(.name == "etcd") | .version' | tr -d '"') +etcd_version=$(fetch "$deps_url" | + "$YQ" '.dependencies[] | select(.name == "etcd") | .version' | tr -d '"') if [[ -z "$etcd_version" ]]; then echo "envtest-sideload: could not resolve etcd version from $deps_url" >&2 exit 1 @@ -132,10 +135,10 @@ k8s_base="https://dl.k8s.io/release/v${K8S_VERSION}/bin/${os}/${arch}" download_k8s_bin() { local bin="$1" echo "envtest-sideload: downloading ${k8s_base}/${bin}" - fetch -o "$stage/k8s-bin/$bin" "$k8s_base/$bin" + fetch -o "$stage/k8s-bin/$bin" "$k8s_base/$bin" fetch -o "$stage/k8s-bin/$bin.sha256" "$k8s_base/$bin.sha256" # The .sha256 sibling is just the hex digest; pair with the filename ourselves. - ( cd "$stage/k8s-bin" && printf '%s %s\n' "$(cat "$bin.sha256")" "$bin" | sha256_check ) + (cd "$stage/k8s-bin" && printf '%s %s\n' "$(cat "$bin.sha256")" "$bin" | sha256_check) chmod +x "$stage/k8s-bin/$bin" } @@ -148,7 +151,7 @@ else # tagged Kubernetes source — exactly what controller-tools does to produce # its darwin envtest archives. First run is slow (~hundreds of MB of source # + a multi-minute compile); the result is cached by setup-envtest afterward. - command -v go >/dev/null 2>&1 || { + command -v go > /dev/null 2>&1 || { echo "envtest-sideload: building kube-apiserver for ${os} requires Go on PATH" >&2 exit 1 } @@ -173,8 +176,8 @@ else ldflags+=" -X ${pkg}.gitTreeState=clean" ldflags+=" -X ${pkg}.buildDate=${build_date}" done - ( cd "$src" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \ - go build -ldflags "$ldflags" -o "$stage/k8s-bin/kube-apiserver" ./cmd/kube-apiserver ) + (cd "$src" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \ + go build -ldflags "$ldflags" -o "$stage/k8s-bin/kube-apiserver" ./cmd/kube-apiserver) chmod +x "$stage/k8s-bin/kube-apiserver" fi @@ -191,17 +194,17 @@ else fi echo "envtest-sideload: downloading ${etcd_base}/${etcd_archive}" fetch -o "$stage/$etcd_archive" "$etcd_base/$etcd_archive" -fetch -o "$stage/SHA256SUMS" "$etcd_base/SHA256SUMS" +fetch -o "$stage/SHA256SUMS" "$etcd_base/SHA256SUMS" # etcd's SHA256SUMS lists ` ` for every platform archive; # select our exact filename (awk field match — no regex metachars) and verify. -( cd "$stage" && awk -v f="$etcd_archive" '$2 == f' SHA256SUMS | sha256_check ) +(cd "$stage" && awk -v f="$etcd_archive" '$2 == f' SHA256SUMS | sha256_check) extract_etcd # --- Assemble the sideload tarball with the three binaries at top level --- sideload_tar="$stage/sideload.tar.gz" tar -czf "$sideload_tar" \ - -C "$stage/k8s-bin" kube-apiserver kubectl \ - -C "$stage/$etcd_dir" etcd + -C "$stage/k8s-bin" kube-apiserver kubectl \ + -C "$stage/$etcd_dir" etcd # Feed it to setup-envtest. --os/--arch matter — sideload writes the cache # entry keyed on platform. @@ -211,5 +214,5 @@ echo "envtest-sideload: sideloading ${K8S_VERSION} into ${BIN_DIR}" < "$sideload_tar" # Sanity-check that setup-envtest can now find it. -"$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -i -p path >/dev/null +"$SETUP_ENVTEST" use "$K8S_VERSION" --bin-dir "$BIN_DIR" -i -p path > /dev/null echo "envtest-sideload: done" diff --git a/scripts/repo-settings.sh b/scripts/repo-settings.sh index 4103919..681f8f5 100755 --- a/scripts/repo-settings.sh +++ b/scripts/repo-settings.sh @@ -21,7 +21,7 @@ set -euo pipefail # DEV_KIT_VERSION dev-kit version to fetch the workflow from # GH, JQ commands used to talk to GitHub and build JSON -"$GH" auth status >/dev/null 2>&1 || { +"$GH" auth status > /dev/null 2>&1 || { echo "error: gh is not authenticated; run 'gh auth login'" >&2 exit 1 } @@ -47,7 +47,7 @@ fi allowed_merge_methods=$( # shellcheck disable=SC2016 # $variables belong to jq, not the shell "$JQ" -cn \ - --argjson merge "$REPO_ALLOW_MERGE_COMMIT" \ + --argjson merge "$REPO_ALLOW_MERGE_COMMIT" \ --argjson squash "$REPO_ALLOW_SQUASH_MERGE" \ --argjson rebase "$REPO_ALLOW_REBASE_MERGE" \ '[ @@ -68,8 +68,8 @@ echo "Reconciling settings for $REPO..." echo " Syncing labels..." while IFS=';' read -r name color desc; do [ -z "$name" ] && continue - "$GH" label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2>/dev/null -done <<'EOF' + "$GH" label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force 2> /dev/null +done << 'EOF' bug;d73a4a;Something isn't working documentation;0075ca;Improvements or additions to documentation duplicate;cfd3d7;This issue or pull request already exists @@ -99,11 +99,11 @@ echo " Configuring merge strategy..." -f allow_squash_merge="$REPO_ALLOW_SQUASH_MERGE" \ -f allow_rebase_merge="$REPO_ALLOW_REBASE_MERGE" \ -f delete_branch_on_merge=true \ - -f allow_auto_merge=true >/dev/null + -f allow_auto_merge=true > /dev/null echo " Enabling secret scanning..." "$GH" api "repos/$REPO" -X PATCH \ - --input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') >/dev/null + --input <(echo '{"security_and_analysis":{"secret_scanning":{"status":"enabled"}}}') > /dev/null RULESET_JSON=$( # shellcheck disable=SC2016 # $variables belong to jq, not the shell @@ -149,12 +149,12 @@ echo " branch up-to-date: $REPO_REQUIRE_BRANCH_UP_TO_DATE" echo " required status checks: $REPO_STATUS_CHECKS_EFFECTIVE" echo " admin bypass: $REPO_ADMIN_BYPASS" -existing=$("$GH" api "repos/$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2>/dev/null || true) +existing=$("$GH" api "repos/$REPO/rulesets" -q '.[] | select(.name=="protect-main") | .id' 2> /dev/null || true) if [ -n "$existing" ]; then - "$GH" api "repos/$REPO/rulesets/$existing" -X PUT --input <(echo "$RULESET_JSON") >/dev/null + "$GH" api "repos/$REPO/rulesets/$existing" -X PUT --input <(echo "$RULESET_JSON") > /dev/null echo " Updated existing ruleset (id: $existing)" else - "$GH" api "repos/$REPO/rulesets" -X POST --input <(echo "$RULESET_JSON") >/dev/null + "$GH" api "repos/$REPO/rulesets" -X POST --input <(echo "$RULESET_JSON") > /dev/null echo " Created new ruleset" fi diff --git a/treefmt.toml b/treefmt.toml new file mode 100644 index 0000000..ec73b04 --- /dev/null +++ b/treefmt.toml @@ -0,0 +1,61 @@ +# Shared formatter front-end for ODC repositories. +# Bootstrapped from dev-kit by common.mk; commit a local treefmt.toml +# file to override it. +# +# `make fmt` writes, `make lint` fails on drift. Both go through treefmt. + +on-unmatched = "info" + +# `{,**/}` so a pattern matches at the repository root as well as nested: a +# bare `**/` requires at least one leading path segment. +excludes = [ + # Generated output. Owned by controller-gen, openapi-gen, crd-ref-docs and + # helm-docs — reformatting it only makes `make codegen` dirty again. + "{,**/}zz_generated.*", + "{,**/}charts/*/files/**", + "{,**/}charts/**/README.md", + "{,**/}docs/user-guide/api-reference.md", + "{,**/}client-go/openapi/**", + # A `template`/`templates` directory holds Go templates (Helm charts, the + # solar renderer), and KRO RGD templates embed CEL in unquoted scalars. + # Neither is parseable YAML. + "{,**/}template/**", + "{,**/}templates/**", + "{,**/}rgd-template.yaml", + "{,**/}*.gotmpl", + "{,**/}*.tpl", + # Test inputs are meant to be byte-exact, including the malformed ones. + "{,**/}testdata/**", + "{,**/}fixtures/**", + # Not ours. + "{,**/}vendor/**", + "{,**/}third_party/**", + "bin/**", + "{,**/}*.lock", + "LICENSE", +] + +# Go: delegated to golangci-lint so import order (gci custom-order) stays +# configured in .golangci.yml only. Running gofmt here instead would fight it. +[formatter.go] +command = "golangci-lint" +options = ["fmt"] +includes = ["*.go"] + +[formatter.yaml] +command = "yamlfmt" +options = ["-formatter", "retain_line_breaks=true,indent=2,include_document_start=false"] +includes = ["*.yml", "*.yaml"] + +# Style comes from .editorconfig, which shfmt reads when given no style flags. +[formatter.shell] +command = "shfmt" +options = ["-w"] +includes = ["*.sh", "*.bash"] + +# --number keeps ordered lists numbered 1. 2. 3. in the source; mdformat's +# default rewrites them all to "1.", which renders the same and reads worse. +[formatter.markdown] +command = "mdformat" +options = ["--number"] +includes = ["*.md"] From 5a1e99363f4d0f4b55b9a11b243e4c38bb1e10bb Mon Sep 17 00:00:00 2001 From: Elisa Date: Fri, 2 Oct 2026 09:27:40 +0200 Subject: [PATCH 2/3] chore: improvements after pr review --- .github/workflows/release-drafter.yaml | 8 +-- .gitignore | 1 + .golangci.yml | 5 -- Makefile | 14 ++-- README.md | 28 +++++++- common.mk | 92 ++++++++++++++++++++------ docs/NEW_REPO.md | 2 +- example/Makefile | 2 + 8 files changed, 109 insertions(+), 43 deletions(-) diff --git a/.github/workflows/release-drafter.yaml b/.github/workflows/release-drafter.yaml index 9814ad8..dd7567d 100644 --- a/.github/workflows/release-drafter.yaml +++ b/.github/workflows/release-drafter.yaml @@ -6,13 +6,13 @@ on: branches: - main - # pull_request event is required only for autolabeler + # pull_request event is required only for autolabeler. + # pull_request_target is required for the autolabeler to support PRs from forks + # pull_request_target: + # types: [opened, reopened, synchronize] pull_request: # Only following types are handled by the action, but one can default to all as well types: [opened, reopened, synchronize] - # pull_request_target event is required for autolabeler to support PRs from forks - # pull_request_target: - # types: [opened, reopened, synchronize] permissions: contents: read diff --git a/.gitignore b/.gitignore index fd6f254..180c0bb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ example/common.mk example/bin/ .pre-commit-config.yaml +.common.mk-configs diff --git a/.golangci.yml b/.golangci.yml index ee07b20..fff1394 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -28,8 +28,6 @@ linters: presets: [comments, common-false-positives, legacy, std-error-handling] paths: [third_party, builtin$, examples$] warn-unused: true - disable: - - nilnil enable: - asasalint - asciicheck @@ -54,13 +52,11 @@ linters: - gocritic - godoclint - godot - - goheader - goprintffuncname - gosec - gosmopolitan - govet - grouper - - importas - ineffassign - interfacebloat - intrange @@ -72,7 +68,6 @@ linters: - musttag - nakedret - nilerr - - nilnil - nlreturn - noctx - nosprintfhostport diff --git a/Makefile b/Makefile index 6958b1a..ec89644 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,7 @@ +# dev-kit is the source of common.mk, so it includes its own copy directly. The +# self-update check skips repositories where common.mk is tracked by git, and +# DEV_KIT_CONFIGS resolves to nothing here because the shared configs live in +# this repository already. DEV_KIT_VERSION := main -DEV_KIT_NO_SELF_UPDATE := 1 - -# The shared configs live in this repository; there is nothing to bootstrap. -DEV_KIT_CONFIGS := - +DEV_KIT_FORMATTING := on include common.mk - -# No Go code here, so golangci-lint is not installed and treefmt must not -# insist on it. Every other repository leaves TREEFMT_ARGS empty. -TREEFMT_ARGS := --allow-missing-formatter diff --git a/README.md b/README.md index 169fa0b..ed8d363 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,21 @@ The included `common.mk` provides: ### Formatting and linting +Formatting is **opt-in**. Set it in the project `Makefile` before +`-include common.mk`, next to `DEV_KIT_VERSION`: + +```make +DEV_KIT_FORMATTING := on +``` + +Until a repository does, `fmt` and `lint` behave exactly as they did before this +existed. That matters because a repository which still defines its own `fmt:` or +`lint:` recipe would otherwise have `fmt-all`/`lint-all` merged in as +prerequisites silently on its next `DEV_KIT_VERSION` bump — make appends +prerequisites without warning — and start failing on every YAML, Markdown and +shell file it has never formatted. Run the one-time `make fmt-all` commit first, +then switch it on deliberately. + One formatter front-end for the whole repository. `treefmt` drives gofmt (via `golangci-lint fmt`, so import order stays configured in one place), `yamlfmt`, `shfmt` and `mdformat`. @@ -53,8 +68,16 @@ insist on a formatter it will never call, and narrow `DEV_KIT_CONFIGS` to drop `treefmt.toml`, `.golangci.yml` and `.editorconfig` are fetched from dev-kit at the pinned `DEV_KIT_VERSION` the first time they are needed, and refetched when that version changes. A repository that commits its own copy of one of these -files keeps it. The fetch rule only fires for a file that is absent. Add the -bootstrapped ones to `.gitignore` next to the existing `common.mk` entry. +files keeps it. The fetch rule only fires for a file that is absent, and an +alternate spelling counts as an override (`.golangci.yaml`, `.treefmt.toml`). +Add the bootstrapped ones to `.gitignore`, together with `.common.mk-configs`, +next to the existing `common.mk` entry — `.common.mk-configs` records what was +fetched so that a `DEV_KIT_VERSION` bump only ever removes files this mechanism +created, never a hand-written override that is not committed yet. + +Refresh happens when the `DEV_KIT_VERSION` string changes. With a moving ref +(`main`) the configs are therefore fetched once and not refreshed again, while +`common.mk` itself keeps updating via its hourly content check. `shfmt` reads its style from `.editorconfig`, so an editor and `make fmt` cannot disagree about shell scripts. @@ -74,6 +97,7 @@ lint: license-headers-check | `BUILD_PATH` | `$(shell pwd)` | Base directory for local binaries | | `LOCALBIN` | `$(BUILD_PATH)/bin` | Directory for installed binaries | | `OSV_SCANNER_CONFIG` | `./.osv-scanner.toml` | Path to osv-scanner configuration | +| `DEV_KIT_FORMATTING` | `off` | `on` enables the shared `fmt`/`lint` formatting. Set it before `-include common.mk` | | `DEV_KIT_CONFIGS` | `.editorconfig .golangci.yml treefmt.toml` | Shared configs to bootstrap; set to empty to opt out | | `TREEFMT_ARGS` | *(empty)* | Extra treefmt flags, e.g. `--allow-missing-formatter` in repos without Go | | `OS` | `$(shell $(GO) env GOOS)` | Current Operating System | diff --git a/common.mk b/common.mk index 9ff74ad..05a5084 100644 --- a/common.mk +++ b/common.mk @@ -34,11 +34,8 @@ HELM ?= helm JQ ?= jq KIND ?= kind KUBECTL ?= kubectl -MDFORMAT ?= mdformat SHELLCHECK ?= shellcheck -SHFMT ?= shfmt TREEFMT ?= treefmt -YAMLFMT ?= yamlfmt YQ ?= yq # External prerequisites (not managed by flake.nix or tools.lock) @@ -132,7 +129,7 @@ clean: .PHONY: shellcheck shellcheck: ## run shellcheck - $(SHELLCHECK) $$(git ls-files '*\.sh') + @git ls-files -z '*.sh' | xargs -0r $(SHELLCHECK) OSV_SCANNER_CONFIG ?= ./.osv-scanner.toml .PHONY: scan @@ -191,41 +188,86 @@ setup-local-cluster: ## Set up a Kind cluster for local development if it does n ##@ Formatting and linting -# Shared configuration shipped from dev-kit. The rule only fires for a file that -# is absent, so a repository that commits its own copy silently overrides it. -DEV_KIT_CONFIGS ?= .editorconfig .golangci.yml treefmt.toml +# Shared configuration shipped from dev-kit, fetched only when absent. A +# repository overrides a file by committing its own copy, including under the +# alternate spellings the org already uses (.golangci.yaml, .treefmt.toml), +# which would otherwise be shadowed: treefmt reads treefmt.toml first, and an +# untracked .golangci.yml trips diff-check's --untracked-files=all. +# .golangci.yml is only fetched for repositories that have Go code. +DEV_KIT_CONFIGS ?= \ + $(if $(wildcard .editorconfig),,.editorconfig) \ + $(if $(wildcard treefmt.toml .treefmt.toml),,treefmt.toml) \ + $(if $(wildcard go.mod),$(if $(wildcard .golangci.yml .golangci.yaml),,.golangci.yml)) + +# Records what this mechanism fetched, so a version bump only ever removes files +# it created itself, never a hand-written override that is not committed yet. +DEV_KIT_CONFIGS_MARKER := .common.mk-configs $(DEV_KIT_CONFIGS): @curl --fail -sSL \ 'https://raw.githubusercontent.com/opendefensecloud/dev-kit/$(DEV_KIT_VERSION)/$@' \ -o '$@.dev-kit-download' @mv '$@.dev-kit-download' '$@' + @grep -qxF '$@' $(DEV_KIT_CONFIGS_MARKER) 2>/dev/null || echo '$@' >> $(DEV_KIT_CONFIGS_MARKER) @echo "fetched $@ from dev-kit $(DEV_KIT_VERSION)" >&2 # treefmt drives gofmt (via golangci-lint), yamlfmt, shfmt and mdformat from one # config. LOCALGOBIN goes on PATH so it picks up the pinned golangci-lint from -# tools.lock rather than whatever happens to be installed. Repos without Go code -# skip that install and set TREEFMT_ARGS := --allow-missing-formatter. -TREEFMT_ARGS ?= +# tools.lock rather than whatever happens to be installed. +# A repository with no go.mod has no golangci-lint to install. +TREEFMT_ARGS ?= $(if $(wildcard go.mod),,--allow-missing-formatter) _TREEFMT_GO_TOOL := $(if $(wildcard go.mod),$(GOLANGCI_LINT)) +# The formatters come from the dev-kit flake, which consumers pin separately +# from DEV_KIT_VERSION. Bumping the Makefile without bumping flake.lock is a +# common mistake, and the bare "command not found" does not say so. +.PHONY: _require-formatters +_require-formatters: + @missing=; \ + for tool in $(TREEFMT) yamlfmt shfmt mdformat; do \ + command -v "$$tool" >/dev/null 2>&1 || missing="$$missing $$tool"; \ + done; \ + if [ -n "$$missing" ]; then \ + echo "error: missing formatter(s):$$missing" >&2; \ + echo " they ship with the dev-kit flake — run 'nix flake update dev-kit'" >&2; \ + echo " and commit flake.lock, or enter the dev shell with 'direnv allow'." >&2; \ + exit 1; \ + fi + +# Order-only prerequisites (after the |) fetch the configs without making them +# part of the up-to-date check. .PHONY: fmt-all -fmt-all: $(_TREEFMT_GO_TOOL) ## Format every file in the repository - @$(if $(DEV_KIT_CONFIGS),$(MAKE) $(DEV_KIT_CONFIGS) >/dev/null) +fmt-all: _require-formatters $(_TREEFMT_GO_TOOL) | $(DEV_KIT_CONFIGS) ## Format every file in the repository @PATH="$(LOCALGOBIN):$$PATH" $(TREEFMT) $(TREEFMT_ARGS) .PHONY: lint-all -lint-all: $(_TREEFMT_GO_TOOL) ## Fail if any file is not formatted - @$(if $(DEV_KIT_CONFIGS),$(MAKE) $(DEV_KIT_CONFIGS) >/dev/null) +lint-all: _require-formatters $(_TREEFMT_GO_TOOL) | $(DEV_KIT_CONFIGS) ## Fail if any file is not formatted @PATH="$(LOCALGOBIN):$$PATH" $(TREEFMT) --ci $(TREEFMT_ARGS) +# Formatting is opt-in. A repository that still defines its own `fmt:`/`lint:` +# recipe would otherwise have fmt-all/lint-all merged in as prerequisites +# silently on the next DEV_KIT_VERSION bump, and start failing on every YAML, +# Markdown and shell file it has never formatted. Set this in the project +# Makefile *before* `-include common.mk`, next to DEV_KIT_VERSION, after running +# the one-time `make fmt-all` commit: +# +# DEV_KIT_FORMATTING := on +# +# With it off, `lint` behaves exactly as it did before this mechanism existed. +DEV_KIT_FORMATTING ?= off +ifeq ($(filter on off,$(DEV_KIT_FORMATTING)),) + $(error DEV_KIT_FORMATTING must be 'on' or 'off', got '$(DEV_KIT_FORMATTING)') +endif +_fmt_enabled = $(filter on,$(DEV_KIT_FORMATTING)) + # Public entry points. These names are what the flake git-hooks and every # workflow call, so they stay stable. A repository extends them by adding +# prerequisites rather than redefining them. .PHONY: fmt -fmt: fmt-all ## Format code +fmt: $(if $(_fmt_enabled),fmt-all) ## Format code .PHONY: lint -lint: lint-all shellcheck $(if $(wildcard go.mod),golangci-lint) ## Check formatting and run all linters +lint: $(if $(_fmt_enabled),lint-all) shellcheck $(if $(wildcard go.mod),golangci-lint) ## Check formatting and run all linters ##@ Common golang targets .PHONY: mod @@ -234,8 +276,11 @@ mod: ## run go mod tidy, download, verify @$(GO) mod download @$(GO) mod verify +# Order-only on DEV_KIT_CONFIGS so a direct `make golangci-lint` on a fresh +# clone, or a parallel `make -j lint`, cannot run before .golangci.yml is there +# and silently lint with golangci-lint's own defaults. .PHONY: golangci-lint -golangci-lint: $(GOLANGCI_LINT) ## run golangci-lint +golangci-lint: $(GOLANGCI_LINT) | $(DEV_KIT_CONFIGS) ## run golangci-lint $(GOLANGCI_LINT) run -v # Cached rather than piped to bash like repo-settings: this is a `make test` @@ -311,17 +356,20 @@ update-common-mk-bootstrap: ## Rewrite the common.mk: rule in Makefile to the cu # If the remote content differs from the local file, deletes this file so that # Make's include-file-remake mechanism triggers the project's common.mk: rule on # its next restart — picking up the pre-downloaded .common.mk-download file. -# DEV_KIT_NO_SELF_UPDATE=1 disables this. dev-kit's own Makefile sets it: there -# common.mk is the source file, and the staleness check would delete it. +# Skipped when common.mk is tracked by git: that means this repository is either +# dev-kit itself, where common.mk is the source file the check would delete, or a +# consumer that deliberately committed a pinned copy. _COMMON_MK_SELF_UPDATE := $(shell \ - [ -z '$(DEV_KIT_NO_SELF_UPDATE)' ] || exit 0; \ + if git ls-files --error-unmatch '$(_COMMON_MK_PATH)' >/dev/null 2>&1; then exit 0; fi; \ hash_cmd=$$(command -v sha256sum >/dev/null 2>&1 && echo "sha256sum" || echo "shasum -a 256"); \ stored=$$(cat .common.mk-version 2>/dev/null); \ if [ "$$stored" != "$(DEV_KIT_VERSION)" ]; then \ rm -f .common.mk-checked .common.mk-download; \ - for f in $(DEV_KIT_CONFIGS); do \ + while read -r f; do \ + [ -n "$$f" ] || continue; \ git ls-files --error-unmatch "$$f" >/dev/null 2>&1 || rm -f "$$f"; \ - done; \ + done < $(DEV_KIT_CONFIGS_MARKER) 2>/dev/null; \ + rm -f $(DEV_KIT_CONFIGS_MARKER); \ elif find .common.mk-checked -mmin -60 2>/dev/null | grep -q .; then \ exit 0; \ fi; \ diff --git a/docs/NEW_REPO.md b/docs/NEW_REPO.md index 65cfeee..32ced4e 100644 --- a/docs/NEW_REPO.md +++ b/docs/NEW_REPO.md @@ -11,7 +11,7 @@ Create the repo on GitHub (via UI or `gh repo create`) under the `opendefenseclo Copy the files from `example/` into your project root and adjust them: - **`flake.nix`** — Set `goVersion`, add extra `packages`, and configure `preCommitHooks` as needed. If your project does not use Go, omit `goVersion`. -- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`). `fmt` and `lint` come from `common.mk`; do not redefine them. To add a repository-specific step, attach it as a prerequisite (e.g. `fmt: license-headers`), which appends without overriding the shared recipe. +- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`) and set `DEV_KIT_FORMATTING := on`; both go before `-include common.mk`. A new repository has nothing to migrate, so formatting can be on from the start. `fmt` and `lint` come from `common.mk`; do not redefine them. To add a repository-specific step, attach it as a prerequisite (e.g. `fmt: license-headers`), which appends without overriding the shared recipe. - **`tools.lock`** — Add any Go tool dependencies your project needs (one per line: ` @`). - **`renovate.json`** — Copy as-is. The custom managers handle `DEV_KIT_VERSION` in your Makefile and entries in `tools.lock`. diff --git a/example/Makefile b/example/Makefile index 0f65231..fd130b2 100644 --- a/example/Makefile +++ b/example/Makefile @@ -1,6 +1,8 @@ # Include ODC common make targets # FIXME(user): replace with a static dev-kit release (https://github.com/opendefensecloud/dev-kit/releases) DEV_KIT_VERSION := refs/heads/main +# Shared formatting via treefmt. A new repository has nothing to migrate. +DEV_KIT_FORMATTING := on -include common.mk common.mk: From caa0785786851df55aae0beb889e66e621342329 Mon Sep 17 00:00:00 2001 From: Elisa Date: Mon, 5 Oct 2026 11:00:54 +0200 Subject: [PATCH 3/3] chore: fixes and improvements after PR review --- .github/actions/setup-nix/action.yml | 3 +- .github/workflows/renovate-auto-approve.yml | 4 +- README.md | 128 ++++++++++---------- common.mk | 72 ++++++----- docs/CONTRIBUTING.md | 20 +-- docs/NEW_REPO.md | 24 ++-- flake.nix | 2 +- treefmt.toml | 2 +- 8 files changed, 137 insertions(+), 118 deletions(-) diff --git a/.github/actions/setup-nix/action.yml b/.github/actions/setup-nix/action.yml index caa1515..dc759fe 100644 --- a/.github/actions/setup-nix/action.yml +++ b/.github/actions/setup-nix/action.yml @@ -7,7 +7,8 @@ inputs: default: opendefensecloud cachix-auth-token: description: >- - Cachix auth token. Leave empty to fall back to a read-only cache, which is what happens on fork pull requests, where secrets are unavailable. + Cachix auth token. Leave empty to fall back to a read-only cache, which + is what happens on fork pull requests, where secrets are unavailable. required: false default: '' cachix-signing-key: diff --git a/.github/workflows/renovate-auto-approve.yml b/.github/workflows/renovate-auto-approve.yml index 96c438b..7078d44 100644 --- a/.github/workflows/renovate-auto-approve.yml +++ b/.github/workflows/renovate-auto-approve.yml @@ -49,7 +49,9 @@ jobs: auto-approve: runs-on: ubuntu-latest if: >- - github.event_name == 'pull_request' && github.event.pull_request.user.login == inputs.renovate-actor && (contains(github.event.pull_request.labels.*.name, inputs.automerge-label) || + github.event_name == 'pull_request' && + github.event.pull_request.user.login == inputs.renovate-actor && + (contains(github.event.pull_request.labels.*.name, inputs.automerge-label) || contains(github.event.pull_request.labels.*.name, inputs.block-label)) steps: - name: Approve an unmodified Renovate PR, or revoke a prior approval diff --git a/README.md b/README.md index ed8d363..b54e637 100644 --- a/README.md +++ b/README.md @@ -17,21 +17,21 @@ Copy the files from `example/` into your project and adjust them for your needs. The included `common.mk` provides: -| Target | Description | -| --- | --- | -| `help` | Display all available targets | -| `clean` | Remove the `bin/` directory | -| `fmt` | Format every file in the repository | -| `lint` | Check formatting, then run shellcheck and, when the repository has a `go.mod`, golangci-lint | -| `fmt-all` | Formatting only (what `fmt` calls) | -| `lint-all` | Fail if any file is not formatted (what `lint` calls) | -| `mod` | Run `go mod tidy`, `download`, and `verify` | -| `golangci-lint` | Run golangci-lint | -| `shellcheck` | Run shellcheck on shell scripts | -| `scan` | Scan for vulnerabilities using osv-scanner | -| `setup-local-cluster` | Create a Kind cluster for local development | -| `repo-settings` | Reconcile GitHub repository settings | -| `envtest-binaries-sideload` | Populate the envtest cache from upstream K8s/etcd releases | +| Target | Description | +| --------------------------- | -------------------------------------------------------------------------------------------- | +| `help` | Display all available targets | +| `clean` | Remove the `bin/` directory | +| `fmt` | Format every file in the repository | +| `lint` | Check formatting, then run shellcheck and, when the repository has a `go.mod`, golangci-lint | +| `fmt-all` | Formatting only (what `fmt` calls) | +| `lint-all` | Fail if any file is not formatted (what `lint` calls) | +| `mod` | Run `go mod tidy`, `download`, and `verify` | +| `golangci-lint` | Run golangci-lint | +| `shellcheck` | Run shellcheck on shell scripts | +| `scan` | Scan for vulnerabilities using osv-scanner | +| `setup-local-cluster` | Create a Kind cluster for local development | +| `repo-settings` | Reconcile GitHub repository settings | +| `envtest-binaries-sideload` | Populate the envtest cache from upstream K8s/etcd releases | ### Formatting and linting @@ -42,13 +42,16 @@ Formatting is **opt-in**. Set it in the project `Makefile` before DEV_KIT_FORMATTING := on ``` -Until a repository does, `fmt` and `lint` behave exactly as they did before this -existed. That matters because a repository which still defines its own `fmt:` or -`lint:` recipe would otherwise have `fmt-all`/`lint-all` merged in as -prerequisites silently on its next `DEV_KIT_VERSION` bump — make appends -prerequisites without warning — and start failing on every YAML, Markdown and -shell file it has never formatted. Run the one-time `make fmt-all` commit first, -then switch it on deliberately. +While it is off, no formatting runs: `fmt` does nothing beyond whatever the +project's own `fmt:` rule does, and `lint` runs `shellcheck` and +`golangci-lint` exactly as before, fetching only `.golangci.yml`. + +This is opt-in because a repository that still defines its own `fmt:` or `lint:` +recipe would otherwise have `fmt-all`/`lint-all` merged in as prerequisites +silently on its next `DEV_KIT_VERSION` bump — make appends prerequisites without +warning — and start failing on every YAML, Markdown and shell file it has never +formatted. Run the one-time `make fmt-all` commit first, then switch it on +deliberately. One formatter front-end for the whole repository. `treefmt` drives gofmt (via `golangci-lint fmt`, so import order stays configured in one place), `yamlfmt`, @@ -60,10 +63,9 @@ make lint # fails on drift, then shellcheck and golangci-lint (Go repos) ``` The Go steps are conditional on a `go.mod` in the repository root: without one, -`lint` skips golangci-lint and `fmt` does not install it. Repositories with no -Go code also set `TREEFMT_ARGS := --allow-missing-formatter` so treefmt does not -insist on a formatter it will never call, and narrow `DEV_KIT_CONFIGS` to drop -`.golangci.yml`. +`lint` skips golangci-lint, `fmt` does not install it, `.golangci.yml` is not +fetched, and treefmt is passed `--allow-missing-formatter` so it does not insist +on a formatter it will never call. None of that needs configuring by hand. `treefmt.toml`, `.golangci.yml` and `.editorconfig` are fetched from dev-kit at the pinned `DEV_KIT_VERSION` the first time they are needed, and refetched when @@ -92,16 +94,16 @@ lint: license-headers-check ### Variables -| Variable | Default | Description | -| --- | --- | --- | -| `BUILD_PATH` | `$(shell pwd)` | Base directory for local binaries | -| `LOCALBIN` | `$(BUILD_PATH)/bin` | Directory for installed binaries | -| `OSV_SCANNER_CONFIG` | `./.osv-scanner.toml` | Path to osv-scanner configuration | -| `DEV_KIT_FORMATTING` | `off` | `on` enables the shared `fmt`/`lint` formatting. Set it before `-include common.mk` | -| `DEV_KIT_CONFIGS` | `.editorconfig .golangci.yml treefmt.toml` | Shared configs to bootstrap; set to empty to opt out | -| `TREEFMT_ARGS` | *(empty)* | Extra treefmt flags, e.g. `--allow-missing-formatter` in repos without Go | -| `OS` | `$(shell $(GO) env GOOS)` | Current Operating System | -| `ARCH` | `$(shell $(GO) env GOARCH)` | Current CPU architecture | +| Variable | Default | Description | +| -------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `BUILD_PATH` | `$(shell pwd)` | Base directory for local binaries | +| `LOCALBIN` | `$(BUILD_PATH)/bin` | Directory for installed binaries | +| `OSV_SCANNER_CONFIG` | `./.osv-scanner.toml` | Path to osv-scanner configuration | +| `DEV_KIT_FORMATTING` | `off` | `on` enables the shared `fmt`/`lint` formatting. Set it before `-include common.mk` | +| `DEV_KIT_CONFIGS` | `.editorconfig .golangci.yml treefmt.toml` | Shared configs to bootstrap; set to empty to opt out. Each file is skipped automatically when the repo overrides or does not need it | +| `TREEFMT_ARGS` | derived | Extra treefmt flags. Defaults to `--allow-missing-formatter` in repos without a `go.mod`, empty otherwise | +| `OS` | `$(shell $(GO) env GOOS)` | Current Operating System | +| `ARCH` | `$(shell $(GO) env GOARCH)` | Current CPU architecture | Any binary defined in your `tools.lock` is also available as a Make target (e.g. `make $(CONTROLLER_GEN)`). Take a look at the variables defined in @@ -185,18 +187,18 @@ It configures: The repository settings are configurable via make variables (set them in your `Makefile` or pass them on the command line, e.g. `make repo-settings REPO_STATUS_CHECKS='["CI","lint"]' REPO_RULESET_BRANCHES='["release/*"]'`): -| Variable | Default | Description | -| --- | --- | --- | -| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | -| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | -| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | -| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | -| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | -| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | -| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | -| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | -| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | -| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | +| Variable | Default | Description | +| -------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `REPO_ALLOW_MERGE_COMMIT` | `true` | Allow merge commits in the merge strategy | +| `REPO_ALLOW_SQUASH_MERGE` | `false` | Allow squash merging | +| `REPO_ALLOW_REBASE_MERGE` | `false` | Allow rebase merging | +| `REPO_REQUIRE_LAST_PUSH_APPROVAL` | `false` | Require the most recent push to be approved before merging | +| `REPO_ADMIN_BYPASS` | `true` | When `false`, org admins cannot bypass the ruleset | +| `REPO_REQUIRED_APPROVING_REVIEW_COUNT` | `1` | Number of approving reviews required to merge | +| `REPO_REQUIRE_CODE_OWNER_REVIEW` | `false` | Require an approving review from code owners | +| `REPO_REQUIRE_BRANCH_UP_TO_DATE` | `false` | Require branches to be up to date before merging (needs at least one `REPO_STATUS_CHECKS` value; `repo-settings` fails otherwise) | +| `REPO_STATUS_CHECKS` | `[]` | JSON array of status-check contexts that must pass (e.g. `["CI","Check action pins"]`); each job name is used as-is, so contexts with spaces work; the `required_status_checks` rule is only added when this is non-empty | +| `REPO_RULESET_BRANCHES` | `[]` | JSON array of additional branch patterns the ruleset applies to (e.g. `["release/*"]`); short names are normalized to `refs/heads/...`; the default branch is always protected | ### GitHub Actions @@ -204,11 +206,11 @@ The repo ships composite actions that consuming repositories reference directly. Pin them to a SHA with a version comment, as the action-pin check requires. The `<40-char-sha>` placeholders below are not copy-pasteable — substitute real digests: -| Action | Description | -| --- | --- | -| `.github/actions/setup-nix` | Install upstream Nix and enable the shared Cachix cache | -| `.github/actions/get-go-version` | Extract the Go version from `flake.nix` | -| `.github/actions/diff-check` | Fail if a command left uncommitted changes behind | +| Action | Description | +| -------------------------------- | ------------------------------------------------------- | +| `.github/actions/setup-nix` | Install upstream Nix and enable the shared Cachix cache | +| `.github/actions/get-go-version` | Extract the Go version from `flake.nix` | +| `.github/actions/diff-check` | Fail if a command left uncommitted changes behind | `setup-nix` replaces the two-step Nix installer + Cachix preamble that every job needs before it can use `nix develop` as its shell. Secrets are not visible @@ -231,22 +233,22 @@ jobs: - run: make test ``` -| Input | Default | Description | -| --- | --- | --- | -| `cachix-name` | `opendefensecloud` | Cachix cache to use | -| `cachix-auth-token` | `''` | Auth token; empty falls back to a read-only cache, as on fork PRs | -| `cachix-signing-key` | `''` | Signing key; empty disables pushing to the cache | +| Input | Default | Description | +| -------------------- | ------------------ | ----------------------------------------------------------------- | +| `cachix-name` | `opendefensecloud` | Cachix cache to use | +| `cachix-auth-token` | `''` | Auth token; empty falls back to a read-only cache, as on fork PRs | +| `cachix-signing-key` | `''` | Signing key; empty disables pushing to the cache | ### Default git hooks The dev shell installs the following git hooks automatically: -| Hook | Stage | Description | -| --- | --- | --- | -| `fmt` | `pre-commit` | Runs `make fmt` | -| `lint` | `pre-commit` | Runs `make lint` | +| Hook | Stage | Description | +| ------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `fmt` | `pre-commit` | Runs `make fmt` | +| `lint` | `pre-commit` | Runs `make lint` | | `osv-scanner` | `pre-commit` | Runs `make scan` on dependency file changes (disabled by default — see [Vulnerability scanning](#vulnerability-scanning)) | -| `commitlint` | `commit-msg` | Validates commit messages against [Conventional Commits](https://www.conventionalcommits.org/) (disabled by default) | +| `commitlint` | `commit-msg` | Validates commit messages against [Conventional Commits](https://www.conventionalcommits.org/) (disabled by default) | To enable the `commitlint` hook, set `commitlint.enable = true` in `preCommitHooks` and add a `.commitlintrc.yml` to the project root: diff --git a/common.mk b/common.mk index 05a5084..ed28f46 100644 --- a/common.mk +++ b/common.mk @@ -188,28 +188,39 @@ setup-local-cluster: ## Set up a Kind cluster for local development if it does n ##@ Formatting and linting -# Shared configuration shipped from dev-kit, fetched only when absent. A -# repository overrides a file by committing its own copy, including under the -# alternate spellings the org already uses (.golangci.yaml, .treefmt.toml), -# which would otherwise be shadowed: treefmt reads treefmt.toml first, and an -# untracked .golangci.yml trips diff-check's --untracked-files=all. -# .golangci.yml is only fetched for repositories that have Go code. -DEV_KIT_CONFIGS ?= \ - $(if $(wildcard .editorconfig),,.editorconfig) \ - $(if $(wildcard treefmt.toml .treefmt.toml),,treefmt.toml) \ - $(if $(wildcard go.mod),$(if $(wildcard .golangci.yml .golangci.yaml),,.golangci.yml)) - -# Records what this mechanism fetched, so a version bump only ever removes files -# it created itself, never a hand-written override that is not committed yet. +# Shared configuration shipped from dev-kit, fetched only when absent. +# The list is static and each file's decision is made inside the recipe, when +# the rule actually runs. +DEV_KIT_CONFIGS ?= .editorconfig .golangci.yml treefmt.toml + +# golangci-lint needs only its own config, so that `lint` with formatting off +# never pulls .editorconfig or treefmt.toml in as untracked files. Empty when +# the repo opted out of fetching it, since no rule then exists to make it. +DEV_KIT_GOLANGCI_CONFIG := $(filter .golangci.yml,$(DEV_KIT_CONFIGS)) + +# Records what was fetched, so a version bump only ever removes files this +# mechanism created itself, never a hand-written override not yet committed. DEV_KIT_CONFIGS_MARKER := .common.mk-configs +# A repository overrides a file by committing its own copy, including under the +# alternate spellings the org already uses: treefmt reads treefmt.toml before +# .treefmt.toml, and an untracked .golangci.yml alongside a committed +# .golangci.yaml would trip diff-check. $(DEV_KIT_CONFIGS): - @curl --fail -sSL \ + @case '$@' in \ + .golangci.yml) \ + if [ -e .golangci.yaml ] || [ ! -e go.mod ]; then exit 0; fi ;; \ + treefmt.toml) \ + if [ -e .treefmt.toml ]; then exit 0; fi ;; \ + esac; \ + curl --fail -sSL \ 'https://raw.githubusercontent.com/opendefensecloud/dev-kit/$(DEV_KIT_VERSION)/$@' \ - -o '$@.dev-kit-download' - @mv '$@.dev-kit-download' '$@' - @grep -qxF '$@' $(DEV_KIT_CONFIGS_MARKER) 2>/dev/null || echo '$@' >> $(DEV_KIT_CONFIGS_MARKER) - @echo "fetched $@ from dev-kit $(DEV_KIT_VERSION)" >&2 + -o '$@.dev-kit-download'; \ + mv '$@.dev-kit-download' '$@'; \ + if ! grep -qxF '$@' $(DEV_KIT_CONFIGS_MARKER) 2>/dev/null; then \ + echo '$@' >> $(DEV_KIT_CONFIGS_MARKER); \ + fi; \ + echo "fetched $@ from dev-kit $(DEV_KIT_VERSION)" >&2 # treefmt drives gofmt (via golangci-lint), yamlfmt, shfmt and mdformat from one # config. LOCALGOBIN goes on PATH so it picks up the pinned golangci-lint from @@ -229,8 +240,9 @@ _require-formatters: done; \ if [ -n "$$missing" ]; then \ echo "error: missing formatter(s):$$missing" >&2; \ - echo " they ship with the dev-kit flake — run 'nix flake update dev-kit'" >&2; \ - echo " and commit flake.lock, or enter the dev shell with 'direnv allow'." >&2; \ + echo " they ship with the dev-kit flake. If flake.nix pins a dev-kit" >&2; \ + echo " tag, bump it to match DEV_KIT_VERSION ($(DEV_KIT_VERSION)); otherwise run" >&2; \ + echo " 'nix flake update dev-kit'. Commit flake.lock, then 'direnv allow'." >&2; \ exit 1; \ fi @@ -276,11 +288,11 @@ mod: ## run go mod tidy, download, verify @$(GO) mod download @$(GO) mod verify -# Order-only on DEV_KIT_CONFIGS so a direct `make golangci-lint` on a fresh -# clone, or a parallel `make -j lint`, cannot run before .golangci.yml is there -# and silently lint with golangci-lint's own defaults. +# Order-only on the config so a direct `make golangci-lint` on a fresh clone, or +# a parallel `make -j lint`, cannot run before .golangci.yml is there and +# silently lint with golangci-lint's own defaults. .PHONY: golangci-lint -golangci-lint: $(GOLANGCI_LINT) | $(DEV_KIT_CONFIGS) ## run golangci-lint +golangci-lint: $(GOLANGCI_LINT) | $(DEV_KIT_GOLANGCI_CONFIG) ## run golangci-lint $(GOLANGCI_LINT) run -v # Cached rather than piped to bash like repo-settings: this is a `make test` @@ -365,11 +377,13 @@ _COMMON_MK_SELF_UPDATE := $(shell \ stored=$$(cat .common.mk-version 2>/dev/null); \ if [ "$$stored" != "$(DEV_KIT_VERSION)" ]; then \ rm -f .common.mk-checked .common.mk-download; \ - while read -r f; do \ - [ -n "$$f" ] || continue; \ - git ls-files --error-unmatch "$$f" >/dev/null 2>&1 || rm -f "$$f"; \ - done < $(DEV_KIT_CONFIGS_MARKER) 2>/dev/null; \ - rm -f $(DEV_KIT_CONFIGS_MARKER); \ + if [ -f $(DEV_KIT_CONFIGS_MARKER) ]; then \ + while read -r f; do \ + [ -n "$$f" ] || continue; \ + git ls-files --error-unmatch "$$f" >/dev/null 2>&1 || rm -f "$$f"; \ + done < $(DEV_KIT_CONFIGS_MARKER); \ + rm -f $(DEV_KIT_CONFIGS_MARKER); \ + fi; \ elif find .common.mk-checked -mmin -60 2>/dev/null | grep -q .; then \ exit 0; \ fi; \ diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3bc9fc5..3059c30 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -12,17 +12,17 @@ This project follows the [Conventional Commits](https://www.conventionalcommits. ### Allowed Types -| Type | Purpose | -| ---------- | ---------------------------------------------------- | -| `feat` | A new feature | -| `fix` | A bug fix | -| `docs` | Documentation changes | -| `chore` | Maintenance tasks (deps, CI config, etc.) | +| Type | Purpose | +| ---------- | ----------------------------------------------------- | +| `feat` | A new feature | +| `fix` | A bug fix | +| `docs` | Documentation changes | +| `chore` | Maintenance tasks (deps, CI config, etc.) | | `refactor` | Code changes that neither fix a bug nor add a feature | -| `test` | Adding or updating tests | -| `ci` | CI/CD pipeline changes | -| `perf` | Performance improvements | -| `revert` | Reverting a previous commit | +| `test` | Adding or updating tests | +| `ci` | CI/CD pipeline changes | +| `perf` | Performance improvements | +| `revert` | Reverting a previous commit | ### Examples diff --git a/docs/NEW_REPO.md b/docs/NEW_REPO.md index 2b5664e..6b606a3 100644 --- a/docs/NEW_REPO.md +++ b/docs/NEW_REPO.md @@ -11,7 +11,7 @@ Create the repo on GitHub (via UI or `gh repo create`) under the `opendefenseclo Copy the files from `example/` into your project root and adjust them: - **`flake.nix`** — Set `goVersion`, add extra `packages`, and configure `preCommitHooks` as needed. If your project does not use Go, omit `goVersion`. -- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`) and set `DEV_KIT_FORMATTING := on`; both go before `-include common.mk`. A new repository has nothing to migrate, so formatting can be on from the start. `fmt` and `lint` come from `common.mk`; do not redefine them. To add a repository-specific step, attach it as a prerequisite (e.g. `fmt: license-headers`), which appends without overriding the shared recipe. +- **`Makefile`** — Pin `DEV_KIT_VERSION` to a release tag (e.g. `v1.0.0`). Implement the `fmt` and `lint` targets or disable their pre-commit hooks in `flake.nix`. - **`tools.lock`** — Add any Go tool dependencies your project needs (one per line: ` @`). - **`renovate.json`** — Copy as-is. The custom managers handle `DEV_KIT_VERSION` in your Makefile and entries in `tools.lock`. @@ -48,10 +48,10 @@ See the README for the full list of `REPO_*` variables. The following secrets must be whitelisted for your repository at the organization level (Settings > Secrets and variables > Actions > Repository access): -| Secret | Used by | -| --------------------- | -------------------------------------------------- | -| `ADD_TO_PROJECT_PAT` | `issues-add-to-project` workflow | -| `CACHIX_AUTH_TOKEN` | any job using the `setup-nix` action (see below) | +| Secret | Used by | +| -------------------- | ------------------------------------------------ | +| `ADD_TO_PROJECT_PAT` | `issues-add-to-project` workflow | +| `CACHIX_AUTH_TOKEN` | any job using the `setup-nix` action (see below) | | `CACHIX_SIGNING_KEY` | any job using the `setup-nix` action (see below) | If your project uses private runners, whitelist the repository in the runner group settings @@ -61,13 +61,13 @@ If your project uses private runners, whitelist the repository in the runner gro Copy the relevant workflows from `.github/workflows/` in this repository: -| Workflow | Purpose | -| ------------------------------- | ------------------------------------------------------- | -| `conventional-commits.yml` | Validates PR titles and commit messages against Conventional Commits | -| `issues-add-labels.yaml` | Automatically adds `needs-triage` label to new issues | -| `issues-add-to-project.yml` | Adds new issues and PRs to the org project board | -| `release-drafter.yaml` | Drafts release notes from merged PRs | -| `update-action-pins.yml` | Fails the PR if any action is not pinned to a SHA | +| Workflow | Purpose | +| --------------------------- | -------------------------------------------------------------------- | +| `conventional-commits.yml` | Validates PR titles and commit messages against Conventional Commits | +| `issues-add-labels.yaml` | Automatically adds `needs-triage` label to new issues | +| `issues-add-to-project.yml` | Adds new issues and PRs to the org project board | +| `release-drafter.yaml` | Drafts release notes from merged PRs | +| `update-action-pins.yml` | Fails the PR if any action is not pinned to a SHA | If using release-drafter, also copy `.github/release-drafter.yml` (the config file). diff --git a/flake.nix b/flake.nix index 2dd697a..3d609dd 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,7 @@ kind kubectl kubernetes-helm - mdformat + (mdformat.withPlugins (ps: [ ps.mdformat-gfm ])) shellcheck shfmt treefmt diff --git a/treefmt.toml b/treefmt.toml index ec73b04..757f882 100644 --- a/treefmt.toml +++ b/treefmt.toml @@ -44,7 +44,7 @@ includes = ["*.go"] [formatter.yaml] command = "yamlfmt" -options = ["-formatter", "retain_line_breaks=true,indent=2,include_document_start=false"] +options = ["-formatter", "retain_line_breaks=true,indent=2,include_document_start=false,scan_folded_as_literal=true"] includes = ["*.yml", "*.yaml"] # Style comes from .editorconfig, which shfmt reads when given no style flags.