mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +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 * fix(cli): use git checkout -f for robust cleanup Address code review feedback from gemini-code-assist bot: - Use `git checkout -f` in on-release and committed cleanup stages to ensure reliable branch switching even when files are staged but not committed (e.g., interrupted after `git add` but before `git commit`) - Remove redundant `git checkout -- <file>` in on-release stage since `-f` already discards all local changes This prevents cleanup failures when the script is interrupted between staging and committing. * fix(cli): address PR #441 review findings - Fix ERR trap bypass: remove `if !` wrapper around `gh pr create` so set -e triggers the trap and prints pushed-stage recovery instructions - Fix command injection: all node -e/-p calls now use process.env instead of interpolating shell variables into JS string literals - Rewrite cli/RELEASE.md to document the new PR-based release flow - Rewrite scripts/tests/publish-cli-test.sh with 10 tests covering the new flow (stubs for bun/gh, pre-flight checks, happy path, cleanup state machine stages) * fix(cli): address PR #441 review findings from @dongmucat - Bind release tag to origin/main: PR body, end-of-run hint, and cli/RELEASE.md now use `git tag $TAG origin/main` so the tag is always placed on the merged commit, regardless of local branch state - Reject prerelease tags in version computation: if the latest cli-v* tag contains non-X.Y.Z characters (e.g., -rc.1), exit with a clear message instead of crashing in node parsing - Add pr-scripts.yml workflow: runs publish-cli-test.sh on scripts/** changes so the release script regression suite gates PRs - Add Test 11 covering prerelease tag rejection * fix(cli): compute publish baseline from origin tags only A failed `git push origin cli-vX.Y.Z` after a successful local tag leaves an orphan tag locally. The previous `git tag --list` baseline would then treat it as the latest release, causing skipped versions or publishes based on an unreleased tag. Switch to `git ls-remote --tags --refs origin 'cli-v*' | sort -V` so the baseline reflects only what is actually on origin. Local orphan tags can still collide with the computed target tag, which fails fast with a clear message as before. Adds test 12 covering the orphan-tag scenario.
291 lines
9.1 KiB
Bash
Executable file
291 lines
9.1 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Release entrypoint for the SkillHub CLI.
|
|
#
|
|
# 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
|
|
|
|
REPO_ROOT="${REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
|
CLI_DIR="$REPO_ROOT/cli"
|
|
PACKAGE_JSON="$CLI_DIR/package.json"
|
|
|
|
log_stage() {
|
|
echo "[publish-cli] $1"
|
|
}
|
|
|
|
usage() {
|
|
echo "Usage: $0 [patch|minor|major]" >&2
|
|
}
|
|
|
|
confirm() {
|
|
local prompt="$1"
|
|
local answer
|
|
read -r -p "$prompt [y/N]: " answer
|
|
[[ "$answer" =~ ^[Yy]$ ]]
|
|
}
|
|
|
|
BUMP_TYPE="${1:-patch}"
|
|
if [[ "$BUMP_TYPE" != "patch" && "$BUMP_TYPE" != "minor" && "$BUMP_TYPE" != "major" ]]; then
|
|
usage
|
|
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 -f "$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 -f "$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
|
|
exit 1
|
|
fi
|
|
|
|
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
|
|
ORIGINAL_BRANCH="$CURRENT_BRANCH"
|
|
|
|
log_stage "pulling latest from origin/$CURRENT_BRANCH"
|
|
git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH"
|
|
|
|
# --- Compute version & pre-flight checks (cheap, run before build) ---
|
|
#
|
|
# Baseline is read from origin via `ls-remote`, not from local tags. A local
|
|
# orphan tag left over from a failed `git push origin cli-vX.Y.Z` must not
|
|
# influence the next version — otherwise we'd skip versions or release on top
|
|
# of something that was never published.
|
|
|
|
log_stage "computing next version from latest origin cli-v* tag"
|
|
LATEST_TAG="$(git -C "$REPO_ROOT" ls-remote --tags --refs origin 'cli-v*' \
|
|
| awk '{sub(/^refs\/tags\//, "", $2); print $2}' \
|
|
| sort -V \
|
|
| tail -n1)"
|
|
if [[ -n "$LATEST_TAG" ]]; then
|
|
BASE_VERSION="${LATEST_TAG#cli-v}"
|
|
# Reject prerelease tags (e.g., cli-v0.2.0-rc.1). Only pure X.Y.Z is supported.
|
|
if [[ "$BASE_VERSION" =~ [^0-9.] ]]; then
|
|
echo "latest origin tag $LATEST_TAG contains prerelease suffix: $BASE_VERSION" >&2
|
|
echo "this script only supports pure X.Y.Z versions" >&2
|
|
echo "skip prerelease tags manually or use a different baseline" >&2
|
|
exit 1
|
|
fi
|
|
log_stage "baseline: $BASE_VERSION (from origin $LATEST_TAG)"
|
|
else
|
|
BASE_VERSION="$(PACKAGE_JSON="$PACKAGE_JSON" node -p "require(process.env.PACKAGE_JSON).version")"
|
|
log_stage "no cli-v* tags on origin, baseline: $BASE_VERSION (from package.json)"
|
|
fi
|
|
|
|
NEW_VERSION="$(BASE_VERSION="$BASE_VERSION" BUMP_TYPE="$BUMP_TYPE" node -e "
|
|
const v = process.env.BASE_VERSION.split('.').map(Number);
|
|
if (v.length !== 3 || v.some(Number.isNaN)) {
|
|
console.error('invalid baseline version: ' + process.env.BASE_VERSION);
|
|
process.exit(1);
|
|
}
|
|
const t = process.env.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 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
|
|
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
|
|
exit 1
|
|
fi
|
|
|
|
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"
|
|
NEW_VERSION="$NEW_VERSION" PACKAGE_JSON="$PACKAGE_JSON" node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync(process.env.PACKAGE_JSON, 'utf8'));
|
|
pkg.version = process.env.NEW_VERSION;
|
|
fs.writeFileSync(process.env.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" "$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 "pushing release branch to origin"
|
|
git -C "$REPO_ROOT" push -u origin "$RELEASE_BRANCH"
|
|
CLEANUP_STAGE="pushed"
|
|
|
|
log_stage "opening pull request"
|
|
PR_BODY="Bumps CLI version to \`$NEW_VERSION\`.
|
|
|
|
Local build-and-test passed (lint, typecheck, test, build).
|
|
|
|
After merging, tag and push to trigger the release:
|
|
\`\`\`bash
|
|
git fetch origin main
|
|
git checkout main
|
|
git merge --ff-only origin/main
|
|
git tag $TAG origin/main
|
|
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 fetch origin main"
|
|
echo " git tag $TAG origin/main"
|
|
echo " git push origin $TAG"
|
|
echo ""
|