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/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..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 new file mode 100644 index 0000000..fff1394 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,100 @@ +# 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 + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - canonicalheader + - contextcheck + - copyloopvar + - decorder + - dogsled + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + - exhaustive + - exptostd + - forbidigo + - ginkgolinter + - gocheckcompilerdirectives + - gochecksumtype + - gocritic + - godoclint + - godot + - goprintffuncname + - gosec + - gosmopolitan + - govet + - grouper + - ineffassign + - interfacebloat + - intrange + - loggercheck + - makezero + - mirror + - misspell + - modernize + - musttag + - nakedret + - nilerr + - 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..ec89644 --- /dev/null +++ b/Makefile @@ -0,0 +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_FORMATTING := on +include common.mk diff --git a/README.md b/README.md index ac83545..b54e637 100644 --- a/README.md +++ b/README.md @@ -17,27 +17,93 @@ 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 | -| `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 + +Formatting is **opt-in**. Set it in the project `Makefile` before +`-include common.mk`, next to `DEV_KIT_VERSION`: + +```make +DEV_KIT_FORMATTING := on +``` + +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`, +`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, `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 +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, 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. + +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_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 @@ -122,7 +188,7 @@ 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 | @@ -140,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 @@ -167,18 +233,18 @@ 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` | | `osv-scanner` | `pre-commit` | Runs `make scan` on dependency file changes (disabled by default — see [Vulnerability scanning](#vulnerability-scanning)) | diff --git a/common.mk b/common.mk index da0cf6f..ed28f46 100644 --- a/common.mk +++ b/common.mk @@ -35,6 +35,7 @@ JQ ?= jq KIND ?= kind KUBECTL ?= kubectl SHELLCHECK ?= shellcheck +TREEFMT ?= treefmt YQ ?= yq # External prerequisites (not managed by flake.nix or tools.lock) @@ -128,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 @@ -185,6 +186,101 @@ 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, 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): + @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' '$@'; \ + 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 +# 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. 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 + +# Order-only prerequisites (after the |) fetch the configs without making them +# part of the up-to-date check. +.PHONY: fmt-all +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: _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: $(if $(_fmt_enabled),fmt-all) ## Format code + +.PHONY: lint +lint: $(if $(_fmt_enabled),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 @@ -192,8 +288,11 @@ mod: ## run go mod tidy, download, verify @$(GO) mod download @$(GO) mod verify +# 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) ## 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` @@ -269,11 +368,22 @@ 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. +# 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 \ + 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; \ + 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 d33efa6..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 872ccfe..6b606a3 100644 --- a/docs/NEW_REPO.md +++ b/docs/NEW_REPO.md @@ -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 | -| --------------------- | -------------------------------------------------- | -| `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) | +| 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 (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 | -| ------------------------------- | ------------------------------------------------------- | -| `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/example/Makefile b/example/Makefile index df8b07f..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: @@ -18,7 +20,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..3d609dd 100644 --- a/flake.nix +++ b/flake.nix @@ -49,8 +49,12 @@ kind kubectl kubernetes-helm + (mdformat.withPlugins (ps: [ ps.mdformat-gfm ])) 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..757f882 --- /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,scan_folded_as_literal=true"] +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"]