Skip to content
Merged
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
25 changes: 25 additions & 0 deletions devlog/_plan/260907_release_note_prefix/010_implementation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Issue #3895: implementation plan

Satisfy-spec work, triggered by issue #3895 and the request to implement separate draft PRs. Goal: remove the exact leading enforcement marker from release summaries and full changelogs. Non-goals: changing workflow enforcement, publishing a release, modifying historical releases, or generic bracket stripping. Stop after verified draft PR; report unresolved gates. Escalate if renderer changes require workflow/security-policy changes. This file records plan and evidence.

Class C2: pure formatting behavior, without modifying release authorization or execution. Independent branch from 522ce5f8c.

File map:
- MODIFY scripts/release-notes.ts: introduce a private exact-prefix normalization helper next to cleanPrTitle. Trim whitespace, remove one leading "[WRONG BRANCH] " marker, retain the rest. Call it before conventional-prefix parsing and for full-changelog titles. Preserve conventional prefixes and author/PR attribution in changelog entries.
- MODIFY tests/ci-workflows/release-notes.test.ts: helper expected scope/casing; complete renderer on generated and carried notes; same-scope grouping; preservation of unrelated bracket tags, nonleading marker, author and PR references. Assert both category and Changelog output.
- MODIFY structure/06_docs-and-release.md: record known-marker handling and preservation of original conventional titles in full changelogs.

Verification: release-notes tests directly import changed helpers; typecheck covers src only and is not represented as script type checking; full prepush is required by scripts/AGENTS.md; privacy scan. Baseline on unchanged code: 71 passed. Regression expectations come from the published issue, not from cleanPrTitle itself.

Audit: cleaning only cleanPrTitle was rejected because changelog emits the raw title. General bracket normalization would remove meaningful content. Private helper is shared by exactly two consumers and adds no runtime dependency. Explicit maintainer review for release-related changes remains pending at draft handoff.

## Verification before draft publication

- `bun install --frozen-lockfile`: passed; lockfile unchanged.
- Before the production change, four new assertions failed for marker leakage: helper cleanup, delta renderer, carried renderer, and same-scope grouping. Existing baseline: 71 passed.
- `bun test tests/ci-workflows/release-notes.test.ts`: 81 passed, 0 failed after expanding preservation cases.
- `bun run typecheck`: passed during prepush.
- `bun x tsc --ignoreConfig --noEmit --strict --target ESNext --module ESNext --moduleResolution bundler --skipLibCheck --types bun scripts/release-notes.ts`: passed; this explicitly covers the script outside the root tsconfig.
- `bun run privacy:scan`: passed.
- `bun run prepush`: not green. The parallel test lane exceeded its repository-defined 900-second deadline and exited 124; later lanes/stages did not run. Eleven failures were emitted before termination: six timeout cases across combo management, Claude messages, loopback injection, integration restore and Responses overflow; one Claude compatibility assertion failure; four Aside file-symlink EPERM cases. The full suite is incomplete, and no successful full-suite count is claimed. These files are outside the renderer change; causes other than the explicit symlink errors remain unverified. Raw local evidence is in ignored `.tmp/prepush.log`.
- Focused independent review found no concrete production blocker; it was limited and did not replace maintainer security review or complete-suite verification. Linux and macOS were not run locally.
3 changes: 2 additions & 1 deletion scripts/build-release-changelog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
parseGeneratedNotes,
rewriteTakeoverCredits,
sanitizeCommitText,
stripPrEnforcementPrefix,
} from "./release-notes";

