mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat(release): add --knowledge flag and publish-knowledge.yml workflow
- packages.mjs: export KSCLI_PACKAGE and ALL_PACKAGES for knowledge-studio-cli - check.mjs: support knowledge option to build/validate kscli - validate.mjs: accept packages param, validate all packages in lockstep - pack-scan.mjs: accept packages param - publish-stable.mjs: refactor to iterate PACKAGES array; add --knowledge flag - publish-channel.mjs: refactor to iterate PACKAGES array; add --knowledge flag - New workflow publish-knowledge.yml: triggers publish with --knowledge flag The original publish.yml (without --knowledge) publishes only core + cli. The new publish-knowledge.yml publishes core + cli + knowledge-studio-cli.
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
name: Publish Knowledge
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
mode:
|
||||
description: "Publish mode"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- channel
|
||||
- stable
|
||||
channel:
|
||||
description: "dist-tag (channel mode only, e.g. mcp/plugin/advisor)"
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-knowledge-${{ inputs.mode }}-${{ inputs.channel }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish-stable:
|
||||
if: inputs.mode == 'stable'
|
||||
name: publish stable (with knowledge) to npm + tag
|
||||
runs-on: ubuntu-latest
|
||||
environment: production # Required Reviewers gate
|
||||
permissions:
|
||||
contents: write # push lightweight tag to origin
|
||||
id-token: write # OIDC for npm Trusted Publishing + provenance
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install gitleaks
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GITLEAKS_VERSION=8.21.2
|
||||
curl -sSfL \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
| sudo tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: publish-stable (with knowledge)
|
||||
run: node tools/release/publish-stable.mjs --knowledge
|
||||
|
||||
publish-channel:
|
||||
if: inputs.mode == 'channel'
|
||||
name: publish beta (with knowledge) to npm
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # no tag, no Release; just publish
|
||||
id-token: write # OIDC for npm Trusted Publishing + provenance
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: pnpm
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
|
||||
- name: Install gitleaks
|
||||
run: |
|
||||
set -euo pipefail
|
||||
GITLEAKS_VERSION=8.21.2
|
||||
curl -sSfL \
|
||||
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
|
||||
| sudo tar -xz -C /usr/local/bin gitleaks
|
||||
gitleaks version
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: publish-channel (with knowledge)
|
||||
run: node tools/release/publish-channel.mjs --knowledge --channel "${{ inputs.channel }}"
|
||||
+13
-3
@@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
|
||||
import { packAndScan } from "./lib/pack-scan.mjs";
|
||||
import { run } from "./lib/proc.mjs";
|
||||
import { assertReadmeSync, loadAndValidatePackages } from "./lib/validate.mjs";
|
||||
import { ALL_PACKAGES, PACKAGES } from "./lib/packages.mjs";
|
||||
|
||||
function log(msg = "") {
|
||||
process.stdout.write(`${msg}\n`);
|
||||
@@ -17,20 +18,24 @@ function step(msg) {
|
||||
* Pure-validation pipeline. Reusable from publish-stable / publish-channel.
|
||||
* Returns { coreJson, cliJson } for callers that need the parsed package.jsons.
|
||||
*
|
||||
* @param {{ channel?: boolean }} [options]
|
||||
* @param {{ channel?: boolean, knowledge?: boolean }} [options]
|
||||
* @param {boolean} [options.channel] — When true (publish-channel): regenerate
|
||||
* `reference/` and assert it matches git, but do not sync `SKILL.md` from the
|
||||
* temporary beta `package.json` version (repo skill stays aligned with stable).
|
||||
* @param {boolean} [options.knowledge] — When true: also build and validate
|
||||
* knowledge-studio-cli alongside the base packages.
|
||||
*/
|
||||
export async function runCheck(options = {}) {
|
||||
const channel = options.channel === true;
|
||||
const knowledge = options.knowledge === true;
|
||||
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
|
||||
|
||||
step("pnpm install --frozen-lockfile");
|
||||
run("pnpm", ["install", "--frozen-lockfile"]);
|
||||
|
||||
step("metadata: README sync, version consistency, workspace:* dep");
|
||||
assertReadmeSync();
|
||||
const { coreJson, cliJson } = loadAndValidatePackages();
|
||||
const { coreJson, cliJson } = loadAndValidatePackages({ packages });
|
||||
log(`bailian-cli-core@${coreJson.version}`);
|
||||
log(`bailian-cli@${cliJson.version}`);
|
||||
|
||||
@@ -63,8 +68,13 @@ export async function runCheck(options = {}) {
|
||||
step("build bailian-cli");
|
||||
run("pnpm", ["--filter", "bailian-cli", "run", "build"]);
|
||||
|
||||
if (knowledge) {
|
||||
step("build knowledge-studio-cli");
|
||||
run("pnpm", ["--filter", "knowledge-studio-cli", "run", "build"]);
|
||||
}
|
||||
|
||||
step("pack + scan (publint, gitleaks)");
|
||||
packAndScan({ log });
|
||||
packAndScan({ log, packages });
|
||||
|
||||
log("\nrelease check passed.");
|
||||
return { coreJson, cliJson };
|
||||
|
||||
@@ -13,10 +13,11 @@ function extractTarball(tarball, tempDir, key) {
|
||||
return extractDir;
|
||||
}
|
||||
|
||||
export function packAndScan({ log }) {
|
||||
export function packAndScan({ log, packages }) {
|
||||
const pkgs = packages ?? PACKAGES;
|
||||
const tempDir = mkdtempSync(join(tmpdir(), "bailian-release-"));
|
||||
try {
|
||||
for (const pkg of PACKAGES) {
|
||||
for (const pkg of pkgs) {
|
||||
const json = readPackageJson(pkg);
|
||||
log(`packing ${pkg.name}@${json.version}`);
|
||||
const tarball = pnpmPack(pkg, tempDir, json);
|
||||
|
||||
@@ -9,6 +9,11 @@ export const PACKAGES = [
|
||||
{ key: "cli", dir: "packages/cli", name: "bailian-cli" },
|
||||
];
|
||||
|
||||
// knowledge-studio-cli shares the same library deps as bailian-cli.
|
||||
// Published via a separate workflow (publish-knowledge.yml) with --knowledge flag.
|
||||
export const KSCLI_PACKAGE = { key: "kscli", dir: "packages/kscli", name: "knowledge-studio-cli" };
|
||||
export const ALL_PACKAGES = [...PACKAGES, KSCLI_PACKAGE];
|
||||
|
||||
export function readJson(path) {
|
||||
return JSON.parse(readFileSync(path, "utf-8"));
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ export function assertReadmeSync() {
|
||||
}
|
||||
}
|
||||
|
||||
export function loadAndValidatePackages() {
|
||||
export function loadAndValidatePackages({ packages } = {}) {
|
||||
const pkgs = packages ?? PACKAGES;
|
||||
const internalNames = new Set(pkgs.map((p) => p.name));
|
||||
const jsonByKey = new Map();
|
||||
for (const pkg of PACKAGES) {
|
||||
for (const pkg of pkgs) {
|
||||
const json = readPackageJson(pkg);
|
||||
if (json.name !== pkg.name) {
|
||||
throw new Error(`${pkg.dir} name must be ${pkg.name}, got ${json.name}`);
|
||||
@@ -30,18 +32,21 @@ export function loadAndValidatePackages() {
|
||||
|
||||
const coreJson = jsonByKey.get("core");
|
||||
const cliJson = jsonByKey.get("cli");
|
||||
const version = coreJson.version;
|
||||
|
||||
if (cliJson.version !== coreJson.version) {
|
||||
throw new Error(
|
||||
`core and cli versions must match, got ${coreJson.version} and ${cliJson.version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const cliCoreDep = cliJson.dependencies?.["bailian-cli-core"];
|
||||
if (cliCoreDep !== "workspace:*") {
|
||||
throw new Error(
|
||||
`packages/cli source dependency on bailian-cli-core must be "workspace:*", got ${cliCoreDep}.`,
|
||||
);
|
||||
for (const pkg of pkgs) {
|
||||
const json = jsonByKey.get(pkg.key);
|
||||
if (json.version !== version) {
|
||||
throw new Error(
|
||||
`all package versions must match ${version} (bailian-cli-core), ` +
|
||||
`but ${pkg.name} is ${json.version}.`,
|
||||
);
|
||||
}
|
||||
for (const [dep, range] of Object.entries(json.dependencies ?? {})) {
|
||||
if (internalNames.has(dep) && range !== "workspace:*") {
|
||||
throw new Error(`${pkg.name} dependency on ${dep} must be "workspace:*", got ${range}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { coreJson, cliJson };
|
||||
|
||||
@@ -6,7 +6,8 @@ import { runCheck } from "./check.mjs";
|
||||
import { headSha7, utcDateStamp } from "./lib/git.mjs";
|
||||
import { npmViewExists, pnpmPublish } from "./lib/npm.mjs";
|
||||
import {
|
||||
findPackage,
|
||||
ALL_PACKAGES,
|
||||
PACKAGES,
|
||||
packageJsonPath,
|
||||
readPackageJson,
|
||||
writePackageJson,
|
||||
@@ -25,11 +26,14 @@ const { values } = parseArgs({
|
||||
options: {
|
||||
channel: { type: "string" },
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
knowledge: { type: "boolean", default: false },
|
||||
},
|
||||
allowPositionals: false,
|
||||
});
|
||||
const channel = values.channel;
|
||||
const dryRun = values["dry-run"];
|
||||
const knowledge = values.knowledge;
|
||||
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
|
||||
assertChannel(channel);
|
||||
|
||||
if (!dryRun && !process.env.CI) {
|
||||
@@ -37,16 +41,15 @@ if (!dryRun && !process.env.CI) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const core = findPackage("core");
|
||||
const cli = findPackage("cli");
|
||||
const corePath = packageJsonPath(core);
|
||||
const cliPath = packageJsonPath(cli);
|
||||
const coreOriginal = readFileSync(corePath, "utf-8");
|
||||
const cliOriginal = readFileSync(cliPath, "utf-8");
|
||||
// Snapshot every package.json so the temporary version bump is reverted in
|
||||
// `finally`, even when the release fails midway.
|
||||
const originals = packages.map((pkg) => {
|
||||
const path = packageJsonPath(pkg);
|
||||
return { pkg, path, content: readFileSync(path, "utf-8") };
|
||||
});
|
||||
|
||||
function restoreOriginals() {
|
||||
writeFileSync(corePath, coreOriginal);
|
||||
writeFileSync(cliPath, cliOriginal);
|
||||
for (const { path, content } of originals) writeFileSync(path, content);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -57,32 +60,29 @@ try {
|
||||
log(`channel=${channel} version=${betaVersion}`);
|
||||
|
||||
step("temporarily bump package.json (not committed)");
|
||||
const coreJson = readPackageJson(core);
|
||||
const cliJson = readPackageJson(cli);
|
||||
coreJson.version = betaVersion;
|
||||
cliJson.version = betaVersion;
|
||||
writePackageJson(core, coreJson);
|
||||
writePackageJson(cli, cliJson);
|
||||
// pnpm pack resolves `workspace:*` to the in-tree version, so CLI tarball
|
||||
// will depend on bailian-cli-core@<betaVersion> after this bump.
|
||||
for (const pkg of packages) {
|
||||
const json = readPackageJson(pkg);
|
||||
json.version = betaVersion;
|
||||
writePackageJson(pkg, json);
|
||||
}
|
||||
|
||||
await runCheck({ channel: true });
|
||||
await runCheck({ channel: true, knowledge });
|
||||
|
||||
step(`idempotency: check ${betaVersion} against registry`);
|
||||
const corePublished = npmViewExists(core.name, betaVersion);
|
||||
const cliPublished = npmViewExists(cli.name, betaVersion);
|
||||
log(`${core.name}@${betaVersion}: ${corePublished ? "already published" : "to publish"}`);
|
||||
log(`${cli.name}@${betaVersion}: ${cliPublished ? "already published" : "to publish"}`);
|
||||
if (corePublished && cliPublished) {
|
||||
log("\nboth packages already published; nothing to do.");
|
||||
const published = new Map();
|
||||
for (const pkg of packages) {
|
||||
const exists = npmViewExists(pkg.name, betaVersion);
|
||||
published.set(pkg.key, exists);
|
||||
log(`${pkg.name}@${betaVersion}: ${exists ? "already published" : "to publish"}`);
|
||||
}
|
||||
if (packages.every((pkg) => published.get(pkg.key))) {
|
||||
log("\nall packages already published; nothing to do.");
|
||||
} else {
|
||||
if (!corePublished) {
|
||||
step(`publish ${core.name}@${betaVersion} (tag=${channel}, provenance)`);
|
||||
pnpmPublish(core, { tag: channel, provenance: true, dryRun });
|
||||
}
|
||||
if (!cliPublished) {
|
||||
step(`publish ${cli.name}@${betaVersion} (tag=${channel}, provenance)`);
|
||||
pnpmPublish(cli, { tag: channel, provenance: true, dryRun });
|
||||
// Publish in dependency order.
|
||||
for (const pkg of packages) {
|
||||
if (published.get(pkg.key)) continue;
|
||||
step(`publish ${pkg.name}@${betaVersion} (tag=${channel}, provenance)`);
|
||||
pnpmPublish(pkg, { tag: channel, provenance: true, dryRun });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseArgs } from "util";
|
||||
import { runCheck } from "./check.mjs";
|
||||
import { createTag, currentBranch, isWorkingTreeClean, pushTag, tagExists } from "./lib/git.mjs";
|
||||
import { npmViewExists, pnpmPublish } from "./lib/npm.mjs";
|
||||
import { findPackage } from "./lib/packages.mjs";
|
||||
import { ALL_PACKAGES, findPackage, PACKAGES } from "./lib/packages.mjs";
|
||||
|
||||
function log(msg = "") {
|
||||
process.stdout.write(`${msg}\n`);
|
||||
@@ -17,10 +17,13 @@ function step(msg) {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
"dry-run": { type: "boolean", default: false },
|
||||
knowledge: { type: "boolean", default: false },
|
||||
},
|
||||
allowPositionals: false,
|
||||
});
|
||||
const dryRun = values["dry-run"];
|
||||
const knowledge = values.knowledge;
|
||||
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
|
||||
|
||||
try {
|
||||
if (!dryRun && !process.env.CI) {
|
||||
@@ -40,28 +43,26 @@ try {
|
||||
log("[dry-run] skipping working-tree + branch preflight");
|
||||
}
|
||||
|
||||
const { coreJson } = await runCheck();
|
||||
const version = coreJson.version; // === cliJson.version, asserted by runCheck
|
||||
const { coreJson } = await runCheck({ knowledge });
|
||||
const version = coreJson.version; // all packages share this, asserted by runCheck
|
||||
|
||||
step(`idempotency: check ${version} against registry`);
|
||||
const core = findPackage("core");
|
||||
const cli = findPackage("cli");
|
||||
const corePublished = npmViewExists(core.name, version);
|
||||
const cliPublished = npmViewExists(cli.name, version);
|
||||
log(`${core.name}@${version}: ${corePublished ? "already published" : "to publish"}`);
|
||||
log(`${cli.name}@${version}: ${cliPublished ? "already published" : "to publish"}`);
|
||||
if (corePublished && cliPublished) {
|
||||
log("\nboth packages already published; nothing to do.");
|
||||
const published = new Map();
|
||||
for (const pkg of packages) {
|
||||
const exists = npmViewExists(pkg.name, version);
|
||||
published.set(pkg.key, exists);
|
||||
log(`${pkg.name}@${version}: ${exists ? "already published" : "to publish"}`);
|
||||
}
|
||||
if (packages.every((pkg) => published.get(pkg.key))) {
|
||||
log("\nall packages already published; nothing to do.");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!corePublished) {
|
||||
step(`publish ${core.name}@${version} (tag=latest, provenance)`);
|
||||
pnpmPublish(core, { tag: "latest", provenance: true, dryRun });
|
||||
}
|
||||
if (!cliPublished) {
|
||||
step(`publish ${cli.name}@${version} (tag=latest, provenance)`);
|
||||
pnpmPublish(cli, { tag: "latest", provenance: true, dryRun });
|
||||
// Publish in dependency order.
|
||||
for (const pkg of packages) {
|
||||
if (published.get(pkg.key)) continue;
|
||||
step(`publish ${pkg.name}@${version} (tag=latest, provenance)`);
|
||||
pnpmPublish(pkg, { tag: "latest", provenance: true, dryRun });
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
|
||||
Reference in New Issue
Block a user