Skip to content
Open
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
86 changes: 86 additions & 0 deletions .github/workflows/ghr_backfill.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Backfill GitHub Package Registry

# Manually triggered. Copies versions already published on npm into the GitHub
# Package Registry (GPR) so GPR mirrors npm. Re-publishing the npm tarball
# guarantees the GPR copy is byte-identical to what npm consumers received
# (no rebuild).
#
# dist-tags: each backfilled version is published under the dist-tag npm
# currently assigns it (latest / v{major}-latest / beta / alpha / rc), so GPR
# mirrors npm's pointers. Versions npm no longer tags are published under an
# "imported" tag, which never disturbs `latest`.
#
# Idempotent: scripts/publish.sh skips any version already present on GPR, so
# this can be re-run safely.

on:
workflow_dispatch:
inputs:
version:
description: "Single version to backfill (e.g. 5.3.4). Leave blank to backfill all versions."
required: false
default: ""
dry_run:
description: "Dry run: report what would be published to GPR without publishing."
type: boolean
required: false
default: false

jobs:
backfill:
name: Backfill npm versions to GPR
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 GitHub Package Registry auth
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Backfill
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
pkg="@optimizely/optimizely-sdk"
npm_registry="https://registry.npmjs.org"
gpr_registry="https://npm.pkg.github.com"

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

# Read npm's current dist-tags once and build a version -> tag map, so
# each backfilled version lands under the same tag it has on npm.
# `npm dist-tag ls` prints "tag: version" lines. Anything not currently
# tagged falls back to "imported" (a neutral tag that never moves latest).
dist_tags_json=$(npm dist-tag ls "$pkg" --registry "$npm_registry" \
| jq -R -s 'split("\n") | map(select(length > 0) | split(": ")) | map({(.[1]): .[0]}) | add // {}')

for v in $versions; do
echo "::group::${pkg}@${v}"
tag=$(printf '%s' "$dist_tags_json" | jq -r --arg v "$v" '.[$v] // "imported"')
# 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 "$npm_registry" | tail -n1)
scripts/publish.sh "$gpr_registry" "$tarball" "$tag"
rm -f "$tarball"
echo "::endgroup::"
done
Comment on lines +69 to +86
53 changes: 39 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Publish SDK to NPM
name: Publish SDK to NPM and GitHub Package Registry

on:
release:
Expand All @@ -7,24 +7,37 @@ on:

jobs:
publish:
name: Publish to NPM
name: Publish to NPM and GitHub Package Registry
runs-on: ubuntu-latest
if: ${{ github.event_name == 'workflow_dispatch' || !github.event.release.draft }}
permissions:
contents: read
packages: write
steps:
- name: Checkout branch
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 18
registry-url: "https://registry.npmjs.org/"
always-auth: "true"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}

# Add auth for the GitHub Package Registry publish. npm expands
# ${NODE_AUTH_TOKEN} at runtime per-step, so each publish step uses the
# right token for its registry. We do NOT route the @optimizely scope to
# GPR, so `npm ci` still resolves dependencies from npm; publish.sh passes
# --registry explicitly to target GPR.
- name: Configure GitHub Package Registry auth
run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc

# Deps only. --ignore-scripts stops `prepare` from building here; we build
# exactly once in the pack step below.
- name: Install dependencies
run: npm install
run: npm ci --ignore-scripts

- id: latest-release
name: Export latest release git tag
Expand All @@ -42,7 +55,7 @@ jobs:
run: |
VERSION=$(jq -r '.version' package.json)
LATEST_RELEASE_TAG="${{ steps.latest-release.outputs['latest-release-tag']}}"

