From 86d6c214fec2b970f2502aa911421961d7767f74 Mon Sep 17 00:00:00 2001 From: "jason.woltje" Date: Sat, 4 Apr 2026 18:07:05 +0000 Subject: [PATCH] feat: gateway publishability + npmjs publish script (#370) --- .woodpecker/publish.yml | 18 ++++- apps/gateway/package.json | 11 ++- apps/gateway/src/main.ts | 1 + scripts/publish-npmjs.sh | 165 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 3 deletions(-) create mode 100755 scripts/publish-npmjs.sh diff --git a/.woodpecker/publish.yml b/.woodpecker/publish.yml index fed5cc5..3f36f61 100644 --- a/.woodpecker/publish.yml +++ b/.woodpecker/publish.yml @@ -35,8 +35,8 @@ steps: - | echo "//git.mosaicstack.dev/api/packages/mosaic/npm/:_authToken=$NPM_TOKEN" > ~/.npmrc echo "@mosaic:registry=https://git.mosaicstack.dev/api/packages/mosaic/npm/" >> ~/.npmrc - # Publish all non-private packages (--no-git-checks skips dirty/branch checks in CI) - # --filter excludes private apps (gateway, web) and the root + # Publish non-private packages to Gitea (--no-git-checks skips dirty/branch checks in CI) + # --filter excludes gateway (published to npmjs instead) and web (private) - > pnpm --filter "@mosaic/*" --filter "!@mosaic/gateway" @@ -46,6 +46,20 @@ steps: depends_on: - build + publish-npmjs: + image: *node_image + environment: + NPM_TOKEN: + from_secret: npmjs_token + commands: + - *enable_pnpm + - apk add --no-cache jq bash + - bash scripts/publish-npmjs.sh + depends_on: + - build + when: + - event: [tag] + build-gateway: image: gcr.io/kaniko-project/executor:debug environment: diff --git a/apps/gateway/package.json b/apps/gateway/package.json index 0b61511..fbb6f08 100644 --- a/apps/gateway/package.json +++ b/apps/gateway/package.json @@ -1,9 +1,18 @@ { "name": "@mosaic/gateway", "version": "0.0.2", - "private": true, "type": "module", "main": "dist/main.js", + "bin": { + "mosaic-gateway": "dist/main.js" + }, + "files": [ + "dist" + ], + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" + }, "scripts": { "build": "tsc", "dev": "tsx watch src/main.ts", diff --git a/apps/gateway/src/main.ts b/apps/gateway/src/main.ts index 4053565..7edd335 100644 --- a/apps/gateway/src/main.ts +++ b/apps/gateway/src/main.ts @@ -1,3 +1,4 @@ +#!/usr/bin/env node import { config } from 'dotenv'; import { existsSync } from 'node:fs'; import { resolve, join } from 'node:path'; diff --git a/scripts/publish-npmjs.sh b/scripts/publish-npmjs.sh new file mode 100755 index 0000000..1d839b6 --- /dev/null +++ b/scripts/publish-npmjs.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# Publish @mosaic/* packages to npmjs.org as @mosaicstack/* +# +# This script patches each package.json to: +# 1. Rename @mosaic/X → @mosaicstack/X +# 2. Replace workspace:^ deps with resolved versions using @mosaicstack/* names +# 3. Run npm publish +# 4. Restore original package.json +# +# Usage: +# scripts/publish-npmjs.sh [--dry-run] [--filter ] +# +# Requirements: +# - NPM_TOKEN env var set (npmjs.org auth token) +# - jq installed +# - Run from monorepo root after `pnpm build` + +set -euo pipefail + +DRY_RUN=false +FILTER="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=true; shift ;; + --filter) FILTER="$2"; shift 2 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" + +# Collect all publishable package directories (non-private, has publishConfig) +PACKAGE_DIRS=() +for pkg_json in "$REPO_ROOT"/packages/*/package.json "$REPO_ROOT"/plugins/*/package.json "$REPO_ROOT"/apps/gateway/package.json; do + [[ -f "$pkg_json" ]] || continue + is_private=$(jq -r '.private // false' "$pkg_json") + [[ "$is_private" == "true" ]] && continue + PACKAGE_DIRS+=("$(dirname "$pkg_json")") +done + +echo "Found ${#PACKAGE_DIRS[@]} publishable packages" + +# Build a version map: @mosaic/X → version +declare -A VERSION_MAP +for dir in "${PACKAGE_DIRS[@]}"; do + name=$(jq -r '.name' "$dir/package.json") + version=$(jq -r '.version' "$dir/package.json") + VERSION_MAP["$name"]="$version" +done + +# Configure npmjs auth +if [[ -z "${NPM_TOKEN:-}" ]] && [[ "$DRY_RUN" == "false" ]]; then + echo "ERROR: NPM_TOKEN is required for publishing. Set it or use --dry-run." + exit 1 +fi + +publish_package() { + local dir="$1" + local orig_json="$dir/package.json" + local backup="$dir/package.json.bak" + + local name + name=$(jq -r '.name' "$orig_json") + + # Apply filter if set + if [[ -n "$FILTER" ]] && [[ "$name" != "$FILTER" ]]; then + return 0 + fi + + local new_name="${name/@mosaic\//@mosaicstack/}" + echo "" + echo "━━━ Publishing $name → $new_name ━━━" + + # Backup original + cp "$orig_json" "$backup" + + # Patch: rename package + local patched + patched=$(jq --arg new_name "$new_name" '.name = $new_name' "$orig_json") + + # Patch: publishConfig to npmjs + patched=$(echo "$patched" | jq '.publishConfig = {"registry": "https://registry.npmjs.org/", "access": "public"}') + + # Patch: replace workspace:^ dependencies with @mosaicstack/* and resolved versions + for dep_field in dependencies devDependencies peerDependencies; do + if echo "$patched" | jq -e ".$dep_field" > /dev/null 2>&1; then + local deps + deps=$(echo "$patched" | jq -r ".$dep_field // {} | keys[]") + for dep in $deps; do + local dep_version + dep_version=$(echo "$patched" | jq -r ".$dep_field[\"$dep\"]") + + # Only transform @mosaic/* workspace deps + if [[ "$dep" == @mosaic/* ]] && [[ "$dep_version" == workspace:* ]]; then + local new_dep="${dep/@mosaic\//@mosaicstack/}" + local resolved="${VERSION_MAP[$dep]:-}" + + if [[ -z "$resolved" ]]; then + echo " WARNING: No version found for $dep — using '*'" + resolved="*" + else + # workspace:^ means ^version, workspace:* means * + if [[ "$dep_version" == "workspace:^" ]]; then + resolved="^$resolved" + fi + fi + + # Rename the dep key and set the resolved version + patched=$(echo "$patched" | jq \ + --arg field "$dep_field" \ + --arg old_dep "$dep" \ + --arg new_dep "$new_dep" \ + --arg version "$resolved" \ + '.[$field] |= (del(.[$old_dep]) | .[$new_dep] = $version)') + fi + done + fi + done + + # Write patched package.json + echo "$patched" > "$orig_json" + + echo " Patched: $new_name" + + # Publish + if [[ "$DRY_RUN" == "true" ]]; then + echo " [DRY RUN] npm publish --dry-run" + (cd "$dir" && npm publish --dry-run 2>&1) || true + else + echo " Publishing to npmjs..." + (cd "$dir" && npm publish 2>&1) || echo " WARNING: Publish failed (may already exist at this version)" + fi + + # Restore original + mv "$backup" "$orig_json" + echo " Restored original package.json" +} + +# Set up npmrc for npmjs +if [[ -n "${NPM_TOKEN:-}" ]]; then + echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc +fi + +# Publish in dependency order: packages first, then plugins, then apps +echo "" +echo "=== Publishing packages ===" +for dir in "$REPO_ROOT"/packages/*/; do + [[ -f "$dir/package.json" ]] || continue + publish_package "$dir" +done + +echo "" +echo "=== Publishing plugins ===" +for dir in "$REPO_ROOT"/plugins/*/; do + [[ -f "$dir/package.json" ]] || continue + publish_package "$dir" +done + +echo "" +echo "=== Publishing apps ===" +publish_package "$REPO_ROOT/apps/gateway" + +echo "" +echo "Done."