From 755b7fedeaf4d7c3ca87fbb43a6a009e5ff734aa Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Fri, 3 Apr 2026 20:03:34 -0700 Subject: [PATCH] feat(scripts): add publish-outdated-gitea-packages.sh Network-aware script to detect and publish only outdated @bytelyst/* packages to the Gitea npm registry. Detection: SHA-256 content fingerprint comparison (local pack vs registry tarball) Publishing: auto-bumps patch version to avoid collisions with existing versions Network: NETWORK=corp uses localhost:3300 tunnel, NETWORK=home uses Azure VM Features: - --dry-run: detect without publishing - --filter: check/publish a single package - --skip-build: skip pnpm build step - --help: usage info - Strips publishConfig from tarballs (avoids hardcoded external domain) - Runs npm publish from /tmp (avoids repo .npmrc scoped registry override) - Corp: unsets all NPM_CONFIG_*/proxy env vars for localhost publish --- scripts/publish-outdated-gitea-packages.sh | 443 +++++++++++++++++++++ 1 file changed, 443 insertions(+) create mode 100755 scripts/publish-outdated-gitea-packages.sh diff --git a/scripts/publish-outdated-gitea-packages.sh b/scripts/publish-outdated-gitea-packages.sh new file mode 100755 index 00000000..49431be9 --- /dev/null +++ b/scripts/publish-outdated-gitea-packages.sh @@ -0,0 +1,443 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ───────────────────────────────────────────────────────────── +# Publish only OUTDATED @bytelyst/* packages to Gitea registry +# +# Compares local built content against what's in the registry. +# Only publishes packages where the content has actually changed. +# +# Usage: +# ./scripts/publish-outdated-gitea-packages.sh # detect + publish +# ./scripts/publish-outdated-gitea-packages.sh --dry-run # detect only +# ./scripts/publish-outdated-gitea-packages.sh --filter @bytelyst/errors +# +# Requires: GITEA_NPM_TOKEN env var (or ~/.gitea_npm_token) +# ───────────────────────────────────────────────────────────── + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +PACKAGES_DIR="$REPO_ROOT/packages" + +# ── Network-aware Gitea resolution ───────────────────────── +# NETWORK=corp -> localhost:3300 (SSH tunnel to Azure VM) +# NETWORK=home -> Azure VM directly (gitea.bytelyst.com or ~/.gitea_vm_host) +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 + # Home network: use Azure VM host from ~/.gitea_vm_host or GITEA_NPM_HOST + 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/}" +TOKEN="${GITEA_NPM_TOKEN:-}" +WORK_DIR="${TMPDIR:-/tmp}/bytelyst-outdated-check-$$" +DRY_RUN=false +PACKAGE_FILTER="" +SKIP_BUILD=false + +# Parse args +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --filter) + if [ -z "${2:-}" ]; then echo "ERROR: --filter requires a package name"; exit 1; fi + PACKAGE_FILTER="$2"; shift 2 ;; + --skip-build) SKIP_BUILD=true; shift ;; + --help|-h) + echo "Usage: $0 [--dry-run] [--skip-build] [--filter @bytelyst/name]" + echo " --dry-run Detect outdated packages without publishing" + echo " --skip-build Skip 'pnpm build' (assumes dist/ is current)" + echo " --filter NAME Only check/publish the named package" + exit 0 ;; + *) echo "Unknown arg: $1 (try --help)"; exit 1 ;; + esac +done + +# Resolve token from file if not in env +if [ -z "$TOKEN" ] && [ -f "$HOME/.gitea_npm_token" ]; then + TOKEN="$(cat "$HOME/.gitea_npm_token")" +fi + +if [ -z "$TOKEN" ]; then + echo "ERROR: GITEA_NPM_TOKEN is required (env var or ~/.gitea_npm_token)" + exit 1 +fi + +# Auth target for npm publish +AUTH_TARGET="${REGISTRY_URL#http://}" +AUTH_TARGET="${AUTH_TARGET#https://}" + +# Skip non-npm packages (native SDKs) +SKIP_DIRS="swift-platform-sdk swift-diagnostics kotlin-platform-sdk react-native-platform-sdk" + +# Cleanup on exit +trap 'rm -rf "$WORK_DIR"' EXIT +mkdir -p "$WORK_DIR" + +# ── Helpers ──────────────────────────────────────────────── + +pkg_field() { + node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).$1||'')" "$2" +} + +# Compute a content fingerprint of all files under a directory. +# Sorts file paths, hashes each file, then hashes the combined result. +# This is metadata-independent (ignores tar headers, timestamps, etc.) +content_fingerprint() { + local dir="$1" + find "$dir" -type f -print0 | sort -z | xargs -0 shasum -a 256 | shasum -a 256 | cut -d' ' -f1 +} + +# Check if a package version exists in the registry. +# Gitea npm API: GET /{encoded_name} returns all versions. +registry_version_exists() { + local name="$1" version="$2" + local encoded_name="${name/@/%40}" + encoded_name="${encoded_name//\//%2F}" + local meta + meta=$(curl -s -H "Authorization: token $TOKEN" \ + "${REGISTRY_URL}${encoded_name}" 2>/dev/null) || return 1 + echo "$meta" | node -e " + let d='';process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + try{const p=JSON.parse(d);process.exit(p.versions&&p.versions['$version']?0:1)} + catch(e){process.exit(1)} + })" 2>/dev/null +} + +# Download a package tarball from the registry. +# Gitea npm API: GET /{encoded_name} → versions.{version}.dist.tarball +download_registry_tarball() { + local name="$1" version="$2" dest_dir="$3" + local encoded_name="${name/@/%40}" + encoded_name="${encoded_name//\//%2F}" + + # Fetch the full package metadata (all versions) + local meta + meta=$(curl -s -H "Authorization: token $TOKEN" \ + "${REGISTRY_URL}${encoded_name}" 2>/dev/null) || return 1 + + local tarball_url + tarball_url=$(echo "$meta" | node -e " + let d='';process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + try{ + const p=JSON.parse(d); + const v=p.versions&&p.versions['$version']; + if(!v||!v.dist||!v.dist.tarball){process.exit(1)} + process.stdout.write(v.dist.tarball) + }catch(e){process.exit(1)} + })" 2>/dev/null) || return 1 + + if [ -z "$tarball_url" ]; then + return 1 + fi + + curl -s -H "Authorization: token $TOKEN" \ + -o "$dest_dir/registry.tgz" \ + "$tarball_url" 2>/dev/null || return 1 + + [ -f "$dest_dir/registry.tgz" ] && [ -s "$dest_dir/registry.tgz" ] +} + +# Bump the patch version of a package.json (e.g., 0.1.0 → 0.1.1, 0.1.1 → 0.1.2). +# Also finds the next available version not yet in the registry. +bump_patch_version() { + local pkg_json="$1" pkg_name="$2" + local encoded_name="${pkg_name/@/%40}" + encoded_name="${encoded_name//\//%2F}" + + # Get all published versions from registry + local published_versions + published_versions=$(curl -s -H "Authorization: token $TOKEN" \ + "${REGISTRY_URL}${encoded_name}" 2>/dev/null | node -e " + let d='';process.stdin.on('data',c=>d+=c); + process.stdin.on('end',()=>{ + try{const p=JSON.parse(d);process.stdout.write(Object.keys(p.versions||{}).join(','))} + catch(e){process.stdout.write('')} + })" 2>/dev/null) || true + + # Compute next available patch version + node -e " + const fs=require('fs'); + const pkg=JSON.parse(fs.readFileSync(process.argv[1],'utf8')); + const published=new Set((process.argv[2]||'').split(',').filter(Boolean)); + const parts=pkg.version.split('.').map(Number); + // Start from current patch+1 and find first unpublished + parts[2]++; + while(published.has(parts.join('.'))){parts[2]++} + pkg.version=parts.join('.'); + fs.writeFileSync(process.argv[1],JSON.stringify(pkg,null,2)+'\n'); + process.stdout.write(pkg.version); + " "$pkg_json" "$published_versions" +} + +# Publish a package using the same double-pack pattern as publish-local-gitea-packages.sh. +publish_package() { + local pkg_dir="$1" + local pkg_name pkg_version safe_name + pkg_name="$(pkg_field name "$pkg_dir/package.json")" + pkg_version="$(pkg_field version "$pkg_dir/package.json")" + safe_name="${pkg_name//@/}" + safe_name="${safe_name//\//-}" + + local pub_dir="$WORK_DIR/publish-$safe_name" + rm -rf "$pub_dir" + mkdir -p "$pub_dir" + + # Step 1: pnpm pack + (cd "$pkg_dir" && pnpm pack --pack-destination "$pub_dir" >/dev/null 2>&1) + local packed_tgz + packed_tgz="$(find "$pub_dir" -maxdepth 1 -name '*.tgz' | head -1)" + if [ -z "$packed_tgz" ]; then + echo " ERROR: pnpm pack failed for $pkg_name" + return 1 + fi + + # Step 2: extract, strip publishConfig (so --registry wins), then npm repack + mkdir -p "$pub_dir/unpacked" + tar -xzf "$packed_tgz" -C "$pub_dir/unpacked" + rm -f "$packed_tgz" # remove pnpm tgz so only npm-repacked tgz remains + node -e " + const fs=require('fs'),f=process.argv[1]; + const p=JSON.parse(fs.readFileSync(f,'utf8')); + delete p.publishConfig; + fs.writeFileSync(f,JSON.stringify(p,null,2)+'\n'); + " "$pub_dir/unpacked/package/package.json" + (cd "$pub_dir/unpacked/package" && npm pack --pack-destination "$pub_dir" >/dev/null 2>&1) + + local final_tgz + final_tgz="$(find "$pub_dir" -maxdepth 1 -name '*.tgz' | head -1)" + if [ -z "$final_tgz" ]; then + echo " ERROR: npm repack failed for $pkg_name" + return 1 + fi + + # Step 3: publish to Gitea registry. + # Run from WORK_DIR (in /tmp with .npmrc for auth) so npm won't find + # the repo's .npmrc which has @bytelyst:registry pointing externally. + if [ "$IS_CORP" = true ]; then + # Corp: unset ALL proxy/registry env vars so npm goes directly to localhost + 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_REGISTRY -u NPM_CONFIG_STRICT_SSL \ + -u NPM_CONFIG_NOPROXY \ + -u NODE_TLS_REJECT_UNAUTHORIZED \ + npm publish "$final_tgz" \ + --registry "$REGISTRY_URL" 2>&1); then + echo " ERROR: publish failed for $pkg_name@$pkg_version" + return 1 + fi + else + # Home: publish directly to Azure VM Gitea (no proxy stripping needed) + if ! (cd "$WORK_DIR" && npm publish "$final_tgz" \ + --registry "$REGISTRY_URL" 2>&1); then + echo " ERROR: publish failed for $pkg_name@$pkg_version" + return 1 + fi + fi +} + +# ── Main ─────────────────────────────────────────────────── + +# Show resolved config +echo "==> Network: $NETWORK_MODE ($( [ "$IS_CORP" = true ] && echo "corp — localhost tunnel" || echo "home — Azure VM" ))" +echo " Registry: $REGISTRY_URL" +echo "" + +# Step 1: Build all packages +if [ "$SKIP_BUILD" = false ]; then + echo "==> Building all packages..." + (cd "$REPO_ROOT" && pnpm build 2>&1 | tail -5) + echo "" +fi + +# Step 2: Check each package +echo "==> Checking packages against registry..." +echo "" + +outdated_dirs=() +outdated_names=() +up_to_date=0 +not_found=0 +changed=0 +skipped=0 +errors=0 + +for pkg_json in "$PACKAGES_DIR"/*/package.json; do + dir_name=$(basename "$(dirname "$pkg_json")") + pkg_dir="$(dirname "$pkg_json")" + + # Skip native SDKs + if echo "$SKIP_DIRS" | grep -qw "$dir_name"; then + ((skipped++)) + continue + fi + + pkg_name="$(pkg_field name "$pkg_json")" + pkg_version="$(pkg_field version "$pkg_json")" + + # Apply filter + if [ -n "$PACKAGE_FILTER" ] && [ "$pkg_name" != "$PACKAGE_FILTER" ]; then + continue + fi + + # Check if dist/ exists (package must be built) + if [ ! -d "$pkg_dir/dist" ] && [ ! -d "$pkg_dir/generated" ]; then + echo " SKIP (no dist/): $pkg_name" + ((skipped++)) + continue + fi + + # Prepare work dirs + local_work="$WORK_DIR/check-$dir_name/local" + registry_work="$WORK_DIR/check-$dir_name/registry" + rm -rf "$WORK_DIR/check-$dir_name" + mkdir -p "$local_work" "$registry_work" + + # Pack locally + if ! (cd "$pkg_dir" && pnpm pack --pack-destination "$local_work" >/dev/null 2>&1); then + echo " ERROR (pack failed): $pkg_name" + ((errors++)) + continue + fi + local_tgz="$(find "$local_work" -maxdepth 1 -name '*.tgz' | head -1)" + if [ -z "$local_tgz" ]; then + echo " ERROR (no tgz): $pkg_name" + ((errors++)) + continue + fi + + # Extract local tarball + mkdir -p "$local_work/extracted" + tar xzf "$local_tgz" -C "$local_work/extracted" 2>/dev/null + + # Try to download registry tarball + if download_registry_tarball "$pkg_name" "$pkg_version" "$registry_work"; then + # Extract registry tarball + mkdir -p "$registry_work/extracted" + tar xzf "$registry_work/registry.tgz" -C "$registry_work/extracted" 2>/dev/null + + # Compare content fingerprints + local_fp="$(content_fingerprint "$local_work/extracted")" + registry_fp="$(content_fingerprint "$registry_work/extracted")" + + if [ "$local_fp" = "$registry_fp" ]; then + echo " UP-TO-DATE: $pkg_name@$pkg_version" + ((up_to_date++)) + else + echo " OUTDATED: $pkg_name@$pkg_version" + outdated_dirs+=("$pkg_dir") + outdated_names+=("$pkg_name@$pkg_version") + ((changed++)) + fi + else + echo " NOT FOUND: $pkg_name@$pkg_version (will publish)" + outdated_dirs+=("$pkg_dir") + outdated_names+=("$pkg_name@$pkg_version") + ((not_found++)) + fi + + # Cleanup check dir to save disk space + rm -rf "$WORK_DIR/check-$dir_name" +done + +# ── Summary ──────────────────────────────────────────────── + +echo "" +echo "==> Summary" +echo " Up-to-date: $up_to_date" +echo " Outdated: $changed" +echo " Not found: $not_found" +echo " Skipped: $skipped" +echo " Errors: $errors" +echo "" + +if [ ${#outdated_dirs[@]} -eq 0 ]; then + echo "All packages are up to date. Nothing to publish." + exit 0 +fi + +echo "Packages to publish:" +for name in "${outdated_names[@]}"; do + echo " - $name" +done +echo "" + +if [ "$DRY_RUN" = true ]; then + echo "(dry-run mode — no changes made)" + exit 0 +fi + +# Step 3: Publish outdated packages +echo "==> Publishing ${#outdated_dirs[@]} package(s)..." +echo "" + +# Write .npmrc into WORK_DIR for auth (npm reads .npmrc from cwd) +cat > "$WORK_DIR/.npmrc" < Done: $published published, $pub_errors failed" + +# Show bumped packages so the user knows which package.json files changed +if [ ${#bumped_packages[@]} -gt 0 ]; then + echo "" + echo "NOTE: ${#bumped_packages[@]} package(s) had their patch version bumped." + echo " These package.json changes are local — commit them when ready:" + for d in "${bumped_packages[@]}"; do + echo " $d/package.json" + done +fi + +exit $((pub_errors > 0 ? 1 : 0))