import { spawnSync } from "node:child_process"; import { existsSync, readFileSync } from "node:fs"; import { createServer } from "node:http"; import { homedir } from "node:os"; import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { CanvasError, createCanvas } from "@github/copilot-sdk/extension"; const servers = new Map(); const CANVAS_ID = "release-notes-showcase"; const CANVAS_TITLE = "Release Notes Showcase"; const releaseNotesInputSchema = { type: "object", additionalProperties: false, properties: { releaseName: { type: "string" }, version: { type: "string" }, releaseDate: { type: "string" }, tagline: { type: "string" }, summary: { type: "string" }, emailSubject: { type: "string" }, emailPreheader: { type: "string" }, heroStats: { type: "array", items: { type: "object", additionalProperties: false, properties: { label: { type: "string" }, value: { type: "string" }, }, required: ["label", "value"], }, }, sections: { type: "array", items: { type: "object", additionalProperties: false, properties: { title: { type: "string" }, kind: { type: "string", enum: ["feature", "improvement", "quality"], }, summary: { type: "string" }, metric: { type: "string" }, bullets: { type: "array", items: { type: "string" }, }, }, required: ["title", "summary"], }, }, contributors: { type: "array", items: { type: "object", additionalProperties: false, properties: { name: { type: "string" }, githubHandle: { type: "string" }, avatarUrl: { type: "string" }, profileUrl: { type: "string" }, area: { type: "string" }, summary: { type: "string" }, }, required: ["name"], }, }, communityThanks: { type: "array", items: { type: "string" }, }, otherChanges: { type: "array", items: { type: "object", additionalProperties: false, properties: { label: { type: "string" }, text: { type: "string" }, }, required: ["text"], }, }, callToAction: { type: "object", additionalProperties: false, properties: { label: { type: "string" }, url: { type: "string" }, }, required: ["label", "url"], }, }, }; const exportInputSchema = { type: "object", additionalProperties: false, properties: { format: { type: "string", enum: ["html", "text", "both"], }, }, }; let repositoryContext = resolveRepositoryContext("", ""); let sampleRelease = Object.freeze(buildDefaultRelease(repositoryContext)); function buildDefaultRelease(context) { const releaseName = context.displayName; const version = "vNext"; const releaseDate = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric", }).format(new Date()); return { releaseName, version, releaseDate, tagline: `No release data loaded yet for ${releaseName}.`, summary: "Use Release source to load a tag or draft unreleased changes from repository history.", emailSubject: `${releaseName} ${version} - release highlights`, emailPreheader: `Release draft for ${releaseName}.`, heroStats: [ { label: "Commits", value: "00" }, { label: "Merged PRs", value: "00" }, { label: "Closed issues", value: "00" }, { label: "Repository", value: context.repoSlug }, ], sections: [], contributors: [], communityThanks: [], otherChanges: [], callToAction: { label: "View repository", url: context.repoUrl, }, }; } function resolveRepositoryContext(preferredWorkingDirectory, sessionId) { const extensionDir = dirname(fileURLToPath(import.meta.url)); const sessionWorkingDirectory = readSessionWorkingDirectoryFromMetadata(sessionId); const repoRoot = findRepositoryRoot(preferredWorkingDirectory ?? "") || findRepositoryRoot(sessionWorkingDirectory) || findRepositoryRoot(process.cwd()) || findRepositoryRoot(extensionDir); const repoName = repoRoot ? basename(repoRoot) : "current-repository"; const remoteUrl = repoRoot ? readRemoteOrigin(repoRoot) : ""; const parsed = parseRepositorySlug(remoteUrl); const repoSlug = parsed ?? repoName; const slugLeaf = repoSlug.split("/").at(-1) || repoName; const displayName = humanizeRepoName(slugLeaf); const repoUrl = parsed ? `https://github.com/${parsed}` : "https://github.com/"; return { repoRoot, repoSlug, displayName, repoUrl, }; } function findRepositoryRoot(startPath) { if (!startPath) { return ""; } let current = startPath; while (true) { if (existsSync(join(current, ".git"))) { return current; } const parent = dirname(current); if (parent === current) { return ""; } current = parent; } } function readRemoteOrigin(repoRoot) { const result = spawnSync("git", ["-C", repoRoot, "config", "--get", "remote.origin.url"], { encoding: "utf8", }); if (result.status !== 0 || typeof result.stdout !== "string") { return ""; } return result.stdout.trim(); } function parseRepositorySlug(remoteUrl) { if (!remoteUrl) { return ""; } const httpsMatch = remoteUrl.match(/github\.com\/([^/]+\/[^/]+?)(?:\.git)?$/i); if (httpsMatch?.[1]) { return httpsMatch[1]; } const sshMatch = remoteUrl.match(/github\.com:([^/]+\/[^/]+?)(?:\.git)?$/i); if (sshMatch?.[1]) { return sshMatch[1]; } return ""; } function humanizeRepoName(value) { return value .replace(/[-_]+/g, " ") .trim() .replace(/\b\w/g, (letter) => letter.toUpperCase()); } function readSessionWorkingDirectoryFromMetadata(sessionId) { const resolvedSessionId = pickString(sessionId, "") || pickString(process.env.SESSION_ID, "") || pickString(process.env.COPILOT_AGENT_SESSION_ID, ""); if (!resolvedSessionId) { return ""; } const metadataPath = join( homedir(), ".copilot", "session-state", resolvedSessionId, "vscode.metadata.json", ); const workspacePath = join( homedir(), ".copilot", "session-state", resolvedSessionId, "workspace.yaml", ); const candidatePaths = [metadataPath, workspacePath]; for (const path of candidatePaths) { if (!existsSync(path)) { continue; } let text = ""; try { text = readFileSync(path, "utf8"); } catch { continue; } const match = text.match(/^cwd:\s*(.+)$/m); if (match?.[1]?.trim()) { return match[1].trim(); } } return ""; } function runGit(repoRoot, args) { if (!repoRoot) { return ""; } const result = spawnSync("git", ["-C", repoRoot, ...args], { encoding: "utf8", }); if (result.status !== 0 || typeof result.stdout !== "string") { return ""; } return result.stdout.trim(); } function listReleaseTags(repoRoot) { const output = runGit(repoRoot, ["tag", "--sort=-creatordate"]); if (!output) { return []; } return output .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); } function readTagDate(repoRoot, tag) { const output = runGit(repoRoot, ["log", "-1", "--date=short", "--format=%ad", tag]); return output || ""; } function readCommitSummaries(repoRoot, rangeExpr) { const output = runGit(repoRoot, [ "log", "--max-count=250", "--pretty=format:%s%x1f%an", rangeExpr, ]); if (!output) { return []; } return output .split(/\r?\n/) .map((line) => line.split("\x1f")) .filter((parts) => parts.length >= 2) .map(([subject, author]) => ({ subject: cleanCommitSubject(subject), author: pickString(author, "Contributor"), })) .filter((entry) => entry.subject); } function cleanCommitSubject(value) { return pickString(value, "") .replace(/^\w+(\([^)]+\))?!?:\s*/i, "") .replace(/\s+\(#\d+\)\s*$/u, "") .trim(); } function classifyCommit(subject) { const lower = subject.toLowerCase(); if (/^(feat|feature)\b/.test(lower) || /add|introduce|support|new/.test(lower)) { return "feature"; } if (/^(fix|perf|refactor)\b/.test(lower) || /improv|stabil|reliab|optim/.test(lower)) { return "improvement"; } return "quality"; } function toReleaseStateFromCommits(context, commits, options) { const releaseName = context.displayName; const version = options.version; const releaseDate = options.releaseDate; const commitCount = commits.length; const mergedPulls = Array.isArray(options.mergedPulls) ? options.mergedPulls : []; const closedIssues = Array.isArray(options.closedIssues) ? options.closedIssues : []; if (commitCount === 0) { const emptyState = buildDefaultRelease(context); return { ...emptyState, releaseName, version, releaseDate, tagline: `No commit changes were detected for ${options.rangeLabel}.`, summary: `There are no commits in ${options.rangeLabel}, so this draft starts from the repository template.`, emailSubject: `${releaseName} ${version} - release highlights`, emailPreheader: `No commit changes detected for ${options.rangeLabel}.`, callToAction: { label: options.callToActionLabel, url: options.callToActionUrl, }, }; } const buckets = { feature: [], improvement: [], quality: [], }; const contributorCounts = new Map(); for (const commit of commits) { const kind = classifyCommit(commit.subject); buckets[kind].push(commit.subject); contributorCounts.set(commit.author, (contributorCounts.get(commit.author) ?? 0) + 1); } const sections = []; if (mergedPulls.length > 0) { sections.push({ title: "Merged pull requests", kind: "feature", summary: `Pull requests merged since ${options.sinceLabel}.`, metric: `${mergedPulls.length} merged`, bullets: mergedPulls.slice(0, 6).map((pull) => `#${pull.number} ${pull.title}`), }); } for (const kind of ["feature", "improvement", "quality"]) { const entries = buckets[kind]; if (entries.length === 0) { continue; } const kindTitle = kind === "feature" ? "Feature work shipped" : kind === "improvement" ? "Improvements and fixes" : "Quality and maintenance updates"; const kindSummary = kind === "feature" ? "New capabilities and user-facing improvements landed in this release." : kind === "improvement" ? "Stability, performance, and reliability updates were delivered." : "Foundational cleanup and maintenance work strengthened the codebase."; sections.push({ title: kindTitle, kind, summary: kindSummary, metric: `${entries.length} commits`, bullets: entries.slice(0, 6), }); } const sortedContributors = [...contributorCounts.entries()] .sort((left, right) => right[1] - left[1]) .slice(0, 6); const contributors = sortedContributors.map(([name, count]) => ({ name, githubHandle: "", avatarUrl: "", profileUrl: context.repoUrl, area: count === 1 ? "1 commit" : `${count} commits`, summary: `Contributed ${count} change${count === 1 ? "" : "s"} in ${options.rangeLabel}.`, })); const otherChanges = commits.slice(0, 7).map((commit) => ({ label: classifyCommit(commit.subject), text: commit.subject, })); if (closedIssues.length > 0) { otherChanges.unshift( ...closedIssues.slice(0, 6).map((issue) => ({ label: `Issue #${issue.number}`, text: issue.title, })), ); } const featureCount = buckets.feature.length; return { releaseName, version, releaseDate, tagline: `${commitCount} commits, ${mergedPulls.length} merged PRs, and ${closedIssues.length} closed issues since ${options.sinceLabel}.`, summary: `This draft combines git history with merged pull requests and closed issues since ${options.sinceLabel}.`, emailSubject: `${releaseName} ${version} - release highlights`, emailPreheader: `${commitCount} commits, ${mergedPulls.length} merged PRs, and ${closedIssues.length} closed issues summarized from ${options.rangeLabel}.`, heroStats: [ { label: "Commits", value: padCount(commitCount) }, { label: "Merged PRs", value: padCount(mergedPulls.length) }, { label: "Closed issues", value: padCount(closedIssues.length) }, { label: "Features", value: padCount(featureCount) }, ], sections: sections.length > 0 ? sections : buildDefaultRelease(context).sections, contributors: contributors.length > 0 ? contributors : buildDefaultRelease(context).contributors, communityThanks: [], otherChanges, callToAction: { label: options.callToActionLabel, url: options.callToActionUrl, }, }; } function getGitHubToken() { const direct = pickString(process.env.GITHUB_TOKEN, ""); if (direct) { return direct; } const key = Object.keys(process.env).find((name) => name.startsWith("COPILOT_GH_ACCOUNT_github_2E_com_"), ); return key ? pickString(process.env[key], "") : ""; } async function fetchGithubJson(url) { const headers = { Accept: "application/vnd.github+json", "User-Agent": "release-notes-showcase", }; const token = getGitHubToken(); if (token) { headers.Authorization = `Bearer ${token}`; } const response = await fetch(url, { headers }); if (!response.ok) { return []; } const payload = await response.json(); return Array.isArray(payload) ? payload : []; } function normalizeIsoDate(dateValue) { if (!dateValue) { return ""; } if (/^\d{4}-\d{2}-\d{2}$/.test(dateValue)) { return `${dateValue}T00:00:00Z`; } return dateValue; } async function fetchUnreleasedGithubSignals(context, sinceDate) { if (!context.repoSlug.includes("/")) { return { mergedPulls: [], closedIssues: [] }; } const sinceIso = normalizeIsoDate(sinceDate); if (!sinceIso) { return { mergedPulls: [], closedIssues: [] }; } const [owner, repo] = context.repoSlug.split("/"); const pullsUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/pulls?state=closed&sort=updated&direction=desc&per_page=100`; const issuesUrl = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues?state=closed&since=${encodeURIComponent(sinceIso)}&sort=updated&direction=desc&per_page=100`; try { const [pulls, issues] = await Promise.all([ fetchGithubJson(pullsUrl), fetchGithubJson(issuesUrl), ]); const mergedPulls = pulls .filter((pull) => isRecord(pull) && typeof pull.merged_at === "string") .filter((pull) => Date.parse(pull.merged_at) >= Date.parse(sinceIso)) .map((pull) => ({ number: Number(pull.number) || 0, title: pickString(pull.title, "Merged pull request"), })) .filter((pull) => pull.number > 0); const closedIssues = issues .filter((issue) => isRecord(issue) && !issue.pull_request) .filter((issue) => typeof issue.closed_at === "string") .filter((issue) => Date.parse(issue.closed_at) >= Date.parse(sinceIso)) .map((issue) => ({ number: Number(issue.number) || 0, title: pickString(issue.title, "Closed issue"), })) .filter((issue) => issue.number > 0); return { mergedPulls, closedIssues }; } catch { return { mergedPulls: [], closedIssues: [] }; } } async function buildReleaseFromRepository(context, mode, selectedTag) { const tags = listReleaseTags(context.repoRoot); const latestTag = tags[0] ?? ""; if (mode === "tag" && selectedTag && tags.includes(selectedTag)) { const index = tags.indexOf(selectedTag); const previousTag = index >= 0 && index < tags.length - 1 ? tags[index + 1] : ""; const rangeExpr = previousTag ? `${previousTag}..${selectedTag}` : selectedTag; const releaseDate = readTagDate(context.repoRoot, selectedTag) || sampleRelease.releaseDate; const commits = readCommitSummaries(context.repoRoot, rangeExpr); const releaseUrl = context.repoUrl !== "https://github.com/" ? `${context.repoUrl}/releases/tag/${encodeURIComponent(selectedTag)}` : context.repoUrl; return toReleaseStateFromCommits(context, commits, { version: selectedTag, releaseDate, rangeLabel: rangeExpr, sinceLabel: previousTag || selectedTag, callToActionLabel: `View ${selectedTag} release`, callToActionUrl: releaseUrl, }); } const rangeExpr = latestTag ? `${latestTag}..HEAD` : "HEAD"; const commits = readCommitSummaries(context.repoRoot, rangeExpr); const latestTagDate = latestTag ? readTagDate(context.repoRoot, latestTag) : ""; const unreleasedSignals = latestTagDate ? await fetchUnreleasedGithubSignals(context, latestTagDate) : { mergedPulls: [], closedIssues: [] }; const compareUrl = context.repoUrl !== "https://github.com/" && latestTag ? `${context.repoUrl}/compare/${encodeURIComponent(latestTag)}...HEAD` : context.repoUrl; return toReleaseStateFromCommits(context, commits, { version: "vNext", releaseDate: sampleRelease.releaseDate, rangeLabel: rangeExpr, sinceLabel: latestTag || "the beginning of the branch", mergedPulls: unreleasedSignals.mergedPulls, closedIssues: unreleasedSignals.closedIssues, callToActionLabel: latestTag ? "Review unreleased commits" : "View repository", callToActionUrl: compareUrl, }); } export const releaseNotesShowcaseCanvas = createCanvas({ id: CANVAS_ID, displayName: CANVAS_TITLE, description: "Presents release notes as a high-impact launch summary with contributor callouts and email-ready export output.", inputSchema: releaseNotesInputSchema, actions: [ { name: "export_email", description: "Returns email-ready subject, HTML, and text for the release notes currently shown in the canvas.", inputSchema: exportInputSchema, handler: async (ctx) => { const entry = servers.get(ctx.instanceId); if (!entry) { throw new CanvasError( "canvas_state_missing", "Open the release notes canvas before exporting email content.", ); } return buildExportPayload(entry.getState(), ctx.input); }, }, { name: "get_release_snapshot", description: "Returns a concise snapshot of the release story shown in the canvas.", handler: async (ctx) => { const entry = servers.get(ctx.instanceId); if (!entry) { throw new CanvasError( "canvas_state_missing", "Open the release notes canvas before requesting a snapshot.", ); } const state = entry.getState(); return { title: `${state.releaseName} ${state.version}`, summary: state.summary, sections: state.sections.map((section) => ({ title: section.title, kind: section.kind, })), contributors: state.contributors.map((contributor) => contributor.name), }; }, }, ], open: async (ctx) => { repositoryContext = resolveRepositoryContext(ctx.session?.workingDirectory, ctx.sessionId); sampleRelease = Object.freeze(buildDefaultRelease(repositoryContext)); const state = buildState(ctx.input); let entry = servers.get(ctx.instanceId); if (!entry) { entry = await startServer(state); servers.set(ctx.instanceId, entry); } else { entry.setState(state); } return { title: `${state.releaseName} ${state.version}`, status: `${state.contributors.length} contributors highlighted`, url: entry.url, }; }, onClose: async (ctx) => { const entry = servers.get(ctx.instanceId); if (!entry) { return; } servers.delete(ctx.instanceId); await new Promise((resolve) => entry.server.close(resolve)); }, }); function buildState(input) { const candidate = isRecord(input) ? input : {}; const releaseName = pickString(candidate.releaseName, sampleRelease.releaseName); const version = pickString(candidate.version, sampleRelease.version); const summary = pickString(candidate.summary, sampleRelease.summary); const sections = normalizeSections(candidate.sections); const contributors = normalizeContributors(candidate.contributors); const heroStats = normalizeHeroStats(candidate.heroStats, sections, contributors); const emailSubject = pickString( candidate.emailSubject, `${releaseName} ${version} - release highlights`, ); return { releaseName, version, releaseDate: pickString(candidate.releaseDate, sampleRelease.releaseDate), tagline: pickString(candidate.tagline, sampleRelease.tagline), summary, emailSubject, emailPreheader: pickString(candidate.emailPreheader, summary), heroStats, sections, contributors, communityThanks: normalizeCommunityThanks(candidate.communityThanks), otherChanges: normalizeOtherChanges(candidate.otherChanges), callToAction: normalizeCallToAction(candidate.callToAction), }; } function normalizeCommunityThanks(value) { if (!Array.isArray(value)) { return []; } const handles = value .filter((handle) => typeof handle === "string") .map((handle) => handle.trim().replace(/^@/, "")) .filter((handle) => handle.length > 0); return handles; } function normalizeOtherChanges(value) { if (!Array.isArray(value) || value.length === 0) { return []; } const changes = value .filter(isRecord) .map((change) => ({ label: pickString(change.label, ""), text: pickString(change.text, ""), })) .filter((change) => change.text); return changes; } function normalizeSections(value) { if (!Array.isArray(value) || value.length === 0) { return []; } return value .filter(isRecord) .map((section) => { const kind = isSectionKind(section.kind) ? section.kind : "feature"; const bullets = toStringArray(section.bullets); const title = pickString(section.title, ""); const summary = pickString(section.summary, ""); if (!title || !summary) { return null; } return { title, kind, summary, metric: pickString(section.metric, ""), bullets, }; }) .filter(Boolean); } function normalizeContributors(value) { if (!Array.isArray(value) || value.length === 0) { return []; } return value .filter(isRecord) .map((contributor) => { const name = pickString(contributor.name, ""); if (!name) { return null; } return { name, githubHandle: pickString(contributor.githubHandle, ""), avatarUrl: pickString(contributor.avatarUrl, ""), profileUrl: pickString(contributor.profileUrl, ""), area: pickString(contributor.area, ""), summary: pickString(contributor.summary, ""), }; }) .filter(Boolean); } function normalizeHeroStats(value, sections, contributors) { if (Array.isArray(value) && value.length > 0) { const stats = value .filter(isRecord) .map((stat) => ({ label: pickString(stat.label, ""), value: pickString(stat.value, ""), })) .filter((stat) => stat.label && stat.value); if (stats.length > 0) { return stats; } } return [ { label: "Top features", value: padCount(countByKind(sections, "feature")), }, { label: "Core improvements", value: padCount(countByKind(sections, "improvement")), }, { label: "Contributors", value: padCount(contributors.length), }, { label: "Areas touched", value: padCount(sections.length), }, ]; } function normalizeCallToAction(value) { if (isRecord(value)) { return { label: pickString(value.label, sampleRelease.callToAction.label), url: pickString(value.url, sampleRelease.callToAction.url), }; } return { ...sampleRelease.callToAction }; } function buildExportPayload(state, input) { const format = isRecord(input) ? pickString(input.format, "both") : "both"; const html = buildEmailHtml(state); const text = buildEmailText(state); const payload = { subject: state.emailSubject, preheader: state.emailPreheader, fileNameBase: slugify(`${state.releaseName}-${state.version}-release-notes-email`), }; if (format === "html") { return { ...payload, html }; } if (format === "text") { return { ...payload, text }; } return { ...payload, html, text }; } function buildEmailHtml(state) { const sectionRows = state.sections .map((section) => { const bullets = section.bullets .map( (bullet) => `
  • ${escapeHtml(bullet)}
  • `, ) .join(""); return `
    ${escapeHtml(kindLabel(section.kind))}

    ${escapeHtml(section.title)}

    ${escapeHtml(section.summary)}

    ${escapeHtml(section.metric)}

    `; }) .join(""); const contributorRows = state.contributors .map( (contributor) => `
    ${escapeHtml(contributor.name)}

    ${escapeHtml(contributor.name)}

    ${escapeHtml(contributor.area)}

    ${escapeHtml(contributor.summary)}

    `, ) .join(""); const otherChangesHtml = (state.otherChanges ?? []) .map( (change) => `
  • ${change.label ? `${escapeHtml(change.label)}: ` : ""}${escapeHtml(change.text)}
  • `, ) .join(""); const communityHtml = (state.communityThanks ?? []) .map( (handle) => `@${escapeHtml(handle)}`, ) .join(" · "); return ` ${escapeHtml(state.emailSubject)}
    ${escapeHtml(state.emailPreheader)}

    ${escapeHtml(state.releaseDate)}

    ${escapeHtml(`${state.releaseName} ${state.version}`)}

    ${escapeHtml(state.tagline)}

    ${escapeHtml(state.summary)}

    ${state.heroStats .map( (stat) => ` `, ) .join("")}

    ${escapeHtml(stat.value)}

    ${escapeHtml(stat.label)}

    ${sectionRows}

    Also in this release

      ${otherChangesHtml}

    Contributors in the spotlight

    ${contributorRows}

    Community thanks: ${communityHtml}

    ${escapeHtml(state.callToAction.label)}
    `; } function buildEmailText(state) { const sectionText = state.sections .map((section) => { const bullets = section.bullets.map((bullet) => `- ${bullet}`).join("\n"); return `${kindLabel(section.kind).toUpperCase()}: ${section.title}\n${section.summary}\n${bullets}`; }) .join("\n\n"); const contributorText = state.contributors .map( (contributor) => `- ${contributor.name} (${contributor.area}): ${contributor.summary}`, ) .join("\n"); const otherChangesText = (state.otherChanges ?? []) .map((change) => `- ${change.label ? `${change.label}: ` : ""}${change.text}`) .join("\n"); const communityText = (state.communityThanks ?? []) .map((handle) => `@${handle}`) .join(", "); return `${state.releaseName} ${state.version} ${state.releaseDate} ${state.tagline} ${state.summary} Highlights ${state.heroStats.map((stat) => `- ${stat.label}: ${stat.value}`).join("\n")} ${sectionText} Also in this release ${otherChangesText} Contributors in the spotlight ${contributorText} Community thanks: ${communityText} ${state.callToAction.label}: ${state.callToAction.url}`; } async function startServer(initialState) { let state = initialState; const server = createServer(async (req, res) => { const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1"); if (req.method === "GET" && requestUrl.pathname === "/") { respondHtml(res, renderHtml(state)); return; } if (req.method === "POST" && requestUrl.pathname === "/actions/export-email") { const body = await readJsonBody(req); respondJson(res, buildExportPayload(state, body)); return; } if (req.method === "GET" && requestUrl.pathname === "/actions/release-options") { const tags = listReleaseTags(repositoryContext.repoRoot); respondJson(res, { repository: repositoryContext.repoSlug, tags: tags.map((tag) => ({ value: tag, label: tag })), latestTag: tags[0] ?? "", }); return; } if (req.method === "POST" && requestUrl.pathname === "/actions/load-release") { const body = await readJsonBody(req); const mode = pickString(body?.mode, "unreleased"); const selectedTag = pickString(body?.tag, ""); if (mode !== "unreleased" && mode !== "tag") { respondJson(res, { error: "Invalid release mode." }, 400); return; } state = await buildReleaseFromRepository(repositoryContext, mode, selectedTag); respondJson(res, { title: `${state.releaseName} ${state.version}`, summary: state.summary, }); return; } respondJson(res, { error: "Not found" }, 404); }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); const port = typeof address === "object" && address ? address.port : 0; return { server, url: `http://127.0.0.1:${port}/`, getState() { return state; }, setState(nextState) { state = nextState; }, }; } function renderHtml(state) { const metricPalette = [ { bg: "#f5e0dc", border: "rgba(220, 138, 120, 0.22)", value: "#dd7878" }, { bg: "#dce7fb", border: "rgba(30, 102, 245, 0.22)", value: "#1e66f5" }, { bg: "#e7f3e0", border: "rgba(64, 160, 43, 0.22)", value: "#40a02b" }, { bg: "#efe3fb", border: "rgba(136, 57, 239, 0.22)", value: "#8839ef" }, ]; const statCards = state.heroStats .map((stat, index) => { const tone = metricPalette[index % metricPalette.length]; return `
    ${escapeHtml(stat.value)}
    ${escapeHtml(stat.label)}
    `; }) .join(""); const featureCards = state.sections .map((section) => { const bullets = section.bullets .slice(0, 2) .map((bullet) => `
  • ${escapeHtml(bullet)}
  • `) .join(""); return `
    ${escapeHtml(kindLabel(section.kind))}
    ${escapeHtml(section.metric)}

    ${escapeHtml(section.title)}

    ${escapeHtml(section.summary)}

    `; }) .join(""); const contributorCards = state.contributors .map((contributor) => { const avatar = contributor.avatarUrl ? `${escapeHtml(contributor.name)}` : `
    ${escapeHtml(getInitials(contributor.name))}
    `; const profileHref = contributor.profileUrl || "#"; const handle = contributor.githubHandle ? `@${contributor.githubHandle}` : ""; return `
    ${avatar}
    ${escapeHtml(contributor.name)} ${escapeHtml(handle)}
    ${escapeHtml(contributor.area)}

    ${escapeHtml(contributor.summary)}

    `; }) .join(""); const communityChips = (state.communityThanks ?? []) .map((handle) => { const profile = `https://github.com/${encodeURIComponent(handle)}`; const avatar = `https://github.com/${encodeURIComponent(handle)}.png?size=64`; return ` @${escapeHtml(handle)} @${escapeHtml(handle)} `; }) .join(""); const otherChangeRows = (state.otherChanges ?? []) .map((change) => { const label = change.label ? `${escapeHtml(change.label)}` : ""; return `
  • ${label}${escapeHtml(change.text)}
  • `; }) .join(""); const featureHeadline = state.sections[0]?.title ?? "Release highlights"; return ` ${escapeHtml(`${state.releaseName} ${state.version}`)}
    ${escapeHtml(state.releaseName)} repository
    ${escapeHtml(state.releaseDate)} · ✨ Fresh from the repo

    ${escapeHtml(state.releaseName)} ${escapeHtml(state.version)}

    ${escapeHtml(state.tagline)}

    ${escapeHtml(state.summary)}

    Top hit
    ${escapeHtml(featureHeadline)}
    ${escapeHtml(state.callToAction.label)}

    Release source

    Pick an existing tag, or draft unreleased work merged/closed since the latest tag.

    Top hits

    A denser dashboard view of the biggest feature work, improvements, and quality moves in this release.

    ${featureCards}

    Also in this release

    Smaller but mighty updates landing across the rest of the repository.

      ${otherChangeRows}
    `; } function readJsonBody(req) { return new Promise((resolve, reject) => { let body = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { if (!body.trim()) { resolve({}); return; } try { resolve(JSON.parse(body)); } catch (error) { reject(error); } }); req.on("error", reject); }); } function respondHtml(res, html) { res.statusCode = 200; res.setHeader("Content-Type", "text/html; charset=utf-8"); res.end(html); } function respondJson(res, payload, statusCode = 200) { res.statusCode = statusCode; res.setHeader("Content-Type", "application/json; charset=utf-8"); res.end(JSON.stringify(payload)); } function pickString(value, fallback) { return typeof value === "string" && value.trim() ? value.trim() : fallback; } function toStringArray(value) { return Array.isArray(value) ? value .filter((item) => typeof item === "string" && item.trim()) .map((item) => item.trim()) : []; } function isRecord(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function isSectionKind(value) { return value === "feature" || value === "improvement" || value === "quality"; } function countByKind(sections, kind) { return sections.filter((section) => section.kind === kind).length; } function padCount(value) { return String(value).padStart(2, "0"); } function kindLabel(kind) { if (kind === "feature") { return "🚀 Feature work"; } if (kind === "improvement") { return "✨ Improvement"; } return "🛡️ Quality"; } function emailAccent(kind) { if (kind === "feature") { return { chip: "#dbeafe", ink: "#1d4ed8", }; } if (kind === "improvement") { return { chip: "#f3e8ff", ink: "#7e22ce", }; } return { chip: "#ffedd5", ink: "#c2410c", }; } function getInitials(name) { return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map((segment) => segment[0]?.toUpperCase() ?? "") .join(""); } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function slugify(value) { return value .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/^-+|-+$/g, ""); }