From e7aecc40508abaf49a4d2d68c589c45efebbd68f Mon Sep 17 00:00:00 2001 From: dongmucat <1127093059@qq.com> Date: Mon, 11 May 2026 11:00:10 +0800 Subject: [PATCH] fix(cli): sync publish version flow --- cli/package.json | 2 +- cli/src/generated/pkg-info.ts | 2 +- scripts/publish-cli.sh | 135 +++++++++++++++--- scripts/tests/publish-cli-test.sh | 223 +++++++++++++++++++++++++++++- 4 files changed, 339 insertions(+), 23 deletions(-) diff --git a/cli/package.json b/cli/package.json index 9e97d9ac..63f63cd6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@astron-team/skillhub", - "version": "0.1.4", + "version": "0.1.5", "description": "Manage and install skills for AI coding agents", "keywords": [ "skillhub", diff --git a/cli/src/generated/pkg-info.ts b/cli/src/generated/pkg-info.ts index 0fd66330..0b559821 100644 --- a/cli/src/generated/pkg-info.ts +++ b/cli/src/generated/pkg-info.ts @@ -1,3 +1,3 @@ // Generated by scripts/generate-pkg-info.ts - do not edit by hand. export const PKG_NAME = "@astron-team/skillhub" -export const PKG_VERSION = "0.1.3" +export const PKG_VERSION = "0.1.5" diff --git a/scripts/publish-cli.sh b/scripts/publish-cli.sh index 972a4dda..48e7cb60 100755 --- a/scripts/publish-cli.sh +++ b/scripts/publish-cli.sh @@ -6,6 +6,8 @@ REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}" CLI_DIR="$REPO_ROOT/cli" ENV_LOCAL="$CLI_DIR/.env.local" PACKAGE_JSON="$CLI_DIR/package.json" +PKG_INFO_TS="$CLI_DIR/src/generated/pkg-info.ts" +DIST_ENTRY="$CLI_DIR/dist/index.js" log_stage() { echo "[publish-cli] $1" @@ -22,6 +24,88 @@ confirm() { [[ "$answer" =~ ^[Yy]$ ]] } +verify_version_sync() { + local expected_version="$1" + local generated_version + local runtime_version + + generated_version="$(node - "$PKG_INFO_TS" <<'NODE' +const fs = require('fs') +const path = process.argv[2] +const contents = fs.readFileSync(path, 'utf8') +const match = contents.match(/export const PKG_VERSION = ["']([^"']+)["']/) +if (!match) process.exit(1) +process.stdout.write(match[1]) +NODE +)" + + runtime_version="$(node "$DIST_ENTRY" version | sed -E 's/^SkillHub CLI //')" + + if [[ "$generated_version" != "$expected_version" ]]; then + echo "generated PKG_VERSION mismatch: expected $expected_version, got $generated_version" >&2 + exit 1 + fi + + if [[ "$runtime_version" != "$expected_version" ]]; then + echo "built CLI version mismatch: expected $expected_version, got $runtime_version" >&2 + exit 1 + fi +} + +assert_version_not_published() { + local package_name="$1" + local version="$2" + local view_error + + view_error="$(mktemp)" + if npm view "${package_name}@${version}" version --registry "$NPM_REGISTRY" >/dev/null 2>"$view_error"; then + rm -f "$view_error" + echo "${package_name}@${version} already exists on $NPM_REGISTRY" >&2 + echo "Update cli/package.json to the latest published version before running this release." >&2 + exit 1 + fi + + if grep -Eiq '(E404|404 Not Found|is not in this registry|Not found)' "$view_error"; then + rm -f "$view_error" + return 0 + fi + + echo "failed to verify whether ${package_name}@${version} exists on $NPM_REGISTRY" >&2 + cat "$view_error" >&2 + rm -f "$view_error" + exit 1 +} + +next_package_version() { + local version="$1" + local bump_type="$2" + + node - "$version" "$bump_type" <<'NODE' +const version = process.argv[2] +const bumpType = process.argv[3] +const parts = version.split('.').map(Number) + +if (parts.length !== 3 || parts.some(part => !Number.isInteger(part) || part < 0)) { + throw new Error(`unsupported package version: ${version}`) +} + +if (bumpType === 'patch') { + parts[2] += 1 +} else if (bumpType === 'minor') { + parts[1] += 1 + parts[2] = 0 +} else if (bumpType === 'major') { + parts[0] += 1 + parts[1] = 0 + parts[2] = 0 +} else if (bumpType !== 'skip') { + throw new Error(`unsupported bump type: ${bumpType}`) +} + +process.stdout.write(parts.join('.')) +NODE +} + BUMP_TYPE="${1:-patch}" if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" && "$BUMP_TYPE" != "skip" ]]; then usage @@ -79,23 +163,14 @@ if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then exit 1 fi -log_stage "running preflight build" -( - cd "$CLI_DIR" - bun run build -) +if [[ "$BUMP_TYPE" == "skip" ]]; then + NEW_VERSION="$PACKAGE_VERSION" +else + NEW_VERSION="$(next_package_version "$PACKAGE_VERSION" "$BUMP_TYPE")" +fi -log_stage "running preflight tests" -( - cd "$CLI_DIR" - bun run test -) - -log_stage "running preflight pack" -( - cd "$CLI_DIR" - npm pack --dry-run -) +log_stage "checking registry version" +assert_version_not_published "$PACKAGE_NAME" "$NEW_VERSION" if [[ "$BUMP_TYPE" != "skip" ]]; then if ! confirm "Proceed with version bump ($BUMP_TYPE)?"; then @@ -110,7 +185,33 @@ if [[ "$BUMP_TYPE" != "skip" ]]; then ) fi -NEW_VERSION="$(node -p "require('$PACKAGE_JSON').version ?? ''")" +ACTUAL_VERSION="$(node -p "require('$PACKAGE_JSON').version ?? ''")" +if [[ "$ACTUAL_VERSION" != "$NEW_VERSION" ]]; then + echo "npm version produced $ACTUAL_VERSION, expected $NEW_VERSION" >&2 + exit 1 +fi + +log_stage "running preflight build" +( + cd "$CLI_DIR" + bun run build +) + +log_stage "running preflight tests" +( + cd "$CLI_DIR" + bun run test +) + +log_stage "verifying built version" +verify_version_sync "$NEW_VERSION" + +log_stage "running preflight pack" +( + cd "$CLI_DIR" + npm pack --dry-run +) + log_stage "ready to publish $PACKAGE_NAME@$NEW_VERSION" if [[ "$DRY_RUN" == "true" ]]; then diff --git a/scripts/tests/publish-cli-test.sh b/scripts/tests/publish-cli-test.sh index 350862b8..2449a219 100755 --- a/scripts/tests/publish-cli-test.sh +++ b/scripts/tests/publish-cli-test.sh @@ -83,6 +83,166 @@ fi grep -F "checking git working tree" "$TMP_DIR/stdout.log" grep -F "git working tree is not clean" "$TMP_DIR/stderr.log" +CONFLICT_DIR="$(mktemp -d)" +cleanup_conflict() { + rm -rf "$CONFLICT_DIR" +} +CONFLICT_STDOUT="$(mktemp)" +CONFLICT_STDERR="$(mktemp)" +trap 'cleanup; cleanup_conflict; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR"' EXIT + +CONFLICT_CLI_DIR="$CONFLICT_DIR/cli" +CONFLICT_SCRIPTS_DIR="$CONFLICT_DIR/scripts" +CONFLICT_BIN_DIR="$CONFLICT_DIR/bin" +CONFLICT_CALLS="$CONFLICT_DIR/calls.log" +mkdir -p "$CONFLICT_CLI_DIR" "$CONFLICT_SCRIPTS_DIR" "$CONFLICT_BIN_DIR" +cp "$PUBLISH_SCRIPT" "$CONFLICT_SCRIPTS_DIR/publish-cli.sh" +cat >"$CONFLICT_CLI_DIR/.env.local" <<'EOF' +NPM_TOKEN=test-token +NPM_ORG=astron-team +DRY_RUN=true +EOF +cat >"$CONFLICT_CLI_DIR/package.json" <<'EOF' +{ + "name": "@astron-team/skillhub", + "version": "0.1.4", + "bin": { "skillhub": "./dist/index.js" }, + "files": ["dist", "README.md", "LICENSE"], + "publishConfig": { "access": "public" } +} +EOF +cat >"$CONFLICT_BIN_DIR/bun" <>"$CONFLICT_CALLS" +exit 1 +EOF +cat >"$CONFLICT_BIN_DIR/npm" <>"$CONFLICT_CALLS" +case "\$1" in + version) + node - "\$PWD/package.json" <<'NODE' +const fs = require("fs") +const path = process.argv[2] +const pkg = JSON.parse(fs.readFileSync(path, "utf8")) +pkg.version = "0.1.5" +fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + "\\n") +NODE + ;; + view) + if [[ "\$2" == "@astron-team/skillhub@0.1.5" ]]; then + echo "0.1.5" + exit 0 + fi + exit 1 + ;; + *) + exit 1 + ;; +esac +EOF +chmod +x "$CONFLICT_BIN_DIR/bun" "$CONFLICT_BIN_DIR/npm" + +git -C "$CONFLICT_DIR" init -q +git -C "$CONFLICT_DIR" config user.name "Test User" +git -C "$CONFLICT_DIR" config user.email "test@example.com" +git -C "$CONFLICT_DIR" add cli/.env.local cli/package.json scripts/publish-cli.sh bin/bun bin/npm +git -C "$CONFLICT_DIR" commit -q -m "init" + +if printf 'y\n' | REPO_ROOT="$CONFLICT_DIR" PATH="$CONFLICT_BIN_DIR:$PATH" bash "$CONFLICT_SCRIPTS_DIR/publish-cli.sh" patch >"$CONFLICT_STDOUT" 2>"$CONFLICT_STDERR"; then + echo "expected script to fail when bumped version already exists on npm" >&2 + exit 1 +fi + +grep -F "checking registry version" "$CONFLICT_STDOUT" +grep -F "@astron-team/skillhub@0.1.5 already exists" "$CONFLICT_STDERR" +grep -F "Update cli/package.json to the latest published version" "$CONFLICT_STDERR" +if grep -Fq "bun run build" "$CONFLICT_CALLS"; then + echo "build should not run when bumped version already exists" >&2 + exit 1 +fi +if grep -Fq "npm version" "$CONFLICT_CALLS"; then + echo "version bump should not run when bumped version already exists" >&2 + exit 1 +fi +grep -F '"version": "0.1.4"' "$CONFLICT_CLI_DIR/package.json" + +REGISTRY_ERROR_DIR="$(mktemp -d)" +cleanup_registry_error() { + rm -rf "$REGISTRY_ERROR_DIR" +} +REGISTRY_ERROR_STDOUT="$(mktemp)" +REGISTRY_ERROR_STDERR="$(mktemp)" +trap 'cleanup; cleanup_conflict; cleanup_registry_error; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR"' EXIT + +REGISTRY_ERROR_CLI_DIR="$REGISTRY_ERROR_DIR/cli" +REGISTRY_ERROR_SCRIPTS_DIR="$REGISTRY_ERROR_DIR/scripts" +REGISTRY_ERROR_BIN_DIR="$REGISTRY_ERROR_DIR/bin" +REGISTRY_ERROR_CALLS="$REGISTRY_ERROR_DIR/calls.log" +mkdir -p "$REGISTRY_ERROR_CLI_DIR" "$REGISTRY_ERROR_SCRIPTS_DIR" "$REGISTRY_ERROR_BIN_DIR" +cp "$PUBLISH_SCRIPT" "$REGISTRY_ERROR_SCRIPTS_DIR/publish-cli.sh" +cat >"$REGISTRY_ERROR_CLI_DIR/.env.local" <<'EOF' +NPM_TOKEN=test-token +NPM_ORG=astron-team +DRY_RUN=true +EOF +cat >"$REGISTRY_ERROR_CLI_DIR/package.json" <<'EOF' +{ + "name": "@astron-team/skillhub", + "version": "0.3.0", + "bin": { "skillhub": "./dist/index.js" }, + "files": ["dist", "README.md", "LICENSE"], + "publishConfig": { "access": "public" } +} +EOF +cat >"$REGISTRY_ERROR_BIN_DIR/bun" <>"$REGISTRY_ERROR_CALLS" +exit 1 +EOF +cat >"$REGISTRY_ERROR_BIN_DIR/npm" <>"$REGISTRY_ERROR_CALLS" +case "\$1" in + view) + echo "npm ERR! code E500" >&2 + echo "npm ERR! registry temporarily unavailable" >&2 + exit 1 + ;; + *) + exit 1 + ;; +esac +EOF +chmod +x "$REGISTRY_ERROR_BIN_DIR/bun" "$REGISTRY_ERROR_BIN_DIR/npm" + +git -C "$REGISTRY_ERROR_DIR" init -q +git -C "$REGISTRY_ERROR_DIR" config user.name "Test User" +git -C "$REGISTRY_ERROR_DIR" config user.email "test@example.com" +git -C "$REGISTRY_ERROR_DIR" add cli/.env.local cli/package.json scripts/publish-cli.sh bin/bun bin/npm +git -C "$REGISTRY_ERROR_DIR" commit -q -m "init" + +if REPO_ROOT="$REGISTRY_ERROR_DIR" PATH="$REGISTRY_ERROR_BIN_DIR:$PATH" bash "$REGISTRY_ERROR_SCRIPTS_DIR/publish-cli.sh" skip >"$REGISTRY_ERROR_STDOUT" 2>"$REGISTRY_ERROR_STDERR"; then + echo "expected script to fail when registry lookup fails unexpectedly" >&2 + exit 1 +fi + +grep -F "checking registry version" "$REGISTRY_ERROR_STDOUT" +grep -F "failed to verify whether @astron-team/skillhub@0.3.0 exists" "$REGISTRY_ERROR_STDERR" +grep -F "npm ERR! code E500" "$REGISTRY_ERROR_STDERR" +if grep -Fq "npm version" "$REGISTRY_ERROR_CALLS"; then + echo "version bump should not run when registry lookup fails" >&2 + exit 1 +fi +if grep -Fq "bun run build" "$REGISTRY_ERROR_CALLS"; then + echo "build should not run when registry lookup fails" >&2 + exit 1 +fi + SUCCESS_DIR="$(mktemp -d)" cleanup_success() { rm -rf "$SUCCESS_DIR" @@ -91,6 +251,8 @@ SUCCESS_STDOUT="$(mktemp)" SUCCESS_STDERR="$(mktemp)" trap 'cleanup; cleanup_success; rm -f "$SUCCESS_STDOUT" "$SUCCESS_STDERR"' EXIT +trap 'cleanup; cleanup_conflict; cleanup_registry_error; cleanup_success; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR" "$SUCCESS_STDOUT" "$SUCCESS_STDERR"' EXIT + SUCCESS_CLI_DIR="$SUCCESS_DIR/cli" SUCCESS_SCRIPTS_DIR="$SUCCESS_DIR/scripts" SUCCESS_BIN_DIR="$SUCCESS_DIR/bin" @@ -118,8 +280,21 @@ set -euo pipefail printf "bun %s\\n" "\$*" >>"$SUCCESS_CALLS" case "\$1 \$2" in "run build") - mkdir -p dist - printf "build output" > dist/index.js + mkdir -p dist src/generated + node - "\$PWD/package.json" "\$PWD/src/generated/pkg-info.ts" "\$PWD/dist/index.js" <<'NODE' +const fs = require("fs") +const pkgPath = process.argv[2] +const generatedPath = process.argv[3] +const distPath = process.argv[4] +const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) +fs.writeFileSync(generatedPath, [ + "// Generated by scripts/generate-pkg-info.ts - do not edit by hand.", + "export const PKG_NAME = " + JSON.stringify(pkg.name), + "export const PKG_VERSION = " + JSON.stringify(pkg.version), + "" +].join("\\n")) +fs.writeFileSync(distPath, "#!/usr/bin/env node\\nconsole.log(\\"SkillHub CLI " + pkg.version + "\\")\\n") +NODE ;; "run test") ;; @@ -135,6 +310,11 @@ printf "npm %s\\n" "\$*" >>"$SUCCESS_CALLS" case "\$1" in pack) ;; + view) + echo "npm ERR! code E404" >&2 + echo "npm ERR! 404 Not Found" >&2 + exit 1 + ;; version) node - "\$PWD/package.json" "\$2" <<'NODE' const fs = require("fs") @@ -178,19 +358,31 @@ fi grep -F "running preflight build" "$SUCCESS_STDOUT" grep -F "running preflight tests" "$SUCCESS_STDOUT" +grep -F "verifying built version" "$SUCCESS_STDOUT" grep -F "running preflight pack" "$SUCCESS_STDOUT" +grep -F "checking registry version" "$SUCCESS_STDOUT" grep -F "bumping version (patch)" "$SUCCESS_STDOUT" grep -F "ready to publish @astron-team/skillhub@0.1.1" "$SUCCESS_STDOUT" grep -F "DRY_RUN=true, skipping npm publish" "$SUCCESS_STDOUT" grep -F "bun run build" "$SUCCESS_CALLS" grep -F "bun run test" "$SUCCESS_CALLS" grep -F "npm pack --dry-run" "$SUCCESS_CALLS" +grep -F "npm version patch --no-git-tag-version" "$SUCCESS_CALLS" if grep -Fq "npm publish" "$SUCCESS_CALLS"; then echo "expected npm publish to be skipped in dry run" >&2 exit 1 fi grep -F '"version": "0.1.1"' "$SUCCESS_CLI_DIR/package.json" +grep -F 'export const PKG_VERSION = "0.1.1"' "$SUCCESS_CLI_DIR/src/generated/pkg-info.ts" +node "$SUCCESS_CLI_DIR/dist/index.js" version | grep -F "SkillHub CLI 0.1.1" + +SUCCESS_VERSION_LINE="$(grep -nF "npm version patch --no-git-tag-version" "$SUCCESS_CALLS" | cut -d: -f1)" +SUCCESS_BUILD_LINE="$(grep -nF "bun run build" "$SUCCESS_CALLS" | cut -d: -f1)" +if [[ "$SUCCESS_VERSION_LINE" -ge "$SUCCESS_BUILD_LINE" ]]; then + echo "expected version bump to happen before build" >&2 + exit 1 +fi CANCEL_DIR="$(mktemp -d)" cleanup_cancel() { @@ -200,6 +392,8 @@ CANCEL_STDOUT="$(mktemp)" CANCEL_STDERR="$(mktemp)" trap 'cleanup; cleanup_success; cleanup_cancel; rm -f "$SUCCESS_STDOUT" "$SUCCESS_STDERR" "$CANCEL_STDOUT" "$CANCEL_STDERR"' EXIT +trap 'cleanup; cleanup_conflict; cleanup_registry_error; cleanup_success; cleanup_cancel; rm -f "$CONFLICT_STDOUT" "$CONFLICT_STDERR" "$REGISTRY_ERROR_STDOUT" "$REGISTRY_ERROR_STDERR" "$SUCCESS_STDOUT" "$SUCCESS_STDERR" "$CANCEL_STDOUT" "$CANCEL_STDERR"' EXIT + CANCEL_CLI_DIR="$CANCEL_DIR/cli" CANCEL_SCRIPTS_DIR="$CANCEL_DIR/scripts" CANCEL_BIN_DIR="$CANCEL_DIR/bin" @@ -227,8 +421,21 @@ set -euo pipefail printf "bun %s\\n" "\$*" >>"$CANCEL_CALLS" case "\$1 \$2" in "run build") - mkdir -p dist - printf "build output" > dist/index.js + mkdir -p dist src/generated + node - "\$PWD/package.json" "\$PWD/src/generated/pkg-info.ts" "\$PWD/dist/index.js" <<'NODE' +const fs = require("fs") +const pkgPath = process.argv[2] +const generatedPath = process.argv[3] +const distPath = process.argv[4] +const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8")) +fs.writeFileSync(generatedPath, [ + "// Generated by scripts/generate-pkg-info.ts - do not edit by hand.", + "export const PKG_NAME = " + JSON.stringify(pkg.name), + "export const PKG_VERSION = " + JSON.stringify(pkg.version), + "" +].join("\\n")) +fs.writeFileSync(distPath, "#!/usr/bin/env node\\nconsole.log(\\"SkillHub CLI " + pkg.version + "\\")\\n") +NODE ;; "run test") ;; @@ -244,6 +451,11 @@ printf "npm %s\\n" "\$*" >>"$CANCEL_CALLS" case "\$1" in pack) ;; + view) + echo "npm ERR! code E404" >&2 + echo "npm ERR! 404 Not Found" >&2 + exit 1 + ;; version) node - "\$PWD/package.json" "\$2" <<'NODE' const fs = require("fs") @@ -293,6 +505,7 @@ if [[ "$CANCEL_EXIT_CODE" -ne 2 ]]; then fi grep -F "ready to publish @astron-team/skillhub@0.2.1" "$CANCEL_STDOUT" +grep -F "verifying built version" "$CANCEL_STDOUT" grep -F "publish cancelled" "$CANCEL_STDERR" if grep -Fq "npm publish" "$CANCEL_CALLS"; then echo "npm publish should not be called after cancellation" >&2 @@ -300,3 +513,5 @@ if grep -Fq "npm publish" "$CANCEL_CALLS"; then fi grep -F '"version": "0.2.1"' "$CANCEL_CLI_DIR/package.json" +grep -F 'export const PKG_VERSION = "0.2.1"' "$CANCEL_CLI_DIR/src/generated/pkg-info.ts" +node "$CANCEL_CLI_DIR/dist/index.js" version | grep -F "SkillHub CLI 0.2.1"