From 56726f6af3c58a2b412b742abd3803f270d1b751 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Sun, 29 Mar 2026 11:29:32 +0200 Subject: [PATCH 1/2] feat: add automated release-prepare workflow with auto-merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add release-prepare.yml with workflow_dispatch (bump_type + auto_merge) - Add scripts/bump-version.js for single-package version bumping - Add scripts/generate-changelog.js (Claude API powered) - Add scripts/update-changelog.js for CHANGELOG.md management - Add initial CHANGELOG.md - Add @anthropic-ai/sdk to devDependencies Flow: manual trigger → verify → bump → changelog → release PR → optional auto-merge --- .github/workflows/release-prepare.yml | 196 ++++++++++++++++++++++++++ CHANGELOG.md | 9 ++ package.json | 1 + scripts/bump-version.js | 48 +++++++ scripts/generate-changelog.js | 164 +++++++++++++++++++++ scripts/update-changelog.js | 184 ++++++++++++++++++++++++ 6 files changed, 602 insertions(+) create mode 100644 .github/workflows/release-prepare.yml create mode 100644 CHANGELOG.md create mode 100755 scripts/bump-version.js create mode 100755 scripts/generate-changelog.js create mode 100755 scripts/update-changelog.js diff --git a/.github/workflows/release-prepare.yml b/.github/workflows/release-prepare.yml new file mode 100644 index 0000000..155c0e9 --- /dev/null +++ b/.github/workflows/release-prepare.yml @@ -0,0 +1,196 @@ +name: "\U0001F4E6 Prepare Release" + +on: + workflow_dispatch: + inputs: + bump_type: + description: 'Version bump type' + required: true + default: 'patch' + type: choice + options: + - patch + - minor + - major + auto_merge: + description: 'Auto-merge the release PR after CI passes' + required: false + default: false + type: boolean + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + name: Prepare Release PR + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Verify + run: npm run verify + + - name: Check for existing release PR + id: check_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING_PR=$(gh pr list --head "release/" --state open --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + echo "Release PR #$EXISTING_PR already exists, skipping" + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "No existing release PR found" + echo "skip=false" >> $GITHUB_OUTPUT + fi + + - name: Check for commits since last tag + if: steps.check_pr.outputs.skip != 'true' + id: check_commits + run: | + LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -z "$LAST_TAG" ]; then + echo "No tags found, will create initial release" + echo "has_commits=true" >> $GITHUB_OUTPUT + exit 0 + fi + COMMIT_COUNT=$(git rev-list --count --no-merges ${LAST_TAG}..HEAD) + echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT + if [ "$COMMIT_COUNT" -eq 0 ]; then + echo "No commits since last tag ($LAST_TAG), skipping" + echo "has_commits=false" >> $GITHUB_OUTPUT + else + echo "Found $COMMIT_COUNT commits since $LAST_TAG" + echo "has_commits=true" >> $GITHUB_OUTPUT + fi + + - name: Bump version + if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' + id: bump + run: | + BUMP_TYPE="${{ github.event.inputs.bump_type }}" + OUTPUT=$(node scripts/bump-version.js $BUMP_TYPE) + echo "$OUTPUT" + NEW_VERSION=$(echo "$OUTPUT" | grep "NEW_VERSION=" | cut -d= -f2) + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + + - name: Update package-lock.json + if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' + run: npm install --package-lock-only + + - name: Generate changelog with Claude + if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' + id: changelog + env: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + CHANGELOG=$(node scripts/generate-changelog.js ${{ steps.bump.outputs.new_version }}) + echo "$CHANGELOG" > changelog-content.md + echo "Generated changelog:" + cat changelog-content.md + + - name: Update CHANGELOG.md + if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' + run: | + CHANGELOG_CONTENT=$(cat changelog-content.md) + node scripts/update-changelog.js "${{ steps.bump.outputs.new_version }}" "$CHANGELOG_CONTENT" + + - name: Create release branch and PR + if: steps.check_pr.outputs.skip != 'true' && steps.check_commits.outputs.has_commits == 'true' + id: create_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.bump.outputs.new_version }}" + BRANCH_NAME="release/v${VERSION}" + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git checkout -b "$BRANCH_NAME" + git add -A + git commit -m "chore: prepare release v${VERSION} + + - Bump version to ${VERSION} + - Update CHANGELOG.md with release notes + + [skip ci]" + git push origin "$BRANCH_NAME" + + CHANGELOG_CONTENT=$(cat changelog-content.md) + + PR_URL=$(gh pr create \ + --title "Release v${VERSION}" \ + --body "$(cat <> $GITHUB_OUTPUT + + gh pr edit "$BRANCH_NAME" --add-label "release" 2>/dev/null || true + + - name: Auto-merge release PR + if: steps.create_pr.outputs.pr_url && github.event.inputs.auto_merge == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "Auto-merge enabled — enabling GitHub auto-merge" + gh pr merge "${{ steps.create_pr.outputs.pr_url }}" --auto --squash + echo "Auto-merge enabled. PR will merge when all checks pass." + + - name: Summary + if: always() + run: | + if [ "${{ steps.check_pr.outputs.skip }}" == "true" ]; then + echo "## Skipped" >> $GITHUB_STEP_SUMMARY + echo "An open release PR already exists." >> $GITHUB_STEP_SUMMARY + elif [ "${{ steps.check_commits.outputs.has_commits }}" == "false" ]; then + echo "## Skipped" >> $GITHUB_STEP_SUMMARY + echo "No commits since last release tag." >> $GITHUB_STEP_SUMMARY + elif [ -n "${{ steps.bump.outputs.new_version }}" ]; then + echo "## Release PR Created" >> $GITHUB_STEP_SUMMARY + echo "Version: v${{ steps.bump.outputs.new_version }}" >> $GITHUB_STEP_SUMMARY + AUTO_MERGE="${{ github.event.inputs.auto_merge }}" + if [ "$AUTO_MERGE" == "true" ]; then + echo "Auto-merge: **enabled** (will merge when checks pass)" >> $GITHUB_STEP_SUMMARY + else + echo "Auto-merge: **disabled** (manual review required)" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Changelog" >> $GITHUB_STEP_SUMMARY + cat changelog-content.md >> $GITHUB_STEP_SUMMARY 2>/dev/null || true + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8661a43 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes to Agentage CLI will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + diff --git a/package.json b/package.json index 3d7f1a6..b951cd1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "ws": "latest" }, "devDependencies": { + "@anthropic-ai/sdk": "latest", "@types/express": "latest", "@types/node": "latest", "@types/ws": "latest", diff --git a/scripts/bump-version.js b/scripts/bump-version.js new file mode 100755 index 0000000..9bb641d --- /dev/null +++ b/scripts/bump-version.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node + +/** + * bump-version.js + * + * Bumps the version in package.json. + * + * Usage: node scripts/bump-version.js [patch|minor|major] + * + * Output: NEW_VERSION=x.y.z to stdout + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const packagePath = join(__dirname, '..', 'package.json'); + +const bumpType = process.argv[2] || 'patch'; + +if (!['patch', 'minor', 'major'].includes(bumpType)) { + console.error(`Invalid bump type: ${bumpType}`); + console.error('Usage: node scripts/bump-version.js [patch|minor|major]'); + process.exit(1); +} + +const pkg = JSON.parse(readFileSync(packagePath, 'utf-8')); +const [major, minor, patch] = pkg.version.split('.').map(Number); + +let newVersion; +switch (bumpType) { + case 'major': + newVersion = `${major + 1}.0.0`; + break; + case 'minor': + newVersion = `${major}.${minor + 1}.0`; + break; + case 'patch': + newVersion = `${major}.${minor}.${patch + 1}`; + break; +} + +pkg.version = newVersion; +writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n'); + +console.error(`Bumped version: ${major}.${minor}.${patch} -> ${newVersion} (${bumpType})`); +console.log(`NEW_VERSION=${newVersion}`); diff --git a/scripts/generate-changelog.js b/scripts/generate-changelog.js new file mode 100755 index 0000000..11a523f --- /dev/null +++ b/scripts/generate-changelog.js @@ -0,0 +1,164 @@ +#!/usr/bin/env node + +/** + * generate-changelog.js + * + * Calls Claude API with commit history to generate a formatted changelog. + * + * Usage: node scripts/generate-changelog.js + * + * Environment variables: + * ANTHROPIC_API_KEY - Required API key for Claude + * + * Output: Writes changelog to stdout or to a file if --output is specified + */ + +import Anthropic from '@anthropic-ai/sdk'; +import { execSync } from 'child_process'; + +const CHANGELOG_PROMPT = `You are a changelog generator for Agentage CLI. +Agentage CLI is the daemon and command-line interface for running and managing AI agents. + +Given the following git commits, generate a concise, user-friendly changelog entry. + +Rules: +1. Group changes into these categories (only include categories that have changes): + - **New Features** - New functionality + - **Improvements** - Enhancements to existing features + - **Bug Fixes** - Fixed issues + - **Performance** - Performance improvements + - **Documentation** - Doc changes (only if significant) + - **Infrastructure** - CI/CD, build system changes (only if user-facing) + +2. Write from a user's perspective - what does this mean for them? +3. Be concise - one line per change, no unnecessary details +4. Skip internal refactoring or cleanup that doesn't affect users +5. Use present tense ("Add" not "Added") +6. Start each item with a verb (Add, Fix, Improve, Update, etc.) +7. If there are no meaningful user-facing changes, output "No significant changes" + +Format your response as markdown bullet points under each category heading. +Do NOT include the version number or date - just the categorized changes. + +Example output format: +### New Features +- Add workspace details page with tabbed navigation +- Add support for custom agent configurations + +### Bug Fixes +- Fix memory leak in agent connection handling + +Commits to analyze: +`; + +/** + * Get commits since the last tag + */ +function getCommitsSinceLastTag() { + try { + // Get the last tag + const lastTag = execSync('git describe --tags --abbrev=0 2>/dev/null || echo ""', { + encoding: 'utf-8', + cwd: process.cwd(), + }).trim(); + + let range; + if (lastTag) { + range = `${lastTag}..HEAD`; + console.error(`Getting commits since tag: ${lastTag}`); + } else { + // No tags, get last 50 commits + range = 'HEAD~50..HEAD'; + console.error('No tags found, getting last 50 commits'); + } + + // Get commit messages with author and date + const commits = execSync( + `git log ${range} --pretty=format:"%h %s" --no-merges 2>/dev/null || echo ""`, + { + encoding: 'utf-8', + cwd: process.cwd(), + } + ).trim(); + + return commits; + } catch (error) { + console.error('Error getting commits:', error.message); + return ''; + } +} + +/** + * Generate changelog using Claude API + */ +async function generateChangelog(commits, version) { + if (!process.env.ANTHROPIC_API_KEY) { + throw new Error('ANTHROPIC_API_KEY environment variable is required'); + } + + if (!commits || commits.trim() === '') { + return 'No significant changes'; + } + + const client = new Anthropic(); + + const message = await client.messages.create({ + model: 'claude-sonnet-4-20250514', + max_tokens: 1024, + messages: [ + { + role: 'user', + content: CHANGELOG_PROMPT + '\n```\n' + commits + '\n```', + }, + ], + }); + + // Extract text content from response + const content = message.content + .filter((block) => block.type === 'text') + .map((block) => block.text) + .join('\n'); + + return content.trim(); +} + +async function main() { + const version = process.argv[2]; + + if (!version) { + console.error('Usage: node generate-changelog.js '); + console.error('Example: node generate-changelog.js 2.1.3'); + process.exit(1); + } + + console.error(`Generating changelog for version ${version}...`); + console.error(''); + + const commits = getCommitsSinceLastTag(); + + if (!commits) { + console.error('No commits found since last tag'); + console.log('No significant changes'); + process.exit(0); + } + + console.error('Commits found:'); + console.error(commits); + console.error(''); + console.error('Calling Claude API...'); + + try { + const changelog = await generateChangelog(commits, version); + console.error(''); + console.error('Generated changelog:'); + console.error('---'); + + // Output the changelog to stdout (can be captured by other scripts) + console.log(changelog); + } catch (error) { + console.error('Error generating changelog:', error.message); + process.exit(1); + } +} + +main(); diff --git a/scripts/update-changelog.js b/scripts/update-changelog.js new file mode 100755 index 0000000..6928aa6 --- /dev/null +++ b/scripts/update-changelog.js @@ -0,0 +1,184 @@ +#!/usr/bin/env node + +/** + * update-changelog.js + * + * Inserts a new changelog entry into CHANGELOG.md. + * + * Usage: node scripts/update-changelog.js [changelog-content] + * + * If changelog-content is not provided, reads from stdin. + * + * The script expects CHANGELOG.md to have a specific format: + * - Header section at the top + * - Entries starting with ## [version] - date + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = join(__dirname, '..'); +const changelogPath = join(rootDir, 'CHANGELOG.md'); + +/** + * Get today's date in YYYY-MM-DD format + */ +function getToday() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +/** + * Create initial CHANGELOG.md if it doesn't exist + */ +function createInitialChangelog() { + const content = `# Changelog + +All notable changes to Agentage CLI will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +--- + +`; + writeFileSync(changelogPath, content); + return content; +} + +/** + * Insert new entry into changelog + */ +function insertChangelogEntry(version, content) { + let changelog; + + if (existsSync(changelogPath)) { + changelog = readFileSync(changelogPath, 'utf-8'); + } else { + changelog = createInitialChangelog(); + } + + const date = getToday(); + const newEntry = `## [${version}] - ${date} + +${content} + +`; + + // Check if there's already an entry for this version + const versionPattern = new RegExp(`^## \\[${version.replace(/\./g, '\\.')}\\]`, 'm'); + if (versionPattern.test(changelog)) { + console.error(`Warning: Version ${version} already exists in CHANGELOG.md`); + console.error('Updating existing entry...'); + + // Replace existing entry (up to next ## [ or ---) + const entryPattern = new RegExp( + `## \\[${version.replace(/\./g, '\\.')}\\][^]*?(?=## \\[|---|\$)`, + 'm' + ); + const updatedContent = changelog.replace(entryPattern, newEntry); + writeFileSync(changelogPath, updatedContent); + return; + } + + // Find the header section end - look for "---" that comes after "Semantic Versioning" + // The header pattern: # Changelog ... Semantic Versioning ... --- + const headerPattern = /^# Changelog[\s\S]*?Semantic Versioning[^\n]*\n+---\n/m; + const headerMatch = changelog.match(headerPattern); + + if (headerMatch) { + const insertPosition = headerMatch.index + headerMatch[0].length; + const before = changelog.slice(0, insertPosition); + const after = changelog.slice(insertPosition).replace(/^\n+/, ''); // Remove leading newlines + + const newChangelog = before + '\n' + newEntry + after; + writeFileSync(changelogPath, newChangelog); + } else { + // Fallback: look for first ## [ entry and insert before it + const firstEntryMatch = changelog.match(/^## \[/m); + if (firstEntryMatch) { + const insertPosition = firstEntryMatch.index; + const before = changelog.slice(0, insertPosition); + const after = changelog.slice(insertPosition); + const newChangelog = before + newEntry + after; + writeFileSync(changelogPath, newChangelog); + } else { + // No entries found, append to end + const newChangelog = changelog + '\n' + newEntry; + writeFileSync(changelogPath, newChangelog); + } + } +} + +/** + * Read from stdin if available + */ +async function readStdin() { + return new Promise((resolve, reject) => { + let data = ''; + + // Check if stdin has data (not a TTY) + if (process.stdin.isTTY) { + resolve(''); + return; + } + + process.stdin.setEncoding('utf-8'); + process.stdin.on('data', (chunk) => { + data += chunk; + }); + process.stdin.on('end', () => { + resolve(data.trim()); + }); + process.stdin.on('error', reject); + + // Set a timeout in case stdin is not connected + setTimeout(() => { + if (!data) { + resolve(''); + } + }, 100); + }); +} + +async function main() { + const version = process.argv[2]; + let content = process.argv[3]; + + if (!version) { + console.error('Usage: node update-changelog.js [changelog-content]'); + console.error(''); + console.error('If changelog-content is not provided, reads from stdin:'); + console.error(' echo "### New Features\\n- Add feature X" | node update-changelog.js 2.1.3'); + process.exit(1); + } + + // If no content provided as argument, try to read from stdin + if (!content) { + content = await readStdin(); + } + + if (!content) { + console.error('Error: No changelog content provided'); + console.error('Provide content as argument or pipe via stdin'); + process.exit(1); + } + + console.log(`Updating CHANGELOG.md for version ${version}...`); + console.log(''); + + insertChangelogEntry(version, content); + + console.log(`Successfully updated CHANGELOG.md with version ${version}`); + console.log(`Date: ${getToday()}`); +} + +main().catch((err) => { + console.error('Error:', err.message); + process.exit(1); +}); From ae8fa5cb070c4533969e0c7f5047ec8ee8d8ed27 Mon Sep 17 00:00:00 2001 From: Volodymyr Vreshch Date: Sun, 29 Mar 2026 11:31:50 +0200 Subject: [PATCH 2/2] fix: sync package-lock.json with @anthropic-ai/sdk addition --- package-lock.json | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/package-lock.json b/package-lock.json index 5b0ee47..c84185b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "agentage": "dist/cli.js" }, "devDependencies": { + "@anthropic-ai/sdk": "latest", "@types/express": "latest", "@types/node": "latest", "@types/ws": "latest", @@ -65,6 +66,27 @@ "@agentage/core": ">=0.2.0" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.80.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.80.0.tgz", + "integrity": "sha512-WeXLn7zNVk3yjeshn+xZHvld6AoFUOR3Sep6pSoHho5YbSi6HwcirqgPA5ccFuW8QTVJAAU7N8uQQC6Wa9TG+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -101,6 +123,16 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -2576,6 +2608,20 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3749,6 +3795,13 @@ "node": ">=0.6" } }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",