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
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
run: |
git checkout -- package.json manifest.json 2>/dev/null || true
bun run scripts/sync-release-manifests.ts
if [[ -z "$(git status --porcelain -- package.json manifest.json)" ]]; then
if [[ -z "$(git status --porcelain -- package.json manifest.json CHANGELOG.md)" ]]; then
echo "manifests already match the latest tag"
exit 0
fi
Expand All @@ -58,11 +58,11 @@ jobs:
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git checkout -b "${BRANCH}"
git add package.json manifest.json
git add package.json manifest.json CHANGELOG.md
git commit -m "chore(release): sync manifests to v${VERSION}"
git push -u origin "${BRANCH}"
gh pr create \
--title "chore(release): sync manifests to v${VERSION}" \
--body "Automated post-release manifest alignment. Sets package.json and manifest.json to the released ${VERSION}." \
--body "Automated post-release manifest alignment. Sets package.json, manifest.json, and CHANGELOG.md to the released ${VERSION}." \
--base main --head "${BRANCH}"
gh pr merge --auto --squash --delete-branch || echo "auto-merge unavailable — merge the sync PR after CI is green"
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- semantic-release now cuts Keep a Changelog `[Unreleased]` into `## [version]` on publish and includes `CHANGELOG.md` in the post-release sync PR.

### Fixed

- Catalog GitHub release notes table no longer renders a blank header row above Plugin.
- Credits sentence no longer mentions Command Code wiring or restates MIT copyright.

## [0.6.0] - 2026-08-28

### Changed

- Renamed the published package and GitHub repository to `@brainervirus/opencode-commandcode`.

## [0.5.1] - 2026-08-28

### Fixed

- Price Command Code free SKUs at `$0` before models.dev, and fill remaining paid gaps from models.dev.

## [0.5.0] - 2026-08-28

### Added

- GitHub flow: PR-gated `main`, CI matrix (`check (test|typecheck|pack)`), semantic-release on merge, catalog updates as PRs.
- Release notes open with a catalog summary table and collapsed lists of fallback / unpriced models.
- `manifest.json` describing the bundled catalog (status, cost sources, command-code version).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Previously published as `@brainervirus/commandcode-go-opencode-provider`. Use th

## Credits

