Merge pull request #11246 from Kilo-Org/sprout-principal

fix: include prerelease notes in stable releases
This commit is contained in:
Marius
2026-06-15 13:41:40 +02:00
committed by GitHub
3 changed files with 138 additions and 35 deletions
+57
View File
@@ -0,0 +1,57 @@
import { expect, test } from "bun:test"
import { buildNotes } from "./release-notes"
const section = (version: string, body: string) => `## ${version}\n\n${body}\n`
const release = (tagName: string, isPrerelease = false, isDraft = false) => ({ tagName, isPrerelease, isDraft })
test("prerelease notes contain only their own section", () => {
const changelog = section("1.2.3", "current prerelease") + section("1.2.2", "previous prerelease")
const notes = buildNotes({
version: "1.2.3",
prerelease: true,
releases: [release("v1.2.2", true)],
changelog,
})
expect(notes).toBe("current prerelease")
})
test("stable notes contain only published prereleases since the previous stable", () => {
const changelog = [
section("1.2.6", "final"),
section("1.2.5", "published prerelease"),
section("1.2.4", "draft prerelease"),
section("1.2.3", "unpublished section"),
section("1.2.1", "previous stable"),
section("1.2.0", "older prerelease"),
].join("\n")
const notes = buildNotes({
version: "1.2.6",
prerelease: false,
releases: [release("v1.2.5", true), release("v1.2.4", true, true), release("v1.2.1"), release("v1.2.0", true)],
changelog,
})
expect(notes).toBe("## 1.2.6\n\nfinal\n\n## 1.2.5\n\npublished prerelease")
})
test("stable notes include prereleases when the current section is empty", () => {
const notes = buildNotes({
version: "1.2.3",
prerelease: false,
releases: [release("v1.2.2", true), release("v1.2.1")],
changelog: section("1.2.3", "") + section("1.2.2", "prerelease") + section("1.2.1", "stable"),
})
expect(notes).toBe("## 1.2.2\n\nprerelease")
})
test("missing and empty prerelease sections use the fallback", () => {
expect(buildNotes({ version: "2.0.0", prerelease: true, releases: [], changelog: "" })).toBe("No notable changes")
expect(buildNotes({ version: "2.0.0", prerelease: true, releases: [], changelog: section("2.0.0", "") })).toBe(
"No notable changes",
)
expect(buildNotes({ version: "2.0.0", prerelease: false, releases: [release("v1.9.0", true)], changelog: "" })).toBe(
"No notable changes",
)
})
+73
View File
@@ -0,0 +1,73 @@
import { $ } from "bun"
const fallback = "No notable changes"
const pattern = /^v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/
type Release = {
tagName: string
isDraft: boolean
isPrerelease: boolean
}
export function buildNotes(input: { version: string; prerelease: boolean; releases: Release[]; changelog: string }) {
const target = version(input.version)
if (!target) throw new Error(`Invalid release version: ${input.version}`)
const sections = parse(input.changelog)
const current = sections.find((section) => section.version === target)
if (!current) return fallback
if (input.prerelease) return current.body || fallback
const stable = input.releases
.flatMap((release) => {
const value = version(release.tagName)
if (!value || release.isDraft || release.isPrerelease || compare(value, target) >= 0) return []
return [value]
})
.sort((a, b) => compare(b, a))[0]
const versions = new Set([
target,
...input.releases.flatMap((release) => {
const value = version(release.tagName)
if (!value || release.isDraft || !release.isPrerelease || compare(value, target) >= 0) return []
if (stable && compare(value, stable) <= 0) return []
return [value]
}),
])
const notes = sections
.filter((section) => versions.has(section.version) && section.body)
.map((section) => `## ${section.version}\n\n${section.body}`)
return notes.join("\n\n") || fallback
}
export async function publishNotes(input: { version: string; prerelease: boolean; repo?: string; temp?: string }) {
const repo = input.repo ? ["--repo", input.repo] : []
const releases: Release[] = await $`gh release list --limit 1000 --json tagName,isDraft,isPrerelease ${repo}`.json()
const changelog = await Bun.file(new URL("../../packages/kilo-vscode/CHANGELOG.md", import.meta.url)).text()
const body = buildNotes({ ...input, releases, changelog })
const notes = `${input.temp ?? "/tmp"}/release-notes.txt`
const target = input.version.startsWith("v") ? input.version : `v${input.version}`
const flags = input.prerelease ? ["--draft=false", "--prerelease"] : ["--draft=false"]
await Bun.write(notes, body)
flags.push("--title", `${target} (${input.prerelease ? "pre-release" : "release"})`, "--notes-file", notes)
await $`gh release edit ${target} ${flags} ${repo}`
}
function parse(input: string) {
const matches = Array.from(input.matchAll(/^##\s+(.+)$/gm))
return matches.flatMap((match, index) => {
const value = version(match[1] ?? "")
if (!value) return []
const start = (match.index ?? 0) + match[0].length
const end = matches[index + 1]?.index ?? input.length
return [{ version: value, body: input.slice(start, end).trim() }]
})
}
function version(input: string) {
return input.trim().match(pattern)?.[1]
}
function compare(a: string, b: string) {
return Bun.semver.order(a, b)
}
+8 -35
View File
@@ -88,27 +88,14 @@ if (Script.release) {
}
// kilocode_change end
// kilocode_change start
// kilocode_change end
// kilocode_change start - mark prerelease GitHub releases accordingly
// and populate release notes from the changelog updated by changeset above.
// Use an absolute path for the CHANGELOG because the imported SDK build
// script chdirs into packages/sdk/js, so a relative path would miss the file
// and fall through to the "No notable changes" default.
const kind = Script.preview ? "pre-release" : "release"
const flags = Script.preview ? ["--draft=false", "--prerelease"] : ["--draft=false"]
flags.push("--title", `v${Script.version} (${kind})`)
const changelogPath = fileURLToPath(new URL("../packages/kilo-vscode/CHANGELOG.md", import.meta.url))
const changelog = await Bun.file(changelogPath)
.text()
.catch(() => "")
const body = extractLatestSection(changelog) || "No notable changes"
const tmp = process.env.RUNNER_TEMP ?? "/tmp"
const notes = `${tmp}/release-notes.txt`
await Bun.write(notes, body)
flags.push("--notes-file", notes)
await $`gh release edit v${Script.version} ${flags} --repo ${process.env.GH_REPO}`
// kilocode_change start - publish channel-aware GitHub release notes
const { publishNotes } = await import("./kilocode/release-notes")
await publishNotes({
version: Script.version,
prerelease: Script.preview,
repo: process.env.GH_REPO,
temp: process.env.RUNNER_TEMP,
})
// kilocode_change end
}
@@ -135,17 +122,3 @@ await import(`../packages/kilo-vscode/script/publish.ts`)
const dir = fileURLToPath(new URL("..", import.meta.url))
process.chdir(dir)
// kilocode_change start - extract latest changelog section for release notes
function extractLatestSection(changelog: string): string {
if (!changelog) return ""
const lines = changelog.split("\n")
const start = lines.findIndex((line) => /^## /.test(line))
if (start < 0) return ""
const end = lines.findIndex((line, i) => i > start && /^## /.test(line))
return lines
.slice(start + 1, end < 0 ? undefined : end)
.join("\n")
.trim()
}
// kilocode_change end