From 423ef2d2bad31b461394dedb55a4e98ed4002a4d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:38:27 +0000 Subject: [PATCH 1/2] chore(deps): update dependency farcloser/limen to v0.26.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .hermetic/hermetic.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.hermetic/hermetic.yaml b/.hermetic/hermetic.yaml index c35dff5..42a6cec 100644 --- a/.hermetic/hermetic.yaml +++ b/.hermetic/hermetic.yaml @@ -20,7 +20,7 @@ registries: packages: # The limen this repository runs, and with it every canonical file it # writes: the one canonical pin kept here, where Renovate bumps it. - - name: farcloser/limen@v0.15.0 # renovate: depName=farcloser/limen + - name: farcloser/limen@v0.26.0 # renovate: depName=farcloser/limen registry: local # The Go toolchain is the repository's own pin, seeded from limen's own # manifest once and Renovate's here afterwards: a standard-library fix From 0581b727782805efd0415019dd8e9298468d4d3f Mon Sep 17 00:00:00 2001 From: "limen-ci-farcloser[bot]" <300983632+limen-ci-farcloser[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 08:39:10 +0000 Subject: [PATCH 2/2] chore: refresh checksums and converge the limen baseline Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .gitattributes | 5 + .github/actions/cache/action.yaml | 43 +++- .github/actions/setup-hermetic/action.yaml | 70 +++--- .../actions/windows-cache-image/action.yaml | 13 ++ .github/release.yml | 22 +- .github/workflows/caches.yaml | 53 +---- .github/workflows/ci.yaml | 2 +- .github/workflows/limen-verify.yaml | 175 --------------- .github/workflows/security.yaml | 73 +++--- .github/workflows/update-aqua-checksum.yaml | 209 +----------------- .hermetic/aqua-checksums.json | 194 +++++++--------- .limen/hermetic-registry.yaml | 88 +++++--- .limen/hermetic.yaml | 32 +-- .limen/just/apple.just | 137 ++++++++++++ .limen/just/build-go.just | 19 +- .limen/just/build-rust.just | 74 +++++++ .limen/just/build.just | 4 + .limen/just/do.just | 5 + .limen/just/fix-rust.just | 19 +- .limen/just/fix.just | 4 +- .limen/just/lib.just | 78 ++++++- .limen/just/lint-homebrew.just | 36 ++- .limen/just/lint-rust.just | 14 +- .limen/just/lint.just | 4 +- .limen/just/perf-go.just | 8 +- .limen/just/release.just | 152 +++++-------- .limen/just/test-go.just | 36 ++- .limen/just/test-rust.just | 25 +++ .limen/just/test.just | 4 + .limen/just/tools.just | 77 ++++--- .limen/lint-go.yaml | 5 +- .limen/renovate.json | 45 +++- .limen/tools/go.mod | 16 ++ .limen/tools/go.sum | 36 +++ AGENTS.md | 72 ++++-- 35 files changed, 998 insertions(+), 851 deletions(-) delete mode 100644 .github/workflows/limen-verify.yaml create mode 100644 .limen/just/apple.just create mode 100644 .limen/just/build-rust.just create mode 100644 .limen/just/test-rust.just diff --git a/.gitattributes b/.gitattributes index 3681327..171bfcc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,3 +15,8 @@ # line-ending rule; the line above still applies to them. *.patch -whitespace *.diff -whitespace + +# Third-party licence texts beside LICENSE (LICENSE-graphviz, LICENSE-expat) +# are kept byte for byte as their projects publish them, trailing whitespace +# included: a stripped copy is no longer the text the pinned archive carries. +LICENSE-* -whitespace diff --git a/.github/actions/cache/action.yaml b/.github/actions/cache/action.yaml index 62e670d..20e0c08 100644 --- a/.github/actions/cache/action.yaml +++ b/.github/actions/cache/action.yaml @@ -1,6 +1,6 @@ name: Job caches description: > - The caches a canonical job keeps: hermetic's package store and, with go, the Go build and module caches, plus the linter's with linter-cache. Called three times per job, each mode once: attach (before setup-hermetic), restore (right after setup-hermetic, before the job runs a tool) and save (after the job's last command). On Windows they live in one disk image (windows-cache-image): attach restores and attaches it, since hermetic's root is inside it, restore does nothing, and save detaches and saves it. On Linux and macOS attach does nothing, restore restores the store and the Go caches as files, and save saves them. Every save is on the default branch only, read from the event (a scheduled run is on it by definition), never assumed by name, and only on a miss: a cache a pull request run saves is scoped to that pull request and restorable by nothing else, and a key is never overwritten (the cache budget, caches.yaml). The Go caches and the image accumulate, so their keys carry the UTC day and the default branch refreshes them at most once a day; the store is fully determined by its pins. What save needs, attach and restore leave in the job's environment, so save cannot use another key or path list, and a mode called twice in one job fails. + The caches a canonical job keeps: hermetic's package store, with go the Go build and module caches, plus the linter's with linter-cache, and in a repository that pins a Rust toolchain (rust-toolchain.toml) what the Rust lanes install under LIMEN_BUILD: rustup with the toolchain, cargo's caches, zig's. Called three times per job, each mode once: attach (before setup-hermetic), restore (right after setup-hermetic, before the job runs a tool) and save (after the job's last command). On Windows they live in one disk image (windows-cache-image): attach restores and attaches it, since hermetic's root is inside it, restore does nothing, and save detaches and saves it. On Linux and macOS attach does nothing, restore restores the store and the Go caches as files, and save saves them. Every save is on the default branch only, read from the event (a scheduled run is on it by definition), never assumed by name, and only on a miss: a cache a pull request run saves is scoped to that pull request and restorable by nothing else, and a key is never overwritten (the cache budget, caches.yaml). The Go caches and the image accumulate, so their keys carry the UTC day and the default branch refreshes them at most once a day; the store is fully determined by its pins. What save needs, attach and restore leave in the job's environment, so save cannot use another key or path list, and a mode called twice in one job fails. inputs: mode: description: attach, restore or save. @@ -37,7 +37,7 @@ runs: with: mode: attach prefix: win-image-${{ runner.os }}-${{ runner.arch }}- - hash: ${{ hashFiles('.hermetic/hermetic.yaml', '.hermetic/aqua-checksums.json', '.limen/hermetic.yaml', '.limen/hermetic-registry.yaml', '**/go.sum') }} + hash: ${{ hashFiles('.hermetic/hermetic.yaml', '.hermetic/aqua-checksums.json', '.limen/hermetic.yaml', '.limen/hermetic-registry.yaml', '**/go.sum', 'rust-toolchain.toml', '**/Cargo.lock') }} - if: inputs.mode == 'attach' && runner.os == 'Windows' shell: bash @@ -102,6 +102,31 @@ runs: ${{ steps.paths.outputs.prefix }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.paths.outputs.version }}- ${{ steps.paths.outputs.prefix }}-${{ runner.os }}-${{ runner.arch }}- + # The Rust toolchain and its caches, in a repository that pins one: what + # rust_env installs under LIMEN_BUILD (book/rust.md), rustup with the + # toolchain and its targets, cargo's registry and git caches, zig's and + # cargo-zigbuild's. No tool to run for the paths: LIMEN_BUILD is + # setup-hermetic's export. Keyed on the toolchain file and every + # Cargo.lock, plus the day: cargo's caches accumulate like Go's. + - id: rust-day + if: inputs.mode == 'restore' && runner.os != 'Windows' && hashFiles('rust-toolchain.toml') != '' + shell: bash + run: echo "day=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" + + - id: rust + if: inputs.mode == 'restore' && runner.os != 'Windows' && hashFiles('rust-toolchain.toml') != '' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ env.LIMEN_BUILD }}/rustup + ${{ env.LIMEN_BUILD }}/cargo + ${{ env.LIMEN_BUILD }}/zig + key: rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}-${{ steps.rust-day.outputs.day }} + restore-keys: | + rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('**/Cargo.lock') }}- + rust-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('rust-toolchain.toml') }}- + rust-${{ runner.os }}-${{ runner.arch }}- + - if: inputs.mode == 'restore' && runner.os != 'Windows' shell: bash env: @@ -112,6 +137,8 @@ runs: BUILD: ${{ steps.paths.outputs.build }} MOD: ${{ steps.paths.outputs.mod }} LINT: ${{ steps.paths.outputs.lint }} + RUST_KEY: ${{ steps.rust.outputs.cache-primary-key }} + RUST_HIT: ${{ steps.rust.outputs.cache-hit }} run: | { echo "LIMEN_CACHE_AQUA_KEY=$AQUA_KEY" @@ -123,6 +150,8 @@ runs: if [ -n "$MOD" ]; then echo "$MOD"; fi if [ -n "$LINT" ]; then echo "$LINT"; fi echo "LIMEN_CACHE_GO_PATHS_END" + echo "LIMEN_CACHE_RUST_KEY=$RUST_KEY" + echo "LIMEN_CACHE_RUST_HIT=$RUST_HIT" } >> "$GITHUB_ENV" # --- save: the default branch only, on a miss --- @@ -138,6 +167,16 @@ runs: path: ${{ env.LIMEN_CACHE_GO_PATHS }} key: ${{ env.LIMEN_CACHE_GO_KEY }} + # The same three paths restore listed: LIMEN_BUILD is the same export. + - if: inputs.mode == 'save' && runner.os != 'Windows' && (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && env.LIMEN_CACHE_RUST_KEY != '' && env.LIMEN_CACHE_RUST_HIT != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + ${{ env.LIMEN_BUILD }}/rustup + ${{ env.LIMEN_BUILD }}/cargo + ${{ env.LIMEN_BUILD }}/zig + key: ${{ env.LIMEN_CACHE_RUST_KEY }} + - if: inputs.mode == 'save' && runner.os == 'Windows' && (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && env.LIMEN_CACHE_IMAGE_KEY != '' && env.LIMEN_CACHE_IMAGE_HIT != 'true' uses: ./.github/actions/windows-cache-image with: diff --git a/.github/actions/setup-hermetic/action.yaml b/.github/actions/setup-hermetic/action.yaml index d5e39fc..f60cbfc 100644 --- a/.github/actions/setup-hermetic/action.yaml +++ b/.github/actions/setup-hermetic/action.yaml @@ -19,11 +19,11 @@ runs: # version. One digest covers every platform: the asset is verified # against checksums.txt, and checksums.txt against this. # renovate: depName=forkcloser/hermetic - HERMETIC_VERSION: v0.2.2 - HERMETIC_CHECKSUMS_SHA256: da752591bf17d7fc5310c97b3bc47764fd876be387f099bc812c94790aee592a - # The job's own token, for `gh attestation verify` only, here and not - # exported: a read of GitHub's attestations API, before any - # branch-declared binary runs. + HERMETIC_VERSION: v0.2.3 + HERMETIC_CHECKSUMS_SHA256: c7eb09712dc3d756bead96ae1286b53de275134d0012601ef37f57ec793f9431 + # The job's own token, for gh alone (the release download and + # `gh attestation verify`), here and not exported: reads of GitHub's + # API, before any branch-declared binary runs. GH_TOKEN: ${{ github.token }} run: | # Pin the root explicitly: hermetic's own default differs per @@ -71,32 +71,46 @@ runs: echo "hermetic ${HERMETIC_VERSION} already in the root (the cache image)" else tmp="$(mktemp -d)" - base="https://github.com/forkcloser/hermetic/releases/download/${HERMETIC_VERSION}" - # Retries for transient runner failures. Windows curl (schannel) - # refuses a handshake when the CA's revocation server is unreachable - # (exit 35, seen repeatedly): best-effort revocation still rejects a - # known-revoked certificate, and the checksums below are the real - # guard. schannel-only flag, so windows-only. - revoke=() - [ "$RUNNER_OS" != Windows ] || revoke=(--ssl-revoke-best-effort) - curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors "${revoke[@]}" \ - -o "${tmp}/checksums.txt" "${base}/checksums.txt" + # Through the releases API with the job's token, by gh: the release + # page's download URL is an anonymous fetch, which GitHub throttles + # per runner IP under load (a 504 on every lane of a run, through + # curl's own retries), and a token sent there is routed, not checked. + # gh retries nothing itself; --clobber so a retry starts over. A + # pattern no asset matches is not an error to gh while another + # matches, so each file is checked for. + attempt=0 + until gh release download "${HERMETIC_VERSION}" --repo forkcloser/hermetic --dir "${tmp}" --clobber \ + --pattern checksums.txt --pattern "${asset}"; do + attempt=$((attempt + 1)) + [ "$attempt" -lt 5 ] || { echo "::error::hermetic ${HERMETIC_VERSION}: checksums.txt or ${asset} could not be downloaded"; exit 1; } + sleep 3 + done + for f in checksums.txt "${asset}"; do + [ -f "${tmp}/${f}" ] || { echo "::error::hermetic ${HERMETIC_VERSION} has no release asset ${f}"; exit 1; } + done # No single digest tool exists everywhere: linux and windows git-bash - # ship coreutils sha256sum, macOS ships perl shasum. - if command -v sha256sum >/dev/null 2>&1; then - echo "${HERMETIC_CHECKSUMS_SHA256} ${tmp}/checksums.txt" | sha256sum -c - - else - echo "${HERMETIC_CHECKSUMS_SHA256} ${tmp}/checksums.txt" | shasum -a 256 -c - - fi - curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors "${revoke[@]}" \ - -o "${tmp}/${asset}" "${base}/${asset}" + # ship coreutils sha256sum, macOS ships perl shasum. A mismatch names + # the computed digest and the size beside the expected one: a stale + # pin (the previous release's hash, which a Renovate bump carries + # until the checksum workflow refreshes it) and a wrong download read + # differently off the log. + verify_sha256() { + local expected="$1" file="$2" actual + if command -v sha256sum >/dev/null 2>&1; then + actual="$(sha256sum "$file" | cut -d' ' -f1)" + else + actual="$(shasum -a 256 "$file" | cut -d' ' -f1)" + fi + if [ "$actual" != "$expected" ]; then + echo "::error::sha256 mismatch for $(basename "$file") ($(wc -c <"$file" | tr -d ' ') bytes): expected ${expected}, computed ${actual}" + exit 1 + fi + echo "sha256 ok: $(basename "$file")" + } + verify_sha256 "${HERMETIC_CHECKSUMS_SHA256}" "${tmp}/checksums.txt" expected="$(awk -v a="$asset" '$2 == a {print $1}' "${tmp}/checksums.txt")" [ -n "$expected" ] || { echo "::error::checksums.txt of hermetic ${HERMETIC_VERSION} does not list ${asset}"; exit 1; } - if command -v sha256sum >/dev/null 2>&1; then - echo "${expected} ${tmp}/${asset}" | sha256sum -c - - else - echo "${expected} ${tmp}/${asset}" | shasum -a 256 -c - - fi + verify_sha256 "${expected}" "${tmp}/${asset}" # GitHub's build provenance, on every runner through gh: the asset was # built by hermetic's release workflow from the tagged commit. gh attestation verify "${tmp}/${asset}" --repo forkcloser/hermetic diff --git a/.github/actions/windows-cache-image/action.yaml b/.github/actions/windows-cache-image/action.yaml index 48a964b..608af2d 100644 --- a/.github/actions/windows-cache-image/action.yaml +++ b/.github/actions/windows-cache-image/action.yaml @@ -87,6 +87,16 @@ runs: $build = ($env:GITHUB_WORKSPACE -replace '\\', '/') + '/.build' New-Item -ItemType Directory -Force -Path C:\vcache\golangci, $build | Out-Null New-Item -ItemType Junction -Path "$build/cache" -Target C:\vcache\golangci | Out-Null + # The Rust lanes' state the same way, in a repository that pins a + # toolchain: rustup, cargo and zig under LIMEN_BUILD are junctions + # into the image (book/rust.md); rustup and cargo take a junction + # as the directory it is. + if (Test-Path "$env:GITHUB_WORKSPACE/rust-toolchain.toml") { + foreach ($d in 'rustup', 'cargo', 'zig') { + New-Item -ItemType Directory -Force -Path "C:\vcache\$d" | Out-Null + New-Item -ItemType Junction -Path "$build/$d" -Target "C:\vcache\$d" | Out-Null + } + } "LIMEN_BUILD=$build" >> $env:GITHUB_ENV "GOCACHE=C:\vcache\go-build" >> $env:GITHUB_ENV "GOMODCACHE=C:\vcache\mod" >> $env:GITHUB_ENV @@ -98,6 +108,9 @@ runs: run: | $ErrorActionPreference = 'Stop' Remove-Item "$env:LIMEN_BUILD/cache" -Force + foreach ($d in 'rustup', 'cargo', 'zig') { + if (Test-Path "$env:LIMEN_BUILD/$d") { Remove-Item "$env:LIMEN_BUILD/$d" -Force } + } Dismount-DiskImage -ImagePath C:\vhd\cache.vhdx | Out-Null - if: inputs.mode == 'save' diff --git a/.github/release.yml b/.github/release.yml index f5f6a53..7fa37d1 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,10 +1,16 @@ # DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift. # -# How GitHub groups a release's notes, which are the titles of the pull -# requests merged since the previous tag (goreleaser's `use: github-native`). -# A pull request lands in the FIRST category it matches, so the catch-all -# must exclude what a later category claims. +# How GitHub writes a release's notes, the titles of the pull requests merged +# since the previous tag (goreleaser's `use: github-native`). Excluded labels +# and authors are left out of the notes; a pull request lands in the FIRST +# category it matches. changelog: + exclude: + labels: + - dependencies + - internal + authors: + - renovate[bot] categories: - title: Breaking changes labels: @@ -12,11 +18,3 @@ changelog: - title: Changes labels: - '*' - exclude: - labels: - - dependencies - authors: - - renovate[bot] - - title: Dependencies - labels: - - '*' diff --git a/.github/workflows/caches.yaml b/.github/workflows/caches.yaml index 3021e58..9a744a8 100644 --- a/.github/workflows/caches.yaml +++ b/.github/workflows/caches.yaml @@ -1,4 +1,5 @@ -# DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift. +# DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift; the one line +# Renovate moves is the `uses:` sha. # # The cache budget. GitHub keeps 10 GB of Actions caches per repository and # evicts the least recently used beyond it, the current entries included, so @@ -9,8 +10,8 @@ # eviction (and the caches that accumulate, Go's and the fuzz corpus, start # one a day). This keeps one generation per key on the default branch: after # each ci run there, and once a day, every entry under a canonical prefix that -# is not the newest of its family is deleted. GitHub's own token, `gh` from -# the runner image; no checkout, no third-party actions. +# is not the newest of its family is deleted. The steps are limen's reusable +# workflow. name: caches on: @@ -31,53 +32,9 @@ permissions: {} concurrency: group: ${{ github.workflow }} -defaults: - run: - shell: bash - jobs: prune: - runs-on: ubuntu-24.04 - timeout-minutes: 10 + uses: farcloser/limen/.github/workflows/limen-caches.yaml@56ef4e8a97ae7666a759b353a94be77e0ea05d12 # v0.26.0 permissions: # Listing and deleting caches is an Actions write. actions: write - steps: - - name: One generation per key on the default branch - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - run: | - # The default branch's entries, newest first, the branch read from - # the repository, never assumed by name (a scheduled run has no - # event naming it). Listed before the loop so a refused token or a - # failed call fails the step: fed through a process - # substitution, the loop would read nothing and pass, and the prune - # would silently not happen. A family is a key without its UTC day - # and content hash (and, for the Go caches, without the Go version, - # so a toolchain bump retires the old version's entry too), plus the - # entry's version: GitHub tells entries apart by key and version, - # and the version follows the cached paths, so two entries caching - # different paths under one key are two families, never each - # other's older generation. The first of each family is kept, the - # rest deleted. Only the canonical prefixes; whatever else is in the - # store is not ours to judge. - default_branch=$(gh api "repos/$GH_REPO" --jq .default_branch) - entries=$(gh cache list --ref "refs/heads/$default_branch" --limit 500 --sort created_at --order desc \ - --json id,key,version,sizeInBytes --jq '.[] | "\(.id)\t\(.key)\t\(.version)\t\(.sizeInBytes)"') - kept= - while IFS=$'\t' read -r id key version size; do - case "$key" in - aqua-pkgs-*|go-*|fuzz-corpus-*|win-image-*) ;; - *) continue ;; - esac - family=$(printf '%s' "$key" | sed -E 's/-[0-9]{8}$//; s/-[0-9a-f]{64}$//; s/-+$//; s/^(go-(lint-)?[^-]+-[^-]+)-go[0-9][^-]*$/\1/') - family="$family $version" - if printf '%s' "$kept" | grep -qxF -- "$family"; then - printf 'delete %s (%d MB)\n' "$key" "$((size / 1000000))" - gh cache delete "$id" - else - kept="$kept$family"$'\n' - printf 'keep %s\n' "$key" - fi - done <<< "$entries" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c980b69..c518249 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,7 +21,7 @@ concurrency: jobs: limen: - uses: ./.github/workflows/limen-verify.yaml + uses: farcloser/limen/.github/workflows/limen-verify.yaml@56ef4e8a97ae7666a759b353a94be77e0ea05d12 # v0.26.0 permissions: contents: read # The binary links Virtualization.framework through cgo and clones disks diff --git a/.github/workflows/limen-verify.yaml b/.github/workflows/limen-verify.yaml deleted file mode 100644 index 25714dd..0000000 --- a/.github/workflows/limen-verify.yaml +++ /dev/null @@ -1,175 +0,0 @@ -# DO NOT EDIT MANUALLY: content-pinned by limen, reset by `limen fix`. -# The shared CI lanes every repository runs, called from its own ci.yaml -# (`uses: ./.github/workflows/limen-verify.yaml`). A change here reaches every -# repository with its limen bump; the project's own jobs live in its ci.yaml. -# -# Minimal glue: GitHub's own actions pinned by SHA, hermetic at .hermetic/hermetic.yaml's -# pins, `just` running the recipes a laptop runs. No third-party actions, -# hence no egress-filtering action either. -name: limen-verify - -on: - workflow_call: - inputs: - os: - # Pinned images, never -latest. macOS is not redundant: its /bin/bash - # is 3.2, the floor the recipes target. Both windows legs run under - # git-bash. These names are NOT the ruleset's required contexts (the - # caller's `gate` is), so a project may trim or extend the list. - description: The verify matrix's runners, as a JSON list. - type: string - default: '["ubuntu-24.04", "ubuntu-24.04-arm", "macos-15", "windows-2025", "windows-11-arm"]' - -# No default token permissions: each job states what it needs, within what -# the caller grants (contents: read). -permissions: {} - -# Explicit bash everywhere: windows defaults to PowerShell and never sees -# git-bash's environment; explicit bash also brings -eo pipefail. -defaults: - run: - shell: bash - -jobs: - verify: - strategy: - fail-fast: false - matrix: - os: ${{ fromJSON(inputs.os) }} - runs-on: ${{ matrix.os }} - # Generous for the windows legs, which are markedly slower. - timeout-minutes: 45 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - 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 do lint commits` validates the commit - # range against the PR's base branch, which a shallow clone lacks. - fetch-depth: 0 - - # The job's caches (.github/actions/cache): attached here, before - # setup-hermetic, since on Windows hermetic's root is inside the cache - # image (restored as files, the caches cost minutes of small-file writes - # there); restored right after setup-hermetic; saved at the end. - - uses: ./.github/actions/cache - with: - mode: attach - - - name: Install hermetic (pinned, verified) - uses: ./.github/actions/setup-hermetic - - # The per-platform analysis legs compile the whole module graph five - # times, cold, on every run without the Go caches. The pins are linked - # by setup-hermetic: a tool downloads lazily on first use, - # checksum-verified, so a job pays only for what its recipes run. - - uses: ./.github/actions/cache - with: - mode: restore - linter-cache: 'true' - - - name: Lint - run: just lint - - - name: Test - run: just test - - - uses: ./.github/actions/cache - with: - mode: save - - # One linux leg, not the matrix: fuzzing explores the same corpus wherever - # it runs, so more legs cost CPU and add nothing. The recipe passes on a - # tree with no targets or no Go module, so this job is safe in every - # repository. - fuzz: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Install hermetic (pinned, verified) - uses: ./.github/actions/setup-hermetic - - # The cached corpus is what makes fuzzing cumulative across runs. Keyed - # on the fuzz sources so a changed target restarts its corpus, and on - # the UTC day so that the default branch saves what a day of runs found - # (a key is never overwritten); restore-keys keep the rest. Saved on the - # default branch only (caches.yaml), read from the event, never assumed - # by name. - - id: fuzzdir - run: | - echo "dir=$(go env GOCACHE)/fuzz" >> "$GITHUB_OUTPUT" - echo "day=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - - id: corpus - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ steps.fuzzdir.outputs.dir }} - key: fuzz-corpus-${{ runner.os }}-${{ hashFiles('**/*_fuzz_test.go', '**/fuzz_test.go') }}-${{ steps.fuzzdir.outputs.day }} - restore-keys: | - fuzz-corpus-${{ runner.os }}-${{ hashFiles('**/*_fuzz_test.go', '**/fuzz_test.go') }}- - fuzz-corpus-${{ runner.os }}- - - - name: Fuzz - run: just do test go fuzz - - - if: (github.event_name == 'schedule' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) && steps.corpus.outputs.cache-hit != 'true' - uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ${{ steps.fuzzdir.outputs.dir }} - key: ${{ steps.corpus.outputs.cache-primary-key }} - - # A crasher is a bug with a reproducer attached; the log names the - # target, the input file is what reproduces it locally. Uploaded only - # on failure, and only if any was written. - - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - if: failure() - with: - name: fuzz-crashers - path: '**/testdata/fuzz/' - if-no-files-found: ignore - - # Every pin resolves. The verify legs link only, and a link verifies - # nothing: an asset that is missing from a release, or that fails its - # checksum, would merge green and fail at first use. One linux leg pays for - # the real install; aqua's package store is cached on the exact pins, so a - # warm key costs seconds and a changed pin is always a real install. (The - # Go-built tools are .limen/tools/go.mod directives, built by the verify legs.) - tools: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Install hermetic (pinned, verified) - uses: ./.github/actions/setup-hermetic - - # The same store as the verify legs', so this job and the linux leg - # share it, and no fallback here: a changed pin must be a real install, - # which is what this job exists to prove. - - uses: ./.github/actions/cache - with: - mode: restore - go: 'false' - aqua-fallback: 'false' - - - name: Install every pinned tool - run: hermetic install - - # Saved on the default branch only, on a miss (caches.yaml); the verify - # leg that shares the key may have saved it first, and a key is never - # overwritten. - - uses: ./.github/actions/cache - with: - mode: save diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml index cd5c918..44bca54 100644 --- a/.github/workflows/security.yaml +++ b/.github/workflows/security.yaml @@ -1,13 +1,14 @@ -# DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift. A project's -# own scans go in its `security` recipe in the root .justfile, which this runs. +# DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift; the one line +# Renovate moves is the `uses:` sha. A project's own scans go in its +# `security` recipe in the root .justfile, which the called workflow runs. # # The security lane, apart from ci: a scan's verdict moves with the # vulnerability database, not with the tree, so this runs on a schedule as # well as on every push and pull request, and its check stands alone — a new # advisory reddens `security`, never `gate`, and the answer is a dependency -# bump, never a code change. Same glue as ci: GitHub's own actions pinned by -# SHA, hermetic at .hermetic/hermetic.yaml's pins, `just` running the recipe a laptop runs. No -# third-party actions. +# bump, never a code change. The steps are limen's reusable workflow; the +# `security` job below is the one check the ruleset requires, a stable name +# over the called job, as ci.yaml's `gate` is over the verify lanes. name: security on: @@ -19,7 +20,7 @@ on: - cron: '17 6 * * *' workflow_dispatch: -# No default token permissions: the job states what it needs. This workflow +# No default token permissions: each job states what it needs. This workflow # only ever reads the repository. permissions: {} @@ -28,46 +29,28 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -defaults: - run: - shell: bash - jobs: - # One linux leg: the scan loops over every supported platform itself - # (_per-platform in lib.just), so more legs would repeat one verdict. - security: - runs-on: ubuntu-24.04 - timeout-minutes: 30 + scan: + uses: farcloser/limen/.github/workflows/limen-security.yaml@56ef4e8a97ae7666a759b353a94be77e0ea05d12 # v0.26.0 permissions: contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - # The token is not left behind in .git/config: nothing in this - # workflow talks to GitHub after checkout. - persist-credentials: false - - - name: Install hermetic (pinned, verified) - uses: ./.github/actions/setup-hermetic - # The job's caches (.github/actions/cache): hermetic's store and the Go - # caches, which the scan needs once per supported platform, cold - # without them; no linter cache, so the Go key is go-, not the verify - # legs' go-lint-. The pins are linked by setup-hermetic: a tool - # downloads lazily on first use, checksum-verified. - - uses: ./.github/actions/cache - with: - mode: restore - - # The project's `security`, like ci's `lint` and `test`: the root - # .justfile names the shared scans and adds its own (a cgo project scans - # the packages the native leg cannot load). The shared scans pass on a - # tree with no Go module, so this job is safe in every repository. - - name: Scan - run: just security - - # Saved on the default branch only, on a miss (the cache budget: - # caches.yaml); the scheduled run is on it by definition. - - uses: ./.github/actions/cache - with: - mode: save + # The required status check. always(), and the result asserted explicitly: + # without always() a failed or cancelled scan SKIPS this job instead of + # failing it, and a skipped required check does not block a merge. + security: + needs: [scan] + if: always() + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: {} + steps: + - name: The scan succeeded + env: + # Via env, never interpolated into the script: the shell sees data, + # not something the expression layer can rewrite into code. + RESULT: ${{ needs.scan.result }} + shell: bash + run: | + printf 'scan: %s\n' "$RESULT" + [ "$RESULT" = "success" ] diff --git a/.github/workflows/update-aqua-checksum.yaml b/.github/workflows/update-aqua-checksum.yaml index 38a5a9f..4228439 100644 --- a/.github/workflows/update-aqua-checksum.yaml +++ b/.github/workflows/update-aqua-checksum.yaml @@ -1,26 +1,11 @@ -# DO NOT EDIT MANUALLY. -# Generic, no project content. Renovate's other half: on its branches, -# regenerate .hermetic/aqua-checksums.json (a bumped pin with a stale checksum breaks -# every install) and, when the branch bumps farcloser/limen, run the branch's -# own pinned `limen fix` so the canonical files move with the pin. One fix-up -# commit. +# DO NOT EDIT MANUALLY: content-pinned by limen, reset on drift; the one line +# Renovate moves is the `uses:` sha. # -# A WRITE workflow, hardened: `push:` on the branch prefix, never -# pull_request_target; no credential kept by checkout and no secrets in the -# update steps' environment beyond the job's own token where a verifier must -# read from GitHub; nothing runs through `just` (a write job does not execute -# recipe code the branch controls — what runs is aqua, and the limen release -# the branch selects by checksum); the commit step uses runner tools only, -# with the push token scoped to that step; the branch name reaches the shell -# via env; no loop, since a default-token commit triggers no workflows. -# -# The commit goes through GraphQL createCommitOnBranch because GitHub signs -# those and the main ruleset requires signatures (see the commit step). Its -# author is the token's identity: github-actions[bot] by default, or the -# App/PAT identity when UPDATE_AQUA_CHECKSUM_APP_ID (+ private key secret) or -# UPDATE_AQUA_CHECKSUM_TOKEN is set — that identity must be in renovate.json's -# gitIgnoredAuthors or Renovate stops rebasing. The App is worth it: -# default-token commits get no CI run. +# Renovate's other half: on its branches, regenerate .hermetic/aqua-checksums.json +# (a bumped pin with a stale checksum breaks every install) and, when the +# branch bumps farcloser/limen, run the branch's own pinned `limen fix` so +# the canonical files move with the pin. One fix-up commit. The steps are +# limen's reusable workflow; its hardening is described there. name: update-aqua-checksum on: @@ -33,184 +18,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -# Explicit bash: windows defaults to PowerShell, and bash brings -eo pipefail. -defaults: - run: - shell: bash - jobs: update: - runs-on: ubuntu-24.04 - timeout-minutes: 15 + uses: farcloser/limen/.github/workflows/limen-update-aqua-checksum.yaml@56ef4e8a97ae7666a759b353a94be77e0ea05d12 # v0.26.0 permissions: contents: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Recompute hermetic's digest for the version Renovate moved - # Before setup-hermetic, which verifies the release's checksums.txt - # against the digest pinned beside the version: Renovate moves that - # version in this repository alone (the action is content-pinned - # everywhere else) and never a digest, so a bump arrives with the old - # one and the action would refuse its own download. Runner tools - # only, on the release's own file; what then guards the bump's run is - # the action's attestation check on the asset, and the digest written - # here guards every run after, as the pin the fleet receives. - if: github.repository == 'farcloser/limen' - run: | - action=.github/actions/setup-hermetic/action.yaml - version="$(sed -n 's/^ *HERMETIC_VERSION: \(v[0-9][^ ]*\)$/\1/p' "$action")" - [ -n "$version" ] || { echo "no HERMETIC_VERSION in $action" >&2; exit 1; } - tmp="$(mktemp -d)" - curl --proto '=https' --tlsv1.2 -fsSL --retry 5 --retry-delay 3 --retry-all-errors -o "${tmp}/checksums.txt" \ - "https://github.com/forkcloser/hermetic/releases/download/${version}/checksums.txt" - sha="$(sha256sum "${tmp}/checksums.txt" | cut -d' ' -f1)" - rm -rf "${tmp}" - sed -i "s/^\( *HERMETIC_CHECKSUMS_SHA256: \)[0-9a-f]*$/\1${sha}/" "$action" - echo "hermetic ${version}: checksums.txt digest ${sha}" - - - name: Install hermetic (pinned, verified) - uses: ./.github/actions/setup-hermetic - with: - # No token for hermetic here: this job's token carries contents:write - # and the steps below run a branch-declared binary. A rate-limit 403 - # fails loudly and is rerun; a write token in branch-controlled code - # would not fail loudly. - github-token: "" - - - name: Regenerate .hermetic/aqua-checksums.json - # No tool install here: update-checksum only reads the manifest and - # hashes upstream artifacts — the smallest possible surface for a - # write job. It is what makes the branch's limen pin installable for - # the converge step below: setup-hermetic linked the pins, and a link - # fetches nothing, so from here on every command the job runs — - # limen's verifiers (gh, cosign), the push's curl and jq — is the - # branch's own pin on the hermetic PATH, pulled when first run and - # verified against the checksums regenerated here, never the runner's - # copy. - run: hermetic update-checksum --prune - - - name: Converge the limen baseline - # The link resolves the branch's own limen pin, verified with the - # checksums from the step above. A released limen leaves its own pin - # alone, and on a renovate/ branch (GITHUB_REF_NAME) it leaves - # .hermetic/hermetic.yaml alone too: Renovate renders that file on - # every run, so a line added here would be removed, added, removed, - # the branch force-pushed each time. What the manifest lacks, fix adds - # on main. Skipped in the limen repository: there the working tree IS - # the next baseline. - if: github.repository != 'farcloser/limen' - run: limen fix . - - - name: Refresh the digests of pinned artifacts, if the project declares any - # The same pinned release, on the project's pins.yaml: a version - # Renovate moved gets its digest recomputed through the verifier the - # entry declares (book/tooling.md, "Pinned artifacts"). The job's own - # token, read-only in use, is what `gh attestation verify` needs to - # ask GitHub for an attestation; the code holding it is the release - # the branch selects by checksum, never recipe text. The verifiers a - # method shells out to (gh, cosign) resolve on the PATH linked above. - if: github.repository != 'farcloser/limen' && hashFiles('pins.yaml') != '' - env: - GH_TOKEN: ${{ github.token }} - run: limen pins refresh - - - name: Mint a push token, if the App is configured - # After the update steps, so those run with no secrets. One-hour token, - # this repository only; the private key only mints. - if: vars.UPDATE_AQUA_CHECKSUM_APP_ID != '' - id: app-token - uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0 - with: - app-id: ${{ vars.UPDATE_AQUA_CHECKSUM_APP_ID }} - private-key: ${{ secrets.UPDATE_AQUA_CHECKSUM_APP_PRIVATE_KEY }} - # Least privilege, but both: naming any permission scopes the token to - # exactly those, and the fix-up carries canonical workflow files whenever - # a limen bump changed one. GitHub refuses that commit from a token - # without workflows:write ("Resource not accessible by integration"). - permission-contents: write - permission-workflows: write - - - name: Commit the update through the API, if any - env: - 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; } - # GraphQL createCommitOnBranch, not `git commit` + push: those commits - # are GitHub-signed, and the main ruleset requires signatures — an - # unsigned fix-up commit made every aqua-bump PR unmergeable - # (book/github.md). - # - # The whole dirty tree, on purpose: exactly three writers ran since - # checkout (update-checksum, the pinned limen's fix and pins refresh), and a path - # list baked into the older running workflow cannot know the new - # baseline's surface. - # - # Limit of the mutation, accepted: FileAddition carries no file - # mode, so an executable bit cannot travel — both writers here - # only ever produce plain configuration files. - # Every file the payload touches travels through a FILE, never a - # command line. A converged baseline is ~120 kB of configuration, - # ~160 kB once base64'd, and Linux caps a single argument at 128 kB - # (MAX_ARG_STRLEN): passing the accumulated array as `--argjson` died - # with "jq: Argument list too long" and left the branch with a bumped - # pin, no checksum, and a red pipeline that never reached its first - # recipe. `--rawfile` and `--slurpfile` read from disk; the request - # body is uploaded from disk too. - work="$(mktemp -d)" - trap 'rm -rf "${work}"' EXIT - printf '[]' > "${work}/additions.json" - printf '[]' > "${work}/deletions.json" - while IFS= read -r -d '' entry; do - path="${entry:3}" - if [ -f "${path}" ]; then - base64 -w0 "${path}" > "${work}/blob" - jq --arg path "${path}" --rawfile contents "${work}/blob" \ - '. + [{path: $path, contents: ($contents | rtrimstr("\n"))}]' \ - "${work}/additions.json" > "${work}/next" - mv "${work}/next" "${work}/additions.json" - else - jq --arg path "${path}" '. + [{path: $path}]' \ - "${work}/deletions.json" > "${work}/next" - mv "${work}/next" "${work}/deletions.json" - fi - # --untracked-files=all: by default a new directory is ONE entry, - # `?? dir/`, which is not a file, would be sent as a deletion, and - # its files never committed — any directory the pinned limen fix - # seeds (.limen/tools/golangci-lint/ did) would be lost. - done < <(git status --porcelain -z --untracked-files=all) - # Signed-off-by: `just do lint commits` enforces DCO on the PR range, - # bot commits included. - jq -n \ - --arg query 'mutation ($input: CreateCommitOnBranchInput!) { createCommitOnBranch(input: $input) { commit { oid } } }' \ - --arg repository "${GITHUB_REPOSITORY}" \ - --arg branch "${BRANCH}" \ - --arg head "$(git rev-parse HEAD)" \ - --slurpfile additions "${work}/additions.json" \ - --slurpfile deletions "${work}/deletions.json" \ - '{query: $query, variables: {input: { - branch: {repositoryNameWithOwner: $repository, branchName: $branch}, - expectedHeadOid: $head, - message: { - headline: "chore: refresh checksums and converge the limen baseline", - body: "Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" - }, - fileChanges: {additions: $additions[0], deletions: $deletions[0]}}}}' \ - > "${work}/payload.json" - # GraphQL reports failure in-body over HTTP 200, so success is the - # presence of the new commit oid, never the HTTP status. No --retry - # and no -L, on purpose: this is a mutation guarded by - # expectedHeadOid — a retry after a success that timed out on the - # way back would fail on the moved head and report a commit that - # landed as an error, and a redirect must never replay the POST. - response="$(curl --proto '=https' --tlsv1.2 -fsS -X POST -H "Authorization: bearer ${TOKEN}" \ - --data-binary "@${work}/payload.json" https://api.github.com/graphql)" - oid="$(jq -r '.data.createCommitOnBranch.commit.oid // empty' <<<"${response}")" - if [ -z "${oid}" ]; then - echo "${response}" >&2 - exit 1 - fi - echo "pushed ${oid}" + # To approve the pull request's runs on the head the job pushes. + actions: write diff --git a/.hermetic/aqua-checksums.json b/.hermetic/aqua-checksums.json index 8fe43d2..b9f73e0 100644 --- a/.hermetic/aqua-checksums.json +++ b/.hermetic/aqua-checksums.json @@ -1,5 +1,25 @@ { "checksums": [ + { + "id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_darwin_arm64.tar.gz", + "checksum": "17012A5A110323BB1069E513A8665F1AE445905C56B4E3F9A9DF4F95C5BB6161", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_linux_amd64.tar.gz", + "checksum": "9C077A7D43029B3B13E98A8DB720661E700C4D58DB6352153A06395AE4A2A704", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_linux_arm64.tar.gz", + "checksum": "410CBFA0AE0F63246BC0057843760B1DE2833CD23919E025E368FD7681308CDB", + "algorithm": "sha256" + }, + { + "id": "github_release/github.com/anchore/syft/v1.57.0/syft_1.57.0_windows_amd64.zip", + "checksum": "06E9663E55F7F38FF966710670FBE0763182C621F7AC2F191BCB209A6DFBD969", + "algorithm": "sha256" + }, { "id": "github_release/github.com/casey/just/1.59.0/just-1.59.0-aarch64-apple-darwin.tar.gz", "checksum": "6574CEA774ED4CAE12E24B6AA11857DE33E80DAB4667CF628FD5840DF96A2A9D", @@ -21,58 +41,58 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/cli/cli/v2.102.0/gh_2.102.0_linux_amd64.tar.gz", - "checksum": "BB766F710EEF8EDE859C18578C72C327597CD4C8A85B06001B1F3843C6019386", + "id": "github_release/github.com/cli/cli/v2.103.3/gh_2.103.3_linux_amd64.tar.gz", + "checksum": "4C806978FFCE1CEC70853302A56D70AC6DA4F19BAE7ADCA4E1A8AB00633DD91D", "algorithm": "sha256" }, { - "id": "github_release/github.com/cli/cli/v2.102.0/gh_2.102.0_linux_arm64.tar.gz", - "checksum": "7862C86C72F43DF3A2D93DDDE6F473285B4E2AF61B494849846827E513EF6484", + "id": "github_release/github.com/cli/cli/v2.103.3/gh_2.103.3_linux_arm64.tar.gz", + "checksum": "EE32DF2323C8847F5FAC6CB6B4B69CA75828E9329A8F22F677021F7B3306CE63", "algorithm": "sha256" }, { - "id": "github_release/github.com/cli/cli/v2.102.0/gh_2.102.0_macOS_arm64.zip", - "checksum": "DA922C20D1792E5B2CBF375593D7A658ACF034C12C84E007E71C76EF959C337E", + "id": "github_release/github.com/cli/cli/v2.103.3/gh_2.103.3_macOS_arm64.zip", + "checksum": "D700207EAE9AE98FE2B89535C2C087C07823D4628C56364695A5A07EF311C062", "algorithm": "sha256" }, { - "id": "github_release/github.com/cli/cli/v2.102.0/gh_2.102.0_windows_amd64.zip", - "checksum": "AE64E556ECC240B200F7EBA60D550E4BB60D78E860E69DD88C449405B86067F4", + "id": "github_release/github.com/cli/cli/v2.103.3/gh_2.103.3_windows_amd64.zip", + "checksum": "98D0CFA794721665D7973DD728C78AFF7BD836D58F10F6D7A03568ECCB0A22E0", "algorithm": "sha256" }, { - "id": "github_release/github.com/cli/cli/v2.102.0/gh_2.102.0_windows_arm64.zip", - "checksum": "5DCF12AA8525EABD0C46EC414F323AB6CF65229FC2CD46543CC705001BBAF223", + "id": "github_release/github.com/cli/cli/v2.103.3/gh_2.103.3_windows_arm64.zip", + "checksum": "197211C98687D7770AC331FE8A3B6C198C389108C1536CDBEDF659FE4703E484", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.15.0/limen_0.15.0_darwin_arm64.tar.gz", - "checksum": "F7A217D14B1868F9727AD58B7B2D1CEA0F64B73C602A12C0F2DC47C5043B5702", + "id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_darwin_arm64.tar.gz", + "checksum": "6C013BBA485F3081BBA15342E7F6A9BB62B63E5D10D548DC2D425FACBE01CE87", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.15.0/limen_0.15.0_linux_amd64.tar.gz", - "checksum": "167548B6799C5E20D34E701848DCEA7927B82EE96AAB62A0778EF6A277215367", + "id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_linux_amd64.tar.gz", + "checksum": "C054388FC8BD99D049C2D342FEC6422AAD181FFA4B1520D2E789E16A90D8C8F6", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.15.0/limen_0.15.0_linux_arm64.tar.gz", - "checksum": "B2F3BB1CE60F4AD44884EFA67E46CDE1F67DB654F2D9BCCEA738173927334D45", + "id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_linux_arm64.tar.gz", + "checksum": "AF2541BCCE26DFEEC8B62F802327E65EAC6C054D8647D251370E4072F668FB5A", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.15.0/limen_0.15.0_windows_amd64.tar.gz", - "checksum": "47FF61CA9E968130DC5A30B5A1B0A56A099F68F9B46A349B0F9C511CDE7B31F0", + "id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_windows_amd64.tar.gz", + "checksum": "53F06B63777631B57F08FBDE8BD466AA456286D54701DFD04553AF06306C14A0", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/limen/v0.15.0/limen_0.15.0_windows_arm64.tar.gz", - "checksum": "108A3FCB23B20AF0C4ED86B04B08253B38251090F46F7C55945F600C59B22F39", + "id": "github_release/github.com/farcloser/limen/v0.26.0/limen_0.26.0_windows_arm64.tar.gz", + "checksum": "E83991B59F70E7DA0AB7A22FEAC93F6BF58768C683901C1B1D458BC4130E4879", "algorithm": "sha256" }, { - "id": "github_release/github.com/farcloser/ossein/v0.3.3/ossein_0.3.3_darwin_arm64.tar.gz", - "checksum": "E59064A90807241A08C67C887514389A173D96003D2B4D8D76EF1F2B8E58838A", + "id": "github_release/github.com/farcloser/ossein/v0.3.4/ossein_0.3.4_darwin_arm64.tar.gz", + "checksum": "9654C95462E5C907D9562695BDDB01675EF580BF847B122FFEADF3ED7F761A67", "algorithm": "sha256" }, { @@ -101,113 +121,63 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/forkcloser/curl/v8.22.0/curl_8.22.0_darwin_arm64.tar.gz", - "checksum": "E733869CF4FED3F5CB0F67E0AA37DD21216690D27C56525749E3F3A211227DD3", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/curl/v8.22.0/curl_8.22.0_linux_amd64.tar.gz", - "checksum": "0DEE3074F44F56ADBCBB94C3DF506EC60BBFDA31DAA98FA537827C2A290A4666", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/curl/v8.22.0/curl_8.22.0_linux_arm64.tar.gz", - "checksum": "C78C61656EC58AB22EA5107B686772811478B91BB97BAD1EADD259C990058EE0", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/curl/v8.22.0/curl_8.22.0_windows_amd64.tar.gz", - "checksum": "9057B8D7890C58B7254F7207938ECA51C0A43D66B88B1FFD713DD84248941CD9", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/curl/v8.22.0/curl_8.22.0_windows_arm64.tar.gz", - "checksum": "4CEBEC6D937A154AE2DC16815E144831CA8F90B85C5E22BA93C6A128518C11DC", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/yamlfmt/v0.2.0/yamlfmt_0.2.0_darwin_arm64.tar.gz", - "checksum": "05B9C928E76D365C2D98E597FCB3367B0C5F94519515E19D00E1124834EA144A", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/yamlfmt/v0.2.0/yamlfmt_0.2.0_linux_amd64.tar.gz", - "checksum": "9D3260A43AE5B919A4B9D80E1AA410B683BAD4256CBA40A68E628B28185E096E", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/yamlfmt/v0.2.0/yamlfmt_0.2.0_linux_arm64.tar.gz", - "checksum": "FBD9C827B4FA7F3C1D57B610CDF7B4D58247755CB4D4F1890C619522CC96AA57", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/yamlfmt/v0.2.0/yamlfmt_0.2.0_windows_amd64.tar.gz", - "checksum": "1F58A6A46FBF39CECC749C12B77332963E9A4B8E667C419E3EE967B11B9E4DEC", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/forkcloser/yamlfmt/v0.2.0/yamlfmt_0.2.0_windows_arm64.tar.gz", - "checksum": "BABCF7C603C684293A2D9914D2DC5FFED5505DE4491A7F4B0961ABB7C8A1F987", - "algorithm": "sha256" - }, - { - "id": "github_release/github.com/git-for-windows/git/v2.56.0.windows.2/Git-2.56.0.2-64-bit.tar.bz2", - "checksum": "16CA394BDB94B372267D79E1E10B68763674F73E03E0648202A7971A8A730A84", + "id": "github_release/github.com/forkcloser/curl/v8.23.0/curl_8.23.0_darwin_arm64.tar.gz", + "checksum": "4BC0577E21BD937DD0E6D3BE1075C86025FD4DF63BB61F12B5FEF9BC841C6ED9", "algorithm": "sha256" }, { - "id": "github_release/github.com/git-for-windows/git/v2.56.0.windows.2/Git-2.56.0.2-arm64.tar.bz2", - "checksum": "221865B66F4E6CE8F86837ACD777C4580E23DE8BF526E99DDA274C2544CAB7A0", + "id": "github_release/github.com/forkcloser/curl/v8.23.0/curl_8.23.0_linux_amd64.tar.gz", + "checksum": "70A243D9895B84C95410B61867A115CA9A6132704A1B376FA1F0565EB7A6B328", "algorithm": "sha256" }, { - "id": "github_release/github.com/goreleaser/goreleaser/v2.18.3/goreleaser_Darwin_all.tar.gz", - "checksum": "156F425536D40CE282B79C637CA00EB943792DE6AD1E2482A4B21CC4B5421F4F", + "id": "github_release/github.com/forkcloser/curl/v8.23.0/curl_8.23.0_linux_arm64.tar.gz", + "checksum": "C68ACE1458E6BF11F8E63E963E4753A76970E8A60FFF3D5CEE368A59186DE449", "algorithm": "sha256" }, { - "id": "github_release/github.com/goreleaser/goreleaser/v2.18.3/goreleaser_Linux_arm64.tar.gz", - "checksum": "FA7011A7F8DE3B504326B8552CF21C708ED3089519B01D372DA32EF4E8D298D7", + "id": "github_release/github.com/forkcloser/curl/v8.23.0/curl_8.23.0_windows_amd64.tar.gz", + "checksum": "9C391B70E577D0C4AC8B2B9B90958432AE72F96400043D720ACED2C5811216FA", "algorithm": "sha256" }, { - "id": "github_release/github.com/goreleaser/goreleaser/v2.18.3/goreleaser_Linux_x86_64.tar.gz", - "checksum": "B8DEE4728BCA1ABA0D71A832A17CD0A3799F75372878E7FFA36AEF2343BCE0BC", + "id": "github_release/github.com/forkcloser/curl/v8.23.0/curl_8.23.0_windows_arm64.tar.gz", + "checksum": "1098AA1A16D737FD7075583013A75B8BAB52C0530A79B6127254C4AE0DFE4220", "algorithm": "sha256" }, { - "id": "github_release/github.com/goreleaser/goreleaser/v2.18.3/goreleaser_Windows_arm64.zip", - "checksum": "371DA2DDDAC0C19C729A74D26305C2D168CFF50728EBE14B581FE5B7C5D98B59", + "id": "github_release/github.com/git-for-windows/git/v2.56.0.windows.4/Git-2.56.0.4-64-bit.tar.bz2", + "checksum": "0DAE059764BA7C447C9604EB6CB0F4AFAF2D90B003ECF1C8CF2DBE5A13E4442C", "algorithm": "sha256" }, { - "id": "github_release/github.com/goreleaser/goreleaser/v2.18.3/goreleaser_Windows_x86_64.zip", - "checksum": "5E7455DF2ACCBFE69416CF9CCBC6AEFE9ABD2543525D5D6DA24426D827AA8171", + "id": "github_release/github.com/git-for-windows/git/v2.56.0.windows.4/Git-2.56.0.4-arm64.tar.bz2", + "checksum": "E3903DB2FEA42263B4DF0D2F581CA8E07EC1C7C9F878156A8D01284F35011642", "algorithm": "sha256" }, { - "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_darwin_arm64.tar.gz", - "checksum": "509CB27AEF747F48FAF9BCE424F59DCF79572C905204B990EE935BBFCC7FA0E9", + "id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Darwin_all.tar.gz", + "checksum": "B4F0702BCB2AAD05BD13AF5AC69EC94D84270DCE59B917E7A8C446C974E29372", "algorithm": "sha256" }, { - "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_amd64.tar.gz", - "checksum": "11CCDDEAF708EF228889F9FE2F68291A75B27013DDFC3B18156E094F5F40E8EE", + "id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Linux_arm64.tar.gz", + "checksum": "2DADA0A6BBCD10F3A05228D98E77D25FB362EFB9E5BAB6362CF37E64C8A26CB9", "algorithm": "sha256" }, { - "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_linux_arm64.tar.gz", - "checksum": "7644A4C5CD1BB978D56245AEAB25A586AC5AC62ADEBED20A399548867C13499D", + "id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Linux_x86_64.tar.gz", + "checksum": "D2483159E6007F814132CB56635AD30A70013DBEB813D6033A7E01ADF6AE108C", "algorithm": "sha256" }, { - "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_windows_amd64.tar.gz", - "checksum": "FD5A6DC69E46A0970593E70D85A7E75F16714E9C61D6D72CCC324EB82DF5BB8A", + "id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Windows_arm64.zip", + "checksum": "B7AE30D558DB74D67BD03E39D3309AABF47E1A18649EF4AC4429F329C357F496", "algorithm": "sha256" }, { - "id": "github_release/github.com/gotestyourself/gotestsum/v1.13.0/gotestsum_1.13.0_windows_arm64.tar.gz", - "checksum": "72A59200F83B3204CD59FD417E384DB0543C5511D7A9E38957E74A4035950943", + "id": "github_release/github.com/goreleaser/goreleaser/v2.20.0/goreleaser_Windows_x86_64.zip", + "checksum": "3C6A58F6941224A25EC28DE733554743E9B2517C40C76991EEF5474F8C1C2C1A", "algorithm": "sha256" }, { @@ -316,43 +286,43 @@ "algorithm": "sha256" }, { - "id": "github_release/github.com/quint-co/quint/v0.33.0/quint-linux-amd64", - "checksum": "79EAA33AEB28A72681122656974557DB553F4D01B792E22727075B41806DB4D3", + "id": "github_release/github.com/quint-co/quint/v0.33.1/quint-linux-amd64", + "checksum": "ACD153075B7EDC6FC8F889930C5FF3EBDF94ADA381A45A26A2D41B18B9F10DB7", "algorithm": "sha256" }, { - "id": "github_release/github.com/quint-co/quint/v0.33.0/quint-linux-arm64", - "checksum": "E99C0FCC88F4EBBDA0DFA5F58257F91DDF1AD40CEA452EB4770302FD24DAB315", + "id": "github_release/github.com/quint-co/quint/v0.33.1/quint-linux-arm64", + "checksum": "FFC0549933DB6787FFB932AA5C0E9A778018FE027E10D21CE4895C57502C11FC", "algorithm": "sha256" }, { - "id": "github_release/github.com/quint-co/quint/v0.33.0/quint-macos-arm64", - "checksum": "E65D0CB2054C9823E3F035656F7F9DDAEBD11B7E270FCF2F773008287B95EEF7", + "id": "github_release/github.com/quint-co/quint/v0.33.1/quint-macos-arm64", + "checksum": "8744447E5C641291A28D29848A4FD22E0D18237E9964934545E6270B78D2B8E3", "algorithm": "sha256" }, { - "id": "github_release/github.com/quint-co/quint/v0.33.0/quint-pc-amd64.exe", - "checksum": "8B419EE31C8A1D31B04FD5164BD966105FDF16FD6153DC687DF89FA37BFF036D", + "id": "github_release/github.com/quint-co/quint/v0.33.1/quint-pc-amd64.exe", + "checksum": "E2BFD802D1FA79A764216AC9D7948535F6EFFA6C68A0EBC5745D433709EB0C6D", "algorithm": "sha256" }, { - "id": "github_release/github.com/sigstore/cosign/v3.1.3/cosign-darwin-arm64", - "checksum": "5CF948C2F4DFE59687BDD0B8523709067383E03982CC543475C8A7DC70E92A76", + "id": "github_release/github.com/sigstore/cosign/v3.1.4/cosign-darwin-arm64", + "checksum": "9F9B8CD557CF2E5B3DF3373A00BAA005A86B02391E3E06DD0CE37B3A1533D3A4", "algorithm": "sha256" }, { - "id": "github_release/github.com/sigstore/cosign/v3.1.3/cosign-linux-amd64", - "checksum": "4629C757B7618056F8DDD7E2625AE9FDD94C0372A65049520BC7D9DF9EFC7F71", + "id": "github_release/github.com/sigstore/cosign/v3.1.4/cosign-linux-amd64", + "checksum": "424E2C97D0B45FD7A6B608A87F090F35633F4C23ECE5F092D48BBA11AAF4BBFA", "algorithm": "sha256" }, { - "id": "github_release/github.com/sigstore/cosign/v3.1.3/cosign-linux-arm64", - "checksum": "C5D324E091826B0D7A78EB16FEF316450B4EB9AAEC045611C08BA06F5E73220A", + "id": "github_release/github.com/sigstore/cosign/v3.1.4/cosign-linux-arm64", + "checksum": "ADE83B7AAC62E131CACFE96D958948B2150CC9EFEC46A570A2649B8E27DEDAD2", "algorithm": "sha256" }, { - "id": "github_release/github.com/sigstore/cosign/v3.1.3/cosign-windows-amd64.exe", - "checksum": "9FE59BE0ECA1271873CE019061335EB1AC419B7059202E797828467DDABE33BE", + "id": "github_release/github.com/sigstore/cosign/v3.1.4/cosign-windows-amd64.exe", + "checksum": "CBE953787074611F1334CBF37DC1CF953995BB8C45398EB4FC7BB781D29C1F2A", "algorithm": "sha256" }, { diff --git a/.limen/hermetic-registry.yaml b/.limen/hermetic-registry.yaml index 55e1eb0..4c5bfc2 100644 --- a/.limen/hermetic-registry.yaml +++ b/.limen/hermetic-registry.yaml @@ -550,6 +550,62 @@ packages: - darwin - linux - windows/amd64 + # rustup, the Rust toolchain installer (book/rust.md): the standard registry's + # entry (v4.578.0, unchanged at main) with two deltas, each for a defect + # recorded in book/upstream.md, so the entry retires when both are fixed: + # - the checksum file is read by pattern. The posix .sha256 files name the + # asset (` *./rustup-init`), and aqua's update-checksum maps a line + # that names an asset back to an environment by the asset's name, the same + # `rustup-init` for every posix environment, so three fetches land under + # one wrong id and the install fails "checksum is required"; a pattern + # takes the bare-digest path, recorded under the fetching environment's + # own id. + # - the Windows download URL ends in .exe (the file the host serves; the + # standard entry's URL answers 404 there), the file renamed on the way. + # Pin it as `rust-lang/rustup@` with `registry: local`. + - name: rust-lang/rustup + aliases: + - name: rust-lang/rustup/rustup-init + type: http + repo_owner: rust-lang + repo_name: rustup + files: + - name: rustup + link: rustup + - name: rustup-init + description: The Rust toolchain installer + version_source: github_tag + version_constraint: "false" + version_overrides: + - version_constraint: "true" + url: https://static.rust-lang.org/rustup/archive/{{.Version}}/{{.Arch}}-{{.OS}}/rustup-init + format: raw + replacements: + amd64: x86_64 + arm64: aarch64 + darwin: apple-darwin + linux: unknown-linux-gnu + windows: pc-windows-msvc + checksum: + type: http + url: https://static.rust-lang.org/rustup/archive/{{.Version}}/{{.Arch}}-{{.OS}}/rustup-init.sha256 + algorithm: sha256 + file_format: regexp + pattern: + checksum: "^([0-9a-f]{64})" + overrides: + - goos: windows + url: https://static.rust-lang.org/rustup/archive/{{.Version}}/{{.Arch}}-{{.OS}}/rustup-init.exe + files: + - name: rustup-init + src: rustup-init.exe + checksum: + type: http + url: https://static.rust-lang.org/rustup/archive/{{.Version}}/{{.Arch}}-{{.OS}}/rustup-init.exe.sha256 + algorithm: sha256 + file_format: regexp + pattern: + checksum: "^([0-9a-f]{64})" # ossein: microVM containers on macOS. The `docker` file puts a command named # docker on the hermetic PATH ahead of any Docker the machine has, on purpose, # for the repositories that pin it: build scripts type `docker`, and get @@ -656,35 +712,3 @@ packages: - linux/arm64 - windows/amd64 - windows/arm64 - # The YAML formatter: forkcloser/yamlfmt, upstream google/yamlfmt with its - # data-changing formats fixed (a placeholder leaked into output, blank lines - # in folded scalars): on the fleet's YAML the fork changes layout only. - # Release and verification in compress's shape; the binary at the archive - # root, .exe on windows. - - type: github_release - repo_owner: forkcloser - repo_name: yamlfmt - description: An extensible command line tool or library to format yaml files (forkcloser/yamlfmt) - asset: yamlfmt_{{trimV .Version}}_{{.OS}}_{{.Arch}}.tar.gz - format: tar.gz - files: - - name: yamlfmt - checksum: - type: github_release - asset: checksums.txt - algorithm: sha256 - cosign: - bundle: - type: github_release - asset: checksums.txt.sigstore.json - opts: - - --certificate-identity - - https://github.com/forkcloser/yamlfmt/.github/workflows/release.yaml@refs/tags/{{.Version}} - - --certificate-oidc-issuer - - https://token.actions.githubusercontent.com - supported_envs: - - darwin/arm64 - - linux/amd64 - - linux/arm64 - - windows/amd64 - - windows/arm64 diff --git a/.limen/hermetic.yaml b/.limen/hermetic.yaml index 7664fe2..9dce0ba 100644 --- a/.limen/hermetic.yaml +++ b/.limen/hermetic.yaml @@ -9,16 +9,21 @@ packages: # collapses a two-line `version:` pin): `aqua update` rewrites only that form # and treats a separate `version:` key as hand-held, and Renovate reads only # that form. - # No Go-built tool is pinned here: every one is a `tool` directive in - # .limen/tools/go.mod, built by the pinned go (book/tooling.md, "Go-built tools - # are go.mod tools"). aqua's go_install would build it once per tool - # version, with whichever go ran first, and share that binary. + # A pure-Go tool is a `tool` directive in .limen/tools/, built by the pinned + # go (book/tooling.md, "Go-built tools are go.mod tools"), never a pin here. + # The Go programs below are the ones nothing here compiles: what a write + # job runs (cosign and gh; goreleaser and syft in the release job, which + # holds contents, id-token and attestations write), each verified against + # its upstream's signed release rather than compiled inside that job; + # compress, whose programs answer to bare names on every PATH, a script's + # and a shell's; ossein, which links a system framework through cgo. + # (limen is the repository's own pin, above the import.) # The coreutils under their bare names, the same pin on every platform: # uutils on macOS and Linux, Git for Windows' MSYS2 coreutils on Windows. # The registry entries say why, and how each is verified. - name: uutils/coreutils@0.12.0 registry: local - - name: git-for-windows/git@v2.56.0.windows.2 + - name: git-for-windows/git@v2.56.0.windows.4 registry: local # --- binary-release tools (standard registry, aqua-verified) --- # Not here: golang/go. The Go toolchain is each repository's own pin, in its @@ -26,18 +31,17 @@ packages: # afterwards): a standard-library fix must not wait for a limen release. - name: casey/just@1.59.0 - name: koalaman/shellcheck@v0.11.0 - - name: forkcloser/yamlfmt@v0.2.0 - registry: local - name: lycheeverse/lychee@lychee-v0.24.2 - - name: goreleaser/goreleaser@v2.18.3 - - name: sigstore/cosign@v3.1.3 - - name: gotestyourself/gotestsum@v1.13.0 + - name: goreleaser/goreleaser@v2.20.0 + - name: sigstore/cosign@v3.1.4 + # The SBOM generator the release lane runs over every archive (book/releasing.md). + - name: anchore/syft@v1.57.0 - name: jqlang/jq@jq-1.8.2 - - name: cli/cli@v2.102.0 + - name: cli/cli@v2.103.3 # --- spec tooling (local registry; see .limen/hermetic-registry.yaml) --- - - name: quint-co/quint@v0.33.0 + - name: quint-co/quint@v0.33.1 registry: local - - name: forkcloser/curl@v8.22.0 + - name: forkcloser/curl@v8.23.0 registry: local # tar, gtar, xz, gzip, zstd, zip and unzip, one pure-Go build on every # platform, so a build script unpacks and packs with the same program @@ -46,5 +50,5 @@ packages: registry: local # darwin/arm64 only. Its `docker` link shadows the machine's Docker on the # hermetic PATH there; aqua skips the package everywhere else. - - name: farcloser/ossein@v0.3.3 + - name: farcloser/ossein@v0.3.4 registry: local diff --git a/.limen/just/apple.just b/.limen/just/apple.just new file mode 100644 index 0000000..1188c47 --- /dev/null +++ b/.limen/just/apple.just @@ -0,0 +1,137 @@ +# Apple signing, notarization and packaging, for a project that releases a +# bundle macOS evaluates (book/releasing.md, the Apple policy), under `just +# do` like release. The project keeps its build and composes its own +# `package ` and `verify-assets ` from these; the Apple +# release lane's sign job runs `package`, goreleaser's before hook runs +# `verify-assets`. +# +# The names are the lane's, exported by its sign job and read here, never the +# project's: APPLE_SIGN_IDENTITY (the identity's name, "Developer ID +# Application"), APPLE_SIGN_TEAM (the ten-character team id read off the +# certificate), APPLE_SIGNING_KEYCHAIN, APPLE_PROVISIONING_PROFILE_PATH, and +# for notarization APPLE_NOTARY_KEY (the .p8's path), APPLE_NOTARY_KEY_ID, +# APPLE_NOTARY_ISSUER_ID. Unset, every recipe treats the build as an ad hoc +# one: nothing to notarize, and an ad hoc signature accepted off the release +# tag (apple-verify-signed). + +# codesign's verdict on a bundle. On a release run the signature must be the +# Developer ID Application identity's, timestamped (a signature without a +# timestamp expires with the certificate); anywhere else an ad hoc signature +# is accepted as such. A release run is the sign job, which exports the +# identity, or the publish job, on the release tag (GITHUB_REF_TYPE); the +# verify lane builds the same bundle ad hoc on branches and pull requests, +# in GitHub Actions too, so "in Actions" is not the key. Notarization is a +# separate verdict (apple-verify-notarized): the policy signs every bundle +# and notarizes the ones a human installs. +[doc('Assert a bundle is signed: Developer ID and timestamped on a release run, ad hoc accepted elsewhere')] +apple-verify-signed bundle: + #!/usr/bin/env bash + set -euo pipefail + fail() { echo "apple-verify-signed: $*" >&2; exit 1; } + bundle='{{ bundle }}' + [ -e "$bundle" ] || fail "$bundle does not exist" + codesign --verify --deep --strict "$bundle" || fail "the code signature of $bundle does not verify" + details=$(codesign -d --verbose=2 "$bundle" 2>&1) + if printf '%s\n' "$details" | grep -q '^Authority=Developer ID Application'; then + printf '%s\n' "$details" | grep -q '^Timestamp=' || fail "the Developer ID signature of $bundle carries no timestamp" + echo "apple-verify-signed: $bundle is signed by Developer ID Application, timestamped" + else + if [ -n "${APPLE_SIGN_IDENTITY:-}" ] || [ "${GITHUB_REF_TYPE:-}" = tag ]; then + fail "$bundle is not Developer ID signed; a release run's bundle must be" + fi + echo "apple-verify-signed: $bundle is ad hoc signed (not a release run)" + fi + +# The stapled notarization ticket: Contents/CodeResources is where stapler +# writes it. `stapler validate` asks Apple's ticket service for the record, so +# a dropped connection fails it with nothing wrong in the bundle: it is tried +# three times before the bundle is refused. spctl has no assessment for a +# plug-in (its execute type answers for app bundles alone), so it is not +# consulted. +[doc('Assert a bundle carries a stapled notarization ticket that validates')] +apple-verify-notarized bundle: + #!/usr/bin/env bash + set -euo pipefail + fail() { echo "apple-verify-notarized: $*" >&2; exit 1; } + bundle='{{ bundle }}' + [ -f "$bundle/Contents/CodeResources" ] || fail "no notarization ticket is stapled to $bundle" + for attempt in 1 2 3; do + xcrun stapler validate "$bundle" && break + [ "$attempt" -lt 3 ] || fail "the ticket stapled to $bundle does not validate" + sleep $((attempt * 10)) + done + echo "apple-verify-notarized: $bundle carries a valid stapled ticket" + +# Submit the signed bundle to Apple and staple the ticket. The verdict is +# notarytool's own "status: Accepted", never an exit status. Without an +# identity (a local build) there is nothing to notarize and the recipe says +# so; with one and no credentials it fails, since a Developer ID build that +# leaves here unnotarized would be refused on every install. +[doc('Notarize a signed bundle with notarytool and staple the ticket')] +apple-notarize bundle: + #!/usr/bin/env bash + set -euo pipefail + bundle='{{ bundle }}' + if [ -z "${APPLE_SIGN_IDENTITY:-}" ]; then + echo "apple-notarize: no signing identity, $bundle is ad hoc signed — nothing to notarize" + exit 0 + fi + for v in APPLE_NOTARY_KEY APPLE_NOTARY_KEY_ID APPLE_NOTARY_ISSUER_ID; do + [ -n "${!v:-}" ] || { echo "apple-notarize: $v is not set; a Developer ID build must be notarized" >&2; exit 1; } + done + mkdir -p "$LIMEN_BUILD/notarize" + archive="$LIMEN_BUILD/notarize/$(basename "$bundle").zip" + rm -f "$archive" + ditto -c -k --sequesterRsrc --keepParent "$bundle" "$archive" + key=(--key "${APPLE_NOTARY_KEY:?}" --key-id "${APPLE_NOTARY_KEY_ID:?}" --issuer "${APPLE_NOTARY_ISSUER_ID:?}") + # Submit, wait and the verdict are separate calls, each retried on its + # own: a connection that drops during the wait loses the verdict on an + # upload Apple already holds. Only a submit that exits 0 has finished its + # upload, so a failed one is submitted again; once one has, its id is + # waited on and asked about, never submitted again. + for attempt in 1 2 3; do + submission=$(xcrun notarytool submit "$archive" --no-wait "${key[@]}" 2>&1 | tee /dev/stderr) && break + [ "$attempt" -lt 3 ] || { echo "apple-notarize: $archive could not be submitted" >&2; exit 1; } + sleep $((attempt * 15)) + done + id=$(printf '%s\n' "$submission" | sed -n 's/^ *id: \([0-9a-f-]*\)$/\1/p' | head -n 1) + [ -n "$id" ] || { echo "apple-notarize: notarytool submit returned no submission id" >&2; exit 1; } + # A "status:" line is the end of processing, whatever wait exits with; + # anything else is a wait cut short, resumed on the same id. + for attempt in 1 2 3 4 5; do + waited=$(xcrun notarytool wait "$id" "${key[@]}" 2>&1 | tee /dev/stderr) && break + printf '%s\n' "$waited" | grep -q '^ *status: ' && break + [ "$attempt" -lt 5 ] || { echo "apple-notarize: submission $id did not finish processing" >&2; exit 1; } + sleep $((attempt * 15)) + done + for attempt in 1 2 3; do + info=$(xcrun notarytool info "$id" "${key[@]}" 2>&1 | tee /dev/stderr) && break + [ "$attempt" -lt 3 ] || { echo "apple-notarize: the status of submission $id could not be read" >&2; exit 1; } + sleep $((attempt * 10)) + done + if ! printf '%s\n' "$info" | grep -q '^ *status: Accepted$'; then + echo "apple-notarize: Apple did not accept the submission $id" >&2 + xcrun notarytool log "$id" "${key[@]}" >&2 || true + exit 1 + fi + # staple fetches the ticket from Apple's ticket service and validates + # what it writes: a connection that drops on the runner fails it with the + # submission accepted, so it is tried five times, further apart each time, + # before the release stops. + for attempt in 1 2 3 4 5; do + xcrun stapler staple "$bundle" && break + [ "$attempt" -lt 5 ] || { echo "apple-notarize: the ticket for submission $id could not be stapled" >&2; exit 1; } + sleep $((attempt * 15)) + done + echo "apple-notarize: $bundle notarized (submission $id), ticket stapled" + +# ditto, not zip: the archiver that keeps a bundle's extended attributes and +# code signature intact, so what a user unzips is what was signed. +[doc('Zip a bundle with ditto, keeping its signature intact')] +apple-package bundle zip: + #!/usr/bin/env bash + set -euo pipefail + mkdir -p "$(dirname '{{ zip }}')" + rm -f '{{ zip }}' + ditto -c -k --sequesterRsrc --keepParent '{{ bundle }}' '{{ zip }}' + echo "apple-package: {{ zip }}" diff --git a/.limen/just/build-go.just b/.limen/just/build-go.just index 00c38da..682e0ce 100644 --- a/.limen/just/build-go.just +++ b/.limen/just/build-go.just @@ -21,8 +21,11 @@ import 'lib.just' # 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' +# out of glibc, which is exactly what makes `static` viable there. Linux only: +# on macOS libc is the one road to Open Directory users (the pure-Go os/user +# reads /etc/passwd alone, so user.Lookup of a real account fails) and to +# scoped DNS, and there is no static libc to keep clear of. +go_tags := if os() == 'linux' { 'netgo,osusergo' } else { '' } # CGO is off unless a project exports GO_CGO := '1' (or CGO_ENABLED=1 for one # run); resolution in lib.just. @@ -65,9 +68,11 @@ release: (_banner "build go" "release") extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" mkdir -p "$LIMEN_BUILD" + built=0 for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue + built=1 echo "→ $LIMEN_BUILD/${name}" # A command that is a module of its own (cmd//go.mod) builds # from inside it: from the root, go refuses a package outside the main @@ -77,6 +82,7 @@ release: (_banner "build go" "release") go -C "${src}" build -trimpath -buildmode=pie -tags='{{ go_tags }}' \ -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}" "${pkg}" done + [ "$built" = 1 ] || echo "no main package under cmd/ — nothing to build (skipped, not failed)." # Debug: optimizations and inlining off, symbols kept, real paths kept — no # trimpath, deliberately: debuggers want actual file locations. -debug suffix. @@ -96,15 +102,18 @@ debug: (_banner "build go" "debug") extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" mkdir -p "$LIMEN_BUILD" + built=0 for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue + built=1 echo "→ $LIMEN_BUILD/${name}-debug" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } go -C "${src}" build -buildmode=pie -gcflags='all=-N -l' -tags='{{ go_tags }}' \ -ldflags "${ldflags}" ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-debug" "${pkg}" done + [ "$built" = 1 ] || echo "no main package under cmd/ — nothing to build (skipped, not failed)." # Race: a diagnostic build with the race detector, which requires cgo (forced # here) and, matching the test module, an external linkmode. Unstripped, real @@ -118,9 +127,11 @@ race: (_banner "build go" "race") extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" mkdir -p "$LIMEN_BUILD" + built=0 for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue + built=1 echo "→ $LIMEN_BUILD/${name}-race" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } @@ -128,6 +139,7 @@ race: (_banner "build go" "race") -ldflags "${ldflags}" \ ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-race" "${pkg}" done + [ "$built" = 1 ] || echo "no main package under cmd/ — nothing to build (skipped, not failed)." # Static: fully static external link. Preconditions enforced, not commented: # Linux only (macOS has no static libc), and CGO_ENABLED=1 (with CGO off, @@ -153,9 +165,11 @@ static: (_banner "build go" "static") extra=() [ -z "${BUILD_GO_FLAGS:-}" ] || read -ra extra <<<"${BUILD_GO_FLAGS}" mkdir -p "$LIMEN_BUILD" + built=0 for dir in cmd/*/; do name=$(basename "${dir}") ls "${dir}"*.go >/dev/null 2>&1 || continue + built=1 echo "→ $LIMEN_BUILD/${name}-static" src=. pkg="./${dir%/}" [ ! -f "${dir}go.mod" ] || { src="${dir%/}"; pkg=.; } @@ -163,3 +177,4 @@ static: (_banner "build go" "static") -ldflags "${ldflags}" \ ${extra[@]+"${extra[@]}"} -o "$LIMEN_BUILD/${name}-static" "${pkg}" done + [ "$built" = 1 ] || echo "no main package under cmd/ — nothing to build (skipped, not failed)." diff --git a/.limen/just/build-rust.just b/.limen/just/build-rust.just new file mode 100644 index 0000000..8ec108d --- /dev/null +++ b/.limen/just/build-rust.just @@ -0,0 +1,74 @@ +# 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: release + +# The crate's release binaries for this host, into target/release/ (cargo's own +# layout; the pinned toolchain through rust_env in lib.just). --locked: +# Cargo.lock is the crate pin. +release: (_banner "build rust" "release") + #!/usr/bin/env bash + set -euo pipefail + if [ ! -f Cargo.toml ]; then + echo "no Cargo.toml — not a Rust crate, nothing to build (skipped, not failed)." + exit 0 + fi + {{ rust_env }} + cargo build --release --locked + +# Release binaries for other targets, each into target//release/, +# linked through the project's pinned zig (ziglang/zig in +# .hermetic/hermetic.yaml) by the project's pinned cargo-zigbuild +# (rust-cross/cargo-zigbuild): one C toolchain for the C and the Rust builds, +# and a macOS or Windows target links from any host (zig ships the libSystem +# stubs and the MinGW-w64 CRT). cargo-zigbuild is what maps a triple to a zig +# target, hands cargo and the cc crate the linker, and drops or rewrites the +# arguments rustc means for GNU ld that zig's lld refuses, a list that moves +# with rustc and zig: a wrapper of our own would be a copy of it, behind. Each +# target's rust-std comes from rust-toolchain.toml's `targets`, fetched by +# rustup and verified like the rest. The triples reach the script as +# arguments, never interpolated into it. +[positional-arguments] +targets +triples: (_banner "build rust" "targets") + #!/usr/bin/env bash + set -euo pipefail + if [ ! -f Cargo.toml ]; then + echo "no Cargo.toml — not a Rust crate, nothing to build (skipped, not failed)." + exit 0 + fi + for tool in zig cargo-zigbuild; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "$tool is not on the hermetic PATH: cross builds link through the project's pinned zig and cargo-zigbuild (ziglang/zig and rust-cross/cargo-zigbuild in .hermetic/hermetic.yaml)." >&2 + exit 1 + fi + done + {{ rust_env }} + # Every cache under .build/: zig's two, and cargo-zigbuild's own (the libc + # and SDK stubs it generates), which otherwise lands in the user's cache + # directory. + export ZIG_GLOBAL_CACHE_DIR="$LIMEN_BUILD/zig/global" ZIG_LOCAL_CACHE_DIR="$LIMEN_BUILD/zig/local" + export CARGO_ZIGBUILD_CACHE_DIR="$LIMEN_BUILD/zig/cargo-zigbuild" + # For the host's own triple, cargo-zigbuild sets target-applies-to-host to + # false: build scripts and proc-macros stop reading [target.] and + # read [host] instead. The linker rust_env gave the host (the arm64 + # Windows one, which ships none) goes to [host] for that build. + host="$(rustc -vV | sed -n 's/^host: //p')" + host_linker_var="CARGO_TARGET_$(printf '%s' "$host" | tr 'a-z-' 'A-Z_')_LINKER" + host_linker="${!host_linker_var:-}" + for triple in "$@"; do + echo "→ target/$triple/release" + if [ "$triple" = "$host" ] && [ -n "$host_linker" ]; then + CARGO_UNSTABLE_HOST_CONFIG=true CARGO_HOST_LINKER="$host_linker" cargo zigbuild --release --locked --target "$triple" + else + cargo zigbuild --release --locked --target "$triple" + fi + done diff --git a/.limen/just/build.just b/.limen/just/build.just index 1af18b4..cf61023 100644 --- a/.limen/just/build.just +++ b/.limen/just/build.just @@ -11,6 +11,10 @@ set quiet # binaries, `just do build go ` (debug, race, static) makes a variant. mod go 'build-go.just' +# Rust builds: `just do build rust` makes the host's release binaries, +# `just do build rust targets …` the cross builds through zig. +mod rust 'build-rust.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 index 5749272..bab9332 100644 --- a/.limen/just/do.just +++ b/.limen/just/do.just @@ -25,3 +25,8 @@ mod perf 'perf.just' # Flat (imported, not a module) so it takes arguments: `just do release v1.2.3`. import 'release.just' + +# Flat too, for the same reason: the Apple recipes take a bundle and a zip +# (`just do apple-notarize `), composed by a project's own package +# recipe (apple.just). +import 'apple.just' diff --git a/.limen/just/fix-rust.just b/.limen/just/fix-rust.just index b2ae977..0b7cb23 100644 --- a/.limen/just/fix-rust.just +++ b/.limen/just/fix-rust.just @@ -10,6 +10,23 @@ set quiet # Shared private _banner recipe (see lib.just). import 'lib.just' +default: rust + +# The mutating twins of `just do lint rust`, from the same pinned toolchain. rust: (_banner "fix" "rust") - cargo clippy --fix --all-targets --all-features --allow-dirty --allow-staged + #!/usr/bin/env bash + set -euo pipefail + if [ ! -f Cargo.toml ]; then + echo "no Cargo.toml — not a Rust crate, nothing to fix (skipped, not failed)." + exit 0 + fi + {{ rust_env }} + # Under the sessions' sandbox, with the human's HOME: cargo fix discovers + # the repository in-process (not through git, so GIT_CONFIG_GLOBAL is not + # read) and opens the home's git configuration on the way, which the + # sandbox denies, and the discovery fails as "no VCS found". The home + # swap the hermetic recipes already use (agent_home) gives it a home with + # nothing to read; CARGO_HOME and RUSTUP_HOME are explicit above, so the + # swap changes nothing else. + {{ agent_home }} cargo clippy --fix --all-targets --all-features --locked --allow-dirty --allow-staged cargo fmt --all diff --git a/.limen/just/fix.just b/.limen/just/fix.just index bf7bf35..aabc85f 100644 --- a/.limen/just/fix.just +++ b/.limen/just/fix.just @@ -36,8 +36,8 @@ just: (_banner "fix" "just") just --fmt --justfile "$f" done < "$list" -yaml: (_banner "fix" "yaml") - yamlfmt -conf .limen/.yamlfmt +yaml: (_banner "fix" "yaml") (_go-tool "yamlfmt" "github.com/forkcloser/yamlfmt/cmd/yamlfmt") + "$LIMEN_BUILD/tools/yamlfmt" -conf .limen/.yamlfmt # Regenerate .hermetic/aqua-checksums.json from .hermetic/hermetic.yaml — the # mutating twin of `just do lint aqua` (needs network). In the default set so diff --git a/.limen/just/lib.just b/.limen/just/lib.just index b2ebec3..1c29104 100644 --- a/.limen/just/lib.just +++ b/.limen/just/lib.just @@ -66,6 +66,75 @@ _per-platform +cmd: # exists, `net` alone would call one to build them. go_tool_build := 'mkdir -p "$LIMEN_BUILD/tools" && GOOS="" GOARCH="" CGO_ENABLED=0 go -C .limen/tools build -o "$LIMEN_BUILD/tools/"' +# cargo through the pinned rustup, never ~/.cargo/bin (book/rust.md). The +# pin is rust-lang/rustup in the project's .hermetic/hermetic.yaml, which +# links rustup-init; the first lane installs rustup from it under +# $LIMEN_BUILD with no toolchain, and the toolchain is rust-toolchain.toml's, +# fetched by the first cargo call and verified against the release manifest. +# Windows: rustup wants native paths for its two homes, hence cygpath, and +# hermetic's link resolves rustup-init to the .exe. Injected as a just +# variable: module recipes cannot share shell functions. Every Rust lane +# passes vacuously without a Cargo.toml, so a module can sit in a default. +rust_env := ''' + export RUSTUP_HOME="$LIMEN_BUILD/rustup" CARGO_HOME="$LIMEN_BUILD/cargo" + if command -v cygpath >/dev/null 2>&1; then + RUSTUP_HOME="$(cygpath -m "$RUSTUP_HOME")"; CARGO_HOME="$(cygpath -m "$CARGO_HOME")" + fi + export RUSTUP_INIT_SKIP_PATH_CHECK=yes + if [ ! -f rust-toolchain.toml ]; then + echo "rust-toolchain.toml is missing: the toolchain is pinned there, an exact version with its components and targets (book/rust.md)." >&2 + exit 1 + fi + if [ ! -x "$LIMEN_BUILD/cargo/bin/cargo" ] && [ ! -x "$LIMEN_BUILD/cargo/bin/cargo.exe" ]; then + if ! command -v rustup-init >/dev/null 2>&1; then + echo "rustup-init is not on the hermetic PATH: pin rust-lang/rustup in .hermetic/hermetic.yaml (just do tools add local,rust-lang/rustup , book/rust.md)." >&2 + exit 1 + fi + # stdout is rustup-init's welcome ("Rust is installed now. Great!", the + # PATH advice): nothing a lane reports; its errors stay on stderr. + rustup-init -y -q --no-modify-path --no-update-default-toolchain --default-toolchain none --profile minimal >/dev/null + fi + export PATH="$LIMEN_BUILD/cargo/bin:$PATH" + # Windows: rustup's default host is the msvc triple, whose host builds + # (build scripts, proc-macros, the tests) link through the runner image's + # Visual Studio, which nothing pins. The GNU triples link through pinned + # tools instead, so the pinned toolchain is the whole build: x64 through + # the toolchain's own rust-mingw; arm64, whose gnullvm host ships no + # linker (it expects a MinGW clang on PATH), through the pinned zig by + # way of cargo-zigbuild's `zig cc`, the linker the cross builds use, + # which maps the libraries rustc asks for (msvcrt) to zig's. Read from + # rustup rather than from uname or the environment, which lie under + # emulation on arm64 (book/windows.md); idempotent, so a cached rustup + # from before this line is moved too. + case "$(rustup show 2>/dev/null | sed -n 's/^Default host: //p')" in + x86_64-pc-windows-msvc) rustup set default-host x86_64-pc-windows-gnu >/dev/null ;; + aarch64-pc-windows-msvc) rustup set default-host aarch64-pc-windows-gnullvm >/dev/null ;; + esac + if [ "$(rustup show 2>/dev/null | sed -n 's/^Default host: //p')" = aarch64-pc-windows-gnullvm ]; then + for tool in zig cargo-zigbuild; do + if ! command -v "$tool" >/dev/null 2>&1; then + echo "$tool is not on the hermetic PATH: the aarch64 Windows host links through the project's pinned zig and cargo-zigbuild (ziglang/zig and rust-cross/cargo-zigbuild in .hermetic/hermetic.yaml)." >&2 + exit 1 + fi + done + mkdir -p "$LIMEN_BUILD/zig" + linker="$LIMEN_BUILD/zig/aarch64-pc-windows-gnullvm-linker.cmd" + printf '@echo off\r\nsetlocal DisableDelayedExpansion\r\n"%s" zig cc -- -target aarch64-windows-gnu %%*\r\n' "$(cygpath -w "$(command -v cargo-zigbuild)")" > "$linker" + linker_native="$(cygpath -m "$linker")" + export CARGO_TARGET_AARCH64_PC_WINDOWS_GNULLVM_LINKER="$linker_native" + export ZIG_GLOBAL_CACHE_DIR="$LIMEN_BUILD/zig/global" ZIG_LOCAL_CACHE_DIR="$LIMEN_BUILD/zig/local" + export CARGO_ZIGBUILD_CACHE_DIR="$LIMEN_BUILD/zig/cargo-zigbuild" + fi + # The toolchain rust-toolchain.toml names, installed explicitly when the + # file's toolchain is not there yet (a first run, a channel bump): the + # proxies would auto-install it under RUSTUP_AUTO_INSTALL, with a warning + # on every first lane, and a lane runs no step it did not state. + if ! RUSTUP_AUTO_INSTALL=0 rustup which cargo >/dev/null 2>&1; then + # --no-self-update: a pinned rustup asks nobody for a newer one. + rustup toolchain install --no-self-update + fi +''' + # Every Go-built tool the recipes run — the Go-source analyzers and the rest # alike — is a `tool` directive in a module under .limen/tools/, built natively ONCE # into .build/tools/ by the pinned go (book/tooling.md, "Go-built tools are @@ -76,10 +145,15 @@ go_tool_build := 'mkdir -p "$LIMEN_BUILD/tools" && GOOS="" GOARCH="" CGO_ENABLED # dependencies past what upstream tested). The isolated module wins when both # exist. A directive missing from both fails with the recipe that adds it. # Recipes depend on this with the tool's name and package, then call -# .build/tools/. -_go-tool name pkg: +# .build/tools/. A third argument names a file the recipe needs to run +# at all (go.mod for a lane CI runs everywhere): without it the tool is not +# built, so a repository with no Go module is not asked for the directive. +_go-tool name pkg requires='': #!/usr/bin/env bash set -euo pipefail + if [ -n '{{ requires }}' ] && [ ! -f '{{ requires }}' ]; then + exit 0 + fi pattern='^(tool[[:space:]]+)?[[:space:]]*{{ pkg }}([[:space:]]|$)' if grep -qE "$pattern" '.limen/tools/{{ name }}/go.mod' 2>/dev/null; then mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C '.limen/tools/{{ name }}' build -o "$LIMEN_BUILD/tools/" '{{ pkg }}' diff --git a/.limen/just/lint-homebrew.just b/.limen/just/lint-homebrew.just index f2a5f50..aa3f5a0 100644 --- a/.limen/just/lint-homebrew.just +++ b/.limen/just/lint-homebrew.just @@ -14,9 +14,12 @@ import 'lib.just' # RuboCop would not know the formula cops. brew is not on the hermetic PATH on # purpose — it is the subject under test, not a substitute for a pinned tool; # BREW_BIN is captured from the ambient PATH in main.just. `style` (and its -# fixing twin) may run from another brew than the machine's: BREW_STYLE_BIN, -# exported by the project's .justfile, defaults to BREW_BIN and leaves `audit` -# and `test` on the machine's, which installs and audits for real. +# fixing twin) and `audit` may each run from another brew than the machine's: +# BREW_STYLE_BIN and BREW_AUDIT_BIN, exported by the project's .justfile, +# default to BREW_BIN. The machine's brew cannot run under a session sandbox +# (it writes under its own prefix and caches) and refuses a tap it already has +# installed, so without them `audit` is CI-only in a tap repository. `test` +# stays on the machine's, which installs for real. default: style audit @@ -89,9 +92,30 @@ audit: (_banner "lint homebrew" "audit") exit 0 fi # shellcheck disable=SC2154 # BREW_BIN is exported by the canonical .justfile (main.just). - brew="${BREW_BIN:-}" + brew="${BREW_AUDIT_BIN:-${BREW_BIN:-}}" {{ _require_brew }} {{ _tap_link }} + # brew loads a non-official tap's formulas only once the tap is trusted + # (Homebrew 6+, docs/Tap-Trust.md); `--tap` allows nothing for audit, and + # HOMEBREW_NO_REQUIRE_TAP_TRUST is deprecated. Whole-tap trust, since the + # audit judges every formula of a tap its author administers; granted for + # the run and withdrawn at exit when this run granted it, so the user's + # trust store is left as it was. A brew without the command (pre-6) needs + # no trust and skips this. `style` has no need: RuboCop loads no formula. + trusted="" + # shellcheck disable=SC2154 # $tap is assigned by the _tap_link snippet above, which the lint lane's dump does not expand. + if out="$("$brew" trust "$tap" 2>/dev/null)"; then + case "$out" in + "Trusted tap: "*) trusted="$tap"; echo "trusted tap $tap for this run" ;; + esac + fi + cleanup_trust() { + if [ -n "$trusted" ]; then + "$brew" untrust "$trusted" >/dev/null 2>&1 || true + fi + cleanup + } + trap cleanup_trust EXIT # shellcheck disable=SC2086 # deliberate word-split of the flags knob. # shellcheck disable=SC2154 # $tap is assigned by the _tap_link snippet above, which the lint lane's dump does not expand. "$brew" audit --strict --tap "$tap" ${LINT_HOMEBREW_AUDIT_FLAGS:-} @@ -108,8 +132,8 @@ _skip_unless_macos := ''' ''' # Refuses when the brew the recipe chose ($brew) is empty: main.just's ambient -# capture came up empty and the project exported no BREW_STYLE_BIN. Twin in -# fix-homebrew.just. +# capture came up empty and the project exported no BREW_STYLE_BIN or +# BREW_AUDIT_BIN. Twin in fix-homebrew.just. [private] _require_brew := ''' if [ -z "${brew:-}" ]; then diff --git a/.limen/just/lint-rust.just b/.limen/just/lint-rust.just index 84e3b4b..08c1b65 100644 --- a/.limen/just/lint-rust.just +++ b/.limen/just/lint-rust.just @@ -10,6 +10,18 @@ set quiet # Shared private _banner recipe (see lib.just). import 'lib.just' +default: rust + +# rustfmt and clippy from the pinned toolchain (rust_env in lib.just): both +# are components rust-toolchain.toml names. --locked: Cargo.lock is the crate +# pin, and a lane never rewrites it. rust: (_banner "lint" "rust") + #!/usr/bin/env bash + set -euo pipefail + if [ ! -f Cargo.toml ]; then + echo "no Cargo.toml — not a Rust crate, nothing to lint (skipped, not failed)." + exit 0 + fi + {{ rust_env }} cargo fmt --all --check - cargo clippy --all-targets --all-features -- --deny warnings + cargo clippy --all-targets --all-features --locked -- --deny warnings diff --git a/.limen/just/lint.just b/.limen/just/lint.just index 8ea40f5..cab92d9 100644 --- a/.limen/just/lint.just +++ b/.limen/just/lint.just @@ -79,8 +79,8 @@ dockerfile: (_banner "lint" "dockerfile") (_go-tool "godolint" "github.com/forkc # pinned one, here. [ ${#files[@]} -eq 0 ] || "$LIMEN_BUILD/tools/godolint" "${files[@]}" -yaml: (_banner "lint" "yaml") - yamlfmt -conf .limen/.yamlfmt -lint +yaml: (_banner "lint" "yaml") (_go-tool "yamlfmt" "github.com/forkcloser/yamlfmt/cmd/yamlfmt") + "$LIMEN_BUILD/tools/yamlfmt" -conf .limen/.yamlfmt -lint shell: (_banner "lint" "shell") #!/usr/bin/env bash diff --git a/.limen/just/perf-go.just b/.limen/just/perf-go.just index b2d6726..46fcbcd 100644 --- a/.limen/just/perf-go.just +++ b/.limen/just/perf-go.just @@ -113,14 +113,10 @@ profile: (_banner "perf go" "profile") (_go-tool "dot" "github.com/forkcloser/do docs="$LIMEN_BUILD/profiles-docs" mkdir -p "$prof" "$docs" # Rendered from pprof's -dot text rather than pprof's own -png, which - # shells out to a dot it expects on PATH. -o with a pre-created file: 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). + # shells out to a dot it expects on PATH. render() { src=$1; png=$2; shift 2 go tool pprof -dot -nodecount=20 "$@" "$src" > "$png.dot" 2>/dev/null - : > "$png" "$LIMEN_BUILD/tools/dot" -Tpng -o "$png" "$png.dot" rm -f "$png.dot" echo " -> $png" @@ -128,7 +124,7 @@ profile: (_banner "perf go" "profile") (_go-tool "dot" "github.com/forkcloser/do for pkg in $(go list ./...); do # One directory per package keyed by the FULL import path: flattening # collides (example/a__b vs example/a/b, '_' being legal in import - # paths). Paths stay cwd-relative for the WASM dot (see render). + # paths). mkdir -p "$prof/$pkg" "$docs/$pkg" echo "Profiling $pkg..." go test -count=1 -o "$prof/$pkg/pkg.test" "$pkg" \ diff --git a/.limen/just/release.just b/.limen/just/release.just index 34f948c..b99bb4a 100644 --- a/.limen/just/release.just +++ b/.limen/just/release.just @@ -9,19 +9,24 @@ # Lanes: `vX.Y.Z` cuts the signed tag and pushes (CI builds, signs keyless, # publishes; without a .release-go.yaml, the recipe creates the release page -# itself); `--local vX.Y.Z` is the fully local, key-based lane and the -# sole publisher; `--local --dry-run` is an unsigned snapshot; `--ci` is the -# workflow half. usage() below and book/recipes.md have the details. +# itself); `--local --dry-run` is an unsigned snapshot; `--ci` is the +# workflow half. usage() below and book/releasing.md have the details. There +# is no key-based lane: a release is signed keyless in CI or not at all +# (book/releasing.md, "The signing and provenance policy"). # # Tag pushes are exactly one tag, never --tags. A signed tag already on HEAD is # reused, so a failed publish retries safely. -[doc('Release: `vX.Y.Z` signed tag (+ CI artifacts when .release-go.yaml exists) · `--local vX.Y.Z` · `--local --dry-run` · `--ci`')] +# The arguments arrive as the script's own "$@" (positional-arguments), never +# interpolated into its text: a `{{ args }}` would run a `;` in an argument as +# a command before the tag below is validated. +[doc('Release: `vX.Y.Z` signed tag (+ CI artifacts when .release-go.yaml exists) · `--local --dry-run` · `--ci`')] +[positional-arguments] release *args: #!/usr/bin/env bash set -euo pipefail # Artifacts are opt-in by carrying a goreleaser config. Without one the # default lane still releases: the signed tag IS the release (a Go module, a - # tap, a configuration). --local and --ci run goreleaser, so they require it. + # tap, a configuration). --local --dry-run and --ci run goreleaser, so they require it. has_goreleaser() { [ -f .release-go.yaml ]; } require_goreleaser() { if ! has_goreleaser; then @@ -31,7 +36,6 @@ release *args: } usage() { echo "usage: just do release # signed tag + push; CI builds, signs (keyless), publishes (no .release-go.yaml: the release page, with notes)" >&2 - echo " just do release --local [--cosign-password-stdin] # 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 } @@ -46,9 +50,13 @@ release *args: } # A fourth segment is a fork's rebuild of the same upstream version # (v10.6.1.2, book/forks.md): never a -suffix, which semver sorts before - # the release it rebuilds. + # the release it rebuilds. A rebuild carries no prerelease of its own: + # goreleaser reads no semver from it (--ci below), so a suffix would be + # published as a full release. + rebuild_tag_re='^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' require_version_tag() { - if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z][0-9A-Za-z.-]*)?$'; then + if ! printf '%s' "$1" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$' && + ! printf '%s' "$1" | grep -qE "$rebuild_tag_re"; then echo "'$1' is not a version tag (vX.Y.Z, vX.Y.Z-prerelease, or a fork's rebuild vX.Y.Z.N)." >&2 exit 2 fi @@ -116,96 +124,17 @@ release *args: git push origin HEAD git push origin "refs/tags/$1" } - set -- {{ args }} case "${1:-}" in --local) shift require_goreleaser if [ "${1:-}" = "--dry-run" ]; then - # Snapshot builds skip signing: no key material needed. + # Snapshot builds skip signing: no key material needed. The + # SBOMs are still made, so a dry run proves the whole lane. exec goreleaser release --config .release-go.yaml --snapshot --clean --skip=sign fi - key="${1:-}" - if [ -z "$key" ]; then - usage - exit 2 - fi - shift - tag="" - password="" - password_from_stdin="" - while [ $# -gt 0 ]; do - case "$1" in - --cosign-password-stdin) - # A boolean flag, NOT a value: the passphrase is read from - # stdin below, so it never enters argv or shell history. - password_from_stdin=1 - shift - ;; - -*) - 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 - require_empty_queue - if [ -z "${GITHUB_TOKEN:-}" ]; then - echo "GITHUB_TOKEN is not set — goreleaser needs it to publish the GitHub release." >&2 - exit 1 - fi - # cosign under goreleaser cannot prompt, so the passphrase travels as - # COSIGN_PASSWORD: read once here, never through argv. Empty is legal - # (unencrypted key). - if [ -n "$password_from_stdin" ]; then - # --cosign-password-stdin: the passphrase is piped in, so `cat` - # would echo it in cleartext if stdin were the terminal. Refuse - # that footgun and point at the two safe forms. - if [ -t 0 ]; then - echo "--cosign-password-stdin expects the passphrase piped on stdin, but stdin is a terminal." >&2 - echo "pipe it (e.g. 'pass show cosign | just do release --local $key $tag --cosign-password-stdin'), or drop the flag to be prompted." >&2 - exit 2 - fi - # Read ALL of stdin; command substitution strips trailing - # newlines, so a here-string, a `printf | ` pipe, and a file with - # or without a trailing newline all yield the same passphrase. - password=$(cat) - else - # No stdin channel: prompt on the terminal itself (/dev/tty, never - # fd 0 — goreleaser may want stdin, and fd 0 may be redirected). - printf 'cosign key password (empty for an unencrypted key): ' >&2 - read -rs password < /dev/tty - echo >&2 - fi - export COSIGN_KEY="$key" - export COSIGN_PASSWORD="$password" - # Sole publisher: push the commit, never the tag. goreleaser creates the - # release and the remote tag through the API, and with no v* tag push - # the release workflow never fires alongside it. The signed tag stays - # local. - ensure_signed_tag "$tag" - git push origin HEAD - exec goreleaser release --config .release-go.yaml --clean + usage + exit 2 ;; --ci) # The workflow half: the tag exists (its push triggered this run, @@ -229,10 +158,22 @@ release *args: 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 --config .release-go.yaml --clean + # goreleaser refuses a tag that is not semver, and a fork's + # rebuild tag is not: --skip=validate is its one way past, and it + # also drops goreleaser's dirty-tree and tag-at-HEAD checks, so + # the two are made here first, on the tag this run was pushed for. + opts=() + tag="${GITHUB_REF_NAME:-}" + if printf '%s' "$tag" | grep -qE "$rebuild_tag_re"; then + require_clean + if ! git describe --exact-match --tags --match "$tag" HEAD >/dev/null 2>&1; then + echo "$tag is not on HEAD — this lane releases the tag its run was pushed for." >&2 + exit 1 + fi + export GORELEASER_CURRENT_TAG="$tag" + opts+=(--skip=validate) + fi + exec goreleaser release --config .release-go.yaml --clean ${opts[@]+"${opts[@]}"} ;; --dry-run) echo "dry runs are a local concern: use 'just do release --local --dry-run'." >&2 @@ -296,3 +237,24 @@ release *args: fi ;; esac + +# goreleaser's sign command (.release-go.yaml `signs:`): one keyless +# signature over a release file, in CI. cosign gives up on Fulcio at the first +# dropped connection and retries Rekor once, so a reset on the runner fails +# the release with nothing wrong in it. Every attempt is a fresh key, a fresh +# certificate and a new log entry, so one that failed halfway cannot collide +# with the next; its partial bundle is removed first. +[doc("Sign a release file keyless with cosign into a Sigstore bundle (goreleaser's sign command)")] +[positional-arguments] +release-sign file bundle: + #!/usr/bin/env bash + set -euo pipefail + file="$1" bundle="$2" + for attempt in 1 2 3; do + rm -f "$bundle" + if cosign sign-blob --yes --bundle "$bundle" "$file" && [ -s "$bundle" ]; then + exit 0 + fi + [ "$attempt" -lt 3 ] || { echo "release-sign: $file could not be signed" >&2; exit 1; } + sleep $((attempt * 15)) + done diff --git a/.limen/just/test-go.just b/.limen/just/test-go.just index 51118f6..25233b5 100644 --- a/.limen/just/test-go.just +++ b/.limen/just/test-go.just @@ -23,10 +23,24 @@ default: unit # CGO_ENABLED resolves through lib.just's go_cgo, so a cgo project tests the # artifact it ships; `race` forces 1 (the detector injects runtime/cgo). +# A compile that needs a generated input (an embedded build product, codegen) +# gets it from the project's `_test-prepare` recipe in the root .justfile, +# run before every shared Go test recipe when it exists: the CI lanes call +# these recipes directly (`just do test go race`), never the project's `test` +# aggregate, so the aggregate's own prerequisites would not run there. A root +# .justfile without the recipe is unchanged. `just --show` sees a private +# recipe where `--summary` does not. +_prepare: + #!/usr/bin/env bash + set -euo pipefail + if just --show _test-prepare >/dev/null 2>&1; then + just _test-prepare + fi + # Unit tests. -unit: (_banner "test go" "unit") +unit: (_banner "test go" "unit") _prepare (_go-tool "gotestsum" "gotest.tools/gotestsum") CGO_ENABLED="{{ go_cgo }}" \ - gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" ./... + "$LIMEN_BUILD/tools/gotestsum" -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" ./... # Unit tests under the race detector. External linkmode: the detector injects # runtime/cgo, and hardened CGO_CFLAGS make those objects reference libc @@ -38,10 +52,16 @@ unit: (_banner "test go" "unit") # windows/arm64 among others. Ask the toolchain rather than keep a platform # list: a probe build of `unsafe` answers in milliseconds. Only the exact # "-race is not supported" refusal skips — loudly, and passes, since the other -# legs run the detector natively; any other probe failure is real. -race: (_banner "test go" "race") +# legs run the detector natively; any other probe failure is real. No go.mod +# at all: says so and passes, like fuzz, since CI runs this in every +# repository. +race: (_banner "test go" "race") _prepare (_go-tool "gotestsum" "gotest.tools/gotestsum" "go.mod") #!/usr/bin/env bash set -euo pipefail + if [ ! -f go.mod ]; then + echo "no go.mod — not a Go module, nothing to run under the detector (skipped, not failed)." + exit 0 + fi export CGO_ENABLED=1 if ! probe="$(go build -race -o /dev/null unsafe 2>&1)"; then if printf '%s\n' "$probe" | grep -q -- '-race is not supported'; then @@ -51,7 +71,7 @@ race: (_banner "test go" "race") printf '%s\n' "$probe" >&2 exit 1 fi - gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -ldflags=-linkmode=external -race ./... + "$LIMEN_BUILD/tools/gotestsum" -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -ldflags=-linkmode=external -race ./... # Every Fuzz* target for a short budget (TEST_GO_FUZZ_TIME, default 10s). # Driven by `go test -fuzz`, one target per invocation — only that build @@ -63,7 +83,7 @@ race: (_banner "test go" "race") # "context deadline exceeded" as fuzztime expires mid-iteration, so exit 1 # without a new file is retried once. No Fuzz* targets, or no go.mod at all: # says so and passes. -fuzz: (_banner "test go" "fuzz") +fuzz: (_banner "test go" "fuzz") _prepare #!/usr/bin/env bash set -euo pipefail if [ ! -f go.mod ]; then @@ -116,13 +136,13 @@ fuzz: (_banner "test go" "fuzz") # 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") +cover: (_banner "test go" "cover") _prepare (_go-tool "gotestsum" "gotest.tools/gotestsum") #!/usr/bin/env bash set -euo pipefail export CGO_ENABLED="{{ go_cgo }}" dir="$LIMEN_BUILD/coverage" mkdir -p "$dir" - gotestsum -- -count=1 -timeout "${TEST_GO_TIMEOUT:-10m}" -coverprofile="$dir/coverage.out" ./... + "$LIMEN_BUILD/tools/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" diff --git a/.limen/just/test-rust.just b/.limen/just/test-rust.just new file mode 100644 index 0000000..c5ce7ac --- /dev/null +++ b/.limen/just/test-rust.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 + +# Shared private _banner recipe (see lib.just). +import 'lib.just' + +default: unit + +# The crate's tests, every feature on, from the pinned toolchain (rust_env in +# lib.just). --locked: Cargo.lock is the crate pin. +unit: (_banner "test rust" "unit") + #!/usr/bin/env bash + set -euo pipefail + if [ ! -f Cargo.toml ]; then + echo "no Cargo.toml — not a Rust crate, nothing to test (skipped, not failed)." + exit 0 + fi + {{ rust_env }} + cargo test --all-features --locked diff --git a/.limen/just/test.just b/.limen/just/test.just index 123e324..96880f5 100644 --- a/.limen/just/test.just +++ b/.limen/just/test.just @@ -13,3 +13,7 @@ default: @exit 1 mod go 'test-go.just' + +# Rust tests: `just do test rust` runs the crate's tests from the pinned +# toolchain (book/rust.md). +mod rust 'test-rust.just' diff --git a/.limen/just/tools.just b/.limen/just/tools.just index 505e24e..4db06be 100644 --- a/.limen/just/tools.just +++ b/.limen/just/tools.just @@ -66,33 +66,40 @@ project_aqua := ''' ''' # Add a new tool at its latest version or at one given, e.g. `just do tools add junegunn/fzf`. +[positional-arguments] add pkg version="": (_banner "tools" "add") #!/usr/bin/env bash set -euo pipefail + pkg="$1"; version="${2:-}" # Regex-escape the name: slugs and package paths carry dots, and an # unescaped '.' would match a sibling (`foo/li.r` vs `foo/liar`). ']' and # '\' are absent from both and trip BSD sed. - esc=$(printf '%s' '{{ pkg }}' | sed 's/[.^$*+?()[{}|]/\\&/g') - version='{{ version }}' - first='{{ pkg }}'; first=${first%%/*} + esc=$(printf '%s' "$pkg" | sed 's/[.^$*+?()[{}|]/\\&/g') + first=$pkg; first=${first%%/*} if [[ $first == *.* ]]; then if grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" .limen/tools/go.mod 2>/dev/null; then - echo "{{ pkg }} is already a tool directive in .limen/tools/go.mod — use 'just do tools update ' or 'just do tools set {{ pkg }} '." >&2 + echo "${pkg} is already a tool directive in .limen/tools/go.mod — use 'just do tools update ' or 'just do tools set ${pkg} '." >&2 exit 1 fi - go -C .limen/tools get -tool "{{ pkg }}@${version:-latest}" # the directive + go -C .limen/tools get -tool "${pkg}@${version:-latest}" # the directive go -C .limen/tools mod tidy - {{ go_tool_build }} '{{ pkg }}' # REAL build: a bad pin fails here, not at first use + {{ go_tool_build }} "$pkg" # REAL build: a bad pin fails here, not at first use exit 0 fi if grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}([[:space:]]|@|$)" .hermetic/hermetic.yaml; then - echo "{{ pkg }} is already in .hermetic/hermetic.yaml — use 'just do tools update ' or 'just do tools set {{ pkg }} '." >&2 + echo "${pkg} is already in .hermetic/hermetic.yaml — use 'just do tools update ' or 'just do tools set ${pkg} '." >&2 exit 1 fi {{ project_aqua }} - entry=$(project_aqua generate '{{ pkg }}') # the entry at the latest version + entry=$(project_aqua generate "$pkg") # the entry at the latest version + # An http package with no version source (a local-registry entry that reads + # a static host, rustup say) generates with no version at all: the one + # given goes on, and none given is an error, not a pin with a hole. if [ -n "$version" ]; then - entry=$(printf '%s\n' "$entry" | sed "1s#@[^[:space:]]*#@${version}#") + entry=$(printf '%s\n' "$entry" | sed "1s#@[^[:space:]]*##; 1s#\$#@${version}#") + elif ! printf '%s\n' "$entry" | head -1 | grep -q '@'; then + echo "${pkg} lists no versions (an http package): give one, 'just do tools add ${pkg} '." >&2 + exit 1 fi # The entry goes right above the import, at its indent: the resolver takes a # package's first declaration, so below it the entry would be shadowed. @@ -116,41 +123,43 @@ add pkg version="": (_banner "tools" "add") {{ agent_home }} hermetic -c .hermetic/hermetic.yaml install # REAL install: a bad pin fails here, not at first use # Set an existing tool to an exact version, e.g. `just do tools set goreleaser/goreleaser v2.0.0`. +[positional-arguments] set pkg version: (_banner "tools" "set") #!/usr/bin/env bash set -euo pipefail + pkg="$1"; version="$2" # See `add` for the escaping. - esc=$(printf '%s' '{{ pkg }}' | sed 's/[.^$*+?()[{}|]/\\&/g') - first='{{ pkg }}'; first=${first%%/*} + esc=$(printf '%s' "$pkg" | sed 's/[.^$*+?()[{}|]/\\&/g') + first=$pkg; first=${first%%/*} if [[ $first == *.* ]]; then # An isolated module (.limen/tools//go.mod, see _go-tool in lib.just) # holds the directive for the tool that builds as . - pkg='{{ pkg }}'; name=${pkg##*/} + name=${pkg##*/} if [[ $name =~ ^v[0-9]+$ ]]; then name=${pkg%/*}; name=${name##*/}; fi if grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" ".limen/tools/${name}/go.mod" 2>/dev/null; then - go -C ".limen/tools/${name}" get -tool '{{ pkg }}@{{ version }}' # moves the pin + go -C ".limen/tools/${name}" get -tool "${pkg}@${version}" # moves the pin go -C ".limen/tools/${name}" mod tidy - mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C ".limen/tools/${name}" build -o "$LIMEN_BUILD/tools/" '{{ pkg }}' + mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C ".limen/tools/${name}" build -o "$LIMEN_BUILD/tools/" "$pkg" exit 0 fi if ! grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" .limen/tools/go.mod 2>/dev/null; then - echo "{{ pkg }} is not a tool directive in .limen/tools/go.mod — add it with 'just do tools add {{ pkg }}'." >&2 + echo "${pkg} is not a tool directive in .limen/tools/go.mod — add it with 'just do tools add ${pkg}'." >&2 exit 1 fi - go -C .limen/tools get -tool '{{ pkg }}@{{ version }}' # moves the pin + go -C .limen/tools get -tool "${pkg}@${version}" # moves the pin go -C .limen/tools mod tidy - {{ go_tool_build }} '{{ pkg }}' # REAL build: a bad pin fails here, not at first use + {{ go_tool_build }} "$pkg" # REAL build: a bad pin fails here, not at first use exit 0 fi if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}@" .hermetic/hermetic.yaml; then - echo "{{ pkg }} is not pinned in .hermetic/hermetic.yaml — add it with 'just do tools add {{ pkg }} {{ version }}'." >&2 + echo "${pkg} is not pinned in .hermetic/hermetic.yaml — add it with 'just do tools add ${pkg} ${version}'." >&2 exit 1 fi # Explicit sibling template: macOS mktemp ignores $TMPDIR, and a sibling # makes the mv an atomic same-filesystem rename. tmp=$(mktemp .hermetic/hermetic.yaml.XXXXXX) trap 'rm -f "$tmp"' EXIT - sed -E "s#(^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc})@[^[:space:]]*#\1@{{ version }}#" .hermetic/hermetic.yaml > "$tmp" + sed -E "s#(^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc})@[^[:space:]]*#\1@${version}#" .hermetic/hermetic.yaml > "$tmp" mv "$tmp" .hermetic/hermetic.yaml {{ agent_home }} {{ aqua_api }} -c .hermetic/hermetic.yaml update-checksum --prune # new checksum in, replaced version's out {{ agent_home }} hermetic -c .hermetic/hermetic.yaml install # REAL install: a bad pin fails here, not at first use @@ -166,9 +175,11 @@ set pkg version: (_banner "tools" "set") # `just --list`; only the line right above the recipe is its doc.) # Update one tool to its latest version (`just do tools update golangci-lint`), or every tool with no argument. +[positional-arguments] update command="": (_banner "tools" "update") #!/usr/bin/env bash set -euo pipefail + command="${1:-}" # Every directive in .limen/tools/go.mod: a `tool` line and a `tool (…)` block are # both walked; a `//` comment line inside the block is not a directive. # (A read loop, not mapfile: macOS ships bash 3. A capture and a @@ -199,7 +210,7 @@ update command="": (_banner "tools" "update") mkdir -p "$LIMEN_BUILD/tools" && GOOS='' GOARCH='' CGO_ENABLED=0 go -C "$dir" build -o "$LIMEN_BUILD/tools/" "$p" } {{ project_aqua }} - if [ -z '{{ command }}' ]; then + if [ -z "$command" ]; then project_aqua update # every project pin and the registries, to the latest {{ agent_home }} {{ aqua_api }} -c .hermetic/hermetic.yaml update-checksum --prune # new checksums in, replaced versions' out {{ agent_home }} hermetic -c .hermetic/hermetic.yaml install # REAL install: a bad pin fails here, not at first use @@ -213,36 +224,38 @@ update command="": (_banner "tools" "update") done exit 0 fi - if [ -f '.limen/tools/{{ command }}/go.mod' ]; then - isolated_update '.limen/tools/{{ command }}' + if [ -f ".limen/tools/${command}/go.mod" ]; then + isolated_update ".limen/tools/${command}" exit 0 fi # ${pkgs[@]+…}: under set -u, bash 3 treats an empty array as unset. for p in ${pkgs[@]+"${pkgs[@]}"}; do - if [ "$(name_of "$p")" = '{{ command }}' ]; then + if [ "$(name_of "$p")" = "$command" ]; then go -C .limen/tools get -tool "${p}@latest" # bump .limen/tools/go.mod to the latest release go -C .limen/tools mod tidy {{ go_tool_build }} "$p" # REAL build: a bad pin fails here, not at first use exit 0 fi done - if ! project_aqua update '{{ command }}'; then # bump .hermetic/hermetic.yaml to the latest release - echo "{{ command }} is not pinned in .hermetic/hermetic.yaml — a canonical tool moves with limen; pin your own version with 'just do tools add '." >&2 + if ! project_aqua update "$command"; then # bump .hermetic/hermetic.yaml to the latest release + echo "${command} is not pinned in .hermetic/hermetic.yaml — a canonical tool moves with limen; pin your own version with 'just do tools add '." >&2 exit 1 fi {{ agent_home }} {{ aqua_api }} -c .hermetic/hermetic.yaml update-checksum --prune # new checksum in, replaced version's out {{ agent_home }} hermetic -c .hermetic/hermetic.yaml install # REAL install: a bad pin fails here, not at first use # Remove a tool entirely, e.g. `just do tools remove junegunn/fzf`. +[positional-arguments] remove pkg: (_banner "tools" "remove") #!/usr/bin/env bash set -euo pipefail + pkg="$1" # See `add` for the escaping. - esc=$(printf '%s' '{{ pkg }}' | sed 's/[.^$*+?()[{}|]/\\&/g') - first='{{ pkg }}'; first=${first%%/*} + esc=$(printf '%s' "$pkg" | sed 's/[.^$*+?()[{}|]/\\&/g') + first=$pkg; first=${first%%/*} if [[ $first == *.* ]]; then # The built binary, named as go names it: last element, /vN stripped. - pkg='{{ pkg }}'; name=${pkg##*/} + name=${pkg##*/} if [[ $name =~ ^v[0-9]+$ ]]; then name=${pkg%/*}; name=${name##*/}; fi # An isolated module (.limen/tools//go.mod) is the tool's alone: it # goes as a whole, go.sum included. @@ -251,16 +264,16 @@ remove pkg: (_banner "tools" "remove") exit 0 fi if ! grep -qE "^(tool[[:space:]]+)?[[:space:]]*${esc}([[:space:]]|$)" .limen/tools/go.mod 2>/dev/null; then - echo "{{ pkg }} is not a tool directive in .limen/tools/go.mod — nothing to remove." >&2 + echo "${pkg} is not a tool directive in .limen/tools/go.mod — nothing to remove." >&2 exit 1 fi - go -C .limen/tools get -tool '{{ pkg }}@none' # drops the directive + go -C .limen/tools get -tool "${pkg}@none" # drops the directive go -C .limen/tools mod tidy # and the requirements only it needed rm -f "$LIMEN_BUILD/tools/${name}" "$LIMEN_BUILD/tools/${name}.exe" exit 0 fi if ! grep -qE "^[[:space:]]*-[[:space:]]*name:[[:space:]]*${esc}([[:space:]]|@|$)" .hermetic/hermetic.yaml; then - echo "{{ pkg }} is not in .hermetic/hermetic.yaml — nothing to remove." >&2 + echo "${pkg} is not in .hermetic/hermetic.yaml — nothing to remove." >&2 exit 1 fi # See `set` for why mktemp gets an explicit sibling-path template. @@ -271,7 +284,7 @@ remove pkg: (_banner "tools" "remove") # entry's extent is decided by indentation, as YAML decides it: deeper lines # belong to it, a line at its own depth or shallower ends it — so a # `# --- section ---` header at item depth stays with what follows. - awk -v pkg="{{ pkg }}" ' + awk -v pkg="$pkg" ' function indent(l) { match(l, /^[[:blank:]]*/); return RLENGTH } { if (skip) { if ($0 !~ /^[[:blank:]]*$/ && indent($0) > depth) next; skip = 0 } diff --git a/.limen/lint-go.yaml b/.limen/lint-go.yaml index 0cd4aad..fb4c6dd 100644 --- a/.limen/lint-go.yaml +++ b/.limen/lint-go.yaml @@ -39,12 +39,15 @@ lint-go: licenses: # What go-licenses accepts in the dependency graph; a project's .lint-go.yaml - # `allowed` replaces the list, its `ignore` names modules to skip. + # `allowed` replaces the list, its `ignore` names modules to skip. A + # permissive license is fine on a dependency where it is no choice for a + # repository's own LICENSE (BSD-2-Clause, the Unlicense: book/mandatory-files.md). allowed: - Apache-2.0 - BSD-2-Clause - BSD-3-Clause - MIT + - Unlicense nilaway: # NilAway (go.uber.org/nilaway) is not a golangci-lint linter and will not diff --git a/.limen/renovate.json b/.limen/renovate.json index e5ab53d..f133be6 100644 --- a/.limen/renovate.json +++ b/.limen/renovate.json @@ -3,6 +3,7 @@ "description": [ "The canonical Renovate configuration of every farcloser repository, content-pinned like the rest of .limen/: each repository carries this file byte for byte and its renovate.json extends it by the repository's own name (local>owner/name//.limen/renovate — Renovate refuses a relative reference in a repository's own config), so a fix here reaches every repository at its next limen bump, in the bump's diff. Reasoning: limen's book, mandatory files (CI workflows).", "extends: config:recommended alone. aqua's own preset detected pins by the file names aqua.yaml and .aqua/aqua.yaml; the pins live in .hermetic/hermetic.yaml now, resolved by hermetic, which that preset never sees, so the customManagers below carry its three managers that apply here, pointed at .hermetic/hermetic.yaml and the canonical .limen/hermetic.yaml: owner/repo@version pins against GitHub releases and against GitHub tags (a package released by tag alone), and the standard registry's `ref:` line. hermetic itself is pinned in the canonical setup-hermetic action as HERMETIC_VERSION, watched by the manager below in limen alone (the content-pinned rule silences the action everywhere else), so it moves through a limen release like every other canonical pin; its sha256 companion is recomputed by the checksum workflow on Renovate's branch, as the project checksums are.", + "packageRules, the limen group: a repository carries limen twice, as the hermetic pin that selects the recipes and the `limen fix` of its branches, and as the `uses:` reference of its workflow callers to limen's reusable workflows at a commit (`farcloser/limen/.github/workflows/limen-*.yaml@ # vX.Y.Z`, the github-actions manager). One group named limen keeps the two in one pull request, so the recipes and the lanes move together; nothing else joins it. The content-pinned pieces that are not workflows (the actions, .limen/) stay silenced outside limen: a limen bump moves them through `limen fix`.", "minimumReleaseAge: supply-chain cooldown before proposing a bump; doubles as protection against a release tag whose assets are not uploaded yet.", "packageRules, golang/go: no cooldown. The Go toolchain is each repository's own pin so that a standard-library fix reaches it without a limen release, and a Go patch release is that fix; three days of cooldown would be three days of red security lanes. Go's releases are not pulled or re-uploaded the way the cooldown guards against.", "packageRules, own releases: the cooldown guards against third parties (a tag whose assets are still uploading, a release pulled the next day). A release cut by one of our organizations carries no risk we do not already own, and its assets are published by the release workflow within minutes \u2014 so farcloser, forkcloser and mycophonic packages, in aqua's owner/repo form and Go's github.com/owner/repo form alike, are proposed as soon as they exist. A repository that wants the cooldown back on one of them adds a later packageRule in its own renovate.json; rules merge in order and the last match wins.", @@ -13,18 +14,22 @@ "postUpdateOptions gomodTidy: a Go module bump rewrites go.mod; without tidy, go.sum keeps the old module's lines and `just do lint go mod` (go mod tidy, diffed) fails every gomod PR. gomodUpdateImportPaths is what keeps tidy running on a MAJOR bump: without it Renovate skips tidy on every major (its guard for the /vN import-path rewrite, which must precede tidy), and a v0 to v1 bump is major while changing no path, so the PR landed a go.sum with the new go.mod hash, no h1 line and the old pseudo-version lines still in it, green for Renovate and red on the tidy diff. With the option, v0 to v1 does no path rewrite and tidies; a real /vN bump rewrites the imports with marwan-at-work/mod (which Renovate go-installs at run time, no image support needed) and then tidies.", "constraints gomodMod: the version Renovate go-installs marwan-at-work/mod at for that rewrite; without it the install is @latest, a third-party binary that rewrites import paths across the tree fetched at whatever is newest the day a /vN bump lands. Pinned means by digest, and this lever takes only a v-prefixed tag, so this is the nearest the lever allows: a version pin, its content fixed and transparency-logged by the checksum database that go install verifies against. Named here as that exception rather than left implicit. Renovate's own config manager marks this constraint unsupported and never scans a preset, so limen's renovate.json watches the value with a custom manager and proposes its bumps there.", "packageRules, content-pinned files: the checksum-update workflow, the shared CI lanes (limen-verify.yaml), the security workflow, the setup-hermetic, cache and windows-cache-image actions and everything under .limen/ are limen's byte for byte; a bump Renovate makes inside one is drift the next `limen check` rejects. Their pins move in limen's own repository, the one place Renovate keeps touching them, and reach every other repository through a limen release.", - "packageRules, Go-built tools: the tool directives under .limen/tools/ (git-validation, godolint, dot, the Go-source analyzers deadcode, govulncheck, go-licenses, and golangci-lint in .limen/tools/golangci-lint/go.mod, a module of its own so its analyzers' dependencies stay upstream's), which Go lists as // indirect and the gomod manager would skip; re-enabled exactly, by module. dot is forkcloser's own tagged module (upstream's cmd/dot is a nested module without tags), so it moves like the others, and as one of ours it is proposed without the cooldown. Two rules, by file: .limen/tools/go.mod for the shared tools, .limen/tools/*/go.mod for golangci-lint and nilaway only. One rule over every gomod file re-enabled golang.org/x/tools where it is a plain // indirect requirement of golangci-lint's and nilaway's own modules, and bumped it past the version each upstream pins (forkcloser/infra#10): the isolated module exists so that graph stays upstream's, so nothing in it moves but the tool itself.", + "packageRules, Go-built tools: the tool directives under .limen/tools/ (git-validation, godolint, dot, yamlfmt, gotestsum, the Go-source analyzers deadcode, govulncheck, go-licenses, and golangci-lint and nilaway in .limen/tools//go.mod, each a module of its own so its dependencies stay upstream's), which Go lists as // indirect and the gomod manager would skip; re-enabled exactly, by module. dot is forkcloser's own tagged module (upstream's cmd/dot is a nested module without tags), so it moves like the others, and as one of ours it is proposed without the cooldown. Two rules, by file: .limen/tools/go.mod for the shared tools, .limen/tools/*/go.mod for the isolated tools only. One rule over every gomod file re-enabled golang.org/x/tools where it is a plain // indirect requirement of golangci-lint's and nilaway's own modules, and bumped it past the version each upstream pins (forkcloser/infra#10): the isolated module exists so that graph stays upstream's, so nothing in it moves but the tool itself.", "packageRules, groups: one pull request per repository per kind, not one per dependency — a registry ref that moves most days and a dozen tool pins were arriving as a dozen pull requests a month per repository, each reviewed alone. Three groups: `aqua tools` (every third-party pin in .hermetic/hermetic.yaml and the standard-registry ref), `github actions`, and `go modules` (non-major gomod bumps, the .limen/tools/go.mod directives included). Renovate keeps the one open pull request current as further versions arrive instead of opening another. Grouped, not scheduled: a bump still arrives as soon as its cooldown ends. Outside the groups on purpose: our own releases (immediate and alone, so a limen bump is its own reviewed change), major Go bumps (code may have to move), pins.yaml entries (each a deliberate supply-chain change), and security fixes, which Renovate's vulnerabilityAlerts defaults keep ungrouped. A repository that wants a dependency out of a group adds a later packageRule with groupName null.", + "packageRules, golang/go, versioning: Go tags its releases go1.N.M, and the candidates golang/go offers are tags (it publishes no GitHub release). The regex managers' default versioning, semver-coerced, parses such a tag but calls every one unstable, since its stability check accepts only a bare or v-prefixed number; from a current version it holds unstable, Renovate lets through only a candidate with the same major, minor and patch, so no bump was ever proposed, silently. Regex versioning that carries the prefix parses the pin and the candidates alike, as lychee's does, and leaves the rc tags out by construction. Out of the aqua tools group: a toolchain patch is the standard-library fix the pin exists to receive without delay, in a pull request that names it.", "packageRules, lychee: releases are tagged lychee-vX.Y.Z next to lychee-lib-vX.Y.Z and nightly; regex versioning that carries the prefix parses the pin and the candidates alike and excludes the others by construction.", "packageRules, git-for-windows: releases are tagged vX.Y.Z.windows.N, and the N moves on its own (a Git for Windows rebuild of the same Git); a semver reading drops it and would never propose that bump. The build group carries it, and release candidates (vX.Y.Z-rcN.windows.N) fall outside the pattern.", + "packageRules, own releases, versioning: a fork's rebuild of the same upstream version is tagged with a fourth segment, vX.Y.Z.N (book/forks.md); the regex managers' default versioning, semver-coerced, reads v8.22.0.2 as 8.22.0 and so never proposes a rebuild (limen stayed on forkcloser/curl v8.22.0 through v8.22.0.1 and v8.22.0.2). The build group carries the fourth segment; absent, the release compares as the one its rebuilds follow, and a -suffix stays a prerelease. The rule claims a pin by its current value, a v-prefixed version, since a packageRule's versioning overrides the one a pins.yaml entry declares for itself: a release line of another shape (ossein-kernel's X.Y.Z-ossein.N, read by its own regex in the pin) keeps its own.", "packageRules, runner images: the github-actions manager, which config:recommended enables, also proposes the runs-on images (ubuntu-24.04 to 26.04, macos-15 to 26) from the github-runners datasource. Those are product targets, not dependencies: the matrix is the set of platforms the code and the commands are proven on, set by the canonical ci.yaml seed and moved deliberately, on purpose, as a reviewed change. Off for that datasource; the actions themselves keep moving.", - "customManagers, pins.yaml: the artifacts a repository fetches by hand (a kernel tarball, a toolchain release, a source archive) live in one limen-owned file, each entry naming its own datasource and depName on its renovate: line, optionally an extract-version regexp and a versioning scheme on the two lines after it, and the version: line that closes the block is what moves. The digest follows in the same pull request: the checksum workflow runs `limen pins refresh`, which recomputes it through the verifier the entry declares. Watched here, once, for every repository; nobody writes a regex manager per pin.", - "commitBody: `just do lint commits` enforces DCO on every PR range, bot commits included." + "customManagers, pins.yaml: the artifacts a repository fetches by hand (a kernel tarball, a toolchain release, a source archive) live in one limen-owned file, each entry naming its own datasource and depName on its renovate: line (or `manual`, the one word, for a pin no datasource serves, which the regex below then leaves alone and a hand moves), optionally an extract-version regexp and a versioning scheme on the two lines after it, the version: line that moves, and for a commit pin (git-refs: the version is the branch) a commit: line after it, captured as the digest, so Renovate proposes the branch's new head. The digest follows in the same pull request: the checksum workflow runs `limen pins refresh`, which recomputes it through the verifier the entry declares. Watched here, once, for every repository; nobody writes a regex manager per pin.", + "commitBody: `just do lint commits` enforces DCO on every PR range, bot commits included.", + "lockFileMaintenance: a lock file pins the transitive dependencies too (Cargo.lock's crates under a crate the project names), and nothing moves one but a bump of a direct dependency or a vulnerability alert; weekly, Renovate regenerates every lock in the repository in one pull request, which the lanes prove as any other (`--locked`, the tests). Go has no lock maintenance in Renovate (go.sum is not on its list): an indirect Go requirement moves with the direct bump that re-resolves it and with vulnerability alerts, which the preset turns on." ], "extends": [ "config:recommended" ], "minimumReleaseAge": "3 days", + "lockFileMaintenance": { "enabled": true, "schedule": ["before 5am on monday"] }, "rebaseWhen": "behind-base-branch", "automerge": true, "automergeType": "pr", @@ -46,7 +51,7 @@ "customType": "regex", "managerFilePatterns": ["/(^|/)pins\\.yaml$/"], "matchStrings": [ - "renovate: (?\\S+) (?\\S+)\\n(?:\\s+extract-version: (?\\S+)\\n)?(?:\\s+versioning: (?\\S+)\\n)?\\s+version: (?\\S+)" + "renovate: (?\\S+) (?\\S+)\\n(?:\\s+extract-version: (?\\S+)\\n)?(?:\\s+versioning: (?\\S+)\\n)?\\s+version: (?\\S+)(?:\\n\\s+commit: (?\\S+))?" ] }, { @@ -97,12 +102,9 @@ }, { "matchFileNames": [ - ".github/workflows/update-aqua-checksum.yaml", ".github/actions/setup-hermetic/**", ".github/actions/windows-cache-image/**", ".github/actions/cache/**", - ".github/workflows/limen-verify.yaml", - ".github/workflows/security.yaml", ".limen/**" ], "matchRepositories": [ @@ -110,6 +112,12 @@ ], "enabled": false }, + { + "matchDepNames": [ + "farcloser/limen" + ], + "groupName": "limen" + }, { "matchManagers": [ "gomod" @@ -121,6 +129,8 @@ "github.com/forkcloser/git-validation", "github.com/forkcloser/godolint", "github.com/forkcloser/dot", + "github.com/forkcloser/yamlfmt", + "gotest.tools/gotestsum", "golang.org/x/tools", "golang.org/x/vuln", "github.com/forkcloser/go-licenses" @@ -182,6 +192,27 @@ ], "versioning": "regex:^v(?\\d+)\\.(?\\d+)\\.(?\\d+)\\.windows\\.(?\\d+)$" }, + { + "matchManagers": [ + "custom.regex" + ], + "matchDatasources": [ + "github-releases", + "github-tags" + ], + "matchDepNames": [ + "/^(farcloser|forkcloser|mycophonic)/[^/]+$/" + ], + "matchCurrentValue": "/^v\\d+\\.\\d+\\.\\d+(\\.\\d+)?(-[0-9A-Za-z.-]+)?$/", + "versioning": "regex:^v(?\\d+)\\.(?\\d+)\\.(?\\d+)(?:\\.(?\\d+))?(?:-(?[0-9A-Za-z.-]+))?$" + }, + { + "matchDepNames": [ + "golang/go" + ], + "versioning": "regex:^go(?\\d+)\\.(?\\d+)\\.(?\\d+)$", + "groupName": null + }, { "matchDatasources": [ "github-runners" diff --git a/.limen/tools/go.mod b/.limen/tools/go.mod index 5b73b7a..6edb5a7 100644 --- a/.limen/tools/go.mod +++ b/.limen/tools/go.mod @@ -10,19 +10,30 @@ tool ( github.com/forkcloser/git-validation/cmd/git-validation github.com/forkcloser/go-licenses/cmd/go-licenses github.com/forkcloser/godolint/cmd/godolint + github.com/forkcloser/yamlfmt/cmd/yamlfmt golang.org/x/tools/cmd/deadcode golang.org/x/vuln/cmd/govulncheck + gotest.tools/gotestsum ) require ( github.com/alecthomas/kong v1.16.1 // indirect + github.com/bitfield/gotestdox v0.2.2 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.3 // indirect github.com/containerd/typeurl/v2 v2.3.0 // indirect + github.com/dnephin/pflag v1.0.7 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/forkcloser/dot v1.3.1 // indirect github.com/forkcloser/git-validation v0.1.1 // indirect github.com/forkcloser/go-graphviz v0.5.2 // indirect github.com/forkcloser/go-licenses v0.1.1 // indirect github.com/forkcloser/godolint v0.5.1 // indirect + github.com/forkcloser/yamlfmt v0.2.1 // indirect + github.com/fsnotify/fsnotify v1.10.1 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/lmittmann/tint v1.2.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.24 // indirect github.com/moby/buildkit v0.34.0 // indirect github.com/mycophonic/primordium v0.15.1 // indirect @@ -30,16 +41,21 @@ require ( github.com/otiai10/mint v1.6.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/tetratelabs/wazero v1.12.0 // indirect + go.yaml.in/yaml/v3 v3.0.5 // indirect golang.org/x/image v0.47.0 // indirect golang.org/x/mod v0.42.0 // indirect golang.org/x/sync v0.24.0 // indirect golang.org/x/sys v0.49.0 // indirect golang.org/x/telemetry v0.0.0-20261010193011-f71dc92da45e // indirect + golang.org/x/term v0.46.0 // indirect golang.org/x/text v0.43.0 // indirect golang.org/x/tools v0.52.0 // indirect golang.org/x/vuln v1.9.0 // indirect google.golang.org/protobuf v1.36.12 // indirect + gotest.tools/gotestsum v1.13.0 // indirect mvdan.cc/sh/v3 v3.14.1 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/.limen/tools/go.sum b/.limen/tools/go.sum index 63e486a..0369b85 100644 --- a/.limen/tools/go.sum +++ b/.limen/tools/go.sum @@ -4,10 +4,18 @@ github.com/alecthomas/kong v1.16.1 h1:ixhCt93XkJ98kGposQ54+bl0IK6XwqB40AsMynU7Z8 github.com/alecthomas/kong v1.16.1/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/bitfield/gotestdox v0.2.2 h1:x6RcPAbBbErKLnapz1QeAlf3ospg8efBsedU93CDsnE= +github.com/bitfield/gotestdox v0.2.2/go.mod h1:D+gwtS0urjBrzguAkTM2wodsTQYFHdpx8eqRJ3N+9pY= +github.com/bmatcuk/doublestar/v4 v4.10.3 h1:1UIv1+fqH3++ViVkpnaPTB+Pi1UHycnPFAq14zgK+gI= +github.com/bmatcuk/doublestar/v4 v4.10.3/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/containerd/typeurl/v2 v2.3.0 h1:HZHPhRWo5XMy3QGQoPrUzbW/2ckwjfweHmOwlkIrPAQ= github.com/containerd/typeurl/v2 v2.3.0/go.mod h1:Qk+PAdUYArVj41TnGi6rJ+48RF0PkcTc4i/taoBcK0w= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dnephin/pflag v1.0.7 h1:oxONGlWxhmUct0YzKTgrpQv9AUA1wtPBn7zuSjJqptk= +github.com/dnephin/pflag v1.0.7/go.mod h1:uxE91IoWURlOiTUIA8Mq5ZZkAv3dPUfZNaT80Zm7OQE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/forkcloser/dot v1.3.1 h1:jhKEBkdZ8PJYgyohT74+I0zYr0NRXwdTWAsZfAJFaBU= github.com/forkcloser/dot v1.3.1/go.mod h1:pM3Q/7e6Ac/vY+7J21Q7QoL69wYSni71xt5SfUkR8Ck= github.com/forkcloser/git-validation v0.1.1 h1:A1FkfOHKgkqCC21/DSTjYlY7IlvbRtnvFAZDO2bQMbc= @@ -18,6 +26,10 @@ github.com/forkcloser/go-licenses v0.1.1 h1:VzTtRdQCmJMSP9EWSr9aTAjzCUNajW2hzb/l github.com/forkcloser/go-licenses v0.1.1/go.mod h1:kWjKtze8KoScVq3ifYckfCwXRxFbOPnNIdA7tXSsAq0= github.com/forkcloser/godolint v0.5.1 h1:hLfaabqsdaWFruEqQijUd4hwI+uNISL24LKOrlIolk4= github.com/forkcloser/godolint v0.5.1/go.mod h1:cev6BNLOrnT1khxHkC6G3jS0nFH9tkZHnbLsLBq0qNw= +github.com/forkcloser/yamlfmt v0.2.1 h1:I6EVh6hskPS+c0DTHDev8QXLuRPRqGuH45xp+9EwchQ= +github.com/forkcloser/yamlfmt v0.2.1/go.mod h1:4qnRMKISOQZBrQ/QwA4BHU5OtXXTN7W+4SqHHYm4z84= +github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho= +github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo= github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= github.com/google/go-cmdtest v0.4.1-0.20220921163831-55ab3332a786 h1:rcv+Ippz6RAtvaGgKxc+8FQIpxHgsF+HBzPyYL2cyVU= @@ -26,6 +38,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -37,6 +51,9 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lmittmann/tint v1.2.1 h1:zOadah+f+nSm3jZM044jOsqh0+vSfgG/d9Ota9a/TLE= github.com/lmittmann/tint v1.2.1/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= github.com/moby/buildkit v0.34.0 h1:bG5h2y1s5IVvc/uSGo6hVlYbd2eXVFRuADl9Zf+jeJM= @@ -54,14 +71,19 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= +github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU= github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo= @@ -70,10 +92,13 @@ golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/telemetry v0.0.0-20261010193011-f71dc92da45e h1:P6NGsugec4oASJ+yKMNgTndeo0h2CbixZLvQIe9h5R4= golang.org/x/telemetry v0.0.0-20261010193011-f71dc92da45e/go.mod h1:GNu+nb+E5axfGmP+Uu3ZVQu5lE/Xk42MjZOHWUD94/o= +golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= +golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/tools v0.52.0 h1:ryxg4U63oaUYk7uOLTF37n43f4qgtGvqV4Ioit1BaEs= @@ -88,7 +113,18 @@ google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fw google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/gotestsum v1.13.0 h1:+Lh454O9mu9AMG1APV4o0y7oDYKyik/3kBOiCqiEpRo= +gotest.tools/gotestsum v1.13.0/go.mod h1:7f0NS5hFb0dWr4NtcsAsF0y1kzjEFfAil0HiBQJE03Q= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= mvdan.cc/sh/v3 v3.14.1 h1:bXkhQWNHCs0KZEChF8hYS6FC+T2N9mUZLbQv9blditI= mvdan.cc/sh/v3 v3.14.1/go.mod h1:syYCoFET8w9tvevxiXUtY8/ICrU+l26jHmhJDra3Vwo= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/AGENTS.md b/AGENTS.md index 005a4e2..db3e787 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,16 @@ is the [engineering book](https://github.com/farcloser/limen/tree/main/book). recipe covers the need — say so when you do it. `just --list` shows every task. - Always name the recipe; module defaults are curated subsets, not "run everything". - Never bump a tool version by hand: `just do tools set` / `just do tools update`. +- **Git is plain `git`.** The rig configures a session's git and ssh, so `git fetch`, + `git push`, `git pull` reach GitHub as the bot as they are; a network command that fails + is broken tooling, reported, never worked around with an option of your own. The force + push has one shape, the allow rule's prefix at the start of its own + Bash command: `git push --force-with-lease=: origin + :refs/heads/`, nothing between `git` and `push`, nothing between `push` + and `--force-with-lease=` (no `-q`), no `&&` or `;` around it; the lease on the sha it + last saw, and the branch by name as the source, never `HEAD`, since the shell's + directory is not the worktree and the command takes no `-C`, so `HEAD:` would force the + clone's checkout onto the branch. Everywhere else, name the tree: `git -C …`. ## Contributing @@ -31,8 +41,10 @@ chapter; the procedure is limen's `skills/contribute`. nowhere else in the tree. - **The pull request's title is its release note.** A release's notes are the titles of the pull requests it merged, so a title says what changed for a consumer; no - `CHANGELOG.md` is kept by hand. One that breaks a consumer carries the `breaking` label - (`gh label create breaking` the first time a repository needs it). + `CHANGELOG.md` is kept by hand. One that breaks a consumer carries the `breaking` label; + one that changes nothing for a consumer (tooling, CI, maintainers' documentation) carries + `internal` and is left out of the notes (`gh label create breaking` and + `gh label create internal` the first time a repository needs them). - **A pull request is read by someone who was not in the session.** Its description says what changes and why in the terms of the code: the defect or the gap, the mechanism, the evidence it works. A decision appears as its technical reason; who asked, who decided and @@ -55,6 +67,11 @@ chapter; the procedure is limen's `skills/contribute`. message, round after round, until settled, and then tells the project-manager session `reviewed: `. A commit after that is a new review: the same exchange, a new sha. Review discussion lives in the sessions, never on the pull request. + A trivial documentation-only change (a typo, a wrap, a stale link, prose with nothing + decided in it), and a pull request that only bumps dependencies, go to the project-manager + session instead, which reviews, logs and arms them; anything else touching code, + configuration, workflows or tests, and any documentation change that is not trivial, goes + to the reviewing session. In doubt, the reviewing session. - **Open the pull request at the reviewed head, bind it, babysit it.** Push, open the pull request, bind it in your own PR pane, and own it to green: a red that needs a redesign goes back to the reviewing session; an environmental red (a links 503, a known flake) gets @@ -65,13 +82,23 @@ chapter; the procedure is limen's `skills/contribute`. queue, or refuses it as `UNREVIEWED:`. The human approves on GitHub; auto-merge merges. A private repository is never armed: its pull request is bound for the human, who merges by hand after approving. No review request, no review comments, no message at open. +- **Your worktree goes when its pull request has merged.** Once `git status` in it is clean + and its head is on `main`, remove it with + `/Users/apostasie/.claude/bin/farclauder worktree remove `, run as the whole Bash + command and by that absolute path: `git worktree remove` cannot finish inside the sandbox, + and the exclusion that runs farclauder outside it matches the call's text, which the `~` + form does not (the book's + [identity](https://github.com/farcloser/limen/blob/main/book/identity.md)). - **A native stack** is reviewed and opened layer by layer the same way; the project manager merges it from the top once every layer is approved, never from the middle. A lower layer merged alone comes back rebased and signed by GitHub, which `lint commits` rejects. -- **Not yours to do:** merge by hand, push to `main`, force-push a shared branch, tag a - release. A release waits for an empty queue: no tag while a pull request is open, a bot's - included, unless the human parks it by number for that tag (`PARKED_PRS`). +- **Not yours to do:** merge by hand, push to `main` (but for the empty `Birth` commit that + creates it on a new repository, the book's + [creating a repository](https://github.com/farcloser/limen/blob/main/book/github.md#creating-a-repository)), + force-push a shared branch, tag a release. A release waits for an empty queue: no tag + while a pull request is open, a bot's included, unless the human parks it by number for + that tag (`PARKED_PRS`). ## Scope @@ -97,13 +124,14 @@ chapter; the procedure is limen's `skills/contribute`. place — another transport, a variable set by hand per command, a manual step for a recipe — carried on as if the rig worked: that hides the defect. A workaround is used only after the breakage is reported and the human agrees, and is named as one every time. -- **A locked laptop is not broken tooling.** The bot's key answers only while the human's - session is unlocked: once the screen locks, the agent refuses to sign or authenticate. So - when signing or pushing fails after it worked earlier in the session, the human is away, - not the rig broken. No retry loop, no workaround: finish the work in the worktree — done, - `just lint` and `just test` green, the commit message written — ready to commit and push - when the human is back, and say so once. Signing that never worked in the session is - broken tooling (above). +- **A locked screen does not stop the key.** The bot's key is created to sign while the + screen is locked, from the human's first unlock after boot until the next boot (taciturn, + `--use locked`). So a signing or push failure that follows earlier successes in the session + is a defect and is reported as broken tooling (above), never waited out. Only a machine + still on Secretive refuses once the screen locks; there the human is away, not the rig + broken: no retry loop, no workaround, finish the work in the worktree — done, `just lint` + and `just test` green, the commit message written — ready to commit and push when the + human is back, and say so once. - **Read what the work needs, never the whole disk.** A targeted read outside the repositories is fine when the work calls for it; a filesystem-wide walk is not: no `find /`, `find ~`, disk-wide `mdfind`, or recursive grep over `/` or `~`. @@ -120,6 +148,11 @@ chapter; the procedure is limen's `skills/contribute`. came about. - **Lead with the answer.** Short sentences; no preamble; no narration of your own reasoning. +- **Correct first; never ask the human for a preference.** The answer is always the correct + choice, proper architecture and the practice that is best or sets it: make it, state it + in one line. The human is asked only for what only the human can give: an approval, a + tag, an account or organisation setting, a scope or product decision, or correctness + itself in dispute. - **A message from another session that needs nothing gets no reply.** Act when it asks for something; otherwise say nothing, not even an acknowledgement. - **A message goes to the one session with a stake in it.** A finding on a pull request @@ -133,11 +166,12 @@ chapter; the procedure is limen's `skills/contribute`. assessment or misreads the code, say so with the evidence and settle it between the two sessions, the reviewer's 🛑 included (the reviewer may miss what the owner knows; the owner may miss the cross-cutting picture). A deadlock goes to the human, as the exception. -- **No GitHub issues unless the human asks for one.** The issue tracker is the human's. - A defect or a request that belongs to another repository goes to the session that owns - that repository, as a message with what, why, and where; that session fixes it, and - the human hears about it as a pull request. An issue you opened on your own is closed - once the owning session has the work. +- **A follow-up is a GitHub issue; a defect in another repository is a message.** Anything + left for later, a follow-up or parked work, is filed as an issue in the repository it + belongs to, one per real item, written so its owner can act on it; never a scratch, + `TODO` or notes file, and never an issue for a passing note. A defect or a request in a + repository another session owns goes to that session as a message with what, why and + where; that session fixes it, and the human hears about it as a pull request. ## Code @@ -163,7 +197,9 @@ chapter; the procedure is limen's `skills/contribute`. package A states A's guarantee, never what B does with it; a wrapper with a stated contract grows no export outside it because a caller wanted a home for a helper. A bug reported to the owning session earns the guarantee and the version that carries it, not - a say in the owner's tests. + a say in the owner's tests. The provider knows nothing of its consumers: what one needs + is offered as a property of the contract any consumer can use, never as that consumer's + name, path or variable in the provider. - **Tests are black-box, never bought with indirection.** External test package (`package foo_test`), no reach into private state, and no interface, function field, or other indirection in production code whose only purpose is a test's fake: that is bad