mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +00:00
245 lines
5.7 KiB
Bash
Executable file
245 lines
5.7 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
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"
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: $0 [patch|minor|major|skip]" >&2
|
|
}
|
|
|
|
confirm() {
|
|
local prompt="$1"
|
|
local answer
|
|
read -r -p "$prompt [y/N]: " answer
|
|
[[ "$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_output
|
|
|
|
if view_output="$(npm view "${package_name}@${version}" version --registry "$NPM_REGISTRY" 2>&1)"; then
|
|
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 echo "$view_output" | grep -Eiq '(E404|404 Not Found|is not in this registry|Not found)'; then
|
|
return 0
|
|
fi
|
|
|
|
echo "failed to verify whether ${package_name}@${version} exists on $NPM_REGISTRY" >&2
|
|
echo "$view_output" >&2
|
|
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
|
|
exit 1
|
|
fi
|
|
|
|
if [[ ! -f "$ENV_LOCAL" ]]; then
|
|
echo "cli/.env.local not found" >&2
|
|
echo "Copy cli/.env.example to cli/.env.local" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_stage "loading environment"
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$ENV_LOCAL"
|
|
set +a
|
|
|
|
: "${NPM_REGISTRY:=https://registry.npmjs.org}"
|
|
: "${DRY_RUN:=false}"
|
|
|
|
if [[ -z "${NPM_TOKEN:-}" ]]; then
|
|
echo "NPM_TOKEN is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${NPM_ORG:-}" ]]; then
|
|
echo "NPM_ORG is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_stage "validating package metadata"
|
|
PACKAGE_NAME="$(node -p "require('$PACKAGE_JSON').name ?? ''")"
|
|
PACKAGE_VERSION="$(node -p "require('$PACKAGE_JSON').version ?? ''")"
|
|
PACKAGE_ACCESS="$(node -p "require('$PACKAGE_JSON').publishConfig?.access ?? ''")"
|
|
|
|
if [[ "$PACKAGE_NAME" != "@${NPM_ORG}/"* ]]; then
|
|
echo "package name must match @${NPM_ORG}/* (got $PACKAGE_NAME)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "$PACKAGE_VERSION" ]]; then
|
|
echo "package version is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$PACKAGE_ACCESS" != "public" ]]; then
|
|
echo "publishConfig.access must be public" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_stage "checking git working tree"
|
|
if [[ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]]; then
|
|
echo "git working tree is not clean" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$BUMP_TYPE" == "skip" ]]; then
|
|
NEW_VERSION="$PACKAGE_VERSION"
|
|
else
|
|
NEW_VERSION="$(next_package_version "$PACKAGE_VERSION" "$BUMP_TYPE")"
|
|
fi
|
|
|
|
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
|
|
echo "version bump cancelled" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log_stage "bumping version ($BUMP_TYPE)"
|
|
(
|
|
cd "$CLI_DIR"
|
|
npm version "$BUMP_TYPE" --no-git-tag-version
|
|
)
|
|
fi
|
|
|
|
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
|
|
log_stage "DRY_RUN=true, skipping npm publish"
|
|
exit 0
|
|
fi
|
|
|
|
if ! confirm "Publish $PACKAGE_NAME@$NEW_VERSION to $NPM_REGISTRY?"; then
|
|
echo "publish cancelled" >&2
|
|
exit 2
|
|
fi
|
|
|
|
NPM_CONFIG_FILE="$(mktemp)"
|
|
cleanup() {
|
|
rm -f "$NPM_CONFIG_FILE"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
REGISTRY_HOST="${NPM_REGISTRY#http://}"
|
|
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
|
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
|
|
|
cat >"$NPM_CONFIG_FILE" <<EOF
|
|
registry=${NPM_REGISTRY}
|
|
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
|
|
always-auth=true
|
|
EOF
|
|
|
|
log_stage "publishing package"
|
|
(
|
|
cd "$CLI_DIR"
|
|
NPM_CONFIG_USERCONFIG="$NPM_CONFIG_FILE" npm publish --access public --registry "$NPM_REGISTRY"
|
|
)
|
|
|
|
log_stage "publish completed"
|