From ba37dcd0c83252390b9f3b5c88d9c74b23cccc7f Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 15 Jul 2026 20:37:39 +0600 Subject: [PATCH 1/2] [FSSDK-12881] impl --- .github/workflows/ghr_backfill.yml | 89 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 51 ++++++++++++----- scripts/publish.sh | 60 ++++++++++++++++++++ 3 files changed, 186 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/ghr_backfill.yml create mode 100755 scripts/publish.sh diff --git a/.github/workflows/ghr_backfill.yml b/.github/workflows/ghr_backfill.yml new file mode 100644 index 000000000..54be52061 --- /dev/null +++ b/.github/workflows/ghr_backfill.yml @@ -0,0 +1,89 @@ +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 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: "https://registry.npmjs.org/" + always-auth: "true" + + # Add GPR auth for publishing. npm expands ${NODE_AUTH_TOKEN} at runtime, + # and scripts/publish.sh passes --registry explicitly, so the default + # registry stays on npm (backfill reads tarballs from npm, writes to GPR). + - 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f3e710a44..2d1261bfa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Publish SDK to NPM +name: Publish SDK to NPM and GitHub Package Registry on: release: @@ -7,9 +7,12 @@ 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 @@ -20,11 +23,19 @@ jobs: 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 @@ -42,7 +53,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 @@ -61,17 +72,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 diff --git a/scripts/publish.sh b/scripts/publish.sh new file mode 100755 index 000000000..57c34626c --- /dev/null +++ b/scripts/publish.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# +# Registry-agnostic, idempotent publish for @optimizely/optimizely-sdk. +# +# Usage: +# scripts/publish.sh +# +# 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 's host, e.g. +# `///:_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 }" +tarball="${2:?usage: publish.sh }" +tag="${3:?usage: publish.sh }" +dry_run="${DRY_RUN:-false}" + +# 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 From eb92334234fb3ecaffed15c4816518a73ff8d557 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:01:07 +0600 Subject: [PATCH 2/2] [FSSDK-12881] feedback improvement --- .github/workflows/ghr_backfill.yml | 7 ++----- .github/workflows/release.yml | 2 ++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ghr_backfill.yml b/.github/workflows/ghr_backfill.yml index 54be52061..d66f6b353 100644 --- a/.github/workflows/ghr_backfill.yml +++ b/.github/workflows/ghr_backfill.yml @@ -36,17 +36,14 @@ jobs: steps: - name: Checkout branch uses: actions/checkout@v4 + with: + persist-credentials: false - name: Set up Node uses: actions/setup-node@v4 with: node-version: 18 - registry-url: "https://registry.npmjs.org/" - always-auth: "true" - # Add GPR auth for publishing. npm expands ${NODE_AUTH_TOKEN} at runtime, - # and scripts/publish.sh passes --registry explicitly, so the default - # registry stays on npm (backfill reads tarballs from npm, writes to GPR). - name: Configure GitHub Package Registry auth run: echo "//npm.pkg.github.com/:_authToken=\${NODE_AUTH_TOKEN}" >> ~/.npmrc env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d1261bfa..c26174892 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,8 @@ jobs: steps: - name: Checkout branch uses: actions/checkout@v4 + with: + persist-credentials: false - name: Setup Node uses: actions/setup-node@v3