chore: add release train workflow
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# shellcheck source=./release-lib.sh
|
||||
. "$REPO_ROOT/scripts/release-lib.sh"
|
||||
export GIT_PAGER=cat
|
||||
|
||||
channel=""
|
||||
@@ -18,7 +20,9 @@ Examples:
|
||||
|
||||
What it does:
|
||||
- verifies the git worktree is clean, including untracked files
|
||||
- verifies you are on the matching release/X.Y.Z branch
|
||||
- shows the last stable tag and the target version(s)
|
||||
- shows the git/npm/GitHub release-train state
|
||||
- shows commits since the last stable tag
|
||||
- highlights migration/schema/breaking-change signals
|
||||
- runs the verification gate:
|
||||
@@ -63,79 +67,19 @@ if [[ ! "$bump_type" =~ ^(patch|minor|major)$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
compute_bumped_version() {
|
||||
node - "$1" "$2" <<'NODE'
|
||||
const current = process.argv[2];
|
||||
const bump = process.argv[3];
|
||||
const match = current.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error(`invalid semver version: ${current}`);
|
||||
}
|
||||
|
||||
let [major, minor, patch] = match.slice(1).map(Number);
|
||||
|
||||
if (bump === 'patch') {
|
||||
patch += 1;
|
||||
} else if (bump === 'minor') {
|
||||
minor += 1;
|
||||
patch = 0;
|
||||
} else if (bump === 'major') {
|
||||
major += 1;
|
||||
minor = 0;
|
||||
patch = 0;
|
||||
} else {
|
||||
throw new Error(`unsupported bump type: ${bump}`);
|
||||
}
|
||||
|
||||
process.stdout.write(`${major}.${minor}.${patch}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
next_canary_version() {
|
||||
local stable_version="$1"
|
||||
local versions_json
|
||||
|
||||
versions_json="$(npm view paperclipai versions --json 2>/dev/null || echo '[]')"
|
||||
|
||||
node - "$stable_version" "$versions_json" <<'NODE'
|
||||
const stable = process.argv[2];
|
||||
const versionsArg = process.argv[3];
|
||||
|
||||
let versions = [];
|
||||
try {
|
||||
const parsed = JSON.parse(versionsArg);
|
||||
versions = Array.isArray(parsed) ? parsed : [parsed];
|
||||
} catch {
|
||||
versions = [];
|
||||
}
|
||||
|
||||
const pattern = new RegExp(`^${stable.replace(/\./g, '\\.')}-canary\\.(\\d+)$`);
|
||||
let max = -1;
|
||||
|
||||
for (const version of versions) {
|
||||
const match = version.match(pattern);
|
||||
if (!match) continue;
|
||||
max = Math.max(max, Number(match[1]));
|
||||
}
|
||||
|
||||
process.stdout.write(`${stable}-canary.${max + 1}`);
|
||||
NODE
|
||||
}
|
||||
|
||||
LAST_STABLE_TAG="$(git -C "$REPO_ROOT" tag --list 'v*' --sort=-version:refname | head -1)"
|
||||
CURRENT_STABLE_VERSION="${LAST_STABLE_TAG#v}"
|
||||
if [ -z "$CURRENT_STABLE_VERSION" ]; then
|
||||
CURRENT_STABLE_VERSION="0.0.0"
|
||||
fi
|
||||
RELEASE_REMOTE="$(resolve_release_remote)"
|
||||
fetch_release_remote "$RELEASE_REMOTE"
|
||||
|
||||
LAST_STABLE_TAG="$(get_last_stable_tag)"
|
||||
CURRENT_STABLE_VERSION="$(get_current_stable_version)"
|
||||
TARGET_STABLE_VERSION="$(compute_bumped_version "$CURRENT_STABLE_VERSION" "$bump_type")"
|
||||
TARGET_CANARY_VERSION="$(next_canary_version "$TARGET_STABLE_VERSION")"
|
||||
EXPECTED_RELEASE_BRANCH="$(release_branch_name "$TARGET_STABLE_VERSION")"
|
||||
CURRENT_BRANCH="$(git_current_branch)"
|
||||
RELEASE_TAG="v$TARGET_STABLE_VERSION"
|
||||
NOTES_FILE="$(release_notes_file "$TARGET_STABLE_VERSION")"
|
||||
|
||||
if [ -n "$(git -C "$REPO_ROOT" status --porcelain)" ]; then
|
||||
echo "Error: working tree is not clean. Commit, stash, or remove changes before releasing." >&2
|
||||
exit 1
|
||||
fi
|
||||
require_clean_worktree
|
||||
|
||||
if [ "$TARGET_STABLE_VERSION" = "$CURRENT_STABLE_VERSION" ]; then
|
||||
echo "Error: next stable version matches the current stable version." >&2
|
||||
@@ -147,10 +91,41 @@ if [[ "$TARGET_CANARY_VERSION" == "${CURRENT_STABLE_VERSION}-canary."* ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ensure_release_branch_for_version "$TARGET_STABLE_VERSION"
|
||||
|
||||
REMOTE_BRANCH_EXISTS="no"
|
||||
REMOTE_TAG_EXISTS="no"
|
||||
LOCAL_TAG_EXISTS="no"
|
||||
NPM_STABLE_EXISTS="no"
|
||||
|
||||
if git_remote_branch_exists "$EXPECTED_RELEASE_BRANCH" "$RELEASE_REMOTE"; then
|
||||
REMOTE_BRANCH_EXISTS="yes"
|
||||
fi
|
||||
|
||||
if git_local_tag_exists "$RELEASE_TAG"; then
|
||||
LOCAL_TAG_EXISTS="yes"
|
||||
fi
|
||||
|
||||
if git_remote_tag_exists "$RELEASE_TAG" "$RELEASE_REMOTE"; then
|
||||
REMOTE_TAG_EXISTS="yes"
|
||||
fi
|
||||
|
||||
if npm_version_exists "$TARGET_STABLE_VERSION"; then
|
||||
NPM_STABLE_EXISTS="yes"
|
||||
fi
|
||||
|
||||
if [ "$LOCAL_TAG_EXISTS" = "yes" ] || [ "$REMOTE_TAG_EXISTS" = "yes" ] || [ "$NPM_STABLE_EXISTS" = "yes" ]; then
|
||||
echo "Error: release train $EXPECTED_RELEASE_BRANCH is frozen because $RELEASE_TAG already exists locally, remotely, or version $TARGET_STABLE_VERSION is already on npm." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Release preflight"
|
||||
echo " Remote: $RELEASE_REMOTE"
|
||||
echo " Channel: $channel"
|
||||
echo " Bump: $bump_type"
|
||||
echo " Current branch: ${CURRENT_BRANCH:-<detached>}"
|
||||
echo " Expected branch: $EXPECTED_RELEASE_BRANCH"
|
||||
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
||||
echo " Next stable version: $TARGET_STABLE_VERSION"
|
||||
@@ -162,6 +137,23 @@ fi
|
||||
echo ""
|
||||
echo "==> Working tree"
|
||||
echo " ✓ Clean"
|
||||
echo " ✓ Branch matches release train"
|
||||
|
||||
echo ""
|
||||
echo "==> Release train state"
|
||||
echo " Remote branch exists: $REMOTE_BRANCH_EXISTS"
|
||||
echo " Local stable tag exists: $LOCAL_TAG_EXISTS"
|
||||
echo " Remote stable tag exists: $REMOTE_TAG_EXISTS"
|
||||
echo " Stable version on npm: $NPM_STABLE_EXISTS"
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
echo " Release notes: present at $NOTES_FILE"
|
||||
else
|
||||
echo " Release notes: missing at $NOTES_FILE"
|
||||
fi
|
||||
|
||||
if [ "$REMOTE_BRANCH_EXISTS" = "no" ]; then
|
||||
echo " Warning: remote branch $EXPECTED_RELEASE_BRANCH does not exist on $RELEASE_REMOTE yet."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "==> Commits since last stable tag"
|
||||
@@ -193,8 +185,10 @@ pnpm build
|
||||
|
||||
echo ""
|
||||
echo "==> Release preflight summary"
|
||||
echo " Remote: $RELEASE_REMOTE"
|
||||
echo " Channel: $channel"
|
||||
echo " Bump: $bump_type"
|
||||
echo " Release branch: $EXPECTED_RELEASE_BRANCH"
|
||||
echo " Last stable tag: ${LAST_STABLE_TAG:-<none>}"
|
||||
echo " Current stable version: $CURRENT_STABLE_VERSION"
|
||||
echo " Next stable version: $TARGET_STABLE_VERSION"
|
||||
|
||||
Reference in New Issue
Block a user