Skip to content

Commit ae45128

Browse files
committed
feat: implement Anthropic release notes action and auto-release workflow
1 parent ee708cd commit ae45128

File tree

6 files changed

+239
-2
lines changed

6 files changed

+239
-2
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: 'anthropic-release-notes'
2+
description: 'Generate release notes with Anthropic Claude from commits since the last release'
3+
author: 'Copilot Breakpoint Debugger'
4+
inputs:
5+
anthropic-api-key:
6+
description: 'Anthropic API Key'
7+
required: true
8+
language:
9+
description: 'Language for release notes (default en)'
10+
required: false
11+
default: 'en'
12+
model:
13+
description: 'Anthropic model name (default claude-3-5-sonnet-latest)'
14+
required: false
15+
default: 'claude-3-5-sonnet-latest'
16+
token:
17+
description: 'GitHub token for API calls'
18+
required: true
19+
version:
20+
description: 'Release version (tag) to create'
21+
required: true
22+
runs:
23+
using: 'node20'
24+
main: 'index.js'
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
const { getInput, setFailed, info, setOutput } = require('@actions/core');
2+
const { context, getOctokit } = require('@actions/github');
3+
const { Anthropic } = require('@anthropic-ai/sdk');
4+
5+
function parseInputs() {
6+
const anthropicApiKey = getInput('anthropic-api-key');
7+
const language = getInput('language');
8+
const model = getInput('model');
9+
const token = getInput('token');
10+
const version = getInput('version');
11+
return { anthropicApiKey, language, model, token, version };
12+
}
13+
14+
async function getPRsFromCommit(octokit, sha) {
15+
try {
16+
const pr = await octokit.rest.repos.listPullRequestsAssociatedWithCommit({
17+
owner: context.repo.owner,
18+
repo: context.repo.repo,
19+
commit_sha: sha,
20+
});
21+
return pr.data.map(p => ({ label: `#${p.number}`, url: p.html_url }));
22+
} catch (e) {
23+
info(`Failed to fetch PRs for commit ${sha}: ${e.message}`);
24+
return [];
25+
}
26+
}
27+
28+
function buildPrompt(language, commitsData) {
29+
return [
30+
'You are a DEV OPS engineer; write a concise changelog for the new software version.',
31+
'The changelog lists new features (use verb "Add"), changes/improvements/updates (also start with "Add"), and bug fixes (start lines with "Fix").',
32+
'Order sections: features/changes first, then fixes.',
33+
'Format exactly as:\n```\n## What\'s Changed\n- Add <feature/change>\n- Fix <bug>\n```',
34+
'Do not invent items. Use ONLY the following commit data (message, author, PRs).',
35+
'If no features or fixes are present, provide a minimal placeholder like "- Add internal refactors".',
36+
`Return output in language: ${language}. Translate commit-derived content.`,
37+
'Commit data JSON follows:',
38+
JSON.stringify(commitsData, null, 2),
39+
].join('\n');
40+
}
41+
42+
async function run() {
43+
info('Running Anthropic release notes action');
44+
const { anthropicApiKey, language, model, token, version } = parseInputs();
45+
const octokit = getOctokit(token);
46+
47+
// Gather commits since last release
48+
let latestRelease = null;
49+
try {
50+
latestRelease = await octokit.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo });
51+
} catch (e) {
52+
info(`No previous release found; using all recent commits (${e.message})`);
53+
}
54+
55+
let commits = [];
56+
try {
57+
if (latestRelease) {
58+
const base = latestRelease.data.tag_name;
59+
const compare = await octokit.rest.repos.compareCommits({
60+
owner: context.repo.owner,
61+
repo: context.repo.repo,
62+
base,
63+
head: context.sha,
64+
});
65+
commits = compare.data.commits;
66+
} else {
67+
// fallback: list recent commits (first page)
68+
const list = await octokit.rest.repos.listCommits({ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 });
69+
commits = list.data;
70+
}
71+
} catch (error) {
72+
return setFailed(`Failed to gather commits: ${error.message || 'unknown error'}`);
73+
}
74+
75+
if (!commits.length) {
76+
return setFailed('No commits found to generate release notes');
77+
}
78+
79+
// Build commit data with PR associations
80+
const commitsStructured = [];
81+
for (const c of commits) {
82+
const prs = await getPRsFromCommit(octokit, c.sha);
83+
commitsStructured.push({
84+
sha: c.sha,
85+
message: c.commit?.message,
86+
author: c.author?.login || c.commit?.author?.name || 'unknown',
87+
authorUrl: c.author?.html_url || null,
88+
prs,
89+
});
90+
}
91+
92+
const prompt = buildPrompt(language || 'en', commitsStructured);
93+
94+
try {
95+
const anthropic = new Anthropic({ apiKey: anthropicApiKey });
96+
const completion = await anthropic.messages.create({
97+
model: model || 'claude-3-5-sonnet-latest',
98+
max_tokens: 800,
99+
messages: [ { role: 'user', content: prompt } ],
100+
});
101+
102+
const contentBlock = completion.content && completion.content[0];
103+
const responseText = contentBlock && contentBlock.text ? contentBlock.text : null;
104+
if (!responseText) {
105+
throw new Error('Anthropic did not return content');
106+
}
107+
108+
// Create release
109+
await octokit.rest.repos.createRelease({
110+
owner: context.repo.owner,
111+
repo: context.repo.repo,
112+
tag_name: version,
113+
name: version,
114+
body: responseText,
115+
});
116+
117+
setOutput('releaseNotes', responseText);
118+
info('Release created successfully.');
119+
} catch (error) {
120+
setFailed(error.message || 'Failed to generate release notes');
121+
}
122+
}
123+
124+
run();

