Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions .github/workflows/ghr_backfill.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Backfill GitHub Package Registry

# Manually triggered. Copies versions already published on npm into the GitHub
# Package Registry so GHR mirrors npm. Re-publishing the npm tarball guarantees
# the GHR copy is byte-identical to what npm consumers received (no rebuild).
#
# Idempotent: scripts/publish.sh skips any version already present on GHR, so
# this can be re-run safely.

on:
workflow_dispatch:
inputs:
version:
description: "Single version to backfill (e.g. 4.0.0). Leave blank to process all published npm versions (versions already on GHR are skipped)."
required: false
default: ""
dry_run:
description: "Dry run: report what would be published to GHR without publishing."
type: boolean
required: false
default: false

jobs:
backfill:
name: Backfill npm versions to GHR
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout branch
uses: actions/checkout@v4
Comment thread
arnica-github-connector[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 18

- name: Configure GHR auth
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc

- name: Backfill
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ inputs.dry_run }}
INPUT_VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
pkg="@optimizely/react-sdk"

if [[ -n "$INPUT_VERSION" ]]; then
versions="$INPUT_VERSION"
else
versions=$(npm view "$pkg" versions --json --registry https://registry.npmjs.org | jq -r '.[]')
fi

for v in $versions; do
echo "::group::${pkg}@${v}"
# npm pack writes the tarball filename to stdout and notices/errors to
# stderr; keep stderr visible so pack failures (auth/404/network) show
# in the logs. pipefail (set above) makes a failed pack abort the job.
tarball=$(npm pack "${pkg}@${v}" --registry https://registry.npmjs.org | tail -n1)
scripts/publish.sh "https://npm.pkg.github.com" "$tarball"
rm -f "$tarball"
echo "::endgroup::"
done
Comment thread
arnica-github-connector[bot] marked this conversation as resolved.
47 changes: 27 additions & 20 deletions .github/workflows/react_release.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
name: Publish React SDK to NPM
name: Publish React SDK

on:
release:
types: [ published ]

jobs:
publish:
name: Publish to NPM
name: Publish to NPM and GitHub Package Registry
runs-on: ubuntu-latest
Comment thread
junaed-optimizely marked this conversation as resolved.
permissions:
contents: read
packages: write
steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Set up Node
uses: actions/setup-node@v4
Expand All @@ -19,27 +24,29 @@ jobs:
registry-url: "https://registry.npmjs.org/"
always-auth: "true"
cache: 'npm'
env:
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_REACT_TO_NPM_FROM_GITHUB }}

- name: Configure GHR auth
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc

- name: Install dependencies
run: npm ci
run: npm ci --ignore-scripts

- id: npm-tag
name: Determine NPM tag
- name: Test, build, and pack
id: pack
run: |
version=$(jq -r '.version' package.json)
if [[ "$version" == *"-beta"* ]]; then
echo "npm-tag=beta" >> "$GITHUB_OUTPUT"
elif [[ "$version" == *"-alpha"* ]]; then
echo "npm-tag=alpha" >> "$GITHUB_OUTPUT"
elif [[ "$version" == *"-rc"* ]]; then
echo "npm-tag=rc" >> "$GITHUB_OUTPUT"
else
echo "npm-tag=latest" >> "$GITHUB_OUTPUT"
fi

- name: Test, build, then publish
npm run test
npm run build
tarball=$(npm pack --ignore-scripts | tail -n1)
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"

- name: Publish to NPM
env:
NODE_AUTH_TOKEN: ${{ secrets.PUBLISH_REACT_TO_NPM_FROM_GITHUB }}
run: npm publish --tag ${{ steps.npm-tag.outputs['npm-tag'] }}
TARBALL: ${{ steps.pack.outputs.tarball }}
run: scripts/publish.sh "https://registry.npmjs.org" "$TARBALL"

- name: Publish to GitHub Package Registry
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TARBALL: ${{ steps.pack.outputs.tarball }}
run: scripts/publish.sh "https://npm.pkg.github.com" "$TARBALL"
143 changes: 143 additions & 0 deletions scripts/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
#!/usr/bin/env bash
#
# Registry-agnostic, idempotent publish for @optimizely/react-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`. Otherwise it is tagged
# `v<major>.<minor>-latest` (its own release line), so `latest` never
# moves backwards onto an older release — this covers both an older
# major (5.x shipped after 6.x) and an older minor of the current major
# (6.4.1 shipped while latest is 6.5.0).
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%%.*}
rest=${version#*.}
minor=${rest%%.*}
tag="v${major}.${minor}-latest"
echo "Current latest is ${current_latest}; ${version} is not newer, tagging as ${tag} to preserve latest."
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
Loading