diff --git a/.github/workflows/build-check-goreleaser.yml b/.github/workflows/build-check-goreleaser.yml index 375df20..e8f5009 100644 --- a/.github/workflows/build-check-goreleaser.yml +++ b/.github/workflows/build-check-goreleaser.yml @@ -27,6 +27,7 @@ on: description: 'GoReleaser version to run' required: false type: string + # renovate: datasource=github-releases depName=goreleaser/goreleaser default: 'v2.18.2' build-deps: description: 'Space-separated apt packages to install before building (e.g. "gcc-aarch64-linux-gnu libc6-dev-arm64-cross")' diff --git a/.github/workflows/build-release-goreleaser.yml b/.github/workflows/build-release-goreleaser.yml index f8a7492..35cda1b 100644 --- a/.github/workflows/build-release-goreleaser.yml +++ b/.github/workflows/build-release-goreleaser.yml @@ -39,6 +39,7 @@ on: description: 'GoReleaser version to run' required: false type: string + # renovate: datasource=github-releases depName=goreleaser/goreleaser default: 'v2.18.2' build-deps: description: 'Space-separated apt packages to install before building (e.g. "gcc-aarch64-linux-gnu libc6-dev-arm64-cross")' diff --git a/.github/workflows/lint-ci.yml b/.github/workflows/lint-ci.yml index 39b9653..265aab8 100644 --- a/.github/workflows/lint-ci.yml +++ b/.github/workflows/lint-ci.yml @@ -22,6 +22,8 @@ jobs: persist-credentials: false - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 with: + # renovate: datasource=github-releases depName=rhysd/actionlint + version: 'v1.7.12' flags: -color zizmor: diff --git a/.github/workflows/lint-go.yml b/.github/workflows/lint-go.yml index ef4019c..94c72ce 100644 --- a/.github/workflows/lint-go.yml +++ b/.github/workflows/lint-go.yml @@ -24,7 +24,8 @@ on: description: 'golangci-lint release to run' required: false type: string - default: 'latest' + # renovate: datasource=github-releases depName=golangci/golangci-lint + default: 'v2.14.0' runner: description: 'Runner for the golangci-lint job (default: ubuntu-slim)' required: false diff --git a/.github/workflows/lint-makefile-common.yml b/.github/workflows/lint-makefile-common.yml new file mode 100644 index 0000000..fd98717 --- /dev/null +++ b/.github/workflows/lint-makefile-common.yml @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT + +name: lint-makefile-common + +# Reusable workflow that fails when the caller's vendored Makefile.common +# differs from the canonical copy in the latest release of +# OpenCHAMI/github-actions. + +on: + workflow_call: + +permissions: + contents: read # checkout; read the canonical file + +jobs: + lint-makefile-common: + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Compare Makefile.common with the latest release + env: + GH_TOKEN: ${{ github.token }} + SOURCE_REPO: OpenCHAMI/github-actions + run: | + set -euo pipefail + if [[ ! -f Makefile.common ]]; then + echo "::error file=Makefile.common::No Makefile.common in this repository. Copy it from https://github.com/${SOURCE_REPO}/releases/latest." + exit 1 + fi + # /releases/latest excludes drafts and prereleases. + tag=$(gh api "repos/${SOURCE_REPO}/releases/latest" --jq .tag_name) + sha=$(gh api "repos/${SOURCE_REPO}/commits/${tag}" --jq .sha) + canonical="${RUNNER_TEMP}/Makefile.common" + if ! gh api -H "Accept: application/vnd.github.raw" \ + "repos/${SOURCE_REPO}/contents/Makefile.common?ref=${sha}" > "$canonical" 2> "${RUNNER_TEMP}/err"; then + if grep -q "HTTP 404" "${RUNNER_TEMP}/err"; then + echo "::error::No Makefile.common in ${SOURCE_REPO} release ${tag} (${sha})." + else + cat "${RUNNER_TEMP}/err" >&2 + echo "::error::Couldn't read Makefile.common from ${SOURCE_REPO} release ${tag} (${sha})." + fi + exit 1 + fi + if ! diff -u --label "${SOURCE_REPO}@${tag} (${sha})" --label "Makefile.common" "$canonical" Makefile.common; then + echo "::error file=Makefile.common::Makefile.common differs from ${SOURCE_REPO} release ${tag} (${sha}). Replace it with that copy." + exit 1 + fi + echo "Makefile.common matches ${SOURCE_REPO} release ${tag} (${sha})." diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index ab74536..9f5ce5e 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -13,6 +13,7 @@ on: reuse-version: type: string required: false + # renovate: datasource=pypi depName=reuse default: '6.2.0' description: 'Reuse CLI version to use (e.g. "6.2.0").' diff --git a/Makefile.common b/Makefile.common new file mode 100644 index 0000000..aac99e5 --- /dev/null +++ b/Makefile.common @@ -0,0 +1,424 @@ +# SPDX-FileCopyrightText: © 2026 OpenCHAMI a Series of LF Projects, LLC +# SPDX-License-Identifier: MIT +# +# Makefile.common: shared targets for OpenCHAMI repositories. +# +# Vendored from https://github.com/OpenCHAMI/github-actions. Don't edit this +# copy: the lint-makefile-common workflow fails on any difference from the +# latest release. Change the canonical file upstream instead. +# +# Usage (repo Makefile): +# +# PROJECT := foo # overrides go before the include +# include Makefile.common +# +# migrate: # repo-only targets go below +# ... +# +# Extending targets: all, deps, build, run, install, and uninstall are +# double-colon rules. Add your own rule with the same name and `::`, and both +# recipes run in order (this file's first): +# +# deps:: +# npm ci +# +# Don't give an extension rule a `## description` (help would list it twice). +# Other targets are single-colon; redefining one makes make warn "overriding +# recipe". To replace build entirely, set BINARIES empty and add a build::. +# +# Extra arguments: pass ARGS (make test ARGS="-run TestFoo"). It's appended by +# test, coverage, lint, lint-fix, vuln, release-snapshot, and build-packages. +# +# Targets that don't apply skip with a message: Go targets without go.mod, +# Fabrica targets without Fabrica, container targets without a Containerfile +# or Dockerfile, lint-ci without .github/workflows. GNU make 3.81 compatible. + +# --------------------------------------------------------------------------- +# Helpers + +# $(call require,[,]) as a recipe line: fail with a +# hint unless is on PATH. Use for anything beyond coreutils, POSIX +# utilities, and shell builtins. +define require +@command -v $(1) >/dev/null 2>&1 || { echo "make: '$(1)' is required but not installed.$(if $(2), $(2))" >&2; exit 1; } +endef + +# $(call skip,) as a recipe line: say why a target does nothing. +define skip +@echo "$@: $(1), skipping" +endef + +GO_MOD := $(wildcard go.mod) + +# --------------------------------------------------------------------------- +# Variables (override before the include). Defaults that run a command are +# computed once (`?=` would re-run them on every use) and fall back to empty. + +PROJECT ?= $(notdir $(CURDIR)) + +GO ?= go + +ifndef VERSION +VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) +endif +ifndef TAG +TAG := $(or $(shell git describe --tags --always --abbrev=0 2>/dev/null),unknown) +endif +ifndef BRANCH +BRANCH := $(or $(shell git branch --show-current 2>/dev/null),unknown) +endif +ifndef COMMIT +COMMIT := $(shell git rev-parse HEAD 2>/dev/null || echo unknown) +endif +ifndef BUILD_DATETIME +BUILD_DATETIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null) +endif +ifndef GIT_STATE +GIT_STATE := $(shell git diff-index --quiet HEAD -- 2>/dev/null && echo clean || echo dirty) +endif +ifndef BUILD_HOST +BUILD_HOST := $(shell hostname 2>/dev/null) +endif +ifndef BUILD_USER +BUILD_USER := $(shell whoami 2>/dev/null) +endif +ifndef GO_VERSION +GO_VERSION := $(shell $(GO) env GOVERSION 2>/dev/null) +endif +IS_PR_BUILD ?= false + +# GoReleaser configs read these as {{ .Env.* }}; build-release-goreleaser sets +# the same names in CI. +export GIT_STATE BUILD_HOST GO_VERSION BUILD_USER IS_PR_BUILD + +# Go version from go.mod: the toolchain line, else the go line. +ifndef GO_TOOLCHAIN_VERSION +GO_TOOLCHAIN_VERSION := $(shell awk '$$1 == "toolchain" { sub(/^go/, "", $$2); t = $$2 } $$1 == "go" { g = $$2 } END { print (t != "" ? t : g) }' go.mod 2>/dev/null) +endif + +ifndef GO_MODULE +GO_MODULE := $(shell awk '$$1 == "module" { print $$2; exit }' go.mod 2>/dev/null) +endif + +GO_BUILD_FLAGS ?= -v + +# Build info for the version package (OpenCHAMI convention: internal/version +# with these variables). Variables the package doesn't declare are ignored by +# the linker. +GO_VERSION_PACKAGE ?= $(GO_MODULE)/internal/version +GO_LDFLAGS ?= \ + -X '$(GO_VERSION_PACKAGE).Version=$(VERSION)' \ + -X '$(GO_VERSION_PACKAGE).Tag=$(TAG)' \ + -X '$(GO_VERSION_PACKAGE).Branch=$(BRANCH)' \ + -X '$(GO_VERSION_PACKAGE).Commit=$(COMMIT)' \ + -X '$(GO_VERSION_PACKAGE).Date=$(BUILD_DATETIME)' \ + -X '$(GO_VERSION_PACKAGE).GoVersion=$(GO_VERSION)' \ + -X '$(GO_VERSION_PACKAGE).GitState=$(GIT_STATE)' \ + -X '$(GO_VERSION_PACKAGE).BuildHost=$(BUILD_HOST)' \ + -X '$(GO_VERSION_PACKAGE).BuildUser=$(BUILD_USER)' +TEST_TIMEOUT ?= 15m +ARGS ?= + +# Binaries as name:path pairs; default one per cmd/ subdirectory. +BINARIES ?= $(foreach name,$(patsubst cmd/%/,%,$(wildcard cmd/*/)),$(name):./cmd/$(name)) +RUN_BINARY ?= $(firstword $(subst :, ,$(firstword $(BINARIES)))) + +ifndef CONTAINER_ENGINE +CONTAINER_ENGINE := $(or $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null),podman) +endif +# install/uninstall destination (GNU conventions; DESTDIR for staging). +prefix ?= /usr/local +exec_prefix ?= $(prefix) +bindir ?= $(exec_prefix)/bin + +CONTAINERFILE ?= $(firstword $(wildcard Containerfile Dockerfile)) +CONTAINER_TAG ?= latest +CONTAINER_IMAGE_FQCN ?= ghcr.io/openchami/$(PROJECT):$(CONTAINER_TAG) + +# lint-ci runs these images with CONTAINER_ENGINE instead of local installs. +ACTIONLINT_IMAGE ?= docker.io/rhysd/actionlint:1.7.12 +ZIZMOR_IMAGE ?= ghcr.io/zizmorcore/zizmor:1.30.1 + +REUSE_COPYRIGHT ?= OpenCHAMI a Series of LF Projects, LLC +REUSE_LICENSE ?= MIT +ifndef REUSE_YEAR +REUSE_YEAR := $(shell date +%Y 2>/dev/null) +endif + +# Fabrica code generation. lint-codegen-fabrica sets LOCAL_FABRICA and +# FABRICA_SOURCE_ARG. +LOCAL_FABRICA ?= +ifndef FABRICA_VERSION +FABRICA_VERSION := $(shell awk '{ for (i = 1; i < NF; i++) if ($$i == "github.com/openchami/fabrica") { print $$(i + 1); exit } }' go.mod 2>/dev/null) +endif +ifneq ($(strip $(LOCAL_FABRICA)),) +FABRICA_CMD ?= $(LOCAL_FABRICA)/bin/fabrica +FABRICA_SOURCE_ARG ?= --fabrica-source $(LOCAL_FABRICA) +FABRICA_FORCE_FLAG ?= --force +FABRICA_ENV ?= GOTOOLCHAIN=auto +else +FABRICA_CMD ?= $(GO) run github.com/openchami/fabrica/cmd/fabrica@$(FABRICA_VERSION) +endif +USES_FABRICA := $(strip $(LOCAL_FABRICA)$(FABRICA_VERSION)) + +GORELEASER_CONFIG := $(firstword $(wildcard .config/goreleaser.yml .config/goreleaser.yaml .goreleaser.yml .goreleaser.yaml goreleaser.yml goreleaser.yaml)) + +CLEANFILES ?= bin/ dist/ coverage.out coverage.html reuse.spdx + +# --------------------------------------------------------------------------- +# Targets + +.PHONY: help +help: ## List targets + @grep -hE '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}' + +.PHONY: all +all:: deps lint test build ## Install dependencies, lint, test, and build + +.PHONY: deps +deps:: ## Install development dependencies (repos add steps) +ifneq ($(GO_MOD),) + $(call require,$(GO)) + $(GO) mod download + $(GO) mod verify +endif + +.PHONY: fmt +fmt: ## Format Go code +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,$(GO)) + $(GO) fmt ./... +endif + +.PHONY: vet +vet: ## Run go vet +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,$(GO)) + $(GO) vet ./... +endif + +.PHONY: tidy +tidy: ## Tidy go.mod and go.sum +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,$(GO)) + $(GO) mod tidy +endif + +.PHONY: test +test: ## Run unit tests +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,$(GO)) + $(GO) test $(GO_BUILD_FLAGS) -timeout $(TEST_TIMEOUT) -race ./... $(ARGS) +endif + +.PHONY: coverage +coverage: ## Run unit tests with coverage (coverage.out, coverage.html) +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,$(GO)) + $(GO) test $(GO_BUILD_FLAGS) -timeout $(TEST_TIMEOUT) -race -coverprofile=coverage.out -covermode=atomic ./... $(ARGS) + $(GO) tool cover -html=coverage.out -o coverage.html +endif + +.PHONY: lint +lint: ## Run golangci-lint +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,golangci-lint,See https://golangci-lint.run/welcome/install/) + GOTOOLCHAIN=go$(GO_TOOLCHAIN_VERSION) golangci-lint run $(ARGS) +endif + +.PHONY: lint-fix +lint-fix: ## Run golangci-lint with --fix +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,golangci-lint,See https://golangci-lint.run/welcome/install/) + GOTOOLCHAIN=go$(GO_TOOLCHAIN_VERSION) golangci-lint run --fix $(ARGS) +endif + +.PHONY: vuln +vuln: ## Scan Go modules for known vulnerabilities +ifeq ($(GO_MOD),) + $(call skip,no go.mod) +else + $(call require,govulncheck,Install with: go install golang.org/x/vuln/cmd/govulncheck@latest) + govulncheck $(ARGS) ./... +endif + +.PHONY: lint-ci +lint-ci: ## Lint GitHub Actions workflows with actionlint and zizmor (in containers) +ifeq ($(wildcard .github/workflows),) + $(call skip,no .github/workflows) +else + $(call require,$(CONTAINER_ENGINE),Install podman or docker.) + $(CONTAINER_ENGINE) run --rm --user 0:0 -v "$(CURDIR):/repo:ro,z" -w /repo $(ACTIONLINT_IMAGE) -color + $(CONTAINER_ENGINE) run --rm --user 0:0 -v "$(CURDIR):/repo:ro,z" -w /repo -e GH_TOKEN $(ZIZMOR_IMAGE) . +endif + +.PHONY: build +build:: ## Build BINARIES into bin/ +ifeq ($(strip $(BINARIES)),) + $(call skip,no BINARIES) +else + $(call require,$(GO)) + @for b in $(BINARIES); do \ + name=$${b%%:*}; path=$${b#*:}; \ + echo "$(GO) build $(GO_BUILD_FLAGS) -o bin/$$name $$path"; \ + $(GO) build $(GO_BUILD_FLAGS) -ldflags "$(GO_LDFLAGS)" -o bin/$$name $$path || exit 1; \ + done +endif + +.PHONY: run +run:: build ## Build and run RUN_BINARY +ifeq ($(strip $(RUN_BINARY)),) + $(call skip,no RUN_BINARY) +else + ./bin/$(RUN_BINARY) +endif + +.PHONY: install +install:: ## Install built BINARIES into $(DESTDIR)$(bindir) (run make build first) +ifeq ($(strip $(BINARIES)),) + $(call skip,no BINARIES) +else + @for b in $(BINARIES); do \ + name=$${b%%:*}; \ + [ -f bin/$$name ] || { echo "$@: bin/$$name not found; run make build first" >&2; exit 1; }; \ + done + install -d $(DESTDIR)$(bindir) + @for b in $(BINARIES); do \ + name=$${b%%:*}; \ + echo "install -m 0755 bin/$$name $(DESTDIR)$(bindir)/$$name"; \ + install -m 0755 bin/$$name $(DESTDIR)$(bindir)/$$name || exit 1; \ + done +endif + +.PHONY: uninstall +uninstall:: ## Remove BINARIES from $(DESTDIR)$(bindir) +ifeq ($(strip $(BINARIES)),) + $(call skip,no BINARIES) +else + @for b in $(BINARIES); do \ + name=$${b%%:*}; \ + echo "rm -f $(DESTDIR)$(bindir)/$$name"; \ + rm -f $(DESTDIR)$(bindir)/$$name; \ + done +endif + +.PHONY: generate +generate: ## Regenerate Fabrica code +ifeq ($(USES_FABRICA),) + $(call skip,Fabrica not in go.mod and LOCAL_FABRICA unset) +else +ifneq ($(strip $(LOCAL_FABRICA)),) + @if [ ! -x $(LOCAL_FABRICA)/bin/fabrica ]; then \ + echo "Local Fabrica binary not found at $(LOCAL_FABRICA)/bin/fabrica" >&2; \ + echo "Build it with: (cd $(LOCAL_FABRICA) && go build -o bin/fabrica ./cmd/fabrica)" >&2; \ + exit 1; \ + fi +else + $(call require,$(GO)) +endif + $(FABRICA_ENV) $(FABRICA_CMD) generate $(FABRICA_SOURCE_ARG) $(FABRICA_FORCE_FLAG) +endif + +.PHONY: generate-check +generate-check: ## Fail if generated Fabrica code is out of date (exit 2: dirty tree, 1: out of date) +ifeq ($(USES_FABRICA),) + $(call skip,Fabrica not in go.mod and LOCAL_FABRICA unset) +else + $(call require,git) + @if [ -n "$$(git status --porcelain)" ]; then \ + echo "Working tree must be clean before running generate-check."; \ + echo "Commit or stash local changes, then re-run make generate-check."; \ + exit 2; \ + fi + @$(MAKE) --no-print-directory generate LOCAL_FABRICA="$(LOCAL_FABRICA)" + @if [ -n "$$(git status --porcelain)" ]; then \ + echo "Generated files are out of date. Run 'make generate' and commit results."; \ + git --no-pager status --short; \ + exit 1; \ + fi +endif + +.PHONY: container-build +container-build: ## Build the container image (CONTAINER_IMAGE_FQCN) +ifeq ($(strip $(CONTAINERFILE)),) + $(call skip,no Containerfile or Dockerfile) +else + $(call require,$(CONTAINER_ENGINE),Install podman or docker.) + $(CONTAINER_ENGINE) build -f $(CONTAINERFILE) \ + --build-arg GO_VERSION=$(GO_TOOLCHAIN_VERSION) \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + --build-arg DATE=$(BUILD_DATETIME) \ + -t $(CONTAINER_IMAGE_FQCN) . +endif + +.PHONY: container-run +container-run: container-build ## Build and run the container image +ifeq ($(strip $(CONTAINERFILE)),) + $(call skip,no Containerfile or Dockerfile) +else + $(CONTAINER_ENGINE) run --rm $(CONTAINER_IMAGE_FQCN) +endif + +.PHONY: release-snapshot +release-snapshot: ## Full local GoReleaser dry run (binaries, packages, images) +ifeq ($(GORELEASER_CONFIG),) + @echo "$@: no GoReleaser config found (.goreleaser.yaml or equivalent)" >&2; exit 1 +else + $(call require,goreleaser,See https://goreleaser.com/install/) + goreleaser release --snapshot --clean $(ARGS) +endif + +.PHONY: build-packages +build-packages: ## Build all packages with GoReleaser (no images or archives) +ifeq ($(GORELEASER_CONFIG),) + @echo "$@: no GoReleaser config found (.goreleaser.yaml or equivalent)" >&2; exit 1 +else + $(call require,goreleaser,See https://goreleaser.com/install/) + goreleaser release --snapshot --clean --skip=docker,archive $(ARGS) +endif + +.PHONY: reuse-check +reuse-check: ## Check REUSE compliance + $(call require,reuse,See https://reuse.software/) + reuse lint --lines + +.PHONY: reuse-annotate +reuse-annotate: ## Add missing SPDX headers (never rewrites existing ones) + $(call require,reuse,See https://reuse.software/) + reuse annotate --copyright="$(REUSE_COPYRIGHT)" --copyright-prefix=spdx-symbol \ + --license=$(REUSE_LICENSE) --year=$(REUSE_YEAR) \ + --skip-existing --fallback-dot-license --recursive . + +.PHONY: reuse-download-license +reuse-download-license: ## Download a license text (LICENSE=MIT) + @if [ -z "$(LICENSE)" ]; then echo "Usage: make $@ LICENSE=" >&2; exit 1; fi + $(call require,reuse,See https://reuse.software/) + reuse download $(LICENSE) + +.PHONY: reuse-spdx +reuse-spdx: ## Write an SPDX bill of materials (reuse.spdx) + $(call require,reuse,See https://reuse.software/) + reuse spdx -o reuse.spdx + +.PHONY: clean +clean: ## Remove CLEANFILES and the Go build cache + rm -rf $(CLEANFILES) +ifneq ($(GO_MOD),) + $(call require,$(GO)) + $(GO) clean -cache +endif diff --git a/README.md b/README.md index 934271d..2b5e464 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Reusable GitHub Actions for CI/CD. - `.github/workflows/validate-packages.yml`: Reusable workflow that validates rpm/deb packages (file list, signature, install, lint) - `.github/workflows/stale.yml`: Reusable workflow that marks and closes inactive issues and PRs (org-default policy) - `.github/workflows/lint-codegen-fabrica.yml`: Reusable workflow that fails when a Fabrica project's committed generated code is out of date +- `.github/workflows/lint-makefile-common.yml`: Reusable workflow that fails when a repo's vendored `Makefile.common` differs from the latest release +- `Makefile.common`: Shared Makefile targets that OpenCHAMI repos vendor and include ### Workflow naming @@ -225,7 +227,7 @@ jobs: ``` ### lint-go (Reusable Workflow) -Lints the caller's Go module. Runs `golangci-lint`, and separately verifies `go.mod`/`go.sum` are tidy by running the tidy command and failing on a dirty diff. Uses Go `stable` unless the caller sets `go-version` or `go-version-file`. `golangci-lint` tracks `latest` unless pinned. +Lints the caller's Go module. Runs `golangci-lint`, and separately verifies `go.mod`/`go.sum` are tidy by running the tidy command and failing on a dirty diff. Uses Go `stable` unless the caller sets `go-version` or `go-version-file`. `golangci-lint` is pinned (Renovate-managed); override with `golangci-lint-version`. **Usage:** ```yaml @@ -240,7 +242,7 @@ jobs: uses: OpenCHAMI/github-actions/.github/workflows/lint-go.yml@v4.0 # Optional overrides: # with: - # golangci-lint-version: v2.13.2 + # golangci-lint-version: v2.14.0 # go-version-file: go.mod # runner: ubuntu-latest ``` @@ -585,6 +587,66 @@ jobs: # config-file: renovate.json ``` +### lint-makefile-common (Reusable Workflow) +Fails when the repo's vendored `Makefile.common` differs from the copy in the latest release of this repository (read at the release's commit SHA), and prints the diff. Takes no inputs. Make it a required check only if drift should block merging. + +**Usage:** +```yaml +name: Lint Makefile.common +on: + pull_request: + schedule: + - cron: '0 6 * * 1' # catch new releases + +jobs: + lint-makefile-common: + uses: OpenCHAMI/github-actions/.github/workflows/lint-makefile-common.yml@v4.0 +``` + +## Makefile.common + +Shared Makefile targets for OpenCHAMI repositories. Copy [`Makefile.common`](Makefile.common) from the latest release to your repo root, don't edit it, and include it from your `Makefile`: + +```make +BINARIES := foo-server:./cmd/server # overrides go before the include +include Makefile.common + +migrate: # repo-only targets go below + ./scripts/migrate.sh +``` + +- `make help` lists every target, including your own (give them a `## description`). +- Covers Go (build, test, lint, vuln), Fabrica codegen, containers, GoReleaser packages, and REUSE. +- `lint-ci` runs actionlint and zizmor from containers (`ACTIONLINT_IMAGE`, `ZIZMOR_IMAGE`) with podman or docker; nothing is installed. Passes `GH_TOKEN` through for zizmor's online audits. Not part of `all`. +- `all`, `deps`, `build`, `run`, `install`, and `uninstall` are double-colon rules: add your own `deps::` (etc.) and both recipes run. +- `install` / `uninstall` put already-built `BINARIES` (`make build` first) in `$(DESTDIR)$(bindir)` (default `/usr/local/bin`; override `prefix`, `bindir`, `DESTDIR`). +- Pass extra arguments with `ARGS` (`make test ARGS="-run TestFoo"`). +- Targets that don't apply skip: Go targets without `go.mod`, `generate` without Fabrica, container targets without a `Containerfile` or `Dockerfile`. `release-snapshot` and `build-packages` fail without a GoReleaser config. +- Tools beyond coreutils and POSIX utilities are checked before use; the file installs nothing. +- GNU make 3.81 or newer (macOS's `/usr/bin/make` works). + +Common overrides: `BINARIES` (default: one `name:./cmd/name` per `cmd/` directory), `CONTAINERFILE`, `CONTAINER_IMAGE_FQCN`, `TEST_TIMEOUT`, `CLEANFILES` (append with `+=`), `GO_LDFLAGS` (append with `+=`; default sets `$(GO_MODULE)/internal/version.*` build info), `GO_VERSION_PACKAGE`. See the variables section at the top of the file for the rest. For local `build-packages` with a GoReleaser config that signs packages (`key_file: "{{ .Env.GPG_KEY_PATH }}"`), run `make build-packages GPG_KEY_PATH=` to build unsigned. + +Release binaries get their build info from GoReleaser, not `make build`. Set the same `internal/version` variables in your GoReleaser config so both match: + +```yaml +builds: + - ldflags: + - -X {{ .ModulePath }}/internal/version.Version={{ .Version }} + - -X {{ .ModulePath }}/internal/version.Tag={{ .Tag }} + - -X {{ .ModulePath }}/internal/version.Branch={{ .Branch }} + - -X {{ .ModulePath }}/internal/version.Commit={{ .FullCommit }} + - -X {{ .ModulePath }}/internal/version.Date={{ .Date }} + - -X {{ .ModulePath }}/internal/version.GoVersion={{ envOrDefault "GO_VERSION" "unknown" }} + - -X {{ .ModulePath }}/internal/version.GitState={{ envOrDefault "GIT_STATE" "unknown" }} + - -X {{ .ModulePath }}/internal/version.BuildHost={{ envOrDefault "BUILD_HOST" "unknown" }} + - -X {{ .ModulePath }}/internal/version.BuildUser={{ envOrDefault "BUILD_USER" "unknown" }} +``` + +`build-release-goreleaser` and `Makefile.common` export `GO_VERSION`, `GIT_STATE`, `BUILD_HOST`, and `BUILD_USER`. + +Add [`lint-makefile-common`](#lint-makefile-common-reusable-workflow) to catch drift. + ## Actions ### gpg-configure-release-keys (Deprecated) diff --git a/renovate.json b/renovate.json index bb200d9..4272363 100644 --- a/renovate.json +++ b/renovate.json @@ -27,6 +27,60 @@ "default: '(?docker\\.io/[^:']+):(?[^@']+)@(?sha256:[a-f0-9]{64})'" ], "datasourceTemplate": "docker" + }, + { + "customType": "regex", + "description": "Versions annotated with a preceding '# renovate: datasource=... depName=...' comment", + "managerFilePatterns": [ + "/^\\.github/workflows/[^/]+\\.ya?ml$/", + "/^actions/.+/action\\.ya?ml$/" + ], + "matchStrings": [ + "#\\s*renovate: datasource=(?\\S+) depName=(?\\S+)\\s*\\n\\s*[\\w-]+: '?(?[^'\\s]+)'?" + ] + }, + { + "customType": "regex", + "description": "Version examples in the README", + "managerFilePatterns": ["/^README\\.md$/"], + "matchStrings": [ + "#\\s+reuse-version: (?\\d+\\.\\d+\\.\\d+)", + "Pins `reuse` (?\\d+\\.\\d+\\.\\d+)" + ], + "depNameTemplate": "reuse", + "datasourceTemplate": "pypi" + }, + { + "customType": "regex", + "description": "Version examples in the README", + "managerFilePatterns": ["/^README\\.md$/"], + "matchStrings": ["#\\s+goreleaser-version: (?v\\d+\\.\\d+\\.\\d+)"], + "depNameTemplate": "goreleaser/goreleaser", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "description": "Version examples in the README", + "managerFilePatterns": ["/^README\\.md$/"], + "matchStrings": ["#\\s+golangci-lint-version: (?v\\d+\\.\\d+\\.\\d+)"], + "depNameTemplate": "golangci/golangci-lint", + "datasourceTemplate": "github-releases" + }, + { + "customType": "regex", + "description": "Container images for make lint-ci", + "managerFilePatterns": ["/^Makefile\\.common$/"], + "matchStrings": ["[A-Z_]+_IMAGE \\?= (?[^:\\s]+):(?[^\\s@]+)"], + "datasourceTemplate": "docker" + }, + { + "customType": "regex", + "description": "check-key-expiry.sh pinned to a gpg-signing-manager commit", + "managerFilePatterns": ["/^actions/gpg-check-key-expiration/action\\.ya?ml$/"], + "matchStrings": ["raw\\.githubusercontent\\.com/OpenCHAMI/gpg-signing-manager/(?[a-f0-9]{40})/"], + "depNameTemplate": "https://github.com/OpenCHAMI/gpg-signing-manager", + "currentValueTemplate": "main", + "datasourceTemplate": "git-refs" } ] }