This package is based on **[FanFan4204/opencode-commandcode-provider](https://github.com/FanFan4204/opencode-commandcode-provider)**. That work started from **[brent-weatherall/opencode-commandcode-provider](https://github.com/brent-weatherall/opencode-commandcode-provider)** by **[Brent Weatherall](https://github.com/brent-weatherall)**. Thank you both — FanFan for the OpenCode provider this repo continues, and Brent for the original plugin, catalog extraction, and Command Code wiring. The license remains MIT; copyright stays with Brent Weatherall.
This package is based on **[FanFan4204/opencode-commandcode-provider](https://github.com/FanFan4204/opencode-commandcode-provider)**. That work started from **[brent-weatherall/opencode-commandcode-provider](https://github.com/brent-weatherall/opencode-commandcode-provider)** by **[Brent Weatherall](https://github.com/brent-weatherall)**. Thank you both — FanFan for the OpenCode provider this repo continues, and Brent for the original plugin, catalog extraction.

### What this package adds

Expand Down
1 change: 1 addition & 0 deletions release.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
"@semantic-release/commit-analyzer",
"./scripts/semantic-release-catalog-notes.cjs",
"@semantic-release/release-notes-generator",
"./scripts/semantic-release-changelog.cjs",
"@semantic-release/npm",
"@semantic-release/github",
],
Expand Down
3 changes: 2 additions & 1 deletion scripts/catalog-release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ import type { PricedModel } from "../src/catalog-release-notes.js";
const ROOT = join(import.meta.dir, "..");
const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf-8")) as CatalogManifest;
const models = JSON.parse(readFileSync(join(ROOT, "models.json"), "utf-8")) as PricedModel[];
process.stdout.write(catalogReleaseNotesFromFiles({ manifest, models }));
const pluginVersion = process.argv[2];
process.stdout.write(catalogReleaseNotesFromFiles({ manifest, models, pluginVersion }));
10 changes: 10 additions & 0 deletions scripts/cut-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bun
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { cutKeepAChangelog } from "../src/cut-changelog.ts";

const version = process.argv[2];
const date = process.argv[3] ?? new Date().toISOString().slice(0, 10);
if (!version) throw new Error("usage: cut-changelog.ts <version> [YYYY-MM-DD]");
const path = join(import.meta.dir, "..", "CHANGELOG.md");
writeFileSync(path, cutKeepAChangelog(readFileSync(path, "utf8"), version, date));
8 changes: 4 additions & 4 deletions scripts/semantic-release-catalog-notes.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const { execFileSync } = require("node:child_process");
const { join } = require("node:path");

module.exports = {
generateNotes() {
return execFileSync("bun", [join(__dirname, "catalog-release-notes.ts")], {
encoding: "utf8",
});
generateNotes(_config, context) {
const args = [join(__dirname, "catalog-release-notes.ts")];
if (context.nextRelease?.version) args.push(context.nextRelease.version);
return execFileSync("bun", args, { encoding: "utf8" });
},
};
14 changes: 14 additions & 0 deletions scripts/semantic-release-changelog.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const { execFileSync } = require("node:child_process");
const { join } = require("node:path");

// Keep a Changelog: promote ## [Unreleased] into ## [version] - date during
// prepare. The post-release sync PR commits CHANGELOG.md (protected main).
module.exports = {
prepare(_config, context) {
const version = context.nextRelease.version;
const date = new Date().toISOString().slice(0, 10);
execFileSync("bun", [join(__dirname, "cut-changelog.ts"), version, date], {
stdio: "inherit",
});
},
};
6 changes: 3 additions & 3 deletions src/catalog-release-notes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,8 @@ export function renderCatalogReleaseNotes(input: CatalogReleaseInput): string {
const sections = [
headline(input.status),
"",
"| | |",
"| --- | --- |",
`| Plugin | ${input.pluginVersion} |`,
"| --- | --- |",
`| Command Code | ${input.commandCodeVersion} |`,
`| Models | ${input.modelCount} (${input.reasoningModelCount} with reasoning) |`,
`| Prices | ${priceSummary(input.costSources)} |`,
Expand Down Expand Up @@ -157,9 +156,10 @@ export function renderCatalogReleaseNotes(input: CatalogReleaseInput): string {
export function catalogReleaseNotesFromFiles(input: {
manifest: CatalogManifest;
models: PricedModel[];
pluginVersion?: string;
}): string {
return renderCatalogReleaseNotes({
pluginVersion: input.manifest.pluginVersion,
pluginVersion: input.pluginVersion ?? input.manifest.pluginVersion,
commandCodeVersion: input.manifest.commandCodeVersion,
modelCount: input.manifest.modelCount,
reasoningModelCount: input.manifest.reasoningModelCount,
Expand Down
40 changes: 40 additions & 0 deletions src/cut-changelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const UNRELEASED = /^## \[Unreleased\]\s*$/;
const VERSION = /^## \[/;

export function cutKeepAChangelog(markdown: string, version: string, date: string): string {
if (new RegExp(`^## \\[${escapeRegExp(version)}\\]`, "m").test(markdown)) return markdown;

const lines = markdown.split(/(?<=\n)/);
let start: number | null = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line !== undefined && UNRELEASED.test(line.replace(/\n$/, ""))) {
start = i;
break;
}
}
if (start === null) throw new Error("CHANGELOG.md has no ## [Unreleased] heading");

let end = lines.length;
for (let j = start + 1; j < lines.length; j++) {
const line = lines[j];
if (line === undefined) continue;
const stripped = line.replace(/\n$/, "");
if (VERSION.test(stripped) && !UNRELEASED.test(stripped)) {
end = j;
break;
}
}

const before = lines.slice(0, start + 1).join("");
const body = lines
.slice(start + 1, end)
.join("")
.replace(/^\n+/, "\n");
const after = lines.slice(end).join("");
return `${before}\n## [${version}] - ${date}\n${body}${after}`;
}

function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
21 changes: 21 additions & 0 deletions tests/unit/catalog-release-notes.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { expect, test, describe } from "bun:test";
import { execFileSync } from "node:child_process";
import { join } from "node:path";
import {
renderCatalogReleaseNotes,
type CatalogReleaseInput,
} from "../../src/catalog-release-notes.ts";

const ROOT = join(import.meta.dir, "../..");

function input(partial: Partial<CatalogReleaseInput> = {}): CatalogReleaseInput {
return {
pluginVersion: "0.5.0",
Expand Down Expand Up @@ -45,6 +49,12 @@ describe("renderCatalogReleaseNotes", () => {
expect(md).toContain("1 no listed price");
});

test("uses Plugin as the table header so GitHub does not render a blank row", () => {
const md = renderCatalogReleaseNotes(input());
expect(md).toContain("| Plugin | 0.5.0 |\n| --- | --- |");
expect(md).not.toContain("| | |");
});

test("collapses models.dev, free, and unmatched lists", () => {
const md = renderCatalogReleaseNotes(input());
expect(md).toContain("<details>");
Expand Down Expand Up @@ -96,3 +106,14 @@ describe("renderCatalogReleaseNotes", () => {
expect(md).toContain('"unmatched": 1');
});
});

describe("catalog-release-notes CLI", () => {
test("honors an explicit plugin version so GitHub notes match the tag", () => {
const out = execFileSync("bun", ["scripts/catalog-release-notes.ts", "0.6.0"], {
encoding: "utf8",
cwd: ROOT,
});
expect(out).toContain("| Plugin | 0.6.0 |");
expect(out).toContain('"pluginVersion": "0.6.0"');
});
});
43 changes: 43 additions & 0 deletions tests/unit/cut-changelog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, test, describe } from "bun:test";
import { cutKeepAChangelog } from "../../src/cut-changelog.ts";

const HEADER = `# Changelog

All notable changes to this project will be documented in this file.

`;

describe("cutKeepAChangelog", () => {
test("moves Unreleased body under the new version and leaves Unreleased empty", () => {
const before = `${HEADER}## [Unreleased]

### Added

- New thing.

## [0.5.0] - 2026-08-01

### Fixed

- Old thing.
`;
const after = cutKeepAChangelog(before, "0.6.0", "2026-08-28");
expect(after).toContain("## [Unreleased]\n\n## [0.6.0] - 2026-08-28\n");
expect(after).toContain("### Added\n\n- New thing.\n");
expect(after.indexOf("## [0.6.0]")).toBeLessThan(after.indexOf("## [0.5.0]"));
expect(after.indexOf("- New thing.")).toBeGreaterThan(after.indexOf("## [0.6.0]"));
expect(after.indexOf("- New thing.")).toBeLessThan(after.indexOf("## [0.5.0]"));
});

test("is a no-op when the version heading already exists", () => {
const already = `${HEADER}## [Unreleased]

## [0.6.0] - 2026-08-28

### Added

- Already cut.
`;
expect(cutKeepAChangelog(already, "0.6.0", "2026-08-29")).toBe(already);
});
});
35 changes: 35 additions & 0 deletions tests/unit/release-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ const json = <T>(rel: string) => JSON.parse(read(rel)) as T;

const NPMJS = "${{ secrets.NPMJS }}";

describe("CHANGELOG.md", () => {
test("records shipped versions under Keep a Changelog headings", () => {
const log = read("CHANGELOG.md");
expect(log).toContain("## [Unreleased]");
expect(log).toContain("## [0.6.0]");
expect(log).toContain("## [0.5.1]");
expect(log).toContain("## [0.5.0]");
});
});

describe("README credits", () => {
test("does not restate Command Code wiring or MIT copyright in the thanks sentence", () => {
const readme = read("README.md");
expect(readme).toContain("Brent for the original plugin, catalog extraction.");
expect(readme).not.toContain(
"and Command Code wiring. The license remains MIT; copyright stays with Brent Weatherall.",
);
});
});

describe("package.json publish identity", () => {
test("is the scoped public package on BrainerVirus/opencode-commandcode", () => {
const pkg = json<{
Expand Down Expand Up @@ -92,6 +112,9 @@ describe("release.yml", () => {
expect(release?.env?.NODE_AUTH_TOKEN).toBe(NPMJS);
const blob = wf.jobs.release.steps.map((s) => s.run ?? "").join("\n");
expect(blob).not.toContain("publish-if-needed");
const sync = wf.jobs.release.steps.find((s) => s.name === "Sync release manifests to main");
expect(sync?.run).toContain("CHANGELOG.md");
expect(sync?.run).toMatch(/git add package\.json manifest\.json CHANGELOG\.md/);
});
});

Expand Down Expand Up @@ -127,17 +150,29 @@ describe("release.config.cjs", () => {
const cfg = read("release.config.cjs");
expect(cfg).toContain("@semantic-release/commit-analyzer");
expect(cfg).toContain("./scripts/semantic-release-catalog-notes.cjs");
expect(cfg).toContain("./scripts/semantic-release-changelog.cjs");
expect(cfg).toContain("@semantic-release/npm");
expect(cfg).toContain("@semantic-release/github");
expect(cfg.indexOf("semantic-release-catalog-notes.cjs")).toBeLessThan(
cfg.indexOf("@semantic-release/release-notes-generator"),
);
expect(cfg.indexOf("semantic-release-changelog.cjs")).toBeLessThan(
cfg.indexOf("@semantic-release/npm"),
);
expect(cfg.indexOf("@semantic-release/npm")).toBeLessThan(
cfg.indexOf("@semantic-release/github"),
);
});
});

describe("semantic-release-catalog-notes.cjs", () => {
test("passes nextRelease.version into the catalog notes script", () => {
const src = read("scripts/semantic-release-catalog-notes.cjs");
expect(src).toContain("nextRelease");
expect(src).toContain("catalog-release-notes.ts");
});
});

describe("verify-release-candidate.ts", () => {
test("is pack-only", () => {
const src = read("scripts/verify-release-candidate.ts")
Expand Down