feat(release): 支持 knowledge-studio-cli 的构建与发布流程

- 新增发布工作流 publish-knowledge.yml,支持 stable 和 channel 模式发布含 knowledge 的包
- runCheck 函数增加 knowledge 参数,支持同时构建和验证 knowledge-studio-cli 包
- publish-stable 和 publish-channel 脚本支持传入 knowledge 参数,调整发布的包列表
- packAndScan 函数支持指定发布包列表,增强灵活性
- 扩展 packages 模块,新增 ALL_PACKAGES 常量包含所有包(基础包加 knowledge-studio-cli)
- loadAndValidate
This commit is contained in:
zeyu.fz
2026-06-26 16:53:59 +08:00
parent eaa6b07c7d
commit 4745d70587
7 changed files with 138 additions and 27 deletions
+86
View File
@@ -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 }}"
+12 -5
View File
@@ -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}`);
@@ -65,11 +70,13 @@ export async function runCheck(options = {}) {
step("build bailian-cli");
run("pnpm", ["--filter", "bailian-cli", "run", "build"]);
step("build knowledge-studio-cli");
run("pnpm", ["--filter", "knowledge-studio-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 };
+3 -2
View File
@@ -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);
+6 -2
View File
@@ -4,16 +4,20 @@ import { fileURLToPath } from "url";
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
// Dependency order: core ← runtime ← commands ← cli / kscli.
// Dependency order: core ← runtime ← commands ← cli.
// Consumers rely on this ordering for build/publish (dependencies first).
export const PACKAGES = [
{ key: "core", dir: "packages/core", name: "bailian-cli-core" },
{ key: "runtime", dir: "packages/runtime", name: "bailian-cli-runtime" },
{ key: "commands", dir: "packages/commands", name: "bailian-cli-commands" },
{ key: "cli", dir: "packages/cli", name: "bailian-cli" },
{ key: "kscli", dir: "packages/kscli", name: "knowledge-studio-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"));
}
+5 -4
View File
@@ -18,10 +18,11 @@ export function assertReadmeSync() {
}
}
export function loadAndValidatePackages() {
const internalNames = new Set(PACKAGES.map((p) => p.name));
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}`);
@@ -32,7 +33,7 @@ export function loadAndValidatePackages() {
const coreJson = jsonByKey.get("core");
const version = coreJson.version;
for (const pkg of PACKAGES) {
for (const pkg of pkgs) {
const json = jsonByKey.get(pkg.key);
// All packages release in lockstep, so every version must match.
if (json.version !== version) {
+17 -8
View File
@@ -5,7 +5,13 @@ import { parseArgs } from "util";
import { runCheck } from "./check.mjs";
import { headSha7, utcDateStamp } from "./lib/git.mjs";
import { npmViewExists, pnpmPublish } from "./lib/npm.mjs";
import { PACKAGES, packageJsonPath, readPackageJson, writePackageJson } from "./lib/packages.mjs";
import {
ALL_PACKAGES,
PACKAGES,
packageJsonPath,
readPackageJson,
writePackageJson,
} from "./lib/packages.mjs";
import { assertChannel } from "./lib/validate.mjs";
function log(msg = "") {
@@ -20,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) {
@@ -34,7 +43,7 @@ if (!dryRun && !process.env.CI) {
// 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 originals = packages.map((pkg) => {
const path = packageJsonPath(pkg);
return { pkg, path, content: readFileSync(path, "utf-8") };
});
@@ -51,7 +60,7 @@ try {
log(`channel=${channel} version=${betaVersion}`);
step("temporarily bump package.json (not committed)");
for (const pkg of PACKAGES) {
for (const pkg of packages) {
const json = readPackageJson(pkg);
json.version = betaVersion;
writePackageJson(pkg, json);
@@ -59,20 +68,20 @@ try {
// pnpm pack resolves `workspace:*` to the in-tree version, so each tarball
// will depend on its siblings at <betaVersion> after this bump.
await runCheck({ channel: true });
await runCheck({ channel: true, knowledge });
step(`idempotency: check ${betaVersion} against registry`);
const published = new Map();
for (const pkg of PACKAGES) {
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))) {
if (packages.every((pkg) => published.get(pkg.key))) {
log("\nall packages already published; nothing to do.");
} else {
// Publish in dependency order (core → runtime → commands → cli).
for (const pkg of PACKAGES) {
// Publish in dependency order (core → runtime → commands → cli [→ kscli]).
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 });
+9 -6
View File
@@ -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 { PACKAGES } from "./lib/packages.mjs";
import { ALL_PACKAGES, 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,23 +43,23 @@ try {
log("[dry-run] skipping working-tree + branch preflight");
}
const { coreJson } = await runCheck();
const { coreJson } = await runCheck({ knowledge });
const version = coreJson.version; // all packages share this, asserted by runCheck
step(`idempotency: check ${version} against registry`);
const published = new Map();
for (const pkg of PACKAGES) {
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))) {
if (packages.every((pkg) => published.get(pkg.key))) {
log("\nall packages already published; nothing to do.");
process.exit(0);
}
// Publish in dependency order (core → runtime → commands → cli).
for (const pkg of PACKAGES) {
// Publish in dependency order (core → runtime → commands → cli [→ kscli]).
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 });