ci: add scope dropdown (monorepo, cli, angular) to release workflows

Each release scope has its own packages, version source, and
independent version track:
- monorepo: 12 core @copilotkit/* packages (shared version)
- cli: copilotkit CLI (independent version)
- angular: @copilotkitnext/angular (independent version)

Branch pattern is now release/publish/<scope>/v<version> and git
tags use <scope>/v<version> for non-monorepo scopes.
This commit is contained in:
Tyler Slaton
2026-04-10 23:04:48 -07:00
parent dacc21ea35
commit 2a880fb09c
11 changed files with 274 additions and 127 deletions
+10 -2
View File
@@ -3,6 +3,14 @@ name: release / pre
on:
workflow_dispatch:
inputs:
scope:
description: "What to release"
required: true
type: choice
options:
- monorepo
- cli
- angular
suffix:
description: "Version suffix (e.g. 'fix-user-issue'). Leave blank for timestamp."
required: false
@@ -55,9 +63,9 @@ jobs:
- name: Publish prerelease
run: |
ARGS=""
ARGS="--scope ${{ inputs.scope }}"
if [ -n "${{ inputs.suffix }}" ]; then
ARGS="--suffix ${{ inputs.suffix }}"
ARGS="$ARGS --suffix ${{ inputs.suffix }}"
fi
if [ "${{ inputs.dry_run }}" == "true" ]; then
ARGS="$ARGS --dry-run"
+29 -9
View File
@@ -20,10 +20,19 @@ jobs:
# Only run when a release PR is merged (not just closed)
if: >
github.event.pull_request.merged == true &&
startsWith(github.event.pull_request.head.ref, 'release/publish/v')
startsWith(github.event.pull_request.head.ref, 'release/publish/')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Extract scope from branch
id: meta
run: |
BRANCH="${{ github.event.pull_request.head.ref }}"
# Branch format: release/publish/<scope>/v<version>
SCOPE=$(echo "$BRANCH" | sed 's|release/publish/\([^/]*\)/v.*|\1|')
echo "scope=$SCOPE" >> $GITHUB_OUTPUT
echo "Detected scope: $SCOPE"
- name: Checkout Repo
uses: actions/checkout@v4
with:
@@ -54,7 +63,7 @@ jobs:
- name: Publish to npm
id: publish
run: pnpm tsx scripts/release/publish-release.ts
run: pnpm tsx scripts/release/publish-release.ts --scope ${{ steps.meta.outputs.scope }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -67,22 +76,33 @@ jobs:
- name: Create and push git tag
run: |
TAG="v${{ steps.publish.outputs.version }}"
git tag -a "$TAG" -m "Release ${{ steps.publish.outputs.version }}"
SCOPE="${{ steps.meta.outputs.scope }}"
VERSION="${{ steps.publish.outputs.version }}"
if [ "$SCOPE" == "monorepo" ]; then
TAG="v${VERSION}"
else
TAG="${SCOPE}/v${VERSION}"
fi
git tag -a "$TAG" -m "Release ${SCOPE} ${VERSION}"
git push origin "$TAG"
echo "tag=$TAG" >> $GITHUB_OUTPUT
id: tag
- name: Create GitHub Release
uses: actions/github-script@v7
env:
RELEASE_TAG: v${{ steps.publish.outputs.version }}
RELEASE_NAME: v${{ steps.publish.outputs.version }}
RELEASE_TAG: ${{ steps.tag.outputs.tag }}
RELEASE_SCOPE: ${{ steps.meta.outputs.scope }}
RELEASE_VERSION: ${{ steps.publish.outputs.version }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fs = require("fs");
const { owner, repo } = context.repo;
const tag = process.env.RELEASE_TAG;
const name = process.env.RELEASE_NAME;
const scope = process.env.RELEASE_SCOPE;
const version = process.env.RELEASE_VERSION;
const name = scope === "monorepo" ? `v${version}` : `${scope}/v${version}`;
let body = "";
try {
@@ -112,6 +132,6 @@ jobs:
run: |
echo "## Release Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Scope:** ${{ steps.meta.outputs.scope }}" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** v${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**npm:** https://www.npmjs.com/package/@copilotkit/react-core/v/${{ steps.publish.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "**Tag:** ${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
+22 -14
View File
@@ -3,6 +3,14 @@ name: release / create-pr
on:
workflow_dispatch:
inputs:
scope:
description: "What to release"
required: true
type: choice
options:
- monorepo
- cli
- angular
bump:
description: "Version bump level"
required: true
@@ -44,10 +52,10 @@ jobs:
owner,
repo,
state: "open",
head_prefix: `${owner}:release/publish/v`,
head_prefix: `${owner}:release/publish/`,
});
const releasePRs = prs.filter(pr => pr.head.ref.startsWith("release/publish/v"));
const releasePRs = prs.filter(pr => pr.head.ref.startsWith("release/publish/"));
if (releasePRs.length > 0) {
const existing = releasePRs.map(pr => ` - #${pr.number}: ${pr.title} (${pr.html_url})`).join("\n");
core.setFailed(
@@ -78,9 +86,9 @@ jobs:
id: prepare
run: |
if [ "${{ inputs.dry_run }}" == "true" ]; then
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }} --dry-run
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }} --scope ${{ inputs.scope }} --dry-run
else
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }}
pnpm tsx scripts/release/prepare-release.ts --bump ${{ inputs.bump }} --scope ${{ inputs.scope }}
fi
- name: Generate AI release notes
@@ -98,21 +106,21 @@ jobs:
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: release/publish/v${{ steps.prepare.outputs.version }}
branch: release/publish/${{ inputs.scope }}/v${{ steps.prepare.outputs.version }}
delete-branch: true
commit-message: "chore: release v${{ steps.prepare.outputs.version }}"
title: "chore: release v${{ steps.prepare.outputs.version }}"
commit-message: "chore: release ${{ inputs.scope }} v${{ steps.prepare.outputs.version }}"
title: "chore: release ${{ inputs.scope }} v${{ steps.prepare.outputs.version }}"
body: |
## Release v${{ steps.prepare.outputs.version }}
## Release ${{ inputs.scope }} v${{ steps.prepare.outputs.version }}
**Bump:** `${{ inputs.bump }}`
**Scope:** `${{ inputs.scope }}` | **Bump:** `${{ inputs.bump }}`
---
### How this release process works
1. **This PR was created automatically** by the "Create Release PR" workflow.
It bumped all `@copilotkit/*` package versions to `${{ steps.prepare.outputs.version }}`
1. **This PR was created automatically** by the "release / create-pr" workflow.
It bumped the `${{ inputs.scope }}` packages to `${{ steps.prepare.outputs.version }}`
and generated AI-enhanced release notes.
2. **CI runs on this PR** — the full test suite (unit tests, lint, type checks, build)
@@ -121,10 +129,10 @@ jobs:
3. **Review the release notes** in `release-notes.md` in this PR.
If a Notion draft was created, you can edit the release notes there before merging.
4. **When this PR is merged**, the `publish-release` workflow automatically:
4. **When this PR is merged**, the `release / publish` workflow automatically:
- Builds all packages
- Publishes all `@copilotkit/*` packages to npm at version `${{ steps.prepare.outputs.version }}`
- Creates git tag `v${{ steps.prepare.outputs.version }}`
- Publishes the `${{ inputs.scope }}` packages to npm at version `${{ steps.prepare.outputs.version }}`
- Creates git tag `${{ inputs.scope }}/v${{ steps.prepare.outputs.version }}`
- Creates a GitHub Release with the final release notes
### Before merging
+2 -2
View File
@@ -21,8 +21,8 @@
"docs": "pnpm -C examples/v2/docs dev",
"demo:angular": "nx run-many -t dev --projects=examples/v2/angular/demo,examples/v2/angular/demo-server",
"demo:react": "pnpm -C examples/v2/react/demo dev",
"release:prepare:dry": "tsx scripts/release/prepare-release.ts --dry-run",
"release:prerelease:dry": "tsx scripts/release/prerelease.ts --dry-run",
"release:prepare:dry": "tsx scripts/release/prepare-release.ts --scope monorepo --bump patch --dry-run",
"release:prerelease:dry": "tsx scripts/release/prerelease.ts --scope monorepo --dry-run",
"prepare": "lefthook install",
"graph": "nx graph",
"publint": "nx run-many -t publint --projects=packages/**",
+30 -15
View File
@@ -1,18 +1,33 @@
{
"prereleaseTag": "canary",
"versionedTogether": [
"@copilotkit/a2ui-renderer",
"@copilotkit/core",
"@copilotkit/react-core",
"@copilotkit/react-textarea",
"@copilotkit/react-ui",
"@copilotkit/runtime",
"@copilotkit/runtime-client-gql",
"@copilotkit/sdk-js",
"@copilotkit/shared",
"@copilotkit/sqlite-runner",
"@copilotkit/voice",
"@copilotkit/web-inspector"
],
"versionedIndependently": ["@copilotkitnext/angular", "copilotkit"]
"scopes": {
"monorepo": {
"packages": [
"@copilotkit/a2ui-renderer",
"@copilotkit/core",
"@copilotkit/react-core",
"@copilotkit/react-textarea",
"@copilotkit/react-ui",
"@copilotkit/runtime",
"@copilotkit/runtime-client-gql",
"@copilotkit/sdk-js",
"@copilotkit/shared",
"@copilotkit/sqlite-runner",
"@copilotkit/voice",
"@copilotkit/web-inspector"
],
"versionSource": "@copilotkit/react-core",
"sharedVersion": true
},
"cli": {
"packages": ["copilotkit"],
"versionSource": "copilotkit",
"sharedVersion": false
},
"angular": {
"packages": ["@copilotkitnext/angular"],
"versionSource": "@copilotkitnext/angular",
"sharedVersion": false
}
}
}
+20 -2
View File
@@ -7,13 +7,31 @@ export const ROOT = path.resolve(
"../../..",
);
export type ReleaseScope = "monorepo" | "cli" | "angular";
export interface ScopeConfig {
packages: string[];
versionSource: string;
sharedVersion: boolean;
}
export interface ReleaseConfig {
prereleaseTag: string;
versionedTogether: string[];
versionedIndependently: string[];
scopes: Record<ReleaseScope, ScopeConfig>;
}
export function loadConfig(): ReleaseConfig {
const configPath = path.join(ROOT, "release.config.json");
return JSON.parse(fs.readFileSync(configPath, "utf8"));
}
export function getScopeConfig(scope: ReleaseScope): ScopeConfig {
const config = loadConfig();
const scopeConfig = config.scopes[scope];
if (!scopeConfig) {
throw new Error(
`Unknown scope: ${scope}. Valid scopes: ${Object.keys(config.scopes).join(", ")}`,
);
}
return scopeConfig;
}
+30 -3
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi } from "vitest";
import {
parseSemver,
computeNextStableVersion,
@@ -10,9 +10,36 @@ vi.mock("./config.js", () => ({
ROOT: "/mock",
loadConfig: () => ({
prereleaseTag: "canary",
versionedTogether: [],
versionedIndependently: [],
scopes: {
monorepo: {
packages: [],
versionSource: "@copilotkit/react-core",
sharedVersion: true,
},
cli: { packages: [], versionSource: "copilotkit", sharedVersion: false },
angular: {
packages: [],
versionSource: "@copilotkitnext/angular",
sharedVersion: false,
},
},
}),
getScopeConfig: (scope: string) => {
const scopes: Record<string, any> = {
monorepo: {
packages: [],
versionSource: "@copilotkit/react-core",
sharedVersion: true,
},
cli: { packages: [], versionSource: "copilotkit", sharedVersion: false },
angular: {
packages: [],
versionSource: "@copilotkitnext/angular",
sharedVersion: false,
},
};
return scopes[scope];
},
}));
describe("parseSemver", () => {
+48 -32
View File
@@ -1,6 +1,11 @@
import fs from "fs";
import path from "path";
import { loadConfig, ROOT } from "./config.js";
import {
loadConfig,
getScopeConfig,
ROOT,
type ReleaseScope,
} from "./config.js";
export type BumpLevel = "patch" | "minor" | "major";
@@ -16,13 +21,27 @@ export interface PublishablePackage {
dir: string;
pkgJsonPath: string;
pkg: Record<string, any>;
isVersionedTogether: boolean;
isVersionedIndependently: boolean;
}
export function getCurrentVersion(): string {
const pkgPath = path.join(ROOT, "packages/react-core/package.json");
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
/** Find a package directory by its npm name. */
function findPackageDir(packageName: string): string {
const packagesDir = path.join(ROOT, "packages");
for (const dir of fs.readdirSync(packagesDir)) {
const pkgJsonPath = path.join(packagesDir, dir, "package.json");
if (!fs.existsSync(pkgJsonPath)) continue;
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
if (pkg.name === packageName) return path.join(packagesDir, dir);
}
throw new Error(`Package not found: ${packageName}`);
}
/** Get the current version for a scope (reads from the scope's versionSource package). */
export function getCurrentVersion(scope: ReleaseScope): string {
const scopeConfig = getScopeConfig(scope);
const dir = findPackageDir(scopeConfig.versionSource);
const pkg = JSON.parse(
fs.readFileSync(path.join(dir, "package.json"), "utf8"),
);
return pkg.version;
}
@@ -72,12 +91,10 @@ export function computePrereleaseVersion(
return `${v.major}.${v.minor}.${v.patch}-${tag}.${id}`;
}
export function getPublishablePackages(): PublishablePackage[] {
const config = loadConfig();
const allPackageNames = new Set([
...config.versionedTogether,
...config.versionedIndependently,
]);
/** Get all publishable packages for a given scope. */
export function getPackagesForScope(scope: ReleaseScope): PublishablePackage[] {
const scopeConfig = getScopeConfig(scope);
const packageNames = new Set(scopeConfig.packages);
const packagesDir = path.join(ROOT, "packages");
const results: PublishablePackage[] = [];
@@ -86,16 +103,12 @@ export function getPublishablePackages(): PublishablePackage[] {
if (!fs.existsSync(pkgJsonPath)) continue;
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
if (allPackageNames.has(pkg.name)) {
if (packageNames.has(pkg.name)) {
results.push({
name: pkg.name,
dir: path.join(packagesDir, dir),
pkgJsonPath,
pkg,
isVersionedTogether: config.versionedTogether.includes(pkg.name),
isVersionedIndependently: config.versionedIndependently.includes(
pkg.name,
),
});
}
}
@@ -103,14 +116,14 @@ export function getPublishablePackages(): PublishablePackage[] {
return results;
}
export function bumpVersionedTogetherPackages(
/** Bump all packages in a scope to a new version. For sharedVersion scopes, also updates internal deps. */
export function bumpPackages(
scope: ReleaseScope,
newVersion: string,
): { name: string; oldVersion: string; newVersion: string }[] {
const packages = getPublishablePackages().filter(
(p) => p.isVersionedTogether,
);
const config = loadConfig();
const togetherNames = new Set(config.versionedTogether);
const scopeConfig = getScopeConfig(scope);
const packages = getPackagesForScope(scope);
const scopeNames = new Set(scopeConfig.packages);
const updated: { name: string; oldVersion: string; newVersion: string }[] =
[];
@@ -119,15 +132,18 @@ export function bumpVersionedTogetherPackages(
const oldVersion = pkg.version;
pkg.version = newVersion;
for (const depField of [
"dependencies",
"peerDependencies",
"devDependencies",
] as const) {
if (!pkg[depField]) continue;
for (const depName of Object.keys(pkg[depField])) {
if (togetherNames.has(depName)) {
pkg[depField][depName] = newVersion;
// For shared-version scopes, update internal dependency references too
if (scopeConfig.sharedVersion) {
for (const depField of [
"dependencies",
"peerDependencies",
"devDependencies",
] as const) {
if (!pkg[depField]) continue;
for (const depName of Object.keys(pkg[depField])) {
if (scopeNames.has(depName)) {
pkg[depField][depName] = newVersion;
}
}
}
}
+30 -14
View File
@@ -2,7 +2,7 @@
* Prepare a release: bump versions, generate raw release notes.
* Runs inside the "create release PR" workflow.
*
* Usage: tsx scripts/release/prepare-release.ts --bump <patch|minor|major> [--dry-run]
* Usage: tsx scripts/release/prepare-release.ts --bump <patch|minor|major> --scope <monorepo|cli|angular> [--dry-run]
*/
import fs from "fs";
@@ -10,8 +10,8 @@ import path from "path";
import {
getCurrentVersion,
computeNextStableVersion,
bumpVersionedTogetherPackages,
getPublishablePackages,
bumpPackages,
getPackagesForScope,
type BumpLevel,
} from "./lib/versions.js";
import {
@@ -19,14 +19,16 @@ import {
type ChangesSummary,
type Commit,
} from "./lib/changes.js";
import { ROOT } from "./lib/config.js";
import { ROOT, type ReleaseScope } from "./lib/config.js";
function generateRawReleaseNotes(
version: string,
scope: ReleaseScope,
summary: ChangesSummary,
): string {
const lines: string[] = [];
lines.push(`## v${version}`, "");
const label = scope === "monorepo" ? "" : ` (${scope})`;
lines.push(`## v${version}${label}`, "");
if (summary.commits.length === 0) {
lines.push("No changes since last release.");
@@ -65,6 +67,8 @@ function generateRawReleaseNotes(
return lines.join("\n");
}
const VALID_SCOPES = ["monorepo", "cli", "angular"];
function main() {
const argv = process.argv.slice(2);
const dryRun = argv.includes("--dry-run");
@@ -72,14 +76,28 @@ function main() {
const bumpLevel = (
bumpIdx !== -1 ? argv[bumpIdx + 1] : null
) as BumpLevel | null;
const scopeIdx = argv.indexOf("--scope");
const scope = (
scopeIdx !== -1 ? argv[scopeIdx + 1] : null
) as ReleaseScope | null;
if (!bumpLevel || !["patch", "minor", "major"].includes(bumpLevel)) {
console.error("Usage: prepare-release.ts --bump <patch|minor|major>");
console.error(
"Usage: prepare-release.ts --bump <patch|minor|major> --scope <monorepo|cli|angular>",
);
process.exit(1);
}
const currentVersion = getCurrentVersion();
if (!scope || !VALID_SCOPES.includes(scope)) {
console.error(
`Invalid scope: ${scope}. Valid scopes: ${VALID_SCOPES.join(", ")}`,
);
process.exit(1);
}
const currentVersion = getCurrentVersion(scope);
const nextVersion = computeNextStableVersion(currentVersion, bumpLevel);
console.log(`Scope: ${scope}`);
console.log(`Current version: ${currentVersion}`);
console.log(`Bump level: ${bumpLevel}`);
console.log(`Next version: ${nextVersion}`);
@@ -91,19 +109,17 @@ function main() {
if (dryRun) {
console.log("\n[DRY RUN] Would bump these packages:");
for (const p of getPublishablePackages().filter(
(p) => p.isVersionedTogether,
)) {
for (const p of getPackagesForScope(scope)) {
console.log(` ${p.name}: ${p.pkg.version} -> ${nextVersion}`);
}
console.log("\n[DRY RUN] Exiting.");
return;
}
const updated = bumpVersionedTogetherPackages(nextVersion);
const updated = bumpPackages(scope, nextVersion);
console.log(`\nBumped ${updated.length} packages to ${nextVersion}`);
const rawNotes = generateRawReleaseNotes(nextVersion, summary);
const rawNotes = generateRawReleaseNotes(nextVersion, scope, summary);
const releaseNotesPath = path.join(ROOT, "release-notes.md");
fs.writeFileSync(releaseNotesPath, rawNotes);
console.log("Raw release notes written to release-notes.md");
@@ -111,10 +127,10 @@ function main() {
const outputPath = process.env.GITHUB_OUTPUT;
if (outputPath) {
fs.appendFileSync(outputPath, `version=${nextVersion}\n`);
fs.appendFileSync(outputPath, `tag=v${nextVersion}\n`);
fs.appendFileSync(outputPath, `scope=${scope}\n`);
}
console.log(`\nRelease prepared: v${nextVersion}`);
console.log(`\nRelease prepared: v${nextVersion} (${scope})`);
}
main();
+24 -17
View File
@@ -1,23 +1,23 @@
/**
* Publish a prerelease (no git commits, no PRs).
*
* Takes the current version on main and appends a canary suffix.
* Takes the current version for the given scope and appends a canary suffix.
* If --suffix is provided (e.g. "fix-user-issue"), the version becomes
* 1.55.2-canary.fix-user-issue. Otherwise it uses a unix timestamp.
*
* Always publishes with the "canary" dist-tag.
*
* Usage: tsx scripts/release/prerelease.ts [--suffix <label>] [--dry-run]
* Usage: tsx scripts/release/prerelease.ts --scope <monorepo|cli|angular> [--suffix <label>] [--dry-run]
*/
import { spawnSync } from "child_process";
import {
getCurrentVersion,
computePrereleaseVersion,
bumpVersionedTogetherPackages,
getPublishablePackages,
bumpPackages,
getPackagesForScope,
} from "./lib/versions.js";
import { ROOT, loadConfig } from "./lib/config.js";
import { ROOT, loadConfig, type ReleaseScope } from "./lib/config.js";
function run(cmd: string, args: string[], opts?: { cwd?: string }) {
const result = spawnSync(cmd, args, {
@@ -31,25 +31,37 @@ function run(cmd: string, args: string[], opts?: { cwd?: string }) {
return result;
}
const VALID_SCOPES = ["monorepo", "cli", "angular"];
function main() {
const argv = process.argv.slice(2);
const dryRun = argv.includes("--dry-run");
const suffixIdx = argv.indexOf("--suffix");
const suffix = suffixIdx !== -1 ? argv[suffixIdx + 1] : undefined;
const scopeIdx = argv.indexOf("--scope");
const scope = (
scopeIdx !== -1 ? argv[scopeIdx + 1] : null
) as ReleaseScope | null;
if (!scope || !VALID_SCOPES.includes(scope)) {
console.error(
`Usage: prerelease.ts --scope <${VALID_SCOPES.join("|")}> [--suffix <label>] [--dry-run]`,
);
process.exit(1);
}
const config = loadConfig();
const distTag = config.prereleaseTag;
const currentVersion = getCurrentVersion();
const currentVersion = getCurrentVersion(scope);
const prereleaseVersion = computePrereleaseVersion(currentVersion, suffix);
console.log(`Scope: ${scope}`);
console.log(`Current version: ${currentVersion}`);
console.log(`Prerelease version: ${prereleaseVersion}`);
console.log(`Dist tag: ${distTag}`);
if (dryRun) {
console.log("\n[DRY RUN] Would bump these packages:");
for (const p of getPublishablePackages().filter(
(p) => p.isVersionedTogether,
)) {
for (const p of getPackagesForScope(scope)) {
console.log(` ${p.name}: ${p.pkg.version} -> ${prereleaseVersion}`);
}
console.log("\n[DRY RUN] Exiting.");
@@ -57,7 +69,7 @@ function main() {
}
// Bump versions in working directory (no commit)
const updated = bumpVersionedTogetherPackages(prereleaseVersion);
const updated = bumpPackages(scope, prereleaseVersion);
console.log(`\nBumped ${updated.length} packages to ${prereleaseVersion}`);
// Build all packages
@@ -66,19 +78,14 @@ function main() {
// Publish each package
console.log("\nPublishing packages...");
const packages = getPublishablePackages().filter(
(p) => p.isVersionedTogether,
);
for (const p of packages) {
for (const p of getPackagesForScope(scope)) {
console.log(
` Publishing ${p.name}@${prereleaseVersion} with tag ${distTag}...`,
);
run(
"pnpm",
["publish", "--no-git-checks", "--tag", distTag, "--access", "public"],
{
cwd: p.dir,
},
{ cwd: p.dir },
);
}
+29 -17
View File
@@ -1,7 +1,7 @@
/**
* Publish a stable release (runs after merge of a release PR).
*
* 1. Reads the current version from package.json (already bumped by the release PR)
* 1. Reads the scope and current version from package.json (already bumped by the release PR)
* 2. Optionally reads the Notion draft for the final release notes
* 3. Builds all packages
* 4. Publishes to npm with "latest" tag
@@ -12,7 +12,7 @@
* NOTION_API_KEY for reading edited release notes from Notion (optional)
* GITHUB_OUTPUT CI output file
*
* Usage: tsx scripts/release/publish-release.ts
* Usage: tsx scripts/release/publish-release.ts --scope <monorepo|cli|angular>
*/
import fs from "fs";
@@ -20,11 +20,11 @@ import path from "path";
import { spawnSync } from "child_process";
import {
getCurrentVersion,
getPublishablePackages,
getPackagesForScope,
parseSemver,
} from "./lib/versions.js";
import { readReleaseDraft } from "./lib/notion.js";
import { ROOT } from "./lib/config.js";
import { ROOT, getScopeConfig, type ReleaseScope } from "./lib/config.js";
function run(cmd: string, args: string[], opts?: { cwd?: string }) {
const result = spawnSync(cmd, args, {
@@ -55,8 +55,25 @@ function isGreaterVersion(next: string, current: string): boolean {
return a.patch > b.patch;
}
const VALID_SCOPES = ["monorepo", "cli", "angular"];
async function main() {
const version = getCurrentVersion();
const argv = process.argv.slice(2);
const scopeIdx = argv.indexOf("--scope");
const scope = (
scopeIdx !== -1 ? argv[scopeIdx + 1] : null
) as ReleaseScope | null;
if (!scope || !VALID_SCOPES.includes(scope)) {
console.error(
`Usage: publish-release.ts --scope <${VALID_SCOPES.join("|")}>`,
);
process.exit(1);
}
const version = getCurrentVersion(scope);
const scopeConfig = getScopeConfig(scope);
console.log(`Scope: ${scope}`);
console.log(`Publishing version: ${version}`);
// Safety check: only allow clean semver (no prerelease suffixes like -canary.123)
@@ -70,13 +87,12 @@ async function main() {
}
// Safety check: refuse to publish if version isn't greater than what's on npm
const published = getPublishedVersion("@copilotkit/react-core");
const published = getPublishedVersion(scopeConfig.versionSource);
if (published) {
console.log(`Currently published version: ${published}`);
if (!isGreaterVersion(version, published)) {
console.error(
`Refusing to publish: ${version} is not greater than the currently published ${published}. ` +
`This can happen if a release/v* branch was created manually without using the release workflow.`,
`Refusing to publish: ${version} is not greater than the currently published ${published}.`,
);
process.exit(1);
}
@@ -107,19 +123,14 @@ async function main() {
console.log("\nBuilding packages...");
run("pnpm", ["run", "build"]);
// Publish each package
// Publish each package in scope
console.log("\nPublishing packages...");
const packages = getPublishablePackages().filter(
(p) => p.isVersionedTogether,
);
for (const p of packages) {
for (const p of getPackagesForScope(scope)) {
console.log(` Publishing ${p.name}@${version}...`);
run(
"pnpm",
["publish", "--no-git-checks", "--tag", "latest", "--access", "public"],
{
cwd: p.dir,
},
{ cwd: p.dir },
);
}
@@ -127,9 +138,10 @@ async function main() {
const outputPath = process.env.GITHUB_OUTPUT;
if (outputPath) {
fs.appendFileSync(outputPath, `version=${version}\n`);
fs.appendFileSync(outputPath, `scope=${scope}\n`);
}
console.log(`\nRelease published: ${version}`);
console.log(`\nRelease published: ${version} (${scope})`);
}
main().catch((err) => {