Skip to content

Commit a966b40

Browse files
committed
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.
1 parent 3951320 commit a966b40

7 files changed

Lines changed: 175 additions & 67 deletions

File tree

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
name: Publish Knowledge
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
mode:
7+
description: "Publish mode"
8+
required: true
9+
type: choice
10+
options:
11+
- channel
12+
- stable
13+
channel:
14+
description: "dist-tag (channel mode only, e.g. mcp/plugin/advisor)"
15+
required: false
16+
type: string
17+
18+
concurrency:
19+
group: publish-knowledge-${{ inputs.mode }}-${{ inputs.channel }}
20+
cancel-in-progress: false
21+
22+
jobs:
23+
publish-stable:
24+
if: inputs.mode == 'stable'
25+
name: publish stable (with knowledge) to npm + tag
26+
runs-on: ubuntu-latest
27+
environment: production # Required Reviewers gate
28+
permissions:
29+
contents: write # push lightweight tag to origin
30+
id-token: write # OIDC for npm Trusted Publishing + provenance
31+
steps:
32+
- uses: actions/checkout@v6
33+
34+
- uses: pnpm/action-setup@v6
35+
36+
- uses: actions/setup-node@v6
37+
with:
38+
node-version: "24"
39+
cache: pnpm
40+
registry-url: "https://registry.npmjs.org/"
41+
42+
- name: Install gitleaks
43+
run: |
44+
set -euo pipefail
45+
GITLEAKS_VERSION=8.21.2
46+
curl -sSfL \
47+
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
48+
| sudo tar -xz -C /usr/local/bin gitleaks
49+
gitleaks version
50+
51+
- run: pnpm install --frozen-lockfile
52+
53+
- name: publish-stable (with knowledge)
54+
run: node tools/release/publish-stable.mjs --knowledge
55+
56+
publish-channel:
57+
if: inputs.mode == 'channel'
58+
name: publish beta (with knowledge) to npm
59+
runs-on: ubuntu-latest
60+
permissions:
61+
contents: read # no tag, no Release; just publish
62+
id-token: write # OIDC for npm Trusted Publishing + provenance
63+
steps:
64+
- uses: actions/checkout@v6
65+
66+
- uses: pnpm/action-setup@v6
67+
68+
- uses: actions/setup-node@v6
69+
with:
70+
node-version: "24"
71+
cache: pnpm
72+
registry-url: "https://registry.npmjs.org/"
73+
74+
- name: Install gitleaks
75+
run: |
76+
set -euo pipefail
77+
GITLEAKS_VERSION=8.21.2
78+
curl -sSfL \
79+
"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
80+
| sudo tar -xz -C /usr/local/bin gitleaks
81+
gitleaks version
82+
83+
- run: pnpm install --frozen-lockfile
84+
85+
- name: publish-channel (with knowledge)
86+
run: node tools/release/publish-channel.mjs --knowledge --channel "${{ inputs.channel }}"

tools/release/check.mjs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
44
import { packAndScan } from "./lib/pack-scan.mjs";
55
import { run } from "./lib/proc.mjs";
66
import { assertReadmeSync, loadAndValidatePackages } from "./lib/validate.mjs";
7+
import { ALL_PACKAGES, PACKAGES } from "./lib/packages.mjs";
78