if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
RELEASE_TAG=${GITHUB_REF#refs/tags/}
else
Expand All @@ -61,17 +74,29 @@ jobs:
echo "npm-tag=v$(echo $VERSION | awk -F. '{print $1}')-latest" >> "$GITHUB_OUTPUT"
fi

# Test, build, and pack a single tarball. Both registries publish this
# same artifact, so npm and GPR receive byte-identical bytes and the test
# suite runs only once. --ignore-scripts on pack avoids a rebuild.
- id: pack
name: Test, build, and pack
run: |
npm test
npm run build
tarball=$(npm pack --ignore-scripts | tail -n1)
echo "tarball=$tarball" >> "$GITHUB_OUTPUT"

- id: release
name: Test, build and publish to npm
name: Publish to NPM
env:
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
run: |
if [[ ${{ github.event_name }} == "workflow_dispatch" ]]; then
DRY_RUN="--dry-run"
fi
npm publish --tag=${{ steps.npm-tag.outputs['npm-tag'] }} $DRY_RUN
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }}
run: scripts/publish.sh "https://registry.npmjs.org" "${{ steps.pack.outputs.tarball }}" "${{ steps.npm-tag.outputs['npm-tag'] }}"

- name: Publish to GitHub Package Registry
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' }}
run: scripts/publish.sh "https://npm.pkg.github.com" "${{ steps.pack.outputs.tarball }}" "${{ steps.npm-tag.outputs['npm-tag'] }}"

# - name: Report results to Jellyfish
# uses: optimizely/jellyfish-deployment-reporter-action@main
Expand Down
60 changes: 60 additions & 0 deletions scripts/publish.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
#
# Registry-agnostic, idempotent publish for @optimizely/optimizely-sdk.
#
# Usage:
# scripts/publish.sh <registry-url> <tarball> <dist-tag>
#
# Args:
# registry-url Target registry, e.g. https://registry.npmjs.org
# or https://npm.pkg.github.com
# tarball Prebuilt package tarball (output of `npm pack`) to publish.
# The release flow packs it once and publishes the same bytes to
# both registries; the backfill packs each version from npm.
# dist-tag npm dist-tag to publish under. The caller decides the tag; this
# script is deliberately tag-agnostic so the JS SDK keeps its
# existing tag logic in the workflow (latest / v{major}-latest /
# beta / alpha / rc).
#
# Env:
# NODE_AUTH_TOKEN Auth token for the target registry. An .npmrc entry must
# reference it for <registry-url>'s host, e.g.
# `//<host>/:_authToken=${NODE_AUTH_TOKEN}` (setup-node writes
# this for npm; the workflow adds one for GitHub Package
# Registry). We pass --registry explicitly, so no
# scope-to-registry routing is needed (and the @optimizely
# scope is intentionally NOT routed to GPR, so `npm ci` still
# installs dependencies from npm).
# DRY_RUN When "true", report the action (publish vs. skip) without
# actually publishing.
#
# Behavior:
# - Skips (exit 0) if the version already exists on the target registry, so
# re-running a release or the backfill is always a safe no-op.
# - Publishes the prebuilt tarball with --ignore-scripts so lifecycle scripts
# (prepublishOnly = test, prepare = build) don't re-run from package.json.
set -euo pipefail

registry="${1:?usage: publish.sh <registry-url> <tarball> <dist-tag>}"
tarball="${2:?usage: publish.sh <registry-url> <tarball> <dist-tag>}"
tag="${3:?usage: publish.sh <registry-url> <tarball> <dist-tag>}"
dry_run="${DRY_RUN:-false}"
Comment on lines +36 to +41

# Derive name/version from the tarball's own package.json so the existence guard
# matches exactly what we're about to publish.
meta=$(tar -xzO -f "$tarball" package/package.json)
pkg=$(printf '%s' "$meta" | jq -r '.name')
version=$(printf '%s' "$meta" | jq -r '.version')

if npm view "${pkg}@${version}" version --registry "$registry" >/dev/null 2>&1; then
echo "Version ${pkg}@${version} already on ${registry}, skipping."
exit 0
fi

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}"
npm publish "$tarball" --registry "$registry" --tag "$tag" --ignore-scripts
Loading