diff --git a/cli/RELEASE.md b/cli/RELEASE.md index f260531b..b0d5e530 100644 --- a/cli/RELEASE.md +++ b/cli/RELEASE.md @@ -5,7 +5,7 @@ CLI releases use a PR-based flow. Running `make publish-cli` on a clean `main` branch: 1. Runs local build-and-test (lint, typecheck, test, build) -2. Computes the next version from the latest `cli-v*` tag +2. Computes the next version from the latest `cli-v*` tag on `origin` 3. Creates a `release/cli-vX.Y.Z` branch with the version bump committed 4. Pushes the branch and opens a PR to `main` @@ -64,7 +64,7 @@ make publish-cli-major # major: 0.1.5 -> 1.0.0 2. Verify the working tree is clean and on `main` 3. `git pull --ff-only` from `origin/main` 4. Run full local build-and-test (lint, typecheck, test, build) -5. Compute the next version from the latest `cli-v*` tag +5. Compute the next version from the latest `cli-v*` tag on `origin` (via `git ls-remote`, so local orphan tags from a failed `git push origin cli-vX.Y.Z` are ignored) 6. Verify the tag and release branch don't already exist 7. After interactive confirmation: create release branch, commit version bump, push, and open PR diff --git a/scripts/publish-cli.sh b/scripts/publish-cli.sh index 822038d6..9da9f45a 100755 --- a/scripts/publish-cli.sh +++ b/scripts/publish-cli.sh @@ -131,26 +131,31 @@ ORIGINAL_BRANCH="$CURRENT_BRANCH" log_stage "pulling latest from origin/$CURRENT_BRANCH" git -C "$REPO_ROOT" pull --ff-only origin "$CURRENT_BRANCH" -log_stage "fetching tags from origin" -git -C "$REPO_ROOT" fetch --tags --prune origin - # --- 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 cli-v* tag" -LATEST_TAG="$(git -C "$REPO_ROOT" tag --list 'cli-v*' --sort=-version:refname | head -n1)" +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 tag $LATEST_TAG contains prerelease suffix: $BASE_VERSION" >&2 + 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 $LATEST_TAG)" + 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 found, baseline: $BASE_VERSION (from package.json)" + 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 " diff --git a/scripts/tests/publish-cli-test.sh b/scripts/tests/publish-cli-test.sh index cee5e54a..c389e044 100755 --- a/scripts/tests/publish-cli-test.sh +++ b/scripts/tests/publish-cli-test.sh @@ -342,7 +342,7 @@ REPO8="$(new_tmp)" init_repo "$REPO8" "0.1.0" "cli-v0.2.0" status="$(run_publish "$REPO8" "patch" $'y\n')" [[ "$status" -eq 0 ]] || { cat "$REPO8/stderr.log" >&2; fail "expected success, got $status"; } -grep -F "baseline: 0.2.0 (from cli-v0.2.0)" "$REPO8/stdout.log" >/dev/null \ +grep -F "baseline: 0.2.0 (from origin cli-v0.2.0)" "$REPO8/stdout.log" >/dev/null \ || fail "baseline log missing — version computed from wrong source" git -C "$REPO8.origin.git" rev-parse "refs/heads/release/cli-v0.2.1" >/dev/null \ || fail "expected branch release/cli-v0.2.1 on origin" @@ -424,4 +424,21 @@ grep -F "contains prerelease suffix" "$REPO11/stderr.log" >/dev/null \ grep -F "only supports pure X.Y.Z" "$REPO11/stderr.log" >/dev/null \ || fail "expected X.Y.Z hint in error" +# ---------------------------------------------------------------------------- +# Test 12: local-only orphan tag must not influence baseline +# ---------------------------------------------------------------------------- +# Simulates the failure mode where `git push origin cli-vX.Y.Z` failed after +# `git tag cli-vX.Y.Z origin/main` succeeded locally. The orphan tag exists +# locally but not on origin. Baseline must come from origin only. +echo "[test] local-only orphan tag ignored for baseline" +REPO12="$(new_tmp)" +init_repo "$REPO12" "0.1.0" "cli-v0.1.0" +git -C "$REPO12" tag "cli-v0.3.0" +status="$(run_publish "$REPO12" "patch" $'y\n')" +[[ "$status" -eq 0 ]] || { cat "$REPO12/stderr.log" >&2; fail "expected success, got $status"; } +grep -F "baseline: 0.1.0 (from origin cli-v0.1.0)" "$REPO12/stdout.log" >/dev/null \ + || { cat "$REPO12/stdout.log" >&2; fail "baseline must come from origin (0.1.0), not local orphan (0.3.0)"; } +git -C "$REPO12.origin.git" rev-parse "refs/heads/release/cli-v0.1.1" >/dev/null \ + || fail "expected branch release/cli-v0.1.1 on origin (orphan should not have shifted target to 0.3.1)" + echo "all tests passed"