diff --git a/.allowed_signers b/.allowed_signers new file mode 100644 index 0000000..a87e50e --- /dev/null +++ b/.allowed_signers @@ -0,0 +1 @@ +spam_blackhole@farcloser.world namespaces="git" sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIACDOXbkl7DthgVLTVZr8TNQcyQUX00MAqB3mWHik/vhAAAABHNzaDo= apostasie@farcloser.world diff --git a/.editorconfig b/.editorconfig index a9bf84e..bd078af 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,8 @@ -# http://editorconfig.org +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# https://editorconfig.org root = true [*] @@ -9,9 +13,45 @@ insert_final_newline = true indent_style = space indent_size = 2 -[*.diff] +# --- whitespace-significant formats --- +[*.{diff,patch}] +trim_trailing_whitespace = false + +[*.md] trim_trailing_whitespace = false +# --- Go: tabs, rendered 4 wide (gofmt emits tabs; width is display-only) --- +[*.go] +indent_style = tab +indent_size = 4 + +# --- task runner / build --- +[{Justfile,justfile,.justfile}] +indent_size = 4 + +[*.just] +indent_size = 4 + [Makefile] indent_style = tab + +# --- data formats (YAML must be spaces, never tabs) --- +[*.{json,jsonc,yaml,yml,toml}] +indent_size = 2 + +# --- JavaScript / TypeScript (2-space matches Prettier/Biome defaults) --- +[*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}] +indent_size = 2 + +# --- CSS family --- +[*.{css,scss,sass,less,pcss}] +indent_size = 2 + +# --- HTML / templates --- +[*.{html,htm,vue,svelte,astro}] +indent_size = 2 + +# --- Rust: rustfmt is 4-space, 100-col --- +[*.rs] indent_size = 4 +max_line_length = 100 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fae95b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files as binary, with no git magic updating line endings. +# This produces predictable results in different environments. +# +# Contributors on Windows will need to use a modern version of git +# and editors capable of LF line endings. +# +# See https://github.com/golangci/golangci-lint/issues/580 +# See https://github.com/golang/go/blob/master/.gitattributes + +* -text \ No newline at end of file diff --git a/.github/actions/setup-aqua/action.yaml b/.github/actions/setup-aqua/action.yaml new file mode 100644 index 0000000..d818972 --- /dev/null +++ b/.github/actions/setup-aqua/action.yaml @@ -0,0 +1,54 @@ +name: Setup aqua +description: > + Install aqua (pinned, checksum-verified), put its bin directory on PATH, and authorize the repository's committed policy. aqua itself is the one tool aqua cannot pin. Same pins as limen-install: the installer script is fetched at an exact tag and checksum-verified, and it installs an exact aqua version. +runs: + using: composite + steps: + - name: Install aqua (pinned, checksum-verified) + shell: bash + env: + # Bumped manually, as a pair: Renovate cannot recompute the sha256 + # that must change with the installer version. + AQUA_INSTALLER_VERSION: v4.0.2 + AQUA_INSTALLER_SHA256: 98b883756cdd0a6807a8c7623404bfc3bc169275ad9064dc23a6e24ad398f43d + # 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. + 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). + 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" \ + "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 + # aqua-managed can help here — this checksum guards the aqua + # installer itself.) + if command -v sha256sum >/dev/null 2>&1; then + echo "${AQUA_INSTALLER_SHA256} ${tmp}/aqua-installer" | sha256sum -c - + else + echo "${AQUA_INSTALLER_SHA256} ${tmp}/aqua-installer" | shasum -a 256 -c - + fi + chmod +x "${tmp}/aqua-installer" + "${tmp}/aqua-installer" -v "${AQUA_VERSION}" + rm -rf "${tmp}" + echo "${AQUA_ROOT_DIR}/bin" >>"$GITHUB_PATH" + - name: Authorize the committed aqua policy + # Before aqua will read the repo's local registry, the committed policy + # must be allowed — every caller needs this, whatever it runs next. + shell: bash + run: aqua policy allow aqua-policy.yaml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index a5243f6..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: CI - -on: [push, pull_request] - -jobs: - test: - # https://github.com/actions/runner-images - strategy: - matrix: - os: [macos-13, macos-14, macos-15] - runs-on: ${{ matrix.os }} - - steps: - - name: Checks-out repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - submodules: true - - - name: test - run: | - brew install shellcheck - ./test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..4dad5f2 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,82 @@ +# DO NOT EDIT MANUALLY. +# This workflow is generic — no project-specific content — and is destined to +# become part of the canonical baseline limen distributes. +# +# Design: minimal GitHub glue around the same tooling every developer runs +# locally. The only marketplace action is GitHub's own checkout, pinned by +# commit SHA (a tag can be moved to malicious code; a SHA cannot). Everything +# else is pinned, checksum-verified shell: aqua installs the repo's tools at +# the versions aqua.yaml pins, and `just` runs the exact recipes a laptop +# runs — CI green means the same thing as local green, by construction. +# Deliberately absent: runner egress filtering — the available options are +# third-party actions, which this workflow avoids on principle. +name: ci + +on: + push: + branches: [main] + pull_request: + +# No default token permissions: each job states what it needs. This workflow +# only ever reads the repository — it cannot write code, releases, or +# packages even if a step is compromised. +permissions: {} + +# A superseded run (new push to the same branch/PR) is cancelled, not raced. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +# Every run step is bash on every runner: without this, windows defaults to +# PowerShell, which never sees the git-bash environment setup-aqua prepares +# (the whole matrix is designed to run the recipes under git-bash). Explicit +# bash also means `-eo pipefail` everywhere, which the implicit linux/macos +# default lacks. +defaults: + run: + shell: bash + +jobs: + verify: + strategy: + fail-fast: false + matrix: + # Pinned images, not -latest: a runner bump is a reviewed change. + # macOS is not redundant: its /bin/bash is 3.2, the portability floor + # the shared recipes target. arm64 (ubuntu-24.04-arm) and windows + # cover the rest of the supported release matrix — windows runs the + # recipes under git-bash. + # These entries are also the default required-status-check contexts of + # the canonical limen:main ruleset (defaultRequiredChecks in + # internal/github/audit.go) — change the two together, in one release. + os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, windows-2025] + runs-on: ${{ matrix.os }} + # Generous for the windows leg: those runners are markedly slower, and the + # per-GOOS lint recipes now run three legs each. + timeout-minutes: 45 + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # The token is not left behind in .git/config: nothing in this + # workflow talks to GitHub after checkout. + persist-credentials: false + # Full history and refs: `just lint commits` validates the commit + # range against the PR's base branch, which a shallow clone lacks. + fetch-depth: 0 + + - name: Install aqua (pinned, checksum-verified) + uses: ./.github/actions/setup-aqua + + - name: Install pinned tools + # Link-only: the shims download each tool lazily on first use (and it + # is verified against the committed aqua-checksums.json then), so a + # job only ever pays for the tools its recipes actually run. + run: aqua install --only-link + + - name: Lint + run: just lint + + - name: Test + run: just test diff --git a/.github/workflows/update-aqua-checksum.yaml b/.github/workflows/update-aqua-checksum.yaml new file mode 100644 index 0000000..aa21b0b --- /dev/null +++ b/.github/workflows/update-aqua-checksum.yaml @@ -0,0 +1,133 @@ +# DO NOT EDIT MANUALLY. +# This workflow is generic — no project-specific content — and is destined to +# become part of the canonical baseline limen distributes. +# +# 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. +# +# 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 push step runs only git, with the token scoped to that single step. +# - The branch name reaches the shell via env, never template interpolation +# (script-injection hygiene). +# - No loop: a push made with the default GITHUB_TOKEN triggers no further +# workflows — and the no-change early exit terminates recursion +# regardless. +# +# 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. +name: update-aqua-checksum + +on: + push: + branches: ["renovate/**"] + +permissions: {} + +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. +defaults: + run: + shell: bash + +jobs: + update: + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Install aqua (pinned, checksum-verified) + uses: ./.github/actions/setup-aqua + + - name: Regenerate aqua-checksums.json + # No tool install: update-checksum only reads the manifest and hashes + # upstream artifacts — the smallest possible surface for a write job. + # Runs first: it is what makes the branch's limen pin installable for + # the converge step below. + 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. + 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. + 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 }} + + - name: Push the update, if any + env: + BRANCH: ${{ github.ref_name }} + 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; } + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # `--all` 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. + git add --all + # Signed-off-by: `just do lint commits` enforces DCO on the PR range, + # bot commits included. + git commit -m "chore: update aqua checksums and converge the limen baseline" \ + -m "Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + git push "https://x-access-token:${TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "HEAD:${BRANCH}" diff --git a/.gitignore b/.gitignore index 1848a53..ae2e9d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,27 @@ .DS_Store .idea -_* + +# --- added by limen fix: baseline patterns --- +Thumbs.db +Desktop.ini +.vscode/ +*.swp +*~ +*.local +_scratch/ +WIP_* +/build +/target/ +**/*.rs.bk +*.pdb +node_modules/ +.svelte-kit/ +.vite/ +vite.config.*.timestamp-* +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# --- added by limen fix: baseline patterns --- +/tmp diff --git a/.limen/.shellcheckrc b/.limen/.shellcheckrc new file mode 100644 index 0000000..4bc2aa1 --- /dev/null +++ b/.limen/.shellcheckrc @@ -0,0 +1,17 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# If you need local overrides for this project, you can use inline `# shellcheck disable=SCXXXX`. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# .shellcheckrc — https://www.shellcheck.net/wiki/ + +# Follow `source`/`.`-ed files so shellcheck checks across includes. +# 'true' lets it follow even non-constant source paths (best for repos with libs). +external-sources=true + +# Opt INTO the optional checks shellcheck ships but doesn't run by default. +enable=quote-safe-variables # flags unquoted vars that should be quoted +# enable=require-variable-braces # enforce ${var} consistently (stylistic — deliberately off) +enable=check-unassigned-uppercase +enable=deprecate-which # `which` → `command -v` +enable=avoid-nullary-conditions diff --git a/.limen/.yamlfmt b/.limen/.yamlfmt new file mode 100644 index 0000000..8269a9c --- /dev/null +++ b/.limen/.yamlfmt @@ -0,0 +1,56 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +# .yamlfmt — https://github.com/google/yamlfmt +# Formatter: the 'basic' formatter is the only one; these are its options. +formatter: + type: basic + + # LF everywhere, explicitly: yamlfmt's default flips to CRLF on Windows, + # which would flag every (correctly LF) file. Same doctrine as the pinned + # .editorconfig and .gitattributes: line endings never vary by platform. + line_ending: lf + + # --- indentation --- + indent: 2 # match your .editorconfig (2-space YAML) + include_document_start: false # don't force a leading `---` on every file + + # --- the defaults worth overriding --- + retain_line_breaks_single: true # collapse runs of blank lines to ONE, but keep + # intentional single blank lines (readability). + # Prefer this over retain_line_breaks, which keeps + # *all* blank lines and barely normalizes anything. + + scan_folded_as_literal: false # leave folded (>) scalars folded; don't rewrite to literal (|) + + # --- correctness / safety --- + disallow_anchors: false # set true ONLY if you want to forbid &anchors/*aliases + max_line_length: 0 # 0 = no wrapping. Leave OFF — yamlfmt's wrapping is + # crude and mangles long values; let humans wrap. + + # --- quoting / strings: leave alone --- + # yamlfmt is deliberately light on string normalization. Don't fight it; it won't + # aggressively re-quote, which is the safe default for mixed YAML (k8s, CI, etc.) + +# --- which files to format --- +# Every pattern below is doublestar syntax (`**/`, `{yaml,yml}` braces), which +# yamlfmt does NOT use unless told to: without this switch it hands patterns to +# the filesystem, where POSIX shrugs (ENOENT, silently no-op) but Windows +# hard-fails (`CreateFile .git/**: ... syntax is incorrect`). +doublestar: true + +include: + - "**/*.{yaml,yml}" + +gitignore_excludes: true + +exclude: + - ".git/**" + - "**/vendor/**" + - "**/testdata/**" # don't reformat fixtures — tests may assert exact bytes + - "**/*.gen.{yaml,yml}" # leave generated YAML alone + +# Stop at a file that fails to parse — malformed YAML must fail hard, in CI +# and everywhere else. +continue_on_error: false diff --git a/.limen/aqua-registry.yaml b/.limen/aqua-registry.yaml new file mode 100644 index 0000000..694b259 --- /dev/null +++ b/.limen/aqua-registry.yaml @@ -0,0 +1,94 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Most tools can be installed directly from aqua registry without local overrides. +# Addition of non-standard tools can be discussed on https://github.com/farcloser/limen + +packages: + # The /v2 module-path suffix is mandatory (Go semantic import versioning): + # `go install github.com/google/go-licenses@v2.x` is rejected by the + # toolchain. Go strips the /v2 when naming the binary; files spells it out. + - type: go_install + path: github.com/google/go-licenses/v2 + description: Report on the licenses of a Go project's dependencies + version_source: github_tag + files: + - name: go-licenses + - type: go_install + path: github.com/vbatts/git-validation + description: Validate git commit rules (DCO sign-off, subject length, …) + version_source: github_tag + # Graphviz `dot` as a single static Go binary: the real graphviz C code + # compiled to WASM, executed via wazero — no C toolchain, no shared + # libraries, which is why this can live in aqua while system graphviz + # cannot. Renders the PNGs of `just do test go profile`. The cmd/dot module is + # nested and untagged upstream, hence the explicit repo and the + # pseudo-version pin in aqua.yaml. + - type: go_install + name: github.com/goccy/go-graphviz/cmd/dot + path: github.com/goccy/go-graphviz/cmd/dot + repo_owner: goccy + repo_name: go-graphviz + description: Graphviz dot CLI, pure-Go build (WASM via wazero) + version_source: github_tag + # The Go team distributes govulncheck via `go install` only, by policy. The + # repo is not inferable from the golang.org import path, hence the explicit + # owner/name (same as the standard registry does for gopls). + - type: go_install + name: golang.org/x/vuln/cmd/govulncheck + path: golang.org/x/vuln/cmd/govulncheck + repo_owner: golang + repo_name: vuln + description: Scan dependencies against the Go vulnerability database + version_source: github_tag + # Same golang.org path situation as govulncheck: explicit name and repo. + - type: go_install + name: golang.org/x/tools/cmd/deadcode + path: golang.org/x/tools/cmd/deadcode + repo_owner: golang + repo_name: tools + description: Whole-program detection of unreachable functions + version_source: github_tag + # hadolint as pure Go, from the farcloser stable. The binary is the nested + # cmd/godolint package of the root module — explicit name and repo, same + # shape as go-graphviz's cmd/dot above (tags live at the repo root). + - type: go_install + name: github.com/farcloser/godolint/cmd/godolint + path: github.com/farcloser/godolint/cmd/godolint + repo_owner: farcloser + repo_name: godolint + description: Lint Dockerfiles (a pure-Go port of hadolint) + version_source: github_tag + # limen itself: every repo pins the version that enforces it, so the binary + # and the canonical files it embeds travel together (no version skew between + # the checker and the checked). Graduates to the standard registry once + # farcloser/limen is registered there. + - type: github_release + repo_owner: farcloser + repo_name: limen + description: Verify a repository against Farcloser engineering rules + asset: limen_{{trimV .Version}}_{{.OS}}_{{.Arch}}.tar.gz + format: tar.gz + files: + - name: limen + checksum: + type: github_release + asset: checksums.txt + algorithm: sha256 + # 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. + cosign: + bundle: + type: github_release + asset: checksums.txt.sigstore.json + opts: + - --certificate-identity + - https://github.com/farcloser/limen/.github/workflows/release.yaml@refs/tags/{{.Version}} + - --certificate-oidc-issuer + - https://token.actions.githubusercontent.com + supported_envs: + - darwin + - linux + - windows diff --git a/.limen/just/build-go.just b/.limen/just/build-go.just new file mode 100644 index 0000000..fdcca9c --- /dev/null +++ b/.limen/just/build-go.just @@ -0,0 +1,159 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# Project recipes live in the root Justfile, below the shared-baseline import. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +# Ported from the historical Make build system, corrected along the way: +# - a "binary" is a DIRECTORY under cmd/ that contains Go sources — a stray +# file or docs directory no longer becomes a phantom target; +# - version stamping is reproducible: main.version carries `git describe`, +# and commit/date come from Go's own VCS embedding (`go version -m`) at +# their COMMIT values — never wall-clock build time, which made the same +# commit produce different binaries; +# - build tags are scoped to these commands, never exported globally where +# they would leak into tests and linters; +# - static states its preconditions (Linux, CGO) and fails fast instead of +# dying with a cryptic linker error; +# - a race build exists. +# Binaries land in build/ (covered by the canonical .gitignore). Projects +# declare `var version = "dev"` in each main package for the stamp to land. +# Extra flags: export BUILD_GO_FLAGS from 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. + +# netgo/osusergo force the pure-Go resolver and user lookups. No-ops when +# CGO_ENABLED=0 (pure Go already); with CGO they keep DNS and user handling +# out of libc, which is exactly what makes `static` viable on glibc. +go_tags := 'netgo,osusergo' + +# CGO is opt-in per invocation (`CGO_ENABLED=1 just do build go`). Defaulted to 0 +# explicitly for every recipe rather than left unset: go's own default is CGO +# on for native builds when a C toolchain exists, and unset must mean pure Go +# here. +export CGO_ENABLED := env('CGO_ENABLED', '0') + +# --- C toolchain hardening — only effective when CGO_ENABLED=1 -------------- +# The canonical hardening set (see the Red Hat compiler-flags guidance), +# platform-gated: stack-clash protection and the -z linker set are Linux-only +# (ld64 on macOS supports neither). +c_warnings := '-Wall -Werror=format-security' +c_security := '-fstack-protector-strong -fPIE -D_FORTIFY_SOURCE=2' + (if os() == 'linux' { ' -fstack-clash-protection' } else { '' }) +cgo_cflags_release := c_warnings + ' -O2 ' + c_security + ' -pipe' + +# Debug: -O0 with real debug info. FORTIFY_SOURCE is deliberately absent — it +# requires -O1 or higher and would only produce warnings at -O0. +cgo_cflags_debug := c_warnings + ' -O0 -g -grecord-gcc-switches -pipe' + +# libstdc++ assertions are a C++-only concern: valid in CXXFLAGS, not CFLAGS. +cgo_cxxflags_debug := cgo_cflags_debug + ' -D_GLIBCXX_ASSERTIONS' +cgo_ldflags := if os() == 'linux' { '-Wl,-z,defs -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack' } else { '' } + +default: release + +# Release: reproducible (trimpath, commit-time VCS stamp), stripped, PIE. +# Twin of the builds section in .goreleaser.yaml (the release builder, where +# a project ships one) — a flag change here must land there too. See the +# comment there for the differences that are intended. +release: (_banner "build go" "release") + #!/usr/bin/env bash + set -euo pipefail + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + ldflags="-s -w -X main.version=${version}" + if [ "${CGO_ENABLED:-0}" = "1" ]; then + export CGO_CFLAGS='{{ cgo_cflags_release }}' + export CGO_CXXFLAGS='{{ cgo_cflags_release }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + ldflags="-linkmode=external -extldflags=-pie ${ldflags}" + fi + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}" + go build -trimpath -buildmode=pie -tags='{{ go_tags }}' \ + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "build/${name}" "./${dir%/}" + done + +# Debug: optimizations and inlining off, symbols kept, real paths kept — no +# trimpath, deliberately: debuggers want actual file locations. -debug suffix. +debug: (_banner "build go" "debug") + #!/usr/bin/env bash + set -euo pipefail + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + ldflags="-X main.version=${version}" + if [ "${CGO_ENABLED:-0}" = "1" ]; then + export CGO_CFLAGS='{{ cgo_cflags_debug }}' + export CGO_CXXFLAGS='{{ cgo_cxxflags_debug }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + ldflags="-linkmode=external -extldflags=-pie ${ldflags}" + fi + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-debug" + go build -buildmode=pie -gcflags='all=-N -l' -tags='{{ go_tags }}' \ + -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "build/${name}-debug" "./${dir%/}" + done + +# Race: a diagnostic build with the race detector, which requires cgo (forced +# here) and, matching the test module, an external linkmode. Unstripped, real +# paths. -race suffix. +race: (_banner "build go" "race") + #!/usr/bin/env bash + set -euo pipefail + export CGO_ENABLED=1 + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-race" + go build -race -tags='{{ go_tags }}' \ + -ldflags "-linkmode=external -X main.version=${version}" \ + ${extra[@]+"${extra[@]}"} -o "build/${name}-race" "./${dir%/}" + done + +# Static: fully static external link. Preconditions enforced, not commented: +# Linux only (macOS has no static libc), and CGO_ENABLED=1 (with CGO off, +# every build is already static — use `release`). No PIE: classic static and +# PIE conflict. -static suffix. +static: (_banner "build go" "static") + #!/usr/bin/env bash + set -euo pipefail + if [ "$(uname -s)" != "Linux" ]; then + echo "static builds require Linux: macOS has no static libc to link against." >&2 + exit 1 + fi + if [ "${CGO_ENABLED:-0}" != "1" ]; then + echo "static needs CGO_ENABLED=1 — a pure-Go build is already static (use \`just do build go\`)." >&2 + exit 1 + fi + version=$(git describe --tags --always --dirty 2>/dev/null || echo dev) + export CGO_CFLAGS='{{ cgo_cflags_release }}' + export CGO_CXXFLAGS='{{ cgo_cflags_release }}' + export CGO_LDFLAGS='{{ cgo_ldflags }}' + extra=() + [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" + mkdir -p build + for dir in cmd/*/; do + name=$(basename "${dir}") + ls "${dir}"*.go >/dev/null 2>&1 || continue + echo "→ build/${name}-static" + go build -trimpath -tags='{{ go_tags }}' \ + -ldflags "-linkmode=external -extldflags=-static -s -w -X main.version=${version}" \ + ${extra[@]+"${extra[@]}"} -o "build/${name}-static" "./${dir%/}" + done diff --git a/.limen/just/build.just b/.limen/just/build.just new file mode 100644 index 0000000..9582374 --- /dev/null +++ b/.limen/just/build.just @@ -0,0 +1,16 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Go builds live in their own submodule: `just do build go` makes the release +# binaries, `just do build go ` (debug, race, static) makes a variant. +mod go 'build-go.just' + +# A submodule's recipe IS addressable as a dependency (go::default); only a +# bare module path (go) is not. +default: go::default diff --git a/.limen/just/do.just b/.limen/just/do.just new file mode 100644 index 0000000..34a69d2 --- /dev/null +++ b/.limen/just/do.just @@ -0,0 +1,25 @@ +# DO NOT EDIT MANUALLY. +# 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. + +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 + +mod build 'build.just' +mod tools 'tools.just' +mod lint 'lint.just' +mod test 'test.just' +mod fix 'fix.just' + +# Flat (imported, not a module) so it takes arguments: `just do release v1.2.3`. +import 'release.just' diff --git a/.limen/just/fix-go.just b/.limen/just/fix-go.just new file mode 100644 index 0000000..f1b010a --- /dev/null +++ b/.limen/just/fix-go.just @@ -0,0 +1,28 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# Project recipes live in the root Justfile, below the shared-baseline import. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: code mod + +# golangci-lint auto-fixes, once per supported platform (see _per-goos in +# lib.just: platform-specific files are only analyzed for the GOOS that +# builds them). The formatter is build-graph-independent and runs once — last, +# so the fixes' edits end up formatted. +code: (_banner "fix go" "go") (_per-goos "golangci-lint run --fix") + golangci-lint fmt + +# Tidy go.mod/go.sum. +mod: (_banner "fix go" "mod") + go mod tidy + +# Update every dependency to its latest version, then tidy. +up: (_banner "fix go" "up") && mod + go get -u ./... diff --git a/.limen/just/fix-homebrew.just b/.limen/just/fix-homebrew.just new file mode 100644 index 0000000..e137dc6 --- /dev/null +++ b/.limen/just/fix-homebrew.just @@ -0,0 +1,65 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +# 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). + +default: style + +# Repair what `just do lint homebrew style` reports; passes vacuously when the +# repository carries no formulas or casks. +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. + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard \ + 'Formula/*.rb' 'Formula/**/*.rb' 'Casks/*.rb' 'Casks/**/*.rb') + if [ "${#files[@]}" -eq 0 ]; then + echo "no formulas or casks (Formula/, Casks/) — nothing to fix." + exit 0 + fi + {{ _require_brew }} + # shellcheck disable=SC2154 # BREW_BIN is exported by the canonical Justfile (main.just). + "$BREW_BIN" style --fix "${files[@]}" + +# Homebrew exists on macOS only: everywhere else this recipe degrades to a +# LOUD no-op — the twin of the variable in lint-homebrew.just; a change here +# must land there too. +[private] +_skip_unless_macos := ''' + if [ "$(uname -s)" != 'Darwin' ]; then + echo "homebrew is macOS-only — nothing to do on this platform (skipped, not failed)." + exit 0 + fi +''' + +# BREW_BIN comes from main.just (ambient-PATH capture) — the twin of the +# variable in lint-homebrew.just; a change here must land there too. +[private] +_require_brew := ''' + if [ -z "${BREW_BIN:-}" ]; then + echo "brew was not found on your PATH when just started." >&2 + echo "brew is a machine-layer tool — install it outside the repo (or export" >&2 + echo "BREW_BIN=/path/to/brew), then re-run." >&2 + exit 1 + fi +''' diff --git a/.limen/just/fix-rust.just b/.limen/just/fix-rust.just new file mode 100644 index 0000000..df77a6b --- /dev/null +++ b/.limen/just/fix-rust.just @@ -0,0 +1,15 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +rust: (_banner "fix" "rust") + cargo clippy --fix --all-targets --all-features --allow-dirty --allow-staged + cargo fmt --all diff --git a/.limen/just/fix.just b/.limen/just/fix.just new file mode 100644 index 0000000..4996ed2 --- /dev/null +++ b/.limen/just/fix.just @@ -0,0 +1,43 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: limen just yaml + +# limen itself: repair what `just 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). +limen: (_banner "fix" "limen") + ${LIMEN_BIN:-limen} fix . + +# Repair what `just do lint github` reports — plans first, applies on consent +# (pass -yes for unattended use, -org for the organization). Never in +# the default set, same reasoning as the lint twin. +github *args: (_banner "fix" "github") + ${LIMEN_BIN:-limen} github fix {{ args }} + +just: (_banner "fix" "just") + #!/usr/bin/env bash + set -euo pipefail + # --cached --others --exclude-standard: tracked *and* new untracked files, + # still honoring .gitignore — so a not-yet-staged file is formatted too. + git ls-files -z --cached --others --exclude-standard 'justfile' 'Justfile' '*.just' '**/justfile' \ + | xargs -0 -I{} just --fmt --justfile {} + +yaml: (_banner "fix" "yaml") + yamlfmt -conf .limen/.yamlfmt + +# Go fixers live in their own submodule: `just do fix go` runs them all, +# `just do fix go ` (e.g. `just do fix go mod`) runs one. +mod go 'fix-go.just' +mod rust 'fix-rust.just' +mod homebrew 'fix-homebrew.just' diff --git a/.limen/just/lib.just b/.limen/just/lib.just new file mode 100644 index 0000000..9336591 --- /dev/null +++ b/.limen/just/lib.just @@ -0,0 +1,45 @@ +# DO NOT EDIT MANUALLY. +# 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). +_banner mod name: + @echo "▶ {{ mod }}: {{ name }}" + +# 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 must export CGO_ENABLED=1 and gets one native run only: reduced +# coverage, announced loudly. +_per-goos +cmd: + #!/usr/bin/env bash + set -euo pipefail + if [ "${CGO_ENABLED:-0}" = "1" ]; then + echo "CGO_ENABLED=1: cross-platform runs are impossible (foreign-GOOS 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 }} + done + fi diff --git a/.limen/just/lint-go.just b/.limen/just/lint-go.just new file mode 100644 index 0000000..0691c79 --- /dev/null +++ b/.limen/just/lint-go.just @@ -0,0 +1,115 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: code mod vuln licenses + +# golangci-lint, once per supported platform (see _per-goos in lib.just: +# platform-specific files are only analyzed for the GOOS that builds them). +# The formatter is build-graph-independent and runs once. 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 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. +mod: (_banner "lint go" "mod") + go mod tidy -diff + +# Known-vulnerability scan against the Go vulnerability database (network: +# govulncheck fetches vuln.go.dev), once per supported platform (see _per-goos +# in lib.just: call paths and dependencies differ by GOOS). +vuln: (_banner "lint go" "vuln") (_per-goos "govulncheck ./...") + +# Dependency license compliance, once per supported platform (see _per-goos in +# lib.just: each GOOS pulls its own dependencies). A project that needs extra +# flags — typically --ignore= for the module-layout false positives of +# https://github.com/google/go-licenses/issues/186 — exports them from its +# the 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 +# +# GOROOT is passed explicitly because go-licenses recognizes stdlib packages by +# their location under GOROOT — but, being a go_install binary, its built-in +# default is the GOROOT of whatever toolchain COMPILED it. When that differs +# from the pinned toolchain on PATH (which is where packages actually load +# from), every stdlib package degrades to "does not have module info" errors. +licenses: (_banner "lint go" "licenses") (_per-goos 'GOROOT="$(go env GOROOT)" go-licenses check --include_tests --allowed_licenses=Apache-2.0,BSD-2-Clause,BSD-3-Clause,MIT ${LINT_GO_LICENSES_FLAGS:-} ./...') + +# Bounds-check-elimination report: every bounds check the compiler could NOT +# eliminate, for performance tuning. Informational — it never fails, so it is +# not in the default set; and it reports for the native architecture only, +# since BCE results are arch-specific and this is a report for the human +# reading it, not a gate. +bce: (_banner "lint go" "bce") + #!/usr/bin/env bash + set -euo pipefail + echo "Bounds Check Elimination Report" + echo "================================" + echo "" + output=$(go build -gcflags='-d=ssa/check_bce/debug=1' ./... 2>&1 | grep -v '^#' || true) + if [ -z "$output" ]; then + echo "No bounds checks detected (BCE fully eliminated)." + else + total=$(echo "$output" | wc -l | tr -d ' ') + echo "Total: $total bounds checks" + echo "" + echo "By file:" + echo "$output" | sed 's/:.*$//' | sort | uniq -c | sort -rn + echo "" + echo "Details:" + echo "$output" | sort + fi + +# Escape-analysis and inlining report — both come from the same compiler pass +# (-gcflags=-m): values the compiler moved to the heap (the prime source of GC +# pressure) and functions it refused to inline, with the reason. Informational +# like bce: never fails, native architecture only, not in the default set. +escape: (_banner "lint go" "escape") + #!/usr/bin/env bash + set -euo pipefail + output=$(go build -gcflags='-m' ./... 2>&1 | grep -v '^#' || true) + heap=$(echo "$output" | grep -E 'escapes to heap|moved to heap' || true) + noinline=$(echo "$output" | grep 'cannot inline' || true) + echo "Escape Analysis Report" + echo "======================" + if [ -z "$heap" ]; then + echo "No heap escapes." + else + echo "Total: $(echo "$heap" | wc -l | tr -d ' ') heap escapes" + echo "" + echo "By file:" + echo "$heap" | sed 's/:.*$//' | sort | uniq -c | sort -rn + echo "" + echo "$heap" | sort + fi + echo "" + echo "Inlining Failures" + echo "=================" + if [ -z "$noinline" ]; then + echo "None." + else + echo "Total: $(echo "$noinline" | wc -l | tr -d ' ') functions the compiler could not inline" + echo "" + echo "$noinline" | sort + fi + +# Unreachable functions, by whole-program call-graph analysis (deeper than the +# per-package `unused` linter in golangci). -test keeps library-only repos +# analyzable (test binaries serve as roots) and stops dead code from hiding +# behind test-only callers. Informational: native only, not in the default set. +deadcode: (_banner "lint go" "deadcode") + deadcode -test ./... diff --git a/.limen/just/lint-homebrew.just b/.limen/just/lint-homebrew.just new file mode 100644 index 0000000..6d7d573 --- /dev/null +++ b/.limen/just/lint-homebrew.just @@ -0,0 +1,137 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +# 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. + +default: style audit + +# Homebrew's RuboCop profile over every formula and cask in the repository; +# passes vacuously when there are none. The mutating twin is +# `just do fix homebrew style`. +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 + # brew-less. + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard \ + 'Formula/*.rb' 'Formula/**/*.rb' 'Casks/*.rb' 'Casks/**/*.rb') + if [ "${#files[@]}" -eq 0 ]; then + echo "no formulas or casks (Formula/, Casks/) — nothing to lint." + exit 0 + fi + {{ _require_brew }} + # 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. +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. + if [ -z "$(git ls-files --cached --others --exclude-standard \ + 'Formula/*.rb' 'Formula/**/*.rb' 'Casks/*.rb' 'Casks/**/*.rb')" ]; then + echo "no formulas or casks (Formula/, Casks/) — nothing to audit." + exit 0 + fi + {{ _require_brew }} + tap="${LINT_HOMEBREW_TAP:-}" + case "$tap" in + */*) ;; + *) + echo "LINT_HOMEBREW_TAP is not set (or not 'user/name') — brew audit addresses formulas" >&2 + echo "by tap name, not by path. Export it in the root Justfile, e.g.:" >&2 + echo " export LINT_HOMEBREW_TAP := 'farcloser/tap'" >&2 + exit 1 + ;; + esac + # shellcheck disable=SC2154 # BREW_BIN is exported by the canonical Justfile (main.just). + link="$("$BREW_BIN" --repository)/Library/Taps/${tap%%/*}/homebrew-${tap##*/}" + created="" + if [ -e "$link" ] || [ -L "$link" ]; then + # Already tapped: proceed only when it is exactly this checkout — + # auditing a stale clone of the same tap would judge the wrong tree. + if [ "$(readlink "$link" 2>/dev/null || true)" != "$PWD" ]; then + echo "tap $tap is already installed at" >&2 + echo " $link" >&2 + echo "and is not this checkout — untap it (brew untap $tap) or audit there." >&2 + exit 1 + fi + else + mkdir -p "$(dirname "$link")" + ln -s "$PWD" "$link" + created="$link" + fi + cleanup() { + if [ -n "$created" ]; then + rm -f "$created" + fi + } + trap cleanup EXIT + # 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. +[private] +_skip_unless_macos := ''' + if [ "$(uname -s)" != 'Darwin' ]; then + echo "homebrew is macOS-only — nothing to do on this platform (skipped, not failed)." + exit 0 + 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. +[private] +_require_brew := ''' + if [ -z "${BREW_BIN:-}" ]; then + echo "brew was not found on your PATH when just started." >&2 + echo "brew is a machine-layer tool — install it outside the repo (or export" >&2 + echo "BREW_BIN=/path/to/brew), then re-run." >&2 + exit 1 + fi +''' diff --git a/.limen/just/lint-rust.just b/.limen/just/lint-rust.just new file mode 100644 index 0000000..f7dd113 --- /dev/null +++ b/.limen/just/lint-rust.just @@ -0,0 +1,15 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# Project recipes live in the root Justfile, below the shared-baseline import. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +rust: (_banner "lint" "rust") + cargo fmt --all --check + cargo clippy --all-targets --all-features -- --deny warnings diff --git a/.limen/just/lint.just b/.limen/just/lint.just new file mode 100644 index 0000000..7520df7 --- /dev/null +++ b/.limen/just/lint.just @@ -0,0 +1,213 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: links yaml shell dockerfile commits just aqua limen + +# Go linters live in their own submodule: `just do lint go` runs them all, +# `just do lint go ` (e.g. `just do lint go mod`) runs one. +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. +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 +# the 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. +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`. +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. + git ls-files -z --cached --others --exclude-standard 'justfile' 'Justfile' '*.just' '**/justfile' \ + | xargs -0 -I{} just --fmt --check --justfile {} + +# godolint — hadolint as pure Go (https://github.com/farcloser/godolint) — +# over every Dockerfile in the repository; passes vacuously when there are +# none, so it can sit in the default set for every repo. Rule exceptions +# belong next to the instruction they excuse, as inline +# `# hadolint ignore=DLxxxx` pragmas — not in the recipe. +dockerfile: (_banner "lint" "dockerfile") + #!/usr/bin/env bash + set -euo pipefail + # --cached --others --exclude-standard: tracked *and* new untracked files, + # still honoring .gitignore — so a not-yet-staged Dockerfile is linted too + # (same as `lint just`). The patterns cover the naming conventions — the + # bare name and the . / . forms, at any + # depth — for both Dockerfile and its OCI-neutral synonym Containerfile + # (podman/buildah). '*.Dockerfile' needs no '**/' twin — a wildcard pattern + # already matches across directories; a literal like 'Dockerfile' does not. + files=() + while IFS= read -r -d '' f; do + files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard \ + 'Dockerfile' '**/Dockerfile' 'Dockerfile.*' '**/Dockerfile.*' '*.Dockerfile' \ + 'Containerfile' '**/Containerfile' 'Containerfile.*' '**/Containerfile.*' '*.Containerfile') + # godolint validates RUN instructions through shellcheck when it finds it + # on PATH — the hermetic PATH (see the root Justfile) always provides the + # pinned one. + [ ${#files[@]} -eq 0 ] || godolint "${files[@]}" + +yaml: (_banner "lint" "yaml") + yamlfmt -conf .limen/.yamlfmt -lint + +shell: (_banner "lint" "shell") + #!/usr/bin/env bash + set -euo pipefail + # A file is a shell script if it declares itself one, by either signal: + # - a .sh/.bash extension → a missing shebang is then a lint error (SC2148), or + # - a shebang on line 1 for a dialect shellcheck lints (sh/bash/dash/ksh — + # not zsh, which shellcheck cannot check) → covers extension-less scripts. + # Filtering on the shebang alone would silently skip an *.sh that forgot its shebang. + # No mapfile (bash 4+): build the array with a read loop, portable to bash 3.2. + files=() + while IFS= read -r -d '' f; do + case "$f" in + *.sh|*.bash) files+=("$f"); continue ;; + esac + head -n 1 "$f" 2>/dev/null | grep -qE '^#!.*\b(bash|dash|ksh|sh)\b' && files+=("$f") + done < <(git ls-files -z --cached --others --exclude-standard) + [ ${#files[@]} -eq 0 ] || shellcheck --rcfile .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. + 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. + 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. + out="$tmpdir/${name//:/_}" + printf '%s' "$body" | base64 -d > "$out" + head -n 1 "$out" | grep -qE '^#!.*\b(bash|dash|ksh|sh)\b' || continue + recipes+=("$out") + done < <(printf '%s' "$dump" | jq -r ' + def recipes_of(prefix): + ((.recipes // {}) | to_entries[] + | [prefix + .key, + (.value.body + | map(map(if type == "string" then . else "INTERP" end) | join("")) + | join("\n") | @base64)]), + ((.modules // {}) | to_entries[] + | .key as $mod | .value | recipes_of(prefix + $mod + "::")); + recipes_of("") | @tsv' | tr -d '\r') + # SC1010 is excluded for recipe bodies only: `just do ` is our shared + # namespace, and shellcheck misreads the argument "do" as the shell keyword. + # The constant-word checks (SC2050/SC2078/SC2157/SC2194) are excluded for + # recipe bodies only: a masked INTERP is constant to shellcheck but dynamic + # in reality, so those hits can only be false positives of the masking. + [ ${#recipes[@]} -eq 0 ] || shellcheck --rcfile .limen/.shellcheckrc --exclude=SC1010,SC2050,SC2078,SC2157,SC2194 "${recipes[@]}" + +# Check documentation links with lychee (respects .gitignore, skips hidden files). +links: (_banner "lint" "links") + #!/usr/bin/env bash + set -euo pipefail + # A directory input (not a glob) is what makes lychee honor .gitignore/hidden. + # Canonical exclusions (and their rationale) live in .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 — + # and lychee merges them, concatenating the exclude lists. + args=(--config .limen/lychee.toml) + if [ -f .lychee.toml ]; then + args+=(--config .lychee.toml) + fi + lychee --no-progress "${args[@]}" . + +# Validate commit hygiene — DCO sign-off, subject length, dangling whitespace — +# over a commit range (default: everything ahead of the upstream default branch; +# pass one explicitly otherwise, e.g. `just do lint commits v1.0.0..HEAD`). +commits range="": (_banner "lint" "commits") + #!/usr/bin/env bash + set -euo pipefail + # Contributors' SSH keys, when the repo ships them, arm signature display + # for humans (git log --show-signature, git tag -v). None of the rules run + # below verify signatures — this is convenience wiring, not enforcement. + # See https://github.com/andyfeller/gh-ssh-allowed-signers for automation + # to retrieve contributor keys. + if [ -f .allowed_signers ]; then + git config --unset-all gpg.ssh.allowedSignersFile 2>/dev/null || true + git config --add gpg.ssh.allowedSignersFile .allowed_signers + fi + range="{{ range }}" + if [ -z "$range" ] && [ -n "${GITHUB_BASE_REF:-}" ]; then + # GitHub Actions pull request: validate against the PR's actual base + # branch, whatever it targets. Requires actions/checkout with + # fetch-depth: 0 — the default shallow checkout has no base refs (and + # only the synthetic merge commit as "history": vacuously green). + if git rev-parse --verify -q "origin/$GITHUB_BASE_REF" >/dev/null 2>&1; then + range="origin/${GITHUB_BASE_REF}..HEAD" + fi + fi + if [ -z "$range" ]; then + # Default to the commits this branch adds over the upstream default + # branch. origin/HEAD is only set when `git remote set-head` ran, so + # fall back through the common default-branch names; with no upstream + # at all, validate the full history. + for ref in origin/HEAD origin/main origin/master; do + if git rev-parse --verify -q "$ref" >/dev/null 2>&1; then + range="$ref..HEAD" + break + fi + done + fi + if [ -n "$range" ]; then + git-validation -run DCO,short-subject,dangling-whitespace -range "$range" + else + git-validation -run DCO,short-subject,dangling-whitespace + fi + +# Verify aqua-checksums.json is complete and in sync with aqua.yaml. +aqua: (_banner "lint" "aqua") + #!/usr/bin/env bash + set -euo pipefail + # aqua has no read-only validator, so regenerate and compare against the + # committed file (needs network). `git diff HEAD` is staging-independent — it + # compares the working tree to the last commit, not the index. On drift the + # corrected file is left in place — review and commit it. + # --log-level warn: the per-package INFO lines are noise when nothing is wrong. + aqua --log-level warn update-checksum --prune + if ! git diff --quiet HEAD -- aqua-checksums.json; then + echo "aqua-checksums.json was out of sync with aqua.yaml (regenerated above) — review and commit it." >&2 + exit 1 + fi diff --git a/.limen/just/main.just b/.limen/just/main.just new file mode 100644 index 0000000..ea20b55 --- /dev/null +++ b/.limen/just/main.just @@ -0,0 +1,65 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# The root Justfile is the project's own: a shim that imports this file and +# then carries the project's recipes — everything shared lives here, so the +# shim never has to change. + +# The project may redefine the shared recipes below (info, or an explicit +# default) in its root Justfile: the later definition — the project's — wins. +set allow-duplicate-recipes + +# 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. +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_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: ambient variables tunnel straight through the hermetic PATH. +# An inherited GOROOT (IDEs inject one, often pointing into the module cache, +# which `go clean -modcache` deletes) overrides where the pinned go finds its +# stdlib — with modern Go it should never be set: go derives it from its own +# location. Emptied rather than unexported: go treats '' as unset, and unlike +# `unexport`, an `export` propagates into module recipes. GOTOOLCHAIN=local +# forbids silent toolchain switching: when go.mod outpaces the pin, recipes +# fail loudly asking for a pin bump instead of downloading an unpinned +# toolchain behind your back. +export GOROOT := '' +export GOTOOLCHAIN := 'local' + +# Hermetic, per-project linter cache (same doctrine as PATH and GOROOT: no +# shared mutable state across repos). Lives under build/ — gitignored, and +# 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. +# Show every available recipe — the project's own and the shared `do` tree. +default: + @just --list + +# Print meaningful information about this project. +info: + @echo "name: {{ project }}" + @echo "upstream: $(git remote get-url origin 2>/dev/null || echo '(none)')" + @echo "semver: $(git describe --tags --abbrev=0 2>/dev/null || echo '(none)')" + @echo "commit: $(git rev-parse --short HEAD 2>/dev/null || echo '(none)')" + @echo "date: $(git log --max-count=1 --format=%cd --date=short 2>/dev/null || echo '(none)')" + +# Every shared limen task: `just do lint`, `just do build go`, `just do release v1.2.3` — the top level stays the project's. +mod do 'do.just' diff --git a/.limen/just/release.just b/.limen/just/release.just new file mode 100644 index 0000000..ee88147 --- /dev/null +++ b/.limen/just/release.just @@ -0,0 +1,205 @@ +# DO NOT EDIT MANUALLY. +# 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. + +# Imported flat into the canonical Justfile — not a module — so the recipe can +# take arguments (`just do release v1.2.3`; a module invocation would parse the +# tag as a recipe path). Deliberately no settings and no _lib import here: an +# import's settings would apply to the whole importing module. + +# Release. The default is the CI lane; everything local is opted into with +# --local (see book/recipes.md): +# +# 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 ] +# The fully local lane: tag + goreleaser + key-based cosign from this +# machine. The key path is a mandatory argument; the password is +# prompted when not given. 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`')] +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 + usage() { + echo "usage: just do release # signed tag + push; CI builds, signs (keyless), publishes" >&2 + echo " just do release --local [--cosign-password

] # fully local, key-based cosign" >&2 + echo " just do release --local --dry-run # local unsigned snapshot into build/release/" >&2 + echo " just do release --ci # workflow half of the default lane (GitHub Actions only)" >&2 + } + require_clean() { + # A dirty tree is the most common failure and goreleaser hard-refuses + # it — check long before anything is created or pushed. + if [ -n "$(git status --porcelain)" ]; then + echo "the working tree is dirty — commit or stash before releasing:" >&2 + git status --short >&2 + exit 1 + fi + } + require_version_tag() { + if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then + echo "'$1' is not a version tag (vX.Y.Z or vX.Y.Z-prerelease, e.g. v0.0.0-test.1)." >&2 + exit 2 + fi + } + tag_and_push() { + # Create the signed tag — unless it already exists pointing at HEAD, so + # a retry after a failed publish resumes instead of failing here. + if existing=$(git rev-parse -q --verify "refs/tags/$1^{commit}"); then + if [ "$existing" != "$(git rev-parse HEAD)" ]; then + echo "tag $1 already exists and does not point at HEAD — refusing." >&2 + exit 1 + fi + echo "tag $1 already on HEAD — reusing it." + else + git tag -s "$1" -m "$1" + fi + git push origin HEAD + git push origin "refs/tags/$1" + } + set -- {{ args }} + case "${1:-}" in + --local) + shift + if [ "${1:-}" = "--dry-run" ]; then + # Snapshot builds skip signing: no key material needed. + exec goreleaser release --snapshot --clean --skip=sign + fi + key="${1:-}" + if [ -z "$key" ]; then + usage + exit 2 + fi + shift + tag="" + password="" + while [ $# -gt 0 ]; do + case "$1" in + --cosign-password) + if [ $# -lt 2 ]; then + echo "--cosign-password needs a value." >&2 + exit 2 + fi + password="$2" + shift 2 + ;; + -*) + echo "unknown option: $1" >&2 + usage + exit 2 + ;; + *) + if [ -n "$tag" ]; then + echo "unexpected argument: $1 (tag already given: $tag)" >&2 + usage + exit 2 + fi + tag="$1" + shift + ;; + esac + done + if [ -z "$tag" ]; then + usage + exit 2 + fi + if [ ! -f "$key" ]; then + echo "cosign key not found: $key" >&2 + echo "generate a key pair once with 'cosign generate-key-pair'; commit cosign.pub, keep the private key out of the tree." >&2 + exit 1 + fi + require_version_tag "$tag" + require_clean + if [ -z "${GITHUB_TOKEN:-}" ]; then + echo "GITHUB_TOKEN is not set — goreleaser needs it to publish the GitHub release." >&2 + exit 1 + fi + if [ -z "$password" ]; then + # cosign under goreleaser cannot prompt (no tty on its stdin): + # collect the passphrase up front. Empty is legal — it means an + # unencrypted key. + printf 'cosign key password (empty for an unencrypted key): ' >&2 + read -rs password + echo >&2 + fi + export COSIGN_KEY="$key" + export COSIGN_PASSWORD="$password" + tag_and_push "$tag" + exec goreleaser release --clean + ;; + --ci) + # The workflow half: the tag exists (its push triggered this run, + # and CI checks out exactly that commit), publishing uses the + # workflow's GITHUB_TOKEN, and signing is keyless — the OIDC + # identity replaces any key. + if [ -z "${GITHUB_ACTIONS:-}" ]; then + echo "--ci is the workflow half — locally, 'just do release ' cuts the tag CI releases." >&2 + exit 1 + fi + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "no OIDC token available — the workflow needs 'id-token: write' for keyless signing." >&2 + exit 1 + fi + if [ -z "${GITHUB_TOKEN:-}" ]; then + echo "GITHUB_TOKEN is not set — pass the workflow token to this step's env." >&2 + exit 1 + fi + if ! git describe --exact-match --tags HEAD >/dev/null 2>&1; then + echo "HEAD carries no tag — this lane only runs on a tag push." >&2 + exit 1 + fi + # Keyless is enforced, not defaulted: a key leaking into the CI + # environment must not silently flip the signing mode. + unset COSIGN_KEY + exec goreleaser release --clean + ;; + --dry-run) + echo "dry runs are a local concern: use 'just do release --local --dry-run'." >&2 + exit 2 + ;; + "") + usage + exit 2 + ;; + -*) + echo "unknown option: $1" >&2 + usage + exit 2 + ;; + *) + # The default lane: cut the signed tag and push — the release + # workflow takes it from here. + tag="$1" + shift + if [ $# -gt 0 ]; then + echo "unexpected argument: $1" >&2 + usage + exit 2 + fi + require_version_tag "$tag" + require_clean + tag_and_push "$tag" + echo "tag $tag pushed — the release workflow builds, signs, and publishes from here." + ;; + esac diff --git a/.limen/just/test-go.just b/.limen/just/test-go.just new file mode 100644 index 0000000..e0b6c87 --- /dev/null +++ b/.limen/just/test-go.just @@ -0,0 +1,105 @@ +# DO NOT EDIT MANUALLY. +# This file provides shared tasks common to all projects and managed by limen. +# Project recipes live in the root Justfile, below the shared-baseline import. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: unit + +# `go test` runs funnel through gotestsum: same exit semantics, readable live +# output (its format is gotestsum's own GOTESTSUM_FORMAT knob). -count=1 +# defeats Go's test result cache — a test task must actually test. The shared +# timeout is project-overridable from the root Justfile, the home of all project +# customization: +# export TEST_GO_TIMEOUT := '20m' + +# Unit tests. +unit: (_banner "test go" "unit") + gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" ./... + +# Unit tests under the race detector. The final link is delegated to the +# system linker: the race detector injects runtime/cgo into every package, and +# hardened CGO_CFLAGS (-fstack-protector-strong, -fPIE) make those objects +# reference libc symbols Go's internal linker cannot resolve on Linux +# ("relocation target stderr not defined") — see golang/go#52690, #54313, +# #58619. On macOS with Xcode 15+, ld emits "has malformed LC_DYSYMTAB" +# warnings for race builds; cosmetic, the binaries are correct (golang/go#61229). +race: (_banner "test go" "race") + CGO_ENABLED=1 gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -ldflags=-linkmode=external -race ./... + +# Benchmarks, with allocation stats. -run '^$' deselects unit tests so only +# benchmarks run — `just do test go` already covers the tests themselves. +bench: (_banner "test go" "bench") + go test -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -run '^$' -bench . -benchmem ./... + +# Coverage: per-function summary, an HTML report under build/coverage/, and an +# optional minimum gate — export TEST_GO_COVER_MIN := '80' (integer percent) +# from the root Justfile to enforce a floor; unset or 0 reports without gating. +cover: (_banner "test go" "cover") + #!/usr/bin/env bash + set -euo pipefail + dir=build/coverage + mkdir -p "$dir" + gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -coverprofile="$dir/coverage.out" ./... + go tool cover -func="$dir/coverage.out" + go tool cover -html="$dir/coverage.out" -o "$dir/coverage.html" + echo "HTML report: $dir/coverage.html" + min="${TEST_GO_COVER_MIN:-0}" + if [ "$min" -gt 0 ] 2>/dev/null; then + total=$(go tool cover -func="$dir/coverage.out" | awk '/^total:/ { gsub(/%/, "", $3); print $3 }') + if [ "${total%%.*}" -lt "$min" ]; then + echo "coverage ${total}% is below the ${min}% minimum" >&2 + exit 1 + fi + echo "coverage ${total}% meets the ${min}% minimum" + fi + +# CPU and memory profiles, one pair per package, with pprof top-20 summaries +# printed and PNG call graphs rendered via the pinned `dot` (goccy/go-graphviz: +# real graphviz compiled to WASM, so it lives in aqua like any Go tool). +# Informational: a package whose tests fail is still profiled past, never +# fails the run. Artifacts land under build/profiles/ (raw) and +# build/profiles-docs/ (PNG); analyze interactively with `go tool pprof`. +profile: (_banner "test go" "profile") + #!/usr/bin/env bash + set -euo pipefail + prof=build/profiles + docs=build/profiles-docs + mkdir -p "$prof" "$docs" + # pprof's own -png shells out to dot writing to a pipe, which the WASM dot + # cannot do (it needs -o) — so render from pprof's -dot text instead. The + # pre-create matters: under a sandbox the WASM runtime can overwrite files + # but not always create them. Paths stay cwd-relative for the same reason + # (the runtime preopens only the working directory). + render() { + src=$1; png=$2; shift 2 + go tool pprof -dot -nodecount=20 "$@" "$src" > "$png.dot" 2>/dev/null + : > "$png" + dot -Tpng -o "$png" "$png.dot" + rm -f "$png.dot" + echo " -> $png" + } + for pkg in $(go list ./...); do + name=${pkg##*/} + echo "Profiling $pkg..." + go test -count=1 -o "$prof/$name.test" "$pkg" \ + -cpuprofile "$prof/${name}_cpu.prof" \ + -memprofile "$prof/${name}_mem.prof" || true + if [ -s "$prof/${name}_cpu.prof" ]; then + echo " CPU profile (top 20):" + go tool pprof -top -nodecount=20 "$prof/${name}_cpu.prof" 2>/dev/null || true + render "$prof/${name}_cpu.prof" "$docs/${name}_cpu.png" + fi + if [ -s "$prof/${name}_mem.prof" ]; then + echo " Memory profile — alloc_space (top 20):" + go tool pprof -top -nodecount=20 -alloc_space "$prof/${name}_mem.prof" 2>/dev/null || true + render "$prof/${name}_mem.prof" "$docs/${name}_alloc.png" -alloc_space + fi + done + echo "Profiles written to $prof/, diagrams to $docs/" diff --git a/.limen/just/test.just b/.limen/just/test.just new file mode 100644 index 0000000..a7540a1 --- /dev/null +++ b/.limen/just/test.just @@ -0,0 +1,17 @@ +# DO NOT EDIT MANUALLY. +# 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. + +# 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-all` in +# the root Justfile (the CI workflow's entry point, mirroring lint-all). +default: + @echo 'just test has no default — name a suite (e.g. `just do test go`), or run the project'"'"'s `just test-all`.' >&2 + @exit 1 + +mod go 'test-go.just' diff --git a/.limen/just/tools.just b/.limen/just/tools.just new file mode 100644 index 0000000..e7068c6 --- /dev/null +++ b/.limen/just/tools.just @@ -0,0 +1,80 @@ +# DO NOT EDIT MANUALLY. +# 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. + +set working-directory := '../..' + +# Silence just's per-line command echo; recipes announce themselves via _banner. +set quiet + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +# --- Project tooling via aqua (see book/tooling.md). Each recipe takes the +# owner/repo exactly as it appears in aqua.yaml, e.g. golangci/golangci-lint. --- + +# Add a new tool at its latest version, e.g. `just do tools add junegunn/fzf`. +add pkg: (_banner "tools" "add") + #!/usr/bin/env bash + set -euo pipefail + if grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}([[:space:]]|@|$)" aqua.yaml; then + echo "{{ pkg }} is already in aqua.yaml — use 'just do tools update {{ pkg }}' or 'just do tools set {{ pkg }} '." >&2 + exit 1 + fi + aqua generate -i "{{ pkg }}" # append the latest version to aqua.yaml + aqua update-checksum --prune # record its checksum (and drop stale ones) + aqua install --only-link # install and verify + +# Set an existing tool to an exact version, e.g. `just do tools set golangci/golangci-lint v1.55.3`. +set pkg version: (_banner "tools" "set") + #!/usr/bin/env bash + set -euo pipefail + if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}@" aqua.yaml; then + echo "{{ pkg }} is not pinned in aqua.yaml — add it with 'just 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. + tmp=$(mktemp aqua.yaml.XXXXXX) + trap 'rm -f "$tmp"' EXIT + sed -E "s#(^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }})@[^[:space:]]*#\1@{{ version }}#" aqua.yaml > "$tmp" + mv "$tmp" aqua.yaml + aqua update-checksum --prune # new checksum in, replaced version's out + aqua install --only-link # install and verify + +# 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.) +update command: (_banner "tools" "update") + #!/usr/bin/env bash + set -euo pipefail + aqua update "{{ command }}" # bump aqua.yaml to the latest release + aqua update-checksum --prune # new checksum in, replaced version's out + aqua install --only-link # install and verify + +# Remove a tool entirely, e.g. `just do tools remove junegunn/fzf`. +remove pkg: (_banner "tools" "remove") + #!/usr/bin/env bash + set -euo pipefail + if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*{{ pkg }}([[:space:]]|@|$)" aqua.yaml; then + echo "{{ pkg }} is not in aqua.yaml — nothing to remove." >&2 + exit 1 + fi + # See `set` for why mktemp gets an explicit sibling-path template. + tmp=$(mktemp aqua.yaml.XXXXXX) + trap 'rm -f "$tmp"' EXIT + awk -v pkg="{{ pkg }}" ' + function isCont(l) { return (l ~ /^[[:blank:]]+[^[:blank:]#-]/) } + { + if (skip) { if (isCont($0)) next; skip = 0 } + if ($0 ~ "^[[:blank:]]*-[[:blank:]]*name:[[:blank:]]*" pkg "([[:blank:]]|@|$)") { skip = 1; next } + print + } + ' aqua.yaml > "$tmp" + mv "$tmp" aqua.yaml + aqua remove "{{ pkg }}" || true # uninstall the binary (no-op for go_install tools) + aqua update-checksum --prune # drop the now-unused checksum diff --git a/.limen/lychee.toml b/.limen/lychee.toml new file mode 100644 index 0000000..27ec773 --- /dev/null +++ b/.limen/lychee.toml @@ -0,0 +1,22 @@ +# DO NOT EDIT MANUALLY. +# Canonical lychee (link checker) configuration, identical in every repository. +# A repository can add its own exclusions in a root .lychee.toml — the `lint links` +# recipe passes both files and lychee merges them, concatenating the exclude +# lists — so this baseline only carries exclusions that apply everywhere. + +# 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. +max_retries = 6 +retry_wait_time = 5 + +exclude = [ + # fsf.org: verbatim (A)GPL license texts link to it, and its server (TLS 1.2 + # with DHE-only key exchange) cannot complete a handshake with lychee's + # rustls, which implements neither. The links themselves are fine. + # gnu.org: rate-limits aggressively, so the license-text links to it fail + # intermittently. + 'https?://(www\.)?(fsf|gnu)\.org(/|$)', +] diff --git a/Justfile b/Justfile new file mode 100644 index 0000000..e9642f7 --- /dev/null +++ b/Justfile @@ -0,0 +1,7 @@ +# This file is the project's own — add recipes below. Keep the import: it +# mounts every shared limen task under `just do ...`. +import '.limen/just/main.just' + +lint: do::lint::default +fix: do::fix::default +test: diff --git a/README.md b/README.md index e3b7b88..d7c7748 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Mumbrew is very simple, easy to use auto-updater for brew. The service by default runs at 2AM, and will upgrade everything that is not pinned. -This is similar in principle to https://github.com/Homebrew/homebrew-autoupdate +This is similar in principle to https://github.com/DomT4/homebrew-autoupdate albeit much simpler and also quite dumber. ## Install @@ -13,6 +13,7 @@ Install with brew: ```bash brew install farcloser/brews/mumbrew +brew trust --formula farcloser/brews/mumbrew brew services start mumbrew ``` @@ -35,4 +36,6 @@ cat $(brew --prefix)/var/log/farcloser.mumbrew.err.log ## Development -You need shellcheck (`brew install shellcheck`). +``` +just lint-all +``` diff --git a/aqua-checksums.json b/aqua-checksums.json new file mode 100644 index 0000000..ce3dbb9 --- /dev/null +++ b/aqua-checksums.json @@ -0,0 +1,339 @@ +{ + "checksums": [ + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-aarch64-apple-darwin.tar.gz", + "checksum": "0225E3899B1B555BC3F4122D0402BA931A6EE697D188C68E4D971468064C78F4", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-aarch64-unknown-linux-musl.tar.gz", + "checksum": "B0EE814C9656427408E339893541E30D9027828686839499B2A2A34DD61AD173", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-x86_64-apple-darwin.tar.gz", + "checksum": "74D7D0AFC195DF14D8E34636FB9B6A9C7D3D443622478DBE86140FFE41AD9192", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-x86_64-pc-windows-msvc.zip", + "checksum": "8106E6C036AEA52DD1D5109BDF9357C37BA87A0E0EF1013D7327103942550C79", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/casey/just/1.55.1/just-1.55.1-x86_64-unknown-linux-musl.tar.gz", + "checksum": "B0EF600F0DF20D5AE91AE931627C499FC52B477FFE5F5EA7B7B3EC616B16C778", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_linux_amd64.tar.gz", + "checksum": "83D5C2CCAD5498F58BF6368ACB1AB32588CF43AB3A4B1C301BF36328B1C8BD60", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_linux_arm64.tar.gz", + "checksum": "06F86EC7103D41993B76CD78072F43595C34AAA56506D971D9860E67140BF909", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_macOS_amd64.zip", + "checksum": "4BD449DF9AD639391BC62B8032546F0FE9EDCD8526E06682A4F88ABD8C5D163C", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_macOS_arm64.zip", + "checksum": "F23A0C37D963AACC3BED703CCBD59B41C5CA22101FAB7F00EB2B7CAD23ABA463", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_windows_amd64.zip", + "checksum": "C2D6ACC935CD2F00E2144D7E036D5CD82E6B6BD5594E8C75AA75EF2A4ED6AAC3", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/cli/cli/v2.96.0/gh_2.96.0_windows_arm64.zip", + "checksum": "C517E0B32C98A4BA90AC95AF8D12CC3AC55781AB4AB72F9A91CE3DE0541D2B09", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_darwin_amd64.tar.gz", + "checksum": "47B07A3FC0A4B9316451B957F10A9D967775B89F475BBCD0AF0793F842B6ACC9", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_darwin_arm64.tar.gz", + "checksum": "F9ACA372C2034A3EFBB3539836AFA884122F70000A1948DBA225696B30F3C3C7", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_linux_amd64.tar.gz", + "checksum": "2ACBB988741AA50337B37E4373345713B3C766675F3347A08CBF1B54EDB235E8", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_linux_arm64.tar.gz", + "checksum": "5B2BBB0DF165717381B7D5E79A49427BF8B4AE9BF7C846D7B50435E27159D88A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_windows_amd64.tar.gz", + "checksum": "39135AC60A939EA22120AA6E32007172960AEF4A3A2D6F44B30A1292EB99237D", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/farcloser/limen/v0.0.2/limen_0.0.2_windows_arm64.tar.gz", + "checksum": "CB7A08AFEC932A8B4AEE3884391EBE34257440AEEBDDEED3B4E12C1001404B35", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-amd64.tar.gz", + "checksum": "F6F06D94B6241521C53D15450C5209B028270BF966F842AFB11C030C79F5BC16", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-darwin-arm64.tar.gz", + "checksum": "A9C54498731B3128F79E090BE6110F3E5FFFCCC617B08142ED244D4126C73F29", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-amd64.tar.gz", + "checksum": "8DF580D2670FED8FA984AAC0507099AF8DF275E665215F5C7A2AE3943893A553", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-linux-arm64.tar.gz", + "checksum": "44CD40A8C76C86755375ADFEEA52CFD3533CB43D7BD647771E0AE065E166DF3A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-amd64.zip", + "checksum": "BD42E3EBC8CB4ECECB86941983BAAF1DC221BBB04D838E94CE63B49CC91E02BB", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/golangci/golangci-lint/v2.12.2/golangci-lint-2.12.2-windows-arm64.zip", + "checksum": "947B9A5BF762D465710B376C156F0184ABB2168378B0826AF1899E0EE7183742", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Darwin_arm64.tar.gz", + "checksum": "4B417ECB94339D57E4C122ECC948C1A00FE328B5853266DE9806E652A92858FA", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Darwin_x86_64.tar.gz", + "checksum": "060E943BCB8583C456810EB1FF4721B4F46C4A0C1A4432449D5DC3BBFE29A22B", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Linux_arm64.tar.gz", + "checksum": "5B2689C963B177271330C5CE8CA7396751107E5A826BE46F03D2CB9B6F0C7784", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Linux_x86_64.tar.gz", + "checksum": "1F300D9257B232BB3B541D7FB1B0E6B3C121BCBAB381C86CD38CB8722BE8A566", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Windows_arm64.tar.gz", + "checksum": "C1E64D1C72CA8986BC5B8C8EDD4EC89F0627804E7E08F8DE9F4B484CB5CAD897", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/google/yamlfmt/v0.21.0/yamlfmt_0.21.0_Windows_x86_64.tar.gz", + "checksum": "07F80CE5D741EB4B0A9380AC78A19C7CB5BD44E2A9A47A5A04839E3BA54DD463", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Darwin_all.tar.gz", + "checksum": "6A102E4D588FD3553A9AC7321CA6023A9F205843F4861CA35BD337DFA9E72ECE", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Linux_arm64.tar.gz", + "checksum": "0102D974373FCDEB77042D1F5897CAFFA193BE36620FDC6C1DA43A01EF8E10D3", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Linux_x86_64.tar.gz", + "checksum": "EAAE05B5EBA07533BD0F06846B68C808399504784DF00C62EB219541FC04E5E2", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Windows_arm64.zip", + "checksum": "1183C81863044CE9BAA89C1393C258949390B8DF683DF7CA959E9C718D7723C9", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/goreleaser/goreleaser/v2.16.0/goreleaser_Windows_x86_64.zip", + "checksum": "6FE5EDA11F0BCAC8069AFF3EF3DCB0B11816C9E95F89773595564697A5278BC9", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_darwin_amd64.tar.gz", + "checksum": "99529350F4C7B780B1EFC543CA0D9721B09F0A4228F0EFA9281261F58FEFA05A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz", + "checksum": "509CB27AEF747F48FAF9BCE424F59DCF79572C905204B990EE935BBFCC7FA0E9", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz", + "checksum": "11CCDDEAF708EF228889F9FE2F68291A75B27013DDFC3B18156E094F5F40E8EE", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_arm64.tar.gz", + "checksum": "7644A4C5CD1BB978D56245AEAB25A586AC5AC62ADEBED20A399548867C13499D", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz", + "checksum": "FD5A6DC69E46A0970593E70D85A7E75F16714E9C61D6D72CCC324EB82DF5BB8A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_windows_arm64.tar.gz", + "checksum": "72A59200F83B3204CD59FD417E384DB0543C5511D7A9E38957E74A4035950943", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-linux-amd64", + "checksum": "B1C22172DD303F3BE49E935AA56AA48A8B7A46E0BC838B4997D3BB451495870F", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-linux-arm64", + "checksum": "8B85C817833814DDCA00A144C33705546355AFCCF0CF39B188F3CDB48B852309", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-macos-amd64", + "checksum": "E94B266E3C26690550006ABE63152B782280F4E14374ACCDF04CBDE844F00BC0", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-macos-arm64", + "checksum": "2D75340BA57A4B4B4C8708A21C2DC8E958A48AAA8BBA13B27F77F6E4C0ECA07E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/jqlang/jq/jq-1.8.2/jq-windows-amd64.exe", + "checksum": "A6FC67FEDAF9128A3309A1E2EBB8B986AECCF70122EE46D2CB4849E423F0C627", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz", + "checksum": "56AFFDD8DE5527894DCA6DC3D7E0A99A873B0F004D7AABC30AE407D3F48B0A79", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.xz", + "checksum": "3C89DB4EDCAB7CF1C27BFF178882E0F6F27F7AFDF54E859FA041FCA10FEBE4C6", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz", + "checksum": "12B331C1D2DB6B9EB13CFCA64306B1B157A86EB69DB83023E261EAA7E7C14588", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz", + "checksum": "8C3BE12B05D5C177A04C29E3C78CE89AC86F1595681CAB149B65B97C4E227198", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/koalaman/shellcheck/v0.11.0/shellcheck-v0.11.0.zip", + "checksum": "8A4E35AB0B331C85D73567B12F2A444DF187F483E5079CEFFA6BDA1FAA2E740E", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-aarch64-apple-darwin.tar.gz", + "checksum": "C9D3740EA2D891854D37116C9FBA840F37B6E7C89D330E7DB84AC333631C4977", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-aarch64-unknown-linux-musl.tar.gz", + "checksum": "5D0B0E3AEAB240F41920C633A6EAF97599BE6EEDDA034B36E858EDE7DBA5E535", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-x86_64-apple-darwin.tar.gz", + "checksum": "887503A9CFF667D322B8D0892B40BF49976EB9507AF8483220A3706CDAD55978", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-x86_64-pc-windows-msvc.zip", + "checksum": "32975D1493EE1A975D6BB41E4FB56FE419CB442DED628BB772BA2E614ACFACAD", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/lycheeverse/lychee/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz", + "checksum": "73657A111819A30C47C08352896796F23D64E4EB2B3ED39B6D32149241566FC5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-darwin-amd64", + "checksum": "14D2678DFBFDE18798151E86FBD91EBDADBB7424B18412A42A155DD8A2DF4C7A", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-darwin-arm64", + "checksum": "94B42A9E697BE95675F6160AB031A9A5F1EC1E646D6F648D7B2F5CD59ECECBC5", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-linux-amd64", + "checksum": "AE1ECD212663F3693AD9EDF8B1A183900C9A52D3155BA6E354237F9A0F6463FC", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-linux-arm64", + "checksum": "2EC865872E331C32FD12B08DAE15332D3F92C0AA029219589684A4903CA85D11", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/sigstore/cosign/v3.1.1/cosign-windows-amd64.exe", + "checksum": "9D2C026E667BFD979FA7BA1CAB8C4B24D2E73F336EC2D57F7FC72C7E73E5B4B6", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.darwin-amd64.tar.gz", + "checksum": "05DC9B5F9997744520AAEBB3D5DEAA7C755371AEBBFB7F97C2511A9F3367538D", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.darwin-arm64.tar.gz", + "checksum": "B62AD2B6D7D2464F12A5BCAD7FF47F19D08325773B5EFD21610E445A05A9BF53", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.linux-amd64.tar.gz", + "checksum": "1153D3D50E0AC764B447ADFE05C2BCF08E889D42A02E0FE0259BD47F6733AD7F", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.linux-arm64.tar.gz", + "checksum": "EF758AE7C6CF9267C9C0EF080B8965F453D89AB2D25D9EB22DE4405925238768", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.windows-amd64.zip", + "checksum": "3CA8FB4630B07C419CBDD51F754E31363CFCFB83B3A5354D9E895C90BE2CC345", + "algorithm": "sha256" + }, + { + "id": "http/golang.org/dl/go1.26.4.windows-arm64.zip", + "checksum": "62247F56FB7D7B827D237152C4E3FCD69A24D0FA9430DC73DBDA7593AE82BC8D", + "algorithm": "sha256" + }, + { + "id": "registries/github_content/github.com/aquaproj/aqua-registry/v4.530.0/registry.yaml", + "checksum": "43CC45E1A514E4375608A8614CC44987EC04A5865980404D3DADBC245CF9006D", + "algorithm": "sha256" + } + ] +} diff --git a/aqua-policy.yaml b/aqua-policy.yaml new file mode 100644 index 0000000..2e4e6b0 --- /dev/null +++ b/aqua-policy.yaml @@ -0,0 +1,13 @@ +# DO NOT EDIT MANUALLY. +# This file is common to all projects and managed by limen. +# Global configuration changes proposals can be discussed on https://github.com/farcloser/limen + +registries: + - type: standard + ref: semver(">= 4.0.0") + - name: local + type: local + path: .limen/aqua-registry.yaml +packages: + - registry: standard + - registry: local diff --git a/aqua.yaml b/aqua.yaml new file mode 100644 index 0000000..b6f5071 --- /dev/null +++ b/aqua.yaml @@ -0,0 +1,49 @@ +# aqua — Declarative CLI Version Manager — https://aquaproj.github.io/ +checksum: + enabled: true + require_checksum: true + supported_envs: + - darwin/amd64 + - darwin/arm64 + - linux/amd64 + - linux/arm64 + - windows/amd64 + - windows/arm64 + +registries: + - type: standard + ref: v4.530.0 # renovate: depName=aquaproj/aqua-registry + - name: local + type: local + path: .limen/aqua-registry.yaml + +packages: + # --- go install tools (local registry, GOSUMDB-verified) --- + - name: github.com/google/go-licenses/v2@v2.0.1 + registry: local + - name: github.com/vbatts/git-validation@v1.2.2 + registry: local + - name: golang.org/x/vuln/cmd/govulncheck@v1.5.0 + registry: local + - name: golang.org/x/tools/cmd/deadcode@v0.47.0 + registry: local + # Pseudo-version: the nested cmd/dot module carries no tags upstream. + - name: github.com/goccy/go-graphviz/cmd/dot@v0.0.0-20251129032125-76e04975df88 + registry: local + - name: github.com/farcloser/godolint/cmd/godolint@v0.1.0 + registry: local + # --- farcloser tools (local registry; standard once registered upstream) --- + - name: farcloser/limen@v0.0.2 # renovate: depName=farcloser/limen + registry: local + # --- toolchain + binary-release tools (standard registry, aqua-verified) --- + - name: golang/go@go1.26.4 + - name: casey/just@1.55.1 + - name: koalaman/shellcheck@v0.11.0 + - name: golangci/golangci-lint@v2.12.2 + - name: google/yamlfmt@v0.21.0 + - name: lycheeverse/lychee@lychee-v0.24.2 + - name: goreleaser/goreleaser@v2.16.0 + - name: sigstore/cosign@v3.1.1 + - name: gotestyourself/gotestsum@v1.13.0 + - name: jqlang/jq@jq-1.8.2 + - name: cli/cli@v2.96.0 diff --git a/lib/lint.sh b/lib/lint.sh deleted file mode 100644 index 7fa13ca..0000000 --- a/lib/lint.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -o errtrace -o functrace -o nounset -o pipefail -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ -# (c) 2024 Farcloser -# Distributed under the terms of the MIT license -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ - -lint::dockerfile(){ - log::info " > %s\n" "$@" - if ! hadolint "$@"; then - log::error "Failed linting Dockerfile" - exit 1 - fi -} - -lint::shell(){ - log::info " > Shellchecking %s\n" "$@" - shellcheck -a -x "$@" || { - log::error "Failed shellchecking shell script" - return 1 - } -} diff --git a/lib/log.sh b/lib/log.sh deleted file mode 100644 index a98115d..0000000 --- a/lib/log.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -o errtrace -o functrace -o nounset -o pipefail -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ -# (c) 2024 Farcloser -# Distributed under the terms of the MIT license -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ - -# shellcheck disable=SC2034 -readonly LOG_COLOR_BLACK=0 -readonly LOG_COLOR_RED=1 -readonly LOG_COLOR_GREEN=2 -readonly LOG_COLOR_YELLOW=3 -# shellcheck disable=SC2034 -readonly LOG_COLOR_BLUE=4 -# shellcheck disable=SC2034 -readonly LOG_COLOR_MAGENTA=5 -# shellcheck disable=SC2034 -readonly LOG_COLOR_CYAN=6 -readonly LOG_COLOR_WHITE=7 -# shellcheck disable=SC2034 -readonly LOG_COLOR_DEFAULT=9 - -# shellcheck disable=SC2034 -readonly LOG_STYLE_DEBUG=( setaf "$LOG_COLOR_WHITE" ) -# shellcheck disable=SC2034 -readonly LOG_STYLE_INFO=( setaf "$LOG_COLOR_GREEN" ) -# shellcheck disable=SC2034 -readonly LOG_STYLE_WARNING=( setaf "$LOG_COLOR_YELLOW" ) -# shellcheck disable=SC2034 -readonly LOG_STYLE_ERROR=( setaf "$LOG_COLOR_RED" ) - -readonly LOG_LEVEL_DEBUG=0 -# shellcheck disable=SC2034 -readonly LOG_LEVEL_INFO=1 -# shellcheck disable=SC2034 -readonly LOG_LEVEL_WARNING=2 -# shellcheck disable=SC2034 -readonly LOG_LEVEL_ERROR=3 -export _PRIVATE_LOG_LEVEL=2 - -_log::log(){ - local level - local style - local numeric_level - local message="$2" - - level="$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]')" - numeric_level="$(printf "LOG_LEVEL_%s" "$level")" - style="LOG_STYLE_${level}[@]" - - [ "${!numeric_level}" -ge "$_PRIVATE_LOG_LEVEL" ] || return 0 - - [ ! "$TERM" ] || [ ! -t 2 ] || >&2 tput "${!style:-}" 2>/dev/null || true - >&2 printf "[%s] %s: %s\n" "$(date 2>/dev/null || true)" "$(printf "%s" "$level" | tr '[:lower:]' '[:upper:]')" "$message" - [ ! "$TERM" ] || [ ! -t 2 ] || >&2 tput op 2>/dev/null || true -} - -log::init(){ - local _ll - _ll="$(printf "LOG_LEVEL_%s" "${LOG_LEVEL:-warning}" | tr '[:lower:]' '[:upper:]')" - _PRIVATE_LOG_LEVEL="${!_ll:-1}" - [ "$_PRIVATE_LOG_LEVEL" != "$LOG_LEVEL_DEBUG" ] || { - log::warning "Your log level is set to 'debug'. This is NOT recommended for production use, and MAY leak sensitive information to stderr." - } -} - -log::debug(){ - _log::log debug "$@" -} - -log::info(){ - _log::log info "$@" -} - -log::warning(){ - _log::log warning "$@" -} - -log::error(){ - _log::log error "$@" -} - -log::init diff --git a/lib/utils.sh b/lib/utils.sh deleted file mode 100644 index b8cb970..0000000 --- a/lib/utils.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -o errtrace -o functrace -o nounset -o pipefail -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ -# (c) 2024 Farcloser -# Distributed under the terms of the MIT license -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ - -portable::mktemp(){ - local prefix="${1:-}" - local directory=${2:-true} - local args=() - - [ "$directory" == true ] && args+=(-dq) || args+=(-q) - - mktemp "${args[@]}" "${TMPDIR:-/tmp}/$prefix.XXXXXX" 2>/dev/null || mktemp "${args[@]}" -} - - -# Helpers -host::require(){ - local binary="$1" - - log::debug "Checking presence of $binary" - command -v "$binary" >/dev/null || { - log::error "You need $binary for this script to work, and it cannot be found in your path" - return 1 - } -} - -host::install(){ - local binary - - for binary in "$@"; do - log::debug "sudo install -D -m 755 $binary /usr/local/bin/$(basename "$binary")" - sudo install -D -m 755 "$binary" /usr/local/bin/"$(basename "$binary")" - done -} - -fs::ensuredir(){ - local pth="$1" - mkdir -p "$pth" 2>/dev/null - [ -d "$pth" ] || { - log::error "Failed to create $pth. Check your permissions." - return 1 - } -} - -curl::get(){ - local url="$1" - log::info "Downloading $url\n" - # 2024-04 Github still does not offer tls 1.3 - curl --proto '=https' --tlsv1.2 -sSfL --compressed "$url" -} diff --git a/logo.jpg b/logo.jpg new file mode 100644 index 0000000..abbce04 Binary files /dev/null and b/logo.jpg differ diff --git a/mumbrew b/mumbrew index d5814af..f7d3fe1 100755 --- a/mumbrew +++ b/mumbrew @@ -17,7 +17,6 @@ brew_exec="$(command -v brew || true)" brew_exec="${brew_exec:-$(pwd)/bin/brew}" notifier="$($brew_exec --repository)/bin/terminal-notifier" -arguments=( "-appIcon" "https://avatars.githubusercontent.com/u/142760738?s=400" ) "$brew_exec" update >/dev/null 2>&1 outdated=$($brew_exec outdated --quiet | sed -e 's/.*\///') @@ -26,25 +25,25 @@ pinned=$($brew_exec list --pinned) # Remove pinned formulae from the list of outdated formulae outdated=$(comm -1 -3 <(echo "$pinned") <(echo "$outdated")) -if [ ! "$outdated" ] ; then - echo "Everything ok, no updates available" - #if [ -e "$notifier" ]; then - # $notifier $arguments \ - # -title "Everything ok" \ - # -message "No updates available." - #fi +if [ -z "$outdated" ] ; then + echo "Mumbrew: everything up to date" + if [ -e "$notifier" ]; then + $notifier \ + -title "Mumbrew" \ + -message "Everything up to date" + fi exit fi if [ -e "$notifier" ]; then - $notifier "${arguments[@]}" -title "Updating brews" -message "$outdated" + $notifier -title "Mumbrew: updating" -message "$outdated" fi $brew_exec upgrade $brew_exec cleanup if [ -e "$notifier" ]; then - $notifier "${arguments[@]}" -title "Done updating brews!" -message "" + $notifier -title "Mumbrew" -message "Done updating!" fi echo "★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★" diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..a4de8b1 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,32 @@ +{ + $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 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. + gitIgnoredAuthors: ["41898282+github-actions[bot]@users.noreply.github.com"], + customManagers: [ + // The aqua preset above only manages the lowercase `aqua_version:` input of + // the official installer action, and aqua.yaml itself. The AQUA_VERSION env + // of the hand-rolled installer (.github/actions/setup-aqua) needs its own + // manager, keyed on the standard renovate comment above the pin. + { + customType: "regex", + managerFilePatterns: ["/^\\.github/.+\\.ya?ml$/"], + matchStrings: [ + "# renovate: depName=(?[^\\s]+)\\n\\s*AQUA_VERSION: (?[^\\s]+)", + ], + datasourceTemplate: "github-releases", + }, + ], +} diff --git a/test.sh b/test.sh deleted file mode 100755 index a703ad8..0000000 --- a/test.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -set -o errexit -o errtrace -o functrace -o nounset -o pipefail -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ -# (c) 2024 Farcloser -# Distributed under the terms of the MIT license -# ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ ★ - -root="$(cd "$(dirname "${BASH_SOURCE[0]:-$PWD}")" 2>/dev/null 1>&2 && pwd)" -readonly root - -. "$root"/lib/log.sh -. "$root"/lib/utils.sh -. "$root"/lib/lint.sh - -# Linting -log::info "Linting" -lint::shell mumbrew ./*.sh ./lib/*.sh -log::info "Linting successful" - -./mumbrew