diff --git a/scripts/publish-all.sh b/scripts/publish-all.sh deleted file mode 100755 index dbeb3bc..0000000 --- a/scripts/publish-all.sh +++ /dev/null @@ -1,137 +0,0 @@ -#!/usr/bin/env bash -# Publish all public @uncaged/* packages to Gitea npm registry. -# -# PITFALL: After bumping versions in package.json, bun pm pack still reads the -# old bun.lock and resolves workspace:* to the previous (stale) versions. -# This script deletes bun.lock and runs bun install before packing to force -# correct resolution of workspace:* dependencies. -# -# Usage: -# ./scripts/publish-all.sh # Publish all packages -# ./scripts/publish-all.sh --dry-run # Show what would be published -# -# Package order is auto-resolved via topological sort of workspace:* dependencies. -# -# Prerequisites: -# - .npmrc in monorepo root with Gitea auth token -# - bun (for packing with workspace:* resolution) -# - npm (for publishing tarballs) - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -MONOREPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" - -REGISTRY="https://git.shazhou.work/api/packages/uncaged/npm/" -DRY_RUN="" - -if [[ "${1:-}" == "--dry-run" ]]; then - DRY_RUN="--dry-run" - echo "๐Ÿ” Dry run mode โ€” no packages will be published" - echo -fi - -# Topological sort: read all package.json files, build dependency graph, emit leaf-first order -ORDERED=$(python3 -c " -import json, os, sys -from pathlib import Path - -pkgs_dir = Path('$MONOREPO_ROOT/packages') -# name -> dir_name, and dependency edges -name_to_dir = {} -deps_graph = {} # name -> set of @uncaged/* dependency names - -for d in sorted(pkgs_dir.iterdir()): - pj = d / 'package.json' - if not pj.exists(): - continue - data = json.loads(pj.read_text()) - name = data.get('name', '') - if not name.startswith('@uncaged/'): - continue - if data.get('private'): - continue - name_to_dir[name] = d.name - local_deps = set() - for section in ('dependencies', 'devDependencies', 'peerDependencies'): - for dep, ver in data.get(section, {}).items(): - if dep.startswith('@uncaged/') and dep in name_to_dir or ver == 'workspace:*': - local_deps.add(dep) - deps_graph[name] = local_deps - -# Kahn's algorithm -in_degree = {n: 0 for n in deps_graph} -for n, ds in deps_graph.items(): - for d in ds: - if d in in_degree: - in_degree[d] = in_degree.get(d, 0) # ensure exists - -# Recount -in_degree = {n: 0 for n in deps_graph} -for n, ds in deps_graph.items(): - for d in ds: - if d in in_degree: - in_degree[d] += 1 - -# Wait, direction is wrong. If A depends on B, B must be published first. -# So edge is: A -> B means B must come before A. -# in_degree[A] = number of deps A has (that are in our set) -in_degree = {n: 0 for n in deps_graph} -for n, ds in deps_graph.items(): - for d in ds: - if d in in_degree: - pass # d is a dependency of n - in_degree[n] = len([d for d in ds if d in deps_graph]) - -queue = [n for n, deg in in_degree.items() if deg == 0] -queue.sort() # stable order -result = [] -while queue: - node = queue.pop(0) - result.append(node) - for n, ds in deps_graph.items(): - if node in ds: - in_degree[n] -= 1 - if in_degree[n] == 0: - queue.append(n) - queue.sort() - -for name in result: - print(name_to_dir[name]) -") - -# Regenerate lockfile so bun pm pack resolves workspace:* to freshly-bumped versions -cd "$MONOREPO_ROOT" -rm -f bun.lock -bun install - -ok=0 -fail=0 - -while IFS= read -r pkg; do - dir="$MONOREPO_ROOT/packages/$pkg" - name=$(grep -m1 '"name"' "$dir/package.json" | sed 's/.*: *"\(.*\)".*/\1/') - - cd "$dir" - - # bun pm pack resolves workspace:* โ†’ actual versions - tgz=$(bun pm pack 2>&1 | grep '\.tgz' | grep -v packed | head -1 | tr -d ' ') - - if [[ -z "$tgz" || ! -f "$tgz" ]]; then - echo "โŒ $name โ€” pack failed" - ((fail++)) || true - continue - fi - - if npm publish "$tgz" --registry="$REGISTRY" $DRY_RUN 2>&1 | tail -1 | grep -q '+'; then - echo "โœ… $name" - ((ok++)) || true - else - echo "โš ๏ธ $name (may already exist at this version)" - fi - - rm -f "$tgz" -done <<< "$ORDERED" - -echo -echo "Published: $ok Skipped/Failed: $fail" diff --git a/scripts/publish.sh b/scripts/publish.sh index baadce2..a8664e1 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,19 +1,32 @@ #!/usr/bin/env bash -# publish.sh โ€” Bump version & publish all @uncaged/workflow-* packages +# publish.sh โ€” Bump version, build, test & publish all @uncaged/workflow-* packages # # Usage: -# ./scripts/publish.sh 0.4.0 # explicit version -# ./scripts/publish.sh patch # 0.3.1 โ†’ 0.3.2 -# ./scripts/publish.sh minor # 0.3.1 โ†’ 0.4.0 +# ./scripts/publish.sh patch # 0.3.1 โ†’ 0.3.2 +# ./scripts/publish.sh minor # 0.3.1 โ†’ 0.4.0 +# ./scripts/publish.sh major # 0.3.1 โ†’ 1.0.0 +# ./scripts/publish.sh 0.5.0 # explicit version +# ./scripts/publish.sh patch --dry-run # preview without publishing # # Env (via `cfg` or export): -# GITEA_TOKEN โ€” Gitea npm registry auth +# GITEA_TOKEN โ€” Gitea npm registry auth (used by .npmrc) set -euo pipefail REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$REPO_ROOT" -GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" # needed by .npmrc +GITEA_TOKEN="${GITEA_TOKEN:?GITEA_TOKEN is required}" +REGISTRY="https://git.shazhou.work/api/packages/uncaged/npm/" + +DRY_RUN="" +VERSION_ARG="" +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN="--dry-run" ;; + *) VERSION_ARG="$arg" ;; + esac +done +[[ -z "$VERSION_ARG" ]] && { echo "Usage: publish.sh [--dry-run]"; exit 1; } # โ”€โ”€โ”€ Version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ current_version() { @@ -32,10 +45,11 @@ bump_version() { } CURRENT=$(current_version) -VERSION=$(bump_version "$CURRENT" "${1:?Usage: publish.sh }") +VERSION=$(bump_version "$CURRENT" "$VERSION_ARG") echo "๐Ÿ“ฆ Publish: $CURRENT โ†’ $VERSION" +[[ -n "$DRY_RUN" ]] && echo "๐Ÿ” Dry run mode โ€” no packages will be published" -# โ”€โ”€โ”€ Bump version โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# โ”€โ”€โ”€ Bump version in all public packages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ echo "๐Ÿ”ข Bumping versions..." for dir in packages/*/; do pkg="$dir/package.json" @@ -50,65 +64,113 @@ for dir in packages/*/; do " done -# โ”€โ”€โ”€ Replace workspace:* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "๐Ÿ”— Replacing workspace:* โ†’ $VERSION..." -for dir in packages/*/; do - pkg="$dir/package.json" - [[ -f "$pkg" ]] || continue - node -e " - const fs = require('fs'); - const p = JSON.parse(fs.readFileSync('$pkg','utf8')); - let c = false; - for (const k of ['dependencies','peerDependencies','devDependencies']) { - if (!p[k]) continue; - for (const [n, v] of Object.entries(p[k])) { - if (n.startsWith('@uncaged/') && v === 'workspace:*') { p[k][n] = '$VERSION'; c = true; } - } - } - if (c) fs.writeFileSync('$pkg', JSON.stringify(p, null, 2) + '\n'); - " -done - # โ”€โ”€โ”€ Build โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ echo "๐Ÿ”จ Building..." npm run build -# โ”€โ”€โ”€ Self-test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# โ”€โ”€โ”€ Self-test โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ echo "๐Ÿงช Running tests..." if ! bun test; then echo "โŒ Tests failed โ€” aborting publish" exit 1 fi -# โ”€โ”€โ”€ Publish (delegate to publish-all.sh) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "๐Ÿš€ Publishing via publish-all.sh..." -"$REPO_ROOT/scripts/publish-all.sh" +# โ”€โ”€โ”€ Topological sort of public packages โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +echo "๐Ÿ“ Resolving publish order..." +ORDERED=$(python3 -c " +import json, os +from pathlib import Path -# โ”€โ”€โ”€ Restore workspace:* โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "๐Ÿ”„ Restoring workspace:*..." -for dir in packages/*/; do - pkg="$dir/package.json" - [[ -f "$pkg" ]] || continue - node -e " - const fs = require('fs'); - const p = JSON.parse(fs.readFileSync('$pkg','utf8')); - let c = false; - for (const k of ['dependencies','peerDependencies','devDependencies']) { - if (!p[k]) continue; - for (const [n, v] of Object.entries(p[k])) { - if (n.startsWith('@uncaged/') && v === '$VERSION') { p[k][n] = 'workspace:*'; c = true; } - } - } - if (c) fs.writeFileSync('$pkg', JSON.stringify(p, null, 2) + '\n'); - " -done +pkgs_dir = Path('$REPO_ROOT/packages') +name_to_dir = {} +deps_graph = {} + +for d in sorted(pkgs_dir.iterdir()): + pj = d / 'package.json' + if not pj.exists(): + continue + data = json.loads(pj.read_text()) + name = data.get('name', '') + if not name.startswith('@uncaged/'): + continue + if data.get('private'): + continue + name_to_dir[name] = d.name + local_deps = set() + for section in ('dependencies', 'devDependencies', 'peerDependencies'): + for dep, ver in data.get(section, {}).items(): + if dep.startswith('@uncaged/') and ver == 'workspace:*': + local_deps.add(dep) + deps_graph[name] = local_deps + +# Kahn's algorithm โ€” deps-first order +in_degree = {n: len([d for d in ds if d in deps_graph]) for n, ds in deps_graph.items()} +queue = sorted(n for n, deg in in_degree.items() if deg == 0) +result = [] +while queue: + node = queue.pop(0) + result.append(node) + for n, ds in deps_graph.items(): + if node in ds: + in_degree[n] -= 1 + if in_degree[n] == 0: + queue.append(n) + queue.sort() + +for name in result: + print(name_to_dir[name]) +") + +# โ”€โ”€โ”€ Regenerate lockfile for correct workspace:* resolution โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +rm -f bun.lock +bun install + +# โ”€โ”€โ”€ Publish via bun pm pack + npm publish โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +echo "๐Ÿš€ Publishing..." +ok=0 +fail=0 + +while IFS= read -r pkg; do + dir="$REPO_ROOT/packages/$pkg" + name=$(node -e "console.log(require('./$dir/package.json').name)") + + cd "$dir" + + # bun pm pack resolves workspace:* โ†’ actual versions in the tarball + tgz=$(bun pm pack 2>&1 | grep '\.tgz' | grep -v packed | head -1 | tr -d ' ') + + if [[ -z "$tgz" || ! -f "$tgz" ]]; then + echo "โŒ $name โ€” pack failed" + ((fail++)) || true + continue + fi + + if npm publish "$tgz" --registry="$REGISTRY" $DRY_RUN 2>&1 | tail -1 | grep -q '+'; then + echo "โœ… $name@$VERSION" + ((ok++)) || true + else + echo "โš ๏ธ $name (may already exist at this version)" + fi + + rm -f "$tgz" +done <<< "$ORDERED" + +cd "$REPO_ROOT" + +echo +echo "Published: $ok Skipped/Failed: $fail" + +# โ”€โ”€โ”€ Restore workspace:* (bun pack doesn't modify source, but version bump did) โ”€ +echo "๐Ÿ”„ Restoring lockfile..." +rm -f bun.lock +bun install # โ”€โ”€โ”€ Commit โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -echo "๐Ÿ“ Committing..." -git add -A -git commit -m "chore: publish v${VERSION} - -ๅฐๆฉ˜ " -git push +if [[ -z "$DRY_RUN" ]]; then + echo "๐Ÿ“ Committing..." + git add -A + git commit -m "chore: publish v${VERSION}" + git push +fi echo "โœ… v${VERSION} published"