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
24 changes: 24 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
ci:
- changed-files:
- any-glob-to-any-file: ['.github/**']
tests:
- changed-files:
- any-glob-to-any-file: ['**/*_test.go']
docs:
- changed-files:
- any-glob-to-any-file: ['*.md', 'docs/**']
deps:
- changed-files:
- any-glob-to-any-file: ['go.mod', 'go.sum']
skills:
- changed-files:
- any-glob-to-any-file: ['skills/**']
seed:
- changed-files:
- any-glob-to-any-file: ['seed/**']
actions:
- changed-files:
- any-glob-to-any-file: ['actions/**']
prompts:
- changed-files:
- any-glob-to-any-file: ['.github/prompts/**', 'prompts/**']
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
36 changes: 36 additions & 0 deletions .github/prompts/detect-breaking.prompt.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
messages:
- role: system
content: |
You analyze Go library diffs for breaking changes to the public API.
A breaking change is:
- Removal or rename of an exported type, function, method, or constant
- Change to an exported function or method signature (parameters, return types)
- Removal of a package
- Breaking an interface contract (adding methods to an exported interface)
- Removal of exported struct fields

NOT breaking: adding new exported types/functions/methods/constants,
adding new packages, internal refactors, test changes, documentation,
adding new struct fields, changes to unexported identifiers.

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
224 changes: 224 additions & 0 deletions .github/workflows/ai-labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
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@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
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
PR=${{ github.event.pull_request.number }}
CURRENT=$(gh pr view "$PR" --json labels --jq '.labels[].name')
for L in bug enhancement documentation; do
if [ "$L" != "$LABEL" ] && echo "$CURRENT" | grep -qx "$L"; then
gh pr edit "$PR" --remove-label "$L" 2>/dev/null || true
fi
done
if ! echo "$CURRENT" | grep -qx "$LABEL"; then
gh pr edit "$PR" --add-label "$LABEL" 2>/dev/null || true
fi

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

- name: Build prompt
id: api-diff
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=${{ github.event.pull_request.number }}
gh pr diff "$PR" > /tmp/full.diff

# Filter diff to exported Go library files (not tests, seed, or non-package dirs)
python3 -c "
import sys, re
diff = open('/tmp/full.diff').read()
dir_exclude = ('seed/', 'internal/', 'actions/', 'prompts/', 'skills/')
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 path.endswith('.go') and not path.endswith('_test.go') and not any(path.startswith(d) for d in dir_exclude):
sys.stdout.write(s)
Comment thread
jeremy marked this conversation as resolved.
" > /tmp/api.diff

if [ ! -s /tmp/api.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 exported Go library files:"
head -c 100000 /tmp/api.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.api-diff.outputs.skip != 'true'
id: detect
uses: actions/ai-inference@e09e65981758de8b2fdab13c2bfb7c7d5493b0b6 # v2.0.7
with:
prompt-file: /tmp/prompt.yml

- name: Apply breaking label
if: steps.api-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"
echo "Model returned invalid JSON. Breaking label was **not** applied."
if [ -s "$RESPONSE_FILE" ]; then
echo '```'
cat "$RESPONSE_FILE"
echo '```'
fi
} >> "$GITHUB_STEP_SUMMARY"
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
17 changes: 17 additions & 0 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Label PRs

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

permissions:
contents: read
pull-requests: write

jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
sync-labels: true
Loading
Loading