From 2d5c5f5e5969929a2b4d7efee068c2430e002f96 Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 14:14:15 +0100 Subject: [PATCH 1/6] add automation for deciding next version and release notes using AI --- .github/workflows/release.yml | 14 +++- Makefile | 31 +++++++- bin/release-interactive.sh | 54 ++++++++++++++ bin/suggest-version-ai.sh | 133 ++++++++++++++++++++++++++++++++++ release-guide.md | 28 ++++++- 5 files changed, 253 insertions(+), 7 deletions(-) create mode 100755 bin/release-interactive.sh create mode 100755 bin/suggest-version-ai.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc0a62aba..e530cf631 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -123,11 +123,23 @@ jobs: with: go-version: ${{ env.GO_VERSION }} + # Use release notes from the tag body when present (set by interactive `make release` or `make release tag=vX.Y.Z` with dist/release_notes.md). Otherwise GoReleaser uses its default changelog. + - name: Get release notes from tag + id: get-tag-notes + run: | + TAG="${GITHUB_REF#refs/tags/}" + BODY=$(git tag -l --format='%(contents:body)' "$TAG") + if [ -n "$BODY" ]; then + mkdir -p dist + echo "$BODY" > dist/release_notes.md + echo "args=--release-notes=dist/release_notes.md" >> $GITHUB_OUTPUT + fi + - name: Run GoReleaser uses: goreleaser/goreleaser-action@v5 with: version: latest - args: release --clean + args: release --clean ${{ steps.get-tag-notes.outputs.args }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FURY_TOKEN: ${{ secrets.FURY_TOKEN }} diff --git a/Makefile b/Makefile index 354565a72..139857914 100644 --- a/Makefile +++ b/Makefile @@ -174,11 +174,34 @@ helm-docs: helm-lint @cd charts/k8s-reporter && docker run --rm --volume "$(PWD):/helm-docs" jnorwood/helm-docs:latest --template-files README.md.gotmpl,_templates.gotmpl --output-file README.md @cd charts/k8s-reporter && docker run --rm --volume "$(PWD):/helm-docs" jnorwood/helm-docs:latest --template-files README.md.gotmpl,_templates.gotmpl --output-file ../../docs.kosli.com/content/helm/_index.md + +# Suggest next semver and changelog using Claude (or KOSLI_RELEASE_SUGGEST_URL proxy). +# Writes changelog to dist/release_notes.md for use with goreleaser --release-notes. +# Requires: ANTHROPIC_API_KEY or KOSLI_RELEASE_SUGGEST_URL; jq, curl. +# See scripts/README-release-suggest.md for seamless (no API key) options. +# Usage: make suggest-version-ai [BASE_REF=v1.2.3] +suggest-version-ai: + @command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1) + @bin/suggest-version-ai.sh $(BASE_REF) -o dist/release_notes.md + +# Release: without tag → suggest version + changelog, then interactive edit & confirm, then tag and push. +# With tag → escape hatch: create annotated tag (body = dist/release_notes.md if present), push. No AI, no prompt. +# Release notes are carried in the tag message so GitHub Actions can pass them to GoReleaser. +# For suggest step: ANTHROPIC_API_KEY or KOSLI_RELEASE_SUGGEST_URL (or use secret manager, see scripts/README-release-suggest.md). release: - @git remote update - @git status -uno | grep --silent "Your branch is up to date" || (echo "ERROR: your branch is NOT up to date with remote" && return 1) - git tag -a $(tag) -m"$(tag)" - git push origin $(tag) + @if [ -z "$(tag)" ]; then \ + command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1); \ + bin/suggest-version-ai.sh -o dist/release_notes.md; \ + if [ ! -f dist/suggested_version ]; then \ + echo "Suggestion failed or no previous tag. Use: make release tag=vX.Y.Z"; exit 1; \ + fi; \ + bin/release-interactive.sh; \ + else \ + git remote update; \ + git status -uno | grep --silent "Your branch is up to date" || (echo "ERROR: your branch is NOT up to date with remote" && exit 1); \ + ([ -f dist/release_notes.md ] && git tag -a $(tag) -F dist/release_notes.md) || git tag -a $(tag) -m"$(tag)"; \ + git push origin $(tag); \ + fi # check-links: # @docker run -v ${PWD}:/tmp:ro --rm -i --entrypoint '' ghcr.io/tcort/markdown-link-check:stable /bin/sh -c 'find /tmp/docs.kosli.com/content -name \*.md -print0 | xargs -0 -n1 markdown-link-check -q -c /tmp/link-checker-config.json' diff --git a/bin/release-interactive.sh b/bin/release-interactive.sh new file mode 100755 index 000000000..463c02da2 --- /dev/null +++ b/bin/release-interactive.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Interactive step after suggest-version-ai: show version and release notes, +# let user edit notes, then confirm before creating tag and pushing. +# Called from Make when running `make release` (no tag). +# Requires: dist/suggested_version and dist/release_notes.md exist. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$REPO_ROOT" + +SUGGESTED_VERSION_FILE="dist/suggested_version" +RELEASE_NOTES_FILE="dist/release_notes.md" + +if [ ! -f "$SUGGESTED_VERSION_FILE" ] || [ ! -f "$RELEASE_NOTES_FILE" ]; then + echo "Missing $SUGGESTED_VERSION_FILE or $RELEASE_NOTES_FILE. Run suggest-version-ai first." >&2 + exit 1 +fi + +VER=$(cat "$SUGGESTED_VERSION_FILE") +if [ -z "$VER" ]; then + echo "Suggested version is empty. Run suggest-version-ai or use: make release tag=vX.Y.Z" >&2 + exit 1 +fi + +echo "Suggested tag: $VER" +echo "" +echo "Release notes ($RELEASE_NOTES_FILE):" +echo "---" +cat "$RELEASE_NOTES_FILE" +echo "---" +echo "" + +# Let user edit release notes +read -r -p "Press Enter to edit release notes (or to continue without editing)..." +"${EDITOR:-vi}" "$RELEASE_NOTES_FILE" + +echo "" +read -r -p "Create tag $VER and push? [y/N] " confirm +case "$confirm" in + y|Y) ;; + *) echo "Aborted. To release later run: make release tag=$VER"; exit 0 ;; +esac + +git remote update +if ! git status -uno | grep -q "Your branch is up to date"; then + echo "ERROR: your branch is NOT up to date with remote" >&2 + exit 1 +fi + +git tag -a "$VER" -F "$RELEASE_NOTES_FILE" +git push origin "$VER" +echo "Pushed tag $VER. Release workflow will run on GitHub." diff --git a/bin/suggest-version-ai.sh b/bin/suggest-version-ai.sh new file mode 100755 index 000000000..d82c196a4 --- /dev/null +++ b/bin/suggest-version-ai.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Suggest next semver and changelog by sending the git diff to Claude. +# Does not rely on commit messages. Changelog is suitable for GoReleaser --release-notes. +# +# Auth (first non-empty wins): +# - ANTHROPIC_API_KEY: call Claude directly. +# - OP_ANTHROPIC_API_KEY_REF: 1Password reference (default below; override if your item path differs). +# +# Requires: curl, jq; for 1Password: op CLI +# Usage: bin/suggest-version-ai.sh [base_ref] [-o release_notes.md] +# base_ref defaults to the latest git tag. +# -o FILE write changelog markdown to FILE (default: dist/release_notes.md) +# +# Output: bump (major|minor|patch), next_version (e.g. v1.3.0), and changelog file. + +set -e + +BASE_REF="" +RELEASE_NOTES_FILE="dist/release_notes.md" +while [[ $# -gt 0 ]]; do + case "$1" in + -o) RELEASE_NOTES_FILE="$2"; shift 2 ;; + *) BASE_REF="$1"; shift ;; + esac +done +BASE_REF="${BASE_REF:-$(git describe --tags --abbrev=0 2>/dev/null)}" + +if [ -z "$BASE_REF" ]; then + echo "ERROR: No base ref. Pass a tag or branch, or create a tag first." >&2 + exit 1 +fi + +# Cap diff size to stay within context +MAX_DIFF_CHARS=50000 +DIFF="$(git diff "$BASE_REF"..HEAD 2>/dev/null | head -c "$MAX_DIFF_CHARS")" + +# Get API key from 1Password if not set (default ref; override with OP_ANTHROPIC_API_KEY_REF) +OP_ANTHROPIC_API_KEY_REF="${OP_ANTHROPIC_API_KEY_REF:-op://Shared/Anthropic API Key/credential}" +if [ -z "$ANTHROPIC_API_KEY" ]; then + if command -v op >/dev/null 2>&1; then + ANTHROPIC_API_KEY=$(op read "$OP_ANTHROPIC_API_KEY_REF" 2>/dev/null) || true + fi +fi +if [ -z "$ANTHROPIC_API_KEY" ]; then + echo "ERROR: Set ANTHROPIC_API_KEY or OP_ANTHROPIC_API_KEY_REF (1Password). See scripts/README-release-suggest.md." >&2 + exit 1 +fi + +if [ -z "$DIFF" ]; then + echo "No changes since $BASE_REF. Bump: patch (no change)." >&2 + CHANGELOG="No code changes since $BASE_REF." + mkdir -p "$(dirname "$RELEASE_NOTES_FILE")" + echo "$CHANGELOG" > "$RELEASE_NOTES_FILE" + echo "patch" + CURRENT="${BASE_REF#v}" + if [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + MAJOR="${CURRENT%%.*}"; REST="${CURRENT#*.}"; MINOR="${REST%%.*}"; PATCH="${REST#*.}"; PATCH="${PATCH%%[-+]*}" + NEXT="v${MAJOR}.${MINOR}.$((PATCH+1))" + echo "$NEXT" > "$(dirname "$RELEASE_NOTES_FILE")/suggested_version" + echo "$NEXT" + fi + exit 0 +fi + +PROMPT="You are a release engineer. Given the following git diff for a CLI application (Kosli CLI), do two things. + +1) Suggest the semantic version bump: + - major: Breaking changes (removed/renamed commands or flags, changed default behavior). + - minor: New commands, flags, subcommands, or features. + - patch: Bug fixes, docs, refactors, internal or dependency changes. + +2) Write a short changelog in markdown for the GitHub release body. Use bullet points; be concise; no preamble. + +Reply in this exact format (no other text before or after): +BUMP: major|minor|patch +---CHANGELOG--- +" + +BODY=$(jq -n \ + --arg prompt "$PROMPT" \ + --rawfile diff - \ + '{model: "claude-sonnet-4-20250514", max_tokens: 1024, messages: [{role: "user", content: ($prompt + "\n\n--- diff ---\n\n" + $diff)}]}' \ + < <(printf '%s' "$DIFF")) + +RESPONSE=$(curl -s -S -X POST "https://api.anthropic.com/v1/messages" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -H "Content-Type: application/json" \ + -d "$BODY") + +CONTENT=$(echo "$RESPONSE" | jq -r '.content[0].text // empty') +if [ -z "$CONTENT" ]; then + echo "ERROR: Anthropic API failed or returned no content. Response:" >&2 + echo "$RESPONSE" | jq . >&2 + exit 1 +fi + +BUMP=$(echo "$CONTENT" | tr '[:upper:]' '[:lower:]' | grep -oE 'major|minor|patch' | head -1) +case "$BUMP" in + major|minor|patch) ;; + *) + echo "WARN: Could not parse bump (got: $CONTENT). Defaulting to patch." >&2 + BUMP=patch + ;; +esac + +if echo "$CONTENT" | grep -q '---CHANGELOG---'; then + CHANGELOG=$(echo "$CONTENT" | sed -n '/---CHANGELOG---/,$ p' | tail -n +2) +else + CHANGELOG=$(echo "$CONTENT" | sed -n '2,$ p') +fi +mkdir -p "$(dirname "$RELEASE_NOTES_FILE")" +echo "$CHANGELOG" > "$RELEASE_NOTES_FILE" + +# Compute next version from current tag +CURRENT="${BASE_REF#v}" +if [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + MAJOR="${CURRENT%%.*}"; REST="${CURRENT#*.}"; MINOR="${REST%%.*}"; PATCH="${REST#*.}"; PATCH="${PATCH%%[-+]*}" + case "$BUMP" in + major) NEXT="v$((MAJOR+1)).0.0" ;; + minor) NEXT="v${MAJOR}.$((MINOR+1)).0" ;; + patch) NEXT="v${MAJOR}.${MINOR}.$((PATCH+1))" ;; + esac +else + NEXT="" +fi + +[ -n "$NEXT" ] && echo "$NEXT" > "$(dirname "$RELEASE_NOTES_FILE")/suggested_version" +echo "Suggested bump: $BUMP (from diff since $BASE_REF)" >&2 +echo "Next version: $NEXT" >&2 +echo "Changelog: $RELEASE_NOTES_FILE" >&2 +echo "$BUMP" +echo "$NEXT" diff --git a/release-guide.md b/release-guide.md index ed5efdb30..95810333b 100644 --- a/release-guide.md +++ b/release-guide.md @@ -35,13 +35,37 @@ Manually triggered from any branch — deploys docs to `staging-docs--kosli-docs ## Release process -A release is triggered by pushing a semver tag: +### Default: interactive release (AI version and changelog) + +Run: + +```bash +make release +``` + +**Prerequisites:** + + You need: + +- The **1Password CLI** (`op`) and `jq` installed. +- Access to 1Password shared vault. +- The **1Password desktop app** linked to the CLI so `op` can read secrets. In the 1Password app: **Settings → Developer → Integrate with 1Password CLI**. See [Turn on the 1Password desktop app integration](https://developer.1password.com/docs/cli/get-started#step-2-turn-on-the-1password-desktop-app-integration). + +**What happens:** + +1. A script uses Claude to suggest the next semver and draft release notes from the **git diff** (no commit messages). It reads the API key from 1Password via `op` using a default secret reference (vault/item/field); you can override with `OP_ANTHROPIC_API_KEY_REF` if your item lives elsewhere. +2. You see the suggested tag and release notes. You can press Enter to open your editor and edit `dist/release_notes.md`, or Enter to skip. +3. You are prompted: **Create tag vX.Y.Z and push? [y/N]**. On **y**, an annotated tag is created with the release notes as the tag body and pushed. The `release.yml` workflow runs on GitHub; it reads the notes from the tag body and passes them to GoReleaser for the GitHub Release. On **n**, nothing is pushed. + +### Fallback: release with an explicit tag + +If you don’t want the AI flow (e.g. 1Password/`op` not available or the suggest step failed), run: ```bash make release tag=v2.x.y ``` -This validates the working tree is clean and up to date with the remote, creates an annotated tag, and pushes it. The `release.yml` workflow then runs: +This checks the branch is up to date, creates an annotated tag (using `dist/release_notes.md` as the tag body if that file exists, otherwise the version string), and pushes it. No prompt. The release workflow runs as above; if the tag has no body, GoReleaser uses its default changelog on the GitHub Release. ### 1. Pre-build From 8058dd4be2637572518040580c2ecebd6708d179 Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 15:00:10 +0100 Subject: [PATCH 2/6] improve prompt and limit releasing to main branch --- Makefile | 4 +++- bin/suggest-version-ai.sh | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 139857914..eac9ca381 100644 --- a/Makefile +++ b/Makefile @@ -189,7 +189,9 @@ suggest-version-ai: # Release notes are carried in the tag message so GitHub Actions can pass them to GoReleaser. # For suggest step: ANTHROPIC_API_KEY or KOSLI_RELEASE_SUGGEST_URL (or use secret manager, see scripts/README-release-suggest.md). release: - @if [ -z "$(tag)" ]; then \ + @current=$$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD); \ + if [ "$$current" != "main" ]; then echo "ERROR: release must be run from main branch (current: $$current)"; exit 1; fi; \ + if [ -z "$(tag)" ]; then \ command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1); \ bin/suggest-version-ai.sh -o dist/release_notes.md; \ if [ ! -f dist/suggested_version ]; then \ diff --git a/bin/suggest-version-ai.sh b/bin/suggest-version-ai.sh index d82c196a4..f8996b9b6 100755 --- a/bin/suggest-version-ai.sh +++ b/bin/suggest-version-ai.sh @@ -64,23 +64,31 @@ fi PROMPT="You are a release engineer. Given the following git diff for a CLI application (Kosli CLI), do two things. -1) Suggest the semantic version bump: +Scope: Consider ONLY changes to the CLI itself—i.e. code under cmd/ and internal/ that affects user-facing commands, flags, and behavior. IGNORE all other changes when deciding the version and when writing the changelog: +- Ignore: documentation (docs*, *.md), Helm charts (charts/), CI/workflows (.github/), scripts (bin/, scripts/), tests (*_test.go, testdata/), Makefile, config files, and any other non-CLI code. +- If the diff contains only ignored changes, recommend a patch bump and write a single short line for the changelog (e.g. \"No user-facing CLI changes.\"). + +1) Suggest the semantic version bump (based only on CLI changes): - major: Breaking changes (removed/renamed commands or flags, changed default behavior). - minor: New commands, flags, subcommands, or features. - - patch: Bug fixes, docs, refactors, internal or dependency changes. + - patch: Bug fixes, refactors, internal or dependency updates; or no user-facing CLI changes. -2) Write a short changelog in markdown for the GitHub release body. Use bullet points; be concise; no preamble. +2) Write a short changelog in markdown for the GitHub release body. Include only user-facing CLI changes. Use bullet points; be concise; no preamble. + - Structure the changelog with section headers (e.g. \"# Breaking changes\", \"# New features\", \"# Bug fixes\" or \"# Improvements\") and list items under each header. Use only headers that have at least one change—omit any section that would be empty. + - Do not write placeholder lines under any header (no \"No other changes\", \"No user-facing CLI changes in this release\", or similar). If there are no CLI changes at all, output a single short line only (no headers). Reply in this exact format (no other text before or after): BUMP: major|minor|patch ---CHANGELOG--- " +DIFF_FILE=$(mktemp) +trap 'rm -f "$DIFF_FILE"' EXIT +printf '%s' "$DIFF" > "$DIFF_FILE" BODY=$(jq -n \ --arg prompt "$PROMPT" \ - --rawfile diff - \ - '{model: "claude-sonnet-4-20250514", max_tokens: 1024, messages: [{role: "user", content: ($prompt + "\n\n--- diff ---\n\n" + $diff)}]}' \ - < <(printf '%s' "$DIFF")) + --rawfile diff "$DIFF_FILE" \ + '{model: "claude-opus-4-6", max_tokens: 1024, messages: [{role: "user", content: ($prompt + "\n\n--- diff ---\n\n" + $diff)}]}') RESPONSE=$(curl -s -S -X POST "https://api.anthropic.com/v1/messages" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ @@ -104,8 +112,9 @@ case "$BUMP" in ;; esac -if echo "$CONTENT" | grep -q '---CHANGELOG---'; then - CHANGELOG=$(echo "$CONTENT" | sed -n '/---CHANGELOG---/,$ p' | tail -n +2) +CHANGELOG_MARKER='---CHANGELOG---' +if echo "$CONTENT" | grep -qF -- "$CHANGELOG_MARKER"; then + CHANGELOG=$(echo "$CONTENT" | sed -n "/${CHANGELOG_MARKER}/,\$ p" | tail -n +2) else CHANGELOG=$(echo "$CONTENT" | sed -n '2,$ p') fi From 866b0f7f2467647f5dce7e52b71e7943532218d7 Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 15:05:14 +0100 Subject: [PATCH 3/6] add cleanup of temp release notes files --- bin/release-interactive.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/release-interactive.sh b/bin/release-interactive.sh index 463c02da2..20bc9ffdc 100755 --- a/bin/release-interactive.sh +++ b/bin/release-interactive.sh @@ -52,3 +52,6 @@ fi git tag -a "$VER" -F "$RELEASE_NOTES_FILE" git push origin "$VER" echo "Pushed tag $VER. Release workflow will run on GitHub." +# Clean up temporary files to avoid reusing them in subsequent releases +rm "$RELEASE_NOTES_FILE" || true +rm "$SUGGESTED_VERSION_FILE" || true From 8e40d21fc3ce22214a8f9433f41e791b179c0d99 Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 15:37:43 +0100 Subject: [PATCH 4/6] first round of improvments --- Makefile | 7 ++----- bin/release-interactive.sh | 13 ++++++++----- bin/suggest-version-ai.sh | 36 ++++++++++++++++++++++++++++-------- 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Makefile b/Makefile index eac9ca381..3779cb677 100644 --- a/Makefile +++ b/Makefile @@ -174,11 +174,9 @@ helm-docs: helm-lint @cd charts/k8s-reporter && docker run --rm --volume "$(PWD):/helm-docs" jnorwood/helm-docs:latest --template-files README.md.gotmpl,_templates.gotmpl --output-file README.md @cd charts/k8s-reporter && docker run --rm --volume "$(PWD):/helm-docs" jnorwood/helm-docs:latest --template-files README.md.gotmpl,_templates.gotmpl --output-file ../../docs.kosli.com/content/helm/_index.md - -# Suggest next semver and changelog using Claude (or KOSLI_RELEASE_SUGGEST_URL proxy). +# Suggest next semver and changelog using Claude. # Writes changelog to dist/release_notes.md for use with goreleaser --release-notes. -# Requires: ANTHROPIC_API_KEY or KOSLI_RELEASE_SUGGEST_URL; jq, curl. -# See scripts/README-release-suggest.md for seamless (no API key) options. +# Requires: ANTHROPIC_API_KEY from 1Password; jq, curl. # Usage: make suggest-version-ai [BASE_REF=v1.2.3] suggest-version-ai: @command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1) @@ -187,7 +185,6 @@ suggest-version-ai: # Release: without tag → suggest version + changelog, then interactive edit & confirm, then tag and push. # With tag → escape hatch: create annotated tag (body = dist/release_notes.md if present), push. No AI, no prompt. # Release notes are carried in the tag message so GitHub Actions can pass them to GoReleaser. -# For suggest step: ANTHROPIC_API_KEY or KOSLI_RELEASE_SUGGEST_URL (or use secret manager, see scripts/README-release-suggest.md). release: @current=$$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD); \ if [ "$$current" != "main" ]; then echo "ERROR: release must be run from main branch (current: $$current)"; exit 1; fi; \ diff --git a/bin/release-interactive.sh b/bin/release-interactive.sh index 20bc9ffdc..dc6d496ab 100755 --- a/bin/release-interactive.sh +++ b/bin/release-interactive.sh @@ -33,8 +33,11 @@ echo "---" echo "" # Let user edit release notes -read -r -p "Press Enter to edit release notes (or to continue without editing)..." -"${EDITOR:-vi}" "$RELEASE_NOTES_FILE" +read -r -p "Edit release notes? [y/N] " edit_notes +case "$edit_notes" in + y|Y) "${EDITOR:-vi}" "$RELEASE_NOTES_FILE" ;; + *) ;; +esac echo "" read -r -p "Create tag $VER and push? [y/N] " confirm @@ -52,6 +55,6 @@ fi git tag -a "$VER" -F "$RELEASE_NOTES_FILE" git push origin "$VER" echo "Pushed tag $VER. Release workflow will run on GitHub." -# Clean up temporary files to avoid reusing them in subsequent releases -rm "$RELEASE_NOTES_FILE" || true -rm "$SUGGESTED_VERSION_FILE" || true +# # Clean up temporary files to avoid reusing them in subsequent releases +# rm "$RELEASE_NOTES_FILE" || true +# rm "$SUGGESTED_VERSION_FILE" || true diff --git a/bin/suggest-version-ai.sh b/bin/suggest-version-ai.sh index f8996b9b6..ef925acc3 100755 --- a/bin/suggest-version-ai.sh +++ b/bin/suggest-version-ai.sh @@ -6,6 +6,8 @@ # - ANTHROPIC_API_KEY: call Claude directly. # - OP_ANTHROPIC_API_KEY_REF: 1Password reference (default below; override if your item path differs). # +# Optional: CLAUDE_MODEL (default: claude-sonnet-4-6) — e.g. claude-opus-4-6. +# # Requires: curl, jq; for 1Password: op CLI # Usage: bin/suggest-version-ai.sh [base_ref] [-o release_notes.md] # base_ref defaults to the latest git tag. @@ -13,7 +15,7 @@ # # Output: bump (major|minor|patch), next_version (e.g. v1.3.0), and changelog file. -set -e +set -euo pipefail BASE_REF="" RELEASE_NOTES_FILE="dist/release_notes.md" @@ -24,6 +26,7 @@ while [[ $# -gt 0 ]]; do esac done BASE_REF="${BASE_REF:-$(git describe --tags --abbrev=0 2>/dev/null)}" +SUGGESTED_VERSION_FILE="$(dirname "$RELEASE_NOTES_FILE")/suggested_version" if [ -z "$BASE_REF" ]; then echo "ERROR: No base ref. Pass a tag or branch, or create a tag first." >&2 @@ -36,16 +39,19 @@ DIFF="$(git diff "$BASE_REF"..HEAD 2>/dev/null | head -c "$MAX_DIFF_CHARS")" # Get API key from 1Password if not set (default ref; override with OP_ANTHROPIC_API_KEY_REF) OP_ANTHROPIC_API_KEY_REF="${OP_ANTHROPIC_API_KEY_REF:-op://Shared/Anthropic API Key/credential}" -if [ -z "$ANTHROPIC_API_KEY" ]; then +if [ -z "${ANTHROPIC_API_KEY:-}" ]; then if command -v op >/dev/null 2>&1; then ANTHROPIC_API_KEY=$(op read "$OP_ANTHROPIC_API_KEY_REF" 2>/dev/null) || true fi fi -if [ -z "$ANTHROPIC_API_KEY" ]; then - echo "ERROR: Set ANTHROPIC_API_KEY or OP_ANTHROPIC_API_KEY_REF (1Password). See scripts/README-release-suggest.md." >&2 +if [ -z "${ANTHROPIC_API_KEY:-}" ]; then + echo "ERROR: Set ANTHROPIC_API_KEY or OP_ANTHROPIC_API_KEY_REF (1Password)." >&2 exit 1 fi +# Remove stale outputs from a previous run so a failure partway through doesn't mislead the next invocation. +trap 'rm -f "$RELEASE_NOTES_FILE" "$SUGGESTED_VERSION_FILE"' EXIT + if [ -z "$DIFF" ]; then echo "No changes since $BASE_REF. Bump: patch (no change)." >&2 CHANGELOG="No code changes since $BASE_REF." @@ -56,9 +62,14 @@ if [ -z "$DIFF" ]; then if [[ "$CURRENT" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then MAJOR="${CURRENT%%.*}"; REST="${CURRENT#*.}"; MINOR="${REST%%.*}"; PATCH="${REST#*.}"; PATCH="${PATCH%%[-+]*}" NEXT="v${MAJOR}.${MINOR}.$((PATCH+1))" - echo "$NEXT" > "$(dirname "$RELEASE_NOTES_FILE")/suggested_version" + if [ -n "$(git tag -l "$NEXT")" ]; then + echo "ERROR: tag $NEXT already exists. Push it or use: make release tag=$NEXT" >&2 + exit 1 + fi + echo "$NEXT" > "$SUGGESTED_VERSION_FILE" echo "$NEXT" fi + trap - EXIT exit 0 fi @@ -82,13 +93,15 @@ BUMP: major|minor|patch ---CHANGELOG--- " +CLAUDE_MODEL="${CLAUDE_MODEL:-claude-sonnet-4-6}" DIFF_FILE=$(mktemp) -trap 'rm -f "$DIFF_FILE"' EXIT +trap 'rm -f "$DIFF_FILE" "$RELEASE_NOTES_FILE" "$SUGGESTED_VERSION_FILE"' EXIT printf '%s' "$DIFF" > "$DIFF_FILE" BODY=$(jq -n \ + --arg model "$CLAUDE_MODEL" \ --arg prompt "$PROMPT" \ --rawfile diff "$DIFF_FILE" \ - '{model: "claude-opus-4-6", max_tokens: 1024, messages: [{role: "user", content: ($prompt + "\n\n--- diff ---\n\n" + $diff)}]}') + '{model: $model, max_tokens: 1024, messages: [{role: "user", content: ($prompt + "\n\n--- diff ---\n\n" + $diff)}]}') RESPONSE=$(curl -s -S -X POST "https://api.anthropic.com/v1/messages" \ -H "x-api-key: $ANTHROPIC_API_KEY" \ @@ -134,9 +147,16 @@ else NEXT="" fi -[ -n "$NEXT" ] && echo "$NEXT" > "$(dirname "$RELEASE_NOTES_FILE")/suggested_version" +if [ -n "$NEXT" ]; then + if [ -n "$(git tag -l "$NEXT")" ]; then + echo "ERROR: tag $NEXT already exists. Push it or use: make release tag=$NEXT" >&2 + exit 1 + fi + echo "$NEXT" > "$SUGGESTED_VERSION_FILE" +fi echo "Suggested bump: $BUMP (from diff since $BASE_REF)" >&2 echo "Next version: $NEXT" >&2 echo "Changelog: $RELEASE_NOTES_FILE" >&2 +trap - EXIT echo "$BUMP" echo "$NEXT" From fdff8ad78e37b16273eb82fc4e96b01652c8b53c Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 15:45:19 +0100 Subject: [PATCH 5/6] apply second round of reviews --- .github/workflows/release.yml | 2 +- Makefile | 4 +++- release-guide.md | 7 ++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e530cf631..4cd4595da 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,7 +131,7 @@ jobs: BODY=$(git tag -l --format='%(contents:body)' "$TAG") if [ -n "$BODY" ]; then mkdir -p dist - echo "$BODY" > dist/release_notes.md + printf '%s' "$BODY" > dist/release_notes.md echo "args=--release-notes=dist/release_notes.md" >> $GITHUB_OUTPUT fi diff --git a/Makefile b/Makefile index 3779cb677..84576fc31 100644 --- a/Makefile +++ b/Makefile @@ -176,10 +176,11 @@ helm-docs: helm-lint # Suggest next semver and changelog using Claude. # Writes changelog to dist/release_notes.md for use with goreleaser --release-notes. -# Requires: ANTHROPIC_API_KEY from 1Password; jq, curl. +# Requires: jq, curl, op (1Password CLI). API key from 1Password via op. # Usage: make suggest-version-ai [BASE_REF=v1.2.3] suggest-version-ai: @command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1) + @command -v curl >/dev/null 2>&1 || (echo "Install curl (e.g. brew install curl)" && exit 1) @bin/suggest-version-ai.sh $(BASE_REF) -o dist/release_notes.md # Release: without tag → suggest version + changelog, then interactive edit & confirm, then tag and push. @@ -190,6 +191,7 @@ release: if [ "$$current" != "main" ]; then echo "ERROR: release must be run from main branch (current: $$current)"; exit 1; fi; \ if [ -z "$(tag)" ]; then \ command -v jq >/dev/null 2>&1 || (echo "Install jq (e.g. brew install jq)" && exit 1); \ + command -v curl >/dev/null 2>&1 || (echo "Install curl (e.g. brew install curl)" && exit 1); \ bin/suggest-version-ai.sh -o dist/release_notes.md; \ if [ ! -f dist/suggested_version ]; then \ echo "Suggestion failed or no previous tag. Use: make release tag=vX.Y.Z"; exit 1; \ diff --git a/release-guide.md b/release-guide.md index 95810333b..d20e8a9b3 100644 --- a/release-guide.md +++ b/release-guide.md @@ -47,9 +47,10 @@ make release You need: -- The **1Password CLI** (`op`) and `jq` installed. -- Access to 1Password shared vault. -- The **1Password desktop app** linked to the CLI so `op` can read secrets. In the 1Password app: **Settings → Developer → Integrate with 1Password CLI**. See [Turn on the 1Password desktop app integration](https://developer.1password.com/docs/cli/get-started#step-2-turn-on-the-1password-desktop-app-integration). +- **jq**, **curl** installed. +- An Anthropic API key, provided in one of the following ways: + - **Via 1Password**: The **1Password CLI** (`op`), access to the shared vault, and the **1Password desktop app** linked to the CLI so `op` can read secrets. In the 1Password app: **Settings → Developer → Integrate with 1Password CLI**. See [Turn on the 1Password desktop app integration](https://developer.1password.com/docs/cli/get-started#step-2-turn-on-the-1password-desktop-app-integration). + - **Via environment variable**: Set `ANTHROPIC_API_KEY` directly in your environment (no 1Password required). **What happens:** From 7225f24d64b646c0edb79031d9a89537be555c4b Mon Sep 17 00:00:00 2001 From: Sami Alajrami Date: Wed, 11 Mar 2026 15:56:48 +0100 Subject: [PATCH 6/6] clean dead comments --- bin/release-interactive.sh | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/release-interactive.sh b/bin/release-interactive.sh index dc6d496ab..c08d10c7b 100755 --- a/bin/release-interactive.sh +++ b/bin/release-interactive.sh @@ -55,6 +55,3 @@ fi git tag -a "$VER" -F "$RELEASE_NOTES_FILE" git push origin "$VER" echo "Pushed tag $VER. Release workflow will run on GitHub." -# # Clean up temporary files to avoid reusing them in subsequent releases -# rm "$RELEASE_NOTES_FILE" || true -# rm "$SUGGESTED_VERSION_FILE" || true