.github/workflows/auto-release.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Auto Release
2+
3+
on:
4+
push:
5+
branches: [main]
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
11+
concurrency:
12+
group: auto-release
13+
cancel-in-progress: false
14+
15+
jobs:
16+
auto-release:
17+
if: ${{ !contains(github.event.head_commit.message, '[skip ci]') }}
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0 # ensure tags are fetched
24+
25+
- name: Setup Node.js
26+
uses: actions/setup-node@v4
27+
with:
28+
node-version: 20.x
29+
cache: npm
30+
31+
- name: Install dependencies
32+
run: npm ci
33+
34+
- name: Determine next version
35+
id: version
36+
uses: actions/github-script@v7
37+
with:
38+
script: |
39+
const latestRelease = await github.rest.repos.getLatestRelease({ owner: context.repo.owner, repo: context.repo.repo }).catch(()=>null);
40+
let nextVersion;
41+
if (latestRelease) {
42+
const prevTagRaw = latestRelease.data.tag_name || '';
43+
const prev = prevTagRaw.replace(/^v/, '');
44+
const parts = prev.split('.');
45+
if (parts.length !== 3 || parts.some(p => isNaN(Number(p)))) {
46+
core.setFailed(`Latest release tag '${prevTagRaw}' is not semver x.y.z`);
47+
return;
48+
}
49+
parts[2] = String(Number(parts[2]) + 1);
50+
nextVersion = parts.join('.');
51+
} else {
52+
nextVersion = '0.0.1';
53+
}
54+
core.info(`Computed next version: ${nextVersion}`);
55+
core.setOutput('version', nextVersion);
56+
57+
- name: Update package.json version
58+
env:
59+
VERSION: ${{ steps.version.outputs.version }}
60+
run: |
61+
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json','utf8')); pkg.version=process.env.VERSION; pkg._versionComment='Version is auto-generated by the CI auto-release workflow. Manual edits are overwritten.'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+ '\n');"
62+
git config user.name "github-actions"
63+
git config user.email "[email protected]"
64+
git add package.json
65+
git commit -m "chore: release ${VERSION} [skip ci]" || echo "No changes to commit"
66+
67+
- name: Create and push tag
68+
env:
69+
VERSION: ${{ steps.version.outputs.version }}
70+
run: |
71+
if git rev-parse ${VERSION} >/dev/null 2>&1; then echo "Tag ${VERSION} already exists"; else git tag ${VERSION}; git push origin ${VERSION}; fi
72+
git push origin HEAD:main
73+
74+
- name: Generate release notes & create release
75+
uses: ./.github/actions/anthropic-release-notes
76+
with:
77+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
78+
language: en
79+
model: claude-3-5-sonnet-latest
80+
token: ${{ secrets.GITHUB_TOKEN }}
81+
version: ${{ steps.version.outputs.version }}
82+
83+
- name: Output release notes
84+
if: steps.version.outputs.version
85+
run: echo "Release notes generated for version ${{ steps.version.outputs.version }}"

.prettierignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ node_modules/
1515
package-lock.json
1616

1717
# Generated files
18-
*.d.ts
18+
*.d.ts
19+
20+
*.md

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,6 @@ export default antfu(
131131
},
132132
// Ignore large external vendor/source trees not meant for linting in this extension
133133
{
134-
ignores: ['external/**', 'coverage/**', 'out/**'],
134+
ignores: ['external/**', 'coverage/**', 'out/**', '.github/workflows/**', '.github/actions/**'],
135135
}
136136
);

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"name": "copilot-breakpoint-debugger",
44
"displayName": "Copilot Breakpoint Debugger",
55
"version": "0.0.21",
6+
"_versionComment": "Version is auto-generated by the CI auto-release workflow. Manual edits to 'version' will be overwritten.",
67
"description": "Use GitHub Copilot to automate starting, inspecting and resuming VS Code debug sessions with conditional breakpoints, exact numeric hit counts (hitCount), logpoints, and capture actions that interpolate variables inside log messages.",
78
"preview": true,
89
"license": "MIT",
@@ -419,6 +420,7 @@
419420
"format:check": "prettier --check ."
420421
},
421422
"dependencies": {
423+
"@anthropic-ai/sdk": "^0.27.3",
422424
"@vscode/prompt-tsx": "^0.4.0-alpha.5",
423425
"mocha": "^10.7.3"
424426
},

0 commit comments

Comments
 (0)