mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-07 02:57:51 +00:00
refactor(cli): improve publish-cli script reliability
- Move version computation and pre-flight checks before build-and-test to fail fast on conflicts (existing branch/tag) instead of wasting minutes on lint/test/build - Add INT/TERM signal handlers to cleanup trap so Ctrl+C during build properly restores working tree state - Update Makefile help text to reflect PR-based workflow
This commit is contained in:
parent
356e507cf9
commit
dda8426fb3
2 changed files with 199 additions and 91 deletions
6
Makefile
6
Makefile
|
|
@ -278,13 +278,13 @@ lint-cli: ## CLI 代码检查
|
|||
typecheck-cli: ## CLI 类型检查
|
||||
cd cli && bun run typecheck
|
||||
|
||||
publish-cli: ## 发布 CLI(patch 版本)- bump + tag + push,触发 CI 自动发布
|
||||
publish-cli: ## 发布 CLI(patch 版本)- 本地 build+test → 推 release 分支 → 开 PR,合并后手动 tag 触发 CI
|
||||
./scripts/publish-cli.sh patch
|
||||
|
||||
publish-cli-minor: ## 发布 CLI(minor 版本)- bump + tag + push,触发 CI 自动发布
|
||||
publish-cli-minor: ## 发布 CLI(minor 版本)- 本地 build+test → 推 release 分支 → 开 PR,合并后手动 tag 触发 CI
|
||||
./scripts/publish-cli.sh minor
|
||||
|
||||
publish-cli-major: ## 发布 CLI(major 版本)- bump + tag + push,触发 CI 自动发布
|
||||
publish-cli-major: ## 发布 CLI(major 版本)- 本地 build+test → 推 release 分支 → 开 PR,合并后手动 tag 触发 CI
|
||||
./scripts/publish-cli.sh major
|
||||
|
||||
db-reset: ## 重置数据库
|
||||
|
|
|
|||
|
|
@ -2,12 +2,10 @@
|
|||
|
||||
# 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.
|
||||
# Runs local build-and-test (lint, typecheck, test, build), bumps the version
|
||||
# in cli/package.json, pushes a release branch, and opens a PR to main.
|
||||
# Tagging is done manually after the PR is merged — the tag push triggers
|
||||
# `release-cli.yml` which builds and publishes to npm.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
|
@ -36,6 +34,88 @@ if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "maj
|
|||
exit 1
|
||||
fi
|
||||
|
||||
# --- Cleanup state machine ---
|
||||
#
|
||||
# Tracks how far we got so the ERR trap can roll back the right pieces.
|
||||
# Stages advance monotonically; the trap inspects this to decide what to undo.
|
||||
#
|
||||
# pre-branch — nothing created yet
|
||||
# on-release — checked out release branch (may have uncommitted edits)
|
||||
# committed — version bump committed locally, not yet pushed
|
||||
# pushed — release branch pushed to origin (PR not yet open)
|
||||
# pr-opened — PR opened (terminal success state, trap is a no-op)
|
||||
#
|
||||
CLEANUP_STAGE="pre-branch"
|
||||
ORIGINAL_BRANCH=""
|
||||
RELEASE_BRANCH=""
|
||||
|
||||
cleanup_on_error() {
|
||||
local exit_code=$?
|
||||
trap - ERR EXIT INT TERM
|
||||
set +e
|
||||
|
||||
# When triggered by a signal with no failed command, $? may be 0; force a
|
||||
# non-zero exit so the caller sees the interruption.
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
exit_code=130
|
||||
fi
|
||||
|
||||
case "$CLEANUP_STAGE" in
|
||||
pre-branch)
|
||||
# build-and-test runs `bun run lint/typecheck/build`, all of which
|
||||
# regenerate cli/src/generated/pkg-info.ts via their pre-* hooks. If
|
||||
# we got interrupted mid-flight, restore it so the working tree is
|
||||
# clean for the next attempt.
|
||||
git -C "$REPO_ROOT" checkout -- "$CLI_DIR/src/generated/pkg-info.ts" 2>/dev/null
|
||||
;;
|
||||
pr-opened)
|
||||
# Already succeeded.
|
||||
;;
|
||||
on-release)
|
||||
echo "" >&2
|
||||
echo "[publish-cli] error before commit — rolling back release branch" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON" "$CLI_DIR/src/generated/pkg-info.ts" 2>/dev/null
|
||||
git -C "$REPO_ROOT" checkout "$ORIGINAL_BRANCH" 2>/dev/null
|
||||
git -C "$REPO_ROOT" branch -D "$RELEASE_BRANCH" 2>/dev/null
|
||||
;;
|
||||
committed)
|
||||
echo "" >&2
|
||||
echo "[publish-cli] error after commit but before push — rolling back local branch" >&2
|
||||
git -C "$REPO_ROOT" checkout "$ORIGINAL_BRANCH" 2>/dev/null
|
||||
git -C "$REPO_ROOT" branch -D "$RELEASE_BRANCH" 2>/dev/null
|
||||
;;
|
||||
pushed)
|
||||
echo "" >&2
|
||||
echo "ERROR: release branch '$RELEASE_BRANCH' was pushed but PR creation failed." >&2
|
||||
echo "" >&2
|
||||
echo "To recover:" >&2
|
||||
echo "" >&2
|
||||
echo " 1. Open the PR manually:" >&2
|
||||
echo " gh pr create --base main --head $RELEASE_BRANCH" >&2
|
||||
echo "" >&2
|
||||
echo " 2. Or roll back:" >&2
|
||||
echo " git checkout $ORIGINAL_BRANCH" >&2
|
||||
echo " git branch -D $RELEASE_BRANCH" >&2
|
||||
echo " git push origin --delete $RELEASE_BRANCH" >&2
|
||||
echo "" >&2
|
||||
;;
|
||||
esac
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
trap cleanup_on_error ERR INT TERM
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "gh CLI is required (https://cli.github.com)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! gh auth status >/dev/null 2>&1; then
|
||||
echo "gh CLI is not authenticated. Run: gh auth login" >&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 — commit or stash changes first" >&2
|
||||
|
|
@ -47,6 +127,7 @@ if [[ "$CURRENT_BRANCH" != "main" ]]; then
|
|||
echo "releases must be cut from 'main' (current: '$CURRENT_BRANCH')" >&2
|
||||
exit 1
|
||||
fi
|
||||
ORIGINAL_BRANCH="$CURRENT_BRANCH"
|
||||
|
||||
log_stage "pulling latest from origin/$CURRENT_BRANCH"
|
||||
git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH"
|
||||
|
|
@ -54,117 +135,144 @@ git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH"
|
|||
log_stage "fetching tags from origin"
|
||||
git -C "$REPO_ROOT" fetch --tags --prune origin
|
||||
|
||||
log_stage "checking for unpushed release artifacts"
|
||||
UNPUSHED_COMMITS="$(git -C "$REPO_ROOT" log --oneline origin/"$CURRENT_BRANCH"..HEAD 2>/dev/null || true)"
|
||||
# --- Compute version & pre-flight checks (cheap, run before build) ---
|
||||
|
||||
# Detect local cli-v* tags that don't exist on origin.
|
||||
# This catches both: (a) commit not pushed + tag not pushed, and
|
||||
# (b) commit pushed but tag push failed (where --no-merged would miss it).
|
||||
UNPUSHED_RELEASE_TAGS=""
|
||||
while IFS= read -r local_tag; do
|
||||
[[ -z "$local_tag" ]] && continue
|
||||
if ! git -C "$REPO_ROOT" ls-remote --exit-code --tags origin "refs/tags/$local_tag" >/dev/null 2>&1; then
|
||||
UNPUSHED_RELEASE_TAGS="${UNPUSHED_RELEASE_TAGS:+$UNPUSHED_RELEASE_TAGS
|
||||
}$local_tag"
|
||||
fi
|
||||
done < <(git -C "$REPO_ROOT" tag --list 'cli-v*' 2>/dev/null)
|
||||
|
||||
if [[ -n "$UNPUSHED_COMMITS" ]] || [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo "" >&2
|
||||
echo "ERROR: Detected unpushed release artifacts from a previous failed push:" >&2
|
||||
echo "" >&2
|
||||
|
||||
if [[ -n "$UNPUSHED_COMMITS" ]]; then
|
||||
echo " Unpushed commits:" >&2
|
||||
echo "$UNPUSHED_COMMITS" | sed 's/^/ /' >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo " Unpushed tags:" >&2
|
||||
echo "$UNPUSHED_RELEASE_TAGS" | sed 's/^/ /' >&2
|
||||
echo "" >&2
|
||||
fi
|
||||
|
||||
echo "Choose a recovery option:" >&2
|
||||
echo "" >&2
|
||||
echo " 1. Retry push (if the previous failure was temporary, e.g., network issue):" >&2
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
FIRST_TAG="$(echo "$UNPUSHED_RELEASE_TAGS" | head -n1)"
|
||||
echo " git push origin $CURRENT_BRANCH $FIRST_TAG" >&2
|
||||
else
|
||||
echo " git push origin $CURRENT_BRANCH" >&2
|
||||
fi
|
||||
echo "" >&2
|
||||
echo " 2. Rollback and retry release (if you want to start fresh):" >&2
|
||||
if [[ -n "$UNPUSHED_RELEASE_TAGS" ]]; then
|
||||
echo " git tag -d $UNPUSHED_RELEASE_TAGS" | tr '\n' ' ' | sed 's/ $/\n/' >&2
|
||||
fi
|
||||
echo " git reset --hard origin/$CURRENT_BRANCH" >&2
|
||||
echo " # Then re-run: make publish-cli" >&2
|
||||
echo "" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "resolving baseline version from latest cli-v* tag"
|
||||
log_stage "computing next 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 "baseline: $BASE_VERSION (from $LATEST_TAG)"
|
||||
else
|
||||
log_stage "no cli-v* tags found, bumping from package.json"
|
||||
BASE_VERSION="$(node -p "require('$PACKAGE_JSON').version")"
|
||||
log_stage "no cli-v* tags found, baseline: $BASE_VERSION (from package.json)"
|
||||
fi
|
||||
|
||||
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}"
|
||||
NEW_VERSION="$(node -e "
|
||||
const v = '$BASE_VERSION'.split('.').map(Number);
|
||||
if (v.length !== 3 || v.some(Number.isNaN)) {
|
||||
console.error('invalid baseline version: $BASE_VERSION');
|
||||
process.exit(1);
|
||||
}
|
||||
const t = '$BUMP_TYPE';
|
||||
if (t === 'patch') v[2]++;
|
||||
else if (t === 'minor') { v[1]++; v[2] = 0; }
|
||||
else if (t === 'major') { v[0]++; v[1] = 0; v[2] = 0; }
|
||||
console.log(v.join('.'));
|
||||
")"
|
||||
|
||||
if [[ -z "$NEW_VERSION" ]]; then
|
||||
echo "failed to parse version from npm output: $NPM_VERSION_OUTPUT" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
echo "failed to compute new version from baseline $BASE_VERSION" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="cli-v${NEW_VERSION}"
|
||||
RELEASE_BRANCH="release/${TAG}"
|
||||
|
||||
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 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
|
||||
|
||||
log_stage "new version: $NEW_VERSION (tag: $TAG)"
|
||||
|
||||
if ! confirm "Commit, tag, and push $TAG to origin?"; then
|
||||
echo "release cancelled — reverting package.json" >&2
|
||||
git -C "$REPO_ROOT" checkout -- "$PACKAGE_JSON"
|
||||
if git -C "$REPO_ROOT" rev-parse -q --verify "refs/heads/$RELEASE_BRANCH" >/dev/null; then
|
||||
echo "branch $RELEASE_BRANCH already exists locally" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git -C "$REPO_ROOT" ls-remote --exit-code --heads origin "refs/heads/$RELEASE_BRANCH" >/dev/null 2>&1; then
|
||||
echo "branch $RELEASE_BRANCH already exists on origin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_stage "target: $NEW_VERSION (branch: $RELEASE_BRANCH)"
|
||||
|
||||
# --- Local build-and-test ---
|
||||
|
||||
log_stage "installing dependencies"
|
||||
(cd "$CLI_DIR" && bun install --frozen-lockfile)
|
||||
|
||||
log_stage "running lint"
|
||||
(cd "$CLI_DIR" && bun run lint)
|
||||
|
||||
log_stage "running typecheck"
|
||||
(cd "$CLI_DIR" && bun run typecheck)
|
||||
|
||||
log_stage "running tests"
|
||||
(cd "$CLI_DIR" && bun test)
|
||||
|
||||
log_stage "running build"
|
||||
(cd "$CLI_DIR" && bun run build)
|
||||
|
||||
log_stage "build-and-test passed"
|
||||
|
||||
# Reset only the codegen file that build-and-test regenerates. We rewrite
|
||||
# pkg-info.ts again after the version bump, so this just keeps the working
|
||||
# tree clean before branching.
|
||||
git -C "$REPO_ROOT" checkout -- "$CLI_DIR/src/generated/pkg-info.ts"
|
||||
|
||||
if ! confirm "Push $RELEASE_BRANCH and open PR to main?"; then
|
||||
echo "release cancelled" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Create branch, bump, push, open PR ---
|
||||
|
||||
log_stage "creating release branch $RELEASE_BRANCH"
|
||||
git -C "$REPO_ROOT" checkout -b "$RELEASE_BRANCH"
|
||||
CLEANUP_STAGE="on-release"
|
||||
|
||||
log_stage "writing version $NEW_VERSION to package.json"
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const pkg = JSON.parse(fs.readFileSync('$PACKAGE_JSON', 'utf8'));
|
||||
pkg.version = '$NEW_VERSION';
|
||||
fs.writeFileSync('$PACKAGE_JSON', JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
|
||||
log_stage "regenerating pkg-info.ts with new version"
|
||||
(cd "$CLI_DIR" && bun run scripts/generate-pkg-info.ts)
|
||||
|
||||
log_stage "committing version bump"
|
||||
git -C "$REPO_ROOT" add "$PACKAGE_JSON"
|
||||
git -C "$REPO_ROOT" add "$PACKAGE_JSON" "$CLI_DIR/src/generated/pkg-info.ts"
|
||||
git -C "$REPO_ROOT" commit -m "chore(cli): bump version to $NEW_VERSION"
|
||||
CLEANUP_STAGE="committed"
|
||||
|
||||
log_stage "creating tag $TAG"
|
||||
git -C "$REPO_ROOT" tag "$TAG"
|
||||
log_stage "pushing release branch to origin"
|
||||
git -C "$REPO_ROOT" push -u origin "$RELEASE_BRANCH"
|
||||
CLEANUP_STAGE="pushed"
|
||||
|
||||
log_stage "pushing commit and tag to origin (atomic)"
|
||||
git -C "$REPO_ROOT" push --atomic origin "$CURRENT_BRANCH" "$TAG"
|
||||
log_stage "opening pull request"
|
||||
PR_BODY="Bumps CLI version to \`$NEW_VERSION\`.
|
||||
|
||||
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"
|
||||
Local build-and-test passed (lint, typecheck, test, build).
|
||||
|
||||
After merging, tag and push to trigger the release:
|
||||
\`\`\`bash
|
||||
git pull origin main
|
||||
git tag $TAG
|
||||
git push origin $TAG
|
||||
\`\`\`
|
||||
|
||||
Watch the release: https://github.com/iflytek/skillhub/actions/workflows/release-cli.yml"
|
||||
|
||||
gh pr create \
|
||||
--base main \
|
||||
--head "$RELEASE_BRANCH" \
|
||||
--title "chore(cli): release $NEW_VERSION" \
|
||||
--body "$PR_BODY"
|
||||
CLEANUP_STAGE="pr-opened"
|
||||
|
||||
log_stage "returning to $CURRENT_BRANCH"
|
||||
git -C "$REPO_ROOT" checkout "$CURRENT_BRANCH"
|
||||
git -C "$REPO_ROOT" branch -D "$RELEASE_BRANCH"
|
||||
|
||||
log_stage "done — PR opened. After merge, tag manually:"
|
||||
echo ""
|
||||
echo " git pull origin main"
|
||||
echo " git tag $TAG"
|
||||
echo " git push origin $TAG"
|
||||
echo ""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue