mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
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:
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user