diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bd078af --- /dev/null +++ b/.editorconfig @@ -0,0 +1,57 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# https://editorconfig.org +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +# --- whitespace-significant formats --- +[*.{diff,patch}] +trim_trailing_whitespace = false + +[*.md] +trim_trailing_whitespace = false + +# --- Go: tabs, rendered 4 wide (gofmt emits tabs; width is display-only) --- +[*.go] +indent_style = tab +indent_size = 4 + +# --- task runner / build --- +[{Justfile,justfile,.justfile}] +indent_size = 4 + +[*.just] +indent_size = 4 + +[Makefile] +indent_style = tab + +# --- data formats (YAML must be spaces, never tabs) --- +[*.{json,jsonc,yaml,yml,toml}] +indent_size = 2 + +# --- JavaScript / TypeScript (2-space matches Prettier/Biome defaults) --- +[*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}] +indent_size = 2 + +# --- CSS family --- +[*.{css,scss,sass,less,pcss}] +indent_size = 2 + +# --- HTML / templates --- +[*.{html,htm,vue,svelte,astro}] +indent_size = 2 + +# --- Rust: rustfmt is 4-space, 100-col --- +[*.rs] +indent_size = 4 +max_line_length = 100 diff --git a/.gitignore b/.gitignore index 4c1b84a..cfa5033 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,26 @@ .idea _* bin -third-party \ No newline at end of file +third-party + +# --- added by limen fix: baseline patterns --- +Thumbs.db +Desktop.ini +.vscode/ +*.swp +*~ +*.local +_scratch/ +WIP_* +/build +/target/ +**/*.rs.bk +*.pdb +node_modules/ +.svelte-kit/ +.vite/ +vite.config.*.timestamp-* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* diff --git a/.golangci.yml b/.golangci.yml index d348028..2f54200 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -199,10 +199,10 @@ linters: settings: govet: enable: - # Struct fields ordered to minimize padding waste. Off by default in - # vet; reordering is safe for us — nothing relies on field order or - # memory layout (JSON marshals by name). - # - fieldalignment + # Struct fields ordered to minimize padding waste. Off by default in + # vet; reordering is safe for us — nothing relies on field order or + # memory layout (JSON marshals by name). + # - fieldalignment # Note: default, no changes errorlint: # Disable err==target → errors.Is(err, target) auto-rewrite. The fixer @@ -251,32 +251,32 @@ linters: - assign-exclusive # Only allow cuddling new OR existing variables, not both - assign-expr # Don't allow assignments cuddled with expressions -# forbidigo: -# forbid: -# - pattern: '^fmt\.Print(ln|f)?$' -# msg: "use structured logging instead" -# - pattern: '^os\.ReadFile$' -# msg: "use xos.ReadFile instead" -# - pattern: '^os\.ReadDir$' -# msg: "use xos.ReadDir instead" -# - pattern: '^os\.WriteFile$' -# msg: "use filesystem.WriteFile instead" -# - pattern: '^os\.Stat$' -# msg: "use xos.Stat instead" -# - pattern: '^os\.Open$' -# msg: "use xos.Open instead" -# - pattern: '^os\.OpenFile$' -# msg: "use xos.OpenFile instead" -# - pattern: '^os\.Create$' -# msg: "use xos.OpenFile instead" -# - pattern: '^os\.CreateTemp$' -# msg: "use xos.CreateTemp instead" -# - pattern: '^os\.MkdirTemp$' -# msg: "use xos.MkdirTemp instead" -# - pattern: '^os\.Truncate$' -# msg: "use xos.Truncate instead" -# - pattern: '^os\.UserHomeDir$' -# msg: "use filesystem.HomeDir instead" + # forbidigo: + # forbid: + # - pattern: '^fmt\.Print(ln|f)?$' + # msg: "use structured logging instead" + # - pattern: '^os\.ReadFile$' + # msg: "use xos.ReadFile instead" + # - pattern: '^os\.ReadDir$' + # msg: "use xos.ReadDir instead" + # - pattern: '^os\.WriteFile$' + # msg: "use filesystem.WriteFile instead" + # - pattern: '^os\.Stat$' + # msg: "use xos.Stat instead" + # - pattern: '^os\.Open$' + # msg: "use xos.Open instead" + # - pattern: '^os\.OpenFile$' + # msg: "use xos.OpenFile instead" + # - pattern: '^os\.Create$' + # msg: "use xos.OpenFile instead" + # - pattern: '^os\.CreateTemp$' + # msg: "use xos.CreateTemp instead" + # - pattern: '^os\.MkdirTemp$' + # msg: "use xos.MkdirTemp instead" + # - pattern: '^os\.Truncate$' + # msg: "use xos.Truncate instead" + # - pattern: '^os\.UserHomeDir$' + # msg: "use filesystem.HomeDir instead" exclude-godoc-examples: true analyze-types: true diff --git a/.just/.shellcheckrc b/.just/.shellcheckrc new file mode 100644 index 0000000..cd23d02 --- /dev/null +++ b/.just/.shellcheckrc @@ -0,0 +1,17 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# If you need local overrides for this project, you can use inline `# shellcheck disable=SCXXXX`. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# .shellcheckrc — https://www.shellcheck.net/wiki/ + +# Follow `source`/`.`-ed files so shellcheck checks across includes. +# 'true' lets it follow even non-constant source paths (best for repos with libs). +external-sources=true + +# Opt INTO the optional checks shellcheck ships but doesn't run by default. +enable=quote-safe-variables # flags unquoted vars that should be quoted +# enable=require-variable-braces # enforce ${var} consistently (optional/stylistic — drop if noisy) +enable=check-unassigned-uppercase +enable=deprecate-which # `which` → `command -v` +enable=avoid-nullary-conditions diff --git a/.just/.yamlfmt b/.just/.yamlfmt new file mode 100644 index 0000000..69c6554 --- /dev/null +++ b/.just/.yamlfmt @@ -0,0 +1,44 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# .yamlfmt — https://github.com/google/yamlfmt +# Formatter: the 'basic' formatter is the only one; these are its options. +formatter: + type: basic + + # --- indentation --- + indent: 2 # match your .editorconfig (2-space YAML) + include_document_start: false # don't force a leading `---` on every file + + # --- the defaults worth overriding --- + retain_line_breaks_single: true # collapse runs of blank lines to ONE, but keep + # intentional single blank lines (readability). + # Prefer this over retain_line_breaks, which keeps + # *all* blank lines and barely normalizes anything. + + scan_folded_as_literal: false # leave folded (>) scalars folded; don't rewrite to literal (|) + + # --- correctness / safety --- + disallow_anchors: false # set true ONLY if you want to forbid &anchors/*aliases + max_line_length: 0 # 0 = no wrapping. Leave OFF — yamlfmt's wrapping is + # crude and mangles long values; let humans wrap. + + # --- quoting / strings: leave alone --- + # yamlfmt is deliberately light on string normalization. Don't fight it; it won't + # aggressively re-quote, which is the safe default for mixed YAML (k8s, CI, etc.) + +# --- which files to format --- +include: + - "**/*.{yaml,yml}" + +gitignore_excludes: true + +exclude: + - ".git/**" + - "**/vendor/**" + - "**/testdata/**" # don't reformat fixtures — tests may assert exact bytes + - "**/*.gen.{yaml,yml}" # leave generated YAML alone + +# Continue formatting other files if one fails to parse, and report all errors. +continue_on_error: false # in CI you want a hard fail on malformed YAML diff --git a/.just/aqua-registry.yaml b/.just/aqua-registry.yaml new file mode 100644 index 0000000..b5a8e89 --- /dev/null +++ b/.just/aqua-registry.yaml @@ -0,0 +1,79 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Most tools can be installed directly from aqua registry without local overrides. +# Addition of non-standard tools can be discussed on https://github.com/farcloser/limen + +packages: + # The /v2 module-path suffix is mandatory (Go semantic import versioning): + # `go install github.com/google/go-licenses@v2.x` is rejected by the + # toolchain. Go strips the /v2 when naming the binary; files spells it out. + - type: go_install + path: github.com/google/go-licenses/v2 + description: Report on the licenses of a Go project's dependencies + version_source: github_tag + files: + - name: go-licenses + - type: go_install + path: github.com/vbatts/git-validation + description: Validate git commit rules (DCO sign-off, subject length, …) + version_source: github_tag + # Graphviz `dot` as a single static Go binary: the real graphviz C code + # compiled to WASM, executed via wazero — no C toolchain, no shared + # libraries, which is why this can live in aqua while system graphviz + # cannot. Renders the PNGs of `just test go profile`. The cmd/dot module is + # nested and untagged upstream, hence the explicit repo and the + # pseudo-version pin in aqua.yaml. + - type: go_install + name: github.com/goccy/go-graphviz/cmd/dot + path: github.com/goccy/go-graphviz/cmd/dot + repo_owner: goccy + repo_name: go-graphviz + description: Graphviz dot CLI, pure-Go build (WASM via wazero) + version_source: github_tag + # The Go team distributes govulncheck via `go install` only, by policy. The + # repo is not inferable from the golang.org import path, hence the explicit + # owner/name (same as the standard registry does for gopls). + - type: go_install + name: golang.org/x/vuln/cmd/govulncheck + path: golang.org/x/vuln/cmd/govulncheck + repo_owner: golang + repo_name: vuln + description: Scan dependencies against the Go vulnerability database + version_source: github_tag + # Same golang.org path situation as govulncheck: explicit name and repo. + - type: go_install + name: golang.org/x/tools/cmd/deadcode + path: golang.org/x/tools/cmd/deadcode + repo_owner: golang + repo_name: tools + description: Whole-program detection of unreachable functions + version_source: github_tag + # hadolint as pure Go, from the farcloser stable. The binary is the nested + # cmd/godolint package of the root module — explicit name and repo, same + # shape as go-graphviz's cmd/dot above (tags live at the repo root). + - type: go_install + name: github.com/farcloser/godolint/cmd/godolint + path: github.com/farcloser/godolint/cmd/godolint + repo_owner: farcloser + repo_name: godolint + description: Lint Dockerfiles (a pure-Go port of hadolint) + version_source: github_tag + # limen itself: every repo pins the version that enforces it, so the binary + # and the canonical files it embeds travel together (no version skew between + # the checker and the checked). Graduates to the standard registry once + # farcloser/limen is registered there. + - type: github_release + repo_owner: farcloser + repo_name: limen + description: Verify a repository against Farcloser engineering rules + asset: limen_{{trimV .Version}}_{{.OS}}_{{.Arch}}.tar.gz + format: tar.gz + files: + - name: limen + checksum: + type: github_release + asset: checksums.txt + algorithm: sha256 + supported_envs: + - darwin + - linux diff --git a/.just/build-go.just b/.just/build-go.just new file mode 100644 index 0000000..57c5394 --- /dev/null +++ b/.just/build-go.just @@ -0,0 +1,159 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +# Ported from the historical Make build system, corrected along the way: +# - a "binary" is a DIRECTORY under cmd/ that contains Go sources — a stray +# file or docs directory no longer becomes a phantom target; +# - version stamping is reproducible: main.version carries `git describe`, +# and commit/date come from Go's own VCS embedding (`go version -m`) at +# their COMMIT values — never wall-clock build time, which made the same +# commit produce different binaries; +# - build tags are scoped to these commands, never exported globally where +# they would leak into tests and linters; +# - static states its preconditions (Linux, CGO) and fails fast instead of +# dying with a cryptic linker error; +# - a race build exists. +# Binaries land in build/ (covered by the canonical .gitignore). Projects +# declare `var version = "dev"` in each main package for the stamp to land. +# Extra flags: export BUILD_GO_FLAGS from project.just, the home of all +# project customization (the variable name mirrors the task path), or set it +# on the invocation for a one-off. + +# netgo/osusergo force the pure-Go resolver and user lookups. No-ops when +# CGO_ENABLED=0 (pure Go already); with CGO they keep DNS and user handling +# out of libc, which is exactly what makes `static` viable on glibc. +go_tags := 'netgo,osusergo' + +# CGO is opt-in per invocation (`CGO_ENABLED=1 just build go`). Defaulted to 0 +# explicitly for every recipe rather than left unset: go's own default is CGO +# on for native builds when a C toolchain exists, and unset must mean pure Go +# here. +export CGO_ENABLED := env('CGO_ENABLED', '0') + +# --- C toolchain hardening — only effective when CGO_ENABLED=1 -------------- +# The canonical hardening set (see the Red Hat compiler-flags guidance), +# platform-gated: stack-clash protection and the -z linker set are Linux-only +# (ld64 on macOS supports neither). +c_warnings := '-Wall -Werror=format-security' +c_security := '-fstack-protector-strong -fPIE -D_FORTIFY_SOURCE=2' + (if os() == 'linux' { ' -fstack-clash-protection' } else { '' }) +cgo_cflags_release := c_warnings + ' -O2 ' + c_security + ' -pipe' + +# Debug: -O0 with real debug info. FORTIFY_SOURCE is deliberately absent — it +# requires -O1 or higher and would only produce warnings at -O0. +cgo_cflags_debug := c_warnings + ' -O0 -g -grecord-gcc-switches -pipe' + +# libstdc++ assertions are a C++-only concern: valid in CXXFLAGS, not CFLAGS. +cgo_cxxflags_debug := cgo_cflags_debug + ' -D_GLIBCXX_ASSERTIONS' +cgo_ldflags := if os() == 'linux' { '-Wl,-z,defs -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack' } else { '' } + +default: release + +# Release: reproducible (trimpath, commit-time VCS stamp), stripped, PIE. +# Twin of the builds section in .goreleaser.yaml (the release builder, where +# a project ships one) — a flag change here must land there too. See the +# comment there for the differences that are intended. +release: (_banner "build go" "release") + #!/usr/bin/env bash + set -euo pipefail + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + ldflags="-s -w -X main.version=${version}" + if [ "${CGO_ENABLED:-0}" = "1" ]; then + export CGO_CFLAGS='{{ cgo_cflags_release }}' + export CGO_CXXFLAGS='{{ cgo_cflags_release }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + ldflags="-linkmode=external -extldflags=-pie ${ldflags}" + fi + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}" + go build -trimpath -buildmode=pie -tags='{{ go_tags }}' \ + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "build/${name}" "./${dir%/}" + done + +# Debug: optimizations and inlining off, symbols kept, real paths kept — no +# trimpath, deliberately: debuggers want actual file locations. -debug suffix. +debug: (_banner "build go" "debug") + #!/usr/bin/env bash + set -euo pipefail + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + ldflags="-X main.version=${version}" + if [ "${CGO_ENABLED:-0}" = "1" ]; then + export CGO_CFLAGS='{{ cgo_cflags_debug }}' + export CGO_CXXFLAGS='{{ cgo_cxxflags_debug }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + ldflags="-linkmode=external -extldflags=-pie ${ldflags}" + fi + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-debug" + go build -buildmode=pie -gcflags='all=-N -l' -tags='{{ go_tags }}' \ + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "build/${name}-debug" "./${dir%/}" + done + +# Race: a diagnostic build with the race detector, which requires cgo (forced +# here) and, matching the test module, an external linkmode. Unstripped, real +# paths. -race suffix. +race: (_banner "build go" "race") + #!/usr/bin/env bash + set -euo pipefail + export CGO_ENABLED=1 + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-race" + go build -race -tags='{{ go_tags }}' \ + -ldflags "-linkmode=external -X main.version=${version}" \ + ${extra[@]+"${extra[@]}"} -o "build/${name}-race" "./${dir%/}" + done + +# Static: fully static external link. Preconditions enforced, not commented: +# Linux only (macOS has no static libc), and CGO_ENABLED=1 (with CGO off, +# every build is already static — use `release`). No PIE: classic static and +# PIE conflict. -static suffix. +static: (_banner "build go" "static") + #!/usr/bin/env bash + set -euo pipefail + if [ "$(uname -s)" != "Linux" ]; then + echo "static builds require Linux: macOS has no static libc to link against." >&2 + exit 1 + fi + if [ "${CGO_ENABLED:-0}" != "1" ]; then + echo "static needs CGO_ENABLED=1 — a pure-Go build is already static (use \`just build go\`)." >&2 + exit 1 + fi + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + export CGO_CFLAGS='{{ cgo_cflags_release }}' + export CGO_CXXFLAGS='{{ cgo_cflags_release }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-static" + go build -trimpath -tags='{{ go_tags }}' \ + -ldflags "-linkmode=external -extldflags=-static -s -w -X main.version=${version}" \ + ${extra[@]+"${extra[@]}"} -o "build/${name}-static" "./${dir%/}" + done diff --git a/.just/build.just b/.just/build.just new file mode 100644 index 0000000..3d7f829 --- /dev/null +++ b/.just/build.just @@ -0,0 +1,17 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Go builds live in their own submodule: `just build go` makes the release +# binaries, `just build go ` (debug, race, static) makes a variant. +mod go 'build-go.just' + +# A nested submodule's default is not addressable as a dependency path, so the +# bare `just build` delegates through a subprocess (same pattern as lint-all). +default: + just build go diff --git a/.just/fix-go.just b/.just/fix-go.just new file mode 100644 index 0000000..1133184 --- /dev/null +++ b/.just/fix-go.just @@ -0,0 +1,28 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +default: code mod + +# golangci-lint auto-fixes, once per supported platform (see _per-goos in +# _lib.just: platform-specific files are only analyzed for the GOOS that +# builds them). The formatter is build-graph-independent and runs once — last, +# so the fixes' edits end up formatted. +code: (_banner "fix go" "go") (_per-goos "golangci-lint run --fix") + golangci-lint fmt + +# Tidy go.mod/go.sum. +mod: (_banner "fix go" "mod") + go mod tidy + +# Update every dependency to its latest version, then tidy. +up: (_banner "fix go" "up") && mod + go get -u ./... diff --git a/.just/fix.just b/.just/fix.just new file mode 100644 index 0000000..c81542e --- /dev/null +++ b/.just/fix.just @@ -0,0 +1,39 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +default: limen just yaml + +# limen itself: repair what `just lint limen` reports — rewrite drifted +# canonical files, create missing mandatory ones, restore aqua pins and +# regenerate aqua-checksums.json (network, when pins changed). Same LIMEN_BIN +# override as the lint recipe (see lint.just). +limen: (_banner "fix" "limen") + ${LIMEN_BIN:-limen} fix . + +just: (_banner "fix" "just") + #!/usr/bin/env bash + set -euo pipefail + # --cached --others --exclude-standard: tracked *and* new untracked files, + # still honoring .gitignore — so a not-yet-staged file is formatted too. + git ls-files -z --cached --others --exclude-standard 'justfile' 'Justfile' '*.just' '**/justfile' \ + | xargs -0 -I{} just --fmt --justfile {} + +rust: (_banner "fix" "rust") + cargo clippy --fix --all-targets --all-features --allow-dirty --allow-staged + cargo fmt --all + +# Go fixers live in their own submodule: `just fix go` runs them all, +# `just fix go ` (e.g. `just fix go mod`) runs one. +mod go 'fix-go.just' + +yaml: (_banner "fix" "yaml") + yamlfmt -conf .just/.yamlfmt . diff --git a/.just/lint-go.just b/.just/lint-go.just new file mode 100644 index 0000000..01ef330 --- /dev/null +++ b/.just/lint-go.just @@ -0,0 +1,110 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +default: code mod vuln licenses + +# golangci-lint, once per supported platform (see _per-goos in _lib.just: +# platform-specific files are only analyzed for the GOOS that builds them). +# The formatter is build-graph-independent and runs once. +code: (_banner "lint go" "go") (_per-goos "golangci-lint run") + golangci-lint fmt --diff + +# go.mod/go.sum tidiness — fails printing the diff; `just fix go mod` repairs. +mod: (_banner "lint go" "mod") + go mod tidy -diff + +# Known-vulnerability scan against the Go vulnerability database (network: +# govulncheck fetches vuln.go.dev), once per supported platform (see _per-goos +# in _lib.just: call paths and dependencies differ by GOOS). +vuln: (_banner "lint go" "vuln") (_per-goos "govulncheck ./...") + +# Dependency license compliance, once per supported platform (see _per-goos in +# _lib.just: each GOOS pulls its own dependencies). A project that needs extra +# flags — typically --ignore= for the module-layout false positives of +# https://github.com/google/go-licenses/issues/186 — exports them from its +# project.just, the home of all project customization (the variable name +# mirrors the task path `lint go licenses`): +# export LINT_GO_LICENSES_FLAGS := '--ignore=gotest.tools/v3' +# For a one-off, set it on the invocation instead: +# LINT_GO_LICENSES_FLAGS='--ignore=x' just lint go licenses +# +# GOROOT is passed explicitly because go-licenses recognizes stdlib packages by +# their location under GOROOT — but, being a go_install binary, its built-in +# default is the GOROOT of whatever toolchain COMPILED it. When that differs +# from the pinned toolchain on PATH (which is where packages actually load +# from), every stdlib package degrades to "does not have module info" errors. +licenses: (_banner "lint go" "licenses") (_per-goos 'GOROOT="$(go env GOROOT)" go-licenses check --include_tests --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-3-Clause,MIT ${LINT_GO_LICENSES_FLAGS:-} ./...') + +# Bounds-check-elimination report: every bounds check the compiler could NOT +# eliminate, for performance tuning. Informational — it never fails, so it is +# not in the default set; and it reports for the native architecture only, +# since BCE results are arch-specific and this is a report for the human +# reading it, not a gate. +bce: (_banner "lint go" "bce") + #!/usr/bin/env bash + set -euo pipefail + echo "Bounds Check Elimination Report" + echo "================================" + echo "" + output=$(go build -gcflags='-d=ssa/check_bce/debug=1' ./... 2>&1 | grep -v '^#' || true) + if [ -z "$output" ]; then + echo "No bounds checks detected (BCE fully eliminated)." + else + total=$(echo "$output" | wc -l | tr -d ' ') + echo "Total: $total bounds checks" + echo "" + echo "By file:" + echo "$output" | sed 's/:.*$//' | sort | uniq -c | sort -rn + echo "" + echo "Details:" + echo "$output" | sort + fi + +# Escape-analysis and inlining report — both come from the same compiler pass +# (-gcflags=-m): values the compiler moved to the heap (the prime source of GC +# pressure) and functions it refused to inline, with the reason. Informational +# like bce: never fails, native architecture only, not in the default set. +escape: (_banner "lint go" "escape") + #!/usr/bin/env bash + set -euo pipefail + output=$(go build -gcflags='-m' ./... 2>&1 | grep -v '^#' || true) + heap=$(echo "$output" | grep -E 'escapes to heap|moved to heap' || true) + noinline=$(echo "$output" | grep 'cannot inline' || true) + echo "Escape Analysis Report" + echo "======================" + if [ -z "$heap" ]; then + echo "No heap escapes." + else + echo "Total: $(echo "$heap" | wc -l | tr -d ' ') heap escapes" + echo "" + echo "By file:" + echo "$heap" | sed 's/:.*$//' | sort | uniq -c | sort -rn + echo "" + echo "$heap" | sort + fi + echo "" + echo "Inlining Failures" + echo "=================" + if [ -z "$noinline" ]; then + echo "None." + else + echo "Total: $(echo "$noinline" | wc -l | tr -d ' ') functions the compiler could not inline" + echo "" + echo "$noinline" | sort + fi + +# Unreachable functions, by whole-program call-graph analysis (deeper than the +# per-package `unused` linter in golangci). -test keeps library-only repos +# analyzable (test binaries serve as roots) and stops dead code from hiding +# behind test-only callers. Informational: native only, not in the default set. +deadcode: (_banner "lint go" "deadcode") + deadcode -test ./... diff --git a/.just/lint-rust.just b/.just/lint-rust.just new file mode 100644 index 0000000..3185f0f --- /dev/null +++ b/.just/lint-rust.just @@ -0,0 +1,15 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +rust: (_banner "lint" "rust") + cargo fmt --all --check + cargo clippy --all-targets --all-features -- --deny warnings diff --git a/.just/lint.just b/.just/lint.just new file mode 100644 index 0000000..d35850e --- /dev/null +++ b/.just/lint.just @@ -0,0 +1,190 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +default: limen just aqua links yaml shell dockerfile commits + +# limen itself: verify the repository against the Farcloser engineering rules +# (mandatory files, canonical baseline, license, aqua pins). First in the +# default set: the other linters trust the canonical files this one verifies. +# LIMEN_BIN exists for one consumer — the limen repository itself, whose +# project.just points it at `go run ./cmd/limen` so the working tree is judged +# by its own enforcer, not by the (always older) released pin. Everyone else +# runs the aqua-pinned binary. +limen: (_banner "lint" "limen") + ${LIMEN_BIN:-limen} check . + +just: (_banner "lint" "just") + #!/usr/bin/env bash + set -euo pipefail + # --cached --others --exclude-standard: tracked *and* new untracked files, + # still honoring .gitignore — so a not-yet-staged file is linted too. + git ls-files -z --cached --others --exclude-standard 'justfile' 'Justfile' '*.just' '**/justfile' \ + | xargs -0 -I{} just --fmt --check --justfile {} + +# Go linters live in their own submodule: `just lint go` runs them all, +# `just lint go ` (e.g. `just lint go mod`) runs one. +mod go 'lint-go.just' +mod rust 'lint-rust.just' + +# godolint — hadolint as pure Go (https://github.com/farcloser/godolint) — +# over every Dockerfile in the repository; passes vacuously when there are +# none, so it can sit in the default set for every repo. Rule exceptions +# belong next to the instruction they excuse, as inline +# `# hadolint ignore=DLxxxx` pragmas — not in the recipe. +dockerfile: (_banner "lint" "dockerfile") + #!/usr/bin/env bash + set -euo pipefail + # --cached --others --exclude-standard: tracked *and* new untracked files, + # still honoring .gitignore — so a not-yet-staged Dockerfile is linted too + # (same as `lint just`). The patterns cover the naming conventions — the + # bare name and the . / . forms, at any + # depth — for both Dockerfile and its OCI-neutral synonym Containerfile + # (podman/buildah). '*.Dockerfile' needs no '**/' twin — a wildcard pattern + # already matches across directories; a literal like 'Dockerfile' does not. + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard \ + 'Dockerfile' '**/Dockerfile' 'Dockerfile.*' '**/Dockerfile.*' '*.Dockerfile' \ + 'Containerfile' '**/Containerfile' 'Containerfile.*' '**/Containerfile.*' '*.Containerfile') + # godolint validates RUN instructions through shellcheck when it finds it + # on PATH — the hermetic PATH (see the root Justfile) always provides the + # pinned one. + [ ${#files[@]} -eq 0 ] || godolint "${files[@]}" + +yaml: (_banner "lint" "yaml") + yamlfmt -conf .just/.yamlfmt -lint . + +shell: (_banner "lint" "shell") + #!/usr/bin/env bash + set -euo pipefail + # A file is a shell script if it declares itself one, by either signal: + # - a .sh/.bash extension → a missing shebang is then a lint error (SC2148), or + # - a shebang on line 1 for a dialect shellcheck lints (sh/bash/dash/ksh — + # not zsh, which shellcheck cannot check) → covers extension-less scripts. + # Filtering on the shebang alone would silently skip an *.sh that forgot its shebang. + # No mapfile (bash 4+): build the array with a read loop, portable to bash 3.2. + files=() + while IFS= read -r -d '' f; do + case "$f" in + *.sh|*.bash) files+=("$f"); continue ;; + esac + head -n 1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|dash|ksh|sh)\b' && files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard) + [ ${#files[@]} -eq 0 ] || shellcheck --rcfile .just/.shellcheckrc "${files[@]}" + # Shebang recipes inside justfiles are shell scripts too — extract each + # body from `just --dump --dump-format json` and shellcheck it standalone. + # In the dump a body line is a fragment list: text fragments are strings, + # interpolations (double-brace expressions — just syntax, not shell) are + # arrays, masked as INTERP. One dump of the root justfile covers the whole + # tree: imports (project.just) are flattened into .recipes — they cannot be + # parsed standalone, their dependencies may name root-level module paths — + # and every `mod` nests recursively under .modules. mktemp gets an explicit + # template because macOS mktemp ignores $TMPDIR. + tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/lint-shell.XXXXXX") + trap 'rm -rf "$tmpdir"' EXIT + recipes=() + dump=$(just --dump --dump-format json) + # Bodies are multi-line: base64 carries each across the one-record-per-line + # pipe intact (quotes, tabs and all). + while IFS=$'\t' read -r name body; do + out="$tmpdir/$name" + printf '%s' "$body" | base64 -d > "$out" + head -n 1 "$out" | grep -qE '^#!.*\b(bash|dash|ksh|sh)\b' || continue + recipes+=("$out") + done < <(printf '%s' "$dump" | jq -r ' + def recipes_of(prefix): + ((.recipes // {}) | to_entries[] + | [prefix + .key, + (.value.body + | map(map(if type == "string" then . else "INTERP" end) | join("")) + | join("\n") | @base64)]), + ((.modules // {}) | to_entries[] + | .key as $mod | .value | recipes_of(prefix + $mod + "::")); + recipes_of("") | @tsv') + # The constant-word checks (SC2050/SC2078/SC2157/SC2194) are excluded for + # recipe bodies only: a masked INTERP is constant to shellcheck but dynamic + # in reality, so those hits can only be false positives of the masking. + [ ${#recipes[@]} -eq 0 ] || shellcheck --rcfile .just/.shellcheckrc --exclude=SC2050,SC2078,SC2157,SC2194 "${recipes[@]}" + +# Check documentation links with lychee (respects .gitignore, skips hidden files). +links: (_banner "lint" "links") + #!/usr/bin/env bash + set -euo pipefail + # A directory input (not a glob) is what makes lychee honor .gitignore/hidden. + # Canonical exclusions (and their rationale) live in .just/lychee.toml; a + # repository adds its own in a root lychee.toml. Both must be passed + # explicitly — any --config disables lychee's ./lychee.toml auto-discovery — + # and lychee merges them, concatenating the exclude lists. + args=(--config .just/lychee.toml) + if [ -f lychee.toml ]; then + args+=(--config lychee.toml) + fi + lychee --no-progress "${args[@]}" . + +# Validate commit hygiene — DCO sign-off, subject length, dangling whitespace — +# over a commit range (default: everything ahead of the upstream default branch; +# pass one explicitly otherwise, e.g. `just lint commits v1.0.0..HEAD`). +commits range="": (_banner "lint" "commits") + #!/usr/bin/env bash + set -euo pipefail + # Contributors' SSH keys, when the repo ships them, arm signature display + # for humans (git log --show-signature, git tag -v). None of the rules run + # below verify signatures — this is convenience wiring, not enforcement. + # See https://github.com/andyfeller/gh-ssh-allowed-signers for automation + # to retrieve contributor keys. + if [ -f .just/allowed_signers ]; then + git config --unset-all gpg.ssh.allowedSignersFile 2>/dev/null || true + git config --add gpg.ssh.allowedSignersFile .just/allowed_signers + fi + range="{{ range }}" + if [ -z "$range" ] && [ -n "${GITHUB_BASE_REF:-}" ]; then + # GitHub Actions pull request: validate against the PR's actual base + # branch, whatever it targets. Requires actions/checkout with + # fetch-depth: 0 — the default shallow checkout has no base refs (and + # only the synthetic merge commit as "history": vacuously green). + if git rev-parse --verify -q "origin/$GITHUB_BASE_REF" >/dev/null 2>&1; then + range="origin/${GITHUB_BASE_REF}..HEAD" + fi + fi + if [ -z "$range" ]; then + # Default to the commits this branch adds over the upstream default + # branch. origin/HEAD is only set when `git remote set-head` ran, so + # fall back through the common default-branch names; with no upstream + # at all, validate the full history. + for ref in origin/HEAD origin/main origin/master; do + if git rev-parse --verify -q "$ref" >/dev/null 2>&1; then + range="$ref..HEAD" + break + fi + done + fi + if [ -n "$range" ]; then + git-validation -run DCO,short-subject,dangling-whitespace -range "$range" + else + git-validation -run DCO,short-subject,dangling-whitespace + fi + +# Verify aqua-checksums.json is complete and in sync with aqua.yaml. +aqua: (_banner "lint" "aqua") + #!/usr/bin/env bash + set -euo pipefail + # aqua has no read-only validator, so regenerate and compare against the + # committed file (needs network). `git diff HEAD` is staging-independent — it + # compares the working tree to the last commit, not the index. On drift the + # corrected file is left in place — review and commit it. + # --log-level warn: the per-package INFO lines are noise when nothing is wrong. + aqua --log-level warn update-checksum --prune + if ! git diff --quiet HEAD -- aqua-checksums.json; then + echo "aqua-checksums.json was out of sync with aqua.yaml (regenerated above) — review and commit it." >&2 + exit 1 + fi diff --git a/.just/lychee.toml b/.just/lychee.toml new file mode 100644 index 0000000..0a3d0f9 --- /dev/null +++ b/.just/lychee.toml @@ -0,0 +1,14 @@ +# DO NOT EDIT MANUALLY. +# Canonical lychee (link checker) configuration, identical in every repository. +# A repository can add its own exclusions in a root lychee.toml — the `lint links` +# recipe passes both files and lychee merges them, concatenating the exclude +# lists — so this baseline only carries exclusions that apply everywhere. + +exclude = [ + # fsf.org: verbatim (A)GPL license texts link to it, and its server (TLS 1.2 + # with DHE-only key exchange) cannot complete a handshake with lychee's + # rustls, which implements neither. The links themselves are fine. + # gnu.org: rate-limits aggressively, so the license-text links to it fail + # intermittently. + 'https?://(www\.)?(fsf|gnu)\.org(/|$)', +] diff --git a/.just/release.just b/.just/release.just new file mode 100644 index 0000000..1fc1266 --- /dev/null +++ b/.just/release.just @@ -0,0 +1,140 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +# Imported flat into the canonical Justfile — not a module — so the recipe can +# take arguments (`just release v1.2.3`; a module invocation would parse the +# tag as a recipe path). Deliberately no settings and no _lib import here: an +# import's settings would apply to the whole root justfile. + +# Release, in two lanes that share every guard (see book/recipes.md): +# +# just release --cut vX.Y.Z the CI lane's local half: verify a clean tree, +# create the signed tag (your key signs the +# INTENT), push — the release workflow builds, +# signs (keyless cosign), and publishes. +# just release --ci the CI lane's workflow half (GitHub Actions +# only): the pushed tag triggered us; run +# goreleaser with keyless signing. +# just release vX.Y.Z the fully local lane: tag + goreleaser + +# key-based cosign from this machine. Requires +# GITHUB_TOKEN, COSIGN_KEY, COSIGN_PASSWORD. +# The lane for private repos (nothing touches +# Rekor's public log) — and the escape hatch +# when CI is down. +# just release --dry-run unsigned local snapshot into build/release/: +# no tag, no token, no publish — and works on a +# dirty tree, by design. +# +# Tag pushes are always exactly one tag, never --tags: stray local tags stay +# local. A tag already on HEAD is reused, so a failed publish retries safely. +[doc('Release: `--cut vX.Y.Z` tag for CI · `vX.Y.Z` fully local · `--ci` (workflow) · `--dry-run`')] +release *args: + #!/usr/bin/env bash + set -euo pipefail + # Releasing is opt-in, by carrying a goreleaser config (project-owned, like + # project.just — limen neither pins nor seeds it). Checked before anything + # else: past this point the recipe creates tags and pushes. + if [ ! -f .goreleaser.yaml ] && [ ! -f .goreleaser.yml ]; then + echo "this project has no .goreleaser.yaml — goreleaser releases are opt-in (see book/recipes.md)." >&2 + exit 1 + fi + usage() { + echo "usage: just release --cut # signed tag + push; CI builds, signs (keyless), publishes" >&2 + echo " just release # fully local release (key-based cosign)" >&2 + echo " just release --dry-run # local unsigned snapshot into build/release/" >&2 + echo " just release --ci # workflow half of --cut (GitHub Actions only)" >&2 + } + require_clean() { + # A dirty tree is the most common failure and goreleaser hard-refuses + # it — check long before anything is created or pushed. + if [ -n "$(git status --porcelain)" ]; then + echo "the working tree is dirty — commit or stash before releasing:" >&2 + git status --short >&2 + exit 1 + fi + } + require_version_tag() { + if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then + echo "'$1' is not a version tag (vX.Y.Z or vX.Y.Z-prerelease, e.g. v0.0.0-test.1)." >&2 + exit 2 + fi + } + tag_and_push() { + # Create the signed tag — unless it already exists pointing at HEAD, so + # a retry after a failed publish resumes instead of failing here. + if existing=$(git rev-parse -q --verify "refs/tags/$1^{commit}"); then + if [ "$existing" != "$(git rev-parse HEAD)" ]; then + echo "tag $1 already exists and does not point at HEAD — refusing." >&2 + exit 1 + fi + echo "tag $1 already on HEAD — reusing it." + else + git tag -s "$1" -m "$1" + fi + git push origin HEAD + git push origin "refs/tags/$1" + } + set -- {{ args }} + case "${1:-}" in + --dry-run) + # Snapshot builds skip signing: no key material needed for a dry-run. + exec goreleaser release --snapshot --clean --skip=sign + ;; + --cut) + [ -n "${2:-}" ] || { usage; exit 2; } + require_clean + require_version_tag "$2" + tag_and_push "$2" + echo "tag $2 pushed — the release workflow builds, signs, and publishes from here." + ;; + --ci) + # The workflow half: the tag exists (its push triggered this run, + # and CI checks out exactly that commit), publishing uses the + # workflow's GITHUB_TOKEN, and signing is keyless — the OIDC + # identity replaces any key. + if [ -z "${GITHUB_ACTIONS:-}" ]; then + echo "--ci is the workflow half — locally, use 'just release --cut ' (or a fully local 'just release ')." >&2 + exit 1 + fi + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "no OIDC token available — the workflow needs 'id-token: write' for keyless signing." >&2 + exit 1 + fi + if [ -z "${GITHUB_TOKEN:-}" ]; then + echo "GITHUB_TOKEN is not set — pass the workflow token to this step's env." >&2 + exit 1 + fi + if ! git describe --exact-match --tags HEAD >/dev/null 2>&1; then + echo "HEAD carries no tag — this lane only runs on a tag push." >&2 + exit 1 + fi + # Keyless is enforced, not defaulted: a key leaking into the CI + # environment must not silently flip the signing mode. + unset COSIGN_KEY + exec goreleaser release --clean + ;; + "") + usage + exit 2 + ;; + *) + tag="$1" + require_clean + require_version_tag "$tag" + if [ -z "${GITHUB_TOKEN:-}" ]; then + echo "GITHUB_TOKEN is not set — goreleaser needs it to publish the GitHub release." >&2 + exit 1 + fi + if [ -z "${COSIGN_KEY:-}" ] || [ ! -f "${COSIGN_KEY}" ]; then + echo "COSIGN_KEY is not set or does not point at the cosign private key —" >&2 + echo "local releases are key-signed (see .goreleaser.yaml). Generate a key pair once" >&2 + echo "with 'cosign generate-key-pair', commit cosign.pub, keep cosign.key private," >&2 + echo "then export COSIGN_KEY=/path/to/cosign.key (COSIGN_PASSWORD is prompted or exported)." >&2 + echo "(Or use the CI lane instead: 'just release --cut $tag' — no key needed.)" >&2 + exit 1 + fi + tag_and_push "$tag" + exec goreleaser release --clean + ;; + esac diff --git a/.just/test-go.just b/.just/test-go.just new file mode 100644 index 0000000..dff80c8 --- /dev/null +++ b/.just/test-go.just @@ -0,0 +1,105 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +default: unit + +# `go test` runs funnel through gotestsum: same exit semantics, readable live +# output (its format is gotestsum's own GOTESTSUM_FORMAT knob). -count=1 +# defeats Go's test result cache — a test task must actually test. The shared +# timeout is project-overridable from project.just, the home of all project +# customization: +# export TEST_GO_TIMEOUT := '20m' + +# Unit tests. +unit: (_banner "test go" "unit") + gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" ./... + +# Unit tests under the race detector. The final link is delegated to the +# system linker: the race detector injects runtime/cgo into every package, and +# hardened CGO_CFLAGS (-fstack-protector-strong, -fPIE) make those objects +# reference libc symbols Go's internal linker cannot resolve on Linux +# ("relocation target stderr not defined") — see golang/go#52690, #54313, +# #58619. On macOS with Xcode 15+, ld emits "has malformed LC_DYSYMTAB" +# warnings for race builds; cosmetic, the binaries are correct (golang/go#61229). +race: (_banner "test go" "race") + CGO_ENABLED=1 gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -ldflags=-linkmode=external -race ./... + +# Benchmarks, with allocation stats. -run '^$' deselects unit tests so only +# benchmarks run — `just test go` already covers the tests themselves. +bench: (_banner "test go" "bench") + go test -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -run '^$' -bench . -benchmem ./... + +# Coverage: per-function summary, an HTML report under build/coverage/, and an +# optional minimum gate — export TEST_GO_COVER_MIN := '80' (integer percent) +# from project.just to enforce a floor; unset or 0 reports without gating. +cover: (_banner "test go" "cover") + #!/usr/bin/env bash + set -euo pipefail + dir=build/coverage + mkdir -p "$dir" + gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -coverprofile="$dir/coverage.out" ./... + go tool cover -func="$dir/coverage.out" + go tool cover -html="$dir/coverage.out" -o "$dir/coverage.html" + echo "HTML report: $dir/coverage.html" + min="${TEST_GO_COVER_MIN:-0}" + if [ "$min" -gt 0 ] 2>/dev/null; then + total=$(go tool cover -func="$dir/coverage.out" | awk '/^total:/ { gsub(/%/, "", $3); print $3 }') + if [ "${total%%.*}" -lt "$min" ]; then + echo "coverage ${total}% is below the ${min}% minimum" >&2 + exit 1 + fi + echo "coverage ${total}% meets the ${min}% minimum" + fi + +# CPU and memory profiles, one pair per package, with pprof top-20 summaries +# printed and PNG call graphs rendered via the pinned `dot` (goccy/go-graphviz: +# real graphviz compiled to WASM, so it lives in aqua like any Go tool). +# Informational: a package whose tests fail is still profiled past, never +# fails the run. Artifacts land under build/profiles/ (raw) and +# build/profiles-docs/ (PNG); analyze interactively with `go tool pprof`. +profile: (_banner "test go" "profile") + #!/usr/bin/env bash + set -euo pipefail + prof=build/profiles + docs=build/profiles-docs + mkdir -p "$prof" "$docs" + # pprof's own -png shells out to dot writing to a pipe, which the WASM dot + # cannot do (it needs -o) — so render from pprof's -dot text instead. The + # pre-create matters: under a sandbox the WASM runtime can overwrite files + # but not always create them. Paths stay cwd-relative for the same reason + # (the runtime preopens only the working directory). + render() { + src=$1; png=$2; shift 2 + go tool pprof -dot -nodecount=20 "$@" "$src" > "$png.dot" 2>/dev/null + : > "$png" + dot -Tpng -o "$png" "$png.dot" + rm -f "$png.dot" + echo " -> $png" + } + for pkg in $(go list ./...); do + name=${pkg##*/} + echo "Profiling $pkg..." + go test -count=1 -o "$prof/$name.test" "$pkg" \ + -cpuprofile "$prof/${name}_cpu.prof" \ + -memprofile "$prof/${name}_mem.prof" || true + if [ -s "$prof/${name}_cpu.prof" ]; then + echo " CPU profile (top 20):" + go tool pprof -top -nodecount=20 "$prof/${name}_cpu.prof" 2>/dev/null || true + render "$prof/${name}_cpu.prof" "$docs/${name}_cpu.png" + fi + if [ -s "$prof/${name}_mem.prof" ]; then + echo " Memory profile — alloc_space (top 20):" + go tool pprof -top -nodecount=20 -alloc_space "$prof/${name}_mem.prof" 2>/dev/null || true + render "$prof/${name}_mem.prof" "$docs/${name}_alloc.png" -alloc_space + fi + done + echo "Profiles written to $prof/, diagrams to $docs/" diff --git a/.just/test.just b/.just/test.just new file mode 100644 index 0000000..3fbe40e --- /dev/null +++ b/.just/test.just @@ -0,0 +1,17 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +# Go tests live in their own submodule: `just test go` runs the unit tests, +# `just test go ` (e.g. `just test go race`) runs one task. +# +# Bare `just test` deliberately refuses: tests are always language-bound, so +# there is nothing a universal default could run that would not fail (or lie) +# on some repo. Which suites apply is each project's call — name one +# explicitly, and define the project-wide aggregate as `test-all` in +# project.just (the CI workflow's entry point, mirroring lint-all). +default: + @echo 'just test has no default — name a suite (e.g. `just test go`), or run the project'"'"'s `just test-all`.' >&2 + @exit 1 + +mod go 'test-go.just' diff --git a/.just/tools.just b/.just/tools.just new file mode 100644 index 0000000..0b3fba5 --- /dev/null +++ b/.just/tools.just @@ -0,0 +1,78 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +set working-directory := '..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see _lib.just). +import '_lib.just' + +# --- Project tooling via aqua (see book/tooling.md). Each recipe takes the +# owner/repo exactly as it appears in aqua.yaml, e.g. golangci/golangci-lint. --- + +# Add a new tool at its latest version, e.g. `just tools add junegunn/fzf`. +add pkg: (_banner "tools" "add") + #!/usr/bin/env bash + set -euo pipefail + if grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}([[:space:]]|@|$)" aqua.yaml; then + echo "{{ pkg }} is already in aqua.yaml — use 'just tools update {{ pkg }}' or 'just tools set {{ pkg }} '." >&2 + exit 1 + fi + aqua generate -i "{{ pkg }}" # append the latest version to aqua.yaml + aqua update-checksum # record its verified checksum + aqua install --only-link # install and verify + +# Set an existing tool to an exact version, e.g. `just tools set golangci/golangci-lint v1.55.3`. +set pkg version: (_banner "tools" "set") + #!/usr/bin/env bash + set -euo pipefail + if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}@" aqua.yaml; then + echo "{{ pkg }} is not pinned in aqua.yaml — add it with 'just tools add {{ pkg }}'." >&2 + exit 1 + fi + # An explicit path template: macOS mktemp ignores $TMPDIR and falls back to + # a per-user temp dir that sandboxes may deny writes to. A sibling file also + # makes the mv a same-filesystem, truly atomic rename. + tmp=$(mktemp aqua.yaml.XXXXXX) + trap 'rm -f "$tmp"' EXIT + sed -E "s#(^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }})@[^[:space:]]*#\1@{{ version }}#" aqua.yaml > "$tmp" + mv "$tmp" aqua.yaml + aqua update-checksum # refresh the checksum for the new version + aqua install --only-link # install and verify + +# Update an existing tool to its latest version, e.g. `just tools update golangci/golangci-lint`. +update pkg: (_banner "tools" "update") + #!/usr/bin/env bash + set -euo pipefail + latest=$(aqua generate "{{ pkg }}" 2>/dev/null | grep -E 'name:' | head -n 1 | sed -E 's/.*@//' | tr -d '[:space:]') + if [ -z "$latest" ]; then + echo "could not resolve the latest version of {{ pkg }}." >&2 + exit 1 + fi + just tools set "{{ pkg }}" "$latest" + +# Remove a tool entirely, e.g. `just tools remove junegunn/fzf`. +remove pkg: (_banner "tools" "remove") + #!/usr/bin/env bash + set -euo pipefail + if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}([[:space:]]|@|$)" aqua.yaml; then + echo "{{ pkg }} is not in aqua.yaml — nothing to remove." >&2 + exit 1 + fi + # See `set` for why mktemp gets an explicit sibling-path template. + tmp=$(mktemp aqua.yaml.XXXXXX) + trap 'rm -f "$tmp"' EXIT + awk -v pkg="{{ pkg }}" ' + function isCont(l) { return (l ~ /^[[:blank:]]+[^[:blank:]#-]/) } + { + if (skip) { if (isCont($0)) next; skip = 0 } + if ($0 ~ "^[[:blank:]]*-[[:blank:]]*name:[[:blank:]]*" pkg "([[:blank:]]|@|$)") { skip = 1; next } + print + } + ' aqua.yaml > "$tmp" + mv "$tmp" aqua.yaml + aqua remove "{{ pkg }}" || true # uninstall the binary (no-op for go_install tools) + aqua update-checksum --prune # drop the now-unused checksum diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..a4efb7f --- /dev/null +++ b/Justfile @@ -0,0 +1,46 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# If you want to add project specific tasks, you can do so in the `project.just` file. + +# Project name, derived from the directory the root Justfile lives in. +project := file_name(justfile_directory()) + +# Hermetic PATH: aqua-pinned tools + base system only (no homebrew), so any tool that +# isn't pinned fails loudly instead of silently resolving to an unpinned copy. +aqua_bin := env_var_or_default('AQUA_ROOT_DIR', env_var_or_default('XDG_DATA_HOME', home_directory() / '.local/share') / 'aquaproj-aqua') / 'bin' +export PATH := aqua_bin + ":/usr/bin:/bin:/usr/sbin:/sbin" + +# Hermetic Go env: ambient variables tunnel straight through the hermetic PATH. +# An inherited GOROOT (IDEs inject one, often pointing into the module cache, +# which `go clean -modcache` deletes) overrides where the pinned go finds its +# stdlib — with modern Go it should never be set: go derives it from its own +# location. Emptied rather than unexported: go treats '' as unset, and unlike +# `unexport`, an `export` propagates into module recipes. GOTOOLCHAIN=local +# forbids silent toolchain switching: when go.mod outpaces the pin, recipes +# fail loudly asking for a pin bump instead of downloading an unpinned +# toolchain behind your back. +export GOROOT := '' +export GOTOOLCHAIN := 'local' + +# Show this list of recipes. +default: + @just --list + +# Print meaningful information about this project. +info: + @echo "name: {{ project }}" + @echo "upstream: $(git remote get-url origin 2>/dev/null || echo '(none)')" + @echo "semver: $(git describe --tags --abbrev=0 2>/dev/null || echo '(none)')" + @echo "commit: $(git rev-parse --short HEAD 2>/dev/null || echo '(none)')" + @echo "date: $(git log --max-count=1 --format=%cd --date=short 2>/dev/null || echo '(none)')" + +mod build '.just/build.just' +mod tools '.just/tools.just' +mod lint '.just/lint.just' +mod test '.just/test.just' +mod fix '.just/fix.just' + +# Flat (imported, not a module) so it takes arguments: `just release v1.2.3`. +import '.just/release.just' + +import? 'project.just' diff --git a/aqua-checksums.json b/aqua-checksums.json new file mode 100644 index 0000000..cd667ce --- /dev/null +++ b/aqua-checksums.json @@ -0,0 +1,244 @@ +{ + "checksums": [ + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-aarch64-apple-darwin.tar.gz", + "checksum": "0225E3899B1B555BC3F4122D0402BA931A6EE697D188C68E4D971468064C78F4", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-aarch64-unknown-linux-musl.tar.gz", + "checksum": "B0EE814C9656427408E339893541E30D9027828686839499B2A2A34DD61AD173", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-x86_64-apple-darwin.tar.gz", + "checksum": "74D7D0AFC195DF14D8E34636FB9B6A9C7D3D443622478DBE86140FFE41AD9192", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-x86_64-unknown-linux-musl.tar.gz", + "checksum": "B0EF600F0DF20D5AE91AE931627C499FC52B477FFE5F5EA7B7B3EC616B16C778", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_linux_amd64.tar.gz", + "checksum": "83D5C2CCAD5498F58BF6368ACB1AB32588CF43AB3A4B1C301BF36328B1C8BD60", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_linux_arm64.tar.gz", + "checksum": "06F86EC7103D41993B76CD78072F43595C34AAA56506D971D9860E67140BF909", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_macOS_amd64.zip", + "checksum": "4BD449DF9AD639391BC62B8032546F0FE9EDCD8526E06682A4F88ABD8C5D163C", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_macOS_arm64.zip", + "checksum": "F23A0C37D963AACC3BED703CCBD59B41C5CA22101FAB7F00EB2B7CAD23ABA463", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.0-test.1/limen_0.0.0-test.1_darwin_amd64.tar.gz", + "checksum": "2B09B245B7F7D3C55561418ED29A885A3F2E00163A34B417901A32DC388060BC", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.0-test.1/limen_0.0.0-test.1_darwin_arm64.tar.gz", + "checksum": "FAA38E03F5F32F2B3240375A5A8A54053FB9E3DDBAAE26A38FDD2F897D49A076", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.0-test.1/limen_0.0.0-test.1_linux_amd64.tar.gz", + "checksum": "68238D869E1A3E0BEB3E5CF0871499B40EF5BC98BDC87B092B0D6F292F761651", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.0-test.1/limen_0.0.0-test.1_linux_arm64.tar.gz", + "checksum": "B0CDDBFC26EE2159A41DF9B0AF78B5CEB454907873ECD9437539150BBFDCE36C", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-amd64.tar.gz", + "checksum": "F6F06D94B6241521C53D15450C5209B028270BF966F842AFB11C030C79F5BC16", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-arm64.tar.gz", + "checksum": "A9C54498731B3128F79E090BE6110F3E5FFFCCC617B08142ED244D4126C73F29", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-amd64.tar.gz", + "checksum": "8DF580D2670FED8FA984AAC0507099AF8DF275E665215F5C7A2AE3943893A553", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-arm64.tar.gz", + "checksum": "44CD40A8C76C86755375ADFEEA52CFD3533CB43D7BD647771E0AE065E166DF3A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Darwin_arm64.tar.gz", + "checksum": "4B417ECB94339D57E4C122ECC948C1A00FE328B5853266DE9806E652A92858FA", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Darwin_x86_64.tar.gz", + "checksum": "060E943BCB8583C456810EB1FF4721B4F46C4A0C1A4432449D5DC3BBFE29A22B", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Linux_arm64.tar.gz", + "checksum": "5B2689C963B177271330C5CE8CA7396751107E5A826BE46F03D2CB9B6F0C7784", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Linux_x86_64.tar.gz", + "checksum": "1F300D9257B232BB3B541D7FB1B0E6B3C121BCBAB381C86CD38CB8722BE8A566", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Darwin_all.tar.gz", + "checksum": "6A102E4D588FD3553A9AC7321CA6023A9F205843F4861CA35BD337DFA9E72ECE", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Linux_arm64.tar.gz", + "checksum": "0102D974373FCDEB77042D1F5897CAFFA193BE36620FDC6C1DA43A01EF8E10D3", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Linux_x86_64.tar.gz", + "checksum": "EAAE05B5EBA07533BD0F06846B68C808399504784DF00C62EB219541FC04E5E2", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz", + "checksum": "99529350F4C7B780B1EFC543CA0D9721B09F0A4228F0EFA9281261F58FEFA05A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz", + "checksum": "509CB27AEF747F48FAF9BCE424F59DCF79572C905204B990EE935BBFCC7FA0E9", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz", + "checksum": "11CCDDEAF708EF228889F9FE2F68291A75B27013DDFC3B18156E094F5F40E8EE", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_arm64.tar.gz", + "checksum": "7644A4C5CD1BB978D56245AEAB25A586AC5AC62ADEBED20A399548867C13499D", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-linux-amd64", + "checksum": "B1C22172DD303F3BE49E935AA56AA48A8B7A46E0BC838B4997D3BB451495870F", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-linux-arm64", + "checksum": "8B85C817833814DDCA00A144C33705546355AFCCF0CF39B188F3CDB48B852309", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-macos-amd64", + "checksum": "E94B266E3C26690550006ABE63152B782280F4E14374ACCDF04CBDE844F00BC0", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-macos-arm64", + "checksum": "2D75340BA57A4B4B4C8708A21C2DC8E958A48AAA8BBA13B27F77F6E4C0ECA07E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz", + "checksum": "56AFFDD8DE5527894DCA6DC3D7E0A99A873B0F004D7AABC30AE407D3F48B0A79", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.xz", + "checksum": "3C89DB4EDCAB7CF1C27BFF178882E0F6F27F7AFDF54E859FA041FCA10FEBE4C6", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz", + "checksum": "12B331C1D2DB6B9EB13CFCA64306B1B157A86EB69DB83023E261EAA7E7C14588", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz", + "checksum": "8C3BE12B05D5C177A04C29E3C78CE89AC86F1595681CAB149B65B97C4E227198", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-aarch64-apple-darwin.tar.gz", + "checksum": "C9D3740EA2D891854D37116C9FBA840F37B6E7C89D330E7DB84AC333631C4977", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-aarch64-unknown-linux-musl.tar.gz", + "checksum": "5D0B0E3AEAB240F41920C633A6EAF97599BE6EEDDA034B36E858EDE7DBA5E535", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-x86_64-apple-darwin.tar.gz", + "checksum": "887503A9CFF667D322B8D0892B40BF49976EB9507AF8483220A3706CDAD55978", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz", + "checksum": "73657A111819A30C47C08352896796F23D64E4EB2B3ED39B6D32149241566FC5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-darwin-amd64", + "checksum": "14D2678DFBFDE18798151E86FBD91EBDADBB7424B18412A42A155DD8A2DF4C7A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-darwin-arm64", + "checksum": "94B42A9E697BE95675F6160AB031A9A5F1EC1E646D6F648D7B2F5CD59ECECBC5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-linux-amd64", + "checksum": "AE1ECD212663F3693AD9EDF8B1A183900C9A52D3155BA6E354237F9A0F6463FC", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-linux-arm64", + "checksum": "2EC865872E331C32FD12B08DAE15332D3F92C0AA029219589684A4903CA85D11", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.darwin-amd64.tar.gz", + "checksum": "05DC9B5F9997744520AAEBB3D5DEAA7C755371AEBBFB7F97C2511A9F3367538D", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.darwin-arm64.tar.gz", + "checksum": "B62AD2B6D7D2464F12A5BCAD7FF47F19D08325773B5EFD21610E445A05A9BF53", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.linux-amd64.tar.gz", + "checksum": "1153D3D50E0AC764B447ADFE05C2BCF08E889D42A02E0FE0259BD47F6733AD7F", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.linux-arm64.tar.gz", + "checksum": "EF758AE7C6CF9267C9C0EF080B8965F453D89AB2D25D9EB22DE4405925238768", + "algorithm": "sha256" + }, + { + "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.530.0/registry.yaml", + "checksum": "43CC45E1A514E4375608A8614CC44987EC04A5865980404D3DADBC245CF9006D", + "algorithm": "sha256" + } + ] +} diff --git a/aqua-policy.yaml b/aqua-policy.yaml new file mode 100644 index 0000000..25d035d --- /dev/null +++ b/aqua-policy.yaml @@ -0,0 +1,13 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +registries: + - type: standard + ref: semver(">= 4.0.0") + - name: local + type: local + path: .just/aqua-registry.yaml +packages: + - registry: standard + - registry: local diff --git a/aqua.yaml b/aqua.yaml new file mode 100644 index 0000000..f780543 --- /dev/null +++ b/aqua.yaml @@ -0,0 +1,48 @@ +# aqua — Declarative CLI Version Manager — https://aquaproj.github.io/ +checksum: + enabled: true + require_checksum: true + supported_envs: + - darwin/amd64 + - darwin/arm64 + - linux/amd64 + - linux/arm64 + +registries: + - type: standard + ref: v4.530.0 # renovate: depName=aquaproj/aqua-registry + - name: local + type: local + path: .just/aqua-registry.yaml + +packages: + # --- go install tools (local registry, GOSUMDB-verified) --- + - name: github.com/google/go-licenses/v2@v2.0.1 + registry: local + - name: github.com/vbatts/git-validation@v1.2.2 + registry: local + - name: golang.org/x/vuln/cmd/govulncheck@v1.5.0 + registry: local + - name: golang.org/x/tools/cmd/deadcode@v0.47.0 + registry: local + # Pseudo-version: the nested cmd/dot module carries no tags upstream. + - name: github.com/goccy/go-graphviz/cmd/dot@v0.0.0-20251129032125-76e04975df88 + registry: local + - name: github.com/farcloser/godolint/cmd/godolint@v0.1.0 + registry: local + # --- farcloser tools (local registry; standard once registered upstream) --- + # TODO: bump to v0.0.1 once the first real release is cut. + - name: farcloser/limen@v0.0.0-test.1 # renovate: depName=farcloser/limen + registry: local + # --- toolchain + binary-release tools (standard registry, aqua-verified) --- + - name: golang/go@go1.26.4 + - name: casey/just@1.55.1 + - name: koalaman/shellcheck@v0.11.0 + - name: golangci/golangci-lint@v2.12.2 + - name: google/yamlfmt@v0.21.0 + - name: lycheeverse/lychee@lychee-v0.24.2 + - name: goreleaser/goreleaser@v2.16.0 + - name: sigstore/cosign@v3.1.1 + - name: gotestyourself/gotestsum@v1.13.0 + - name: jqlang/jq@jq-1.8.2 + - name: cli/cli@v2.96.0 diff --git a/examples/quark-integration/Dockerfile b/examples/quark-integration/Dockerfile index 03ea15b..ec783bb 100644 --- a/examples/quark-integration/Dockerfile +++ b/examples/quark-integration/Dockerfile @@ -1,3 +1,3 @@ -FROM debian +FROM debian:bookworm -RUN echo something \ No newline at end of file +RUN echo something diff --git a/go.mod b/go.mod index a5ada5f..cfadf5f 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,6 @@ require ( require ( github.com/containerd/typeurl/v2 v2.3.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index 4910cd8..fb82692 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,19 @@ -github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= -github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= -github.com/moby/buildkit v0.26.2 h1:EIh5j0gzRsCZmQzvgNNWzSDbuKqwUIiBH7ssqLv8RU8= -github.com/moby/buildkit v0.26.2/go.mod h1:ylDa7IqzVJgLdi/wO7H1qLREFQpmhFbw2fbn4yoTw40= github.com/moby/buildkit v0.31.1 h1:j3p55abBl4kiXXPZgYX+6zWgB2aefqHXoPown12fIzU= github.com/moby/buildkit v0.31.1/go.mod h1:YM5iNEbNCc6L1Zt3YWFB/aXNLufvf4Rcu0DPlc9HwQg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -41,58 +24,17 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= -github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/urfave/cli/v3 v3.6.1 h1:j8Qq8NyUawj/7rTYdBGrxcH7A/j7/G8Q5LhWEW4G3Mo= -github.com/urfave/cli/v3 v3.6.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.10.1 h1:7Kx9H50hrHbRbyxgO1KP6/BcbiGRz0uYh5YyQ30JEEY= github.com/urfave/cli/v3 v3.10.1/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= -mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= diff --git a/project.just b/project.just new file mode 100644 index 0000000..57da9be --- /dev/null +++ b/project.just @@ -0,0 +1,13 @@ +# Project-specific just recipes go here. +# For example, you might want to define your own lint-all / test-all tasks +# (the aggregates CI runs; a dependency names a recipe, never a bare module): + +lint-all: lint::default lint::go::default lint::go::bce lint::go::escape lint::go::deadcode +test-all: test::go::unit test::go::race test::go::bench test::go::cover test::go::profile + +# Regenerate rule stubs and ported tests from the vendored hadolint sources +# (third-party/hadolint): gen-rules rewrites the DLxxxx metadata files (always) +# and stubs unimplemented rules; gen-tests ports the test corpus for +# implemented rules. See internal/rules/generate.go. +generate: + go generate ./internal/rules diff --git a/sdk/linter.go b/sdk/linter.go index 457deb1..cd5d21f 100644 --- a/sdk/linter.go +++ b/sdk/linter.go @@ -159,7 +159,7 @@ func (*Linter) LintFile(_ context.Context, _ string) (*Result, error) { func convertSeverity(s rule.Severity) Severity { // Info is also the deliberate fallback for severities this switch does not // know, so the default branch intentionally mirrors the Info case. - //nolint:revive // identical-switch-branches: intentional, see above. + //revive:disable:identical-switch-branches intentional, see above. switch s { case rule.Error: return SeverityError diff --git a/sdk/rules.go b/sdk/rules.go index 44239d1..b82b097 100644 --- a/sdk/rules.go +++ b/sdk/rules.go @@ -99,7 +99,7 @@ func GetRuleSet(set RuleSet) []rule.Rule { // The explicit Strict/All cases document the mapping; every other (invalid) // value also falls back to the full set, so default mirrors them on purpose. - //nolint:revive // identical-switch-branches: intentional, see above. + //revive:disable:identical-switch-branches intentional, see above. switch set { case RuleSetRecommended: // Filter to Error and Warning severity only diff --git a/tools/gen-rules/main.go b/tools/gen-rules/main.go index 3430ac5..0a2c124 100644 --- a/tools/gen-rules/main.go +++ b/tools/gen-rules/main.go @@ -259,7 +259,7 @@ func canGenerateImplementation(haskellCheck string) bool { func mapSeverity(haskellSeverity string) string { // The explicit DLWarningC case documents the hadolint mapping; Warning is // also the fallback for unknown severities, so default mirrors it on purpose. - //nolint:revive // identical-switch-branches: intentional, see above. + //revive:disable:identical-switch-branches intentional, see above. switch haskellSeverity { case "DLErrorC": return "rule.Error"