From fcb0b1acae64dd9be968436c3ce0471cee854c87 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 10:26:53 -0500 Subject: [PATCH 01/22] chore: centralize pre-commit hooks around shared .github config --- .pre-commit-config.yaml | 17 ++++++----------- CLAUDE.md | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcd9b41..661d378 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,12 +1,4 @@ repos: - # ── Git Conventions ── - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 - hooks: - - id: no-commit-to-branch - name: no commit to main - args: [--branch, main] - - repo: https://github.com/sensiblebit/.github rev: "fb8829a3bdb52f8263024528909088706009c1e1" hooks: @@ -27,9 +19,12 @@ repos: - id: go-mod-update - id: npm-update - # ── Markdown ── - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.47.0 + - repo: local hooks: - id: markdownlint + name: markdownlint + language: node + entry: markdownlint + additional_dependencies: [markdownlint-cli@0.47.0] args: [--config, .markdownlint.yaml] + types: [markdown] diff --git a/CLAUDE.md b/CLAUDE.md index 294b567..d7dca28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -343,7 +343,7 @@ pre-commit run --all-files # Manual run against all files - **PC-1 (MUST)** Use `pre-commit run --all-files` for required checks; do not run individual tools manually unless debugging. -Configured hooks: `no-commit-to-branch`, `branch-name`, `commit-message` (commit-msg stage), `goimports`, `go-fix`, `go-vet`, `golangci-lint`, `wasm`, `go-build`, `go-test`, `gendocs`, `prettier`, `vitest`, `wrangler-build`, `markdownlint`. +Configured hooks: `branch-name`, `commit-message` (commit-msg stage), `goimports`, `go-fix`, `go-vet`, `golangci-lint`, `wasm`, `go-build`, `go-test`, `gendocs`, `prettier`, `vitest`, `wrangler-build`, `markdownlint`. ### Tooling gates From 3a7118531ffdc7333a7b8f2ebb8899122c37aa94 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 10:31:56 -0500 Subject: [PATCH 02/22] chore: consume shared markdownlint hook to remove local pre-commit config --- .pre-commit-config.yaml | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 661d378..c84fef5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/sensiblebit/.github - rev: "fb8829a3bdb52f8263024528909088706009c1e1" + rev: "6b8b7cc2c3ff7549b510865c478cb5f8b43d0f4e" hooks: - id: branch-name - id: commit-message @@ -14,17 +14,9 @@ repos: - id: govulncheck - id: gendocs - id: prettier + - id: markdownlint + args: [--config, .markdownlint.yaml] - id: vitest - id: wrangler-build - id: go-mod-update - id: npm-update - - - repo: local - hooks: - - id: markdownlint - name: markdownlint - language: node - entry: markdownlint - additional_dependencies: [markdownlint-cli@0.47.0] - args: [--config, .markdownlint.yaml] - types: [markdown] From a7bcfcd9138973b198191fec887ac9d1cfedcf00 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 10:36:06 -0500 Subject: [PATCH 03/22] chore: run dependency updates before pre-commit validation hooks --- .pre-commit-config.yaml | 20 +++++++++++--------- go.mod | 4 ++-- go.sum | 14 ++++++-------- web/package-lock.json | 6 +++--- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c84fef5..2445592 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,24 @@ repos: - repo: https://github.com/sensiblebit/.github - rev: "6b8b7cc2c3ff7549b510865c478cb5f8b43d0f4e" + rev: "18310fd25db7cb182403c55e6495a2f9049b3078" hooks: - id: branch-name - id: commit-message + - id: go-mod-update + stages: [pre-commit] + - id: npm-update + stages: [pre-commit] - id: goimports - id: go-fix + - id: prettier + - id: markdownlint + args: [--config, .markdownlint.yaml] - id: go-vet - id: golangci-lint + - id: govulncheck + - id: gendocs - id: wasm - id: go-build + - id: wrangler-build - id: go-test - - id: govulncheck - - id: gendocs - - id: prettier - - id: markdownlint - args: [--config, .markdownlint.yaml] - id: vitest - - id: wrangler-build - - id: go-mod-update - - id: npm-update diff --git a/go.mod b/go.mod index 11f9f10..556a1ae 100644 --- a/go.mod +++ b/go.mod @@ -27,12 +27,12 @@ require ( github.com/ncruces/go-strftime v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/transparency-dev/merkle v0.0.2 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/sys v0.41.0 // indirect + golang.org/x/tools v0.42.0 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect k8s.io/klog/v2 v2.130.1 // indirect - modernc.org/libc v1.68.0 // indirect + modernc.org/libc v1.69.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) diff --git a/go.sum b/go.sum index 8ab331b..0b14d98 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,6 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -149,18 +147,18 @@ k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.30.2 h1:4yPaaq9dXYXZ2V8s1UgrC3KIj580l2N4ClrLwnbv2so= -modernc.org/ccgo/v4 v4.30.2/go.mod h1:yZMnhWEdW0qw3EtCndG1+ldRrVGS+bIwyWmAWzS0XEw= -modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= -modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg= +modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= -modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= +modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8= +modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= diff --git a/web/package-lock.json b/web/package-lock.json index 30dfd1f..5c6f74f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1536,9 +1536,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { From 6089dba503db82399c17b9d06bdfe5b2ebb1360e Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 11:34:39 -0500 Subject: [PATCH 04/22] docs: align hook docs and changelog with update-first pre-commit flow --- CHANGELOG.md | 2 ++ CLAUDE.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c595d4..270677c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Use `MemStore` count accessors for scan snapshot totals to avoid unnecessary full-slice materialization when reporting progress. ([#112]) - Normalize `connect` verify status labels from `OK`/`FAILED` to lowercase `ok`/`failed`. ([#112]) - Consolidate shared `connect` status-line formatting between standard and verbose text output paths. ([#121]) +- Centralize pre-commit hooks under the shared `sensiblebit/.github` hook set (including shared `markdownlint`) and run dependency update hooks first; refresh resulting indirect Go and web lockfile dependencies. ([#128]) ### Added @@ -1032,6 +1033,7 @@ Initial release. [#122]: https://github.com/sensiblebit/certkit/pull/122 [#126]: https://github.com/sensiblebit/certkit/pull/126 [#127]: https://github.com/sensiblebit/certkit/pull/127 +[#128]: https://github.com/sensiblebit/certkit/pull/128 [#73]: https://github.com/sensiblebit/certkit/pull/73 [#64]: https://github.com/sensiblebit/certkit/pull/64 [#63]: https://github.com/sensiblebit/certkit/pull/63 diff --git a/CLAUDE.md b/CLAUDE.md index d7dca28..ee45185 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -343,7 +343,7 @@ pre-commit run --all-files # Manual run against all files - **PC-1 (MUST)** Use `pre-commit run --all-files` for required checks; do not run individual tools manually unless debugging. -Configured hooks: `branch-name`, `commit-message` (commit-msg stage), `goimports`, `go-fix`, `go-vet`, `golangci-lint`, `wasm`, `go-build`, `go-test`, `gendocs`, `prettier`, `vitest`, `wrangler-build`, `markdownlint`. +Configured hooks: `branch-name`, `commit-message` (commit-msg stage), `go-mod-update`, `npm-update`, `goimports`, `go-fix`, `prettier`, `markdownlint`, `go-vet`, `golangci-lint`, `govulncheck`, `gendocs`, `wasm`, `go-build`, `wrangler-build`, `go-test`, `vitest`. ### Tooling gates From f6ce637b830ae17ac05849ee60c5449a96d52488 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 11:55:50 -0500 Subject: [PATCH 05/22] ci: publish certkit-nightly formula from main snapshots --- .github/workflows/nightly-formula.yml | 88 +++++++++++++++++++++++++++ CHANGELOG.md | 1 + 2 files changed, 89 insertions(+) create mode 100644 .github/workflows/nightly-formula.yml diff --git a/.github/workflows/nightly-formula.yml b/.github/workflows/nightly-formula.yml new file mode 100644 index 0000000..d49b0d4 --- /dev/null +++ b/.github/workflows/nightly-formula.yml @@ -0,0 +1,88 @@ +name: Nightly Formula + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + publish-nightly-formula: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Compute nightly metadata + id: meta + run: | + set -euo pipefail + + nightly_ts="$(date -u +%Y%m%d%H%M%S)" + short_sha="${GITHUB_SHA::7}" + nightly_version="nightly-${nightly_ts}-${short_sha}" + source_url="https://github.com/sensiblebit/certkit/archive/${GITHUB_SHA}.tar.gz" + + curl -fsSL "${source_url}" -o /tmp/certkit-nightly.tar.gz + source_sha256="$(sha256sum /tmp/certkit-nightly.tar.gz | awk '{print $1}')" + + { + echo "nightly_version=${nightly_version}" + echo "source_url=${source_url}" + echo "source_sha256=${source_sha256}" + } >> "${GITHUB_OUTPUT}" + + - name: Clone tap repository + env: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + set -euo pipefail + git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/sensiblebit/homebrew-tap.git" /tmp/homebrew-tap + + - name: Render nightly formula + run: | + set -euo pipefail + mkdir -p /tmp/homebrew-tap/Formula + cat > /tmp/homebrew-tap/Formula/certkit-nightly.rb < :build + + def install + ldflags = "-s -w -X main.version=#{version}" + system "go", "build", *std_go_args(ldflags:, output: bin/"certkit-nightly"), "./cmd/certkit" + end + + test do + assert_match version.to_s, shell_output("#{bin}/certkit-nightly --version") + end +end +EOF + + - name: Commit and push tap update + run: | + set -euo pipefail + cd /tmp/homebrew-tap + + if git diff --quiet -- Formula/certkit-nightly.rb; then + echo "No formula changes detected." + exit 0 + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add Formula/certkit-nightly.rb + git commit -m "chore: update certkit-nightly to ${{ steps.meta.outputs.nightly_version }}" + git push origin HEAD:main diff --git a/CHANGELOG.md b/CHANGELOG.md index 270677c..418f964 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add configurable `--aia-timeout` flag to `scan` for AIA certificate fetches (default: `2s`). ([#122]) - Add exported sentinel errors `ErrUnsupportedOutputFormat` and `ErrBinaryOutputRequiresFile` to `cmd/certkit`, plus `ErrUnsupportedKeyAlgorithm` and `ErrUnsupportedCurve` to `internal/keygen`, and wrap command errors with `%w` to support typed matching via `errors.Is`. ([#126]) - Add `ErrParsingIssuerCertificate` sentinel in `cmd/certkit` so `ocsp --issuer` parse failures support stable typed matching via `errors.Is`. ([#127]) +- Add `Nightly Formula` workflow to publish `certkit-nightly` in `sensiblebit/homebrew-tap` on every push to `main`. ([#128]) ### Removed From 83717383b9196bab71e4b296fd23d9106de3ca4e Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 11:57:34 -0500 Subject: [PATCH 06/22] docs: add nightly tap install instructions in README install section --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 45cbdc6..701855f 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,15 @@ Use certkit directly in your browser at **[certkit.pages.dev](https://certkit.pa brew install sensiblebit/tap/certkit ``` +### Homebrew Nightly (main snapshots) + +Nightly formula updates are published automatically on every push to `main`. + +```sh +brew install sensiblebit/tap/certkit-nightly +certkit-nightly --version +``` + ### Debian/Ubuntu (Linux) Download the `.deb` package from the [latest release](https://github.com/sensiblebit/certkit/releases/latest) and install: From 615837d3c275ac62e600180626ac6886093447cb Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:05:48 -0500 Subject: [PATCH 07/22] ci: publish certkit@nightly cask with stable binary name --- .github/workflows/nightly-cask.yml | 138 ++++++++++++++++++++++++++ .github/workflows/nightly-formula.yml | 88 ---------------- CHANGELOG.md | 2 +- README.md | 6 +- 4 files changed, 142 insertions(+), 92 deletions(-) create mode 100644 .github/workflows/nightly-cask.yml delete mode 100644 .github/workflows/nightly-formula.yml diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml new file mode 100644 index 0000000..0676106 --- /dev/null +++ b/.github/workflows/nightly-cask.yml @@ -0,0 +1,138 @@ +name: Nightly Cask + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: write + +jobs: + publish-nightly-cask: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version: stable + check-latest: true + cache: true + + - name: Build nightly archives + id: meta + run: | + set -euo pipefail + + nightly_ts="$(date -u +%Y%m%d%H%M%S)" + short_sha="${GITHUB_SHA::7}" + nightly_version="nightly-${nightly_ts}-${short_sha}" + + mkdir -p /tmp/nightly-dist /tmp/nightly-build + + for target in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64"; do + read -r goos goarch <<<"${target}" + archive="certkit_${nightly_version}_${goos}_${goarch}.tar.gz" + + CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ + go build -trimpath -ldflags "-s -w -X main.version=${nightly_version}" \ + -o /tmp/nightly-build/certkit ./cmd/certkit + + tar -C /tmp/nightly-build -czf "/tmp/nightly-dist/${archive}" certkit + sha="$(sha256sum "/tmp/nightly-dist/${archive}" | awk '{print $1}')" + echo "sha_${goos}_${goarch}=${sha}" >> "${GITHUB_OUTPUT}" + done + + echo "nightly_version=${nightly_version}" >> "${GITHUB_OUTPUT}" + + - name: Publish nightly release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + if gh release view nightly >/dev/null 2>&1; then + gh release edit nightly --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." + else + gh release create nightly --target "${GITHUB_SHA}" --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." + fi + + gh release view nightly --json assets --jq '.assets[].id' | while read -r asset_id; do + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" + done + + gh release upload nightly /tmp/nightly-dist/*.tar.gz + + - name: Clone tap repository + env: + HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + set -euo pipefail + git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/sensiblebit/homebrew-tap.git" /tmp/homebrew-tap + + - name: Render nightly cask + run: | + set -euo pipefail + mkdir -p /tmp/homebrew-tap/Casks + cat > /tmp/homebrew-tap/Casks/certkit@nightly.rb <> "${GITHUB_OUTPUT}" - - - name: Clone tap repository - env: - HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - run: | - set -euo pipefail - git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/sensiblebit/homebrew-tap.git" /tmp/homebrew-tap - - - name: Render nightly formula - run: | - set -euo pipefail - mkdir -p /tmp/homebrew-tap/Formula - cat > /tmp/homebrew-tap/Formula/certkit-nightly.rb < :build - - def install - ldflags = "-s -w -X main.version=#{version}" - system "go", "build", *std_go_args(ldflags:, output: bin/"certkit-nightly"), "./cmd/certkit" - end - - test do - assert_match version.to_s, shell_output("#{bin}/certkit-nightly --version") - end -end -EOF - - - name: Commit and push tap update - run: | - set -euo pipefail - cd /tmp/homebrew-tap - - if git diff --quiet -- Formula/certkit-nightly.rb; then - echo "No formula changes detected." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add Formula/certkit-nightly.rb - git commit -m "chore: update certkit-nightly to ${{ steps.meta.outputs.nightly_version }}" - git push origin HEAD:main diff --git a/CHANGELOG.md b/CHANGELOG.md index 418f964..7e4638c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add configurable `--aia-timeout` flag to `scan` for AIA certificate fetches (default: `2s`). ([#122]) - Add exported sentinel errors `ErrUnsupportedOutputFormat` and `ErrBinaryOutputRequiresFile` to `cmd/certkit`, plus `ErrUnsupportedKeyAlgorithm` and `ErrUnsupportedCurve` to `internal/keygen`, and wrap command errors with `%w` to support typed matching via `errors.Is`. ([#126]) - Add `ErrParsingIssuerCertificate` sentinel in `cmd/certkit` so `ocsp --issuer` parse failures support stable typed matching via `errors.Is`. ([#127]) -- Add `Nightly Formula` workflow to publish `certkit-nightly` in `sensiblebit/homebrew-tap` on every push to `main`. ([#128]) +- Add `Nightly Cask` workflow to publish `certkit@nightly` in `sensiblebit/homebrew-tap` on every push to `main`. ([#128]) ### Removed diff --git a/README.md b/README.md index 701855f..83721ca 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ brew install sensiblebit/tap/certkit ### Homebrew Nightly (main snapshots) -Nightly formula updates are published automatically on every push to `main`. +Nightly cask updates are published automatically on every push to `main`. ```sh -brew install sensiblebit/tap/certkit-nightly -certkit-nightly --version +brew install sensiblebit/tap/certkit@nightly +certkit --version ``` ### Debian/Ubuntu (Linux) From fb51c97c4f0883e983b205abc77852da28790406 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:11:31 -0500 Subject: [PATCH 08/22] ci: enforce mutual conflicts for stable and nightly casks --- .github/workflows/nightly-cask.yml | 30 +++++++++++++++++++++++++++++- .goreleaser.yaml | 1 + 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index 0676106..94e8253 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -120,12 +120,37 @@ cask "certkit@nightly" do end EOF + stable_cask="/tmp/homebrew-tap/Casks/certkit.rb" + if [[ -f "${stable_cask}" ]] && ! grep -q 'conflicts_with cask: "certkit@nightly"' "${stable_cask}"; then + awk ' + /^[[:space:]]*binary "certkit"/ && !inserted { + print " conflicts_with cask: \"certkit@nightly\"" + inserted=1 + } + { print } + END { + if (!inserted) { + exit 7 + } + } + ' "${stable_cask}" > "${stable_cask}.tmp" + mv "${stable_cask}.tmp" "${stable_cask}" + fi + - name: Commit and push tap update run: | set -euo pipefail cd /tmp/homebrew-tap - if git diff --quiet -- Casks/certkit@nightly.rb; then + changed=0 + if ! git diff --quiet -- Casks/certkit@nightly.rb; then + changed=1 + fi + if [[ -f Casks/certkit.rb ]] && ! git diff --quiet -- Casks/certkit.rb; then + changed=1 + fi + + if [[ "${changed}" -eq 0 ]]; then echo "No cask changes detected." exit 0 fi @@ -134,5 +159,8 @@ EOF git config user.email "github-actions[bot]@users.noreply.github.com" git add Casks/certkit@nightly.rb + if [[ -f Casks/certkit.rb ]]; then + git add Casks/certkit.rb + fi git commit -m "chore: update certkit@nightly to ${{ steps.meta.outputs.nightly_version }}" git push origin HEAD:main diff --git a/.goreleaser.yaml b/.goreleaser.yaml index da54511..6dfacc9 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -62,6 +62,7 @@ homebrew_casks: homepage: https://github.com/sensiblebit/certkit description: A certificate management tool that ingests TLS/SSL certificates and keys, catalogs them in SQLite, and exports organized bundles. custom_block: | + conflicts_with cask: "certkit@nightly" postflight do system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}"] From 13d84014da00a311020a64fa308e90ccdd51fab5 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:16:27 -0500 Subject: [PATCH 09/22] test: avoid staticcheck nil-deref false positives --- internal/inspect_test.go | 2 ++ internal/keygen_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/internal/inspect_test.go b/internal/inspect_test.go index ef47b07..7d04e46 100644 --- a/internal/inspect_test.go +++ b/internal/inspect_test.go @@ -116,6 +116,7 @@ func TestInspectFile_PrivateKey(t *testing.T) { } if keyResult == nil { t.Fatal("expected to find a private_key result") + return } if keyResult.KeyType != "RSA" { t.Errorf("key type = %s, want RSA", keyResult.KeyType) @@ -286,6 +287,7 @@ func TestInspectFile_CSR(t *testing.T) { } if csrResult == nil { t.Fatal("expected to find a csr result") + return } if !strings.Contains(csrResult.CSRSubject, "csr.example.com") { t.Errorf("CSR subject should contain CN, got %s", csrResult.CSRSubject) diff --git a/internal/keygen_test.go b/internal/keygen_test.go index 840afa3..ed8abec 100644 --- a/internal/keygen_test.go +++ b/internal/keygen_test.go @@ -152,6 +152,7 @@ func TestGenerateKeyFiles(t *testing.T) { pubBlock, _ := pem.Decode(pubData) if pubBlock == nil { t.Fatal("pub.pem contains no PEM block") + return } parsedPub, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) if err != nil { From 042ebf19981b3e1f331e488386f16ce297464dfd Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:32:39 -0500 Subject: [PATCH 10/22] test: satisfy staticcheck nil-guard in stdout keygen test --- internal/keygen_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/keygen_test.go b/internal/keygen_test.go index ed8abec..882d06a 100644 --- a/internal/keygen_test.go +++ b/internal/keygen_test.go @@ -209,6 +209,7 @@ func TestGenerateKeyFiles_Stdout(t *testing.T) { pubBlock, _ := pem.Decode([]byte(result.PubPEM)) if pubBlock == nil { t.Fatal("PubPEM contains no PEM block") + return } if _, err := x509.ParsePKIXPublicKey(pubBlock.Bytes); err != nil { t.Errorf("PubPEM is not parseable: %v", err) From eab68da2c87c040483a4e72aea3bd6aadd221624 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:37:24 -0500 Subject: [PATCH 11/22] ci: add explicit local SA5011 staticcheck hook --- .golangci-sa5011.yml | 13 +++++++++++++ .pre-commit-config.yaml | 8 ++++++++ 2 files changed, 21 insertions(+) create mode 100644 .golangci-sa5011.yml diff --git a/.golangci-sa5011.yml b/.golangci-sa5011.yml new file mode 100644 index 0000000..83f388b --- /dev/null +++ b/.golangci-sa5011.yml @@ -0,0 +1,13 @@ +version: "2" + +run: + tests: true + +linters: + default: none + enable: + - staticcheck + settings: + staticcheck: + checks: + - SA5011 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2445592..1de42af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,3 +22,11 @@ repos: - id: wrangler-build - id: go-test - id: vitest + - repo: local + hooks: + - id: golangci-lint-sa5011 + name: golangci-lint (staticcheck SA5011) + entry: golangci-lint run -c .golangci-sa5011.yml ./... + language: system + pass_filenames: false + types: [go] From 9ff36b4bfa4ee62f545fb119cac2cc1802097941 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 12:45:59 -0500 Subject: [PATCH 12/22] ci: replace SA5011-only hook with clean-cache full lint --- .golangci-sa5011.yml | 13 ------------- .pre-commit-config.yaml | 6 +++--- 2 files changed, 3 insertions(+), 16 deletions(-) delete mode 100644 .golangci-sa5011.yml diff --git a/.golangci-sa5011.yml b/.golangci-sa5011.yml deleted file mode 100644 index 83f388b..0000000 --- a/.golangci-sa5011.yml +++ /dev/null @@ -1,13 +0,0 @@ -version: "2" - -run: - tests: true - -linters: - default: none - enable: - - staticcheck - settings: - staticcheck: - checks: - - SA5011 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1de42af..ff1099a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -24,9 +24,9 @@ repos: - id: vitest - repo: local hooks: - - id: golangci-lint-sa5011 - name: golangci-lint (staticcheck SA5011) - entry: golangci-lint run -c .golangci-sa5011.yml ./... + - id: golangci-lint-clean-cache + name: golangci-lint (clean cache) + entry: bash -c 'golangci-lint cache clean && golangci-lint run ./...' language: system pass_filenames: false types: [go] From 6b43139f780d7e7a412b9298aaabc04ec00bdbbd Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:14:34 -0500 Subject: [PATCH 13/22] chore: bump shared hooks for golangci cache parity --- .pre-commit-config.yaml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff1099a..c613a1f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/sensiblebit/.github - rev: "18310fd25db7cb182403c55e6495a2f9049b3078" + rev: "c50f977afaa9eaf67c13a08f5fae77ed4baf0015" hooks: - id: branch-name - id: commit-message @@ -22,11 +22,3 @@ repos: - id: wrangler-build - id: go-test - id: vitest - - repo: local - hooks: - - id: golangci-lint-clean-cache - name: golangci-lint (clean cache) - entry: bash -c 'golangci-lint cache clean && golangci-lint run ./...' - language: system - pass_filenames: false - types: [go] From b7b1ab2c43cd58f1b8d66bd8422d65b833829b0a Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:17:26 -0500 Subject: [PATCH 14/22] chore: repin shared hooks to merged .github main --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c613a1f..d6302b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/sensiblebit/.github - rev: "c50f977afaa9eaf67c13a08f5fae77ed4baf0015" + rev: "ddffb3a8d3bc086e7f51cec776d94d3ef21755c7" hooks: - id: branch-name - id: commit-message From 2d3b078b9caa7e1f910e927cf65704a6dad52802 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:20:13 -0500 Subject: [PATCH 15/22] ci: harden nightly cask publish workflow --- .github/workflows/nightly-cask.yml | 42 ++++++++++++++++-------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index 94e8253..dd3d9a9 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -6,6 +6,10 @@ on: - main workflow_dispatch: +concurrency: + group: nightly-cask + cancel-in-progress: true + permissions: contents: write @@ -70,18 +74,19 @@ jobs: gh release upload nightly /tmp/nightly-dist/*.tar.gz - - name: Clone tap repository - env: - HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - run: | - set -euo pipefail - git clone "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/sensiblebit/homebrew-tap.git" /tmp/homebrew-tap + - name: Checkout tap repository + uses: actions/checkout@v6 + with: + repository: sensiblebit/homebrew-tap + path: homebrew-tap + fetch-depth: 0 + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - name: Render nightly cask run: | set -euo pipefail - mkdir -p /tmp/homebrew-tap/Casks - cat > /tmp/homebrew-tap/Casks/certkit@nightly.rb < homebrew-tap/Casks/certkit@nightly.rb < Date: Tue, 3 Mar 2026 13:25:19 -0500 Subject: [PATCH 16/22] ci: harden nightly cask update flow --- .github/workflows/nightly-cask.yml | 58 +++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index dd3d9a9..75d6301 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -63,15 +63,11 @@ jobs: set -euo pipefail if gh release view nightly >/dev/null 2>&1; then - gh release edit nightly --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." + gh release edit nightly --target "${GITHUB_SHA}" --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." else gh release create nightly --target "${GITHUB_SHA}" --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." fi - gh release view nightly --json assets --jq '.assets[].id' | while read -r asset_id; do - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - done - gh release upload nightly /tmp/nightly-dist/*.tar.gz - name: Checkout tap repository @@ -133,19 +129,29 @@ EOF stable_cask="homebrew-tap/Casks/certkit.rb" if [[ -f "${stable_cask}" ]] && ! grep -q 'conflicts_with cask: "certkit@nightly"' "${stable_cask}"; then - awk ' - /^[[:space:]]*binary "certkit"/ && !inserted { - print " conflicts_with cask: \"certkit@nightly\"" - inserted=1 - } - { print } - END { - if (!inserted) { - exit 7 + if grep -q '^[[:space:]]*binary "certkit"' "${stable_cask}"; then + awk ' + /^[[:space:]]*binary "certkit"/ && !inserted { + print " conflicts_with cask: \"certkit@nightly\"" + inserted=1 + } + { print } + ' "${stable_cask}" > "${stable_cask}.tmp" + mv "${stable_cask}.tmp" "${stable_cask}" + elif grep -q '^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$' "${stable_cask}"; then + awk ' + /^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$/ && !inserted { + print + print " conflicts_with cask: \"certkit@nightly\"" + inserted=1 + next } - } - ' "${stable_cask}" > "${stable_cask}.tmp" - mv "${stable_cask}.tmp" "${stable_cask}" + { print } + ' "${stable_cask}" > "${stable_cask}.tmp" + mv "${stable_cask}.tmp" "${stable_cask}" + else + echo "::warning::Unable to patch ${stable_cask}; add conflicts_with cask: \\\"certkit@nightly\\\" manually if needed." + fi fi - name: Commit and push tap update @@ -168,3 +174,21 @@ EOF fi git commit -m "chore: update certkit@nightly to ${{ steps.meta.outputs.nightly_version }}" git push origin HEAD:main + + - name: Prune superseded nightly release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + keep_prefix="certkit_${{ steps.meta.outputs.nightly_version }}_" + gh release view nightly --json assets --jq '.assets[] | [.id, .name] | @tsv' \ + | while IFS=$'\t' read -r asset_id asset_name; do + if [[ "${asset_name}" != certkit_* ]]; then + continue + fi + if [[ "${asset_name}" == "${keep_prefix}"* ]]; then + continue + fi + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" + done From 7476fafbe32a8e5d1cf63445b36aae495388551a Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:32:45 -0500 Subject: [PATCH 17/22] ci: scope nightly quarantine cleanup to macOS --- .github/workflows/nightly-cask.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index 75d6301..d66d1a5 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -96,14 +96,14 @@ cask "certkit@nightly" do conflicts_with cask: "certkit" - postflight do - system_command "/usr/bin/xattr", - args: ["-dr", "com.apple.quarantine", "#{staged_path}"] - end - binary "certkit" on_macos do + postflight do + system_command "/usr/bin/xattr", + args: ["-dr", "com.apple.quarantine", "#{staged_path}"] + end + on_intel do url "https://github.com/sensiblebit/certkit/releases/download/nightly/certkit_#{version}_darwin_amd64.tar.gz" sha256 "${{ steps.meta.outputs.sha_darwin_amd64 }}" From 7f031524dd47fb25db1bce1b099813a63f386cf4 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:33:22 -0500 Subject: [PATCH 18/22] ci: rebase tap updates before push --- .github/workflows/nightly-cask.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index d66d1a5..068fdf7 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -173,6 +173,7 @@ EOF git add Casks/certkit.rb fi git commit -m "chore: update certkit@nightly to ${{ steps.meta.outputs.nightly_version }}" + git pull --rebase origin main git push origin HEAD:main - name: Prune superseded nightly release assets From 43108d636ff5f68d64a82e6c5bd3a5c6f818caec Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:37:28 -0500 Subject: [PATCH 19/22] ci: refactor nightly cask workflow scripts --- .github/scripts/nightly-cask.sh | 246 +++++++++++++++++++++++++++++ .github/workflows/nightly-cask.yml | 166 +++---------------- 2 files changed, 271 insertions(+), 141 deletions(-) create mode 100755 .github/scripts/nightly-cask.sh diff --git a/.github/scripts/nightly-cask.sh b/.github/scripts/nightly-cask.sh new file mode 100755 index 0000000..9cd2c66 --- /dev/null +++ b/.github/scripts/nightly-cask.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -euo pipefail + +NIGHTLY_TAG="${NIGHTLY_TAG:-nightly}" +NIGHTLY_DIST_DIR="${NIGHTLY_DIST_DIR:-/tmp/nightly-dist}" +NIGHTLY_BUILD_DIR="${NIGHTLY_BUILD_DIR:-/tmp/nightly-build}" +TAP_REPO_PATH="${TAP_REPO_PATH:-homebrew-tap}" +NIGHTLY_RELEASE_TITLE="${NIGHTLY_RELEASE_TITLE:-Nightly}" +NIGHTLY_RELEASE_NOTES="${NIGHTLY_RELEASE_NOTES:-Auto-updated nightly snapshot of main.}" + +usage() { + cat <<'EOF' +Usage: .github/scripts/nightly-cask.sh + +Commands: + build-archives + publish-release-assets + render-nightly-cask + patch-stable-cask-conflict + commit-and-push-tap-update + prune-superseded-assets +EOF +} + +require_env() { + local var_name="$1" + if [[ -z "${!var_name:-}" ]]; then + echo "::error::Missing required environment variable: ${var_name}" + exit 1 + fi +} + +build_archives() { + require_env "GITHUB_SHA" + require_env "GITHUB_OUTPUT" + + local nightly_ts short_sha nightly_version target goos goarch archive sha + + nightly_ts="$(date -u +%Y%m%d%H%M%S)" + short_sha="${GITHUB_SHA::7}" + nightly_version="nightly-${nightly_ts}-${short_sha}" + + mkdir -p "${NIGHTLY_DIST_DIR}" "${NIGHTLY_BUILD_DIR}" + + for target in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64"; do + read -r goos goarch <<<"${target}" + archive="certkit_${nightly_version}_${goos}_${goarch}.tar.gz" + + CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ + go build -trimpath -ldflags "-s -w -X main.version=${nightly_version}" \ + -o "${NIGHTLY_BUILD_DIR}/certkit" ./cmd/certkit + + tar -C "${NIGHTLY_BUILD_DIR}" -czf "${NIGHTLY_DIST_DIR}/${archive}" certkit + sha="$(sha256sum "${NIGHTLY_DIST_DIR}/${archive}" | awk '{print $1}')" + echo "sha_${goos}_${goarch}=${sha}" >> "${GITHUB_OUTPUT}" + done + + echo "nightly_version=${nightly_version}" >> "${GITHUB_OUTPUT}" +} + +publish_release_assets() { + require_env "GITHUB_SHA" + + if gh release view "${NIGHTLY_TAG}" >/dev/null 2>&1; then + gh release edit "${NIGHTLY_TAG}" \ + --target "${GITHUB_SHA}" \ + --prerelease \ + --title "${NIGHTLY_RELEASE_TITLE}" \ + --notes "${NIGHTLY_RELEASE_NOTES}" + else + gh release create "${NIGHTLY_TAG}" \ + --target "${GITHUB_SHA}" \ + --prerelease \ + --title "${NIGHTLY_RELEASE_TITLE}" \ + --notes "${NIGHTLY_RELEASE_NOTES}" + fi + + gh release upload "${NIGHTLY_TAG}" "${NIGHTLY_DIST_DIR}"/*.tar.gz +} + +render_nightly_cask() { + require_env "NIGHTLY_VERSION" + require_env "SHA_DARWIN_AMD64" + require_env "SHA_DARWIN_ARM64" + require_env "SHA_LINUX_AMD64" + require_env "SHA_LINUX_ARM64" + + mkdir -p "${TAP_REPO_PATH}/Casks" + cat > "${TAP_REPO_PATH}/Casks/certkit@nightly.rb" < "${stable_cask}.tmp" + mv "${stable_cask}.tmp" "${stable_cask}" + return + fi + + if grep -q '^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$' "${stable_cask}"; then + awk ' + /^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$/ && !inserted { + print + print " conflicts_with cask: \"certkit@nightly\"" + inserted=1 + next + } + { print } + ' "${stable_cask}" > "${stable_cask}.tmp" + mv "${stable_cask}.tmp" "${stable_cask}" + return + fi + + echo "::warning::Unable to patch ${stable_cask}; add conflicts_with cask: \\\"certkit@nightly\\\" manually if needed." +} + +commit_and_push_tap_update() { + require_env "NIGHTLY_VERSION" + + local changed_files + cd "${TAP_REPO_PATH}" + + changed_files="$(git status --porcelain -- Casks/certkit@nightly.rb Casks/certkit.rb || true)" + if [[ -z "${changed_files}" ]]; then + echo "No cask changes detected." + return + fi + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add Casks/certkit@nightly.rb + if [[ -f Casks/certkit.rb ]]; then + git add Casks/certkit.rb + fi + git commit -m "chore: update certkit@nightly to ${NIGHTLY_VERSION}" + git pull --rebase origin main + git push origin HEAD:main +} + +prune_superseded_assets() { + require_env "NIGHTLY_VERSION" + require_env "GITHUB_REPOSITORY" + + local keep_prefix asset_id asset_name + keep_prefix="certkit_${NIGHTLY_VERSION}_" + + gh release view "${NIGHTLY_TAG}" --json assets --jq '.assets[] | [.id, .name] | @tsv' \ + | while IFS=$'\t' read -r asset_id asset_name; do + if [[ "${asset_name}" != certkit_* ]]; then + continue + fi + if [[ "${asset_name}" == "${keep_prefix}"* ]]; then + continue + fi + gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" + done +} + +main() { + local command="${1:-}" + case "${command}" in + build-archives) + build_archives + ;; + publish-release-assets) + publish_release_assets + ;; + render-nightly-cask) + render_nightly_cask + ;; + patch-stable-cask-conflict) + patch_stable_cask_conflict + ;; + commit-and-push-tap-update) + commit_and_push_tap_update + ;; + prune-superseded-assets) + prune_superseded_assets + ;; + *) + usage + exit 1 + ;; + esac +} + +main "$@" diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index 068fdf7..2b8e3fd 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -13,6 +13,14 @@ concurrency: permissions: contents: write +env: + NIGHTLY_TAG: nightly + NIGHTLY_DIST_DIR: /tmp/nightly-dist + NIGHTLY_BUILD_DIR: /tmp/nightly-build + NIGHTLY_RELEASE_TITLE: Nightly + NIGHTLY_RELEASE_NOTES: Auto-updated nightly snapshot of main. + TAP_REPO_PATH: homebrew-tap + jobs: publish-nightly-cask: runs-on: ubuntu-latest @@ -32,164 +40,40 @@ jobs: - name: Build nightly archives id: meta - run: | - set -euo pipefail - - nightly_ts="$(date -u +%Y%m%d%H%M%S)" - short_sha="${GITHUB_SHA::7}" - nightly_version="nightly-${nightly_ts}-${short_sha}" - - mkdir -p /tmp/nightly-dist /tmp/nightly-build - - for target in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64"; do - read -r goos goarch <<<"${target}" - archive="certkit_${nightly_version}_${goos}_${goarch}.tar.gz" - - CGO_ENABLED=0 GOOS="${goos}" GOARCH="${goarch}" \ - go build -trimpath -ldflags "-s -w -X main.version=${nightly_version}" \ - -o /tmp/nightly-build/certkit ./cmd/certkit - - tar -C /tmp/nightly-build -czf "/tmp/nightly-dist/${archive}" certkit - sha="$(sha256sum "/tmp/nightly-dist/${archive}" | awk '{print $1}')" - echo "sha_${goos}_${goarch}=${sha}" >> "${GITHUB_OUTPUT}" - done - - echo "nightly_version=${nightly_version}" >> "${GITHUB_OUTPUT}" + run: ./.github/scripts/nightly-cask.sh build-archives - name: Publish nightly release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - - if gh release view nightly >/dev/null 2>&1; then - gh release edit nightly --target "${GITHUB_SHA}" --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." - else - gh release create nightly --target "${GITHUB_SHA}" --prerelease --title "Nightly" --notes "Auto-updated nightly snapshot of main." - fi - - gh release upload nightly /tmp/nightly-dist/*.tar.gz + run: ./.github/scripts/nightly-cask.sh publish-release-assets - name: Checkout tap repository uses: actions/checkout@v6 with: repository: sensiblebit/homebrew-tap - path: homebrew-tap + path: ${{ env.TAP_REPO_PATH }} fetch-depth: 0 token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - name: Render nightly cask - run: | - set -euo pipefail - mkdir -p homebrew-tap/Casks - cat > homebrew-tap/Casks/certkit@nightly.rb < "${stable_cask}.tmp" - mv "${stable_cask}.tmp" "${stable_cask}" - elif grep -q '^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$' "${stable_cask}"; then - awk ' - /^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$/ && !inserted { - print - print " conflicts_with cask: \"certkit@nightly\"" - inserted=1 - next - } - { print } - ' "${stable_cask}" > "${stable_cask}.tmp" - mv "${stable_cask}.tmp" "${stable_cask}" - else - echo "::warning::Unable to patch ${stable_cask}; add conflicts_with cask: \\\"certkit@nightly\\\" manually if needed." - fi - fi + - name: Patch stable cask conflict + run: ./.github/scripts/nightly-cask.sh patch-stable-cask-conflict - name: Commit and push tap update - run: | - set -euo pipefail - cd homebrew-tap - - changed_files="$(git status --porcelain -- Casks/certkit@nightly.rb Casks/certkit.rb || true)" - if [[ -z "${changed_files}" ]]; then - echo "No cask changes detected." - exit 0 - fi - - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - git add Casks/certkit@nightly.rb - if [[ -f Casks/certkit.rb ]]; then - git add Casks/certkit.rb - fi - git commit -m "chore: update certkit@nightly to ${{ steps.meta.outputs.nightly_version }}" - git pull --rebase origin main - git push origin HEAD:main + env: + NIGHTLY_VERSION: ${{ steps.meta.outputs.nightly_version }} + run: ./.github/scripts/nightly-cask.sh commit-and-push-tap-update - name: Prune superseded nightly release assets env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - - keep_prefix="certkit_${{ steps.meta.outputs.nightly_version }}_" - gh release view nightly --json assets --jq '.assets[] | [.id, .name] | @tsv' \ - | while IFS=$'\t' read -r asset_id asset_name; do - if [[ "${asset_name}" != certkit_* ]]; then - continue - fi - if [[ "${asset_name}" == "${keep_prefix}"* ]]; then - continue - fi - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" - done + NIGHTLY_VERSION: ${{ steps.meta.outputs.nightly_version }} + run: ./.github/scripts/nightly-cask.sh prune-superseded-assets From 4cd7a2767a4f2306a570578d744620f71c08fdd2 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 13:38:04 -0500 Subject: [PATCH 20/22] ci: gate nightly publish job to main --- .github/workflows/nightly-cask.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/nightly-cask.yml b/.github/workflows/nightly-cask.yml index 2b8e3fd..a3c850d 100644 --- a/.github/workflows/nightly-cask.yml +++ b/.github/workflows/nightly-cask.yml @@ -23,6 +23,7 @@ env: jobs: publish-nightly-cask: + if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: From c2a5fac880362dc33e9cca8134bb78fab04b92e8 Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 20:45:04 -0500 Subject: [PATCH 21/22] ci: fix nightly cask script review findings --- .github/scripts/nightly-cask.sh | 10 +++++----- CHANGELOG.md | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/scripts/nightly-cask.sh b/.github/scripts/nightly-cask.sh index 9cd2c66..ed069ee 100755 --- a/.github/scripts/nightly-cask.sh +++ b/.github/scripts/nightly-cask.sh @@ -155,7 +155,7 @@ patch_stable_cask_conflict() { return fi - if grep -q '^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$' "${stable_cask}"; then + if grep -Eq '^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$' "${stable_cask}"; then awk ' /^[[:space:]]*cask[[:space:]]+"certkit"[[:space:]]+do[[:space:]]*$/ && !inserted { print @@ -200,18 +200,18 @@ prune_superseded_assets() { require_env "NIGHTLY_VERSION" require_env "GITHUB_REPOSITORY" - local keep_prefix asset_id asset_name + local keep_prefix asset_api_url asset_name keep_prefix="certkit_${NIGHTLY_VERSION}_" - gh release view "${NIGHTLY_TAG}" --json assets --jq '.assets[] | [.id, .name] | @tsv' \ - | while IFS=$'\t' read -r asset_id asset_name; do + gh release view "${NIGHTLY_TAG}" --json assets --jq '.assets[] | [.apiUrl, .name] | @tsv' \ + | while IFS=$'\t' read -r asset_api_url asset_name; do if [[ "${asset_name}" != certkit_* ]]; then continue fi if [[ "${asset_name}" == "${keep_prefix}"* ]]; then continue fi - gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" + gh api -X DELETE "${asset_api_url}" done } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e4638c..22e69fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Normalize `connect` verify status labels from `OK`/`FAILED` to lowercase `ok`/`failed`. ([#112]) - Consolidate shared `connect` status-line formatting between standard and verbose text output paths. ([#121]) - Centralize pre-commit hooks under the shared `sensiblebit/.github` hook set (including shared `markdownlint`) and run dependency update hooks first; refresh resulting indirect Go and web lockfile dependencies. ([#128]) +- Add Homebrew cask conflict metadata so stable `certkit` and `certkit@nightly` are explicitly mutually exclusive installs. ([#128]) ### Added From 08b83bf1f6a9c59cfad1bce54162300a37df3f5e Mon Sep 17 00:00:00 2001 From: Daniel Wood Date: Tue, 3 Mar 2026 22:39:52 -0500 Subject: [PATCH 22/22] docs: move nightly cask changelog note to added --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e69fc..c2d1c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Normalize `connect` verify status labels from `OK`/`FAILED` to lowercase `ok`/`failed`. ([#112]) - Consolidate shared `connect` status-line formatting between standard and verbose text output paths. ([#121]) - Centralize pre-commit hooks under the shared `sensiblebit/.github` hook set (including shared `markdownlint`) and run dependency update hooks first; refresh resulting indirect Go and web lockfile dependencies. ([#128]) -- Add Homebrew cask conflict metadata so stable `certkit` and `certkit@nightly` are explicitly mutually exclusive installs. ([#128]) ### Added @@ -23,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add exported sentinel errors `ErrUnsupportedOutputFormat` and `ErrBinaryOutputRequiresFile` to `cmd/certkit`, plus `ErrUnsupportedKeyAlgorithm` and `ErrUnsupportedCurve` to `internal/keygen`, and wrap command errors with `%w` to support typed matching via `errors.Is`. ([#126]) - Add `ErrParsingIssuerCertificate` sentinel in `cmd/certkit` so `ocsp --issuer` parse failures support stable typed matching via `errors.Is`. ([#127]) - Add `Nightly Cask` workflow to publish `certkit@nightly` in `sensiblebit/homebrew-tap` on every push to `main`. ([#128]) +- Add Homebrew cask conflict metadata so stable `certkit` and `certkit@nightly` are explicitly mutually exclusive installs. ([#128]) ### Removed