mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
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:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user