release.sh → release-gitea-packages.sh: 1. Renamed to clearly describe purpose (Gitea npm package release, not a generic release script). 2. Added NETWORK=corp/home detection matching publish-outdated-gitea- packages.sh pattern: - corp: localhost:3300 SSH tunnel + proxy env var stripping - home: Azure VM directly via gitea.bytelyst.com or ~/.gitea_vm_host 3. Added ~/.gitea_npm_token file fallback (same as sibling scripts). 4. Corp publishes now strip HTTP_PROXY/HTTPS_PROXY/npm_config_proxy env vars so npm reaches localhost tunnel directly instead of going through the corporate proxy (which can't reach the tunnel). 5. Updated package.json 'release' script reference.
445 lines
15 KiB
Bash
Executable File
445 lines
15 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# release-gitea-packages.sh — Version-bump + publish @bytelyst/* packages to Gitea npm registry
|
|
#
|
|
# Usage:
|
|
# ./scripts/release-gitea-packages.sh # apply pending changesets + publish missing/outdated packages
|
|
# ./scripts/release-gitea-packages.sh --patch # auto-bump all packages (patch) + publish
|
|
# ./scripts/release-gitea-packages.sh --minor # auto-bump all packages (minor) + publish
|
|
# ./scripts/release-gitea-packages.sh --major # auto-bump all packages (major) + publish
|
|
# ./scripts/release-gitea-packages.sh --dry-run # show what would be published, no side effects
|
|
#
|
|
# Required env:
|
|
# GITEA_NPM_TOKEN — auth token for the Gitea npm registry (or ~/.gitea_npm_token file)
|
|
#
|
|
# Optional env:
|
|
# GITEA_NPM_REGISTRY_URL — override auto-detected registry URL
|
|
#
|
|
# Network handling (automatic via NETWORK env var set by switch-network.sh):
|
|
# NETWORK=corp → localhost:3300 (SSH tunnel to Azure VM, proxy env stripped)
|
|
# NETWORK=home → Azure VM directly (gitea.bytelyst.com or ~/.gitea_vm_host)
|
|
|
|
set -euo pipefail
|
|
|
|
# ── Config ─────────────────────────────────────────────────────────────────────
|
|
|
|
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
|
|
# ── Network-aware Gitea resolution ─────────────────────────────────────────────
|
|
# Matches the pattern in publish-outdated-gitea-packages.sh
|
|
NETWORK_MODE="${NETWORK:-home}"
|
|
|
|
if [ "$NETWORK_MODE" = "corp" ]; then
|
|
GITEA_HOST="${GITEA_NPM_HOST:-localhost}"
|
|
GITEA_PORT="${GITEA_NPM_PORT:-3300}"
|
|
GITEA_BASE="http://${GITEA_HOST}:${GITEA_PORT}"
|
|
IS_CORP=true
|
|
else
|
|
if [ -n "${GITEA_NPM_HOST:-}" ] && [ "${GITEA_NPM_HOST}" != "localhost" ]; then
|
|
GITEA_HOST="$GITEA_NPM_HOST"
|
|
elif [ -f "$HOME/.gitea_vm_host" ]; then
|
|
GITEA_HOST="$(cat "$HOME/.gitea_vm_host")"
|
|
else
|
|
GITEA_HOST="gitea.bytelyst.com"
|
|
fi
|
|
GITEA_PORT="${GITEA_NPM_PORT:-3300}"
|
|
GITEA_BASE="http://${GITEA_HOST}:${GITEA_PORT}"
|
|
IS_CORP=false
|
|
fi
|
|
|
|
REGISTRY_URL="${GITEA_NPM_REGISTRY_URL:-${GITEA_BASE}/api/packages/ByteLyst/npm/}"
|
|
AUTH_TARGET="${REGISTRY_URL#http://}"
|
|
AUTH_TARGET="${AUTH_TARGET#https://}"
|
|
TOKEN="${GITEA_NPM_TOKEN:-}"
|
|
TMP_DIR="${TMPDIR:-/tmp}/bytelyst-release-$$"
|
|
NPMRC_FILE="$TMP_DIR/.npmrc" # single shared npmrc for all npm calls
|
|
|
|
# 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")
|
|
|
|
# Files/dirs to exclude from release commits
|
|
GIT_EXCLUDE_PATTERNS=("node-compile-cache" "*.tsbuildinfo")
|
|
|
|
# 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; }
|
|
info() { echo " $*"; }
|
|
|
|
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
|
|
[[ "$name" == *"$skip"* ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Check registry using shared npmrc (reliable auth for both GET and PUT)
|
|
version_on_registry() {
|
|
local name="$1" version="$2"
|
|
npm view "${name}@${version}" version \
|
|
--registry "$REGISTRY_URL" \
|
|
--userconfig "$NPMRC_FILE" \
|
|
--silent \
|
|
2>/dev/null || true
|
|
}
|
|
|
|
# ── Preflight ──────────────────────────────────────────────────────────────────
|
|
|
|
cd "$REPO_ROOT"
|
|
|
|
# Resolve token from file if not in env
|
|
if [ -z "$TOKEN" ] && [ -f "$HOME/.gitea_npm_token" ]; then
|
|
TOKEN="$(cat "$HOME/.gitea_npm_token")"
|
|
fi
|
|
[ -z "$TOKEN" ] && fail "GITEA_NPM_TOKEN is not set (env var or ~/.gitea_npm_token)"
|
|
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"
|
|
command -v node >/dev/null 2>&1 || fail "node not found in PATH"
|
|
|
|
# Create shared tmp dir + npmrc early so version checks work before publishing
|
|
mkdir -p "$TMP_DIR"
|
|
trap 'rm -rf "$TMP_DIR"' EXIT
|
|
printf '//%s:_authToken=%s\n' "$AUTH_TARGET" "$TOKEN" > "$NPMRC_FILE"
|
|
|
|
# Show resolved config
|
|
log "Network: $NETWORK_MODE ($( [ "$IS_CORP" = true ] && echo "corp — localhost tunnel" || echo "home — Azure VM" ))"
|
|
info "Registry: $REGISTRY_URL"
|
|
|
|
# Verify token can read from registry
|
|
log "Verifying registry credentials..."
|
|
if ! npm view "@bytelyst/errors" version \
|
|
--registry "$REGISTRY_URL" \
|
|
--userconfig "$NPMRC_FILE" \
|
|
--silent 2>/dev/null; then
|
|
fail "Registry auth failed — check GITEA_NPM_TOKEN has read:package scope"
|
|
fi
|
|
ok "Registry credentials verified"
|
|
|
|
[ "$DRY_RUN" = true ] && log "Dry-run mode — no packages will be published or committed"
|
|
|
|
# ── 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 2>/dev/null || true
|
|
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."
|
|
info "Resolve the conflicts, then run:"
|
|
info " git add <resolved-files> && git stash drop"
|
|
info " 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 2>&1 | grep -v "^Progress:" | grep -v "^packages/react-native" | grep -v "WARN deprecated" || true
|
|
ok "Dependencies installed"
|
|
|
|
# ── Phase 3: Build all packages ───────────────────────────────────────────────
|
|
|
|
log "Building all packages..."
|
|
pnpm build 2>&1 | grep -E "Done|Error|error|failed|✓|✗" | grep -v "^packages.*build:" | head -20 || true
|
|
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
|
|
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
|
|
|
|
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 (${#BUMP_PKGS[@]} packages)"
|
|
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 2>&1 | grep -E "Done|Error|failed" | head -10 || true
|
|
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/missing packages ─────────────────────
|
|
|
|
PUBLISHED=()
|
|
SKIPPED=()
|
|
ALREADY_PUBLISHED=()
|
|
FAILED=()
|
|
|
|
publish_package() {
|
|
local pkg_dir="$1"
|
|
local pkg_json="$pkg_dir/package.json"
|
|
local name version private_flag
|
|
|
|
name="$(pkg_field name "$pkg_json")"
|
|
version="$(pkg_field version "$pkg_json")"
|
|
private_flag="$(pkg_field private "$pkg_json")"
|
|
|
|
# Skip private packages
|
|
if [ "$private_flag" = "true" ]; then
|
|
SKIPPED+=("$name@$version (private)")
|
|
info " ⊘ $name@$version (private)"
|
|
return
|
|
fi
|
|
|
|
# Skip native SDKs
|
|
if is_skip_package "$name"; then
|
|
SKIPPED+=("$name@$version (native SDK)")
|
|
info " ⊘ $name@$version (native SDK)"
|
|
return
|
|
fi
|
|
|
|
# Skip packages with no version
|
|
if [ -z "$version" ]; then
|
|
SKIPPED+=("$name (no version)")
|
|
warn " ⊘ $name — no version field"
|
|
return
|
|
fi
|
|
|
|
# Check registry (uses shared npmrc — reliable auth)
|
|
local remote_version
|
|
remote_version="$(version_on_registry "$name" "$version")"
|
|
|
|
if [ -n "$remote_version" ]; then
|
|
ALREADY_PUBLISHED+=("$name@$version")
|
|
info " ✓ $name@$version (up-to-date)"
|
|
return
|
|
fi
|
|
|
|
# Not on registry — needs publishing
|
|
if [ "$DRY_RUN" = true ]; then
|
|
log " → [dry-run] would publish: $name@$version"
|
|
PUBLISHED+=("$name@$version")
|
|
return
|
|
fi
|
|
|
|
log " → Publishing $name@$version..."
|
|
|
|
local safe_name work_dir packed_tgz final_tgz
|
|
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
|
|
if ! (cd "$pkg_dir" && pnpm pack --pack-destination "$work_dir" 2>&1 | grep -v "^Wrote\|^packing"); then
|
|
warn " ✗ $name@$version — pack failed"
|
|
FAILED+=("$name@$version (pack failed)")
|
|
return
|
|
fi
|
|
|
|
packed_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | head -1)"
|
|
if [ -z "$packed_tgz" ]; then
|
|
warn " ✗ $name@$version — no tarball after pack"
|
|
FAILED+=("$name@$version (pack failed)")
|
|
return
|
|
fi
|
|
|
|
# Repack with npm for Gitea compatibility
|
|
mkdir -p "$work_dir/unpacked"
|
|
tar -xzf "$packed_tgz" -C "$work_dir/unpacked" 2>/dev/null
|
|
if ! (cd "$work_dir/unpacked/package" && npm pack --pack-destination "$work_dir" 2>/dev/null); then
|
|
warn " ✗ $name@$version — repack failed"
|
|
FAILED+=("$name@$version (repack failed)")
|
|
return
|
|
fi
|
|
|
|
final_tgz="$(find "$work_dir" -maxdepth 1 -name '*.tgz' | sort | tail -1)"
|
|
if [ -z "$final_tgz" ]; then
|
|
warn " ✗ $name@$version — no tarball after repack"
|
|
FAILED+=("$name@$version (repack failed)")
|
|
return
|
|
fi
|
|
|
|
# Publish using shared npmrc (corp: strip proxy env so npm reaches localhost directly)
|
|
local publish_log="$work_dir/publish.log"
|
|
local publish_ok=false
|
|
if [ "$IS_CORP" = true ]; then
|
|
if (cd "$work_dir" && env \
|
|
-u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY \
|
|
-u npm_config_proxy -u npm_config_https_proxy \
|
|
-u NPM_CONFIG_PROXY -u NPM_CONFIG_HTTPS_PROXY \
|
|
-u npm_config_noproxy -u NPM_CONFIG_NOPROXY \
|
|
-u NODE_TLS_REJECT_UNAUTHORIZED \
|
|
npm publish "$final_tgz" \
|
|
--registry "$REGISTRY_URL" \
|
|
--userconfig "$NPMRC_FILE" \
|
|
--silent 2>"$publish_log"); then
|
|
publish_ok=true
|
|
fi
|
|
else
|
|
if npm publish "$final_tgz" \
|
|
--registry "$REGISTRY_URL" \
|
|
--userconfig "$NPMRC_FILE" \
|
|
--silent \
|
|
2>"$publish_log"; then
|
|
publish_ok=true
|
|
fi
|
|
fi
|
|
if [ "$publish_ok" = true ]; then
|
|
ok " ✅ $name@$version published"
|
|
PUBLISHED+=("$name@$version")
|
|
else
|
|
local err
|
|
err="$(cat "$publish_log" | grep -v '^npm notice' | head -3)"
|
|
warn " ✗ $name@$version — publish failed: $err"
|
|
FAILED+=("$name@$version (publish error)")
|
|
fi
|
|
}
|
|
|
|
log "Scanning workspace packages..."
|
|
echo ""
|
|
for ws_dir in "${WORKSPACE_DIRS[@]}"; do
|
|
[ -d "$REPO_ROOT/$ws_dir" ] || continue
|
|
log "[$ws_dir]"
|
|
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
|
|
echo ""
|
|
|
|
# ── Phase 6: Commit and push ──────────────────────────────────────────────────
|
|
|
|
if [ "$DRY_RUN" = false ]; then
|
|
# Exclude generated/cache dirs from commits
|
|
for pattern in "${GIT_EXCLUDE_PATTERNS[@]}"; do
|
|
git restore --staged "**/$pattern" 2>/dev/null || true
|
|
done
|
|
|
|
# Stage only tracked files + known release artifacts (not caches)
|
|
git add \
|
|
"packages/*/package.json" \
|
|
"services/*/package.json" \
|
|
"dashboards/*/package.json" \
|
|
"package.json" \
|
|
"pnpm-lock.yaml" \
|
|
".changeset/" \
|
|
"CHANGELOG.md" 2>/dev/null || true
|
|
|
|
if ! git diff --cached --quiet; then
|
|
log "Committing version bumps..."
|
|
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 version bump changes to commit"
|
|
fi
|
|
fi
|
|
|
|
# ── Summary ────────────────────────────────────────────────────────────────────
|
|
|
|
TOTAL_PKGS=$(( ${#PUBLISHED[@]} + ${#ALREADY_PUBLISHED[@]} + ${#SKIPPED[@]} + ${#FAILED[@]} ))
|
|
|
|
echo "════════════════════════════════════════"
|
|
echo " Release Summary (${TOTAL_PKGS} packages scanned)"
|
|
echo "════════════════════════════════════════"
|
|
|
|
if [ ${#PUBLISHED[@]} -gt 0 ]; then
|
|
echo ""
|
|
echo "Published (${#PUBLISHED[@]}):"
|
|
for p in "${PUBLISHED[@]}"; do echo " ✅ $p"; done
|
|
fi
|
|
|
|
if [ ${#FAILED[@]} -gt 0 ]; then
|
|
echo ""
|
|
echo "Failed (${#FAILED[@]}):"
|
|
for p in "${FAILED[@]}"; 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)"
|
|
|
|
# Exit non-zero if any publishes failed
|
|
if [ ${#FAILED[@]} -gt 0 ]; then
|
|
echo " ⚠️ ${#FAILED[@]} package(s) failed to publish — see above"
|
|
exit 1
|
|
fi
|
|
|
|
echo "════════════════════════════════════════"
|