Skip to content
Open
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"test:create": "vitest run ./alchemy/test/create.test.ts",
"test:init": "vitest run ./alchemy/test/init.test.ts",
"bump": "bun ./scripts/bump.ts",
"changelog:draft": "bun ./scripts/update-draft-changelog.ts",
"claude:yolo": "claude --dangerously-skip-permissions"
},
"workspaces": {
Expand Down
26 changes: 26 additions & 0 deletions scripts/bump.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,29 @@ export async function generateReleaseNotes(tag: string) {
);
}

export async function cleanupDraftSection() {
console.log("Cleaning up draft section from changelog...");

try {
const changelogPath = join(process.cwd(), "CHANGELOG.md");
const changelogContent = await readFile(changelogPath, "utf-8");

// Remove any draft section
const draftSectionRegex = /## Unreleased Changes[\s\S]*?(?=\n## |$)/;
const cleanedChangelog = changelogContent.replace(draftSectionRegex, "");

// Remove any extra "---" separators at the top
const finalChangelog = cleanedChangelog.replace(/^---\n+/, "");

if (finalChangelog !== changelogContent) {
await writeFile(changelogPath, finalChangelog);
console.log("Draft section removed from changelog");
}
} catch (error) {
console.error("Failed to cleanup draft section:", error);
}
}

async function checkNpmVersion(
packageName: string,
version: string,
Expand Down Expand Up @@ -162,6 +185,9 @@ await $`git tag v${newVersion}`;

await generateReleaseNotes(`v${newVersion}`);

// Clean up any draft section from changelog
await cleanupDraftSection();

await $`git add CHANGELOG.md`;
await $`git commit --amend --no-edit`;
await $`git tag -d v${newVersion}`;
Expand Down
100 changes: 100 additions & 0 deletions scripts/update-draft-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { $ } from "bun";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";

/**
* Updates the draft changelog with unreleased changes since the last tag
*/
export async function updateDraftChangelog() {
console.log("Updating draft changelog with unreleased changes...");

try {
// Get the latest tag
const latestTagOutput = await $`git describe --tags --abbrev=0`.text();
const latestTag = latestTagOutput.trim();

// Get commits since the latest tag
const commitsOutput =
await $`git log ${latestTag}..HEAD --oneline --no-merges`.text();
const commits = commitsOutput.trim();

if (!commits) {
console.log("No new commits since last release");
return;
}

console.log(
`Found ${commits.split("\n").length} commits since ${latestTag}`,
);

// Generate summary using Claude Code CLI
const prompt = `You are analyzing git commits for Alchemy, a TypeScript-native Infrastructure-as-Code framework.

Here are the recent commits since the last release (${latestTag}):

${commits}

Please create a brief, organized summary of the changes. Group similar changes together and focus on:
1. New features
2. Bug fixes
3. Improvements/enhancements
4. Breaking changes (if any)
5. Other notable changes

Keep it concise but informative. Use bullet points and focus on user-facing changes. Ignore internal/chore commits unless they're significant.

Format as markdown with sections like:

## Unreleased Changes

### ✨ New Features
- ...

### 🐛 Bug Fixes
- ...

### 🚀 Improvements
- ...

If there are no significant changes, just say "No significant changes since ${latestTag}".

IMPORTANT: Respond ONLY with the markdown changelog content. Do not include any explanatory text, meta-commentary, or additional context. Your response should start with "## Unreleased Changes" and contain only the formatted changelog content.`;

const claudeResult = await $`claude ${prompt}`.text();
const result = { text: claudeResult.trim() };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to parse this?

also, should we look at claude code's typescript sdk? https://www.npmjs.com/package/@anthropic-ai/claude-code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good question. The main reason why I opted for using the cli directly is because it's likely that whoever is running this (one of us) already has claude code installed and authenticated. So far as I can tell the SDK requires an API token.

In terms of parsing, no, I don't think so. It's just a text content response.

Copy link
Collaborator

@sam-goodwin sam-goodwin Aug 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is claude guaranteed to produce a pure markdown file or might it also add prose around it?

E.g.

Here is your markdown file:
```md
(actual content we want)
```


// Read current changelog
const changelogPath = join(process.cwd(), "CHANGELOG.md");
const currentChangelog = await readFile(changelogPath, "utf-8");

// Check if draft section already exists
const draftSectionRegex = /## Unreleased Changes[\s\S]*?(?=\n## |$)/;
const hasDraftSection = draftSectionRegex.test(currentChangelog);

let updatedChangelog: string;

if (hasDraftSection) {
// Replace existing draft section
updatedChangelog = currentChangelog.replace(
draftSectionRegex,
`${result.text}\n\n---\n`,
);
} else {
// Add draft section at the top
updatedChangelog = `${result.text}\n\n---\n\n${currentChangelog}`;
}

await writeFile(changelogPath, updatedChangelog);
console.log("Draft changelog updated successfully");

return result.text;
} catch (error) {
console.error("Failed to update draft changelog:", error);
return null;
}
}

// Run if called directly
if (import.meta.main) {
await updateDraftChangelog();
}