Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ indent_size = 2
indent_style = space
indent_size = 2

# BATS test files
[*.bats]
indent_style = space
indent_size = 2

# Dockerfiles
[Dockerfile*]
indent_style = space
Expand Down
6 changes: 6 additions & 0 deletions .github/codeql/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
queries:
- uses: security-and-quality

query-filters:
- exclude:
kind: [diagnostic, metric]
41 changes: 41 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
commands:
- changed-files:
- any-glob-to-any-file: "internal/commands/**"

tui:
- changed-files:
- any-glob-to-any-file: "internal/tui/**"

sdk:
- changed-files:
- any-glob-to-any-file:
- "internal/sdk/**"
- "internal/version/sdk-provenance.json"

tests:
- changed-files:
- any-glob-to-any-file:
- "e2e/**"
- "**/*_test.go"

ci:
- changed-files:
- any-glob-to-any-file: ".github/**"

skills:
- changed-files:
- any-glob-to-any-file: "skills/**"

plugin:
- changed-files:
- any-glob-to-any-file: ".claude-plugin/**"

auth:
- changed-files:
- any-glob-to-any-file: "internal/auth/**"

output:
- changed-files:
- any-glob-to-any-file:
- "internal/output/**"
- "internal/presenter/**"
24 changes: 24 additions & 0 deletions .github/prompts/classify-pr.prompt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
messages:
- role: system
content: |
You classify pull requests for release note categorization.
Respond with exactly one word: bug, enhancement, or documentation.

- bug: corrects wrong behavior, broken defaults, incorrect error codes,
retry/backoff defects, auth handling bugs, compatibility regressions.
Test-only changes that fix assertions for previously-wrong behavior
count as bug.
- enhancement: new API coverage, new SDK features, new configuration
options, new test coverage, generator/tooling improvements.
If only generated files changed with no bug claim, default to
enhancement.
- documentation: README, CONTRIBUTING, SECURITY, or other docs-only
changes with no runtime behavior change. SDK README updates that
accompany code changes don't count — label the code change.

When a PR mixes categories: bug > enhancement > documentation.
Prefer diff evidence over the PR title.
model: openai/gpt-4o-mini
modelParameters:
maxCompletionTokens: 10
temperature: 0
34 changes: 34 additions & 0 deletions .github/prompts/detect-breaking.prompt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
messages:
- role: system
content: |
You analyze CLI tool diffs for breaking changes. A breaking change is:
- Removal or rename of a CLI command or subcommand
- Removal or rename of a flag (--flag)
- Change in output format that would break scripts parsing the output
- Change in exit codes
- Removal of environment variable support

NOT breaking: adding new commands, adding new flags, internal refactors,
test changes, documentation, adding new output fields.

Respond with a JSON object:
{"breaking": true/false, "items": ["description of each breaking change"]}
model: openai/gpt-4o-mini
responseFormat: json_schema
jsonSchema: |-
{
"name": "breaking_analysis",
"strict": true,
"schema": {
"type": "object",
"properties": {
"breaking": { "type": "boolean" },
"items": { "type": "array", "items": { "type": "string" } }
},
"required": ["breaking", "items"],
"additionalProperties": false
}
}
modelParameters:
maxCompletionTokens: 500
temperature: 0
17 changes: 17 additions & 0 deletions .github/prompts/summarize-changelog.prompt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
messages:
- role: system
content: |
You write release notes for a CLI tool. Given a list of commits and a diff,
produce a concise changelog in markdown.

Rules:
- Group changes under headings: Features, Bug Fixes, Improvements,
Documentation (omit empty groups)
- Each item: one line, imperative voice, no commit hash
- Highlight breaking changes with a ⚠️ prefix and a "Breaking Changes"
group at the top if any exist
- Keep it under 30 lines
model: openai/gpt-4o
modelParameters:
maxCompletionTokens: 1000
temperature: 0.2
4 changes: 4 additions & 0 deletions .github/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ changelog:
exclude:
labels:
- dependencies
- github-actions
authors:
- dependabot[bot]
categories:
- title: "⚠️ Breaking Changes"
labels:
- breaking
- title: Features
labels:
- enhancement
Expand Down
222 changes: 222 additions & 0 deletions .github/workflows/ai-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
name: Classify PR

on:
pull_request_target:
types: [opened, synchronize, reopened]

concurrency:
group: classify-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
issues: write
models: read
pull-requests: write

jobs:
classify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Build prompt
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=${{ github.event.pull_request.number }}
gh pr diff "$PR" > /tmp/pr.diff
gh pr view "$PR" --json title --jq .title > /tmp/pr-title.txt
gh pr view "$PR" --json body --jq '.body // ""' > /tmp/pr-body.txt

# Compose user message
{
printf 'PR #%s: %s\n' "$PR" "$(cat /tmp/pr-title.txt)"
echo ""
cat /tmp/pr-body.txt
echo ""
echo "Diff (truncated):"
head -c 100000 /tmp/pr.diff
} > /tmp/user-message.txt

# Build full prompt YAML: splice user message into the messages array
python3 -c "
with open('.github/prompts/classify-pr.prompt.yml') as f:
lines = f.readlines()
with open('/tmp/user-message.txt') as f:
user_msg = f.read()

insert_at = len(lines)
for i, line in enumerate(lines):
if i == 0:
continue
if line.strip() and not line[0].isspace():
insert_at = i
break

