From ca58c448ab5f2f5fbceda754a681ea66e299e945 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 3 Sep 2026 18:43:52 +0300 Subject: [PATCH 1/2] Taskfile: added the common tasks every Go repository includes --- .github/scripts/generate-versions | 32 ++++++ .github/workflows/ci.yml | 15 ++- .github/workflows/versions.yml | 8 +- Taskfile.yml | 166 ++++++++++++++++++++++++++++++ 4 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 Taskfile.yml diff --git a/.github/scripts/generate-versions b/.github/scripts/generate-versions index 2ed9c8f..0bc04dd 100755 --- a/.github/scripts/generate-versions +++ b/.github/scripts/generate-versions @@ -484,3 +484,35 @@ HEADER } >"${out}" echo "wrote ${out}" >&2 + +# --- Taskfile.yml + +# The shared Taskfile bakes the pins into its vars block, so a repository that +# includes it needs no second download. A key becomes a var name by dropping +# any go: prefix and the module path, keeping the last segment upper-cased with +# dashes as underscores: go:golang.org/x/tools/cmd/goimports is GOIMPORTS, +# golangci-lint is GOLANGCI_LINT. Only the block between the markers is +# rewritten; the rest of the file is hand-written. +taskfile="${TASKFILE:-${root}/Taskfile.yml}" + +jq -r ' + to_entries + | sort_by(.key) + | .[] + | (.key | sub("^go:"; "") | split("/") | .[-1] | ascii_upcase | gsub("-"; "_")) as $name + | " \($name): \(.value.version)" +' "${releases}" >"${work}/taskvars" + +awk -v vars="${work}/taskvars" ' + /# BEGIN generated/ { + print + while ((getline line < vars) > 0) print line + skipping = 1 + next + } + /# END generated/ { skipping = 0 } + !skipping { print } +' "${taskfile}" >"${work}/Taskfile.yml" +mv -- "${work}/Taskfile.yml" "${taskfile}" + +echo "wrote ${taskfile}" >&2 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 907dc3a..10750e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,18 +51,31 @@ jobs: go-version: ${{ steps.pins.outputs.go }} check-latest: false - - name: Check .versions agrees with .versions.json + - name: Check .versions and Taskfile.yml agree with .versions.json shell: bash run: | set -euo pipefail cp .versions "${RUNNER_TEMP}/versions.committed" + cp Taskfile.yml "${RUNNER_TEMP}/Taskfile.committed" .github/scripts/generate-versions --no-refresh if ! diff -u "${RUNNER_TEMP}/versions.committed" .versions; then echo "::error::.versions is not what .versions.json flattens to; run .github/scripts/generate-versions" exit 1 fi + if ! diff -u "${RUNNER_TEMP}/Taskfile.committed" Taskfile.yml; then + echo "::error::Taskfile.yml does not carry the pins in .versions.json; run .github/scripts/generate-versions" + exit 1 + fi + + - name: Check the shared Taskfile parses and lists its tasks + shell: bash + run: | + set -euo pipefail + mise install task@3.53.1 + mise exec task@3.53.1 -- task --list-all + mise exec task@3.53.1 -- task pins - name: Setup mise uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 diff --git a/.github/workflows/versions.yml b/.github/workflows/versions.yml index 9931c0a..8aab7ca 100644 --- a/.github/workflows/versions.yml +++ b/.github/workflows/versions.yml @@ -34,14 +34,14 @@ jobs: run: | set -euo pipefail - if git diff --quiet -- .versions .versions.json; then + if git diff --quiet -- .versions .versions.json Taskfile.yml; then echo "found=false" >> "${GITHUB_OUTPUT}" echo "already up to date" exit 0 fi echo "found=true" >> "${GITHUB_OUTPUT}" - git --no-pager diff -- .versions .versions.json + git --no-pager diff -- .versions .versions.json Taskfile.yml - name: Setup the SSH signing key if: steps.changes.outputs.found == 'true' @@ -74,7 +74,7 @@ jobs: exit 1 fi - if git diff --quiet FETCH_HEAD -- .versions .versions.json; then + if git diff --quiet FETCH_HEAD -- .versions .versions.json Taskfile.yml; then echo "${BRANCH} already carries these versions" exit 0 fi @@ -138,7 +138,7 @@ jobs: git config gpg.format ssh git switch --create "${BRANCH}" - git add .versions .versions.json + git add .versions .versions.json Taskfile.yml git commit --message "${title}" git push --force "${remote}" "${BRANCH}" diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..c18a2f9 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,166 @@ +# The tasks every Go repository at Smallstep shares. A repository includes this +# file from its own Taskfile.yml under a namespace of its choosing: +# +# includes: +# common: +# taskfile: https://raw.githubusercontent.com/smallstep/workflows/main/Taskfile.yml +# +# There are two layers. The runners (goimports, gotestsum, govulncheck and +# golangci-lint) run one tool at the version .versions pins, with whatever +# arguments the caller passes. The stock tasks (fmt, lint, test and vuln) are +# the defaults most repositories want, built on the runners. A repository's own +# Taskfile.yml delegates to a stock task where the default fits and writes its +# own on top of a runner where it does not; nothing here owns a repository's +# fmt, lint or test. +# +# goimports, gotestsum and govulncheck run through `go run @`, +# so they are the pinned version by construction. golangci-lint is the +# exception: it breaks when built from source, so it is expected on PATH at +# exactly the pinned version, and the runner stops when it is not. Go itself is +# not pinned or enforced here: `check` reports the version found against the +# pin, and a repository's go.mod governs its toolchain. +# +# Every task body runs through Task's own POSIX shell on every platform, so +# nothing here may assume bash, Unix-only tools or /-rooted paths. +version: "3" + +vars: + # BEGIN generated by .github/scripts/generate-versions; DO NOT EDIT + GO: 1.27.0 + GOIMPORTS: 0.49.0 + GOPLS: 0.23.0 + GOVULNCHECK: 1.7.0 + GOLANGCI_LINT: 2.13.1 + GOLANGCI_LINT_LANGSERVER: 0.0.12 + GOTESTSUM: 1.13.0 + # END generated + + # The defaults a repository may override through its include's vars. + GOIMPORTS_LOCAL: go.step.sm/,github.com/smallstep/ + TEST_FLAGS: -race -coverpkg=./... -coverprofile=coverage.out -covermode=atomic ./... + LINT_FLAGS: --timeout=30m + GOLANGCI_CONFIG_URL: https://raw.githubusercontent.com/smallstep/workflows/main/.golangci.yml + +tasks: + pins: + desc: print the tool versions this Taskfile pins + silent: true + cmds: + - echo "go {{.GO}}" + - echo "golangci-lint {{.GOLANGCI_LINT}}" + - echo "goimports {{.GOIMPORTS}}" + - echo "gotestsum {{.GOTESTSUM}}" + - echo "govulncheck {{.GOVULNCHECK}}" + + check: + desc: say which tools on PATH are not the versions smallstep pins + silent: true + cmds: + - | + found="$(go env GOVERSION)" + [ "$found" = "go{{.GO}}" ] || + echo "go: $found on PATH; smallstep builds with go{{.GO}}" + - | + if command -v golangci-lint >/dev/null 2>&1; then + found="$(golangci-lint version --short 2>/dev/null)" + [ "$found" = "{{.GOLANGCI_LINT}}" ] || + echo "golangci-lint: $found on PATH; smallstep pins {{.GOLANGCI_LINT}}" + fi + - | + if command -v gotestsum >/dev/null 2>&1; then + found="$(gotestsum --version 2>/dev/null)" + found="${found##* }" + found="${found#v}" + [ "$found" = "{{.GOTESTSUM}}" ] || + echo "gotestsum: $found on PATH; smallstep pins {{.GOTESTSUM}} (tasks run the pinned one)" + fi + + # --- runners: one tool each, at the pinned version, with the caller's args + + goimports: + desc: run the pinned goimports with the given arguments + cmds: + - go run golang.org/x/tools/cmd/goimports@v{{.GOIMPORTS}} {{.ARGS}} {{.CLI_ARGS}} + + gotestsum: + desc: run the pinned gotestsum with the given arguments + cmds: + - go run gotest.tools/gotestsum@v{{.GOTESTSUM}} {{.ARGS}} {{.CLI_ARGS}} + + govulncheck: + desc: run the pinned govulncheck with the given arguments + cmds: + - go run golang.org/x/vuln/cmd/govulncheck@v{{.GOVULNCHECK}} {{.ARGS}} {{.CLI_ARGS}} + + golangci-lint: + desc: run the golangci-lint on PATH, which must be the pinned version + cmds: + - cmd: | + if ! command -v golangci-lint >/dev/null 2>&1; then + echo "golangci-lint is not on PATH; smallstep pins {{.GOLANGCI_LINT}}:" >&2 + echo " mise use -g golangci-lint@{{.GOLANGCI_LINT}}" >&2 + echo " https://github.com/golangci/golangci-lint/releases/tag/v{{.GOLANGCI_LINT}}" >&2 + exit 1 + fi + found="$(golangci-lint version --short 2>/dev/null)" + if [ "$found" != "{{.GOLANGCI_LINT}}" ]; then + echo "golangci-lint $found is on PATH; smallstep pins {{.GOLANGCI_LINT}}:" >&2 + echo " mise use -g golangci-lint@{{.GOLANGCI_LINT}}" >&2 + echo " https://github.com/golangci/golangci-lint/releases/tag/v{{.GOLANGCI_LINT}}" >&2 + exit 1 + fi + silent: true + - golangci-lint {{.ARGS}} {{.CLI_ARGS}} + + # --- stock tasks: the defaults, built on the runners + + fmt: + desc: format the Go sources with the pinned goimports and smallstep's import groups + cmds: + - task: goimports + vars: + ARGS: -local {{.GOIMPORTS_LOCAL}} -l -w . + + lint: + desc: lint with the pinned golangci-lint, using the repository's own config or smallstep's shared one + deps: [check] + vars: + OWN_CONFIG: + sh: | + for f in .golangci.yml .golangci.yaml .golangci.toml .golangci.json; do + if [ -f "$f" ]; then + echo "$f" + break + fi + done + cmds: + # The shared config is fetched fresh, and the last copy serves when the + # network is away; a repository with a config of its own uses that, the + # same rule the goLint workflow applies. + - cmd: | + if [ -n "{{.OWN_CONFIG}}" ]; then + exit 0 + fi + mkdir -p .task + curl -fsSL -o .task/golangci.yml.tmp {{.GOLANGCI_CONFIG_URL}} && + mv .task/golangci.yml.tmp .task/golangci.yml || + [ -f .task/golangci.yml ] + silent: true + - task: golangci-lint + vars: + ARGS: run {{if .OWN_CONFIG}}{{else}}--config .task/golangci.yml{{end}} {{.LINT_FLAGS}} + + test: + desc: run the tests with the pinned gotestsum + deps: [check] + cmds: + - task: gotestsum + vars: + ARGS: -- {{.TEST_FLAGS}} + + vuln: + desc: scan the module with the pinned govulncheck + cmds: + - task: govulncheck + vars: + ARGS: ./... From 99eb0d14d04a3e2712b73c026523398a1d9b0d76 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 3 Sep 2026 18:48:24 +0300 Subject: [PATCH 2/2] ci: checked the shared Taskfile after mise is set up --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10750e0..003505a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,6 +69,11 @@ jobs: exit 1 fi + - name: Setup mise + uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 + with: + install: false + - name: Check the shared Taskfile parses and lists its tasks shell: bash run: | @@ -77,11 +82,6 @@ jobs: mise exec task@3.53.1 -- task --list-all mise exec task@3.53.1 -- task pins - - name: Setup mise - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0 - with: - install: false - # NOTE(@azazeal): .versions doubles as a .tool-versions, so we check # whether mise can resolve - name: Check mise resolves every pin