feat(cli): add automated build and publish workflow

- Add release-cli.yml GitHub Actions workflow: build, test, npm publish,
  and GitHub Release triggered by cli-v* tags
- Rewrite scripts/publish-cli.sh: local bump + commit + tag + push,
  enforces main branch, idempotent tag checks
- Add concurrency group and release idempotency to workflow
- Add make publish-cli / publish-cli-minor / publish-cli-major targets
- Add cli/RELEASE.md documenting the full release process
This commit is contained in:
Cheney 2026-05-12 10:32:06 +08:00
parent 15e55e8055
commit 378216c6da
4 changed files with 470 additions and 220 deletions

248
.github/workflows/release-cli.yml vendored Normal file
View file

@ -0,0 +1,248 @@
name: Release CLI
on:
push:
tags: ['cli-v*']
workflow_dispatch:
inputs:
tag:
description: 'Tag to release (e.g. cli-v0.1.5)'
required: true
skip_npm:
description: 'Skip npm publish'
type: boolean
default: false
permissions:
contents: write
concurrency:
group: release-cli-${{ github.event.inputs.tag || github.ref_name }}
cancel-in-progress: false
jobs:
build-and-test:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.extract.outputs.version }}
package_name: ${{ steps.extract.outputs.package_name }}
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.13
- name: Extract version from tag
id: extract
working-directory: cli
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
if [[ ! "$TAG" =~ ^cli-v([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?)$ ]]; then
echo "Invalid tag format: $TAG (expected cli-vX.Y.Z)"
exit 1
fi
VERSION="${BASH_REMATCH[1]}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '$VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
PACKAGE_NAME=$(node -p "require('./package.json').name")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "package_name=$PACKAGE_NAME" >> "$GITHUB_OUTPUT"
echo "Version set to: $VERSION"
echo "Package name: $PACKAGE_NAME"
- name: Install dependencies
working-directory: cli
run: bun install --frozen-lockfile
- name: Run linter
working-directory: cli
run: bun run lint
- name: Run type check
working-directory: cli
run: bun run typecheck
- name: Run tests
working-directory: cli
run: bun test
- name: Build CLI
working-directory: cli
run: bun run build
- name: Verify built CLI
working-directory: cli
run: |
node dist/index.js version
RUNTIME_VERSION=$(node dist/index.js version | sed -E 's/^SkillHub CLI //')
if [ "$RUNTIME_VERSION" != "${{ steps.extract.outputs.version }}" ]; then
echo "Version mismatch: runtime=$RUNTIME_VERSION, tag=${{ steps.extract.outputs.version }}"
exit 1
fi
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: cli-dist
path: |
cli/dist/
cli/package.json
cli/README.md
cli/LICENSE
retention-days: 7
publish-npm:
needs: build-and-test
runs-on: ubuntu-latest
if: ${{ !inputs.skip_npm }}
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.13
- name: Set version from tag
working-directory: cli
run: |
VERSION="${{ needs.build-and-test.outputs.version }}"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '$VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
- name: Install dependencies
working-directory: cli
run: bun install --frozen-lockfile
- name: Build CLI
working-directory: cli
run: bun run build
- name: Check if version exists on npm
id: check_npm
env:
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
run: |
PACKAGE_NAME="${{ needs.build-and-test.outputs.package_name }}"
VERSION="${{ needs.build-and-test.outputs.version }}"
if npm view "${PACKAGE_NAME}@${VERSION}" version --registry "$NPM_REGISTRY" 2>&1 | grep -Eiq '(E404|404 Not Found|is not in this registry|Not found)'; then
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Version $VERSION does not exist on registry, proceeding with publish"
else
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Version $VERSION already exists on registry, skipping publish"
fi
- name: Configure npm authentication
if: steps.check_npm.outputs.exists == 'false'
env:
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
REGISTRY_HOST="${NPM_REGISTRY#http://}"
REGISTRY_HOST="${REGISTRY_HOST#https://}"
REGISTRY_HOST="${REGISTRY_HOST%/}"
cat > ~/.npmrc <<EOF
registry=${NPM_REGISTRY}
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
always-auth=true
EOF
- name: Publish to npm
if: steps.check_npm.outputs.exists == 'false'
working-directory: cli
env:
NPM_REGISTRY: ${{ vars.NPM_REGISTRY || 'https://registry.npmjs.org' }}
run: |
npm publish --access public --registry "$NPM_REGISTRY"
echo "Published ${{ needs.build-and-test.outputs.package_name }}@${{ needs.build-and-test.outputs.version }}"
create-release:
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: cli-dist
path: cli-release
- name: Create release archives
run: |
VERSION="${{ needs.build-and-test.outputs.version }}"
# Create tar.gz
tar -czf "skillhub-cli-${VERSION}.tar.gz" -C cli-release .
# Create zip
(cd cli-release && zip -r "../skillhub-cli-${VERSION}.zip" .)
# Generate checksums
sha256sum "skillhub-cli-${VERSION}.tar.gz" > "skillhub-cli-${VERSION}.tar.gz.sha256"
sha256sum "skillhub-cli-${VERSION}.zip" > "skillhub-cli-${VERSION}.zip.sha256"
- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VERSION="${{ needs.build-and-test.outputs.version }}"
PACKAGE_NAME="${{ needs.build-and-test.outputs.package_name }}"
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "Release $TAG already exists, skipping"
exit 0
fi
# Generate release notes
cat > release-notes.md <<EOF
# SkillHub CLI ${VERSION}
## Installation
### npm
\`\`\`bash
npm install -g ${PACKAGE_NAME}@${VERSION}
\`\`\`
### From source
Download and extract the archive, then:
\`\`\`bash
npm install -g .
\`\`\`
## Verify installation
\`\`\`bash
skillhub version
\`\`\`
## Changes
See commit history for details.
EOF
gh release create "$TAG" \
--title "CLI ${VERSION}" \
--notes-file release-notes.md \
"skillhub-cli-${VERSION}.tar.gz" \
"skillhub-cli-${VERSION}.tar.gz.sha256" \
"skillhub-cli-${VERSION}.zip" \
"skillhub-cli-${VERSION}.zip.sha256"

View file

@ -1,4 +1,4 @@
.PHONY: help dev dev-all dev-down dev-all-down dev-all-reset dev-logs dev-status build test check clean web-deps web-install web-install-ci dev-server dev-server-restart dev-web build-backend test-backend build-frontend test-frontend test-e2e-frontend test-e2e-smoke-frontend build-web test-web typecheck-web lint-web generate-api db-reset namespace-smoke validate-release-config staging staging-down staging-logs pr parallel-init parallel-sync parallel-up parallel-down docs-dev docs-build docs-preview cli-install test-cli build-cli lint-cli typecheck-cli
.PHONY: build build-backend build-backend-app build-cli build-frontend build-web check clean cli-install db-reset dev dev-all dev-all-down dev-all-reset dev-down dev-logs dev-server dev-server-restart dev-status dev-web docs-build docs-dev docs-preview generate-api help lint-cli lint-web namespace-smoke parallel-down parallel-init parallel-sync parallel-up pr publish-cli publish-cli-major publish-cli-minor staging staging-down staging-logs test test-backend test-backend-app test-cli test-e2e-frontend test-e2e-smoke-frontend test-frontend test-web typecheck-cli typecheck-web validate-release-config web-deps web-install web-install-ci
DEV_DIR := .dev
DEV_SERVER_PID := $(DEV_DIR)/server.pid
@ -263,6 +263,9 @@ lint-web: ## 前端代码检查
# CLI 相关目标
cli-install: ## 安装 CLI 依赖
cd cli && bun install --frozen-lockfile
build-cli: ## 构建 CLI
cd cli && bun run build
@ -275,33 +278,15 @@ lint-cli: ## CLI 代码检查
typecheck-cli: ## CLI 类型检查
cd cli && bun run typecheck
publish-cli: ## 发布 CLI 到 npm(patch 版本)
@if [ ! -f cli/.env.local ]; then \
echo "Error: cli/.env.local not found."; \
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
exit 1; \
fi
publish-cli: ## 发布 CLI(patch 版本)- bump + tag + push,触发 CI 自动发布
./scripts/publish-cli.sh patch
publish-cli-minor: ## 发布 CLI 到 npm(minor 版本)
@if [ ! -f cli/.env.local ]; then \
echo "Error: cli/.env.local not found."; \
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
exit 1; \
fi
publish-cli-minor: ## 发布 CLI(minor 版本)- bump + tag + push,触发 CI 自动发布
./scripts/publish-cli.sh minor
publish-cli-major: ## 发布 CLI 到 npm(major 版本)
@if [ ! -f cli/.env.local ]; then \
echo "Error: cli/.env.local not found."; \
echo "Create cli/.env.local with NPM_TOKEN before publishing."; \
exit 1; \
fi
publish-cli-major: ## 发布 CLI(major 版本)- bump + tag + push,触发 CI 自动发布
./scripts/publish-cli.sh major
publish-cli-dry: ## 发布 CLI 到 npm(dry run)
DRY_RUN=true ./scripts/publish-cli.sh patch
db-reset: ## 重置数据库
$(DEV_COMPOSE) down -v --remove-orphans
$(DEV_COMPOSE) up -d --wait --remove-orphans postgres
@ -408,6 +393,3 @@ docs-build: ## 构建文档站点
docs-preview: ## 预览构建后的文档站点
cd docs/skillhub && npm run preview
cli-install: ## 安装 CLI 依赖
cd cli && bun install --frozen-lockfile

146
cli/RELEASE.md Normal file
View file

@ -0,0 +1,146 @@
# CLI Release Guide
## Overview
CLI releases are fully automated. Running `make publish-cli` on a clean `main` branch bumps the version, commits, creates a `cli-vX.Y.Z` tag, and pushes everything to origin. The GitHub Actions workflow [`release-cli.yml`](../.github/workflows/release-cli.yml) listens for the tag and handles build, test, npm publish, and GitHub Release creation.
## Prerequisites
### Repository Secrets
Configure in GitHub repository → Settings → Secrets and variables → Actions:
- `NPM_TOKEN`: npm token with publish permissions
- Generate at https://www.npmjs.com/settings/YOUR_USERNAME/tokens
- Use **Classic Automation Token** (bypasses 2FA automatically), or
- **Granular Access Token** with "Allow bypass 2FA" enabled, scoped to the package
### Repository Variables (optional)
- `NPM_REGISTRY`: npm registry URL (default: `https://registry.npmjs.org`)
### Local Environment
- `node` and `npm` installed (the script uses `npm version` to bump)
- `git` installed with push access to the repository
- On the `main` branch with a clean working tree
### Package Configuration
In [`cli/package.json`](./package.json):
```json
{
"name": "@astron-team/skillhub",
"publishConfig": {
"access": "public"
}
}
```
## Release Process
### One-shot Release
From the repository root, on a clean `main` branch:
```bash
make publish-cli # patch: 0.1.5 -> 0.1.6
make publish-cli-minor # minor: 0.1.5 -> 0.2.0
make publish-cli-major # major: 0.1.5 -> 1.0.0
```
[`scripts/publish-cli.sh`](../scripts/publish-cli.sh) performs the following steps:
1. Verify the working tree is clean
2. Require the current branch to be `main`, otherwise abort
3. `git pull --ff-only` from `origin/main`
4. Fetch remote tags and align `package.json` with the latest `cli-v*` tag
5. Compute the new version via `npm version <bump>`
6. Verify the new tag does not exist locally or on origin
7. After interactive confirmation: commit the bump, create the `cli-vX.Y.Z` tag, push both commit and tag to origin
Pushing the tag triggers CI — no further manual action required.
### CI Workflow
[`release-cli.yml`](../.github/workflows/release-cli.yml) contains three jobs:
1. **build-and-test**
- Extract version from tag name (`cli-v0.1.6` → `0.1.6`) and write it into `cli/package.json`
- Install deps, lint, typecheck, test, build
- Verify the built CLI's runtime version matches the tag
2. **publish-npm**
- Skip if the target version already exists on the registry
- Configure `~/.npmrc` and run `npm publish --access public`
3. **create-release**
- Package `dist/` + README + LICENSE as `tar.gz` and `zip`
- Generate SHA256 checksums
- Create a GitHub Release and upload artifacts
### Verify Release
- Workflow: https://github.com/iflytek/skillhub/actions/workflows/release-cli.yml
- Release: https://github.com/iflytek/skillhub/releases
- npm: `npm view @astron-team/skillhub@<version>`
## Release Audit Trail
GitHub Actions automatically records on each workflow run page:
- **Triggering user** (the developer who pushed the tag, i.e. `github.actor`)
- **Trigger event** (`push` tag or `workflow_dispatch`)
- **Tag name and commit SHA**
The team can review the full audit trail in the Actions tab without any extra configuration.
## Manual Trigger
From the Actions UI:
1. Actions → Release CLI → "Run workflow"
2. Enter an existing tag name matching `cli-vX.Y.Z`
3. Optionally enable skip npm publish
## Troubleshooting
### `releases must be cut from 'main'`
Switch back to `main`, pull the latest, and retry.
### `git working tree is not clean`
Commit or stash local changes first.
### `tag cli-vX.Y.Z already exists`
The previous release didn't clean up, or someone else released the same version. Check `git tag --list 'cli-v*'` and remote tags, then retry with a higher version.
### npm Publish Fails
- **403 with 2FA message**: `NPM_TOKEN` is not an Automation Token, or bypass 2FA is not enabled — regenerate with the correct type
- **403 Forbidden**: Package scope doesn't match token permissions — confirm publish rights for the `@astron-team` org
- **E404**: The registry doesn't host this scope — check `NPM_REGISTRY`
### Build / Test Fails
Reproduce locally:
```bash
make lint-cli && make typecheck-cli && make test-cli && make build-cli
```
Confirm the Bun version matches `packageManager` in [`cli/package.json`](./package.json).
### Version Mismatch (runtime ≠ tag)
CI runs `node dist/index.js version` and requires the output to match the tag. If the CLI's `version` command implementation changes, update the verification logic in [`release-cli.yml`](../.github/workflows/release-cli.yml) accordingly.
## Tag Naming Convention
- CLI releases: `cli-v*` (e.g., `cli-v0.1.6`)
- Repository releases: `v*` (e.g., `v0.3.0`)
The two tag namespaces are independent, allowing CLI and server to version separately.

View file

@ -1,20 +1,26 @@
#!/usr/bin/env bash
# Release entrypoint for the SkillHub CLI.
#
# This script bumps cli/package.json, commits the bump, creates a `cli-vX.Y.Z`
# tag, and pushes it. The GitHub Actions workflow `release-cli.yml` picks up
# the tag and performs the actual build + npm publish + GitHub Release.
#
# Prefer this over direct `npm publish`: avoids local network/TLS issues with
# registry.npmjs.org, and keeps release provenance tied to CI.
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
echo "Usage: $0 [patch|minor|major]" >&2
}
confirm() {
@ -24,222 +30,90 @@ 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_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
if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" ]]; 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
echo "git working tree is not clean — commit or stash changes first" >&2
exit 1
fi
if [[ "$BUMP_TYPE" == "skip" ]]; then
NEW_VERSION="$PACKAGE_VERSION"
else
NEW_VERSION="$(next_package_version "$PACKAGE_VERSION" "$BUMP_TYPE")"
CURRENT_BRANCH="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD)"
if [[ "$CURRENT_BRANCH" != "main" ]]; then
echo "releases must be cut from 'main' (current: '$CURRENT_BRANCH')" >&2
exit 1
fi
log_stage "checking registry version"
assert_version_not_published "$PACKAGE_NAME" "$NEW_VERSION"
log_stage "pulling latest from origin/$CURRENT_BRANCH"
git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH"
if [[ "$BUMP_TYPE" != "skip" ]]; then
if ! confirm "Proceed with version bump ($BUMP_TYPE)?"; then
echo "version bump cancelled" >&2
exit 1
log_stage "fetching tags from origin"
git -C "$REPO_ROOT" fetch --tags --prune origin
log_stage "resolving baseline version from latest cli-v* tag"
LATEST_TAG="$(git -C "$REPO_ROOT" tag --list 'cli-v*' --sort=-version:refname | head -n1)"
if [[ -n "$LATEST_TAG" ]]; then
BASE_VERSION="${LATEST_TAG#cli-v}"
CURRENT_PKG_VERSION="$(node -p "require('$PACKAGE_JSON').version")"
if [[ "$BASE_VERSION" != "$CURRENT_PKG_VERSION" ]]; then
log_stage "syncing package.json $CURRENT_PKG_VERSION -> $BASE_VERSION (from $LATEST_TAG)"
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('$PACKAGE_JSON', 'utf8'));
pkg.version = '$BASE_VERSION';
fs.writeFileSync('$PACKAGE_JSON', JSON.stringify(pkg, null, 2) + '\n');
"
fi
log_stage "bumping version ($BUMP_TYPE)"
(
cd "$CLI_DIR"
npm version "$BUMP_TYPE" --no-git-tag-version
)
else
log_stage "no cli-v* tags found, bumping from package.json"
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
log_stage "bumping version ($BUMP_TYPE)"
NPM_VERSION_OUTPUT="$(cd "$CLI_DIR" && npm version "$BUMP_TYPE" --no-git-tag-version)"
NEW_VERSION="${NPM_VERSION_OUTPUT#v}"
if [[ -z "$NEW_VERSION" ]]; then
echo "failed to parse version from npm output: $NPM_VERSION_OUTPUT" >&2
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
exit 1
fi
log_stage "running preflight build"
(
cd "$CLI_DIR"
bun run build
)
TAG="cli-v${NEW_VERSION}"
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
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
echo "tag $TAG already exists locally" >&2
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
exit 1
fi
if ! confirm "Publish $PACKAGE_NAME@$NEW_VERSION to $NPM_REGISTRY?"; then
echo "publish cancelled" >&2
exit 2
if git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$TAG" >/dev/null 2>&1; then
echo "tag $TAG already exists on origin" >&2
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
exit 1
fi
NPM_CONFIG_FILE="$(mktemp)"
cleanup() {
rm -f "$NPM_CONFIG_FILE"
}
trap cleanup EXIT
log_stage "new version: $NEW_VERSION (tag: $TAG)"
REGISTRY_HOST="${NPM_REGISTRY#http://}"
REGISTRY_HOST="${REGISTRY_HOST#https://}"
REGISTRY_HOST="${REGISTRY_HOST%/}"
if ! confirm "Commit, tag, and push $TAG to origin?"; then
echo "release cancelled — reverting package.json" >&2
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
exit 1
fi
cat >"$NPM_CONFIG_FILE" <<EOF
registry=${NPM_REGISTRY}
//${REGISTRY_HOST}/:_authToken=${NPM_TOKEN}
always-auth=true
EOF
log_stage "committing version bump"
git -C "$REPO_ROOT" add "$PACKAGE_JSON"
git -C "$REPO_ROOT" commit -m "chore(cli): bump version to $NEW_VERSION"
log_stage "publishing package"
(
cd "$CLI_DIR"
NPM_CONFIG_USERCONFIG="$NPM_CONFIG_FILE" npm publish --access public --registry "$NPM_REGISTRY"
)
log_stage "creating tag $TAG"
git -C "$REPO_ROOT" tag "$TAG"
log_stage "publish completed"
log_stage "pushing commit and tag to origin"
git -C "$REPO_ROOT" push origin "$CURRENT_BRANCH"
git -C "$REPO_ROOT" push origin "$TAG"
log_stage "release triggered — CI workflow will build and publish"
log_stage "watch progress at: https://github.com/iflytek/skillhub/actions/workflows/release-cli.yml"