Skip to content

Commit 4f968a3

Browse files
[FSSDK-12882] release pipeline update (#343)
* [FSSDK-12882] release pipeline update * [FSSDK-12882] review feedback * [FSSDK-12882] review feedback * [FSSDK-12882] optimization * [FSSDK-12882] optimization * [FSSDK-12881] feedback addressed
1 parent 37bf184 commit 4f968a3

3 files changed

Lines changed: 238 additions & 20 deletions

File tree

‎.github/workflows/ghr_backfill.yml‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Backfill GitHub Package Registry
2+
3+
# Manually triggered. Copies versions already published on npm into the GitHub
4+
# Package Registry so GHR mirrors npm. Re-publishing the npm tarball guarantees
5+
# the GHR copy is byte-identical to what npm consumers received (no rebuild).
6+
#
7+
# Idempotent: scripts/publish.sh skips any version already present on GHR, so
8+
# this can be re-run safely.
9+
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
version:
14+
description: "Single version to backfill (e.g. 4.0.0). Leave blank to process all published npm versions (versions already on GHR are skipped)."
15+
required: false
16+
default: ""
17+
dry_run:
18+
description: "Dry run: report what would be published to GHR without publishing."
19+
type: boolean
20+
required: false
21+
default: false
22+
23+
jobs:
24+
backfill:
25+
name: Backfill npm versions to GHR
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
packages: write
30+
steps:
31+
- name: Checkout branch
32+
uses: actions/checkout@v4
33+
with:
34+
persist-credentials: false
35+
36+
- name: Set up Node
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: 18
40+
41+
- name: Configure GHR auth
42+
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc
43+
44+
- name: Backfill
45+
env:
46+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
47+
DRY_RUN: ${{ inputs.dry_run }}
48+
INPUT_VERSION: ${{ inputs.version }}
49+
run: |
50+
set -euo pipefail
51+
pkg="@optimizely/react-sdk"
52+
53+
if [[ -n "$INPUT_VERSION" ]]; then
54+
versions="$INPUT_VERSION"
55+
else
56+
versions=$(npm view "$pkg" versions --json --registry https://registry.npmjs.org | jq -r '.[]')
57+
fi
58+
59+
for v in $versions; do
60+
echo "::group::${pkg}@${v}"
61+
# npm pack writes the tarball filename to stdout and notices/errors to
62+
# stderr; keep stderr visible so pack failures (auth/404/network) show
63+
# in the logs. pipefail (set above) makes a failed pack abort the job.
64+
tarball=$(npm pack "${pkg}@${v}" --registry https://registry.npmjs.org | tail -n1)
65+
scripts/publish.sh "https://npm.pkg.github.com" "$tarball"
66+
rm -f "$tarball"
67+
echo "::endgroup::"
68+
done
Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
name: Publish React SDK to NPM
1+
name: Publish React SDK
22

33
on:
44
release:
55
types: [ published ]
66

77
jobs:
88
publish:
9-
name: Publish to NPM
9+
name: Publish to NPM and GitHub Package Registry
1010
runs-on: ubuntu-latest
11+
permissions:
12+
contents: read
13+
packages: write
1114
steps:
1215
- name: Checkout branch
1316
uses: actions/checkout@v4
17+
with:
18+
persist-credentials: false
1419

1520
- name: Set up Node
1621
uses: actions/setup-node@v4
@@ -19,27 +24,29 @@ jobs:
1924
registry-url: "https://registry.npmjs.org/"
2025
always-auth: "true"
2126
cache: 'npm'
22-
env:
23-
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_REACT_TO_NPM_FROM_GITHUB }}
27+
28+
- name: Configure GHR auth
29+
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc
2430

2531
- name: Install dependencies
26-
run: npm ci
32+
run: npm ci --ignore-scripts
2733

28-
- id: npm-tag
29-
name: Determine NPM tag
34+
- name: Test, build, and pack
35+
id: pack
3036
run: |
31-
version=$(jq -r '.version' package.json)
32-
if [[ "$version" == *"-beta"* ]]; then
33-
echo "npm-tag=beta" >> "$GITHUB_OUTPUT"
34-
elif [[ "$version" == *"-alpha"* ]]; then
35-
echo "npm-tag=alpha" >> "$GITHUB_OUTPUT"
36-
elif [[ "$version" == *"-rc"* ]]; then
37-
echo "npm-tag=rc" >> "$GITHUB_OUTPUT"
38-
else
39-
echo "npm-tag=latest" >> "$GITHUB_OUTPUT"
40-
fi
41-
42-
- name: Test, build, then publish
37+
npm run test
38+
npm run build
39+
tarball=$(npm pack --ignore-scripts | tail -n1)
40+
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"
41+
42+
- name: Publish to NPM
4343
env:
4444
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_REACT_TO_NPM_FROM_GITHUB }}
45-
run: npm publish --tag ${{ steps.npm-tag.outputs['npm-tag'] }}
45+
TARBALL: ${{ steps.pack.outputs.tarball }}
46+
run: scripts/publish.sh "https://registry.npmjs.org" "$TARBALL"
47+
48+
- name: Publish to GitHub Package Registry
49+
env:
50+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
TARBALL: ${{ steps.pack.outputs.tarball }}
52+
run: scripts/publish.sh "https://npm.pkg.github.com" "$TARBALL"

‎scripts/publish.sh‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Registry-agnostic, idempotent publish for @optimizely/react-sdk.
4+
#
5+
# Usage:
6+
# scripts/publish.sh <registry-url> [tarball]
7+
#
8+
# Args:
9+
# registry-url Target registry, e.g. https://registry.npmjs.org
10+
# or https://npm.pkg.github.com
11+
# tarball Optional. When given, publishes that packed tarball instead
12+
# of the current working directory (used by the GHR backfill).
13+
#
14+
# Env:
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.
25+
#
26+
# Behavior:
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>.<minor>-latest` (its own release line), so `latest` never
39+
# moves backwards onto an older release — this covers both an older
40+
# major (5.x shipped after 6.x) and an older minor of the current major
41+
# (6.4.1 shipped while latest is 6.5.0).
42+
set -euo pipefail
43+
44+
dry_run="${DRY_RUN:-false}"
45+
46+
registry="${1:?usage: publish.sh <registry-url> [tarball]}"
47+
tarball="${2:-}"
48+
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)
58+
fi
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+
rest=${version#*.}
124+
minor=${rest%%.*}
125+
tag="v${major}.${minor}-latest"
126+
echo "Current latest is ${current_latest}; ${version} is not newer, tagging as ${tag} to preserve latest."
127+
fi
128+
;;
129+
esac
130+
131+
if [[ "$dry_run" == "true" ]]; then
132+
echo "[dry-run] would publish ${pkg}@${version} (tag: ${tag}) to ${registry}"
133+
exit 0
134+
fi
135+
136+
echo "Publishing ${pkg}@${version} (tag: ${tag}) to ${registry}"
137+
if [[ -n "$tarball" ]]; then
138+
# The tarball is a prebuilt artifact; skip lifecycle scripts (prepublishOnly
139+
# = test + build) that would otherwise run from the current package.json.
140+
npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts
141+
else
142+
npm publish --registry "$registry" --tag "$tag"
143+
fi

0 commit comments

Comments
 (0)