Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .copier-answers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Managed by copier — do not edit by hand. `uvx copier update --trust`
_commit: a676cfe
_src_path: gh:jacaudi/template
build_context: .
extra_build_contexts: []
has_chart: false
image_name: ghcr.io/jacaudi/wireguard-operator/manager
integration_kind: envtest
lang_go: true
lang_node: false
lang_python: false
repo_name: wireguard-operator
variant: service

170 changes: 170 additions & 0 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# Installs the language toolchains and lint tools a repo needs, plus Task.
# THE tool-version pin for the whole fleet: ci-lint.yml and `task <lang>:ci` both
# invoke bare binaries, so the versions below are the single source of truth.
name: Set up toolchains
description: Install the language toolchains and lint tools present in this repo, plus Task.

inputs:
repo-token:
description: GITHUB_TOKEN — an input because `secrets` is unavailable inside a composite action.
required: false
default: ''

# The single reason ci-lint.yml needs no GO_DIRS script: a composite action's
# `steps.*.outputs` do NOT escape without this block, so detect.sh's already-
# computed values — including the SHALLOWEST go.mod, which shallowest() exists
# to find because nested examples/*/go.mod modules occur in this fleet — were
# unreachable to every caller. Callers read them as steps.<id>.outputs.<name>.
outputs:
# PUBLISH ONLY WHAT IS CONSUMED. Six were declared originally; after all seven
# stages were built, `go` was the only one any of them read (ci-lint.yml x5),
# so the rest were cut. Adding an output back later is ADDITIVE — a stage that
# does not read it is unaffected — while removing one breaks every stage that
# does. That asymmetry says publish on demand.
#
# `godir` is that demand, and it closes a real defect rather than anticipating
# one. `gomod` drives setup-go's `go-version-file` below, but setup-go only
# installs the TOOLCHAIN; it does not change directory. So in a repo whose
# module is at svc/go.mod, every Go command ci-lint.yml ran afterwards ran in
# the repo root against no module — `go mod tidy -diff` reporting `go.mod file
# not found in current directory or any parent directory`, `go test ./...`
# reporting `directory prefix . does not contain main module`. ci-lint.yml
# now takes this as each Go step's `working-directory:`.
go:
description: 'true when the repo contains a Go module'
value: ${{ steps.d.outputs.go }}
godir:
description: 'directory holding the shallowest go.mod, or "." — the working-directory for module-root Go commands'
value: ${{ steps.d.outputs.godir }}

runs:
using: composite
steps:
# Detection is a shell step, not inline hashFiles(), for two reasons found by
# running it against real repos:
# - NOTHING is guaranteed to be at the repo root, Go included. A root-only
# check silently skips setup for a module in a subdirectory.
# - `**/Dockerfile*` matches junk like <tool-dir>/Dockerfile.json, which
# would install hadolint and then lint a JSON file.
# Detection lives in detect.sh so tests/detect-test.sh exercises the SAME
# code that ships here. Three fail-silent find traps are documented there.
# `env:`, not direct interpolation. `github.action_path` is the runner's own
# value and not attacker-controlled, but scripts/run-interpolation.sh admits
# no exceptions — and actionlint cannot lint a composite action at all, so
# that gate is the only static check this block gets.
- name: Detect repo shape
id: d
shell: bash
env:
ACTION_PATH: ${{ github.action_path }}
run: |
set -euo pipefail
out=$(bash "${ACTION_PATH}/detect.sh")
printf '%s\n' "${out}"
printf '%s\n' "${out}" >> "$GITHUB_OUTPUT"

- name: Set up Go
if: ${{ steps.d.outputs.go == 'true' }}
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: ${{ steps.d.outputs.gomod }}

