mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
feat(release): add unified release script
This commit is contained in:
@@ -1,86 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# 自动打 tag 并推送触发发布
|
||||
# 用法:
|
||||
# ./scripts/release.sh # 自动递增 patch 版本
|
||||
# ./scripts/release.sh minor # 递增 minor 版本
|
||||
# ./scripts/release.sh major # 递增 major 版本
|
||||
# ./scripts/release.sh agents-v0.3.0 # 手动指定版本号
|
||||
# ./scripts/release.sh v0.3.0 # 手动指定旧 dbx-agents 版本号,会转换为 agents-v0.3.0
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
|
||||
BUMP="${1:-patch}"
|
||||
|
||||
to_agents_tag() {
|
||||
case "$1" in
|
||||
agents-v*) echo "$1" ;;
|
||||
v*) echo "agents-$1" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
bump_tag() {
|
||||
local tag="$1"
|
||||
local bump="$2"
|
||||
local version major minor patch
|
||||
|
||||
version="${tag#agents-v}"
|
||||
IFS='.' read -r major minor patch <<< "$version"
|
||||
|
||||
case "$bump" in
|
||||
major)
|
||||
echo "agents-v$((major + 1)).0.0"
|
||||
;;
|
||||
minor)
|
||||
echo "agents-v${major}.$((minor + 1)).0"
|
||||
;;
|
||||
patch)
|
||||
echo "agents-v${major}.${minor}.$((patch + 1))"
|
||||
;;
|
||||
agents-v*|v*)
|
||||
to_agents_tag "$bump"
|
||||
;;
|
||||
*)
|
||||
echo "错误: 未知的版本递增方式 '$bump',请使用 patch/minor/major 或 agents-vX.Y.Z" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 获取当前仓库最新的 agents-v 开头的 tag
|
||||
latest_tag=$(git tag --sort=-v:refname | grep '^agents-v' | head -1 || true)
|
||||
|
||||
if [ -z "$latest_tag" ]; then
|
||||
# 迁移后首次发布时,沿用旁边旧 dbx-agents 仓库的 vX.Y.Z tag。
|
||||
legacy_tag=""
|
||||
if git -C ../dbx-agents rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
legacy_tag=$(git -C ../dbx-agents tag --sort=-v:refname | grep '^v' | head -1 || true)
|
||||
fi
|
||||
|
||||
if [ -n "$legacy_tag" ]; then
|
||||
latest_tag=$(to_agents_tag "$legacy_tag")
|
||||
echo "当前仓库未找到 Agent 版本 tag,沿用旧 dbx-agents 最新 tag: $legacy_tag"
|
||||
else
|
||||
latest_tag="agents-v0.0.0"
|
||||
echo "未找到任何 Agent 版本 tag,从 agents-v0.1.0 开始"
|
||||
fi
|
||||
else
|
||||
echo "当前最新 tag: $latest_tag"
|
||||
fi
|
||||
|
||||
new_tag=$(bump_tag "$latest_tag" "$BUMP")
|
||||
|
||||
echo "新版本: $new_tag"
|
||||
|
||||
# 确认
|
||||
read -r -p "确认创建并推送 tag ${new_tag}? [y/N] " confirm
|
||||
if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then
|
||||
echo "已取消"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 创建并推送 tag
|
||||
git tag "$new_tag"
|
||||
git push origin "$new_tag"
|
||||
|
||||
echo "完成! tag $new_tag 已推送,CI 将自动触发发布流程。"
|
||||
cd "${ROOT_DIR}"
|
||||
exec node scripts/release.mjs agents "$@"
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
"test:packages": "pnpm --filter @dbx-app/node-core test && pnpm --filter @dbx-app/cli test && pnpm --filter @dbx-app/mcp-server test",
|
||||
"pack:packages": "rm -rf /tmp/dbx-pack-check && mkdir -p /tmp/dbx-pack-check && pnpm --filter @dbx-app/node-core pack --pack-destination /tmp/dbx-pack-check && pnpm --filter @dbx-app/cli pack --pack-destination /tmp/dbx-pack-check && pnpm --filter @dbx-app/mcp-server pack --pack-destination /tmp/dbx-pack-check",
|
||||
"publish:dry-run": "pnpm build:packages && pnpm pack:packages",
|
||||
"release": "node scripts/release.mjs",
|
||||
"release:agents": "node scripts/release.mjs agents",
|
||||
"release:app": "node scripts/release.mjs app",
|
||||
"release:packages": "node scripts/release.mjs packages",
|
||||
"typecheck": "vue-tsc --noEmit --project apps/desktop/tsconfig.json",
|
||||
"build": "vite build --config apps/desktop/vite.config.ts",
|
||||
"build:checked": "pnpm typecheck && pnpm build",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const releaseScript = fileURLToPath(new URL("./release.mjs", import.meta.url));
|
||||
const result = spawnSync(process.execPath, [releaseScript, "packages", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
process.exit(result.status ?? 1);
|
||||
@@ -0,0 +1,401 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { stdin as input, stdout as output } from "node:process";
|
||||
|
||||
const REPO = "t8y2/dbx";
|
||||
const PACKAGES_WORKFLOW = "mcp-release.yml";
|
||||
const APP_PUBLISH_WORKFLOW = "publish-packages.yml";
|
||||
const PACKAGE_TAG_PREFIX = "packages-v";
|
||||
const AGENT_TAG_PREFIX = "agents-v";
|
||||
const APP_TAG_PREFIX = "v";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
let target = null;
|
||||
let requestedBump = null;
|
||||
let dryRun = false;
|
||||
let yes = false;
|
||||
let skipFetch = false;
|
||||
|
||||
for (const arg of args) {
|
||||
switch (arg) {
|
||||
case "--":
|
||||
break;
|
||||
case "--dry-run":
|
||||
dryRun = true;
|
||||
break;
|
||||
case "-y":
|
||||
case "--yes":
|
||||
yes = true;
|
||||
break;
|
||||
case "--skip-fetch":
|
||||
skipFetch = true;
|
||||
break;
|
||||
case "-h":
|
||||
case "--help":
|
||||
printHelp();
|
||||
process.exit(0);
|
||||
break;
|
||||
default: {
|
||||
const normalizedTarget = normalizeTarget(arg);
|
||||
if (!target && normalizedTarget) {
|
||||
target = normalizedTarget;
|
||||
} else if (!requestedBump) {
|
||||
requestedBump = arg;
|
||||
} else {
|
||||
fail(`Unexpected argument: ${arg}`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const repoRoot = run("git", ["rev-parse", "--show-toplevel"]).stdout.trim();
|
||||
process.chdir(repoRoot);
|
||||
|
||||
if (!skipFetch) {
|
||||
run("git", ["fetch", "--tags", "--quiet", "origin"]);
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
if (!process.stdin.isTTY) {
|
||||
fail("Release target is required in a non-interactive shell. Use packages, agents, or app.");
|
||||
}
|
||||
target = await promptTarget();
|
||||
}
|
||||
|
||||
if (target === "packages") {
|
||||
await releasePackages(requestedBump ?? "patch");
|
||||
} else if (target === "agents") {
|
||||
await releaseAgents(requestedBump ?? "patch");
|
||||
} else if (target === "app") {
|
||||
await publishApp(requestedBump);
|
||||
} else {
|
||||
fail(`Unknown release target: ${target}`);
|
||||
}
|
||||
|
||||
async function releasePackages(bump) {
|
||||
const latestVersion = getLatestPackageVersion();
|
||||
const releaseVersion = resolveReleaseVersion(bump, latestVersion, PACKAGE_TAG_PREFIX);
|
||||
const releaseTag = `${PACKAGE_TAG_PREFIX}${releaseVersion}`;
|
||||
const workflowArgs = ["workflow", "run", PACKAGES_WORKFLOW, "--repo", REPO, "-f", `version=${releaseVersion}`];
|
||||
|
||||
console.log(`Release target: Node packages / MCP`);
|
||||
console.log(`Current package version: ${latestVersion ?? "none"}`);
|
||||
console.log(`New package version: ${releaseVersion}`);
|
||||
console.log(`Release tag: ${releaseTag}`);
|
||||
console.log(`Workflow: Node Packages Release (${PACKAGES_WORKFLOW})`);
|
||||
console.log(`Command: gh ${workflowArgs.join(" ")}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log("Dry run only; workflow was not triggered.");
|
||||
return;
|
||||
}
|
||||
|
||||
ensureGhReady(PACKAGES_WORKFLOW);
|
||||
await confirmOrExit(`Confirm triggering Node Packages Release for ${releaseVersion}? [y/N] `);
|
||||
|
||||
run("gh", workflowArgs, { stdio: "inherit" });
|
||||
console.log(`Triggered Node Packages Release for ${releaseVersion}.`);
|
||||
}
|
||||
|
||||
async function releaseAgents(bump) {
|
||||
const latest = getLatestAgentTag();
|
||||
const releaseTag = resolveAgentTag(bump, latest.tag);
|
||||
|
||||
if (tagExists(releaseTag)) {
|
||||
fail(`Tag ${releaseTag} already exists.`);
|
||||
}
|
||||
|
||||
console.log(`Release target: Agents`);
|
||||
console.log(`Current agent tag: ${latest.tag}${latest.source ? ` (${latest.source})` : ""}`);
|
||||
console.log(`New agent tag: ${releaseTag}`);
|
||||
console.log(`Workflow: Agents Release (.github/workflows/agents-release.yml)`);
|
||||
console.log(`Commands: git tag ${releaseTag} && git push origin ${releaseTag}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log("Dry run only; tag was not created or pushed.");
|
||||
return;
|
||||
}
|
||||
|
||||
await confirmOrExit(`Confirm creating and pushing tag ${releaseTag}? [y/N] `);
|
||||
|
||||
run("git", ["tag", releaseTag], { stdio: "inherit" });
|
||||
run("git", ["push", "origin", releaseTag], { stdio: "inherit" });
|
||||
console.log(`Pushed ${releaseTag}; Agents Release will run from the tag push.`);
|
||||
}
|
||||
|
||||
async function publishApp(tagInput) {
|
||||
const latest = getLatestAppTag();
|
||||
const releaseTag = tagInput ? resolveAppTag(tagInput) : latest.tag;
|
||||
const workflowArgs = ["workflow", "run", APP_PUBLISH_WORKFLOW, "--repo", REPO, "-f", `tag=${releaseTag}`];
|
||||
|
||||
console.log(`Release target: App distribution`);
|
||||
console.log(`Latest app tag: ${latest.tag}`);
|
||||
console.log(`Publish tag: ${releaseTag}`);
|
||||
console.log(`Workflow: Publish Packages (${APP_PUBLISH_WORKFLOW})`);
|
||||
console.log(`Command: gh ${workflowArgs.join(" ")}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.log("Dry run only; workflow was not triggered.");
|
||||
return;
|
||||
}
|
||||
|
||||
ensureGhReady(APP_PUBLISH_WORKFLOW);
|
||||
run("gh", ["release", "view", releaseTag, "--repo", REPO], { stdio: "inherit" });
|
||||
await confirmOrExit(`Confirm publishing app distribution for ${releaseTag}? [y/N] `);
|
||||
|
||||
run("gh", workflowArgs, { stdio: "inherit" });
|
||||
console.log(`Triggered Publish Packages for ${releaseTag}.`);
|
||||
}
|
||||
|
||||
async function promptTarget() {
|
||||
const answer = await ask(`Select release target:
|
||||
1. Node packages / MCP
|
||||
2. Agents
|
||||
3. App distribution
|
||||
Choice [1]: `);
|
||||
|
||||
const normalized = answer.trim().toLowerCase();
|
||||
if (!normalized || normalized === "1" || normalized === "packages" || normalized === "mcp") return "packages";
|
||||
if (normalized === "2" || normalized === "agents" || normalized === "agent") return "agents";
|
||||
if (normalized === "3" || normalized === "app" || normalized === "desktop" || normalized === "publish") return "app";
|
||||
fail(`Unknown release target: ${answer}`);
|
||||
}
|
||||
|
||||
async function confirmOrExit(message) {
|
||||
if (yes) return;
|
||||
if (!process.stdin.isTTY) {
|
||||
fail("Refusing to trigger release without confirmation in a non-interactive shell. Re-run with --yes if this is intentional.");
|
||||
}
|
||||
|
||||
const answer = await ask(message);
|
||||
if (!["y", "yes"].includes(answer.trim().toLowerCase())) {
|
||||
console.log("Cancelled.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function ask(question) {
|
||||
const rl = createInterface({ input, output });
|
||||
const answer = await rl.question(question);
|
||||
rl.close();
|
||||
return answer;
|
||||
}
|
||||
|
||||
function normalizeTarget(value) {
|
||||
if (["package", "packages", "node-packages", "node", "mcp"].includes(value)) return "packages";
|
||||
if (["agent", "agents"].includes(value)) return "agents";
|
||||
if (["app", "desktop", "publish", "publish-packages", "distribution"].includes(value)) return "app";
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLatestPackageVersion() {
|
||||
const tag = getLatestSemverTag(PACKAGE_TAG_PREFIX);
|
||||
if (tag) return tag.versionText;
|
||||
|
||||
const packageVersions = [
|
||||
"packages/node-core/package.json",
|
||||
"packages/cli/package.json",
|
||||
"packages/mcp-server/package.json",
|
||||
].map((path) => JSON.parse(readFileSync(path, "utf8")).version);
|
||||
|
||||
const uniqueVersions = [...new Set(packageVersions)];
|
||||
if (uniqueVersions.length !== 1) {
|
||||
fail(`Package versions differ and no ${PACKAGE_TAG_PREFIX} tag was found: ${uniqueVersions.join(", ")}`);
|
||||
}
|
||||
|
||||
return uniqueVersions[0];
|
||||
}
|
||||
|
||||
function getLatestAgentTag() {
|
||||
const tag = getLatestSemverTag(AGENT_TAG_PREFIX);
|
||||
if (tag) return { tag: tag.tag, source: "current repo" };
|
||||
|
||||
const legacyRepo = "../dbx-agents";
|
||||
const legacyRepoCheck = spawnSync("git", ["-C", legacyRepo, "rev-parse", "--is-inside-work-tree"], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
if (legacyRepoCheck.status === 0) {
|
||||
const legacyTags = run("git", ["-C", legacyRepo, "tag", "--list", "v*"]).stdout
|
||||
.split(/\r?\n/)
|
||||
.map((legacyTag) => legacyTag.trim())
|
||||
.filter(Boolean)
|
||||
.map((legacyTag) => ({ legacyTag, version: parseVersion(legacyTag.replace(/^v/, "")) }))
|
||||
.filter((entry) => entry.version)
|
||||
.sort((a, b) => compareVersions(b.version, a.version));
|
||||
|
||||
if (legacyTags.length > 0) {
|
||||
return {
|
||||
tag: `${AGENT_TAG_PREFIX}${formatVersion(legacyTags[0].version)}`,
|
||||
source: `${legacyRepo} ${legacyTags[0].legacyTag}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { tag: `${AGENT_TAG_PREFIX}0.0.0`, source: "initial baseline" };
|
||||
}
|
||||
|
||||
function getLatestAppTag() {
|
||||
const tag = getLatestSemverTag(APP_TAG_PREFIX);
|
||||
if (!tag) {
|
||||
fail("No v* app release tag was found.");
|
||||
}
|
||||
return { tag: tag.tag };
|
||||
}
|
||||
|
||||
function getLatestSemverTag(prefix) {
|
||||
const tags = run("git", ["tag", "--list", `${prefix}*`]).stdout
|
||||
.split(/\r?\n/)
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.map((tag) => ({ tag, version: parseVersion(tag.replace(prefix, "")) }))
|
||||
.filter((entry) => entry.version)
|
||||
.sort((a, b) => compareVersions(b.version, a.version));
|
||||
|
||||
if (tags.length === 0) return null;
|
||||
return { ...tags[0], versionText: formatVersion(tags[0].version) };
|
||||
}
|
||||
|
||||
function resolveAgentTag(bump, latestTag) {
|
||||
const explicitVersion = normalizeExplicitVersion(bump, AGENT_TAG_PREFIX);
|
||||
if (explicitVersion) return `${AGENT_TAG_PREFIX}${explicitVersion}`;
|
||||
|
||||
const latestVersion = latestTag.replace(AGENT_TAG_PREFIX, "");
|
||||
return `${AGENT_TAG_PREFIX}${resolveReleaseVersion(bump, latestVersion, AGENT_TAG_PREFIX)}`;
|
||||
}
|
||||
|
||||
function resolveAppTag(value) {
|
||||
if (["patch", "minor", "major"].includes(value)) {
|
||||
fail("App distribution publishing requires an existing vX.Y.Z release tag, not a version bump.");
|
||||
}
|
||||
|
||||
const explicitVersion = normalizeExplicitVersion(value, APP_TAG_PREFIX);
|
||||
if (!explicitVersion) {
|
||||
fail(`Invalid app release tag '${value}'. Use vX.Y.Z or X.Y.Z.`);
|
||||
}
|
||||
|
||||
return `${APP_TAG_PREFIX}${explicitVersion}`;
|
||||
}
|
||||
|
||||
function resolveReleaseVersion(bump, latestVersion, explicitTagPrefix) {
|
||||
const normalizedVersion = normalizeExplicitVersion(bump, explicitTagPrefix);
|
||||
if (normalizedVersion) return normalizedVersion;
|
||||
|
||||
if (!["patch", "minor", "major"].includes(bump)) {
|
||||
fail(`Unknown version bump '${bump}'. Use patch, minor, major, or an explicit semver version.`);
|
||||
}
|
||||
|
||||
const latest = parseVersion(latestVersion);
|
||||
if (!latest || latest.prerelease) {
|
||||
fail(`Cannot ${bump} bump from non-standard version '${latestVersion}'. Pass an explicit version instead.`);
|
||||
}
|
||||
|
||||
if (bump === "major") return `${latest.major + 1}.0.0`;
|
||||
if (bump === "minor") return `${latest.major}.${latest.minor + 1}.0`;
|
||||
return `${latest.major}.${latest.minor}.${latest.patch + 1}`;
|
||||
}
|
||||
|
||||
function normalizeExplicitVersion(value, explicitTagPrefix) {
|
||||
const trimmed = value.trim().replace(new RegExp(`^${escapeRegExp(explicitTagPrefix)}`), "").replace(/^v/, "");
|
||||
const version = parseVersion(trimmed);
|
||||
return version ? formatVersion(version) : null;
|
||||
}
|
||||
|
||||
function parseVersion(value) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(value);
|
||||
if (!match) return null;
|
||||
return {
|
||||
major: Number(match[1]),
|
||||
minor: Number(match[2]),
|
||||
patch: Number(match[3]),
|
||||
prerelease: match[4] ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function formatVersion(version) {
|
||||
return `${version.major}.${version.minor}.${version.patch}${version.prerelease ? `-${version.prerelease}` : ""}`;
|
||||
}
|
||||
|
||||
function compareVersions(a, b) {
|
||||
for (const key of ["major", "minor", "patch"]) {
|
||||
if (a[key] !== b[key]) return a[key] - b[key];
|
||||
}
|
||||
if (a.prerelease === b.prerelease) return 0;
|
||||
if (!a.prerelease) return 1;
|
||||
if (!b.prerelease) return -1;
|
||||
return a.prerelease.localeCompare(b.prerelease);
|
||||
}
|
||||
|
||||
function tagExists(tag) {
|
||||
const result = spawnSync("git", ["rev-parse", "-q", "--verify", `refs/tags/${tag}`], {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: "pipe",
|
||||
});
|
||||
return result.status === 0;
|
||||
}
|
||||
|
||||
function ensureGhReady(workflow) {
|
||||
run("gh", ["auth", "status", "--hostname", "github.com"], { stdio: "inherit" });
|
||||
run("gh", ["workflow", "view", workflow, "--repo", REPO], { stdio: "inherit" });
|
||||
}
|
||||
|
||||
function run(command, commandArgs, options = {}) {
|
||||
const result = spawnSync(command, commandArgs, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
encoding: "utf8",
|
||||
stdio: options.stdio ?? "pipe",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
fail(`${command} ${commandArgs.join(" ")} failed: ${result.error.message}`);
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
||||
fail(`${command} ${commandArgs.join(" ")} failed${stderr ? `:\n${stderr}` : ""}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function fail(message) {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`Usage: node scripts/release.mjs [packages|agents|app] [patch|minor|major|version] [options]
|
||||
|
||||
Unified release trigger for DBX packages, agents, and app distribution.
|
||||
|
||||
Targets:
|
||||
packages Trigger Node Packages Release via gh workflow run
|
||||
agents Create and push an agents-v* tag
|
||||
app Trigger Publish Packages for an existing v* app release tag
|
||||
|
||||
Arguments:
|
||||
patch Bump the latest target tag by one patch version (default)
|
||||
minor Bump the latest target tag by one minor version
|
||||
major Bump the latest target tag by one major version
|
||||
0.4.14 Trigger an explicit version
|
||||
packages-v0.4.14 Explicit package tag style, for packages
|
||||
agents-v0.4.14 Explicit agent tag style, for agents
|
||||
v0.5.38 Existing app release tag, for app
|
||||
|
||||
Options:
|
||||
--dry-run Print the release command without triggering it
|
||||
-y, --yes Skip the confirmation prompt
|
||||
--skip-fetch Do not run git fetch --tags before reading release tags
|
||||
-h, --help Show this help
|
||||
`);
|
||||
}
|
||||
Reference in New Issue
Block a user