Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fcb0b1a
chore: centralize pre-commit hooks around shared .github config
danielewood Mar 3, 2026
3a71185
chore: consume shared markdownlint hook to remove local pre-commit co…
danielewood Mar 3, 2026
a7bcfcd
chore: run dependency updates before pre-commit validation hooks
danielewood Mar 3, 2026
6089dba
docs: align hook docs and changelog with update-first pre-commit flow
danielewood Mar 3, 2026
f6ce637
ci: publish certkit-nightly formula from main snapshots
danielewood Mar 3, 2026
8371738
docs: add nightly tap install instructions in README install section
danielewood Mar 3, 2026
615837d
ci: publish certkit@nightly cask with stable binary name
danielewood Mar 3, 2026
fb51c97
ci: enforce mutual conflicts for stable and nightly casks
danielewood Mar 3, 2026
13d8401
test: avoid staticcheck nil-deref false positives
danielewood Mar 3, 2026
042ebf1
test: satisfy staticcheck nil-guard in stdout keygen test
danielewood Mar 3, 2026
eab68da
ci: add explicit local SA5011 staticcheck hook
danielewood Mar 3, 2026
9ff36b4
ci: replace SA5011-only hook with clean-cache full lint
danielewood Mar 3, 2026
6b43139
chore: bump shared hooks for golangci cache parity
danielewood Mar 3, 2026
b7b1ab2
chore: repin shared hooks to merged .github main
danielewood Mar 3, 2026
2d3b078
ci: harden nightly cask publish workflow
danielewood Mar 3, 2026
621f34f
ci: harden nightly cask update flow
danielewood Mar 3, 2026
7476faf
ci: scope nightly quarantine cleanup to macOS
danielewood Mar 3, 2026
7f03152
ci: rebase tap updates before push
danielewood Mar 3, 2026
43108d6
ci: refactor nightly cask workflow scripts
danielewood Mar 3, 2026
4cd7a27
ci: gate nightly publish job to main
danielewood Mar 3, 2026
c2a5fac
ci: fix nightly cask script review findings
danielewood Mar 4, 2026
08b83bf
docs: move nightly cask changelog note to added
danielewood Mar 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 246 additions & 0 deletions .github/scripts/nightly-cask.sh
Original file line number Diff line number Diff line change
@@ -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 <command>

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" <<EOF
# This file is auto-generated by certkit nightly workflow. DO NOT EDIT.
cask "certkit@nightly" do
name "certkit nightly"
desc "Nightly snapshots of certkit certificate tooling"
homepage "https://github.com/sensiblebit/certkit"
version "${NIGHTLY_VERSION}"

livecheck do
skip "Updated on every push to main."
end

conflicts_with cask: "certkit"

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 "${SHA_DARWIN_AMD64}"
end
on_arm do
url "https://github.com/sensiblebit/certkit/releases/download/nightly/certkit_#{version}_darwin_arm64.tar.gz"
sha256 "${SHA_DARWIN_ARM64}"
end
end

on_linux do
on_intel do
url "https://github.com/sensiblebit/certkit/releases/download/nightly/certkit_#{version}_linux_amd64.tar.gz"
sha256 "${SHA_LINUX_AMD64}"
end
on_arm do
url "https://github.com/sensiblebit/certkit/releases/download/nightly/certkit_#{version}_linux_arm64.tar.gz"
sha256 "${SHA_LINUX_ARM64}"
end
end
end
EOF
}

patch_stable_cask_conflict() {
local stable_cask
stable_cask="${TAP_REPO_PATH}/Casks/certkit.rb"

if [[ ! -f "${stable_cask}" ]]; then
return
fi
if grep -q 'conflicts_with cask: "certkit@nightly"' "${stable_cask}"; then
return
fi

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}"
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 "$@"
80 changes: 80 additions & 0 deletions .github/workflows/nightly-cask.yml
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}"]
Expand Down
33 changes: 11 additions & 22 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -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]
Loading