export type AssociatedPullRequest = {
Expand Down Expand Up @@ -232,7 +233,7 @@ function renderReleaseNotes(input: {
const commits = input.entries.filter((entry): entry is CommitChange => entry.kind === "commit");

for (const pr of prs) {
changelog.push(`- #${pr.number} ${pr.title.trim()} @${pr.author || "unknown"}`);
changelog.push(`- #${pr.number} ${stripPrEnforcementPrefix(pr.title)} @${pr.author || "unknown"}`);
}
for (const commit of commits) {
const short = commit.sha.slice(0, 8);
Expand Down
10 changes: 8 additions & 2 deletions scripts/release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,8 +546,14 @@ export function parseGeneratedNotes(body: string): ReleaseNoteCategory[] {
const CONVENTIONAL_COMMIT_PREFIX =
/^(?:feat|fix|docs|chore|refactor|perf|test|build|ci|style|revert|merge|release)(?:\(([^)]+)\))?:\s*(.+)$/i;

export function stripPrEnforcementPrefix(title: string): string {
const text = title.trim();
const prefix = "[WRONG BRANCH] ";
return text.startsWith(prefix) ? text.slice(prefix.length).trim() : text;
}

export function cleanPrTitle(title: string, prNumber: number | null = null): { scope: string | null; text: string } {
let text = title.trim();
let text = stripPrEnforcementPrefix(title);
let scope: string | null = null;
const prefix = CONVENTIONAL_COMMIT_PREFIX.exec(text);
if (prefix) {
Expand Down Expand Up @@ -689,7 +695,7 @@ export function renderReleaseNotes(input: {
changelog.push(`Full Changelog: https://github.com/${repo}/compare/${from}...${to}`, "");
}
for (const pr of allPrs) {
changelog.push(`- #${pr.number} ${pr.title.trim()} @${pr.author}`);
changelog.push(`- #${pr.number} ${stripPrEnforcementPrefix(pr.title)} @${pr.author}`);
Comment thread
lidge-jun marked this conversation as resolved.
}
parts.push(changelog.join("\n"));
}
Expand Down
22 changes: 13 additions & 9 deletions structure/06_docs-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,15 +217,19 @@ separate channel-aware invariant and release-note baseline design.

### Release notes

Release notes are rendered OpenAI-Codex-style by `scripts/release-notes.ts render` inside
`.github/workflows/release.yml`: `## New Features` / `## Bug Fixes` / `## Documentation` /
`## Chores` / `## Other Changes` sections with prefix-free, scope-grouped summary bullets
(`- Providers: Add X; Add Y (#1, #2)`), followed by a `## Changelog` section listing every PR
as `- #N <title> @author`; when a comparison baseline exists, that section also includes a
compare link. Carried preview changelogs and the since-preview delta feed the same renderer,
so stable notes are the aggregate of their preview train. The raw commit dump is
intentionally gone — non-PR commits stay reachable via the Full Changelog compare link when
that link is available.
The release workflow invokes `scripts/build-release-changelog.ts`, which builds notes from
the actual Git range and uses generated PR notes as enrichment. Its categorized summaries
contain one bullet per PR or direct commit, followed by `## Changelog` entries retaining PR
titles and authors or sanitized direct-commit text. A comparison baseline adds a compare link.
Preview notes are incremental; stable notes cover the range since the previous stable tag.
The standalone `scripts/release-notes.ts render` command retains its separate scope-grouped
summary and carried-preview rendering behavior.

Both renderers strip the exact leading `[WRONG BRANCH]` marker followed by one ASCII space
from PR summary bullets and full-changelog titles. Other bracketed text is preserved.
Summary bullets remove conventional commit prefixes; PR changelog entries keep those prefixes,
PR numbers, and author attribution. This normalization does not change category selection,
direct-commit coverage, or PR-target enforcement.

The deterministic renderer produces the structure but not curated prose. Maintainers who want
the OpenAI-style grouped summaries can run the optional local polish step against the rendered
Expand Down
36 changes: 36 additions & 0 deletions tests/ci-workflows/build-release-changelog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,42 @@ const generatedBugFix = [
"**Full Changelog**: https://github.com/lidge-jun/opencodex/compare/v1.0.0...v1.1.0",
].join("\n");

describe("active release builder enforcement markers", () => {
test.each(["generated", "associated"])("normalizes summary and changelog titles from %s PRs", source => {
const title = "[WRONG BRANCH] fix(api): preserve release coverage";
const result = buildReleaseNotes({
version: "1.1.0", tags: ["v1.0.0"], npmMetadata: "", repository: "lidge-jun/opencodex",
generatedNotes: source === "generated"
? `## What's Changed\n### Bug Fixes\n* ${title} by @alice in https://github.com/lidge-jun/opencodex/pull/10`
: "",
commits: [commit("a", "fix(api): preserve release coverage (#10)", [
{ number: 10, title, author: "alice", labels: ["bug"], merged: true },
])],
});
expect(result.errors).toEqual([]);
expect(result.releasableCommitCount).toBe(1);
expect(result.body).toContain("- Preserve release coverage (#10)");
expect(result.body).toContain("- #10 fix(api): preserve release coverage @alice");
expect(result.body).not.toContain("[WRONG BRANCH]");
});

test.each([
"[Preview] fix(api): retain this marker",
"fix(api): explain [WRONG BRANCH] markers",
"[WRONG BRANCH]ish: retain this title",
])("retains meaningful changelog title text: %s", title => {
const result = buildReleaseNotes({
version: "1.1.0", tags: ["v1.0.0"], npmMetadata: "", repository: "lidge-jun/opencodex",
generatedNotes: "",
commits: [commit("a", "fix(api): preserve release coverage (#10)", [
{ number: 10, title, author: "alice", labels: ["bug"], merged: true },
])],
});
expect(result.errors).toEqual([]);
expect(result.body).toContain(`- #10 ${title} @alice`);
});
});

describe("selectReleaseBaseline", () => {
test("skips a newer release that is not reachable from the target", () => {
// A preview lives on its own lineage. Selecting the newest tag regardless of
Expand Down
63 changes: 63 additions & 0 deletions tests/ci-workflows/release-notes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,20 @@ describe("rewriteTakeoverCredits", () => {
});

describe("cleanPrTitle", () => {
test("removes the enforcement marker before extracting scope and sentence casing", () => {
expect(cleanPrTitle(" [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813) ", 3813)).toEqual({
scope: "release",
text: "Promote validated 2.45.0 to main",
});
});

test.each([
["[Preview] chore(release): keep this marker", "[Preview] chore(release): keep this marker"],
["fix: document [WRONG BRANCH] markers", "Document [WRONG BRANCH] markers"],
["[WRONG BRANCH]ish: keep this title", "[WRONG BRANCH]ish: keep this title"],
])("preserves meaningful title text: %s", (title, text) => {
expect(cleanPrTitle(title).text).toBe(text);
});
test("strips conventional prefix, keeps scope, and sentence-cases the title", () => {
expect(cleanPrTitle("feat(providers): add Baseten Model APIs preset", 653)).toEqual({
scope: "providers",
Expand Down Expand Up @@ -488,6 +502,55 @@ describe("cleanPrTitle", () => {
});

describe("renderReleaseNotes", () => {
test.each(["delta", "carried"])("removes the bot marker from summaries and full changelogs (%s)", source => {
const body = [
"## What's Changed",
"### Chores",
"* [WRONG BRANCH] chore(release): promote validated 2.45.0 to main by @lidge-jun in https://github.com/lidge-jun/opencodex/pull/3813",
].join("\n");
const notes = renderReleaseNotes({
npmMetadata: "",
...(source === "delta" ? { deltaPrNotes: body } : { carriedPreviewNotes: [
"## Chores", "",
"- [WRONG BRANCH] chore(release): promote validated 2.45.0 to main (#3813)", "",
"## Changelog", "",
"- #3813 [WRONG BRANCH] chore(release): promote validated 2.45.0 to main @lidge-jun",
].join("\n") }),
});
expect(notes).toBe([
"## Chores", "",
"- Promote validated 2.45.0 to main (#3813)", "",
"## Changelog", "",
"- #3813 chore(release): promote validated 2.45.0 to main @lidge-jun", "",
].join("\n"));
});

test("groups a bot-prefixed title with ordinary titles of the same scope", () => {
const notes = renderReleaseNotes({
npmMetadata: "",
deltaPrNotes: [
"## What's Changed", "### Chores",
"* [WRONG BRANCH] chore(release): promote verified version by @maintainer in https://github.com/lidge-jun/opencodex/pull/10",
"* chore(release): update notes by @contributor in https://github.com/lidge-jun/opencodex/pull/11",
].join("\n"),
});
expect(notes).toContain("- Release: Promote verified version; Update notes (#10, #11)");
expect(notes).toContain("- #10 chore(release): promote verified version @maintainer");
expect(notes).toContain("- #11 chore(release): update notes @contributor");
expect(notes).not.toContain("[WRONG BRANCH]");
});

test.each([
"[Preview] chore(release): retain the preview marker",
"fix: document [WRONG BRANCH] markers (#99)",
"[WRONG BRANCH]ish: retain this title",
])("preserves meaningful full-changelog title text: %s", title => {
const notes = renderReleaseNotes({
npmMetadata: "",
deltaPrNotes: `## What's Changed\n### Chores\n* ${title} by @contributor in https://github.com/lidge-jun/opencodex/pull/12`,
});
expect(notes).toContain(`- #12 ${title} @contributor`);
});
const carried = [
"<!-- Release notes generated using configuration in .github/release.yml at abc -->",
"",
Expand Down
Loading