Skip to content

Publish to npm

Publish to npm #386

Workflow file for this run

# Single top-level workflow for every npm publish in this repo.
#
# Why: npmjs.com Trusted Publishing matches the `workflow_ref` OIDC claim,
# which is always the TOP-LEVEL workflow filename. npm allows only ONE
# trusted publisher per package, so every `npm publish` must originate
# from the same top-level file. By consolidating all publish triggers
# here, the OIDC claim is always `publish-npm.yml`.
#
# This replaces the previous separate entry points:
# - publish-release.yml (tag push) → mode=release
# - nightly.yml (cron/dispatch) → mode=nightly
# - publish-bumped-packages.yml (main/stable branch push) → mode=bumped-packages
#
# See https://docs.npmjs.com/trusted-publishers
name: Publish to npm
on:
push:
tags:
- 'v0.*.*' # This should match v0.X.Y
- 'v0.*.*-rc.*' # This should match v0.X.Y-RC.0
branches:
- 'main'
- '*-stable'
workflow_dispatch:
inputs:
release-tag:
description: Tag whose successful publish needs post-release recovery
required: false
type: string
draft-release-id:
description: Existing draft release ID when resuming post-release recovery
required: false
type: string
skip-podfile:
description: Skip an already-completed Podfile.lock recovery
required: false
default: false
type: boolean
# nightly build @ 2:15 AM UTC
schedule:
- cron: '15 2 * * *'
permissions:
contents: read
jobs:
# ─── Determine what kind of publish this is ──────────────────────
determine_mode:
runs-on: ubuntu-latest
if: github.repository == 'react/react-native'
outputs:
mode: ${{ steps.mode.outputs.mode }}
release-type: ${{ steps.mode.outputs.release-type }}
release-tag: ${{ steps.mode.outputs.release-tag }}
steps:
- id: mode
env:
RECOVERY_RELEASE_TAG: ${{ inputs.release-tag }}
RECOVERY_DRAFT_RELEASE_ID: ${{ inputs.draft-release-id }}
run: |
if [[ "${{ github.ref_type }}" == "tag" ]]; then
echo "mode=release" >> $GITHUB_OUTPUT
echo "release-type=release" >> $GITHUB_OUTPUT
echo "release-tag=${{ github.ref_name }}" >> $GITHUB_OUTPUT
elif [[ -n "$RECOVERY_RELEASE_TAG" ]]; then
if [[ ! "$RECOVERY_RELEASE_TAG" =~ ^v0\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "Invalid release tag: $RECOVERY_RELEASE_TAG" >&2
exit 1
fi
if [[ -n "$RECOVERY_DRAFT_RELEASE_ID" && ! "$RECOVERY_DRAFT_RELEASE_ID" =~ ^[0-9]+$ ]]; then
echo "Invalid draft release ID: $RECOVERY_DRAFT_RELEASE_ID" >&2
exit 1
fi
echo "mode=post-release" >> $GITHUB_OUTPUT
echo "release-type=" >> $GITHUB_OUTPUT
echo "release-tag=$RECOVERY_RELEASE_TAG" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "mode=nightly" >> $GITHUB_OUTPUT
echo "release-type=nightly" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "push" ]]; then
echo "mode=bumped-packages" >> $GITHUB_OUTPUT
echo "release-type=" >> $GITHUB_OUTPUT
fi
- run: |
echo "Mode: ${{ steps.mode.outputs.mode }}"
echo "Release type: ${{ steps.mode.outputs.release-type }}"
echo "Release tag: ${{ steps.mode.outputs.release-tag }}"
# ─── Release-only: extract Hermes version for draft release ──────
set_hermes_versions:
runs-on: ubuntu-latest
needs: [determine_mode]
if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'post-release'
outputs:
HERMES_VERSION: ${{ steps.set_hermes_versions.outputs.HERMES_VERSION }}
HERMES_V1_VERSION: ${{ steps.set_hermes_versions.outputs.HERMES_V1_VERSION }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.determine_mode.outputs.release-tag }}
- id: set_hermes_versions
run: |
echo "Setting hermes versions to latest"
hermes_version=$(grep -oE 'HERMES_VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+)' packages/react-native/sdks/hermes-engine/version.properties | cut -d'=' -f2)
hermes_v1_version=$(grep -oE 'HERMES_V1_VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+)' packages/react-native/sdks/hermes-engine/version.properties | cut -d'=' -f2)
echo "HERMES_VERSION=$hermes_version" >> $GITHUB_OUTPUT
echo "HERMES_V1_VERSION=$hermes_v1_version" >> $GITHUB_OUTPUT
- name: Print hermes versions
run: |
echo "HERMES_VERSION=${{ steps.set_hermes_versions.outputs.HERMES_VERSION }}"
echo "HERMES_V1_VERSION=${{ steps.set_hermes_versions.outputs.HERMES_V1_VERSION }}"
# ─── Apple prebuilds (release + nightly) ─────────────────────────
prebuild_apple_dependencies:
needs: [determine_mode]
if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly'
uses: ./.github/workflows/prebuild-ios-dependencies.yml
secrets: inherit
prebuild_react_native_core:
needs: [determine_mode, prebuild_apple_dependencies]
if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly'
uses: ./.github/workflows/prebuild-ios-core.yml
secrets: inherit
with:
use-hermes-nightly: ${{ needs.determine_mode.outputs.mode == 'nightly' }}
version-type: ${{ needs.determine_mode.outputs.mode == 'nightly' && 'nightly' || '' }}
# ─── Android build (nightly only — releases handle this in the
# build-npm-package action's Gradle step) ─────────────────────
build_android:
needs: [determine_mode]
if: needs.determine_mode.outputs.mode == 'nightly'
runs-on: ubuntu-latest
container:
image: reactnativecommunity/react-native-android:latest
env:
TERM: 'dumb'
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
LC_ALL: C.UTF8
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Build Android
uses: ./.github/actions/build-android
with:
release-type: nightly
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
# ─── Build + Publish: react-native + all @react-native/* packages
# (release and nightly modes) ─────────────────────────────────
publish_react_native:
needs:
[
determine_mode,
build_android,
prebuild_apple_dependencies,
prebuild_react_native_core,
]
# For nightly, also wait on build_android. Use always() so this
# job isn't skipped when build_android is skipped (release mode).
# The explicit status checks below handle the real gating.
if: |
always() &&
(needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly') &&
needs.determine_mode.result == 'success' &&
needs.prebuild_apple_dependencies.result == 'success' &&
needs.prebuild_react_native_core.result == 'success' &&
(needs.determine_mode.outputs.mode == 'release' || needs.build_android.result == 'success')
runs-on: ubuntu-latest
environment: npm-publish
# `id-token: write` is required so the npm CLI can mint the OIDC
# token that npm Trusted Publishing exchanges for a publish token.
permissions:
contents: read
id-token: write
container:
image: reactnativecommunity/react-native-android:latest
env:
TERM: 'dumb'
# Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers
# via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127
LC_ALL: C.UTF8
GRADLE_OPTS: '-Dorg.gradle.daemon=false'
# By default we only build ARM64 to save time/resources. For release/nightlies, we override this value to build all archs.
ORG_GRADLE_PROJECT_reactNativeArchitectures: 'arm64-v8a'
REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads
env:
ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }}
ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }}
ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }}
ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-tags: true
- name: Build and Publish NPM Package
uses: ./.github/actions/build-npm-package
with:
release-type: ${{ needs.determine_mode.outputs.release-type }}
gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }}
# ─── Publish bumped monorepo packages (main/stable push) ─────────
publish_bumped_packages:
needs: [determine_mode]
if: needs.determine_mode.outputs.mode == 'bumped-packages'
runs-on: ubuntu-latest
environment: npm-publish
# `id-token: write` is required so the npm CLI can mint the OIDC
# token that npm Trusted Publishing exchanges for a publish token.
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup node.js
uses: ./.github/actions/setup-node
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
- name: Run Yarn Install
uses: ./.github/actions/yarn-install
- name: Build packages
run: yarn build
- name: Build types
run: yarn build-types --skip-snapshot
- name: Find and publish all bumped packages
run: node ./scripts/releases-ci/publish-updated-packages.js
# ─── Release-only: post-publish steps ────────────────────────────
post_publish:
runs-on: ubuntu-latest
needs: [determine_mode, publish_react_native]
if: |
always() &&
needs.determine_mode.result == 'success' &&
((needs.determine_mode.outputs.mode == 'release' && needs.publish_react_native.result == 'success') ||
(needs.determine_mode.outputs.mode == 'post-release' && inputs.draft-release-id == ''))
env:
REACT_NATIVE_BOT_GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ needs.determine_mode.outputs.release-tag }}
fetch-depth: 0
fetch-tags: true
- name: Publish @react-native-community/template
id: publish-template-to-npm
uses: actions/github-script@v8
with:
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
script: |
const {publishTemplate} = require('./.github/workflow-scripts/publishTemplate.js')
const version = "${{ needs.determine_mode.outputs.release-tag }}"
const isDryRun = false
await publishTemplate(github, version, isDryRun);
- name: Wait for template to be published
timeout-minutes: 3
uses: actions/github-script@v8
with:
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
script: |
const {verifyPublishedTemplate, isLatest} = require('./.github/workflow-scripts/publishTemplate.js')
const version = "${{ needs.determine_mode.outputs.release-tag }}"
await verifyPublishedTemplate(version, isLatest());
- name: Update rn-diff-purge to generate upgrade-support diff
run: |
curl -X POST https://api.github.com/repos/react-native-community/rn-diff-purge/dispatches \
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \
-d "{\"event_type\": \"publish\", \"client_payload\": { \"version\": \"${{ needs.determine_mode.outputs.release-tag }}\" }}"
- name: Verify Release is on NPM
timeout-minutes: 3
uses: actions/github-script@v8
with:
github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }}
script: |
const {verifyReleaseOnNpm} = require('./.github/workflow-scripts/verifyReleaseOnNpm.js');
const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js');
const version = "${{ needs.determine_mode.outputs.release-tag }}";
await verifyReleaseOnNpm(version, isLatest());
- name: Verify that artifacts are on Maven
uses: actions/github-script@v8
with:
script: |
const {verifyArtifactsAreOnMaven} = require('./.github/workflow-scripts/verifyArtifactsAreOnMaven.js');
const version = "${{ needs.determine_mode.outputs.release-tag }}";
await verifyArtifactsAreOnMaven(version);
# ─── Release-only: changelog, podfile bump, draft release ────────
generate_changelog:
needs: [determine_mode, publish_react_native]
if: |
always() &&
needs.determine_mode.result == 'success' &&
((needs.determine_mode.outputs.mode == 'release' && needs.publish_react_native.result == 'success') ||
(needs.determine_mode.outputs.mode == 'post-release' && inputs.draft-release-id == ''))
uses: ./.github/workflows/generate-changelog.yml
secrets: inherit
with:
releaseVersion: ${{ needs.determine_mode.outputs.release-tag }}
checkoutRef: ${{ needs.determine_mode.outputs.release-tag }}
bump_podfile_lock:
needs: [determine_mode, publish_react_native]
if: |
always() &&
needs.determine_mode.result == 'success' &&
((needs.determine_mode.outputs.mode == 'release' && needs.publish_react_native.result == 'success') ||
(needs.determine_mode.outputs.mode == 'post-release' && !inputs.skip-podfile))
uses: ./.github/workflows/bump-podfile-lock.yml
secrets: inherit
with:
releaseVersion: ${{ needs.determine_mode.outputs.release-tag }}
checkoutRef: ${{ needs.determine_mode.outputs.release-tag }}
create_draft_release:
needs: [determine_mode, generate_changelog, set_hermes_versions]
if: |
always() &&
needs.determine_mode.result == 'success' &&
needs.set_hermes_versions.result == 'success' &&
((needs.determine_mode.outputs.mode == 'release' && needs.generate_changelog.result == 'success') ||
(needs.determine_mode.outputs.mode == 'post-release' &&
(needs.generate_changelog.result == 'success' || inputs.draft-release-id != '')))
uses: ./.github/workflows/create-draft-release.yml
secrets: inherit
with:
releaseVersion: ${{ needs.determine_mode.outputs.release-tag }}
checkoutRef: ${{ inputs.draft-release-id != '' && github.ref || needs.determine_mode.outputs.release-tag }}
releaseId: ${{ inputs.draft-release-id }}
hermesVersion: ${{ needs.set_hermes_versions.outputs.HERMES_VERSION }}
hermesV1Version: ${{ needs.set_hermes_versions.outputs.HERMES_V1_VERSION }}