#!/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))