# A lockfile must exist or setup-node throws on an empty cache hash.
- name: Set up Node
if: ${{ steps.d.outputs.node == 'true' && steps.d.outputs.nodelock == 'true' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: '**/package-lock.json'

- name: Set up Node (no lockfile — cache disabled)
if: ${{ steps.d.outputs.node == 'true' && steps.d.outputs.nodelock != 'true' }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: .node-version

- name: Set up Python (uv)
if: ${{ steps.d.outputs.python == 'true' }}
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
enable-cache: true

- name: Set up Task
uses: go-task/setup-task@01a4adf9db2d14c1de7a560f09170b6e0df736aa # v2.1.0
with:
version: 3.x
repo-token: ${{ inputs.repo-token }}

# NONE of the linters below ship on ubuntu-latest — `task ci` cannot pass
# without them. Pinned release tarballs, not a piped install script.
# yamllint IS preinstalled; do not reach for it through uvx (uv is not).
- name: Install actionlint
if: ${{ steps.d.outputs.workflows == 'true' }}
shell: bash
env:
# renovate: datasource=github-releases depName=rhysd/actionlint
VERSION: 1.7.12
run: |
set -euo pipefail
curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${VERSION}/actionlint_${VERSION}_linux_amd64.tar.gz" \
| sudo tar xz -C /usr/local/bin actionlint
actionlint --version

- name: Install golangci-lint
if: ${{ steps.d.outputs.go == 'true' }}
shell: bash
env:
# renovate: datasource=github-releases depName=golangci/golangci-lint
VERSION: 2.12.2
run: |
set -euo pipefail
curl -sSfL "https://github.com/golangci/golangci-lint/releases/download/v${VERSION}/golangci-lint-${VERSION}-linux-amd64.tar.gz" \
| sudo tar xz --strip-components=1 -C /usr/local/bin "golangci-lint-${VERSION}-linux-amd64/golangci-lint"
golangci-lint --version

- name: Install govulncheck
if: ${{ steps.d.outputs.go == 'true' }}
shell: bash
env:
# renovate: datasource=go depName=golang.org/x/vuln
VERSION: v1.1.4
run: go install "golang.org/x/vuln/cmd/govulncheck@${VERSION}"

- name: Install hadolint
if: ${{ steps.d.outputs.docker == 'true' }}
shell: bash
env:
# renovate: datasource=github-releases depName=hadolint/hadolint
VERSION: 2.15.1
run: |
set -euo pipefail
sudo curl -sSfLo /usr/local/bin/hadolint \
"https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64"
sudo chmod +x /usr/local/bin/hadolint
hadolint --version

# THE Python tool pin, for the whole repo. The stock template pinned ruff
# inside .taskfiles/python.yml instead — invisible to CI, and under deviation
# D1 CI does not read the taskfile at all, so the two would silently diverge.
# Pinning here means ci-lint.yml and `task python:ci` run the SAME ruff.
#
# `uv tool install` puts them on PATH, so both callers invoke a bare `ruff` /
# `ty` with no version string at the call site — which is what keeps the pin
# single-sourced.
- name: Install ruff and ty
if: ${{ steps.d.outputs.python == 'true' }}
shell: bash
env:
# renovate: datasource=pypi depName=ruff
RUFF_VERSION: 0.16.2
# renovate: datasource=pypi depName=ty
# Pre-1.0 and moving fast; pinned exactly so a release cannot turn the
# fleet red overnight with nothing to bisect against.
TY_VERSION: 0.0.70
run: |
set -euo pipefail
uv tool install "ruff==${RUFF_VERSION}"
uv tool install "ty==${TY_VERSION}"
ruff --version
ty --version
107 changes: 107 additions & 0 deletions .github/actions/setup/detect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Detect which languages and artefacts a repo contains. Prints key=value lines.
#
# Shared by action.yml (which appends this to $GITHUB_OUTPUT) and by
# tests/detect-test.sh. ONE implementation so the test cannot drift from what
# actually ships.
#
# Usage: detect.sh [DIR] (defaults to $PWD)
#
# THREE TRAPS ARE ENCODED HERE, each of which returns a confidently empty or
# wrong answer rather than erroring:
#
# 1. `-mindepth 1` is REQUIRED. The basename of the starting point `.` matches
# the `.*` prune glob, so without it find prunes the entire tree and prints
# nothing — every language silently undetected, in every repo.
#
# 2. The Dockerfile extension filter is anchored to `/Dockerfile.`. A blanket
# `\.(json|md|txt)$` also strips every package.json, so Node can never be
# detected.
#
# 3. The caller's expression is wrapped in \( \) inside find_real. A bare `-o`
# binds looser than `-type f` and the `-prune` clause and discards both.
#
# THE DOCKERFILE GLOB IS NOT WRITTEN HERE. It lives in scripts/dockerfile-list.sh,
# which ci-lint.yml's hadolint step reads too. Trap 1's `.*` prune is right for
# every other language marker and WRONG for Dockerfiles, and while this file
# owned its own copy of the glob the two callers disagreed about exactly that: a
# repo with .devcontainer/Dockerfile got docker=false here, so hadolint was never
# installed, while ci-lint.yml found the file anyway and died with exit 127.
set -euo pipefail

# Resolved BEFORE the cd below, which moves us into the tree under inspection.
# Fail loudly rather than reporting docker=false: a missing capability that
# answers "no Dockerfile" is the fail-silent shape this header already documents
# three instances of.
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
dockerfile_list="${here}/../../../scripts/dockerfile-list.sh"
[ -f "${dockerfile_list}" ] || {
echo "detect.sh: cannot find ${dockerfile_list}" >&2
exit 1
}

cd "${1:-$PWD}"

# `testdata` is in the list for a reason the other entries do not share: the Go
# toolchain itself defines it as excluded from the build, so a Go repo's
# testdata/**/package.json is a FIXTURE, not this repo's Node project. Detected
# as one, node=true made the setup action run actions/setup-node with
# `node-version-file: .node-version` — a file copier never wrote, because the
# repo answered lang_node=false — and setup-node fails outright on an absent
# version file. The whole job died before a single linter ran.
#
# The same gap existed in scripts/discover-dirs.sh, chart-list.sh and
# dockerfile-list.sh, which carry this prune list for their own callers. Keep
# the four in step.
pruned=(-name node_modules -o -name vendor -o -name .git -o -name .worktrees -o -name testdata -o -name '.*')
find_real() {
find . -mindepth 1 \( "${pruned[@]}" \) -prune -o -type f \( "$@" \) -print 2>/dev/null
}

# SHALLOWEST path wins, not the lexically first. A repo with example or test
# modules (examples/analytics/go.mod) would otherwise beat the real ./go.mod,
# because "./e" sorts before "./g" — and go-version-file would then point at a
# nested module's Go directive.
shallowest() { awk '{ n = gsub("/", "/"); print n, $0 }' | sort -k1,1n -k2,2 | head -1 | cut -d' ' -f2-; }

gomod=$(find_real -name go.mod | shallowest || true)
node=$(find_real -name package.json | sort | head -1 || true)
lock=$(find_real -name package-lock.json | sort | head -1 || true)
py=$(find_real -name pyproject.toml -o -name requirements.txt | sort | head -1 || true)
# NOT find_real. The shared list already emits a sorted answer, and it prunes
# the vendored trees by name instead of pruning every dot-directory — which is
# what lets a .devcontainer/Dockerfile be seen here and by ci-lint.yml alike.
# Containerfile, the OCI/Podman spelling, and the anchored extension filter both
# live there; the reasons they are not optional are in that script's header.
docker=$(sh "${dockerfile_list}" | head -1 || true)
wf=$(find .github/workflows -maxdepth 1 -type f \( -name '*.yml' -o -name '*.yaml' \) 2>/dev/null | head -1 || true)

b() { [ -n "$1" ] && echo true || echo false; }

# EVERY KEY BELOW HAS A CONSUMER, and that is the rule this list is held to —
# action.yml's `outputs:` block and its five `if:` guards are the only readers,
# and tests/detect-test.sh's C1/C3 assert the two sets agree in both directions.
#
# Five keys were emitted here with no reader at all: rust, chart, chartpath,
# dockerfile and variant. Each cost something rather than merely sitting idle —
# `chart`/`chartpath` needed a THIRD copy of the Chart.yaml glob, without the
# subchart exclusion scripts/chart-list.sh exists to provide, and `dockerfile`
# carried a comment claiming ci-build reads it to decide `--file`, which it does
# not: ci-build resolves <context>/Dockerfile then <context>/Containerfile
# itself. Re-adding an output later is additive; publishing one nothing reads is
# a claim that ages into a lie.
echo "gomod=${gomod#./}"
# THE DIRECTORY, not just the file, because they have different consumers.
# setup-go takes `go-version-file: <gomod>` and sets the TOOLCHAIN — it does not
# change directory. Every Go command ci-lint.yml runs afterwards is a
# module-root operation, so each takes `working-directory: <godir>`; without it
# a module under svc/ left `go mod tidy -diff` and `go test ./...` running in the
# repo root, where they fail with `go.mod file not found` and `directory prefix
# . does not contain main module`.
echo "godir=$(dirname "${gomod:-.}" | sed 's|^\./||')"
echo "go=$(b "${gomod}")"
echo "node=$(b "${node}")"
echo "nodelock=$(b "${lock}")"
echo "python=$(b "${py}")"
echo "docker=$(b "${docker}")"
echo "workflows=$(b "${wf}")"
69 changes: 69 additions & 0 deletions .github/release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"separate-pull-requests": false,
"pull-request-title-pattern": "chore: release ${version}",
"include-component-in-tag": false,
"changelog-sections": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "deps",
"section": "Dependencies"
},
{
"type": "revert",
"section": "Reverts"
},
{
"type": "chore",
"section": "Miscellaneous Chores",
"hidden": false
},
{
"type": "docs",
"section": "Documentation",
"hidden": true
},
{
"type": "style",
"section": "Styles",
"hidden": true
},
{
"type": "refactor",
"section": "Code Refactoring",
"hidden": true
},
{
"type": "test",
"section": "Tests",
"hidden": true
},
{
"type": "build",
"section": "Build System",
"hidden": true
},
{
"type": "ci",
"section": "Continuous Integration",
"hidden": true
}
],
"packages": {
".": {
"release-type": "go",
"include-v-in-tag": true
}
}
}
3 changes: 3 additions & 0 deletions .github/release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "2.11.0"
}
7 changes: 7 additions & 0 deletions .github/renovate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>jacaudi/renovate-config:base",
"github>jacaudi/renovate-config:go"
]
}
Loading
Loading