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
This commit is contained in:
parent
b30cc7b5e4
commit
755b7fedea
443
scripts/publish-outdated-gitea-packages.sh
Executable file
443
scripts/publish-outdated-gitea-packages.sh
Executable file
@ -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" <<EOF
|
||||
//${AUTH_TARGET}:_authToken=${TOKEN}
|
||||
EOF
|
||||
|
||||
published=0
|
||||
pub_errors=0
|
||||
bumped_packages=()
|
||||
|
||||
for i in "${!outdated_dirs[@]}"; do
|
||||
pkg_dir="${outdated_dirs[$i]}"
|
||||
pkg_name="$(pkg_field name "$pkg_dir/package.json")"
|
||||
pkg_version="$(pkg_field version "$pkg_dir/package.json")"
|
||||
|
||||
echo " [$((i+1))/${#outdated_dirs[@]}] $pkg_name@$pkg_version"
|
||||
|
||||
# If version already exists in registry, bump patch version
|
||||
if registry_version_exists "$pkg_name" "$pkg_version"; then
|
||||
echo " Version $pkg_version exists in registry, bumping patch..."
|
||||
new_version="$(bump_patch_version "$pkg_dir/package.json" "$pkg_name")"
|
||||
echo " Bumped to $new_version"
|
||||
bumped_packages+=("$pkg_dir")
|
||||
fi
|
||||
|
||||
# Re-read version after potential bump
|
||||
pkg_version="$(pkg_field version "$pkg_dir/package.json")"
|
||||
|
||||
# Publish
|
||||
echo " Publishing $pkg_name@$pkg_version..."
|
||||
if publish_package "$pkg_dir"; then
|
||||
echo " Published $pkg_name@$pkg_version"
|
||||
((published++))
|
||||
else
|
||||
echo " FAILED to publish $pkg_name@$pkg_version"
|
||||
((pub_errors++))
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
echo "==> 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))
|
||||
Loading…
Reference in New Issue
Block a user