|
1 | | -# Reusable workflow that performs every `npm publish` in this repo. |
| 1 | +# Single top-level workflow for every npm publish in this repo. |
2 | 2 | # |
3 | | -# Why this exists: npmjs.com Trusted Publishing accepts only ONE |
4 | | -# (org, repo, workflow_filename, environment) tuple per package. If |
5 | | -# `react-native` were published from `publish-release.yml` AND |
6 | | -# `nightly.yml` directly, we'd need two Trusted Publisher entries per |
7 | | -# package — npm rejects that. By moving every `npm publish` into this |
8 | | -# single reusable workflow file, the OIDC `job_workflow_ref` claim |
9 | | -# always resolves to `publish-npm.yml` regardless of which top-level |
10 | | -# workflow triggered the run, so each package needs exactly one |
11 | | -# Trusted Publisher entry pointing here. |
| 3 | +# Why: npmjs.com Trusted Publishing matches the `workflow_ref` OIDC claim, |
| 4 | +# which is always the TOP-LEVEL workflow filename. npm allows only ONE |
| 5 | +# trusted publisher per package, so every `npm publish` must originate |
| 6 | +# from the same top-level file. By consolidating all publish triggers |
| 7 | +# here, the OIDC claim is always `publish-npm.yml`. |
12 | 8 | # |
13 | | -# See https://docs.npmjs.com/trusted-publishers and |
14 | | -# https://docs.github.com/en/actions/sharing-automations/reusing-workflows . |
15 | | -name: Publish to npm (reusable) |
| 9 | +# This replaces the previous separate entry points: |
| 10 | +# - publish-release.yml (tag push) → mode=release |
| 11 | +# - nightly.yml (cron/dispatch) → mode=nightly |
| 12 | +# - publish-bumped-packages.yml (main/stable branch push) → mode=bumped-packages |
| 13 | +# |
| 14 | +# See https://docs.npmjs.com/trusted-publishers |
| 15 | +name: Publish to npm |
16 | 16 |
|
17 | 17 | on: |
18 | | - workflow_call: |
19 | | - inputs: |
20 | | - mode: |
21 | | - description: | |
22 | | - 'react-native' runs the full Android/iOS-prebuilt + JS build |
23 | | - and publishes via scripts/releases-ci/publish-npm.js (which |
24 | | - publishes `react-native` and, in nightly mode, every |
25 | | - @react-native/* package). 'monorepo-packages' runs only the |
26 | | - JS build and publishes via |
27 | | - scripts/releases-ci/publish-updated-packages.js (delta-based, |
28 | | - gated on a #publish-packages-to-npm commit message). |
29 | | - type: string |
30 | | - required: true |
31 | | - release-type: |
32 | | - description: "For mode=react-native: release | nightly | dry-run." |
33 | | - type: string |
34 | | - required: false |
35 | | - default: "dry-run" |
36 | | - skip-apple-prebuilts: |
37 | | - description: "For mode=react-native: skip downloading prebuilt Apple artifacts." |
38 | | - type: boolean |
39 | | - required: false |
40 | | - default: false |
| 18 | + push: |
| 19 | + tags: |
| 20 | + - "v0.*.*" # This should match v0.X.Y |
| 21 | + - "v0.*.*-rc.*" # This should match v0.X.Y-RC.0 |
| 22 | + branches: |
| 23 | + - "main" |
| 24 | + - "*-stable" |
| 25 | + workflow_dispatch: |
| 26 | + # nightly build @ 2:15 AM UTC |
| 27 | + schedule: |
| 28 | + - cron: "15 2 * * *" |
| 29 | + |
| 30 | +permissions: |
| 31 | + contents: read |
41 | 32 |
|
42 | 33 | jobs: |
43 | | - publish-react-native: |
44 | | - if: inputs.mode == 'react-native' |
| 34 | + # ─── Determine what kind of publish this is ────────────────────── |
| 35 | + determine_mode: |
| 36 | + runs-on: ubuntu-latest |
| 37 | + if: github.repository == 'react/react-native' |
| 38 | + outputs: |
| 39 | + mode: ${{ steps.mode.outputs.mode }} |
| 40 | + release-type: ${{ steps.mode.outputs.release-type }} |
| 41 | + steps: |
| 42 | + - id: mode |
| 43 | + run: | |
| 44 | + if [[ "${{ github.ref_type }}" == "tag" ]]; then |
| 45 | + echo "mode=release" >> $GITHUB_OUTPUT |
| 46 | + echo "release-type=release" >> $GITHUB_OUTPUT |
| 47 | + elif [[ "${{ github.event_name }}" == "schedule" || "${{ github.event_name }}" == "workflow_dispatch" ]]; then |
| 48 | + echo "mode=nightly" >> $GITHUB_OUTPUT |
| 49 | + echo "release-type=nightly" >> $GITHUB_OUTPUT |
| 50 | + elif [[ "${{ github.event_name }}" == "push" ]]; then |
| 51 | + echo "mode=bumped-packages" >> $GITHUB_OUTPUT |
| 52 | + echo "release-type=" >> $GITHUB_OUTPUT |
| 53 | + fi |
| 54 | + - run: | |
| 55 | + echo "Mode: ${{ steps.mode.outputs.mode }}" |
| 56 | + echo "Release type: ${{ steps.mode.outputs.release-type }}" |
| 57 | +
|
| 58 | + # ─── Release-only: extract Hermes version for draft release ────── |
| 59 | + set_hermes_version: |
| 60 | + runs-on: ubuntu-latest |
| 61 | + if: github.ref_type == 'tag' |
| 62 | + outputs: |
| 63 | + HERMES_VERSION: ${{ steps.set_hermes_version.outputs.HERMES_VERSION }} |
| 64 | + steps: |
| 65 | + - name: Checkout |
| 66 | + uses: actions/checkout@v6 |
| 67 | + - id: set_hermes_version |
| 68 | + run: | |
| 69 | + hermes_version=$(grep -oE 'HERMES_VERSION_NAME=([0-9]+\.[0-9]+\.[0-9]+)' packages/react-native/sdks/hermes-engine/version.properties | cut -d'=' -f2) |
| 70 | + echo "HERMES_VERSION=$hermes_version" >> $GITHUB_OUTPUT |
| 71 | + echo "HERMES_VERSION=$hermes_version" |
| 72 | +
|
| 73 | + # ─── Apple prebuilds (release + nightly) ───────────────────────── |
| 74 | + prebuild_apple_dependencies: |
| 75 | + needs: [determine_mode] |
| 76 | + if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly' |
| 77 | + uses: ./.github/workflows/prebuild-ios-dependencies.yml |
| 78 | + secrets: inherit |
| 79 | + |
| 80 | + prebuild_react_native_core: |
| 81 | + needs: [determine_mode, prebuild_apple_dependencies] |
| 82 | + if: needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly' |
| 83 | + uses: ./.github/workflows/prebuild-ios-core.yml |
| 84 | + secrets: inherit |
| 85 | + with: |
| 86 | + use-hermes-prebuilt: ${{ needs.determine_mode.outputs.mode == 'nightly' }} |
| 87 | + version-type: ${{ needs.determine_mode.outputs.mode == 'nightly' && 'nightly' || '' }} |
| 88 | + |
| 89 | + # ─── Android build (nightly only — releases handle this in the |
| 90 | + # build-npm-package action's Gradle step) ───────────────────── |
| 91 | + build_android: |
| 92 | + needs: [determine_mode] |
| 93 | + if: needs.determine_mode.outputs.mode == 'nightly' |
| 94 | + runs-on: ubuntu-latest |
| 95 | + container: |
| 96 | + image: reactnativecommunity/react-native-android:latest |
| 97 | + env: |
| 98 | + TERM: "dumb" |
| 99 | + # Set the encoding to resolve a known character encoding issue with decompressing tar.gz files in containers |
| 100 | + # via Gradle: https://github.com/gradle/gradle/issues/23391#issuecomment-1878979127 |
| 101 | + LC_ALL: C.UTF8 |
| 102 | + GRADLE_OPTS: "-Dorg.gradle.daemon=false" |
| 103 | + ORG_GRADLE_PROJECT_SIGNING_PWD: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_PWD }} |
| 104 | + ORG_GRADLE_PROJECT_SIGNING_KEY: ${{ secrets.ORG_GRADLE_PROJECT_SIGNING_KEY }} |
| 105 | + ORG_GRADLE_PROJECT_SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_USERNAME }} |
| 106 | + ORG_GRADLE_PROJECT_SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPE_PASSWORD }} |
| 107 | + REACT_NATIVE_DOWNLOADS_DIR: /opt/react-native-downloads |
| 108 | + steps: |
| 109 | + - name: Checkout |
| 110 | + uses: actions/checkout@v6 |
| 111 | + - name: Build Android |
| 112 | + uses: ./.github/actions/build-android |
| 113 | + with: |
| 114 | + release-type: nightly |
| 115 | + gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} |
| 116 | + |
| 117 | + # ─── Build + Publish: react-native + all @react-native/* packages |
| 118 | + # (release and nightly modes) ───────────────────────────────── |
| 119 | + publish_react_native: |
| 120 | + needs: |
| 121 | + [ |
| 122 | + determine_mode, |
| 123 | + build_android, |
| 124 | + prebuild_apple_dependencies, |
| 125 | + prebuild_react_native_core, |
| 126 | + ] |
| 127 | + # For nightly, also wait on build_android. Use always() so this |
| 128 | + # job isn't skipped when build_android is skipped (release mode). |
| 129 | + # The explicit status checks below handle the real gating. |
| 130 | + if: | |
| 131 | + always() && |
| 132 | + (needs.determine_mode.outputs.mode == 'release' || needs.determine_mode.outputs.mode == 'nightly') && |
| 133 | + needs.determine_mode.result == 'success' && |
| 134 | + needs.prebuild_apple_dependencies.result == 'success' && |
| 135 | + needs.prebuild_react_native_core.result == 'success' && |
| 136 | + (needs.determine_mode.outputs.mode == 'release' || needs.build_android.result == 'success') |
45 | 137 | runs-on: ubuntu-latest |
46 | 138 | environment: npm-publish |
47 | 139 | # `id-token: write` is required so the npm CLI can mint the OIDC |
@@ -91,14 +183,17 @@ jobs: |
91 | 183 | - name: Build and Publish NPM Package |
92 | 184 | uses: ./.github/actions/build-npm-package |
93 | 185 | with: |
94 | | - release-type: ${{ inputs.release-type }} |
| 186 | + release-type: ${{ needs.determine_mode.outputs.release-type }} |
95 | 187 | gradle-cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} |
96 | | - skip-apple-prebuilts: ${{ inputs.skip-apple-prebuilts && 'true' || 'false' }} |
97 | 188 |
|
98 | | - publish-monorepo-packages: |
99 | | - if: inputs.mode == 'monorepo-packages' |
| 189 | + # ─── Publish bumped monorepo packages (main/stable push) ───────── |
| 190 | + publish_bumped_packages: |
| 191 | + needs: [determine_mode] |
| 192 | + if: needs.determine_mode.outputs.mode == 'bumped-packages' |
100 | 193 | runs-on: ubuntu-latest |
101 | 194 | environment: npm-publish |
| 195 | + # `id-token: write` is required so the npm CLI can mint the OIDC |
| 196 | + # token that npm Trusted Publishing exchanges for a publish token. |
102 | 197 | permissions: |
103 | 198 | contents: read |
104 | 199 | id-token: write |
@@ -134,3 +229,80 @@ jobs: |
134 | 229 | run: yarn build-types --skip-snapshot |
135 | 230 | - name: Find and publish all bumped packages |
136 | 231 | run: node ./scripts/releases-ci/publish-updated-packages.js |
| 232 | + |
| 233 | + # ─── Release-only: post-publish steps ──────────────────────────── |
| 234 | + post_publish: |
| 235 | + runs-on: ubuntu-latest |
| 236 | + needs: [determine_mode, publish_react_native] |
| 237 | + if: needs.determine_mode.outputs.mode == 'release' |
| 238 | + env: |
| 239 | + REACT_NATIVE_BOT_GITHUB_TOKEN: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} |
| 240 | + steps: |
| 241 | + - name: Checkout |
| 242 | + uses: actions/checkout@v6 |
| 243 | + with: |
| 244 | + fetch-depth: 0 |
| 245 | + fetch-tags: true |
| 246 | + - name: Publish @react-native-community/template |
| 247 | + id: publish-template-to-npm |
| 248 | + uses: actions/github-script@v8 |
| 249 | + with: |
| 250 | + github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} |
| 251 | + script: | |
| 252 | + const {publishTemplate} = require('./.github/workflow-scripts/publishTemplate.js') |
| 253 | + const version = "${{ github.ref_name }}" |
| 254 | + const isDryRun = false |
| 255 | + await publishTemplate(github, version, isDryRun); |
| 256 | + - name: Wait for template to be published |
| 257 | + timeout-minutes: 3 |
| 258 | + uses: actions/github-script@v8 |
| 259 | + with: |
| 260 | + github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} |
| 261 | + script: | |
| 262 | + const {verifyPublishedTemplate, isLatest} = require('./.github/workflow-scripts/publishTemplate.js') |
| 263 | + const version = "${{ github.ref_name }}" |
| 264 | + await verifyPublishedTemplate(version, isLatest()); |
| 265 | + - name: Update rn-diff-purge to generate upgrade-support diff |
| 266 | + run: | |
| 267 | + curl -X POST https://api.github.com/repos/react-native-community/rn-diff-purge/dispatches \ |
| 268 | + -H "Accept: application/vnd.github.v3+json" \ |
| 269 | + -H "Authorization: Bearer $REACT_NATIVE_BOT_GITHUB_TOKEN" \ |
| 270 | + -d "{\"event_type\": \"publish\", \"client_payload\": { \"version\": \"${{ github.ref_name }}\" }}" |
| 271 | + - name: Verify Release is on NPM |
| 272 | + timeout-minutes: 3 |
| 273 | + uses: actions/github-script@v8 |
| 274 | + with: |
| 275 | + github-token: ${{ secrets.REACT_NATIVE_BOT_GITHUB_TOKEN }} |
| 276 | + script: | |
| 277 | + const {verifyReleaseOnNpm} = require('./.github/workflow-scripts/verifyReleaseOnNpm.js'); |
| 278 | + const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js'); |
| 279 | + const version = "${{ github.ref_name }}"; |
| 280 | + await verifyReleaseOnNpm(version, isLatest()); |
| 281 | + - name: Verify that artifacts are on Maven |
| 282 | + uses: actions/github-script@v8 |
| 283 | + with: |
| 284 | + script: | |
| 285 | + const {verifyArtifactsAreOnMaven} = require('./.github/workflow-scripts/verifyArtifactsAreOnMaven.js'); |
| 286 | + const version = "${{ github.ref_name }}"; |
| 287 | + await verifyArtifactsAreOnMaven(version); |
| 288 | +
|
| 289 | + # ─── Release-only: changelog, podfile bump, draft release ──────── |
| 290 | + generate_changelog: |
| 291 | + needs: [determine_mode, publish_react_native] |
| 292 | + if: needs.determine_mode.outputs.mode == 'release' |
| 293 | + uses: ./.github/workflows/generate-changelog.yml |
| 294 | + secrets: inherit |
| 295 | + |
| 296 | + bump_podfile_lock: |
| 297 | + needs: [determine_mode, publish_react_native] |
| 298 | + if: needs.determine_mode.outputs.mode == 'release' |
| 299 | + uses: ./.github/workflows/bump-podfile-lock.yml |
| 300 | + secrets: inherit |
| 301 | + |
| 302 | + create_draft_release: |
| 303 | + needs: [determine_mode, generate_changelog, set_hermes_version] |
| 304 | + if: needs.determine_mode.outputs.mode == 'release' |
| 305 | + uses: ./.github/workflows/create-draft-release.yml |
| 306 | + secrets: inherit |
| 307 | + with: |
| 308 | + hermesVersion: ${{ needs.set_hermes_version.outputs.HERMES_VERSION }} |
0 commit comments