Skip to content

Commit 3bad946

Browse files
[FSSDK-12881] improvements
1 parent eb92334 commit 3bad946

3 files changed

Lines changed: 140 additions & 95 deletions

File tree

.github/workflows/ghr_backfill.yml

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,34 @@
11
name: Backfill GitHub Package Registry
22

33
# Manually triggered. Copies versions already published on npm into the GitHub
4-
# Package Registry (GPR) so GPR mirrors npm. Re-publishing the npm tarball
5-
# guarantees the GPR copy is byte-identical to what npm consumers received
4+
# Package Registry (GHR) so GHR mirrors npm. Re-publishing the npm tarball
5+
# guarantees the GHR copy is byte-identical to what npm consumers received
66
# (no rebuild).
77
#
8-
# dist-tags: each backfilled version is published under the dist-tag npm
9-
# currently assigns it (latest / v{major}-latest / beta / alpha / rc), so GPR
10-
# mirrors npm's pointers. Versions npm no longer tags are published under an
11-
# "imported" tag, which never disturbs `latest`.
8+
# dist-tags: scripts/publish.sh computes the tag for each version from the
9+
# version string against GHR's current latest (latest only when strictly newer,
10+
# otherwise v<major>-latest; beta/alpha/rc for pre-releases), so latest never
11+
# moves backwards while backfilling historical versions.
1212
#
13-
# Idempotent: scripts/publish.sh skips any version already present on GPR, so
13+
# Idempotent: scripts/publish.sh skips any version already present on GHR, so
1414
# this can be re-run safely.
1515

1616
on:
1717
workflow_dispatch:
1818
inputs:
1919
version:
20-
description: "Single version to backfill (e.g. 5.3.4). Leave blank to backfill all versions."
20+
description: "Single version to backfill (e.g. 5.3.4). Leave blank to process all published npm versions (versions already on GHR are skipped)."
2121
required: false
2222
default: ""
2323
dry_run:
24-
description: "Dry run: report what would be published to GPR without publishing."
24+
description: "Dry run: report what would be published to GHR without publishing."
2525
type: boolean
2626
required: false
2727
default: false
2828

2929
jobs:
3030
backfill:
31-
name: Backfill npm versions to GPR
31+
name: Backfill npm versions to GHR
3232
runs-on: ubuntu-latest
3333
permissions:
3434
contents: read
@@ -52,8 +52,8 @@ jobs:
5252
- name: Backfill
5353
env:
5454
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55-
DRY_RUN: ${{ github.event.inputs.dry_run }}
56-
INPUT_VERSION: ${{ github.event.inputs.version }}
55+
DRY_RUN: ${{ inputs.dry_run }}
56+
INPUT_VERSION: ${{ inputs.version }}
5757
run: |
5858
set -euo pipefail
5959
pkg="@optimizely/optimizely-sdk"
@@ -66,21 +66,13 @@ jobs:
6666
versions=$(npm view "$pkg" versions --json --registry "$npm_registry" | jq -r '.[]')
6767
fi
6868
69-
# Read npm's current dist-tags once and build a version -> tag map, so
70-
# each backfilled version lands under the same tag it has on npm.
71-
# `npm dist-tag ls` prints "tag: version" lines. Anything not currently
72-
# tagged falls back to "imported" (a neutral tag that never moves latest).
73-
dist_tags_json=$(npm dist-tag ls "$pkg" --registry "$npm_registry" \
74-
| jq -R -s 'split("\n") | map(select(length > 0) | split(": ")) | map({(.[1]): .[0]}) | add // {}')
75-
7669
for v in $versions; do
7770
echo "::group::${pkg}@${v}"
78-
tag=$(printf '%s' "$dist_tags_json" | jq -r --arg v "$v" '.[$v] // "imported"')
7971
# npm pack writes the tarball filename to stdout and notices/errors to
8072
# stderr; keep stderr visible so pack failures (auth/404/network) show
8173
# in the logs. pipefail (set above) makes a failed pack abort the job.
8274
tarball=$(npm pack "${pkg}@${v}" --registry "$npm_registry" | tail -n1)
83-
scripts/publish.sh "$gpr_registry" "$tarball" "$tag"
75+
scripts/publish.sh "$gpr_registry" "$tarball"
8476
rm -f "$tarball"
8577
echo "::endgroup::"
8678
done

.github/workflows/release.yml

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ jobs:
2929
# Add auth for the GitHub Package Registry publish. npm expands
3030
# ${NODE_AUTH_TOKEN} at runtime per-step, so each publish step uses the
3131
# right token for its registry. We do NOT route the @optimizely scope to
32-
# GPR, so `npm ci` still resolves dependencies from npm; publish.sh passes
33-
# --registry explicitly to target GPR.
32+
# GHR, so `npm ci` still resolves dependencies from npm; publish.sh passes
33+
# --registry explicitly to target GHR.
3434
- name: Configure GitHub Package Registry auth
3535
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc
3636

