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