Skip to content
Closed
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
45 changes: 45 additions & 0 deletions .github/workflows/changelog-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Receives platform-release dispatch and opens an editorial PR adding the new
# version to the changelog page (RELEASE-PROCESS-PLAN.md §5.4).
name: changelog-sync

on:
repository_dispatch:
types: [platform-release]

permissions:
contents: write
pull-requests: write

jobs:
open-changelog-pr:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Generate changelog entry
env:
VERSION: ${{ github.event.client_payload.version }}
BODY_B64: ${{ github.event.client_payload.release_body_b64 }}
run: |
set -eu
printf '%s' "$BODY_B64" | base64 -d > /tmp/release-body.md
node scripts/changelog-from-release.mjs "$VERSION" /tmp/release-body.md src/pages/changelog.mdx
- name: Open PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ github.event.client_payload.version }}
RELEASE_URL: ${{ github.event.client_payload.release_url }}
run: |
set -eu
branch="chore/changelog-${VERSION}"
git config user.name "futureagi-release-bot"
git config user.email "release-bot@futureagi.com"
git switch -c "$branch"
git add src/pages/changelog.mdx
git commit -m "docs(changelog): add ${VERSION}"
git push origin "$branch"
gh pr create --base main --head "$branch" \
--title "docs(changelog): ${VERSION}" \
--body "Auto-generated from the [${VERSION} release notes](${RELEASE_URL}). Edit for a product audience before merging — rewrite or drop raw commit bullets; merging as-is is acceptable."
58 changes: 58 additions & 0 deletions scripts/changelog-from-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env node
// Transforms a GitHub Release body (release-please format) into a changelog.mdx
// section and inserts it below the marker. Usage:
// node scripts/changelog-from-release.mjs <version> <bodyFile> <changelogFile>
import { readFileSync, writeFileSync } from "node:fs";

const MARKER = "{/* changelog:insert-below — automation inserts new releases here; do not remove */}";
const SECTION_MAP = new Map([
["Features", "New Features"],
["Bug Fixes", "Bug Fixes"],
["Performance Improvements", "Improvements"],
["Reverts", "Reverts"],
]);

export function transform(version, body, now = new Date()) {
const month = now.toLocaleString("en-US", { month: "long", year: "numeric" });
const lines = body.split("\n");
const out = [`## ${version} - ${month}`, ""];
let currentMapped = null;
let breaking = [];
let inBreaking = false;
for (const line of lines) {
const h = line.match(/^#{2,3}\s+(.*)$/);
if (h) {
const title = h[1].trim();
if (/BREAKING CHANGES/i.test(title)) { inBreaking = true; currentMapped = null; continue; }
inBreaking = false;
currentMapped = SECTION_MAP.get(title) ?? null;
if (currentMapped) out.push(`### ${currentMapped}`, "");
continue;
}
if (inBreaking && line.trim().startsWith("*")) breaking.push(line.replace(/^\s*\*/, "-"));
else if (currentMapped && line.trim().startsWith("*")) out.push(line.replace(/^\s*\*/, "-"));
else if (currentMapped && line.trim() === "") {
if (out[out.length - 1] !== "") out.push("");
}
}
if (breaking.length) out.push("### Breaking Changes", "", ...breaking, "");
if (out[out.length - 1] !== "") out.push("");
out.push("---", "");
return out.join("\n");
}

export function insert(changelog, section) {
const idx = changelog.indexOf(MARKER);
if (idx === -1) throw new Error("changelog marker not found");
const insertAt = idx + MARKER.length;
return changelog.slice(0, insertAt) + "\n\n" + section.trimEnd() + "\n" + changelog.slice(insertAt);
}

const isMain = process.argv[1] && import.meta.url.endsWith(process.argv[1].split("/").pop());
if (isMain && process.argv.length >= 5) {
const [, , version, bodyFile, changelogFile] = process.argv;
const body = readFileSync(bodyFile, "utf8");
const changelog = readFileSync(changelogFile, "utf8");
writeFileSync(changelogFile, insert(changelog, transform(version, body)));
console.log(`Inserted ${version} into ${changelogFile}`);
}
49 changes: 49 additions & 0 deletions scripts/changelog-from-release.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { transform, insert } from "./changelog-from-release.mjs";

const RELEASE_BODY = `## [2.1.0](https://github.com/future-agi/future-agi/compare/v2.0.0...v2.1.0) (2026-08-01)

### ⚠ BREAKING CHANGES

* **api:** remove deprecated /v1/eval endpoint

### Features

* **observe:** session-level trace grouping ([#901](https://github.com/future-agi/future-agi/pull/901))
* **gateway:** streaming responses ([#905](https://github.com/future-agi/future-agi/pull/905))

### Bug Fixes

* **tracer:** off-by-one in span pagination ([#903](https://github.com/future-agi/future-agi/pull/903))

### Chores

* bump deps ([#900](https://github.com/future-agi/future-agi/pull/900))
`;

test("transform maps release-please sections to changelog sections", () => {
const s = transform("v2.1.0", RELEASE_BODY, new Date("2026-08-01T00:00:00Z"));
assert.match(s, /^## v2\.1\.0 - August 2026/m);
assert.match(s, /^### New Features/m);
assert.match(s, /session-level trace grouping/);
assert.match(s, /^### Bug Fixes/m);
assert.match(s, /^### Breaking Changes/m);
assert.match(s, /remove deprecated \/v1\/eval endpoint/);
assert.doesNotMatch(s, /Chores/);
assert.doesNotMatch(s, /bump deps/);
assert.match(s, /---\s*$/);
});

test("insert places section after marker and preserves the rest", () => {
const changelog = `intro\n\n{/* changelog:insert-below — automation inserts new releases here; do not remove */}\n\n## v2.0.0 - July 2026\nold entry\n`;
const out = insert(changelog, "## v2.1.0 - August 2026\nnew\n\n---");
const iMarker = out.indexOf("changelog:insert-below");
const iNew = out.indexOf("## v2.1.0");
const iOld = out.indexOf("## v2.0.0");
assert.ok(iMarker < iNew && iNew < iOld);
});

test("insert throws when marker missing", () => {
assert.throws(() => insert("no marker here", "x"), /marker not found/);
});
2 changes: 2 additions & 0 deletions src/pages/changelog.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import Callout from '../components/docs/Callout.astro';

Stay up to date with the latest features, improvements, and bug fixes.

{/* changelog:insert-below — automation inserts new releases here; do not remove */}

## v2.0.0 - December 2024

<Callout type="success" title="Major Release">
Expand Down
Loading