|
3 | 3 | # Registry-agnostic, idempotent publish for @optimizely/optimizely-sdk. |
4 | 4 | # |
5 | 5 | # Usage: |
6 | | -# scripts/publish.sh <registry-url> <tarball> <dist-tag> |
| 6 | +# scripts/publish.sh <registry-url> [tarball] |
7 | 7 | # |
8 | 8 | # Args: |
9 | 9 | # registry-url Target registry, e.g. https://registry.npmjs.org |
10 | 10 | # or https://npm.pkg.github.com |
11 | | -# tarball Prebuilt package tarball (output of `npm pack`) to publish. |
12 | | -# The release flow packs it once and publishes the same bytes to |
13 | | -# both registries; the backfill packs each version from npm. |
14 | | -# dist-tag npm dist-tag to publish under. The caller decides the tag; this |
15 | | -# script is deliberately tag-agnostic so the JS SDK keeps its |
16 | | -# existing tag logic in the workflow (latest / v{major}-latest / |
17 | | -# beta / alpha / rc). |
| 11 | +# tarball Optional. When given, publishes that packed tarball instead |
| 12 | +# of the current working directory (used by the GHR backfill). |
18 | 13 | # |
19 | 14 | # Env: |
20 | | -# NODE_AUTH_TOKEN Auth token for the target registry. An .npmrc entry must |
21 | | -# reference it for <registry-url>'s host, e.g. |
22 | | -# `//<host>/:_authToken=${NODE_AUTH_TOKEN}` (setup-node writes |
23 | | -# this for npm; the workflow adds one for GitHub Package |
24 | | -# Registry). We pass --registry explicitly, so no |
25 | | -# scope-to-registry routing is needed (and the @optimizely |
26 | | -# scope is intentionally NOT routed to GPR, so `npm ci` still |
27 | | -# installs dependencies from npm). |
28 | | -# DRY_RUN When "true", report the action (publish vs. skip) without |
29 | | -# actually publishing. |
| 15 | +# NODE_AUTH_TOKEN Auth token for the target registry. Used both to read the |
| 16 | +# registry (existence/dist-tag lookup) and, via the caller's |
| 17 | +# .npmrc entry (`//<host>/:_authToken=${NODE_AUTH_TOKEN}`, |
| 18 | +# which setup-node writes), to publish. This script passes |
| 19 | +# --registry explicitly, so no scope-to-registry routing is |
| 20 | +# required (and the GHR job intentionally does NOT route |
| 21 | +# @optimizely to GHR, so that `npm ci` still installs |
| 22 | +# dependencies from npm). |
| 23 | +# DRY_RUN When "true", report what would happen (publish vs. skip) |
| 24 | +# without actually publishing. |
30 | 25 | # |
31 | 26 | # Behavior: |
32 | | -# - Skips (exit 0) if the version already exists on the target registry, so |
33 | | -# re-running a release or the backfill is always a safe no-op. |
34 | | -# - Publishes the prebuilt tarball with --ignore-scripts so lifecycle scripts |
35 | | -# (prepublishOnly = test, prepare = build) don't re-run from package.json. |
| 27 | +# - Reads the target registry's packument once (a single HTTP GET) and: |
| 28 | +# * 200 -> package exists; skip (exit 0) if this exact version is already |
| 29 | +# present, so re-running a release or the backfill is a safe no-op. |
| 30 | +# * 404 -> package/version absent; proceed to publish. |
| 31 | +# * any other status or a network failure -> abort (exit 1) rather than |
| 32 | +# guess. A flaky lookup must never be misread as "not published" and |
| 33 | +# trigger a publish. |
| 34 | +# - Computes the dist-tag from the version: |
| 35 | +# * pre-releases (beta/alpha/rc) get their own tag, never `latest`. |
| 36 | +# * a stable release gets `latest` ONLY when it is strictly greater (by |
| 37 | +# semver) than the registry's current `latest`. Otherwise it is tagged |
| 38 | +# `v<major>-latest` (the JS SDK's long-standing per-major release line, |
| 39 | +# which consumers install via @<pkg>@v<major>-latest), so `latest` never |
| 40 | +# moves backwards onto an older release — e.g. 5.x shipped after 6.x, or |
| 41 | +# a 6.4.1 patch shipped while latest is 6.5.0. |
36 | 42 | set -euo pipefail |
37 | 43 |
|
38 | | -registry="${1:?usage: publish.sh <registry-url> <tarball> <dist-tag>}" |
39 | | -tarball="${2:?usage: publish.sh <registry-url> <tarball> <dist-tag>}" |
40 | | -tag="${3:?usage: publish.sh <registry-url> <tarball> <dist-tag>}" |
41 | 44 | dry_run="${DRY_RUN:-false}" |
42 | 45 |
|
43 | | -# Derive name/version from the tarball's own package.json so the existence guard |
44 | | -# matches exactly what we're about to publish. |
45 | | -meta=$(tar -xzO -f "$tarball" package/package.json) |
46 | | -pkg=$(printf '%s' "$meta" | jq -r '.name') |
47 | | -version=$(printf '%s' "$meta" | jq -r '.version') |
| 46 | +registry="${1:?usage: publish.sh <registry-url> [tarball]}" |
| 47 | +tarball="${2:-}" |
48 | 48 |
|
49 | | -if npm view "${pkg}@${version}" version --registry "$registry" >/dev/null 2>&1; then |
50 | | - echo "Version ${pkg}@${version} already on ${registry}, skipping." |
51 | | - exit 0 |
| 49 | +if [[ -n "$tarball" ]]; then |
| 50 | + # Derive name/version from the tarball's own package.json so the guard matches |
| 51 | + # exactly what we're about to publish (backfill of historical versions). |
| 52 | + meta=$(tar -xzO -f "$tarball" package/package.json) |
| 53 | + pkg=$(printf '%s' "$meta" | jq -r '.name') |
| 54 | + version=$(printf '%s' "$meta" | jq -r '.version') |
| 55 | +else |
| 56 | + pkg=$(jq -r '.name' package.json) |
| 57 | + version=$(jq -r '.version' package.json) |
52 | 58 | fi |
53 | 59 |
|
| 60 | +# Returns 0 if $1 is strictly greater than $2 (numeric major.minor.patch). Only |
| 61 | +# called for stable versions, so no pre-release precedence handling is needed. |
| 62 | +version_gt() { |
| 63 | + local -a a b |
| 64 | + local i x y |
| 65 | + IFS=. read -ra a <<< "$1" |
| 66 | + IFS=. read -ra b <<< "$2" |
| 67 | + for i in 0 1 2; do |
| 68 | + x=${a[i]:-0} |
| 69 | + y=${b[i]:-0} |
| 70 | + (( 10#$x > 10#$y )) && return 0 |
| 71 | + (( 10#$x < 10#$y )) && return 1 |
| 72 | + done |
| 73 | + return 1 |
| 74 | +} |
| 75 | + |
| 76 | +# --- Existence / current-latest lookup (single packument GET) ---------------- |
| 77 | +# npm scoped names are URL-encoded with the slash as %2f: @scope/name. |
| 78 | +pkg_encoded="${pkg//\//%2f}" |
| 79 | +packument_url="${registry%/}/${pkg_encoded}" |
| 80 | + |
| 81 | +body_file=$(mktemp) |
| 82 | +trap 'rm -f "$body_file"' EXIT |
| 83 | + |
| 84 | +# curl -sS (no --fail) returns 0 for any HTTP response, non-zero only on |
| 85 | +# network/protocol errors — so we can cleanly separate "server answered" from |
| 86 | +# "could not reach server". |
| 87 | +if ! http_code=$(curl -sS -m 30 -o "$body_file" -w '%{http_code}' \ |
| 88 | + -H "Authorization: Bearer ${NODE_AUTH_TOKEN:-}" "$packument_url"); then |
| 89 | + echo "ERROR: request to ${packument_url} failed (network/timeout); refusing to publish on an ambiguous result." >&2 |
| 90 | + exit 1 |
| 91 | +fi |
| 92 | + |
| 93 | +current_latest="" |
| 94 | +case "$http_code" in |
| 95 | + 200) |
| 96 | + if jq -e --arg v "$version" '.versions[$v] != null' "$body_file" >/dev/null 2>&1; then |
| 97 | + echo "Version ${pkg}@${version} already on ${registry}, skipping." |
| 98 | + exit 0 |
| 99 | + fi |
| 100 | + current_latest=$(jq -r '.["dist-tags"].latest // empty' "$body_file") |
| 101 | + ;; |
| 102 | + 404) |
| 103 | + # Package (or this version) not published yet; nothing to compare against. |
| 104 | + current_latest="" |
| 105 | + ;; |
| 106 | + *) |
| 107 | + echo "ERROR: unexpected HTTP ${http_code} querying ${packument_url}; refusing to publish on an ambiguous result." >&2 |
| 108 | + exit 1 |
| 109 | + ;; |
| 110 | +esac |
| 111 | + |
| 112 | +# --- dist-tag selection ------------------------------------------------------ |
| 113 | +case "$version" in |
| 114 | + *-beta*) tag=beta ;; |
| 115 | + *-alpha*) tag=alpha ;; |
| 116 | + *-rc*) tag=rc ;; |
| 117 | + *) |
| 118 | + # Stable release: `latest` only if strictly greater than the current latest. |
| 119 | + if [[ -z "$current_latest" ]] || version_gt "$version" "$current_latest"; then |
| 120 | + tag=latest |
| 121 | + else |
| 122 | + major=${version%%.*} |
| 123 | + tag="v${major}-latest" |
| 124 | + echo "Current latest is ${current_latest}; ${version} is not newer, tagging as ${tag} to preserve latest." |
| 125 | + fi |
| 126 | + ;; |
| 127 | +esac |
| 128 | + |
54 | 129 | if [[ "$dry_run" == "true" ]]; then |
55 | 130 | echo "[dry-run] would publish ${pkg}@${version} (tag: ${tag}) to ${registry}" |
56 | 131 | exit 0 |
57 | 132 | fi |
58 | 133 |
|
59 | 134 | echo "Publishing ${pkg}@${version} (tag: ${tag}) to ${registry}" |
60 | | -npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts |
| 135 | +if [[ -n "$tarball" ]]; then |
| 136 | + # The tarball is a prebuilt artifact; skip lifecycle scripts (prepublishOnly |
| 137 | + # = test + build) that would otherwise run from the current package.json. |
| 138 | + npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts |
| 139 | +else |
| 140 | + npm publish --registry "$registry" --tag "$tag" |
| 141 | +fi |
0 commit comments