@@ -39,43 +39,8 @@ jobs:
3939
- name: Install dependencies
4040
run: npm ci --ignore-scripts
4141

42-
- id: latest-release
43-
name: Export latest release git tag
44-
run: |
45-
echo "latest-release-tag=$(curl -qsSL \
46-
-H "Accept: application/vnd.github+json" \
47-
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
48-
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/releases/latest" \
49-
| jq -r .tag_name)" >> $GITHUB_OUTPUT
50-
51-
- id: npm-tag
52-
name: Determine NPM tag
53-
env:
54-
GITHUB_RELEASE_TAG: ${{ github.event.release.tag_name }}
55-
run: |
56-
VERSION=$(jq -r '.version' package.json)
57-
LATEST_RELEASE_TAG="${{ steps.latest-release.outputs['latest-release-tag']}}"
58-
59-
if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
60-
RELEASE_TAG=${GITHUB_REF#refs/tags/}
61-
else
62-
RELEASE_TAG=$GITHUB_RELEASE_TAG
63-
fi
64-
65-
if [[ $RELEASE_TAG == $LATEST_RELEASE_TAG ]]; then
66-
echo "npm-tag=latest" >> "$GITHUB_OUTPUT"
67-
elif [[ "$VERSION" == *"-beta"* ]]; then
68-
echo "npm-tag=beta" >> "$GITHUB_OUTPUT"
69-
elif [[ "$VERSION" == *"-alpha"* ]]; then
70-
echo "npm-tag=alpha" >> "$GITHUB_OUTPUT"
71-
elif [[ "$VERSION" == *"-rc"* ]]; then
72-
echo "npm-tag=rc" >> "$GITHUB_OUTPUT"
73-
else
74-
echo "npm-tag=v$(echo $VERSION | awk -F. '{print $1}')-latest" >> "$GITHUB_OUTPUT"
75-
fi
76-
7742
# Test, build, and pack a single tarball. Both registries publish this
78-
# same artifact, so npm and GPR receive byte-identical bytes and the test
43+
# same artifact, so npm and GHR receive byte-identical bytes and the test
7944
# suite runs only once. --ignore-scripts on pack avoids a rebuild.
8045
- id: pack
8146
name: Test, build, and pack
@@ -85,18 +50,25 @@ jobs:
8550
tarball=$(npm pack --ignore-scripts | tail -n1)
8651
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"
8752
53+
# publish.sh derives name/version from the tarball and computes the
54+
# dist-tag itself: `latest` only when strictly newer than the registry's
55+
# current latest, otherwise v<major>-latest; beta/alpha/rc for
56+
# pre-releases. npm publishes first; if it fails the job stops before GHR,
57+
# keeping the registries in sync.
8858
- id: release
8959
name: Publish to NPM
9060
env:
9161
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
62+
TARBALL: ${{ steps.pack.outputs.tarball }}
9263
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }}
93-
run: scripts/publish.sh "https://registry.npmjs.org" "${{ steps.pack.outputs.tarball }}" "${{ steps.npm-tag.outputs['npm-tag'] }}"
64+
run: scripts/publish.sh "https://registry.npmjs.org" "$TARBALL"
9465

9566
- name: Publish to GitHub Package Registry
9667
env:
9768
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69+
TARBALL: ${{ steps.pack.outputs.tarball }}
9870
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }}
99-
run: scripts/publish.sh "https://npm.pkg.github.com" "${{ steps.pack.outputs.tarball }}" "${{ steps.npm-tag.outputs['npm-tag'] }}"
71+
run: scripts/publish.sh "https://npm.pkg.github.com" "$TARBALL"
10072

10173
# - name: Report results to Jellyfish
10274
# uses: optimizely/jellyfish-deployment-reporter-action@main

scripts/publish.sh

Lines changed: 115 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,139 @@
33
# Registry-agnostic, idempotent publish for @optimizely/optimizely-sdk.
44
#
55
# Usage:
6-
# scripts/publish.sh <registry-url> <tarball> <dist-tag>
6+
# scripts/publish.sh <registry-url> [tarball]
77
#
88
# Args:
99
# registry-url Target registry, e.g. https://registry.npmjs.org
1010
# 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).
1813
#
1914
# 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.
3025
#
3126
# 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.
3642
set -euo pipefail
3743

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>}"
4144
dry_run="${DRY_RUN:-false}"
4245

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:-}"
4848

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)
5258
fi
5359

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+
54129
if [[ "$dry_run" == "true" ]]; then
55130
echo "[dry-run] would publish ${pkg}@${version} (tag: ${tag}) to ${registry}"
56131
exit 0
57132
fi
58133

59134
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

Comments
 (0)