diff --git a/.github/scripts/nightly-cask.sh b/.github/scripts/nightly-cask.sh new file mode 100755 index 0000000..ed069ee --- /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 -Eq '^[[: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_api_url asset_name + keep_prefix="certkit_${NIGHTLY_VERSION}_" + + 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 "${asset_api_url}" + 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 new file mode 100644 index 0000000..a3c850d --- /dev/null +++ b/.github/workflows/nightly-cask.yml @@ -0,0 +1,80 @@ +name: Nightly Cask + +on: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: nightly-cask + cancel-in-progress: true + +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: + if: github.ref == 'refs/heads/main' + 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: ./.github/scripts/nightly-cask.sh build-archives + + - name: Publish nightly release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./.github/scripts/nightly-cask.sh publish-release-assets + + - name: Checkout tap repository + uses: actions/checkout@v6 + with: + repository: sensiblebit/homebrew-tap + path: ${{ env.TAP_REPO_PATH }} + fetch-depth: 0 + token: ${{ secrets.HOMEBREW_TAP_TOKEN }} + + - name: Render nightly cask + env: + NIGHTLY_VERSION: ${{ steps.meta.outputs.nightly_version }} + SHA_DARWIN_AMD64: ${{ steps.meta.outputs.sha_darwin_amd64 }} + SHA_DARWIN_ARM64: ${{ steps.meta.outputs.sha_darwin_arm64 }} + SHA_LINUX_AMD64: ${{ steps.meta.outputs.sha_linux_amd64 }} + SHA_LINUX_ARM64: ${{ steps.meta.outputs.sha_linux_arm64 }} + run: ./.github/scripts/nightly-cask.sh render-nightly-cask + + - name: Patch stable cask conflict + run: ./.github/scripts/nightly-cask.sh patch-stable-cask-conflict + + - name: Commit and push tap update + 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 }} + NIGHTLY_VERSION: ${{ steps.meta.outputs.nightly_version }} + run: ./.github/scripts/nightly-cask.sh prune-superseded-assets 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}"] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dcd9b41..d6302b4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,35 +1,24 @@ 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" + rev: "ddffb3a8d3bc086e7f51cec776d94d3ef21755c7" 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: vitest - - id: wrangler-build - - id: go-mod-update - - id: npm-update - - # ── Markdown ── - - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.47.0 - hooks: - - id: markdownlint - args: [--config, .markdownlint.yaml] diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c595d4..c2d1c35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,12 +14,15 @@ 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 - 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 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 @@ -1032,6 +1035,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 294b567..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: `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), `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 diff --git a/README.md b/README.md index 45cbdc6..83721ca 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 cask updates are published automatically on every push to `main`. + +```sh +brew install sensiblebit/tap/certkit@nightly +certkit --version +``` + ### Debian/Ubuntu (Linux) Download the `.deb` package from the [latest release](https://github.com/sensiblebit/certkit/releases/latest) and install: 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/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..882d06a 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 { @@ -208,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) 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": [ {