learning_ai_common_plat/scripts/release.sh

315 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# release.sh — Full release pipeline for learning_ai_common_plat
#
# Usage:
# ./scripts/release.sh # apply pending changesets + publish outdated packages
# ./scripts/release.sh --patch # auto-bump all packages (patch) + publish
# ./scripts/release.sh --minor # auto-bump all packages (minor) + publish
# ./scripts/release.sh --major # auto-bump all packages (major) + publish
# ./scripts/release.sh --dry-run # show what would be published, no side effects
#
# Required env:
# GITEA_NPM_TOKEN — auth token for the Gitea npm registry
#
# Optional env:
# GITEA_NPM_REGISTRY_URL — defaults to https://gitea.bytelyst.com/api/packages/ByteLyst/npm/
set -euo pipefail
# ── Config ─────────────────────────────────────────────────────────────────────
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-https://gitea.bytelyst.com/api/packages/ByteLyst/npm/}"
AUTH_TARGET="${REGISTRY_URL#http://}"
AUTH_TARGET="${AUTH_TARGET#https://}"
TOKEN="${GITEA_NPM_TOKEN:-}"
TMP_DIR="${TMPDIR:-/tmp}/bytelyst-release-$$"
# Workspace dirs that contain publishable npm packages
WORKSPACE_DIRS=("packages" "services" "dashboards")
# Native SDKs — not published to npm
SKIP_PACKAGES=("swift-platform-sdk" "swift-diagnostics" "kotlin-platform-sdk")
# Parse flags
BUMP_TYPE=""
DRY_RUN=false
for arg in "$@"; do
case "$arg" in
--patch|--minor|--major) BUMP_TYPE="${arg#--}" ;;
--dry-run) DRY_RUN=true ;;
*) echo "Unknown argument: $arg"; exit 1 ;;
esac
done
# ── Helpers ────────────────────────────────────────────────────────────────────
log() { echo "$*"; }
ok() { echo "$*"; }
warn() { echo "⚠️ $*"; }
fail() { echo "$*" >&2; exit 1; }
pkg_field() {
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
process.stdout.write(String(pkg['${1}'] ?? ''));
" "$2"
}
is_skip_package() {
local name="$1"
for skip in "${SKIP_PACKAGES[@]}"; do
if [[ "$name" == *"$skip"* ]]; then return 0; fi
done
return 1
}
version_on_registry() {
local name="$1" version="$2"
npm view "${name}@${version}" version \
--registry "$REGISTRY_URL" \
"--//${AUTH_TARGET}:_authToken=${TOKEN}" \
2>/dev/null || true
}
# ── Preflight checks ───────────────────────────────────────────────────────────
cd "$REPO_ROOT"
[ -z "$TOKEN" ] && fail "GITEA_NPM_TOKEN is not set"
command -v pnpm >/dev/null 2>&1 || fail "pnpm not found in PATH"
command -v git >/dev/null 2>&1 || fail "git not found in PATH"
if [ "$DRY_RUN" = true ]; then
log "Dry-run mode — no packages will be published, no commits will be made"
fi
# ── Phase 1: Pull-rebase from origin main ─────────────────────────────────────
log "Rebasing from origin main..."
STASHED=false
if ! git diff --quiet || ! git diff --cached --quiet; then
log "Stashing local changes before rebase..."
git stash push -u -m "release.sh: auto-stash before rebase"
STASHED=true
fi
git fetch origin main
if ! git rebase origin/main; then
[ "$STASHED" = true ] && git stash pop
fail "Rebase failed — resolve conflicts and re-run"
fi
if [ "$STASHED" = true ]; then
log "Restoring stashed changes..."
if ! git stash pop; then
echo ""
warn "Stash pop has conflicts with the rebased code."
echo " Resolve the conflicts above, then run:"
echo " git add <resolved-files>"
echo " git stash drop"
echo " Then re-run: ./scripts/release.sh"
exit 1
fi
fi
ok "Rebased to origin/main ($(git rev-parse --short HEAD))"
# ── Phase 2: Install dependencies ─────────────────────────────────────────────
log "Installing dependencies..."
pnpm install
ok "Dependencies installed"
# ── Phase 3: Build all packages ───────────────────────────────────────────────
log "Building all packages..."
pnpm build
ok "Build complete"
# ── Phase 4: Apply changesets (if any) ────────────────────────────────────────
CHANGESET_FILES="$(find .changeset -maxdepth 1 -name '*.md' ! -name 'README.md' 2>/dev/null | wc -l | tr -d ' ')"
if [ -n "$BUMP_TYPE" ]; then
log "Creating $BUMP_TYPE changeset for all publishable packages..."
if [ "$DRY_RUN" = false ]; then
# Collect all non-private workspace package names
BUMP_PKGS=()
for ws_dir in "${WORKSPACE_DIRS[@]}"; do
[ -d "$REPO_ROOT/$ws_dir" ] || continue
while IFS= read -r -d '' pkg_json; do
private="$(pkg_field private "$pkg_json")"
[ "$private" = "true" ] && continue
name="$(pkg_field name "$pkg_json")"
is_skip_package "$name" && continue
BUMP_PKGS+=("$name")
done < <(find "$REPO_ROOT/$ws_dir" -mindepth 2 -maxdepth 2 -name package.json -print0 | sort -z)
done
# Write a synthetic changeset file
CS_FILE=".changeset/release-$(date +%Y%m%d%H%M%S).md"
{
echo "---"
for pkg in "${BUMP_PKGS[@]}"; do
echo "\"$pkg\": $BUMP_TYPE"
done
echo "---"
echo ""
echo "Release: automated $BUMP_TYPE version bump"
} > "$CS_FILE"
log "Created changeset: $CS_FILE"
CHANGESET_FILES="1"
fi
fi
if [ "$CHANGESET_FILES" -gt 0 ]; then
log "Applying $CHANGESET_FILES changeset(s)..."
if [ "$DRY_RUN" = false ]; then
pnpm changeset version
ok "Version bumps applied"
log "Rebuilding after version bumps..."
pnpm build
ok "Rebuild complete"
else
log "[dry-run] Would run: pnpm changeset version && pnpm build"
fi
else
log "No pending changesets — skipping version bump"
fi
# ── Phase 5: Detect and publish outdated packages ─────────────────────────────
rm -rf "$TMP_DIR"
mkdir -p "$TMP_DIR"
trap 'rm -rf "$TMP_DIR"' EXIT
PUBLISHED=()
SKIPPED=()
ALREADY_PUBLISHED=()
publish_package() {
local pkg_dir="$1"
local pkg_json="$pkg_dir/package.json"
local name version safe_name work_dir packed_tgz final_tgz
name="$(pkg_field name "$pkg_json")"
version="$(pkg_field version "$pkg_json")"
local private_flag
private_flag="$(pkg_field private "$pkg_json")"
# Skip private packages
if [ "$private_flag" = "true" ]; then
SKIPPED+=("$name@$version (private)")
return
fi
# Skip native SDKs
if is_skip_package "$name"; then
SKIPPED+=("$name@$version (native SDK)")
return
fi
# Skip packages with no version
[ -z "$version" ] && { SKIPPED+=("$name (no version)"); return; }
# Check if this exact version is already on the registry
local remote_version
remote_version="$(version_on_registry "$name" "$version")"
if [ -n "$remote_version" ]; then
ALREADY_PUBLISHED+=("$name@$version")
return
fi
if [ "$DRY_RUN" = true ]; then
log "[dry-run] Would publish: $name@$version"
PUBLISHED+=("$name@$version")
return
fi
log "Publishing $name@$version..."
safe_name="${name//@/}"
safe_name="${safe_name//\//-}"
work_dir="$TMP_DIR/$safe_name-$version"
rm -rf "$work_dir"
mkdir -p "$work_dir"
# Pack with pnpm, repack with npm (Gitea registry compatibility)
(cd "$pkg_dir" && pnpm pack --pack-destination "$work_dir" >/dev/null)
packed_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | head -1)"
[ -z "$packed_tgz" ] && { warn "Pack failed for $name@$version — skipping"; SKIPPED+=("$name@$version (pack failed)"); return; }
mkdir -p "$work_dir/unpacked"
tar -xzf "$packed_tgz" -C "$work_dir/unpacked"
(cd "$work_dir/unpacked/package" && npm pack --pack-destination "$work_dir" >/dev/null)
final_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | sort | tail -1)"
[ -z "$final_tgz" ] && { warn "Repack failed for $name@$version — skipping"; SKIPPED+=("$name@$version (repack failed)"); return; }
if npm publish "$final_tgz" \
--registry "$REGISTRY_URL" \
"--//${AUTH_TARGET}:_authToken=${TOKEN}" 2>&1; then
ok "$name@$version published"
PUBLISHED+=("$name@$version")
else
warn "Publish failed for $name@$version"
SKIPPED+=("$name@$version (publish error)")
fi
}
log "Scanning workspace packages..."
for ws_dir in "${WORKSPACE_DIRS[@]}"; do
[ -d "$REPO_ROOT/$ws_dir" ] || continue
while IFS= read -r -d '' pkg_json; do
publish_package "$(dirname "$pkg_json")"
done < <(find "$REPO_ROOT/$ws_dir" -mindepth 2 -maxdepth 2 -name package.json -print0 | sort -z)
done
# ── Phase 6: Commit and push version bumps ────────────────────────────────────
if [ "$DRY_RUN" = false ]; then
if ! git diff --quiet HEAD 2>/dev/null || [ -n "$(git status --porcelain)" ]; then
log "Committing version bumps..."
git add -A
git commit -m "chore: release version bumps [skip ci]"
log "Pushing to origin main..."
git push origin main
ok "Pushed to origin/main"
else
log "No git changes to commit"
fi
fi
# ── Summary ────────────────────────────────────────────────────────────────────
echo ""
echo "════════════════════════════════════════"
echo " Release Summary"
echo "════════════════════════════════════════"
if [ ${#PUBLISHED[@]} -gt 0 ]; then
echo ""
echo "Published (${#PUBLISHED[@]}):"
for p in "${PUBLISHED[@]}"; do echo "$p"; done
fi
if [ ${#ALREADY_PUBLISHED[@]} -gt 0 ]; then
echo ""
echo "Already up-to-date (${#ALREADY_PUBLISHED[@]}):"
for p in "${ALREADY_PUBLISHED[@]}"; do echo " · $p"; done
fi
if [ ${#SKIPPED[@]} -gt 0 ]; then
echo ""
echo "Skipped (${#SKIPPED[@]}):"
for p in "${SKIPPED[@]}"; do echo "$p"; done
fi
echo ""
[ "$DRY_RUN" = true ] && echo " (dry-run — no changes made)"
echo "════════════════════════════════════════"