/** Write release notes with a versioned skill. Needs: npm install jszip */
import { writeFile } from 'node:fs/promises';
import { basename } from 'node:path';
import JSZip from 'jszip';
import OpenAI from 'openai';
const client = new OpenAI({ apiKey: process.env.ORCA_API_KEY!, baseURL: process.env.ORCA_BASE_URL! });
const agents = client.beta.agents;
const SKILL_MD = `---
name: release-notes
description: Turn a raw changelog into customer-facing release notes. Use when asked for release notes or a changelog summary.
---
Read the changelog you are given. Use only the newest version's entries.
1. Sort each entry into Features, Fixes, or Breaking changes. Drop internal-only entries
such as refactors, tests, and CI changes.
2. Rewrite each entry as one sentence for customers: what they can now do, or what no
longer goes wrong. No ticket numbers, no internal names.
3. Fill in template.md and save the result to /workspace/outputs/release_notes.md.
4. If an entry is unclear, keep it out of the notes and list it under "Needs a human"
in your reply.
`;
const TEMPLATE_MD = `# What's new in {version}
## Features
- ...
## Fixes
- ...
## Breaking changes
- ... (leave this section out if there are none)
`;
const CHANGELOG = `# Changelog
## 2.4.0
- Export reports as PDF (#812)
- Fix totals rounding on invoices with more than 100 lines (#820)
- Refactor billing service tests
- The /v1/reports endpoint now requires a date range (#801)
`;
/** Zip a skill folder from {path: text}. */
async function bundle(files: Record<string, string>) {
const zip = new JSZip();
for (const [path, text] of Object.entries(files)) zip.file(path, text);
return new File([await zip.generateAsync({ type: 'nodebuffer' })], 'release-notes.zip');
}
let skill: { id: string; default_version: string } | undefined;
let template: { id: string } | undefined;
let agent: { id: string } | undefined;
let session: { id: string } | undefined;
try {
// 1. Upload the skill. Version 1 becomes its default.
skill = await client.skills.create({
files: [await bundle({ 'release-notes/SKILL.md': SKILL_MD, 'release-notes/template.md': TEMPLATE_MD })],
});
// 2. Save a template that installs exactly this version, with no internet.
template = await agents.environments.templates.create({
name: 'release-workspace',
network: { access: 'disabled' },
skills: [{ type: 'skill_reference', skill_id: skill.id, version: skill.default_version }],
});
agent = await agents.create({
model: process.env.ORCA_MODEL!,
name: 'release-writer',
instructions:
'You write release notes. Before you write anything, load the release-notes skill\n' +
'with the skill tool, read its template.md, and follow both exactly.\n' +
'Create /workspace/outputs if it does not exist.\n' +
'The changelog is at /workspace/inputs/CHANGELOG.md. Do not use emojis.',
});
// 3. Each session builds its sandbox from the template, plus this run's changelog.
session = await agents.sessions.create({
agent_id: agent.id,
environment: {
type: 'openai_hosted',
environment_template_id: template.id,
files: [{
type: 'inline',
path: '/workspace/inputs/CHANGELOG.md',
data: Buffer.from(CHANGELOG).toString('base64'),
}],
},
});
for await (const event of agents.sessions.stream(session.id, { input: 'Write the release notes for 2.4.0.' })) {
if (event.type === 'agent.session.turn.completed') console.log('turn:', event.turn.status);
}
for await (const artifact of agents.sessions.artifacts.list(session.id)) {
const content = await agents.sessions.artifacts.content(artifact.id, { session_id: session.id });
await writeFile(basename(artifact.path), Buffer.from(await content.arrayBuffer()));
console.log('saved:', basename(artifact.path));
}
} finally {
// Delete everything this run created, each on its own, so one failed
// delete does not skip the rest. To reuse the skill and template, keep them.
if (session) await agents.sessions.delete(session.id).catch(() => {});
if (agent) await agents.delete(agent.id).catch(() => {});
if (template) await agents.environments.templates.delete(template.id).catch(() => {});
if (skill) await client.skills.delete(skill.id).catch(() => {});
}