89
function log(msg = "") {
910
process.stdout.write(`${msg}\n`);
@@ -17,20 +18,24 @@ function step(msg) {
1718
* Pure-validation pipeline. Reusable from publish-stable / publish-channel.
1819
* Returns { coreJson, cliJson } for callers that need the parsed package.jsons.
1920
*
20-
* @param {{ channel?: boolean }} [options]
21+
* @param {{ channel?: boolean, knowledge?: boolean }} [options]
2122
* @param {boolean} [options.channel] — When true (publish-channel): regenerate
2223
* `reference/` and assert it matches git, but do not sync `SKILL.md` from the
2324
* temporary beta `package.json` version (repo skill stays aligned with stable).
25+
* @param {boolean} [options.knowledge] — When true: also build and validate
26+
* knowledge-studio-cli alongside the base packages.
2427
*/
2528
export async function runCheck(options = {}) {
2629
const channel = options.channel === true;
30+
const knowledge = options.knowledge === true;
31+
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
2732

2833
step("pnpm install --frozen-lockfile");
2934
run("pnpm", ["install", "--frozen-lockfile"]);
3035

3136
step("metadata: README sync, version consistency, workspace:* dep");
3237
assertReadmeSync();
33-
const { coreJson, cliJson } = loadAndValidatePackages();
38+
const { coreJson, cliJson } = loadAndValidatePackages({ packages });
3439
log(`bailian-cli-core@${coreJson.version}`);
3540
log(`bailian-cli@${cliJson.version}`);
3641

@@ -63,8 +68,13 @@ export async function runCheck(options = {}) {
6368
step("build bailian-cli");
6469
run("pnpm", ["--filter", "bailian-cli", "run", "build"]);
6570

71+
if (knowledge) {
72+
step("build knowledge-studio-cli");
73+
run("pnpm", ["--filter", "knowledge-studio-cli", "run", "build"]);
74+
}
75+
6676
step("pack + scan (publint, gitleaks)");
67-
packAndScan({ log });
77+
packAndScan({ log, packages });
6878

6979
log("\nrelease check passed.");
7080
return { coreJson, cliJson };

tools/release/lib/pack-scan.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ function extractTarball(tarball, tempDir, key) {
1313
return extractDir;
1414
}
1515

16-
export function packAndScan({ log }) {
16+
export function packAndScan({ log, packages }) {
17+
const pkgs = packages ?? PACKAGES;
1718
const tempDir = mkdtempSync(join(tmpdir(), "bailian-release-"));
1819
try {
19-
for (const pkg of PACKAGES) {
20+
for (const pkg of pkgs) {
2021
const json = readPackageJson(pkg);
2122
log(`packing ${pkg.name}@${json.version}`);
2223
const tarball = pnpmPack(pkg, tempDir, json);

tools/release/lib/packages.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export const PACKAGES = [
99
{ key: "cli", dir: "packages/cli", name: "bailian-cli" },
1010
];
1111

12+
// knowledge-studio-cli shares the same library deps as bailian-cli.
13+
// Published via a separate workflow (publish-knowledge.yml) with --knowledge flag.
14+
export const KSCLI_PACKAGE = { key: "kscli", dir: "packages/kscli", name: "knowledge-studio-cli" };
15+
export const ALL_PACKAGES = [...PACKAGES, KSCLI_PACKAGE];
16+
1217
export function readJson(path) {
1318
return JSON.parse(readFileSync(path, "utf-8"));
1419
}

tools/release/lib/validate.mjs

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ export function assertReadmeSync() {
1818
}
1919
}
2020

21-
export function loadAndValidatePackages() {
21+
export function loadAndValidatePackages({ packages } = {}) {
22+
const pkgs = packages ?? PACKAGES;
23+
const internalNames = new Set(pkgs.map((p) => p.name));
2224
const jsonByKey = new Map();
23-
for (const pkg of PACKAGES) {
25+
for (const pkg of pkgs) {
2426
const json = readPackageJson(pkg);
2527
if (json.name !== pkg.name) {
2628
throw new Error(`${pkg.dir} name must be ${pkg.name}, got ${json.name}`);
@@ -30,18 +32,21 @@ export function loadAndValidatePackages() {
3032

3133
const coreJson = jsonByKey.get("core");
3234
const cliJson = jsonByKey.get("cli");
35+
const version = coreJson.version;
3336

34-
if (cliJson.version !== coreJson.version) {
35-
throw new Error(
36-
`core and cli versions must match, got ${coreJson.version} and ${cliJson.version}.`,
37-
);
38-
}
39-
40-
const cliCoreDep = cliJson.dependencies?.["bailian-cli-core"];
41-
if (cliCoreDep !== "workspace:*") {
42-
throw new Error(
43-
`packages/cli source dependency on bailian-cli-core must be "workspace:*", got ${cliCoreDep}.`,
44-
);
37+
for (const pkg of pkgs) {
38+
const json = jsonByKey.get(pkg.key);
39+
if (json.version !== version) {
40+
throw new Error(
41+
`all package versions must match ${version} (bailian-cli-core), ` +
42+
`but ${pkg.name} is ${json.version}.`,
43+
);
44+
}
45+
for (const [dep, range] of Object.entries(json.dependencies ?? {})) {
46+
if (internalNames.has(dep) && range !== "workspace:*") {
47+
throw new Error(`${pkg.name} dependency on ${dep} must be "workspace:*", got ${range}.`);
48+
}
49+
}
4550
}
4651

4752
return { coreJson, cliJson };

tools/release/publish-channel.mjs

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import { runCheck } from "./check.mjs";
66
import { headSha7, utcDateStamp } from "./lib/git.mjs";
77
import { npmViewExists, pnpmPublish } from "./lib/npm.mjs";
88
import {
9-
findPackage,
9+
ALL_PACKAGES,
10+
PACKAGES,
1011
packageJsonPath,
1112
readPackageJson,
1213
writePackageJson,
@@ -25,28 +26,30 @@ const { values } = parseArgs({
2526
options: {
2627
channel: { type: "string" },
2728
"dry-run": { type: "boolean", default: false },
29+
knowledge: { type: "boolean", default: false },
2830
},
2931
allowPositionals: false,
3032
});
3133
const channel = values.channel;
3234
const dryRun = values["dry-run"];
35+
const knowledge = values.knowledge;
36+
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
3337
assertChannel(channel);
3438

3539
if (!dryRun && !process.env.CI) {
3640
process.stderr.write("publish-channel is CI-only. Pass --dry-run to test locally.\n");
3741
process.exit(1);
3842
}
3943

40-
const core = findPackage("core");
41-
const cli = findPackage("cli");
42-
const corePath = packageJsonPath(core);
43-
const cliPath = packageJsonPath(cli);
44-
const coreOriginal = readFileSync(corePath, "utf-8");
45-
const cliOriginal = readFileSync(cliPath, "utf-8");
44+
// Snapshot every package.json so the temporary version bump is reverted in
45+
// `finally`, even when the release fails midway.
46+
const originals = packages.map((pkg) => {
47+
const path = packageJsonPath(pkg);
48+
return { pkg, path, content: readFileSync(path, "utf-8") };
49+
});
4650

4751
function restoreOriginals() {
48-
writeFileSync(corePath, coreOriginal);
49-
writeFileSync(cliPath, cliOriginal);
52+
for (const { path, content } of originals) writeFileSync(path, content);
5053
}
5154

5255
try {
@@ -57,32 +60,29 @@ try {
5760
log(`channel=${channel} version=${betaVersion}`);
5861

5962
step("temporarily bump package.json (not committed)");
60-
const coreJson = readPackageJson(core);
61-
const cliJson = readPackageJson(cli);
62-
coreJson.version = betaVersion;
63-
cliJson.version = betaVersion;
64-
writePackageJson(core, coreJson);
65-
writePackageJson(cli, cliJson);
66-
// pnpm pack resolves `workspace:*` to the in-tree version, so CLI tarball
67-
// will depend on bailian-cli-core@<betaVersion> after this bump.
63+
for (const pkg of packages) {
64+
const json = readPackageJson(pkg);
65+
json.version = betaVersion;
66+
writePackageJson(pkg, json);
67+
}
6868

69-
await runCheck({ channel: true });
69+
await runCheck({ channel: true, knowledge });
7070

7171
step(`idempotency: check ${betaVersion} against registry`);
72-
const corePublished = npmViewExists(core.name, betaVersion);
73-
const cliPublished = npmViewExists(cli.name, betaVersion);
74-
log(`${core.name}@${betaVersion}: ${corePublished ? "already published" : "to publish"}`);
75-
log(`${cli.name}@${betaVersion}: ${cliPublished ? "already published" : "to publish"}`);
76-
if (corePublished && cliPublished) {
77-
log("\nboth packages already published; nothing to do.");
72+
const published = new Map();
73+
for (const pkg of packages) {
74+
const exists = npmViewExists(pkg.name, betaVersion);
75+
published.set(pkg.key, exists);
76+
log(`${pkg.name}@${betaVersion}: ${exists ? "already published" : "to publish"}`);
77+
}
78+
if (packages.every((pkg) => published.get(pkg.key))) {
79+
log("\nall packages already published; nothing to do.");
7880
} else {
79-
if (!corePublished) {
80-
step(`publish ${core.name}@${betaVersion} (tag=${channel}, provenance)`);
81-
pnpmPublish(core, { tag: channel, provenance: true, dryRun });
82-
}
83-
if (!cliPublished) {
84-
step(`publish ${cli.name}@${betaVersion} (tag=${channel}, provenance)`);
85-
pnpmPublish(cli, { tag: channel, provenance: true, dryRun });
81+
// Publish in dependency order.
82+
for (const pkg of packages) {
83+
if (published.get(pkg.key)) continue;
84+
step(`publish ${pkg.name}@${betaVersion} (tag=${channel}, provenance)`);
85+
pnpmPublish(pkg, { tag: channel, provenance: true, dryRun });
8686
}
8787
}
8888

tools/release/publish-stable.mjs

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { parseArgs } from "util";
44
import { runCheck } from "./check.mjs";
55
import { createTag, currentBranch, isWorkingTreeClean, pushTag, tagExists } from "./lib/git.mjs";
66
import { npmViewExists, pnpmPublish } from "./lib/npm.mjs";
7-
import { findPackage } from "./lib/packages.mjs";
7+
import { ALL_PACKAGES, findPackage, PACKAGES } from "./lib/packages.mjs";
88

99
function log(msg = "") {
1010
process.stdout.write(`${msg}\n`);
@@ -17,10 +17,13 @@ function step(msg) {
1717
const { values } = parseArgs({
1818
options: {
1919
"dry-run": { type: "boolean", default: false },
20+
knowledge: { type: "boolean", default: false },
2021
},
2122
allowPositionals: false,
2223
});
2324
const dryRun = values["dry-run"];
25+
const knowledge = values.knowledge;
26+
const packages = knowledge ? ALL_PACKAGES : PACKAGES;
2427

2528
try {
2629
if (!dryRun && !process.env.CI) {
@@ -40,28 +43,26 @@ try {
4043
log("[dry-run] skipping working-tree + branch preflight");
4144
}
4245

43-
const { coreJson } = await runCheck();
44-
const version = coreJson.version; // === cliJson.version, asserted by runCheck
46+
const { coreJson } = await runCheck({ knowledge });
47+
const version = coreJson.version; // all packages share this, asserted by runCheck
4548

4649
step(`idempotency: check ${version} against registry`);
47-
const core = findPackage("core");
48-
const cli = findPackage("cli");
49-
const corePublished = npmViewExists(core.name, version);
50-
const cliPublished = npmViewExists(cli.name, version);
51-
log(`${core.name}@${version}: ${corePublished ? "already published" : "to publish"}`);
52-
log(`${cli.name}@${version}: ${cliPublished ? "already published" : "to publish"}`);
53-
if (corePublished && cliPublished) {
54-
log("\nboth packages already published; nothing to do.");
50+
const published = new Map();
51+
for (const pkg of packages) {
52+
const exists = npmViewExists(pkg.name, version);
53+
published.set(pkg.key, exists);
54+
log(`${pkg.name}@${version}: ${exists ? "already published" : "to publish"}`);
55+
}
56+
if (packages.every((pkg) => published.get(pkg.key))) {
57+
log("\nall packages already published; nothing to do.");
5558
process.exit(0);
5659
}
5760

58-
if (!corePublished) {
59-
step(`publish ${core.name}@${version} (tag=latest, provenance)`);
60-
pnpmPublish(core, { tag: "latest", provenance: true, dryRun });
61-
}
62-
if (!cliPublished) {
63-
step(`publish ${cli.name}@${version} (tag=latest, provenance)`);
64-
pnpmPublish(cli, { tag: "latest", provenance: true, dryRun });
61+
// Publish in dependency order.
62+
for (const pkg of packages) {
63+
if (published.get(pkg.key)) continue;
64+
step(`publish ${pkg.name}@${version} (tag=latest, provenance)`);
65+
pnpmPublish(pkg, { tag: "latest", provenance: true, dryRun });
6566
}
6667

6768
if (dryRun) {

0 commit comments

Comments
 (0)