diff --git a/.github/actions/setup-aqua/action.yaml b/.github/actions/setup-aqua/action.yaml index 112008d..32f5f8d 100644 --- a/.github/actions/setup-aqua/action.yaml +++ b/.github/actions/setup-aqua/action.yaml @@ -4,7 +4,7 @@ description: > inputs: github-token: description: > - Token aqua authenticates its GitHub API calls with (exported as AQUA_GITHUB_TOKEN for the rest of the job). aqua resolves every github_release package through the API — the release by tag, its assets, the checksum file — and unauthenticated calls are limited to 60/hour PER IP, an IP the hosted runners share with every other tenant: "403 API rate limit exceeded" on the first tool a lint leg touches is that limit, not this repository's usage. The job's own token lifts it to a per-token budget. Default: the job's token, whose permissions the job declares (contents: read on every canonical read-only job). Pass "" to opt out — the write-capable checksum workflow does, by doctrine. + Token for aqua's GitHub API calls (exported as AQUA_GITHUB_TOKEN). Unauthenticated calls are limited per runner IP, shared with every tenant: a "403 API rate limit exceeded" on the first tool is that limit. Default: the job's token. Pass "" to opt out — the write-capable checksum workflow does. required: false default: ${{ github.token }} runs: @@ -20,25 +20,28 @@ runs: # renovate: depName=aquaproj/aqua AQUA_VERSION: v2.60.1 run: | - # Pin the root explicitly: aqua's own Windows build defaults it to - # %LOCALAPPDATA% (adrg/xdg), while the shell installer and the - # hermetic Justfile PATH compute unix-style ~/.local/share — without - # this, the layers disagree on Windows and nothing resolves. Exported - # to GITHUB_ENV so the recipes' aqua agrees too. + # Pin the root explicitly: aqua's Windows build defaults it to + # %LOCALAPPDATA%, the installer and the Justfile compute ~/.local/share, + # and the layers must agree. AQUA_ROOT_DIR="${AQUA_ROOT_DIR:-$HOME/.local/share/aquaproj-aqua}" - # On windows the pin must be in NATIVE form: git-bash's $HOME is a - # POSIX-only path (/c/Users/...) that native binaries (aqua, - # aqua-proxy, just) misread as current-drive-relative — aqua would - # link tools under D:\c\... while bash's PATH looks in C:\Users\.... - # cygpath -m yields C:/Users/..., which every layer reads correctly - # (git-bash included). + # Native form on windows: git-bash's POSIX $HOME (/c/Users/…) is misread + # by native binaries as drive-relative; cygpath -m yields C:/Users/…, + # which every layer reads. if command -v cygpath >/dev/null 2>&1; then AQUA_ROOT_DIR="$(cygpath -m "$AQUA_ROOT_DIR")" fi export AQUA_ROOT_DIR echo "AQUA_ROOT_DIR=${AQUA_ROOT_DIR}" >>"$GITHUB_ENV" tmp="$(mktemp -d)" - curl --proto '=https' --tlsv1.2 -fsSL -o "${tmp}/aqua-installer" \ + # Retries for transient runner failures. Windows curl (schannel) refuses + # a handshake when the CA's revocation server is unreachable (exit 35, + # seen repeatedly): best-effort revocation still rejects a known-revoked + # certificate, and the sha256 below is the real guard. schannel-only + # flag, so windows-only. + revoke=() + [ "${RUNNER_OS:-}" != "Windows" ] || revoke=(--ssl-revoke-best-effort) + curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors "${revoke[@]}" \ + -o "${tmp}/aqua-installer" \ "https://raw.githubusercontent.com/aquaproj/aqua-installer/${AQUA_INSTALLER_VERSION}/aqua-installer" # No single digest tool exists everywhere: linux and windows git-bash # ship coreutils sha256sum, macOS ships perl shasum. (Nothing @@ -50,20 +53,11 @@ runs: echo "${AQUA_INSTALLER_SHA256} ${tmp}/aqua-installer" | shasum -a 256 -c - fi chmod +x "${tmp}/aqua-installer" - # The installer picks aqua's build from `uname -m`. On the windows - # arm64 runners that answer is WRONG: Git for Windows' arm64 build - # ships an x86_64 Git Bash (git.exe is native; bash, coreutils and - # uname are not — its release notes say so), so under this shell - # `uname -m` is x86_64, the installer fetches aqua_windows_amd64, and - # that aqua then resolves EVERY package for windows/amd64: go, just, - # gotestsum, the race runtime — the whole leg emulated, and no arm64 - # coverage at all. The runner knows what it is (RUNNER_ARCH); tell the - # installer through the one input it reads. A shim on PATH, for the - # installer's process only, answering `uname -m` with aarch64: the - # installer's own checksum table already covers aqua_windows_arm64 - # and its bootstrap aqua then installs the pinned version natively — - # every byte still verified the same way, only the choice corrected. - # Self-retiring: skipped once the shell's uname is honest. + # Git for Windows' arm64 build ships an x86_64 bash, so `uname -m` lies + # and the installer would fetch amd64 aqua, which then resolves EVERY + # package emulated. RUNNER_ARCH is the truth: a uname shim for the + # installer's process only steers it, every byte still + # checksum-verified. Self-retiring once the shell's uname is honest. if [ "${RUNNER_OS:-}" = "Windows" ] && [ "${RUNNER_ARCH:-}" = "ARM64" ] && [ "$(uname -m)" != "aarch64" ]; then echo "windows/arm64 runner under an emulated shell (uname -m: $(uname -m)); steering the installer to arm64" mkdir -p "${tmp}/shim" @@ -93,11 +87,9 @@ runs: shell: bash run: aqua policy allow aqua-policy.yaml - name: Authenticate aqua's GitHub API calls - # See the github-token input. Via env, never interpolated into the - # script; GITHUB_ENV so every later step's aqua (and the shims it links) - # sees it — that is where the downloads actually happen, lazily, on - # first use. AQUA_GITHUB_TOKEN rather than GITHUB_TOKEN: only aqua reads - # it, nothing else in the job inherits a credential it did not ask for. + # GITHUB_ENV so every later step's aqua and its shims see it (downloads + # happen lazily, on first use). AQUA_GITHUB_TOKEN, not GITHUB_TOKEN: only + # aqua reads it. if: inputs.github-token != '' shell: bash env: diff --git a/.github/workflows/update-aqua-checksum.yaml b/.github/workflows/update-aqua-checksum.yaml index 4da45ef..9c48623 100644 --- a/.github/workflows/update-aqua-checksum.yaml +++ b/.github/workflows/update-aqua-checksum.yaml @@ -1,61 +1,24 @@ # DO NOT EDIT MANUALLY. -# This workflow is generic — no project-specific content — and is destined to -# become part of the canonical baseline limen distributes. +# Generic, no project content. Renovate's other half: on its branches, +# regenerate aqua-checksums.json (a bumped pin with a stale checksum breaks +# every install) and, when the branch bumps farcloser/limen, run the branch's +# own pinned `limen fix` so the canonical files move with the pin. One fix-up +# commit. # -# The other half of Renovate: the bot bumps versions in aqua.yaml but cannot -# do the repo-specific follow-up, so without this workflow every version-bump -# PR would merge half-applied. Two follow-ups, on pushes to Renovate's -# branches, one fix-up commit: -# 1. Regenerate aqua-checksums.json with real aqua — a bumped pin with a -# stale checksum breaks every install. -# 2. Converge the limen baseline: when the branch bumps the farcloser/limen -# pin, the repo's canonical files must move with it — a repo is coherent -# only when the limen that wrote its files is the limen it pins (an old -# baseline checked by the new limen is red, and vice versa). The -# branch's own pinned limen runs `fix`; on branches bumping anything -# else it is a no-op. +# A WRITE workflow, hardened: `push:` on the branch prefix, never +# pull_request_target; no credential kept by checkout and no secrets in the +# update steps' environment; nothing runs through `just` (a write job does not +# execute recipe code the branch controls); the commit step uses runner tools +# only, with the token scoped to that step; the branch name reaches the shell +# via env; no loop, since a default-token commit triggers no workflows. # -# This is a WRITE workflow — the hardening is deliberate: -# - `push:` on the branch prefix, never pull_request_target: it runs in the -# repo's own context, on branches only writers (the Renovate app) can -# create. -# - The checkout keeps no credential, and the update steps run with no -# secrets in their environment. Neither goes through `just` — the one -# sanctioned deviation: a write-capable workflow does not execute recipe -# code the branch controls. `aqua update-checksum` only downloads and -# hashes declared artifacts; the converge step executes exactly one -# binary, the checksum-pinned limen release the branch declares. -# - The commit step runs only runner-provided tools (git, jq, base64, -# curl) — no branch-pinned binary ever touches the token, which is -# scoped to that single step. -# - The branch name reaches the shell via env, never template interpolation -# (script-injection hygiene). -# - No loop: a commit made with the default GITHUB_TOKEN triggers no -# further workflows — and the no-change early exit terminates recursion -# regardless. -# -# The commit is created through the GraphQL createCommitOnBranch mutation, -# never a local `git commit` + push: GitHub signs the mutation's commits, and -# the `limen:main` ruleset requires signatures — an unsigned fix-up commit -# made every aqua-bump PR unmergeable (see the commit step for the full -# argument). Consequence of the mutation: the commit's author IS the token's -# identity, always — with the default token that is github-actions[bot], -# already listed in the canonical renovate.json5's gitIgnoredAuthors. -# -# Known trade of the default token: GitHub suppresses workflow runs for -# commits it pushes, so the PR's CI does not re-run on the checksum commit. -# To get CI on the final state of Renovate PRs, register a GitHub App — -# contents:write only, webhook disabled, installed on the org; no infra, it -# is just an identity — and set the org variable UPDATE_AQUA_CHECKSUM_APP_ID -# plus the org secret UPDATE_AQUA_CHECKSUM_APP_PRIVATE_KEY: the workflow -# then mints a one-hour, this-repo-only token per run, so there is no -# long-lived broad credential and nothing that expires on a calendar. A -# fine-grained PAT with contents:write as UPDATE_AQUA_CHECKSUM_TOKEN is the -# drop-in alternative; the token preference order is App, PAT, default. -# With an App or PAT the commit is authored as that identity (the mutation -# offers no override), so it must be added to gitIgnoredAuthors in -# renovate.json5 — or Renovate treats the branch as human-modified and -# stops rebasing it. +# The commit goes through GraphQL createCommitOnBranch because GitHub signs +# those and the main ruleset requires signatures (see the commit step). Its +# author is the token's identity: github-actions[bot] by default, or the +# App/PAT identity when UPDATE_AQUA_CHECKSUM_APP_ID (+ private key secret) or +# UPDATE_AQUA_CHECKSUM_TOKEN is set — that identity must be in renovate.json's +# gitIgnoredAuthors or Renovate stops rebasing. The App is worth it: +# default-token commits get no CI run. name: update-aqua-checksum on: @@ -68,10 +31,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# Every run step is bash, explicitly: runner-OS shell defaults are a trap -# (windows defaults to PowerShell), and explicit bash adds `-eo pipefail`. -# Uniform across all canonical workflows so adding a runner never changes -# what the steps mean. +# Explicit bash: windows defaults to PowerShell, and bash brings -eo pipefail. defaults: run: shell: bash @@ -90,12 +50,10 @@ jobs: - name: Install aqua (pinned, checksum-verified) uses: ./.github/actions/setup-aqua with: - # No token for aqua here, by doctrine: this job's token carries - # contents:write, and the update steps below run a branch-declared - # binary (the branch's limen pin). Unauthenticated API calls are - # rate-limited per runner IP; this is one job per Renovate branch, - # and a 403 here fails loudly and is rerun — a write token in the - # environment of branch-controlled code would not fail loudly. + # No token for aqua here: this job's token carries contents:write and + # the steps below run a branch-declared binary. A rate-limit 403 + # fails loudly and is rerun; a write token in branch-controlled code + # would not fail loudly. github-token: "" - name: Regenerate aqua-checksums.json @@ -106,32 +64,28 @@ jobs: run: aqua update-checksum --prune - name: Converge the limen baseline - # `aqua exec` resolves the branch's own farcloser/limen pin (checksums - # fresh from the step above), downloads it verified, and `limen fix` - # rewrites whatever the new baseline moved. A released limen leaves - # its own pin alone (it already matches), so this cannot ping-pong. - # Skipped in the limen repository itself: there the working tree IS - # the next baseline (the recipes run it via LIMEN_BIN), and a released - # limen "converging" it would revert in-flight baseline work. + # `aqua exec` resolves the branch's own limen pin, verified with the + # checksums from the step above. A released limen leaves its own pin + # alone, so this cannot ping-pong. Skipped in the limen repository: + # there the working tree IS the next baseline. if: github.repository != 'farcloser/limen' run: aqua exec -- limen fix . - name: Mint a push token, if the App is configured - # Placed after the update steps so those still run with no secrets in - # their environment. The minted token lives one hour and is scoped to - # this repository only; the long-lived private key exists solely to - # mint and never authorizes a push itself. Guarded on the variable so - # repos without the App fall through to the token chain below. + # After the update steps, so those run with no secrets. One-hour token, + # this repository only; the private key only mints. if: vars.UPDATE_AQUA_CHECKSUM_APP_ID != '' id: app-token uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 with: app-id: ${{ vars.UPDATE_AQUA_CHECKSUM_APP_ID }} private-key: ${{ secrets.UPDATE_AQUA_CHECKSUM_APP_PRIVATE_KEY }} - # Least privilege: the minted token only ever pushes a commit, so scope it - # to contents:write rather than inheriting whatever the App installation - # happens to hold now or later. + # Least privilege, but both: naming any permission scopes the token to + # exactly those, and the fix-up carries canonical workflow files whenever + # a limen bump changed one. GitHub refuses that commit from a token + # without workflows:write ("Resource not accessible by integration"). permission-contents: write + permission-workflows: write - name: Commit the update through the API, if any env: @@ -139,51 +93,54 @@ jobs: TOKEN: ${{ steps.app-token.outputs.token || secrets.UPDATE_AQUA_CHECKSUM_TOKEN || github.token }} run: | [ -z "$(git status --porcelain)" ] && { echo "checksums and baseline already in sync"; exit 0; } - # The commit is made through GraphQL createCommitOnBranch, not a - # local `git commit` + push, because commits made through that - # mutation are signed by GitHub — and the `limen:main` ruleset - # requires signatures on everything landing on the default branch. - # A plain git push is unsigned, and one unsigned commit on the - # branch made every aqua-bump PR unmergeable: a merge commit would - # land it, GitHub disables rebase while signatures are required, - # and squash of a bot-authored PR is refused to everyone but the - # bot (see limen's book/github.md). + # GraphQL createCommitOnBranch, not `git commit` + push: those commits + # are GitHub-signed, and the main ruleset requires signatures — an + # unsigned fix-up commit made every aqua-bump PR unmergeable + # (book/github.md). # - # Enumerating the whole dirty tree is deliberate, not sloppiness. - # The tree is fully accounted for: a fresh checkout, aqua rooted - # outside the workspace, and exactly two writers between checkout - # and here — update-checksum and the pinned limen's fix. Whatever - # is dirty IS the payload. An enumerated path list would be wrong: - # the converge step's job is to commit whatever the NEW limen's - # baseline says, and a list baked into the older running workflow - # cannot know that surface (the updated workflow arrives in the - # very commit being built). Nor would a list add safety — the only - # writer that could plant a file is limen fix itself, and - # .limen/.github would be on any list. + # The whole dirty tree, on purpose: exactly two writers ran since + # checkout (update-checksum and the pinned limen's fix), and a path + # list baked into the older running workflow cannot know the new + # baseline's surface. # # Limit of the mutation, accepted: FileAddition carries no file # mode, so an executable bit cannot travel — both writers here # only ever produce plain configuration files. - additions="[]" - deletions="[]" + # Every file the payload touches travels through a FILE, never a + # command line. A converged baseline is ~120 kB of configuration, + # ~160 kB once base64'd, and Linux caps a single argument at 128 kB + # (MAX_ARG_STRLEN): passing the accumulated array as `--argjson` died + # with "jq: Argument list too long" and left the branch with a bumped + # pin, no checksum, and a red pipeline that never reached its first + # recipe. `--rawfile` and `--slurpfile` read from disk; the request + # body is uploaded from disk too. + work="$(mktemp -d)" + trap 'rm -rf "${work}"' EXIT + printf '[]' > "${work}/additions.json" + printf '[]' > "${work}/deletions.json" while IFS= read -r -d '' entry; do path="${entry:3}" if [ -f "${path}" ]; then - additions="$(jq --arg path "${path}" --arg contents "$(base64 -w0 "${path}")" \ - '. + [{path: $path, contents: $contents}]' <<<"${additions}")" + base64 -w0 "${path}" > "${work}/blob" + jq --arg path "${path}" --rawfile contents "${work}/blob" \ + '. + [{path: $path, contents: ($contents | rtrimstr("\n"))}]' \ + "${work}/additions.json" > "${work}/next" + mv "${work}/next" "${work}/additions.json" else - deletions="$(jq --arg path "${path}" '. + [{path: $path}]' <<<"${deletions}")" + jq --arg path "${path}" '. + [{path: $path}]' \ + "${work}/deletions.json" > "${work}/next" + mv "${work}/next" "${work}/deletions.json" fi done < <(git status --porcelain -z) # Signed-off-by: `just do lint commits` enforces DCO on the PR range, # bot commits included. - payload="$(jq -n \ + jq -n \ --arg query 'mutation ($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }' \ --arg repository "${GITHUB_REPOSITORY}" \ --arg branch "${BRANCH}" \ --arg head "$(git rev-parse HEAD)" \ - --argjson additions "${additions}" \ - --argjson deletions "${deletions}" \ + --slurpfile additions "${work}/additions.json" \ + --slurpfile deletions "${work}/deletions.json" \ '{query: $query, variables: {input: { branch: {repositoryNameWithOwner: $repository, branchName: $branch}, expectedHeadOid: $head, @@ -191,11 +148,16 @@ jobs: headline: "chore: update aqua checksums and converge the limen baseline", body: "Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" }, - fileChanges: {additions: $additions, deletions: $deletions}}}}')" + fileChanges: {additions: $additions[0], deletions: $deletions[0]}}}}' \ + > "${work}/payload.json" # GraphQL reports failure in-body over HTTP 200, so success is the - # presence of the new commit oid, never the HTTP status. - response="$(curl -sS -X POST -H "Authorization: bearer ${TOKEN}" \ - -d "${payload}" https://api.github.com/graphql)" + # presence of the new commit oid, never the HTTP status. No --retry + # and no -L, on purpose: this is a mutation guarded by + # expectedHeadOid — a retry after a success that timed out on the + # way back would fail on the moved head and report a commit that + # landed as an error, and a redirect must never replay the POST. + response="$(curl --proto '=https' --tlsv1.2 -fsS -X POST -H "Authorization: bearer ${TOKEN}" \ + --data-binary "@${work}/payload.json" https://api.github.com/graphql)" oid="$(jq -r '.data.createCommitOnBranch.commit.oid // empty' <<<"${response}")" if [ -z "${oid}" ]; then echo "${response}" >&2 diff --git a/.limen/.yamlfmt b/.limen/.yamlfmt index 8269a9c..fe0cc6b 100644 --- a/.limen/.yamlfmt +++ b/.limen/.yamlfmt @@ -17,21 +17,13 @@ formatter: 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. + retain_line_breaks_single: true # collapse runs of blank lines to one, keep single ones 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.) + max_line_length: 0 # no wrapping: yamlfmt's wrapping mangles long values # --- which files to format --- # Every pattern below is doublestar syntax (`**/`, `{yaml,yml}` braces), which diff --git a/.limen/aqua-registry.yaml b/.limen/aqua-registry.yaml index da3f46b..81494b2 100644 --- a/.limen/aqua-registry.yaml +++ b/.limen/aqua-registry.yaml @@ -4,83 +4,8 @@ # Addition of non-standard tools can be discussed on https://github.com/farcloser/limen packages: - # Every go_install entry names its GitHub repo (repo_owner/repo_name): that - # is where aqua lists versions from (`aqua update`, hence - # `just do tools update `), and without it the update is a silent no-op. - # Naming the repo changes aqua's default package name to owner/repo, so - # `name:` is spelled out as the import path every time — the name aqua.yaml - # pins under. - # - # 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 - name: github.com/google/go-licenses/v2 - path: github.com/google/go-licenses/v2 - repo_owner: google - repo_name: go-licenses - description: Report on the licenses of a Go project's dependencies - version_source: github_tag - files: - - name: go-licenses - - type: go_install - name: github.com/vbatts/git-validation - path: github.com/vbatts/git-validation - repo_owner: vbatts - repo_name: 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 do test go profile`. The cmd/dot module is - # nested and untagged upstream, hence the pseudo-version pin in aqua.yaml. - # Its versions, when upstream ever tags it, will be the nested-module tags - # `cmd/dot/vX.Y.Z` (Go's convention) — so that is the only shape aqua may - # propose: the root module's v0.2.x tags do not contain cmd/dot and would - # break the install. Until such a tag exists `aqua update dot` finds nothing - # and leaves the pin alone. - - 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 - version_prefix: cmd/dot/ - version_filter: Version startsWith "cmd/dot/" - # 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. - # golang/tools also tags gopls releases (gopls/vX.Y.Z) in the same repo; - # those are not x/tools versions and are filtered out, as the standard - # registry does for its own x/tools commands. - - 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 - version_filter: not (Version startsWith "gopls/") - # 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 + # No go_install entries: every Go-built tool is a tool directive in + # tools/go.mod (book/tooling.md, "Go-built tools are go.mod tools"). # 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 @@ -97,11 +22,9 @@ packages: type: github_release asset: checksums.txt algorithm: sha256 - # Signatures are verified whenever upstream publishes them (see the - # book's signature doctrine): the release signs checksums.txt keyless - # from CI (goreleaser cosign lane), which transitively covers every - # asset the file lists. The identity IS the release workflow at the - # exact tag — a bundle signed by anything else fails the install. + # Signed keyless from CI: the bundle over checksums.txt transitively + # covers every listed asset, and the identity IS the release workflow at + # the exact tag — anything else fails the install. cosign: bundle: type: github_release @@ -115,3 +38,37 @@ packages: - darwin/arm64 - linux - windows + # uutils/coreutils under the names recipes type: the binary dispatches on + # argv[0], so `sha256sum` and `base64` link to it and a bare name on the + # hermetic PATH is the pin on every platform. Carried locally until the + # extra names land upstream; when upstream's asset naming changes, this copy + # needs the same edit. Upstream publishes no checksum file and no + # provenance: verify each asset against GitHub's release attestation at + # every bump (`gh release verify-asset -R uutils/coreutils`), + # which aqua cannot check itself. + - type: github_release + repo_owner: uutils + repo_name: coreutils + description: Cross-platform Rust rewrite of the GNU coreutils + asset: coreutils-{{.Version}}-{{.Arch}}-{{.OS}}.{{.Format}} + format: tar.gz + replacements: + amd64: x86_64 + arm64: aarch64 + darwin: apple-darwin + linux: unknown-linux-musl + windows: pc-windows-msvc + overrides: + - goos: windows + format: zip + files: + - name: coreutils + src: coreutils-{{.Version}}-{{.Arch}}-{{.OS}}/coreutils + - name: sha256sum + src: coreutils-{{.Version}}-{{.Arch}}-{{.OS}}/coreutils + - name: base64 + src: coreutils-{{.Version}}-{{.Arch}}-{{.OS}}/coreutils + supported_envs: + - darwin/arm64 + - linux + - windows diff --git a/.limen/just/build-go.just b/.limen/just/build-go.just index 60977ee..ef13239 100644 --- a/.limen/just/build-go.just +++ b/.limen/just/build-go.just @@ -10,44 +10,22 @@ 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 the root Justfile, the home of all -# project customization (the variable name mirrors the task path), or set it -# on the invocation for a one-off. +# Binaries land in build/. Projects declare `var version = "dev"` in each main +# package for the stamp. Extra flags: export BUILD_GO_FLAGS from the root +# Justfile, or set it on the invocation. # -# Extra LINKER flags: BUILD_GO_LDFLAGS, same convention. It exists because -# -ldflags cannot come through BUILD_GO_FLAGS: go takes the LAST occurrence of a -# repeated flag, and BUILD_GO_FLAGS is appended after the -ldflags these recipes -# compute, so a project passing its own would silently REPLACE the version stamp -# and the CGO linkmode rather than add to them. BUILD_GO_LDFLAGS is appended -# INSIDE the -ldflags string instead, last, so it composes — and, being last, -# also wins on a genuine conflict (a project overriding -X main.version). -# Typical use is a second stamp the project needs pinned at link time: -# export BUILD_GO_LDFLAGS := '-X main.someImage=' + some_digest_pinned_ref +# BUILD_GO_LDFLAGS exists because -ldflags cannot ride BUILD_GO_FLAGS: go keeps +# the LAST -ldflags, so a project's own would replace the version stamp and CGO +# linkmode. It is appended inside the -ldflags string, last, so it composes and +# wins a genuine conflict. # 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 off unless asked for: per invocation with `CGO_ENABLED=1 just do build -# go`, or for good with `export GO_CGO := '1'` in the root Justfile, for a project -# whose product cannot link without it. Every recipe below resolves it through -# lib.just's `go_cgo`, which documents the precedence and why it is a shell -# expansion rather than a just variable. +# CGO is off unless a project exports GO_CGO := '1' (or CGO_ENABLED=1 for one +# run); resolution in lib.just. # --- C toolchain hardening — only effective when CGO_ENABLED=1 -------------- # The canonical hardening set (see the Red Hat compiler-flags guidance), diff --git a/.limen/just/do.just b/.limen/just/do.just index 34a69d2..793c591 100644 --- a/.limen/just/do.just +++ b/.limen/just/do.just @@ -7,10 +7,6 @@ set working-directory := '../..' # Silence just's per-line command echo; recipes announce themselves via _banner. set quiet -# The `do` namespace holds every shared task, so the top level stays free for -# a project's own recipes (a project may define its own `just lint`; the -# shared one is `just do lint`). - # Show the shared tasks. default: @just --list do diff --git a/.limen/just/fix-go.just b/.limen/just/fix-go.just index 6662c3e..e3d092c 100644 --- a/.limen/just/fix-go.just +++ b/.limen/just/fix-go.just @@ -12,23 +12,14 @@ 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 last, so the -# fixes' edits end up formatted — but it runs to CONVERGENCE, not once. -# -# `golangci-lint fmt` applies every enabled formatter in a single pass, and the -# set is not confluent: one formatter's output can be work for another (gofumpt -# wanting a blank line that golines' rewrite exposed, say). One pass can -# therefore leave a tree that `fmt --diff` still rejects — so `just fix` would -# report success and `just lint` immediately fail, which is the worst possible -# pairing. Observed on protoc-gen-go output, which needs exactly two passes. -# -# Bounded, because non-convergence has two causes and only one is benign: a -# formatter set that merely needs another pass settles in two or three, while -# two formatters that undo each other never settle. Loop for the first, fail -# loudly for the second instead of hanging CI forever. -code: (_banner "fix go" "go") (_per-goos "golangci-lint run --fix") +# golangci-lint auto-fixes, once per supported platform (see _per-platform in +# lib.just). The formatter runs LAST and to CONVERGENCE: `golangci-lint fmt` +# applies every formatter in one pass and the set is not confluent (one +# formatter's output can be another's work), so a single pass can leave a tree +# `fmt --diff` still rejects — `just fix` green, `just lint` red. Bounded, +# because two formatters undoing each other never settle: loop for the benign +# case, fail loudly for the other. +code: (_banner "fix go" "go") (_per-platform "golangci-lint run --fix") #!/usr/bin/env bash set -euo pipefail max=5 @@ -45,9 +36,10 @@ code: (_banner "fix go" "go") (_per-goos "golangci-lint run --fix") golangci-lint fmt done -# Tidy go.mod/go.sum. +# Tidy go.mod/go.sum, for the module and for tools/ (the analyzers' module). mod: (_banner "fix go" "mod") go mod tidy + go -C tools mod tidy # Update every dependency to its latest version, then tidy. up: (_banner "fix go" "up") && mod diff --git a/.limen/just/fix-homebrew.just b/.limen/just/fix-homebrew.just index c154dbe..4cb584a 100644 --- a/.limen/just/fix-homebrew.just +++ b/.limen/just/fix-homebrew.just @@ -10,10 +10,8 @@ set quiet # Shared private _banner recipe (see lib.just). import 'lib.just' -# The mutating counterpart of `just do lint homebrew` — brew's vendored -# RuboCop with --fix. Same machine-layer exception as the lint module: -# BREW_BIN is captured from the ambient PATH at just startup, never from the -# hermetic PATH (the reasoning lives in lint-homebrew.just and main.just). +# The mutating twin of `just do lint homebrew`; BREW_BIN as in +# lint-homebrew.just. default: style @@ -23,11 +21,7 @@ style: (_banner "fix homebrew" "style") #!/usr/bin/env bash set -euo pipefail {{ _skip_unless_macos }} - # --cached --others --exclude-standard: tracked *and* new untracked files, - # still honoring .gitignore (same as `fix just`). Modern tap layout ONLY — - # Formula/ and Casks/; the legacy locations are deliberately unsupported - # (see lint-homebrew.just). Discovery runs before the brew lookup so a - # repo without formulas passes vacuously even brew-less. + # Same discovery as `lint homebrew style`. files=() while IFS= read -r -d '' f; do # Tracked-but-deleted files are skipped (see lint.just's `just` recipe). diff --git a/.limen/just/fix.just b/.limen/just/fix.just index 19e1a74..5ebc45c 100644 --- a/.limen/just/fix.just +++ b/.limen/just/fix.just @@ -12,10 +12,8 @@ import 'lib.just' default: limen just yaml aqua -# limen itself: repair what `just do 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). +# Repairs what `just do lint limen` reports; regenerates aqua-checksums.json +# when pins changed (network). Same LIMEN_BIN override as lint.just. limen: (_banner "fix" "limen") ${LIMEN_BIN:-limen} fix . @@ -28,13 +26,7 @@ github *args: (_banner "fix" "github") 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. - # Tracked-but-deleted files are skipped (see `lint just` for the rationale: - # enumeration is git's, truth is the working tree's). - # Same pathspecs as `lint just`: literals carry their '**/' twins (git - # pathspec literals are case-sensitive and do not match across - # directories), and the hidden .justfile spelling is covered. + # Same enumeration and pathspecs as `lint just`. while IFS= read -r -d '' f; do [ -f "$f" ] || continue just --fmt --justfile "$f" diff --git a/.limen/just/lib.just b/.limen/just/lib.just index 15f421e..1741ee9 100644 --- a/.limen/just/lib.just +++ b/.limen/just/lib.just @@ -2,65 +2,67 @@ # This file provides shared tasks common to all projects and managed by limen. # Project recipes live in the root Justfile, below the shared-baseline import. -# Uniform per-recipe banner, shared across the lint/fix/tools modules via `import` -# (a `mod` can't be depended on, and modules don't inherit the parent's recipes). -# Private → hidden from --list. The module name is passed in because just has no -# current-module variable; depended on with arguments so it prints identically for -# plain *and* shebang recipes, which just otherwise echoes inconsistently (plain -# recipes echo their lines; shebang recipes do not). +# Per-recipe banner, imported (a `mod` cannot be depended on). Depended on WITH +# arguments so plain and shebang recipes print it identically. _banner mod name: @echo "▶ {{ mod }}: {{ name }}" -# go_cgo answers "does THIS project build with cgo?" for every Go recipe — -# `export CGO_ENABLED="{{ go_cgo }}"` in a shebang body, or as a command prefix in -# a plain one. Interpolated into build, test and the per-platform analysis legs so -# they cannot disagree about the nature of the project. +# go_cgo: "does THIS project build with cgo?", shared by build, test and the +# per-platform legs. # -# It is a SHELL EXPANSION, not a just value, and that is the entire design: it is -# evaluated when the recipe runs, where a parent's exports are present. Resolving -# it at parse time instead — `export CGO_ENABLED := env('CGO_ENABLED', '0')`, how -# this once read — cannot work: a child module cannot see its parent's variables, -# and env() reads just's PROCESS environment, which never holds one, so a project's -# `export GO_CGO := '1'` was silently discarded and 0 re-exported over it. +# It is a SHELL expansion, not a just value, on purpose: a child module cannot +# see its parent's variables and env() reads just's process environment, so +# `export CGO_ENABLED := env('CGO_ENABLED', '0')` here would silently discard a +# project's GO_CGO. # -# Precedence, in order: an explicit CGO_ENABLED on the invocation wins (including -# downgrading a cgo project for a one-off pure-Go run), then the project's GO_CGO, -# then off — because go's own default is cgo ON for native builds wherever a C -# toolchain happens to exist, which would make the same tree behave differently on -# two machines. +# Precedence: CGO_ENABLED on the invocation, then the project's GO_CGO, then +# off — go's own default is cgo on wherever a C toolchain happens to exist. go_cgo := '${CGO_ENABLED:-${GO_CGO:-0}}' -# Run CMD once per supported platform (the list below mirrors the canonical -# checksum.supported_envs in aqua.yaml) with CGO disabled: the Go build/dependency graph -# differs per GOOS, so a single native run misses the other platforms' files -# and dependencies. CGO prevents exactly that — loading packages for a foreign -# GOOS needs that platform's C toolchain — so a project that genuinely needs -# cgo declares it (`export GO_CGO := '1'`, or CGO_ENABLED=1 for one invocation) +# Run CMD once per supported platform — every GOOS/GOARCH pair in the canonical +# checksum.supported_envs of aqua.yaml (the list below mirrors it; extend them +# together) — with CGO disabled. The Go build graph is selected by both axes: +# a file built only on linux is invisible to a darwin run, and a file built +# only on amd64 (an _amd64.go, an _amd64.s) is invisible to an arm64 one, so a +# GOOS-only loop at the host's architecture silently skips half the matrix. +# CGO would prevent exactly this — loading packages for a foreign platform +# needs that platform's C toolchain — so a project that genuinely needs cgo +# declares it (`export GO_CGO := '1'`, or CGO_ENABLED=1 for one invocation) # and gets one native run only: reduced coverage, announced loudly. -# The same resolution as the build module, so ONE project knob governs every Go -# task — analysis and build agree on whether this project is a cgo project. -_per-goos +cmd: +_per-platform +cmd: #!/usr/bin/env bash set -euo pipefail if [ "{{ go_cgo }}" = "1" ]; then - echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-GOOS package" >&2 + echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-platform package" >&2 echo "loading needs that platform's C toolchain) — native platform only." >&2 {{ cmd }} else - # Native platform FIRST, always: go_install tools build lazily on - # first use, and `go install` refuses to cross-compile when GOBIN is - # set (which is how aqua builds Go tools) — so a cold start must - # trigger that build on the native leg; the foreign legs then merely - # run the already-built binary. - # Extend this list together with checksum.supported_envs in aqua.yaml. - platforms=(darwin linux windows) - native="$(go env GOHOSTOS)" - ordered=("$native") - for goos in "${platforms[@]}"; do - [ "$goos" = "$native" ] || ordered+=("$goos") - done - for goos in "${ordered[@]}"; do - echo "→ GOOS=$goos" - CGO_ENABLED=0 GOOS="$goos" {{ cmd }} + platforms=(darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64) + for platform in "${platforms[@]}"; do + echo "→ GOOS=${platform%/*} GOARCH=${platform#*/}" + CGO_ENABLED=0 GOOS="${platform%/*}" GOARCH="${platform#*/}" {{ cmd }} done fi + +# Every Go-built tool the recipes run — the Go-source analyzers and the rest +# alike — is a `tool` directive in tools/go.mod, a module of its own, built +# natively ONCE into build/tools/ by the pinned go (book/tooling.md, "Go-built +# tools are go.mod tools"). Never `go tool ` from a per-platform leg: it +# honours GOOS/GOARCH and would cross-compile the tool itself (exec format +# error). A tools/go.mod without the directive fails with the command to add +# it. Recipes depend on this with the tool's name and package, then call +# build/tools/. +_go-tool name pkg: + #!/usr/bin/env bash + set -euo pipefail + if ! grep -qE '^(tool[[:space:]]+)?[[:space:]]*{{ pkg }}([[:space:]]|$)' tools/go.mod 2>/dev/null; then + echo "tools/go.mod lacks 'tool {{ pkg }}' — add it: go -C tools get -tool {{ pkg }}@ && go -C tools mod tidy (see book/tooling.md)" >&2 + exit 1 + fi + mkdir -p build/tools + # Directory form of -o: go names the binary itself, which on windows means + # the .exe suffix the loader needs — git-bash then resolves the bare + # build/tools/ the recipes call to it. (go strips a /vN major suffix + # when naming, so github.com/google/go-licenses/v2 lands as go-licenses.) + # -o is resolved relative to -C. + GOOS='' GOARCH='' go -C tools build -o '../build/tools/' '{{ pkg }}' diff --git a/.limen/just/lint-go.just b/.limen/just/lint-go.just index fef0795..5a17e8f 100644 --- a/.limen/just/lint-go.just +++ b/.limen/just/lint-go.just @@ -10,57 +10,126 @@ set quiet # Shared private _banner recipe (see lib.just). import 'lib.just' -default: code mod vuln licenses +default: code vet 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. The last step bans -# blanket revive suppressions: nolintlint polices golangci directives, and -# revive findings proved environment-nondeterministic across the per-GOOS -# legs — the suppression itself then flakes as "unused". revive's own -# selective directives are invisible to nolintlint, hence stable. -code: (_banner "lint go" "go") (_per-goos "golangci-lint run") +# golangci-lint, once per supported platform (see _per-platform in lib.just: +# platform-specific files are only analyzed for the GOOS/GOARCH that builds +# them). +# The formatter is build-graph-independent and runs once. The last step +# enforces the suppression doctrine (book/per-language.md, "silencing a +# finding"): a finding is silenced by its rule, never by its linter. +code: (_banner "lint go" "go") _golangci-toolchain-check (_per-platform "golangci-lint run") _suppression-check golangci-lint fmt --diff - @if git grep -nE 'nolint:[a-z, ]*revive' -- '*.go'; then echo 'blanket revive suppression is banned — use //revive:disable-next-line: (or a disable/enable block) instead' >&2; exit 1; fi -# go.mod/go.sum tidiness — fails printing the diff; `just do fix go mod` repairs. +# Polices the shape the book requires (per-language.md, "silencing a +# finding"): nolintlint checks a directive's form, not which linter it names, +# and bag linters (revive, gosec, staticcheck) must be silenced per rule. +# Tracked Go files only. +[private] +_suppression-check: + #!/usr/bin/env bash + set -euo pipefail + fail=0 + # ban []: every tracked Go line matching + # and not is a violation. + ban() { + local hits + hits="$(git grep -nE "$2" -- '*.go' || true)" + if [ -n "$hits" ] && [ -n "${3:-}" ]; then hits="$(printf '%s\n' "$hits" | grep -vE "$3" || true)"; fi + if [ -n "$hits" ]; then printf '%s\n' "$hits" >&2; echo "$1" >&2; fail=1; fi + } + ban 'blanket revive suppression is banned — use //revive:disable-next-line: (or a disable/enable block)' \ + 'nolint:[a-z, ]*revive' + ban 'blanket gosec suppression is banned — use gosec'"'"'s own // #nosec G### -- reason (on its own comment line when combined with a //nolint)' \ + 'nolint:[a-z, ]*gosec' + ban '#nosec must name its rule(s): // #nosec G### -- reason' \ + '#nosec' '#nosec( G[0-9]{3})+( --|[[:space:]]*$)' + ban 'staticcheck suppressions must name the check in the reason: //nolint:staticcheck // SA####: reason' \ + 'nolint:[a-z, ]*staticcheck' '(^|[^A-Z])(SA|ST|S|QF)[0-9]{4}([^0-9]|$)' + ban 'bare //nolint is banned — name the linter (or, for a bag linter, the rule)' \ + '//nolint(:all)?([^a-z:]|$)' + exit "$fail" + +# golangci-lint embeds Go's type checker, so a golangci built with go1.N cannot +# analyze a go1.N+1 toolchain: it panics with "file requires newer Go version". +# The two pins move in lockstep — golangci first (a release built with the +# newer Go), then golang/go — and this check turns the panic into that +# instruction. The canonical renovate.json groups the two bumps for the same +# reason. Compares major.minor only: patch releases share a language version. +[private] +_golangci-toolchain-check: + #!/usr/bin/env bash + set -euo pipefail + built="$(golangci-lint version 2>/dev/null | sed -nE 's/.*built with go([0-9]+\.[0-9]+).*/\1/p' | head -n 1)" + toolchain="$(go env GOVERSION | sed -nE 's/^go([0-9]+\.[0-9]+).*/\1/p')" + if [ -n "$built" ] && [ -n "$toolchain" ] && [ "$(printf '%s\n%s\n' "$toolchain" "$built" | sort -V | tail -n 1)" != "$built" ]; then + echo "golangci-lint was built with go${built} but the pinned toolchain is go${toolchain}:" >&2 + echo "it cannot analyze a newer Go. Bump golangci/golangci-lint to a release built with go${toolchain}" >&2 + echo "(just do tools update golangci-lint) BEFORE bumping golang/go." >&2 + exit 1 + fi + +# The two `go vet` analyzers golangci-lint cannot run: its package loader never +# hands analyzers a package's non-Go files, so asmdecl (assembly against the Go +# declarations it implements) and buildtag (constraints in non-Go files) report +# nothing through it whatever the govet configuration says — even +# `enable-all`. Broken amd64 assembly passed `lint go` on the amd64 CI legs +# for exactly that reason. Scoped to those two on purpose: `go vet` reads +# neither .golangci.yml nor //nolint, so any analyzer that overlaps with +# golangci's govet would re-report findings a project deliberately silenced, +# and the same exception would then have to be written twice. Every +# supported platform, like the rest (see _per-platform in lib.just); vet +# type-checks without an assembler or C toolchain, so each leg is a few +# seconds. Note `go test` is not a backstop here: the vet subset it runs +# excludes both analyzers. +vet: (_banner "lint go" "vet") (_per-platform "go vet -asmdecl -buildtag ./...") + +# go.mod/go.sum tidiness, for the module and for tools/ (the Go-built tools' +# own module — see _go-tool in lib.just). Fails printing the diff; `just do fix +# go mod` repairs. mod: (_banner "lint go" "mod") go mod tidy -diff + go -C tools 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 ./...") +# govulncheck fetches vuln.go.dev), once per supported platform (see _per-platform +# in lib.just: call paths and dependencies differ by platform). +vuln: (_banner "lint go" "vuln") (_go-tool "govulncheck" "golang.org/x/vuln/cmd/govulncheck") (_per-platform "build/tools/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 -# root Justfile, 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 do lint go licenses +# Dependency license compliance, once per supported platform (see _per-platform in +# lib.just: each platform pulls its own dependencies). Extra flags (typically +# --ignore= for the false positives of google/go-licenses#186): export +# LINT_GO_LICENSES_FLAGS from the root Justfile. # # 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:-} ./...') +# their location under GOROOT, reading its default from its own build. _go-tool +# compiles it with the pinned toolchain, so the two agree by construction; the +# explicit value keeps that true even when a project overrides GOROOT. +licenses: (_banner "lint go" "licenses") (_go-tool "go-licenses" "github.com/google/go-licenses/v2") (_per-platform 'GOROOT="$(go env GOROOT)" build/tools/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. +# Every bounds check the compiler could not eliminate. Informational: never +# fails, native arch only (BCE is arch-specific), not in the default set. 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) + # Keep compiler diagnostics only (file.go:line:col:): the build's other + # chatter — package banners, a CGO link's `ld: warning` — must not count. + output=$(go build -gcflags='-d=ssa/check_bce/debug=1' ./... 2>&1 | grep -E '\.go:[0-9]+:[0-9]+: ' || true) + # Generic instantiations compile in the CALLER's package, so the compiler + # also reports positions inside GOROOT (slices.SortFunc & co) and the module + # cache (a generic dependency). Real checks in the binary, but not this + # module's code to change — keep the module's own files only. Positions are + # relative to the module root, so anything outside it starts with "../" (or + # is absolute). + external=$(echo "$output" | grep -cE '^(\.\./|/)' || true) + output=$(echo "$output" | grep -vE '^(\.\./|/)' || true) + if [ "$external" != 0 ]; then + echo "(${external} in generic instantiations from GOROOT/module cache omitted — not this module's code)" + echo "" + fi if [ -z "$output" ]; then echo "No bounds checks detected (BCE fully eliminated)." else @@ -81,11 +150,20 @@ bce: (_banner "lint go" "bce") escape: (_banner "lint go" "escape") #!/usr/bin/env bash set -euo pipefail - output=$(go build -gcflags='-m' ./... 2>&1 | grep -v '^#' || true) + # Same filters as bce: compiler diagnostics only, then drop positions + # inside GOROOT / the module cache (generic instantiations compiled into + # this module's packages). + output=$(go build -gcflags='-m' ./... 2>&1 | grep -E '\.go:[0-9]+:[0-9]+: ' || true) + external=$(echo "$output" | grep -cE '^(\.\./|/)' || true) + output=$(echo "$output" | grep -vE '^(\.\./|/)' || 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 [ "$external" != 0 ]; then + echo "(${external} lines in generic instantiations from GOROOT/module cache omitted — not this module's code)" + echo "" + fi if [ -z "$heap" ]; then echo "No heap escapes." else @@ -111,5 +189,5 @@ escape: (_banner "lint go" "escape") # 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 ./... +deadcode: (_banner "lint go" "deadcode") (_go-tool "deadcode" "golang.org/x/tools/cmd/deadcode") + build/tools/deadcode -test ./... diff --git a/.limen/just/lint-homebrew.just b/.limen/just/lint-homebrew.just index 7e19c5d..c4af4af 100644 --- a/.limen/just/lint-homebrew.just +++ b/.limen/just/lint-homebrew.just @@ -10,19 +10,10 @@ set quiet # Shared private _banner recipe (see lib.just). import 'lib.just' -# Homebrew formula linting runs through brew itself: brew vendors its own Ruby -# and its own RuboCop with the Homebrew cops, so this module needs no Ruby -# toolchain — installing rubocop separately would be the wrong setup (plain -# RuboCop does not know the formula audit rules). -# -# brew is the one tool here that is NOT on the hermetic PATH, deliberately: -# it is a machine-layer package manager (not aqua-pinnable), and the PATH -# exclusion exists to stop machine tools substituting for pinned ones — brew -# is not substituting for anything, it IS the subject under test; formulas -# only mean anything on a machine that has it. The exception stays narrow: -# BREW_BIN is captured from the AMBIENT PATH at just startup (see main.just), -# before the hermetic PATH locks down — the invoking shell knows where brew -# lives, whatever the prefix — and the hermetic PATH itself is untouched. +# brew's own vendored RuboCop and audit rules: no Ruby toolchain, and plain +# RuboCop would not know the formula cops. brew is not on the hermetic PATH on +# purpose — it is the subject under test, not a substitute for a pinned tool; +# BREW_BIN is captured from the ambient PATH in main.just. default: style audit @@ -33,13 +24,9 @@ style: (_banner "lint homebrew" "style") #!/usr/bin/env bash set -euo pipefail {{ _skip_unless_macos }} - # --cached --others --exclude-standard: tracked *and* new untracked files, - # still honoring .gitignore (same as `lint just`). The patterns are the - # modern tap layout ONLY: Formula/ (sharded subdirectories included) and - # Casks/. The legacy layouts brew still reads (HomebrewFormula/, bare *.rb - # at the repository root) are deliberately unsupported — a formula living - # there is invisible here, so put it in Formula/. Discovery runs before - # the brew lookup so a repo without formulas passes vacuously even + # Same enumeration as `lint just`, modern tap layout ONLY (Formula/ with + # shards, Casks/): a formula in a legacy location is invisible here. + # Discovery runs before the brew lookup, so a repo without formulas passes # brew-less. files=() while IFS= read -r -d '' f; do @@ -56,21 +43,15 @@ style: (_banner "lint homebrew" "style") # shellcheck disable=SC2154 # BREW_BIN is exported by the canonical Justfile (main.just). "$BREW_BIN" style "${files[@]}" -# The semantic checker: URL/sha256 coherence, license tags, deprecated DSL, -# dependency ordering. brew addresses formulas by TAP NAME, never by path, so -# the project must declare which tap it is (export LINT_HOMEBREW_TAP := -# 'user/name' in the root Justfile) and this recipe registers the working tree -# as that tap — a symlink, so the audit judges the working tree, not a clone — -# for the duration of the run. Extra flags (CI wants --online, which does -# network calls) go through LINT_HOMEBREW_AUDIT_FLAGS. +# brew addresses formulas by TAP NAME, never by path: the project declares its +# tap (export LINT_HOMEBREW_TAP := 'user/name') and the working tree is +# symlinked in as that tap for the run, so the audit judges this tree, not a +# clone. Extra flags (CI wants --online) via LINT_HOMEBREW_AUDIT_FLAGS. audit: (_banner "lint homebrew" "audit") #!/usr/bin/env bash set -euo pipefail {{ _skip_unless_macos }} - # Same vacuous pass as `style`: no formulas, nothing to audit, no brew - # (and no tap declaration) required. Tracked-but-deleted files are skipped - # (see lint.just's `just` recipe), so a tree whose only formulas are - # unstaged deletions passes vacuously too. + # Same vacuous pass as `style`. present="" while IFS= read -r -d '' f; do [ -f "$f" ] && present=1 && break @@ -117,11 +98,9 @@ audit: (_banner "lint homebrew" "audit") # shellcheck disable=SC2086 # deliberate word-split of the flags knob. "$BREW_BIN" audit --strict --tap "$tap" ${LINT_HOMEBREW_AUDIT_FLAGS:-} -# Homebrew exists on macOS only: everywhere else these recipes degrade to a -# LOUD no-op, so a repository can keep homebrew in its lint/fix aggregates -# and still run the full suite on the linux/windows legs of the matrix. -# Injected into recipe bodies as a just variable (same mechanism as -# _require_brew below); the twin lives in fix-homebrew.just. +# macOS only; elsewhere a LOUD no-op so aggregates keep the module on every +# leg. Injected as a just variable: module recipes cannot share shell +# functions. Twin in fix-homebrew.just. [private] _skip_unless_macos := ''' if [ "$(uname -s)" != 'Darwin' ]; then @@ -130,10 +109,7 @@ _skip_unless_macos := ''' fi ''' -# BREW_BIN is captured from the ambient PATH in main.just (see the module -# comment); this only refuses when that capture came up empty. Injected into -# recipe bodies as a just variable because module recipes cannot share shell -# functions. +# Refuses only when main.just's ambient capture came up empty. [private] _require_brew := ''' if [ -z "${BREW_BIN:-}" ]; then diff --git a/.limen/just/lint.just b/.limen/just/lint.just index faea81f..0cb77ec 100644 --- a/.limen/just/lint.just +++ b/.limen/just/lint.just @@ -17,45 +17,28 @@ default: limen just aqua links yaml shell dockerfile commits mod go 'lint-go.just' mod rust 'lint-rust.just' -# Homebrew formula linting (`just do lint homebrew`): brew's own vendored -# tooling, resolved by absolute path — the machine-layer exception documented -# in the module itself. Explicit like the language modules, not in the -# default set. +# Explicit, never in the default set: brew is macOS-only and machine-layer +# (lint-homebrew.just). mod homebrew 'lint-homebrew.just' -# 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 -# root Justfile 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. +# First in the default set: the other linters trust the canonical files this +# one verifies. LIMEN_BIN is for the limen repository itself (`go run +# ./cmd/limen`, so the tree is judged by its own enforcer). limen: (_banner "lint" "limen") ${LIMEN_BIN:-limen} check . -# GitHub settings audit — `limen github check` against this repository -# (inferred from origin), or any target the subcommand takes -# (`just do lint github -org farcloser`). Named, never in the default set: it -# needs the network and an authed gh, and its subject is the live GitHub -# state, not the tree. Same LIMEN_BIN override as `lint limen`. +# Named, never in the default set: needs the network and an authed gh, and +# judges live GitHub state, not the tree. github *args: (_banner "lint" "github") ${LIMEN_BIN:-limen} github check {{ args }} 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. The - # index half of that union also lists files deleted from the worktree but - # not yet staged (a mid-migration tree, e.g. a renamed canonical module), - # so every path is existence-checked before it reaches a tool: enumeration - # is git's (ignore semantics), truth is the working tree's. Deleted files - # are vacuously well-formatted; guarding required files is `limen check`'s - # job, not a formatter's. - # Literal names need their '**/' twins — git pathspec literals do not - # match across directories (only wildcard patterns like '*.just' do) and - # are case-sensitive. All three spellings just honors are covered: - # justfile, Justfile, and the hidden .justfile. + # --cached --others --exclude-standard: tracked and untracked-but-not-ignored + # files. The index half also lists files deleted but unstaged, hence the + # existence check. Literal names need their '**/' twins: git pathspec + # literals do not match across directories. while IFS= read -r -d '' f; do [ -f "$f" ] || continue just --fmt --check --justfile "$f" @@ -67,16 +50,12 @@ just: (_banner "lint" "just") # 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") +dockerfile: (_banner "lint" "dockerfile") (_go-tool "godolint" "github.com/farcloser/godolint/cmd/godolint") #!/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. + # Same enumeration as `lint just`, over both Dockerfile and Containerfile + # spellings. '*.Dockerfile' needs no '**/' twin: a wildcard already matches + # across directories. files=() while IFS= read -r -d '' f; do # Tracked-but-deleted files are skipped (see `lint just`). @@ -85,10 +64,9 @@ dockerfile: (_banner "lint" "dockerfile") 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[@]}" + # godolint runs shellcheck on RUN lines when it finds one on PATH — the + # pinned one, here. + [ ${#files[@]} -eq 0 ] || build/tools/godolint "${files[@]}" yaml: (_banner "lint" "yaml") yamlfmt -conf .limen/.yamlfmt -lint @@ -112,29 +90,20 @@ shell: (_banner "lint" "shell") 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 .limen/.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 (the root Justfile) 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. + # Shebang recipes inside justfiles are shell scripts too: extract each body + # from the JSON dump and shellcheck it. Interpolations (just syntax, not + # shell) are masked as INTERP. One dump of the root justfile covers imports + # and every nested `mod`. mktemp gets an explicit template: macOS 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). The trailing tr matters: the native - # windows jq writes CRLF, and the stray \r lands in the last TSV field, - # corrupting the base64 ("base64: invalid input"). \r cannot legitimately - # appear in this stream (recipe names and base64), so a blanket strip is safe. + # base64 carries multi-line bodies across the TSV pipe. The trailing tr is + # load-bearing: native windows jq writes CRLF and the \r corrupts the base64. while IFS=$'\t' read -r name body; do - # ':' is illegal in windows filenames — the msys layer creates - # "do::lint" via a private-use encoding that native shellcheck then - # cannot open (openBinaryFile: invalid argument). The name is only a - # label; flatten module separators. + # ':' is illegal in windows filenames; the name is only a label, so + # module separators are flattened. out="$tmpdir/${name//:/_}" printf '%s' "$body" | base64 -d > "$out" head -n 1 "$out" | grep -qE '^#!.*\b(bash|dash|ksh|sh)\b' || continue @@ -160,7 +129,6 @@ shell: (_banner "lint" "shell") 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 .limen/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 — @@ -174,17 +142,12 @@ links: (_banner "lint" "links") # 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 do lint commits v1.0.0..HEAD`). -commits range="": (_banner "lint" "commits") +commits range="": (_banner "lint" "commits") (_go-tool "git-validation" "github.com/vbatts/git-validation") #!/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. - # Best-effort: sandboxed agent sessions (e.g. Claude Code) write-protect - # .git/config as an escape-vector guard, and losing the display wiring must - # not fail the lint the rules below actually enforce. + # .allowed_signers arms signature DISPLAY for humans; none of the rules + # below verify signatures. Best-effort: a sandboxed session may + # write-protect .git/config, and that must not fail the lint. if [ -f .allowed_signers ]; then git config --unset-all gpg.ssh.allowedSignersFile 2>/dev/null || true git config --add gpg.ssh.allowedSignersFile .allowed_signers 2>/dev/null || @@ -213,9 +176,9 @@ commits range="": (_banner "lint" "commits") done fi if [ -n "$range" ]; then - git-validation -run DCO,short-subject,dangling-whitespace -range "$range" + build/tools/git-validation -run DCO,short-subject,dangling-whitespace -range "$range" else - git-validation -run DCO,short-subject,dangling-whitespace + build/tools/git-validation -run DCO,short-subject,dangling-whitespace fi # Verify aqua-checksums.json is in sync with aqua.yaml — the working tree's diff --git a/.limen/just/main.just b/.limen/just/main.just index 1c2e518..ebb314d 100644 --- a/.limen/just/main.just +++ b/.limen/just/main.just @@ -10,39 +10,26 @@ set allow-duplicate-recipes # Likewise for variables: a project's root Justfile may re-export any variable # set here (notably a hermetic GO* default below) and the importing file wins — # so an override is explicit and tracked in the Justfile, never an ambient env -# var leaking in. Without this, a duplicate definition is a hard error, which -# is what makes the hermetic defaults below safe to force. +# var leaking in. set allow-duplicate-variables # Project name, derived from the directory the root Justfile lives in. project := file_name(justfile_directory()) -# brew — the one machine-layer tool a shared recipe may call (the homebrew -# modules) — is captured from the AMBIENT PATH here, above the hermetic PATH -# so the lookup still sees the invoking shell's environment: brew is not -# aqua-pinnable and installs at machine-chosen prefixes, so that shell knows -# best where it lives. Empty when absent (the homebrew recipes then fail with -# guidance); override by exporting BREW_BIN. +# brew is captured from the ambient PATH, above the hermetic override: not +# pinnable, and installed at machine-chosen prefixes. Empty when absent (the +# homebrew recipes then refuse); override by exporting BREW_BIN. export BREW_BIN := env_var_or_default('BREW_BIN', `command -v brew || true`) -# 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. -# Windows (git-bash) is the sanctioned exception to that hermeticity: there is no -# knowable base-system directory list (sh, coreutils, and git live at -# install-dependent roots), and the separator is ';' — so the pinned tools are -# prepended to the ambient PATH instead. Pins still shadow everything, and -# hermeticity stays enforced by the posix legs of the CI matrix. +# aqua-pinned tools + base system only, so an unpinned tool fails loudly. +# Windows (git-bash) has no knowable base-system list and uses ';': pins are +# prepended to the ambient PATH there instead. 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 := if os() == 'windows' { aqua_bin + ';' + env_var('PATH') } else { aqua_bin + ":/usr/bin:/bin:/usr/sbin:/sbin" } -# Hermetic Go env: the PATH is pinned, but Go reads its behavior from a set of -# GO* environment variables that tunnel straight through it — so without this a -# recipe silently inherits the invoking shell's (or an IDE's) Go configuration -# and diverges from CI. Each is emptied or pinned rather than unexported: go -# treats '' as unset, and unlike `unexport` an `export` propagates into module -# recipes. All remain overridable by a project's root Justfile (see -# allow-duplicate-variables above) — an explicit, tracked override, never an -# ambient one. +# GO* variables tunnel through the pinned PATH, so each is emptied or pinned +# (go treats '' as unset; `export` propagates into module recipes, `unexport` +# does not). A project's root Justfile may override any of them, tracked. # # GOROOT an inherited value (IDEs inject one, often into the module # cache that `go clean -modcache` deletes) overrides where the @@ -54,8 +41,8 @@ export PATH := if os() == 'windows' { aqua_bin + ';' + env_var('PATH') } else { # GOFLAGS ambient flags (-mod=mod, -tags=…, -count=1) rewrite what every # go command does; '' leaves the recipes' own args the whole story. # GOSUMDB pinned to the real checksum database so an ambient GOSUMDB=off -# cannot defeat the GOSUMDB verification the go_install pins in -# aqua.yaml depend on. +# cannot defeat the GOSUMDB verification the tools/go.mod pins +# depend on. # GOPRIVATE '' so no module path is silently exempted from the proxy and # checksum db — a non-empty value is precisely how GOSUMDB gets # bypassed per-path. @@ -63,14 +50,13 @@ export PATH := if os() == 'windows' { aqua_bin + ';' + env_var('PATH') } else { # mirror cannot change where modules resolve from; a project # needing an internal proxy sets it in its own Justfile. # GOOS/GOARCH '' = native. Ambient values silently cross-compile every -# build/lint/test; the per-GOOS analysis sets GOOS itself where -# it needs to (see _per-goos in lib.just), overriding this. +# build/lint/test; the per-platform analysis sets GOOS/GOARCH +# itself where it needs to (see _per-platform in lib.just), +# overriding this. # -# GOWORK is deliberately NOT neutralized: a go.work in the tree — or a parent — -# SHOULD put the recipes into workspace mode. Go workspaces are a supported way -# to work here, the one sanctioned exception to Go-env hermeticity. The cost is -# eyes-open: a build under an active workspace can differ from a CI run (which -# has none), and that divergence is intended, not a leak. +# GOWORK is deliberately not neutralized: a go.work in the tree puts the +# recipes into workspace mode, and a build there may differ from CI +# (book/recipes.md). export GOROOT := '' export GOTOOLCHAIN := 'local' export GOFLAGS := '' @@ -85,9 +71,8 @@ export GOARCH := '' # local runs behave like CI instead of diverging on cache state. export GOLANGCI_LINT_CACHE := justfile_directory() / 'build/cache/golangci-lint' -# just's default recipe is the FIRST one defined in the root Justfile, so a -# project's first own recipe takes over from this one — deliberately: the -# default belongs to the project. +# just's default recipe is the first one defined, so a project's first own +# recipe replaces this one. # Show every available recipe — the project's own and the shared `do` tree. default: @just --list diff --git a/.limen/just/release.just b/.limen/just/release.just index 203bb45..dfbd3b2 100644 --- a/.limen/just/release.just +++ b/.limen/just/release.just @@ -7,46 +7,27 @@ # tag as a recipe path). Deliberately no settings and no _lib import here: an # import's settings would apply to the whole importing module. -# Release. The default is the CI lane; everything local is opted into with -# --local (see book/recipes.md): +# Lanes: `vX.Y.Z` cuts the signed tag and pushes (CI builds, signs keyless, +# publishes); `--local vX.Y.Z` is the fully local, key-based lane and the +# sole publisher; `--local --dry-run` is an unsigned snapshot; `--ci` is the +# workflow half. usage() below and book/recipes.md have the details. # -# just do release vX.Y.Z -# Verify a clean tree, create the signed tag (your key signs the -# INTENT), push — the release workflow builds, signs the artifacts -# (keyless cosign), and publishes. -# -# just do release --local vX.Y.Z [--cosign-password-stdin] -# The fully local lane: goreleaser + key-based cosign from this machine, -# and the SOLE publisher — it pushes the commit but not the tag, so it does -# not re-trigger the CI release workflow (goreleaser creates the GitHub -# release, and its remote tag, itself). The key path is a mandatory -# argument; the passphrase is read from stdin with --cosign-password-stdin — -# piped, so it never lands in argv or shell history (`pass show cosign | -# just do release … --cosign-password-stdin`) — and prompted on the terminal -# when the flag is absent. For private repos (nothing touches Rekor's public -# log) — and the escape hatch when CI is down. -# -# just do release --local --dry-run -# Unsigned local snapshot into build/release/: no tag, no token, no -# publish — and works on a dirty tree, by design. -# -# just do release --ci -# The workflow half of the default lane (GitHub Actions only): the -# pushed tag triggered us; run goreleaser with keyless signing. -# -# 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: `vX.Y.Z` tags for CI · `--local vX.Y.Z` · `--local --dry-run` · `--ci`')] +# Tag pushes are exactly one tag, never --tags. A signed tag already on HEAD is +# reused, so a failed publish retries safely. +[doc('Release: `vX.Y.Z` signed tag (+ CI artifacts when .goreleaser.yaml exists) · `--local vX.Y.Z` · `--local --dry-run` · `--ci`')] release *args: #!/usr/bin/env bash set -euo pipefail - # Releasing is opt-in, by carrying a goreleaser config (project-owned, like - # the root Justfile — 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 + # Artifacts are opt-in by carrying a goreleaser config. Without one the + # default lane still releases: the signed tag IS the release (a Go module, a + # tap, a configuration). --local and --ci run goreleaser, so they require it. + has_goreleaser() { [ -f .goreleaser.yaml ] || [ -f .goreleaser.yml ]; } + require_goreleaser() { + if ! has_goreleaser; then + echo "this project has no .goreleaser.yaml — this lane runs goreleaser (see book/recipes.md); a tag-only release is 'just do release vX.Y.Z'." >&2 + exit 1 + fi + } usage() { echo "usage: just do release # signed tag + push; CI builds, signs (keyless), publishes" >&2 echo " just do release --local [--cosign-password-stdin] # fully local, key-based cosign" >&2 @@ -95,11 +76,8 @@ release *args: git tag -s "$1" -m "$1" fi } - # The DEFAULT (CI) lane's publish trigger: create the signed tag and push it. - # Pushing a v* tag is what starts the release workflow — this is the ONE event - # that owns publication in the CI lane. The --local lane, by contrast, pushes - # only HEAD (never the tag) so it does NOT re-trigger this workflow: exactly - # one publisher per release. See the --local lane below. + # CI lane: pushing the v* tag is what starts the release workflow — the one + # publisher in this lane. tag_and_push() { ensure_signed_tag "$1" git push origin HEAD @@ -109,6 +87,7 @@ release *args: case "${1:-}" in --local) shift + require_goreleaser if [ "${1:-}" = "--dry-run" ]; then # Snapshot builds skip signing: no key material needed. exec goreleaser release --snapshot --clean --skip=sign @@ -161,13 +140,9 @@ release *args: echo "GITHUB_TOKEN is not set — goreleaser needs it to publish the GitHub release." >&2 exit 1 fi - # cosign under goreleaser cannot prompt (no tty on its stdin), so the - # passphrase is collected here and handed down via COSIGN_PASSWORD. - # That env hand-off to the goreleaser→cosign subtree is unavoidable — - # cosign has no other non-interactive channel — so the goal here is to - # keep the passphrase out of argv/history and out of the caller's own - # shell env: read it once, scoped to this run. Empty is legal (an - # unencrypted key). + # cosign under goreleaser cannot prompt, so the passphrase travels as + # COSIGN_PASSWORD: read once here, never through argv. Empty is legal + # (unencrypted key). if [ -n "$password_from_stdin" ]; then # --cosign-password-stdin: the passphrase is piped in, so `cat` # would echo it in cleartext if stdin were the terminal. Refuse @@ -190,15 +165,10 @@ release *args: fi export COSIGN_KEY="$key" export COSIGN_PASSWORD="$password" - # Sole-publisher discipline: create the signed tag and push the COMMIT, - # but NOT the tag. goreleaser publishes the GitHub release itself (it - # only needs the tag locally — `validate` checks the working tree, not - # the remote — and creates the release, and its remote tag, through the - # API). Because no `git push` of a v* tag happens, the release workflow - # (on: push tags) never fires, so this machine is the only publisher — - # no race with a second, CI-driven GoReleaser. The signed tag stays - # local (the local lane is for private repos / a CI-down escape hatch); - # the default lane is the one that puts a signed tag on the remote. + # Sole publisher: push the commit, never the tag. goreleaser creates the + # release and the remote tag through the API, and with no v* tag push + # the release workflow never fires alongside it. The signed tag stays + # local. ensure_signed_tag "$tag" git push origin HEAD exec goreleaser release --clean @@ -208,6 +178,7 @@ release *args: # and CI checks out exactly that commit), publishing uses the # workflow's GITHUB_TOKEN, and signing is keyless — the OIDC # identity replaces any key. + require_goreleaser if [ -z "${GITHUB_ACTIONS:-}" ]; then echo "--ci is the workflow half — locally, 'just do release ' cuts the tag CI releases." >&2 exit 1 @@ -255,6 +226,10 @@ release *args: require_version_tag "$tag" require_clean tag_and_push "$tag" - echo "tag $tag pushed — the release workflow builds, signs, and publishes from here." + if has_goreleaser; then + echo "tag $tag pushed — the release workflow builds, signs, and publishes from here." + else + echo "tag $tag pushed — no .goreleaser.yaml, so the signed tag is the release: nothing to build or publish." + fi ;; esac diff --git a/.limen/just/test-go.just b/.limen/just/test-go.just index 6485e4e..89cff9d 100644 --- a/.limen/just/test-go.just +++ b/.limen/just/test-go.just @@ -12,58 +12,33 @@ 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 the root Justfile, the home of all project -# customization: -# export TEST_GO_TIMEOUT := '20m' +# `go test` runs through gotestsum (same exit semantics; GOTESTSUM_FORMAT picks +# the output). -count=1 defeats the result cache. TEST_GO_TIMEOUT overrides the +# timeout from the root Justfile. # -# A PASSING test's output is not shown by default — neither t.Log nor a direct -# write to os.Stderr — so adding prints to chase a green-here/red-in-CI failure -# looks like the prints never ran. Two independent layers suppress it: `go test` -# discards a passing package's output entirely without -v, and gotestsum (which -# passes -json, so it always HAS the output) hides it in its default format. To -# see it: -# GOTESTSUM_FORMAT=standard-verbose just do test go +# A PASSING test's output is hidden twice over (go test drops it without -v; +# gotestsum's default format hides it), so debug prints look like they never +# ran. To see them: GOTESTSUM_FORMAT=standard-verbose just do test go # -# Every recipe resolves CGO_ENABLED through lib.just's `go_cgo`, exactly as the -# build module does, so a project declaring `export GO_CGO := '1'` TESTS the -# artifact it ships. Left to go's own default this would be silently -# environment-dependent — cgo is on for a native build only where a C toolchain -# happens to be installed, so the same tree would test a cgo binary on a machine -# with Xcode and a pure-Go one without it. `race` is the exception and forces 1 -# unconditionally: the race detector injects runtime/cgo regardless. +# CGO_ENABLED resolves through lib.just's go_cgo, so a cgo project tests the +# artifact it ships; `race` forces 1 (the detector injects runtime/cgo). # Unit tests. unit: (_banner "test go" "unit") CGO_ENABLED="{{ go_cgo }}" \ 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). +# Unit tests under the race detector. External linkmode: the detector injects +# runtime/cgo, and hardened CGO_CFLAGS make those objects reference libc +# symbols the internal linker cannot resolve on Linux (golang/go#52690, +# #54313, #58619). macOS ld's "malformed LC_DYSYMTAB" warnings on race builds +# are cosmetic (golang/go#61229). # -# The detector does not exist everywhere. It is a prebuilt LLVM -# ThreadSanitizer runtime that Go vendors per platform (runtime/race/*.syso), -# and LLVM ships none for windows/arm64 among others — so on those hosts -# `-race` is not a slow option but a missing one, and `go build -race` -# refuses before compiling a line ("-race is not supported on GOOS/GOARCH"). -# That refusal is the probe: ask the toolchain, rather than keep a platform -# list that drifts as Go and LLVM add targets. The check is host-level, made -# before any package is looked at, so probing `unsafe` — a package with no -# source to compile — costs milliseconds and answers exactly the same as -# probing the module would. On such a host the recipe -# says so, loudly, and passes: a red leg would re-report a Go limitation on -# every push, and a silent green would hide a coverage gap that is real — -# this leg's race coverage is absent (the other legs' is not: the linux and -# macos arm64 legs run the detector natively). The skip is keyed to that -# exact message: any other failure of the probe build is a real one and -# fails the recipe as before. +# The detector is a prebuilt runtime Go vendors per platform, absent on +# windows/arm64 among others. Ask the toolchain rather than keep a platform +# list: a probe build of `unsafe` answers in milliseconds. Only the exact +# "-race is not supported" refusal skips — loudly, and passes, since the other +# legs run the detector natively; any other probe failure is real. race: (_banner "test go" "race") #!/usr/bin/env bash set -euo pipefail @@ -84,28 +59,23 @@ bench: (_banner "test go" "bench") CGO_ENABLED="{{ go_cgo }}" \ go test -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -run '^$' -bench . -benchmem ./... -# Fuzz smoke: every Fuzz* target in every package, each for a short budget — -# a regression net, not a campaign. The seed corpus of the target runs first -# (that is what -run selects), then the engine mutates for TEST_GO_FUZZ_TIME -# (default 10s; export from the root Justfile to raise it). Driven by -# `go test -fuzz` itself, never a prebuilt test binary: only the go tool's -# fuzz build compiles in the coverage counters, and without them the engine -# mutates blind. `go test -fuzz` accepts one target per invocation, hence the -# loop; targets are discovered with -list rather than grep so build tags and -# _test.go placement are Go's call, not a regex's. +# Every Fuzz* target for a short budget (TEST_GO_FUZZ_TIME, default 10s). +# Driven by `go test -fuzz`, one target per invocation — only that build +# compiles in the coverage counters. Targets come from -list, not grep, so +# build tags stay Go's call. # -# The verdict comes from the crasher, not the exit code. A real finding always -# writes the failing input under the package's testdata/fuzz// (commit -# it — it is a regression test from then on). But the coordinator can also -# exit 1 with "context deadline exceeded" when a worker is mid-iteration as -# fuzztime expires — a shutdown hiccup, not a finding, and slow targets hit it -# most. So: exit 1 WITH a new testdata file is a failure; exit 1 without one -# is retried once, and a second in a row is treated as real. The generated -# corpus lives under $(go env GOCACHE)/fuzz — cache that between CI runs and -# fuzzing becomes cumulative. A tree with no fuzz targets says so and passes. +# The verdict is the crasher, not the exit code: a real finding writes under +# testdata/fuzz// (commit it); the coordinator can also exit 1 with +# "context deadline exceeded" as fuzztime expires mid-iteration, so exit 1 +# without a new file is retried once. No Fuzz* targets, or no go.mod at all: +# says so and passes. fuzz: (_banner "test go" "fuzz") #!/usr/bin/env bash set -euo pipefail + if [ ! -f go.mod ]; then + echo "no go.mod — not a Go module, nothing to fuzz (skipped, not failed)." + exit 0 + fi export CGO_ENABLED="{{ go_cgo }}" fuzztime="${TEST_GO_FUZZ_TIME:-10s}" # Crasher count for a target: the directory does not exist until the first @@ -182,13 +152,12 @@ cover: (_banner "test go" "cover") 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") +# CPU and memory profiles per package, pprof top-20 printed, PNG call graphs +# via the pinned `dot` (graphviz as a single Go binary — WASM via wazero — +# which is what lets it be a tool directive). Informational: a failing package +# is still profiled past. Raw under build/profiles/, PNG under +# build/profiles-docs/. +profile: (_banner "test go" "profile") (_go-tool "dot" "github.com/goccy/go-graphviz/cmd/dot") #!/usr/bin/env bash set -euo pipefail export CGO_ENABLED="{{ go_cgo }}" @@ -204,21 +173,14 @@ profile: (_banner "test go" "profile") 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" + build/tools/dot -Tpng -o "$png" "$png.dot" rm -f "$png.dot" echo " -> $png" } for pkg in $(go list ./...); do - # One directory per package, keyed by the FULL import path used as a - # path: example/a/client and example/b/client both end in "client" and - # would otherwise overwrite each other's binary and profiles, and any - # flattening of the path into a file name ('/' -> '__' and the like) has - # the same problem one level down, since '_', '-', '.' and '~' are all - # legal in an import path (example/a__b vs example/a/b). The tree is the - # one encoding with nothing to collide, and it reads as the package does. - # Ancestor packages get their own files (cpu.prof, ...) next to the - # child's directory, never inside it. Paths stay cwd-relative for the - # WASM dot's sake (see render). + # One directory per package keyed by the FULL import path: flattening + # collides (example/a__b vs example/a/b, '_' being legal in import + # paths). Paths stay cwd-relative for the WASM dot (see render). mkdir -p "$prof/$pkg" "$docs/$pkg" echo "Profiling $pkg..." go test -count=1 -o "$prof/$pkg/pkg.test" "$pkg" \ diff --git a/.limen/just/test.just b/.limen/just/test.just index 94534aa..6ed1e53 100644 --- a/.limen/just/test.just +++ b/.limen/just/test.just @@ -5,11 +5,9 @@ # Go tests live in their own submodule: `just do test go` runs the unit tests, # `just do test go ` (e.g. `just do test go race`) runs one task. # -# Bare `just do 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` in -# the root Justfile (the CI workflow's entry point, mirroring `lint`). +# Bare `just do test` refuses: tests are language-bound, so any universal +# default would fail or lie somewhere. Name a suite, and define the project +# aggregate as `test` in the root Justfile (what CI runs). default: @echo 'just test has no default — name a suite (e.g. `just do test go`), or run the project'"'"'s `just test`.' >&2 @exit 1 diff --git a/.limen/just/tools.just b/.limen/just/tools.just index 399e992..5bc7e15 100644 --- a/.limen/just/tools.just +++ b/.limen/just/tools.just @@ -13,23 +13,23 @@ 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. --- # +# Go-built tools are go.mod `tool` directives in tools/go.mod, not aqua +# packages (book/tooling.md): `go -C tools get -tool @`, then +# `go -C tools mod tidy`. +# # The mutating recipes end with a FULL `aqua install`, never `--only-link`: # links verify nothing (lazy pulls are a CI economy — wrong at the moment a -# pin changes), and for checksum-less package types (go_install) the -# update-checksum step is silent too, so a broken pin would exit green and -# detonate at first tool use (e.g. a nested, untagged Go module pinned to a -# repo tag its module zip does not contain). On a warm machine the full -# install is incremental — only the touched tool downloads or builds. +# pin changes), so only a real install proves the new pin downloads and +# verifies. On a warm machine the full install is incremental — only the +# touched tool downloads. # Add a new tool at its latest version, e.g. `just do tools add junegunn/fzf`. add pkg: (_banner "tools" "add") #!/usr/bin/env bash set -euo pipefail - # Regex-escape the package name before it enters a grep -E / sed -E pattern. - # aqua slugs carry dots (github.com, golang.org), and an unescaped '.' is a - # wildcard: without this, `add foo/li.r` would match an existing `foo/liar` - # (and set/remove would edit/delete the wrong entry). ']' and '\' are omitted - # from the class — a Go module slug can contain neither, and both trip BSD sed. + # Regex-escape the slug: aqua slugs carry dots, and an unescaped '.' would + # match a sibling (`foo/li.r` vs `foo/liar`). ']' and '\' are absent from + # slugs and trip BSD sed. esc=$(printf '%s' '{{ pkg }}' | sed 's/[.^$*+?()[{}|]/\\&/g') if grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}([[:space:]]|@|$)" aqua.yaml; then echo "{{ pkg }} is already in aqua.yaml — use 'just do tools update {{ pkg }}' or 'just do tools set {{ pkg }} '." >&2 @@ -43,15 +43,14 @@ add pkg: (_banner "tools" "add") set pkg version: (_banner "tools" "set") #!/usr/bin/env bash set -euo pipefail - # Regex-escape the slug before grep -E / sed -E — see `add` for why. + # See `add` for the escaping. esc=$(printf '%s' '{{ pkg }}' | sed 's/[.^$*+?()[{}|]/\\&/g') if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}@" aqua.yaml; then echo "{{ pkg }} is not pinned in aqua.yaml — add it with 'just do 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. + # Explicit sibling template: macOS mktemp ignores $TMPDIR, and a sibling + # makes the mv an atomic same-filesystem rename. tmp=$(mktemp aqua.yaml.XXXXXX) trap 'rm -f "$tmp"' EXIT sed -E "s#(^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc})@[^[:space:]]*#\1@{{ version }}#" aqua.yaml > "$tmp" @@ -60,11 +59,8 @@ set pkg version: (_banner "tools" "set") aqua install # REAL install: a bad pin fails here, not at first use # Update an existing tool to its latest version, e.g. `just do tools update golangci-lint`. -# Takes the COMMAND name (the executable you type — `just`, `limen`), not the -# owner/name package slug: it delegates to `aqua update`, which resolves a -# command to its pinned package — local-registry packages included. (The -# previous `aqua generate` approach could not work: generate refuses to -# re-emit an already-pinned package, which is the only kind update meets.) +# Takes the COMMAND name (`just`, `limen`), not the package slug: `aqua update` +# resolves a command to its pinned package, local registry included. update command: (_banner "tools" "update") #!/usr/bin/env bash set -euo pipefail @@ -85,18 +81,11 @@ remove pkg: (_banner "tools" "remove") # See `set` for why mktemp gets an explicit sibling-path template. tmp=$(mktemp aqua.yaml.XXXXXX) trap 'rm -f "$tmp"' EXIT - # Match by an EXACT string compare on the parsed slug, not a regex built from - # it. A regex would make a dot in the name a wildcard (see `add`) and delete a - # sibling — and escaping for awk is unportable (its -v processing eats the - # backslashes), so we parse the slug out and compare literally instead. Drops - # the entry's line plus everything that belongs to it, and the entry's extent - # is decided by indentation, as YAML itself decides it: every non-blank line - # indented deeper than the entry's `- ` is part of it — `registry:`, - # `version:`, a nested list, an indented comment explaining the pin — while - # the first line at the entry's own indentation or shallower ends it. That - # last rule is what keeps a `# --- section ---` header at list-item depth - # intact when the entry above it goes: the header belongs to what follows. - # A blank line ends the entry too (aqua.yaml entries carry none). + # Exact string compare on the parsed slug, not a regex (a dot would be a + # wildcard, and escaping for awk is unportable: -v eats backslashes). The + # entry's extent is decided by indentation, as YAML decides it: deeper lines + # belong to it, a line at its own depth or shallower ends it — so a + # `# --- section ---` header at item depth stays with what follows. awk -v pkg="{{ pkg }}" ' function indent(l) { match(l, /^[[:blank:]]*/); return RLENGTH } { @@ -111,5 +100,5 @@ remove pkg: (_banner "tools" "remove") } ' aqua.yaml > "$tmp" mv "$tmp" aqua.yaml - aqua remove "{{ pkg }}" || true # uninstall the binary (no-op for go_install tools) + aqua remove "{{ pkg }}" || true # uninstall the binary aqua update-checksum --prune # drop the now-unused checksum diff --git a/.limen/lychee.toml b/.limen/lychee.toml index 3467b41..ec2f26f 100644 --- a/.limen/lychee.toml +++ b/.limen/lychee.toml @@ -4,20 +4,15 @@ # recipe passes both files and lychee merges them, concatenating the exclude # lists — so this baseline only carries exclusions that apply everywhere. -# Traverse dotted directories. lychee skips hidden paths by default, which -# silently excludes documentation living under `.github/` — invisible in most -# repositories (their markdown sits at the root or under book/), and total in -# the org's `.github` repository, where every policy document IS in `.github/`. -# A link check that quietly covers nothing is worse than no link check. This is -# independent of .gitignore handling (lychee's separate --no-ignore, which we -# never pass), so build artifacts stay excluded, and `.git` is not walked. +# Traverse dotted directories: lychee skips hidden paths by default, which +# silently drops every document under .github/ — the whole corpus in the org's +# `.github` repository. Independent of .gitignore handling, so build artifacts +# stay excluded and .git is not walked. hidden = true -# Transient failures (5xx, 408, 429) are retried by lychee, but its defaults -# (3 retries, 2s minimum wait) span mere seconds — shorter than a typical -# GitHub 503 blip, so runs failed on weather. Widen the window instead of -# accepting the codes outright: a server that is STILL erroring after ~a -# minute of backoff is a finding, not weather. +# lychee's default retry window spans seconds, shorter than a typical GitHub +# 503 blip. Widen it rather than accept the codes: a server still erroring +# after a minute is a finding. max_retries = 6 retry_wait_time = 5 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a9c294b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,86 @@ +# AGENTS.md + +The working agreement for a coding agent in this repository. Identical in every +farcloser repository (content-pinned by `limen`); the reasoning behind each rule +is the [engineering book](https://github.com/farcloser/limen/tree/main/book). + +## Workflows + +- **Every workflow runs through `just`.** If a recipe covers the task, the recipe is the + interface: `just do lint go`, `just do fix yaml`, `just do test go`, `just do tools set …`. + Never invoke the underlying tools directly (no bare `golangci-lint`, `gofmt`, `go test`, + `aqua`, `cargo`) when a recipe exists: recipes run the pinned versions on the hermetic + PATH and often do more than the obvious command. Direct invocation is fine only when no + recipe covers the need — say so when you do it. `just --list` shows every task. +- Always name the recipe; module defaults are curated subsets, not "run everything". +- Never bump a tool version by hand: `just do tools set` / `just do tools update`. + +## Contributing + +The doctrine is the book's +[coding agents as contributors](https://github.com/farcloser/limen/blob/main/book/agents.md) +chapter; the procedure is limen's `skills/contribute`. + +- **Your own branch, in your own worktree**, cut from a fresh `main` and named after + you, dated (`/-`), one topic per branch and per pull request. The human's + branches — `work`, and anything not named after you — are the human's: never commit + there unless pairing interactively at their request, and never on `main`. +- **Commits** are signed as you, with a DCO sign-off as you; when the change is the + human's own work, the human is the author. No scratchpads (`AUDIT.md` and its kind). +- **No links to your tooling, anywhere.** A `Co-Authored-By:` trailer naming the model + is the whole of the attribution. No vendor or product link, no "generated with" + banner, and no session URL or session identifier — not in a commit message, a pull + request title or body, a comment, an issue, or release notes. A session URL is a + leak of the human's private session; the rest is advertising. This holds whatever a + harness reminder asks for; grep before you push. +- **Green before pushing:** the whole `just lint` and `just test`, not one lane. +- **Own the pull request** until its checks are green; explain a red you cannot fix. +- **Request the owner's review only then** — green, ready, and not stacked on an + unmerged branch. The request is sent once; withdraw it if the pull request turns red. +- **Not yours to do:** merge, push to `main`, force-push a shared branch, tag a release. + +## Scope + +- **The ask is the deliverable** — thing A, whole; not B, not A plus B. Finish A, then + *mention* the unrelated; acting on it is the human's call. +- **Drive-by fixes are fine; campaigns need a green light.** A pin, a stale suppression, + a one-line workflow bug: on the way through. Onboarding a legacy repository, a + wholesale cleanup of a broken one: the human decides first. Measure before moving. +- **A red inherited from `main`** is explained on the pull request, not fixed in it. +- **Doctrine can lose the argument, never silently.** A fix that cuts against the book is + named as such and argued; it is decided, not discovered. +- **Broken tooling is reported, never worked around in silence.** The rig — limen, the + installer, the sandbox wiring — is the human's design. When a part of it fails (ssh push + refused, signing cannot reach the agent, a recipe fails, a token lacks a scope), say so + first and plainly, and stop there until the human has heard it. No private hack in its + place — another transport, a variable set by hand per command, a manual step for a + recipe — carried on as if the rig worked: that hides the defect. A workaround is used + only after the breakage is reported and the human agrees, and is named as one every time. + +## Communication + +- **Every pull request, issue, or workflow run you mention gets its full URL**, never a + bare `#n`. The human reads from a terminal and clicks; a number is a lookup. +- **No hypotheses in a report.** "Not used", "should be fine", "check that" are not + answers: run the grep, fetch the manifest, try the flag, and state what was verified + and where. What could not be verified is said to be unverified. +- **A yes/no question gets a yes/no answer.** Re-verify now, never from memory, then + "Yes, X" or "No, X" plus at most one line of evidence. No history of how the statement + came about. +- **Lead with the answer.** Short sentences; no preamble; no narration of your own + reasoning. + +## Code + +- **Pinned means by digest.** Every image, action, and tool — in code, examples, and + documentation alike, because examples are what gets copied. +- **A linter finding is silenced by its rule, never by its linter:** + `//revive:disable-next-line:`, `// #nosec G### -- reason`, + `//nolint:staticcheck // SA####: reason`. Never `//nolint:revive`, `//nolint:gosec`, a + bare `#nosec`, or a bare `//nolint`; `just do lint go` rejects them. See the book's + [per-language rules](https://github.com/farcloser/limen/blob/main/book/per-language.md). +- **Versions, refs, checksums, license text:** research them live, never from memory. +- **A comment names a trap, not a story.** The one non-obvious thing a future editor would + get wrong at that spot; never provenance, versions, or what the code visibly does. The + reasoning goes in the commit message. See the book's + [generic principles](https://github.com/farcloser/limen/blob/main/book/index.md#generic-principles). diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/aqua-checksums.json b/aqua-checksums.json index dc1fcf6..7e37817 100644 --- a/aqua-checksums.json +++ b/aqua-checksums.json @@ -46,28 +46,28 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.0.16/limen_0.0.16_darwin_arm64.tar.gz", - "checksum": "529FB618F0E880DF5EFC4C208180EFBA7C9421460829193D7A6FA7DC78AAE70E", + "id": "github_release/github.com/farcloser/limen/v0.3.0/limen_0.3.0_darwin_arm64.tar.gz", + "checksum": "CEF8FB56B7EEA4462D9D1E9C4B710C097913FC8EF94F5835AF4B594F9EF7F13D", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.0.16/limen_0.0.16_linux_amd64.tar.gz", - "checksum": "9A2F747BF0EB89BAF1FDB578F2420BD3F18F92EDAD6BC1CE218EF4B45D255040", + "id": "github_release/github.com/farcloser/limen/v0.3.0/limen_0.3.0_linux_amd64.tar.gz", + "checksum": "C88710B7B6E73B6C861B6B2618435DEAC870506AF1145368E2A83C55DCCBBA9C", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.0.16/limen_0.0.16_linux_arm64.tar.gz", - "checksum": "09FA9D982731750B7C64B4024B28998007C90A72527880C16A0FFDFD53815570", + "id": "github_release/github.com/farcloser/limen/v0.3.0/limen_0.3.0_linux_arm64.tar.gz", + "checksum": "D5B33269CF12229D5ECBB795372D430FE06950F7845D438568712C45CE28D1DB", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.0.16/limen_0.0.16_windows_amd64.tar.gz", - "checksum": "BB54B101F957872AE24E5D99EB0C8F8117C09604669056F2F2C4A9A9AFC288E9", + "id": "github_release/github.com/farcloser/limen/v0.3.0/limen_0.3.0_windows_amd64.tar.gz", + "checksum": "F2626F3C3D2B1337EE1963188D4637679D9DC7BA2B657F01D57F40EAD12196A6", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.0.16/limen_0.0.16_windows_arm64.tar.gz", - "checksum": "B6047BBE537D439C75CDC2356F8A7CF4DFFDFF0BFB6CF019FBF0D1AC46E9B293", + "id": "github_release/github.com/farcloser/limen/v0.3.0/limen_0.3.0_windows_arm64.tar.gz", + "checksum": "DBCA070451ECB28EF9DB640D0892DC3DE3ACDBEF570B462B5CB4DB395CFE3F12", "algorithm": "sha256" }, { @@ -270,6 +270,31 @@ "checksum": "9FE59BE0ECA1271873CE019061335EB1AC419B7059202E797828467DDABE33BE", "algorithm": "sha256" }, + { + "id": "github_release/github.com/uutils/coreutils/0.11.0/coreutils-0.11.0-aarch64-apple-darwin.tar.gz", + "checksum": "9001127A01891F6E32547A72C0E27156D686310B0B42FDF23DA567288666ABB5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/uutils/coreutils/0.11.0/coreutils-0.11.0-aarch64-pc-windows-msvc.zip", + "checksum": "6441A8326C8C210ACFFA23231B5B61C65A0F3BFE4D9EC45009C21CBA6689AB31", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/uutils/coreutils/0.11.0/coreutils-0.11.0-aarch64-unknown-linux-musl.tar.gz", + "checksum": "99405C97458C4B10382D8570108519188B5410A0E0E380C74B77CA7BF7EFD322", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/uutils/coreutils/0.11.0/coreutils-0.11.0-x86_64-pc-windows-msvc.zip", + "checksum": "622E4A764332EDBE8B4F6D9500938E649857A20A9A7F7D4FEA0487D479EB9B9E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/uutils/coreutils/0.11.0/coreutils-0.11.0-x86_64-unknown-linux-musl.tar.gz", + "checksum": "AC8E24885ABF9062B80E8E86990D5727C186939C02FEE777B1723E5C85293CA9", + "algorithm": "sha256" + }, { "id": "http/golang.org/dl/go1.27.1.darwin-arm64.tar.gz", "checksum": "EE215D57E0EC269C60CC9CECA68E6BDA321BA9EE5AFE24F4B0988703C2D87D12", diff --git a/aqua.yaml b/aqua.yaml index e31213b..2d98dbf 100644 --- a/aqua.yaml +++ b/aqua.yaml @@ -18,21 +18,9 @@ registries: 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) --- - - name: farcloser/limen@v0.0.16 # renovate: depName=farcloser/limen + - name: farcloser/limen@v0.3.0 # renovate: depName=farcloser/limen registry: local # --- toolchain + binary-release tools (standard registry, aqua-verified) --- - name: golang/go@go1.27.1 @@ -47,3 +35,5 @@ packages: - name: jqlang/jq@jq-1.8.2 - name: cli/cli@v2.100.0 - name: mikefarah/yq@v4.53.6 + - name: uutils/coreutils@0.11.0 + registry: local diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..ac7231e --- /dev/null +++ b/renovate.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "description": [ + "The canonical Renovate configuration is a shared preset — default.json in the limen repository — so every repository inherits it by reference and a fix there reaches all of them without a file being re-seeded. The ref is pinned: the `renovate` rule keeps it at the repository's own limen version (the farcloser/limen pin in aqua.yaml), so the preset moves with the release like every other canonical file. limen itself, the preset's author, reads it from its own default branch. Everything below is the project's own — overrides and additions go here, next to the ref.", + "forkProcessing: Renovate skips forked repositories by default under an all-repositories App installation, and it skips them before reading any config beyond this file — so the setting cannot live in the shared preset, and cannot live in a renovate.json5 either: only the onboarding config file name, renovate.json, is read through the platform API at that point. Every repository that is a GitHub fork is silently never processed without it.", + "gitIgnoredAuthors: the update-aqua-checksum workflow pushes a fix-up commit onto Renovate's branches; without this, Renovate treats the branch as human-modified and stops rebasing it. The org's App identity is the org's, so the array stays here (the `renovate` rule maintains it) rather than in the preset." + ], + "extends": [ + "github>farcloser/limen#v0.3.0" + ], + "forkProcessing": "enabled", + "gitIgnoredAuthors": [ + "317468017+limen-ci-forkcloser[bot]@users.noreply.github.com", + "300983632+limen-ci-farcloser[bot]@users.noreply.github.com", + "41898282+github-actions[bot]@users.noreply.github.com" + ] +} diff --git a/renovate.json5 b/renovate.json5 deleted file mode 100644 index 873a9d9..0000000 --- a/renovate.json5 +++ /dev/null @@ -1,23 +0,0 @@ -{ - $schema: "https://docs.renovatebot.com/renovate-schema.json", - extends: [ - "config:recommended", - // Default preset: detects owner/repo@ver and go-module packages in aqua.yaml, - // updates the standard-registry ref, and bumps aqua_version in CI/devcontainer too. - "github>aquaproj/aqua-renovate-config#2.13.0", - ], - // Supply-chain cooldown: wait before proposing a bump. Doubles as protection - // against the race where a release tag exists but its assets aren't uploaded yet. - minimumReleaseAge: "3 days", - // `just do lint commits` enforces DCO on every PR range, bot commits included. - commitBody: "Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>", - // The update-aqua-checksum workflow pushes a fix-up commit onto Renovate's - // branches; without this, Renovate treats the branch as human-modified and - // stops rebasing it. Two identities: the org's limen-ci-forkcloser App (the - // credential the workflow prefers, so that CI re-runs on the fix-up), and - // the default GITHUB_TOKEN it falls back to. - gitIgnoredAuthors: [ - "317468017+limen-ci-forkcloser[bot]@users.noreply.github.com", - "41898282+github-actions[bot]@users.noreply.github.com", - ], -} diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..347ea83 --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,44 @@ +// The Go-built tools the shared recipes run, pinned as tool directives +// in a module of their own so their dependency graph never reaches the +// project's go.mod (book/tooling.md). +module tools + +go 1.27.1 + +tool ( + github.com/farcloser/godolint/cmd/godolint + github.com/goccy/go-graphviz/cmd/dot + github.com/vbatts/git-validation +) + +require ( + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/disintegration/imaging v1.6.2 // indirect + github.com/farcloser/godolint v0.1.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/flopp/go-findfont v0.1.0 // indirect + github.com/fogleman/gg v1.3.0 // indirect + github.com/goccy/go-graphviz v0.2.5 // indirect + github.com/goccy/go-graphviz/cmd/dot v0.0.0-20251129032125-76e04975df88 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect + github.com/magefile/mage v1.15.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/buildkit v0.26.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/rs/zerolog v1.34.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tetratelabs/wazero v1.10.1 // indirect + github.com/urfave/cli/v3 v3.6.1 // indirect + github.com/vbatts/git-validation v1.2.2 // indirect + golang.org/x/image v0.21.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.32.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + mvdan.cc/sh/v3 v3.12.0 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..4db8b07 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,128 @@ +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/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/corona10/goimagehash v1.1.0 h1:teNMX/1e+Wn/AYSbLHX8mj+mF9r60R1kBeqE9MkoYwI= +github.com/corona10/goimagehash v1.1.0/go.mod h1:VkvE0mLn84L4aF8vCb6mafVajEb6QYMHl2ZJLn0mOGI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/farcloser/godolint v0.1.0 h1:KQQ5A6rGAk2wVUD2JX8VZquE6ltEW5u4f2QnSEESBZs= +github.com/farcloser/godolint v0.1.0/go.mod h1:JhI6JkK7kaElq27ZGPyILmJrTs3Sb8HNpC6OORWjRpg= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/flopp/go-findfont v0.1.0 h1:lPn0BymDUtJo+ZkV01VS3661HL6F4qFlkhcJN55u6mU= +github.com/flopp/go-findfont v0.1.0/go.mod h1:wKKxRDjD024Rh7VMwoU90i6ikQRCr+JTHB5n4Ejkqvw= +github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= +github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= +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/goccy/go-graphviz v0.2.5 h1:Z3JNjmdN6q8n2/Jc1qpB+yaMH2ZcRAfq3J52Go+sm3g= +github.com/goccy/go-graphviz v0.2.5/go.mod h1:hssjl/qbvUXGmloY81BwXt2nqoApKo7DFgDj5dLJGb8= +github.com/goccy/go-graphviz/cmd/dot v0.0.0-20251129032125-76e04975df88 h1:Mh6M/K1492BAgcwespMWPM2PUgcHFHcKDfzQplI9kFw= +github.com/goccy/go-graphviz/cmd/dot v0.0.0-20251129032125-76e04975df88/go.mod h1:zmFNqIWM1SMSOLqn7ZqbMALvyy6XgXrTFLcMGE33rxY= +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/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= +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/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +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/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +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-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17/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/moby/buildkit v0.26.1 h1:Cf/AB/8/5N+GBQnVPBW+hR2tDWoImuZ28ciqaF+mzgs= +github.com/moby/buildkit v0.26.1/go.mod h1:ylDa7IqzVJgLdi/wO7H1qLREFQpmhFbw2fbn4yoTw40= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +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/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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/tetratelabs/wazero v1.10.1 h1:2DugeJf6VVk58KTPszlNfeeN8AhhpwcZqkJj2wwFuH8= +github.com/tetratelabs/wazero v1.10.1/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU= +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/vbatts/git-validation v1.2.2 h1:AHTS8Jara7Pcu0ub7RusMBAvyLtjsJtF/xPU5Pm1BPE= +github.com/vbatts/git-validation v1.2.2/go.mod h1:Fj+04EdPcZ0rMOR+dqvppMVVgyNcOFRT5iFWuIc593A= +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/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= +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-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.13.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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +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/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +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=