entry = [' - role: user\n', ' content: |\n']
for ln in user_msg.splitlines():
entry.append(' ' + ln + '\n')

lines[insert_at:insert_at] = entry
with open('/tmp/prompt.yml', 'w') as f:
f.writelines(lines)

try:
import yaml
doc = yaml.safe_load(open('/tmp/prompt.yml'))
assert doc['messages'][-1]['role'] == 'user', 'prompt splice failed'
except ImportError:
pass
"

- name: Classify
id: classify
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1
with:
prompt-file: /tmp/prompt.yml

- name: Apply label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LABEL=$(echo "${{ steps.classify.outputs.response }}" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
case "$LABEL" in
bug|enhancement|documentation) ;;
*) echo "Unexpected: $LABEL — skipping"; exit 0 ;;
esac
for L in bug enhancement documentation; do
gh pr edit ${{ github.event.pull_request.number }} --remove-label "$L" 2>/dev/null || true
done
gh pr edit ${{ github.event.pull_request.number }} --add-label "$LABEL"

breaking:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

- name: Build prompt
id: cmd-diff
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=${{ github.event.pull_request.number }}

# CLI command surface files
PATTERNS=(
"internal/commands/*.go"
"internal/cli/root.go"
)
gh pr diff "$PR" > /tmp/full.diff

# Filter diff to only command surface files
python3 -c "
import sys, re, fnmatch
diff = open('/tmp/full.diff').read()
patterns = sys.argv[1:]
sections = re.split(r'(?=^diff --git)', diff, flags=re.MULTILINE)
for s in sections:
m = re.match(r'diff --git a/(\S+)', s)
if m:
path = m.group(1)
if any(fnmatch.fnmatch(path, p) for p in patterns):
sys.stdout.write(s)
" "${PATTERNS[@]}" > /tmp/cmd.diff

if [ ! -s /tmp/cmd.diff ]; then
echo "skip=true" >> $GITHUB_OUTPUT
else
TITLE=$(gh pr view "$PR" --json title --jq .title)

{
printf 'PR #%s: %s\n' "$PR" "$TITLE"
echo ""
echo "Diff of CLI command surface files:"
head -c 100000 /tmp/cmd.diff
} > /tmp/user-message.txt

python3 -c "
with open('.github/prompts/detect-breaking.prompt.yml') as f:
lines = f.readlines()
with open('/tmp/user-message.txt') as f:
user_msg = f.read()

insert_at = len(lines)
for i, line in enumerate(lines):
if i == 0:
continue
if line.strip() and not line[0].isspace():
insert_at = i
break

entry = [' - role: user\n', ' content: |\n']
for ln in user_msg.splitlines():
entry.append(' ' + ln + '\n')

lines[insert_at:insert_at] = entry
with open('/tmp/prompt.yml', 'w') as f:
f.writelines(lines)

try:
import yaml
doc = yaml.safe_load(open('/tmp/prompt.yml'))
assert doc['messages'][-1]['role'] == 'user', 'prompt splice failed'
except ImportError:
pass
"
echo "skip=false" >> $GITHUB_OUTPUT
fi

- name: Detect breaking changes
if: steps.cmd-diff.outputs.skip != 'true'
id: detect
uses: actions/ai-inference@b81b2afb8390ee6839b494a404766bef6493c7d9 # v1
with:
prompt-file: /tmp/prompt.yml

- name: Apply breaking label
if: steps.cmd-diff.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RESPONSE_FILE="${{ steps.detect.outputs.response-file }}"
if [ -z "$RESPONSE_FILE" ] || [ ! -f "$RESPONSE_FILE" ]; then
echo "::warning::Model response file is missing; skipping breaking label."
exit 0
fi
if ! jq empty "$RESPONSE_FILE" 2>/dev/null; then
echo "::warning::Model response is not valid JSON; skipping breaking label."
echo "## Breaking change detection failed" >> "$GITHUB_STEP_SUMMARY"
echo "Model returned invalid JSON. Breaking label was **not** applied." >> "$GITHUB_STEP_SUMMARY"
if [ -s "$RESPONSE_FILE" ]; then
echo '```' >> "$GITHUB_STEP_SUMMARY"
cat "$RESPONSE_FILE" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
fi
exit 0
fi
BREAKING=$(jq -r '.breaking' "$RESPONSE_FILE")
PR=${{ github.event.pull_request.number }}

if [ "$BREAKING" = "true" ]; then
ITEMS=$(jq -r '.items[]' "$RESPONSE_FILE" | sed 's/^/- /')
gh label create breaking --color "B60205" 2>/dev/null || true
gh pr edit "$PR" --add-label "breaking"

{
echo "⚠️ **Potential breaking changes detected:**"
echo ""
echo "$ITEMS"
echo ""
echo "_Review carefully before merging. Consider a major version bump._"
} > /tmp/breaking-comment.md

EXISTING=$(gh pr view "$PR" --json comments --jq '.comments[] | select(.body | startswith("⚠️ **Potential breaking")) | .id' | head -1)
if [ -n "$EXISTING" ]; then
gh api graphql -f query="mutation { updateIssueComment(input: {id: \"$EXISTING\", body: $(jq -Rs . /tmp/breaking-comment.md)}) { issueComment { id } } }"
else
gh pr comment "$PR" --body-file /tmp/breaking-comment.md
fi
else
gh pr edit "$PR" --remove-label "breaking" 2>/dev/null || true
fi
Loading
Loading