Unified release process (#36456)
Sebastian "Sebbie" Silbermann committed
May 26, 2026 at 17:23 UTC
75b0945b18f4a60c80c931fd8067d9c715957879
26 files changed
+377
-1711
.github/workflows/runtime_build_and_test.yml
+24
-25
@@ -10,12 +10,11 @@ on:
10
pull_request:
11
paths-ignore:
12
- compiler/**
13
+ # Manual re-runs are allowed. The workflow always runs on the SHA it was
14
+ # dispatched from (github.sha); no user-supplied ref is accepted, because
15
+ # downstream workflows (e.g. runtime_release_from_ci) consume this
16
+ # workflow's artifacts under a protected environment.
17
workflow_dispatch:
14
- inputs:
15
- commit_sha:
16
- required: false
17
- type: string
18
- default: ''
18
19
permissions: {}
20
@@ -38,7 +37,7 @@ jobs:
37
steps:
38
- uses: actions/checkout@v4
39
with:
41
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
40
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
41
42
- name: Prepare cache
43
id: node_modules
@@ -63,7 +62,7 @@ jobs:
62
steps:
63
- uses: actions/checkout@v4
64
with:
66
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
65
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
66
- uses: actions/setup-node@v4
67
with:
68
node-version-file: '.nvmrc'
@@ -89,7 +88,7 @@ jobs:
88
steps:
89
- uses: actions/checkout@v4
90
with:
92
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
91
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
92
- uses: actions/github-script@v7
93
id: set-matrix
94
with:
@@ -108,7 +107,7 @@ jobs:
107
steps:
108
- uses: actions/checkout@v4
109
with:
111
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
110
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
111
- uses: actions/setup-node@v4
112
with:
113
node-version-file: '.nvmrc'
@@ -134,7 +133,7 @@ jobs:
133
steps:
134
- uses: actions/checkout@v4
135
with:
137
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
136
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
137
- uses: actions/setup-node@v4
138
with:
139
node-version-file: '.nvmrc'
@@ -162,7 +161,7 @@ jobs:
161
steps:
162
- uses: actions/checkout@v4
163
with:
165
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
164
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
165
- uses: actions/setup-node@v4
166
with:
167
node-version-file: '.nvmrc'
@@ -216,7 +215,7 @@ jobs:
215
steps:
216
- uses: actions/checkout@v4
217
with:
219
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
218
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
219
- uses: actions/setup-node@v4
220
with:
221
node-version-file: '.nvmrc'
@@ -278,7 +277,7 @@ jobs:
277
steps:
278
- uses: actions/checkout@v4
279
with:
281
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
280
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
281
- uses: actions/setup-node@v4
282
with:
283
node-version-file: '.nvmrc'
@@ -364,7 +363,7 @@ jobs:
363
steps:
364
- uses: actions/checkout@v4
365
with:
367
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
366
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
367
- uses: actions/setup-node@v4
368
with:
369
node-version-file: '.nvmrc'
@@ -409,7 +408,7 @@ jobs:
408
steps:
409
- uses: actions/checkout@v4
410
with:
412
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
411
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
412
- uses: actions/setup-node@v4
413
with:
414
node-version-file: '.nvmrc'
@@ -447,7 +446,7 @@ jobs:
446
steps:
447
- uses: actions/checkout@v4
448
with:
450
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
449
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
450
- uses: actions/setup-node@v4
451
with:
452
node-version-file: '.nvmrc'
@@ -471,7 +470,7 @@ jobs:
470
merge-multiple: true
471
- name: Display structure of build
472
run: ls -R build
474
- - run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
473
+ - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
474
- name: Scrape warning messages
475
run: |
476
mkdir -p ./build/__test_utils__
@@ -508,7 +507,7 @@ jobs:
507
steps:
508
- uses: actions/checkout@v4
509
with:
511
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
510
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
511
- uses: actions/setup-node@v4
512
with:
513
node-version-file: '.nvmrc'
@@ -544,7 +543,7 @@ jobs:
543
steps:
544
- uses: actions/checkout@v4
545
with:
547
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
546
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
547
- uses: actions/setup-node@v4
548
with:
549
node-version-file: '.nvmrc'
@@ -577,7 +576,7 @@ jobs:
576
steps:
577
- uses: actions/checkout@v4
578
with:
580
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
579
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
580
- uses: actions/setup-node@v4
581
with:
582
node-version-file: '.nvmrc'
@@ -616,7 +615,7 @@ jobs:
615
steps:
616
- uses: actions/checkout@v4
617
with:
619
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
618
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
619
- uses: actions/setup-node@v4
620
with:
621
node-version-file: '.nvmrc'
@@ -688,7 +687,7 @@ jobs:
687
steps:
688
- uses: actions/checkout@v4
689
with:
691
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
690
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
691
- uses: actions/setup-node@v4
692
with:
693
node-version-file: '.nvmrc'
@@ -746,7 +745,7 @@ jobs:
745
steps:
746
- uses: actions/checkout@v4
747
with:
749
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
748
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
749
- uses: actions/setup-node@v4
750
with:
751
node-version-file: '.nvmrc'
@@ -802,7 +801,7 @@ jobs:
801
steps:
802
- uses: actions/checkout@v4
803
with:
805
- ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
804
+ ref: ${{ github.event.pull_request.head.sha || github.sha }}
805
- uses: actions/setup-node@v4
806
with:
807
node-version-file: '.nvmrc'
@@ -849,7 +848,7 @@ jobs:
848
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
849
- name: Display structure of build for PR
850
run: ls -R build
852
- - run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
851
+ - run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
852
- run: node ./scripts/tasks/danger
853
- name: Archive sizebot results
854
uses: actions/upload-artifact@v4
.github/workflows/runtime_prereleases.yml
deleted
-108
@@ -1,108 +0,0 @@
1
-name: (Runtime) Publish Prereleases
2
-
3
-on:
4
- workflow_call:
5
- inputs:
6
- commit_sha:
7
- required: true
8
- default: ''
9
- type: string
10
- release_channel:
11
- required: true
12
- type: string
13
- dist_tag:
14
- required: true
15
- type: string
16
- enableFailureNotification:
17
- description: 'Whether to notify the team on Discord when the release fails. Useful if this workflow is called from an automation.'
18
- required: false
19
- type: boolean
20
- only_packages:
21
- description: Packages to publish (space separated)
22
- type: string
23
- skip_packages:
24
- description: Packages to NOT publish (space separated)
25
- type: string
26
- dry:
27
- required: true
28
- description: Dry run instead of publish?
29
- type: boolean
30
- default: true
31
- secrets:
32
- DISCORD_WEBHOOK_URL:
33
- description: 'Discord webhook URL to notify on failure. Only required if enableFailureNotification is true.'
34
- required: false
35
- GH_TOKEN:
36
- required: true
37
- NPM_TOKEN:
38
- required: true
39
-
40
-permissions: {}
41
-
42
-env:
43
- TZ: /usr/share/zoneinfo/America/Los_Angeles
44
- # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
45
- SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
46
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
47
-
48
-jobs:
49
- publish_prerelease:
50
- name: Publish prelease (${{ inputs.release_channel }}) ${{ inputs.commit_sha }} @${{ inputs.dist_tag }}
51
- runs-on: ubuntu-latest
52
- permissions:
53
- # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
54
- actions: read
55
- steps:
56
- - uses: actions/checkout@v4
57
- - uses: actions/setup-node@v4
58
- with:
59
- node-version-file: '.nvmrc'
60
- - name: Cache node_modules
61
- uses: actions/cache@v4
62
- id: node_modules
63
- with:
64
- path: |
65
- **/node_modules
66
- key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
67
- - name: Ensure clean build directory
68
- run: rm -rf build
69
- - run: yarn install --frozen-lockfile
70
- if: steps.node_modules.outputs.cache-hit != 'true'
71
- - run: yarn --cwd scripts/release install --frozen-lockfile
72
- if: steps.node_modules.outputs.cache-hit != 'true'
73
- - run: cp ./scripts/release/ci-npmrc ~/.npmrc
74
- - run: |
75
- GH_TOKEN=${{ secrets.GH_TOKEN }} scripts/release/prepare-release-from-ci.js --skipTests -r ${{ inputs.release_channel }} --commit=${{ inputs.commit_sha }}
76
- - name: Check prepared files
77
- run: ls -R build/node_modules
78
- - if: '${{ inputs.only_packages }}'
79
- name: 'Publish ${{ inputs.only_packages }}'
80
- run: |
81
- scripts/release/publish.js \
82
- --ci \
83
- --tags=${{ inputs.dist_tag }} \
84
- --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
85
- ${{ inputs.dry && '--dry' || '' }}
86
- - if: '${{ inputs.skip_packages }}'
87
- name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
88
- run: |
89
- scripts/release/publish.js \
90
- --ci \
91
- --tags=${{ inputs.dist_tag }} \
92
- --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
93
- ${{ inputs.dry && '--dry' || '' }}
94
- - if: '${{ !inputs.skip_packages && !inputs.only_packages }}'
95
- name: 'Publish all packages'
96
- run: |
97
- scripts/release/publish.js \
98
- --ci \
99
- --tags=${{ inputs.dist_tag }} ${{ (inputs.dry && '') || '\'}}
100
- ${{ inputs.dry && '--dry' || '' }}
101
- - name: Notify Discord on failure
102
- if: failure() && inputs.enableFailureNotification == true
103
- uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
104
- with:
105
- webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
106
- embed-author-name: "GitHub Actions"
107
- embed-title: '[Runtime] Publish of ${{ inputs.release_channel }}@${{ inputs.dist_tag}} release failed'
108
- embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
.github/workflows/runtime_prereleases_manual.yml
deleted
-102
@@ -1,102 +0,0 @@
1
-name: (Runtime) Publish Prereleases Manual
2
-
3
-on:
4
- workflow_dispatch:
5
- inputs:
6
- prerelease_commit_sha:
7
- required: true
8
- only_packages:
9
- description: Packages to publish (space separated)
10
- type: string
11
- skip_packages:
12
- description: Packages to NOT publish (space separated)
13
- type: string
14
- dry:
15
- required: true
16
- description: Dry run instead of publish?
17
- type: boolean
18
- default: true
19
- experimental_only:
20
- type: boolean
21
- description: Only publish to the experimental tag
22
- default: false
23
- force_notify:
24
- description: Force a Discord notification?
25
- type: boolean
26
- default: false
27
-
28
-permissions: {}
29
-
30
-env:
31
- TZ: /usr/share/zoneinfo/America/Los_Angeles
32
-
33
-jobs:
34
- notify:
35
- if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
36
- runs-on: ubuntu-latest
37
- steps:
38
- - name: Discord Webhook Action
39
- uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
40
- with:
41
- webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
42
- embed-author-name: ${{ github.event.sender.login }}
43
- embed-author-url: ${{ github.event.sender.html_url }}
44
- embed-author-icon-url: ${{ github.event.sender.avatar_url }}
45
- embed-title: "⚠️ Publishing ${{ inputs.experimental_only && 'EXPERIMENTAL' || 'CANARY & EXPERIMENTAL' }} release ${{ (inputs.dry && ' (dry run)') || '' }}"
46
- embed-description: |
47
- ```json
48
- ${{ toJson(inputs) }}
49
- ```
50
- embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
51
-
52
- publish_prerelease_canary:
53
- if: ${{ !inputs.experimental_only }}
54
- name: Publish to Canary channel
55
- uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
56
- permissions:
57
- # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
58
- actions: read
59
- with:
60
- commit_sha: ${{ inputs.prerelease_commit_sha }}
61
- release_channel: stable
62
- # The tags to use when publishing canaries. The main one we should
63
- # always include is "canary" but we can use multiple (e.g. alpha,
64
- # beta, rc). To declare multiple, use a comma-separated string, like
65
- # this:
66
- # dist_tag: "canary,alpha,beta,rc"
67
- #
68
- # TODO: We currently tag canaries with "next" in addition to "canary"
69
- # because this used to be called the "next" channel and some
70
- # downstream consumers might still expect that tag. We can remove this
71
- # after some time has elapsed and the change has been communicated.
72
- dist_tag: canary,next
73
- only_packages: ${{ inputs.only_packages }}
74
- skip_packages: ${{ inputs.skip_packages }}
75
- dry: ${{ inputs.dry }}
76
- secrets:
77
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
78
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79
-
80
- publish_prerelease_experimental:
81
- name: Publish to Experimental channel
82
- uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
83
- permissions:
84
- # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
85
- actions: read
86
- # NOTE: Intentionally running these jobs sequentially because npm
87
- # will sometimes fail if you try to concurrently publish two
88
- # different versions of the same package, even if they use different
89
- # dist tags.
90
- needs: publish_prerelease_canary
91
- # Ensures the job runs even if canary is skipped
92
- if: always()
93
- with:
94
- commit_sha: ${{ inputs.prerelease_commit_sha }}
95
- release_channel: experimental
96
- dist_tag: experimental
97
- only_packages: ${{ inputs.only_packages }}
98
- skip_packages: ${{ inputs.skip_packages }}
99
- dry: ${{ inputs.dry }}
100
- secrets:
101
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
102
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/runtime_prereleases_nightly.yml
deleted
-51
@@ -1,51 +0,0 @@
1
-name: (Runtime) Publish Prereleases Nightly
2
-
3
-on:
4
- schedule:
5
- # At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri
6
- - cron: 10 16 * * 1,2,3,4,5
7
-
8
-permissions: {}
9
-
10
-env:
11
- TZ: /usr/share/zoneinfo/America/Los_Angeles
12
-
13
-jobs:
14
- publish_prerelease_canary:
15
- name: Publish to Canary channel
16
- uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
17
- permissions:
18
- # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
19
- actions: read
20
- with:
21
- commit_sha: ${{ github.sha }}
22
- release_channel: stable
23
- dist_tag: canary,next
24
- enableFailureNotification: true
25
- dry: false
26
- secrets:
27
- DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
28
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
29
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30
-
31
- publish_prerelease_experimental:
32
- name: Publish to Experimental channel
33
- uses: facebook/react/.github/workflows/runtime_prereleases.yml@main
34
- permissions:
35
- # We use github.token to download the build artifact from a previous runtime_build_and_test.yml run
36
- actions: read
37
- # NOTE: Intentionally running these jobs sequentially because npm
38
- # will sometimes fail if you try to concurrently publish two
39
- # different versions of the same package, even if they use different
40
- # dist tags.
41
- needs: publish_prerelease_canary
42
- with:
43
- commit_sha: ${{ github.sha }}
44
- release_channel: experimental
45
- dist_tag: experimental
46
- enableFailureNotification: true
47
- dry: false
48
- secrets:
49
- DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
50
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.github/workflows/runtime_release_from_ci.yml
new
+219
@@ -0,0 +1,219 @@
1
+name: (Runtime) Release From CI
2
+
3
+on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ type:
7
+ required: true
8
+ description: Type of release to publish
9
+ type: choice
10
+ # The `─── ... ───` entries are visual separators in the dropdown.
11
+ # They're rejected by the `resolve` job below, so picking one fails
12
+ # the workflow up front instead of silently doing the wrong release.
13
+ # GitHub Actions requires choice values to be unique, so the strings
14
+ # differ by the number of dashes.
15
+ options:
16
+ # publishes canary + experimental.
17
+ - nightly
18
+ - "────────────────"
19
+ - "─────────────────"
20
+ # semver stable published with @latest.
21
+ - stable-latest
22
+ - "──────────────────"
23
+ - "───────────────────"
24
+ # semver stable published with @backport (used for patches to
25
+ # older release lines that shouldn't move @latest).
26
+ - stable-backport
27
+ - "────────────────────"
28
+ - "─────────────────────"
29
+ # only experimental is published.
30
+ - experimental_only
31
+ only_packages:
32
+ description: Packages to publish (space separated allow-list; empty means all)
33
+ type: string
34
+ dry:
35
+ description: Dry run
36
+ type: boolean
37
+ default: false
38
+ force_notify:
39
+ description: Force a Discord notification?
40
+ type: boolean
41
+ default: false
42
+ schedule:
43
+ # At 10 minutes past 16:00 on Mon, Tue, Wed, Thu, and Fri.
44
+ # Scheduled runs always publish a nightly (see `resolve` job).
45
+ - cron: 10 16 * * 1,2,3,4,5
46
+
47
+permissions: {}
48
+
49
+env:
50
+ TZ: /usr/share/zoneinfo/America/Los_Angeles
51
+ # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
52
+ SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
53
+
54
+jobs:
55
+ resolve:
56
+ name: Resolve release inputs
57
+ runs-on: ubuntu-latest
58
+ outputs:
59
+ release_type: ${{ steps.resolve.outputs.release_type }}
60
+ steps:
61
+ - name: Resolve release inputs
62
+ id: resolve
63
+ run: |
64
+ # Scheduled runs always publish a nightly. Manual dispatches always
65
+ # supply `inputs.type`. Anything else is unsupported and fails fast.
66
+ if [ "${{ github.event_name }}" = "schedule" ]; then
67
+ release_type=nightly
68
+ elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
69
+ release_type="${{ inputs.type }}"
70
+ else
71
+ echo "Unsupported event: ${{ github.event_name }}" >&2
72
+ exit 1
73
+ fi
74
+ # Reject the dropdown's separator entries (and anything else that
75
+ # isn't one of the four real release types). Without this, picking
76
+ # a separator would fall through to all of publish.js's `if:` gates
77
+ # being false and the job would succeed without doing anything.
78
+ case "$release_type" in
79
+ nightly|stable-latest|stable-backport|experimental_only) ;;
80
+ *)
81
+ echo "Invalid release type: '$release_type' — pick one of nightly, stable-latest, stable-backport, experimental_only." >&2
82
+ exit 1
83
+ ;;
84
+ esac
85
+ echo "release_type=$release_type" >> "$GITHUB_OUTPUT"
86
+
87
+ notify_starting:
88
+ name: Notify Discord (release starting)
89
+ # Manual dispatches always notify before the release starts so the team
90
+ # has a heads-up that a release is incoming. Scheduled (nightly) runs
91
+ # don't notify up front; we only notify on failure (see `notify` job).
92
+ if: ${{ github.event_name == 'workflow_dispatch' && vars.DISABLE_DISCORD_NOTIFICATIONS != 'true' }}
93
+ runs-on: ubuntu-latest
94
+ steps:
95
+ - uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
96
+ with:
97
+ webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
98
+ embed-author-name: ${{ github.event.sender.login }}
99
+ embed-author-url: ${{ github.event.sender.html_url }}
100
+ embed-author-icon-url: ${{ github.event.sender.avatar_url }}
101
+ embed-title: "⚠️ Publishing ${{ inputs.type }} release from source"
102
+ embed-description: |
103
+ ```json
104
+ ${{ toJson(inputs) }}
105
+ ```
106
+ embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
107
+
108
+ publish:
109
+ name: Publish release
110
+ needs: resolve
111
+ if: ${{ !cancelled() && needs.resolve.result == 'success' }}
112
+ runs-on: ubuntu-latest
113
+ # Protected environment — requires reviewer approval before the publish
114
+ # job starts running.
115
+ environment: npm
116
+ permissions:
117
+ id-token: write
118
+ contents: read
119
+ # `actions: read` lets prepare-release-from-ci.js fetch the
120
+ # runtime_build_and_test workflow's artifacts for github.sha.
121
+ actions: read
122
+ env:
123
+ # Required by scripts/release/shared-commands/download-build-artifacts.js.
124
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125
+ steps:
126
+ # Always check out the commit the workflow file itself was loaded from.
127
+ # Crucially, no user-supplied ref is accepted, since this job runs in a
128
+ # protected environment.
129
+ - uses: actions/checkout@v4
130
+ with:
131
+ ref: ${{ github.sha }}
132
+ - uses: actions/setup-node@v4
133
+ with:
134
+ # Using modern Node.js that ships with NPM supporting trusted publishing
135
+ node-version: "24"
136
+ # scripts/release is the only thing this job actually needs to run —
137
+ # publish.js and prepare-release-from-ci.js both live there and use its
138
+ # own node_modules. Cache is owned by this workflow (this is the only
139
+ # consumer), so on a miss we install and save.
140
+ - name: Restore cached scripts/release node_modules
141
+ uses: actions/cache@v4
142
+ id: release_node_modules
143
+ with:
144
+ path: scripts/release/node_modules
145
+ key: release-scripts-node_modules-v1-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('scripts/release/yarn.lock') }}
146
+ - run: yarn --cwd scripts/release install --frozen-lockfile
147
+ if: steps.release_node_modules.outputs.cache-hit != 'true'
148
+
149
+ # ----- stable (semver) — either @latest or @backport -----
150
+ - name: Stage semver stable artifacts
151
+ if: ${{ startsWith(needs.resolve.outputs.release_type, 'stable-') }}
152
+ run: scripts/release/prepare-release-from-ci.js --skipTests -r latest --commit=${{ github.sha }}
153
+ - name: Publish semver stable to @latest
154
+ if: ${{ needs.resolve.outputs.release_type == 'stable-latest' }}
155
+ run: |
156
+ scripts/release/publish.js \
157
+ --tag=latest \
158
+ ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
159
+ ${{ inputs.dry && '--dry' || '' }}
160
+ # Backport releases stay on the `@backport` dist-tag — we don't move
161
+ # @latest, and (under OIDC) we can't add/remove dist-tags after publish
162
+ # anyway. The tag goes on at publish time and is left in place.
163
+ - name: Publish semver stable to @backport
164
+ if: ${{ needs.resolve.outputs.release_type == 'stable-backport' }}
165
+ run: |
166
+ scripts/release/publish.js \
167
+ --tag=backport \
168
+ ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
169
+ ${{ inputs.dry && '--dry' || '' }}
170
+
171
+ # ----- nightly: canary first, then experimental -----
172
+ # NOTE: Intentionally running sequentially because npm will sometimes
173
+ # fail if you try to concurrently publish two different versions of the
174
+ # same package, even if they use different dist tags.
175
+ - name: Stage canary artifacts
176
+ if: ${{ needs.resolve.outputs.release_type == 'nightly' }}
177
+ run: scripts/release/prepare-release-from-ci.js --skipTests -r stable --commit=${{ github.sha }}
178
+ # Single tag only — OIDC publish tokens can't add additional dist-tags
179
+ # after publish, so the historical `canary,next` aliasing is gone.
180
+ - name: Publish canary to @canary
181
+ if: ${{ needs.resolve.outputs.release_type == 'nightly' }}
182
+ run: |
183
+ scripts/release/publish.js \
184
+ --tag=canary \
185
+ ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
186
+ ${{ inputs.dry && '--dry' || '' }}
187
+
188
+ # ----- experimental (nightly + experimental_only) -----
189
+ - name: Stage experimental artifacts
190
+ if: ${{ needs.resolve.outputs.release_type == 'nightly' || needs.resolve.outputs.release_type == 'experimental_only' }}
191
+ run: scripts/release/prepare-release-from-ci.js --skipTests -r experimental --commit=${{ github.sha }}
192
+ - name: Publish experimental to @experimental
193
+ if: ${{ needs.resolve.outputs.release_type == 'nightly' || needs.resolve.outputs.release_type == 'experimental_only' }}
194
+ run: |
195
+ scripts/release/publish.js \
196
+ --tag=experimental \
197
+ ${{ inputs.only_packages && format('--onlyPackages={0}', inputs.only_packages) || '' }} \
198
+ ${{ inputs.dry && '--dry' || '' }}
199
+
200
+ notify:
201
+ name: Notify Discord on failure
202
+ needs: [resolve, publish]
203
+ # Runs for every workflow run (manual + scheduled) and only fires when
204
+ # something didn't complete successfully — i.e. an actual failure or a
205
+ # cancellation. Successful runs stay silent.
206
+ if: ${{ always() && (needs.resolve.result == 'failure' || needs.resolve.result == 'cancelled' || needs.publish.result == 'failure' || needs.publish.result == 'cancelled') && vars.DISABLE_DISCORD_NOTIFICATIONS != 'true' }}
207
+ runs-on: ubuntu-latest
208
+ steps:
209
+ - name: Discord Webhook Action
210
+ uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
211
+ with:
212
+ webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
213
+ embed-author-name: "GitHub Actions"
214
+ embed-title: "❌ [Runtime] Release from source failed (${{ needs.resolve.outputs.release_type || inputs.type || 'nightly' }})"
215
+ embed-description: |
216
+ resolve: `${{ needs.resolve.result }}`
217
+ publish: `${{ needs.publish.result }}`
218
+ event: `${{ github.event_name }}`
219
+ embed-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/attempts/${{ github.run_attempt }}
.github/workflows/runtime_releases_from_npm_manual.yml
deleted
-128
@@ -1,128 +0,0 @@
1
-name: (Runtime) Publish Releases from NPM Manual
2
-
3
-on:
4
- workflow_dispatch:
5
- inputs:
6
- version_to_promote:
7
- required: true
8
- description: Current npm version (non-experimental) to promote
9
- type: string
10
- version_to_publish:
11
- required: true
12
- description: Version to publish for the specified packages
13
- type: string
14
- only_packages:
15
- description: Packages to publish (space separated)
16
- type: string
17
- skip_packages:
18
- description: Packages to NOT publish (space separated)
19
- type: string
20
- tags:
21
- description: NPM tags (space separated)
22
- type: string
23
- default: untagged
24
- dry:
25
- required: true
26
- description: Dry run instead of publish?
27
- type: boolean
28
- default: true
29
- force_notify:
30
- description: Force a Discord notification?
31
- type: boolean
32
- default: false
33
-
34
-permissions: {}
35
-
36
-env:
37
- TZ: /usr/share/zoneinfo/America/Los_Angeles
38
- # https://github.com/actions/cache/blob/main/tips-and-workarounds.md#cache-segment-restore-timeout
39
- SEGMENT_DOWNLOAD_TIMEOUT_MINS: 1
40
- NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41
-
42
-jobs:
43
- notify:
44
- if: ${{ inputs.force_notify || inputs.dry == false || inputs.dry == 'false' }}
45
- runs-on: ubuntu-latest
46
- steps:
47
- - name: Discord Webhook Action
48
- uses: tsickert/discord-webhook@86dc739f3f165f16dadc5666051c367efa1692f4
49
- with:
50
- webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
51
- embed-author-name: ${{ github.event.sender.login }}
52
- embed-author-url: ${{ github.event.sender.html_url }}
53
- embed-author-icon-url: ${{ github.event.sender.avatar_url }}
54
- embed-title: "⚠️ Publishing release from NPM${{ (inputs.dry && ' (dry run)') || '' }}"
55
- embed-description: |
56
- ```json
57
- ${{ toJson(inputs) }}
58
- ```
59
- embed-url: https://github.com/facebook/react/actions/runs/${{ github.run_id }}
60
-
61
- publish:
62
- name: Publish releases
63
- runs-on: ubuntu-latest
64
- steps:
65
- - uses: actions/checkout@v4
66
- - uses: actions/setup-node@v4
67
- with:
68
- node-version-file: '.nvmrc'
69
- cache: yarn
70
- cache-dependency-path: yarn.lock
71
- - name: Cache node_modules
72
- uses: actions/cache@v4
73
- id: node_modules
74
- with:
75
- path: |
76
- **/node_modules
77
- key: runtime-release-node_modules-v6-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('yarn.lock', 'scripts/release/yarn.lock') }}
78
- - name: Ensure clean build directory
79
- run: rm -rf build
80
- - run: yarn install --frozen-lockfile
81
- if: steps.node_modules.outputs.cache-hit != 'true'
82
- - run: yarn --cwd scripts/release install --frozen-lockfile
83
- if: steps.node_modules.outputs.cache-hit != 'true'
84
- - run: cp ./scripts/release/ci-npmrc ~/.npmrc
85
- - if: '${{ inputs.only_packages }}'
86
- name: 'Prepare ${{ inputs.only_packages }} from NPM'
87
- run: |
88
- scripts/release/prepare-release-from-npm.js \
89
- --ci \
90
- --skipTests \
91
- --version=${{ inputs.version_to_promote }} \
92
- --publishVersion=${{ inputs.version_to_publish }} \
93
- --onlyPackages=${{ inputs.only_packages }}
94
- - if: '${{ inputs.skip_packages }}'
95
- name: 'Prepare all packages EXCEPT ${{ inputs.skip_packages }} from NPM'
96
- run: |
97
- scripts/release/prepare-release-from-npm.js \
98
- --ci \
99
- --skipTests \
100
- --version=${{ inputs.version_to_promote }} \
101
- --publishVersion=${{ inputs.version_to_publish }} \
102
- --skipPackages=${{ inputs.skip_packages }}
103
- - name: Check prepared files
104
- run: ls -R build/node_modules
105
- - if: '${{ inputs.only_packages }}'
106
- name: 'Publish ${{ inputs.only_packages }}'
107
- run: |
108
- scripts/release/publish.js \
109
- --ci \
110
- --tags=${{ inputs.tags }} \
111
- --publishVersion=${{ inputs.version_to_publish }} \
112
- --onlyPackages=${{ inputs.only_packages }} ${{ (inputs.dry && '') || '\'}}
113
- ${{ inputs.dry && '--dry' || '' }}
114
- - if: '${{ inputs.skip_packages }}'
115
- name: 'Publish all packages EXCEPT ${{ inputs.skip_packages }}'
116
- run: |
117
- scripts/release/publish.js \
118
- --ci \
119
- --tags=${{ inputs.tags }} \
120
- --publishVersion=${{ inputs.version_to_publish }} \
121
- --skipPackages=${{ inputs.skip_packages }} ${{ (inputs.dry && '') || '\'}}
122
- ${{ inputs.dry && '--dry' || '' }}
123
- - name: Archive released package for debugging
124
- uses: actions/upload-artifact@v4
125
- with:
126
- name: build
127
- path: |
128
- ./build/node_modules
scripts/release/README.md
+26
-141
@@ -19,18 +19,9 @@ The release process consists of several phases, each one represented by one of t
19
20
A typical release cycle goes like this:
21
1. When a commit is pushed to the React repo, [GitHub Actions](https://github.com/facebook/react/actions) will build all release bundles and run unit tests against both the source code and the built bundles.
22
-2. Each weekday, an automated CI cron job publishes prereleases to the `next` and `experimental` channels, from tip of the main branch.
23
- 1. You can also [trigger an automated prerelease via the GitHub UI](#trigger-an-automated-prerelease), instead of waiting until the next time the cron job runs.
24
- 2. For advanced cases, you can [**manually prepare and publish to the `next` channel**](#publishing-release) using the [`prepare-release-from-ci`](#prepare-release-from-ci) and [`publish`](#publish) scripts; or to the [**`experimental` channel**](#publishing-an-experimental-release) using the same scripts (but different build artifacts).
25
-3. Finally, a "next" release can be [**promoted to stable**](#publishing-a-stable-release)<sup>1</sup> using the [`prepare-release-from-npm`](#prepare-release-from-npm) and [`publish`](#publish) scripts. (This process is always manual.)
26
-
27
-The high level process of creating releases is [documented below](#process). Individual scripts are documented as well:
28
-* [`build-release-locally`](#build-release-locally): Build a release locally from the checked out source code.
29
-* [`prepare-release-from-ci`](#prepare-release-from-ci): Download a pre-built release from CI.
30
-* [`prepare-release-from-npm`](#prepare-release-from-npm): Prepare an NPM "next" release to be published as a "stable" release.
31
-* [`publish`](#publish): Publish the downloaded (or prepared) release to NPM.
32
-
33
-<sup>1. [**Creating a patch release**](#creating-a-patch-release) has a slightly different process than a major/minor release.</sup>
22
+2. Each weekday, an automated CI cron job publishes prereleases to the `canary` and `experimental` channels, from tip of the main branch.
23
+ You can also [trigger an automated prerelease via the GitHub UI](#trigger-an-automated-prerelease), instead of waiting until the next time the cron job runs.
24
+3. Finally, a "canary" release can be [**promoted to stable**](#publishing-a-stable-release)<sup>1</sup> (This process is always manual.)
25
26
## Trigger an Automated Prerelease
27
@@ -38,151 +29,45 @@ If your code lands in the main branch, it will be automatically published to the
29
30
1. Wait for the commit you want to release to finish its [(Runtime) Build and Test workflow](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml), as the prerelease script needs to download the build from that workflow.
31
2. Copy the full git sha of whichever commit you are trying to release
41
-3. Go to https://github.com/facebook/react/actions/workflows/runtime_prereleases_manual.yml
32
+3. Go to https://github.com/facebook/react/actions/workflows/runtime_release_from_ci.yml
33
4. Paste the git sha into the "Run workflow" dropdown
34
5. Let the job finish and it will be released on npm
35
36
This will grab the specified revision on the main branch and publish it to the Next and Experimental channels.
46
-## Publishing Without Tags
47
-
48
-The sections below include meaningful `--tags` in the instructions. However, keep in mind that **the `--tags` arguments is optional**, and you can omit it if you don't want to tag the release on npm at all. This can be useful when preparing breaking changes.
49
-
50
-## Publishing Next
37
52
-"Next" builds are meant to be lightweight and published often. In most cases, they can be published using artifacts built by Circle CI.
53
-
54
-To prepare a build for a particular commit:
55
-1. Choose a commit from [the commit log](https://github.com/facebook/react/commits/main).
56
-2. Copy the SHA (by clicking the 📋 button)
57
-5. Run the [`prepare-release-from-ci`](#prepare-release-from-ci) script with the SHA <sup>1</sup> you found:
58
-```sh
59
-scripts/release/prepare-release-from-ci.js -r stable --commit=0e526bc
60
-```
61
-
62
-Once the build has been checked out and tested locally, you're ready to publish it:
63
-```sh
64
-scripts/release/publish.js --tags next
65
-```
66
-
67
-<sup>1: You can omit the `commit` param if you just want to release the latest commit as to "next".</sup>
38
39
## Publishing an Experimental Release
40
71
-Experimental releases are special because they have additional features turned on.
72
-
73
-The steps for publishing an experimental release are almost the same as for publishing a "next" release except for the release channel (`-r`) flag.
74
-
75
-```sh
76
-scripts/release/prepare-release-from-ci.js -r experimental --commit=0e526bc
77
-```
41
+Same as for a prerelease except choose `experimental-only` as the type
42
79
-Once the build has been checked out and tested locally, you're ready to publish it. When publishing an experimental release, use the `experimental` tag:
43
+## Publishing Without Tags
44
81
-```sh
82
-scripts/release/publish.js --tags experimental
83
-```
45
+The sections below include meaningful `--tags` in the instructions. However, keep in mind that **the `--tags` arguments is optional**, and you can omit it if you don't want to tag the release on npm at all. This can be useful when preparing breaking changes.
46
47
## Publishing a Stable Release
48
87
-Stable releases should always be created from the "next" channel. This encourages better testing of the actual release artifacts and reduces the chance of unintended changes accidentally being included in a stable release.
88
-
89
-To prepare a stable release, choose a "next" version and run the [`prepare-release-from-npm`](#prepare-release-from-npm) script <sup>1</sup>:
90
-
91
-```sh
92
-scripts/release/prepare-release-from-npm.js --version=0.0.0-241c4467e-20200129
93
-```
94
-
95
-This script will prompt you to select stable version numbers for each of the packages. It will update the package JSON versions (and dependencies) based on the numbers you select.
96
-
97
-Once this step is complete, you're ready to publish the release:
98
-
99
-```sh
100
-scripts/release/publish.js --tags latest
101
-
102
-# Or, if you want to bump "next" as well:
103
-scripts/release/publish.js --tags latest next
104
-```
105
-
106
-After successfully publishing the release, follow the on-screen instructions to ensure that all of the appropriate post-release steps are executed.
107
-
108
-<sup>1: You can omit the `version` param if you just want to promote the latest "next" candidate to stable.</sup>
109
-
110
-## Creating a Patch Release
111
-
112
-Patch releases should always be created by branching from a previous release. This reduces the likelihood of unstable changes being accidentally included in the release.
113
-
114
-Begin by creating a branch from the previous git tag<sup>1</sup>:
115
-
116
-```sh
117
-git checkout -b 16.8.3 v16.8.2
118
-```
119
-
120
-Next cherry pick any changes from main that you want to include in the release:
121
-
122
-```sh
123
-git cherry-pick <commit-hash>
124
-```
125
-
126
-Once you have cherry picked all of the commits you want to include in the release, push your feature branch and create a Pull Request (so that Circle CI will create a build):
127
-
128
-```sh
129
-git push origin 16.8.3
130
-```
131
-
132
-Once CI is complete, follow the regular [**next**](#publishing-release) and [**promote to stable**](#publishing-a-stable-release) processes.
133
-
134
-<sup>1: The `build-info.json` artifact can also be used to identify the appropriate commit (e.g. [unpkg.com/react@16.8.3/build-info.json](https://unpkg.com/react@16.8.3/build-info.json) shows us that react version 16.8.3 was created from commit [`29b7b775f`](https://github.com/facebook/react/commit/29b7b775f)).</sup>
135
-
136
-# Scripts
137
-
138
-## `build-release-locally`
139
-Creates a "next" build from the current (local) Git revision.
140
-
141
-**This script is an escape hatch.** It allows a release to be created without pushing a commit to be verified by Circle CI. **It does not run any automated unit tests.** Testing is solely the responsibility of the release engineer.
142
-
143
-Note that this script git-archives the React repo (at the current revision) to a temporary directory before building, so **uncommitted changes are not included in the build**.
144
-
145
-#### Example usage
146
-To create a build from the current branch and revision:
147
-```sh
148
-scripts/release/build-release-locally.js
149
-```
150
-
151
-## `prepare-release-from-ci`
152
-Downloads build artifacts from Circle CI in preparation to be published to NPM as either a "next" or "experimental" release.
153
-
154
-All artifacts built by Circle CI have already been unit-tested (both source and bundles) but these candidates should **always be manually tested** before being published. Upon completion, this script prints manual testing instructions.
155
-
156
-#### Example usage
157
-To prepare the artifacts created by Circle CI for commit [0e526bc](https://github.com/facebook/react/commit/0e526bc) you would run:
158
-```sh
159
-scripts/release/prepare-release-from-ci.js --commit=0e526bc -r stable
160
-```
161
-
162
-## `prepare-release-from-npm`
163
-Checks out a "next" release from NPM and prepares it to be published as a stable release.
164
-
165
-This script prompts for new (stable) release versions for each public package and updates the package contents (both `package.json` and inline version numbers) to match. It also updates inter-package dependencies to account for the new versions.
166
-
167
-"Next" releases have already been tested but it is still a good idea to **manually test and verify a release** before publishing to ensure that e.g. version numbers are correct. Upon completion, this script prints manual testing instructions.
49
+Stable releases should always be created from the "canary" channel. This encourages better testing of the actual release artifacts and reduces the chance of unintended changes accidentally being included in a stable release.
50
169
-#### Example usage
170
-To promote the "next" release `0.0.0-241c4467e-20200129` (aka commit [241c4467e](https://github.com/facebook/react/commit/241c4467e)) to stable:
171
-```sh
172
-scripts/release/prepare-release-from-npm.js --version=0.0.0-241c4467e-20200129
173
-```
51
+Before promoting, make sure the versions have been bumped:
52
175
-## `publish`
176
-Publishes the current contents of `build/node_modules` to NPM.
53
+1. [ReactVersions.js](../../ReactVersions.js)
54
+1. `package.json` files for each package
55
+1. [packages/shared/ReactVersion.js](../../packages/shared/ReactVersion.js)
56
178
-This script publishes each public package to NPM and updates the specified tag(s) to match. **It does not test or verify the local package contents before publishing**. This should be done by the release engineer prior to running the script.
57
+Once the "canary" release has been tested and verified, you can promote it to stable by running the [Publish release](./actions/workflows/runtime_release_from_ci.yml) GitHub Action workflow. This workflow will prepare the release artifacts and publish them to NPM as either `stable-latest` (e.g. for `react@latest`) or `stable-backport` for an older release line that shouldn't move `@latest` (published under the `@backport` dist-tag instead).
58
180
-Upon completion, this script provides instructions for tagging the Git commit that the package was created from and updating the release CHANGELOG.
59
+> [!IMPORTANT]
60
+> The designated commit must be able to build in CI. If runtime_build_and_test.yml fails for that commit, the release workflow will also fail.
61
182
-**Specify a `--dry` flag when running this script if you want to skip the NPM-publish step.** In this event, the script will print the NPM commands but it will not actually run them.
62
+### Backport
63
184
-#### Example usage
185
-To publish a release to NPM as both `next` and `latest`:
186
-```sh
187
-scripts/release/publish.js --tags latest next
188
-```
64
+1. Pick a commit from `main` which you want to backport
65
+1. Choose which release you want to backport it to
66
+1. Find or create a branch for that release following our release branch naming convention `releases/**/*` e.g. `releases/19.1.x`
67
+1. Make sure versions (`ReactVersions.js`, `ReactVersion.js`, `package.json`) are set to an unreleased version
68
+1. Cherry-pick desired commits
69
+1. Push to branch
70
+1. [Publish release](./actions/workflows/runtime_release_from_ci.yml)
71
+ - `workflow_from` is the newly pushed commit containing cherry-picked changes,
72
+ - `type` is either `stable-latest` (e.g. for `react@latest`) or `stable-backport` for an older release line that shouldn't move `@latest` (published under the `@backport` dist-tag instead).
73
+1. For stable releases the workflow will prepare everything for an automated publish. However, the final publish step will require manual approval in the GitHub Actions UI.
scripts/release/prepare-release-from-npm-commands/check-out-packages.js
deleted
-60
@@ -1,60 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const {exec} = require('child-process-promise');
6
-const {existsSync} = require('fs');
7
-const {join} = require('path');
8
-const {execRead, logPromise} = require('../utils');
9
-const theme = require('../theme');
10
-
11
-const run = async ({cwd, local, packages, version}) => {
12
- if (local) {
13
- // Sanity test
14
- if (!existsSync(join(cwd, 'build', 'node_modules', 'react'))) {
15
- console.error(theme.error`No local build exists.`);
16
- process.exit(1);
17
- }
18
- return;
19
- }
20
-
21
- if (!existsSync(join(cwd, 'build'))) {
22
- await exec(`mkdir ./build`, {cwd});
23
- }
24
-
25
- // Cleanup from previous builds
26
- await exec(`rm -rf ./build/node_modules*`, {cwd});
27
- await exec(`mkdir ./build/node_modules`, {cwd});
28
-
29
- const nodeModulesPath = join(cwd, 'build/node_modules');
30
-
31
- // Checkout "next" release from NPM for all local packages
32
- for (let i = 0; i < packages.length; i++) {
33
- const packageName = packages[i];
34
-
35
- // We previously used `npm install` for this,
36
- // but in addition to checking out a lot of transient dependencies that we don't care about–
37
- // the NPM client also added a lot of registry metadata to the package JSONs,
38
- // which we had to remove as a separate step before re-publishing.
39
- // It's easier for us to just download and extract the tarball.
40
- const url = await execRead(
41
- `npm view ${packageName}@${version} dist.tarball`
42
- );
43
- const filePath = join(nodeModulesPath, `${packageName}.tgz`);
44
- const packagePath = join(nodeModulesPath, `${packageName}`);
45
- const tempPackagePath = join(nodeModulesPath, 'package');
46
-
47
- // Download packages from NPM and extract them to the expected build locations.
48
- await exec(`curl -L ${url} > ${filePath}`, {cwd});
49
- await exec(`tar -xvzf ${filePath} -C ${nodeModulesPath}`, {cwd});
50
- await exec(`mv ${tempPackagePath} ${packagePath}`, {cwd});
51
- await exec(`rm ${filePath}`, {cwd});
52
- }
53
-};
54
-
55
-module.exports = async params => {
56
- return logPromise(
57
- run(params),
58
- theme`Checking out "next" from NPM {version ${params.version}}`
59
- );
60
-};
scripts/release/prepare-release-from-npm-commands/confirm-stable-version-numbers.js
deleted
-71
@@ -1,71 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const prompt = require('prompt-promise');
6
-const semver = require('semver');
7
-const theme = require('../theme');
8
-const {confirm} = require('../utils');
9
-
10
-const run = async ({ci, skipPackages}, versionsMap) => {
11
- const groupedVersionsMap = new Map();
12
-
13
- // Group packages with the same source versions.
14
- // We want these to stay lock-synced anyway.
15
- // This will require less redundant input from the user later,
16
- // and reduce the likelihood of human error (entering the wrong version).
17
- versionsMap.forEach((version, packageName) => {
18
- if (!groupedVersionsMap.has(version)) {
19
- groupedVersionsMap.set(version, [packageName]);
20
- } else {
21
- groupedVersionsMap.get(version).push(packageName);
22
- }
23
- });
24
-
25
- if (ci !== true) {
26
- // Prompt user to confirm or override each version group if not running in CI.
27
- const entries = [...groupedVersionsMap.entries()];
28
- for (let i = 0; i < entries.length; i++) {
29
- const [bestGuessVersion, packages] = entries[i];
30
- const packageNames = packages.map(name => theme.package(name)).join(', ');
31
-
32
- let version = bestGuessVersion;
33
- if (
34
- skipPackages.some(skipPackageName => packages.includes(skipPackageName))
35
- ) {
36
- await confirm(
37
- theme`{spinnerSuccess ✓} Version for ${packageNames} will remain {version ${bestGuessVersion}}`
38
- );
39
- } else {
40
- const defaultVersion = bestGuessVersion
41
- ? theme.version(` (default ${bestGuessVersion})`)
42
- : '';
43
- version =
44
- (await prompt(
45
- theme`{spinnerSuccess ✓} Version for ${packageNames}${defaultVersion}: `
46
- )) || bestGuessVersion;
47
- prompt.done();
48
- }
49
-
50
- // Verify a valid version has been supplied.
51
- try {
52
- semver(version);
53
-
54
- packages.forEach(packageName => {
55
- versionsMap.set(packageName, version);
56
- });
57
- } catch (error) {
58
- console.log(
59
- theme`{spinnerError ✘} Version {version ${version}} is invalid.`
60
- );
61
-
62
- // Prompt again
63
- i--;
64
- }
65
- }
66
- }
67
-};
68
-
69
-// Run this directly because it's fast,
70
-// and logPromise would interfere with console prompting.
71
-module.exports = run;
scripts/release/prepare-release-from-npm-commands/get-latest-next-version.js
deleted
-15
@@ -1,15 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const {execRead, logPromise} = require('../utils');
6
-
7
-const run = async () => {
8
- const version = await execRead('npm info react@canary version');
9
-
10
- return version;
11
-};
12
-
13
-module.exports = async params => {
14
- return logPromise(run(params), 'Determining latest "canary" release version');
15
-};
scripts/release/prepare-release-from-npm-commands/guess-stable-version-numbers.js
deleted
-63
@@ -1,63 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const semver = require('semver');
6
-const {execRead, logPromise} = require('../utils');
7
-
8
-const run = async (
9
- {cwd, packages, skipPackages, ci, publishVersion},
10
- versionsMap
11
-) => {
12
- const branch = await execRead('git branch | grep \\* | cut -d " " -f2', {
13
- cwd,
14
- });
15
-
16
- for (let i = 0; i < packages.length; i++) {
17
- const packageName = packages[i];
18
-
19
- if (ci === true) {
20
- if (publishVersion != null) {
21
- versionsMap.set(packageName, publishVersion);
22
- } else {
23
- console.error(
24
- 'When running in CI mode, a publishVersion must be supplied'
25
- );
26
- process.exit(1);
27
- }
28
- } else {
29
- try {
30
- // In case local package JSONs are outdated,
31
- // guess the next version based on the latest NPM release.
32
- const version = await execRead(`npm show ${packageName} version`);
33
-
34
- if (skipPackages.includes(packageName)) {
35
- versionsMap.set(packageName, version);
36
- } else {
37
- const {major, minor, patch} = semver(version);
38
-
39
- // Guess the next version by incrementing patch.
40
- // The script will confirm this later.
41
- // By default, new releases from mains should increment the minor version number,
42
- // and patch releases should be done from branches.
43
- if (branch === 'main') {
44
- versionsMap.set(packageName, `${major}.${minor + 1}.0`);
45
- } else {
46
- versionsMap.set(packageName, `${major}.${minor}.${patch + 1}`);
47
- }
48
- }
49
- } catch (error) {
50
- // If the package has not yet been published,
51
- // we'll require a version number to be entered later.
52
- versionsMap.set(packageName, null);
53
- }
54
- }
55
- }
56
-};
57
-
58
-module.exports = async (params, versionsMap) => {
59
- return logPromise(
60
- run(params, versionsMap),
61
- 'Guessing stable version numbers'
62
- );
63
-};
scripts/release/prepare-release-from-npm-commands/parse-params.js
deleted
-62
@@ -1,62 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const commandLineArgs = require('command-line-args');
6
-const {splitCommaParams} = require('../utils');
7
-
8
-const paramDefinitions = [
9
- {
10
- name: 'local',
11
- type: Boolean,
12
- description:
13
- 'Skip NPM and use the build already present in "build/node_modules".',
14
- defaultValue: false,
15
- },
16
- {
17
- name: 'onlyPackages',
18
- type: String,
19
- multiple: true,
20
- description: 'Packages to include in publishing',
21
- defaultValue: [],
22
- },
23
- {
24
- name: 'skipPackages',
25
- type: String,
26
- multiple: true,
27
- description: 'Packages to exclude from publishing',
28
- defaultValue: [],
29
- },
30
- {
31
- name: 'skipTests',
32
- type: Boolean,
33
- description: 'Skip automated fixture tests.',
34
- defaultValue: false,
35
- },
36
- {
37
- name: 'version',
38
- type: String,
39
- description:
40
- 'Version of published "next" release (e.g. 0.0.0-0e526bcec-20210202)',
41
- },
42
- {
43
- name: 'publishVersion',
44
- type: String,
45
- description: 'Version to publish',
46
- },
47
- {
48
- name: 'ci',
49
- type: Boolean,
50
- description: 'Run in automated environment, without interactive prompts.',
51
- defaultValue: false,
52
- },
53
-];
54
-
55
-module.exports = () => {
56
- const params = commandLineArgs(paramDefinitions);
57
-
58
- splitCommaParams(params.skipPackages);
59
- splitCommaParams(params.onlyPackages);
60
-
61
- return params;
62
-};
scripts/release/prepare-release-from-npm-commands/update-stable-version-numbers.js
deleted
-167
@@ -1,167 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const clear = require('clear');
6
-const {readFileSync, writeFileSync} = require('fs');
7
-const {readJson, writeJson} = require('fs-extra');
8
-const {join, relative} = require('path');
9
-const {confirm, execRead, printDiff} = require('../utils');
10
-const theme = require('../theme');
11
-
12
-const run = async ({cwd, packages, version, ci}, versionsMap) => {
13
- const nodeModulesPath = join(cwd, 'build/node_modules');
14
-
15
- // Cache all package JSONs for easy lookup below.
16
- const sourcePackageJSONs = new Map();
17
- for (let i = 0; i < packages.length; i++) {
18
- const packageName = packages[i];
19
- const sourcePackageJSON = await readJson(
20
- join(cwd, 'packages', packageName, 'package.json')
21
- );
22
- sourcePackageJSONs.set(packageName, sourcePackageJSON);
23
- }
24
-
25
- const updateDependencies = async (targetPackageJSON, key) => {
26
- const targetDependencies = targetPackageJSON[key];
27
- if (targetDependencies) {
28
- const sourceDependencies = sourcePackageJSONs.get(targetPackageJSON.name)[
29
- key
30
- ];
31
-
32
- for (let i = 0; i < packages.length; i++) {
33
- const dependencyName = packages[i];
34
- const targetDependency = targetDependencies[dependencyName];
35
-
36
- if (targetDependency) {
37
- // For example, say we're updating react-dom's dependency on scheduler.
38
- // We compare source packages to determine what the new scheduler dependency constraint should be.
39
- // To do this, we look at both the local version of the scheduler (e.g. 0.11.0),
40
- // and the dependency constraint in the local version of react-dom (e.g. scheduler@^0.11.0).
41
- const sourceDependencyVersion =
42
- sourcePackageJSONs.get(dependencyName).version;
43
- const sourceDependencyConstraint = sourceDependencies[dependencyName];
44
-
45
- // If the source dependency's version and the constraint match,
46
- // we will need to update the constraint to point at the dependency's new release version,
47
- // (e.g. scheduler@^0.11.0 becomes scheduler@^0.12.0 when we release scheduler 0.12.0).
48
- // Otherwise we leave the constraint alone (e.g. react@^16.0.0 doesn't change between releases).
49
- // Note that in both cases, we must update the target package JSON,
50
- // since "next" releases are all locked to the version (e.g. 0.0.0-0e526bcec-20210202).
51
- if (
52
- sourceDependencyVersion ===
53
- sourceDependencyConstraint.replace(/^[\^\~]/, '')
54
- ) {
55
- targetDependencies[dependencyName] =
56
- sourceDependencyConstraint.replace(
57
- sourceDependencyVersion,
58
- versionsMap.get(dependencyName)
59
- );
60
- } else {
61
- targetDependencies[dependencyName] = sourceDependencyConstraint;
62
- }
63
- }
64
- }
65
- }
66
- };
67
-
68
- // Update all package JSON versions and their dependencies/peerDependencies.
69
- // This must be done in a way that respects semver constraints (e.g. 16.7.0, ^16.7.0, ^16.0.0).
70
- // To do this, we use the dependencies defined in the source package JSONs,
71
- // because the "next" dependencies have already been flattened to an exact match (e.g. 0.0.0-0e526bcec-20210202).
72
- for (let i = 0; i < packages.length; i++) {
73
- const packageName = packages[i];
74
- const packageJSONPath = join(nodeModulesPath, packageName, 'package.json');
75
- const packageJSON = await readJson(packageJSONPath);
76
- packageJSON.version = versionsMap.get(packageName);
77
-
78
- await updateDependencies(packageJSON, 'dependencies');
79
- await updateDependencies(packageJSON, 'peerDependencies');
80
-
81
- await writeJson(packageJSONPath, packageJSON, {spaces: 2});
82
- }
83
-
84
- clear();
85
-
86
- // Print the map of versions and their dependencies for confirmation.
87
- const printDependencies = (maybeDependency, label) => {
88
- if (maybeDependency) {
89
- for (let dependencyName in maybeDependency) {
90
- if (packages.includes(dependencyName)) {
91
- console.log(
92
- theme`• {package ${dependencyName}} {version ${maybeDependency[dependencyName]}} {dimmed ${label}}`
93
- );
94
- }
95
- }
96
- }
97
- };
98
- for (let i = 0; i < packages.length; i++) {
99
- const packageName = packages[i];
100
- const packageJSONPath = join(nodeModulesPath, packageName, 'package.json');
101
- const packageJSON = await readJson(packageJSONPath);
102
- console.log(
103
- theme`\n{package ${packageName}} {version ${versionsMap.get(
104
- packageName
105
- )}}`
106
- );
107
- printDependencies(packageJSON.dependencies, 'dependency');
108
- printDependencies(packageJSON.peerDependencies, 'peer');
109
- }
110
- if (ci !== true) {
111
- await confirm('Do the versions above look correct?');
112
- }
113
-
114
- clear();
115
-
116
- if (packages.includes('react')) {
117
- // We print the diff to the console for review,
118
- // but it can be large so let's also write it to disk.
119
- const diffPath = join(cwd, 'build', 'temp.diff');
120
- let diff = '';
121
- let numFilesModified = 0;
122
-
123
- // Find-and-replace hardcoded version (in built JS) for renderers.
124
- for (let i = 0; i < packages.length; i++) {
125
- const packageName = packages[i];
126
- const packagePath = join(nodeModulesPath, packageName);
127
-
128
- let files = await execRead(
129
- `find ${packagePath} -name '*.js' -exec echo {} \\;`,
130
- {cwd}
131
- );
132
- files = files.split('\n');
133
- files.forEach(path => {
134
- const newStableVersion = versionsMap.get(packageName);
135
- const beforeContents = readFileSync(path, 'utf8', {cwd});
136
- let afterContents = beforeContents;
137
- // Replace all "next" version numbers (e.g. header @license).
138
- while (afterContents.indexOf(version) >= 0) {
139
- afterContents = afterContents.replace(version, newStableVersion);
140
- }
141
- if (beforeContents !== afterContents) {
142
- numFilesModified++;
143
- // Using a relative path for diff helps with the snapshot test
144
- diff += printDiff(relative(cwd, path), beforeContents, afterContents);
145
- writeFileSync(path, afterContents, {cwd});
146
- }
147
- });
148
- }
149
- writeFileSync(diffPath, diff, {cwd});
150
- console.log(theme.header(`\n${numFilesModified} files have been updated.`));
151
- console.log(
152
- theme`A full diff is available at {path ${relative(cwd, diffPath)}}.`
153
- );
154
- if (ci !== true) {
155
- await confirm('Do the changes above look correct?');
156
- }
157
- } else {
158
- console.log(
159
- theme`Skipping React renderer version update because React is not included in the release.`
160
- );
161
- }
162
-
163
- clear();
164
-};
165
-
166
-// Run this directly because logPromise would interfere with printing package dependencies.
167
-module.exports = run;
scripts/release/prepare-release-from-npm.js
deleted
-71
@@ -1,71 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const {join} = require('path');
6
-const {getPublicPackages, handleError} = require('./utils');
7
-
8
-const checkOutPackages = require('./prepare-release-from-npm-commands/check-out-packages');
9
-const confirmStableVersionNumbers = require('./prepare-release-from-npm-commands/confirm-stable-version-numbers');
10
-const getLatestNextVersion = require('./prepare-release-from-npm-commands/get-latest-next-version');
11
-const guessStableVersionNumbers = require('./prepare-release-from-npm-commands/guess-stable-version-numbers');
12
-const parseParams = require('./prepare-release-from-npm-commands/parse-params');
13
-const printPrereleaseSummary = require('./shared-commands/print-prerelease-summary');
14
-const testPackagingFixture = require('./shared-commands/test-packaging-fixture');
15
-const updateStableVersionNumbers = require('./prepare-release-from-npm-commands/update-stable-version-numbers');
16
-const theme = require('./theme');
17
-
18
-const run = async () => {
19
- try {
20
- const params = parseParams();
21
- params.cwd = join(__dirname, '..', '..');
22
-
23
- const isExperimental = params.version.includes('experimental');
24
-
25
- if (!params.version) {
26
- params.version = await getLatestNextVersion();
27
- }
28
-
29
- if (params.onlyPackages.length > 0 && params.skipPackages.length > 0) {
30
- console.error(
31
- '--onlyPackages and --skipPackages cannot be used together'
32
- );
33
- process.exit(1);
34
- }
35
-
36
- params.packages = await getPublicPackages(isExperimental);
37
- params.packages = params.packages.filter(packageName => {
38
- if (params.onlyPackages.length > 0) {
39
- return params.onlyPackages.includes(packageName);
40
- }
41
- return !params.skipPackages.includes(packageName);
42
- });
43
-
44
- // Map of package name to upcoming stable version.
45
- // This Map is initially populated with guesses based on local versions.
46
- // The developer running the release later confirms or overrides each version.
47
- const versionsMap = new Map();
48
-
49
- if (isExperimental) {
50
- console.error(
51
- theme.error`Cannot promote an experimental build to stable.`
52
- );
53
- process.exit(1);
54
- }
55
-
56
- await checkOutPackages(params);
57
- await guessStableVersionNumbers(params, versionsMap);
58
- await confirmStableVersionNumbers(params, versionsMap);
59
- await updateStableVersionNumbers(params, versionsMap);
60
-
61
- if (!params.skipTests) {
62
- await testPackagingFixture(params);
63
- }
64
-
65
- await printPrereleaseSummary(params, true);
66
- } catch (error) {
67
- handleError(error);
68
- }
69
-};
70
-
71
-run();
scripts/release/publish-commands/check-npm-permissions.js
deleted
-44
@@ -1,44 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const {execRead, logPromise} = require('../utils');
6
-const theme = require('../theme');
7
-
8
-const run = async ({cwd, packages, version}) => {
9
- const currentUser = await execRead('npm whoami');
10
- const failedProjects = [];
11
-
12
- const checkProject = async project => {
13
- const owners = (await execRead(`npm owner ls ${project}`))
14
- .split('\n')
15
- .filter(owner => owner)
16
- .map(owner => owner.split(' ')[0]);
17
-
18
- if (!owners.includes(currentUser)) {
19
- failedProjects.push(project);
20
- }
21
- };
22
-
23
- await logPromise(
24
- Promise.all(packages.map(checkProject)),
25
- theme`Checking NPM permissions for {underline ${currentUser}}.`
26
- );
27
-
28
- if (failedProjects.length) {
29
- console.error(
30
- theme`
31
- {error Insufficient NPM permissions}
32
- \nNPM user {underline ${currentUser}} is not an owner for: ${failedProjects
33
- .map(name => theme.package(name))
34
- .join(', ')}
35
- \nPlease contact a React team member to be added to the above project(s).
36
- `
37
- .replace(/\n +/g, '\n')
38
- .trim()
39
- );
40
- process.exit(1);
41
- }
42
-};
43
-
44
-module.exports = run;
scripts/release/publish-commands/confirm-skipped-packages.js
deleted
-31
@@ -1,31 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const clear = require('clear');
6
-const {confirm} = require('../utils');
7
-const theme = require('../theme');
8
-
9
-const run = async ({cwd, packages, skipPackages, tags}) => {
10
- if (skipPackages.length === 0) {
11
- return;
12
- }
13
-
14
- clear();
15
-
16
- console.log(
17
- theme`{spinnerSuccess ✓} The following packages will not be published as part of this release`
18
- );
19
-
20
- skipPackages.forEach(packageName => {
21
- console.log(theme`• {package ${packageName}}`);
22
- });
23
-
24
- await confirm('Do you want to proceed?');
25
-
26
- clear();
27
-};
28
-
29
-// Run this directly because it's fast,
30
-// and logPromise would interfere with console prompting.
31
-module.exports = run;
scripts/release/publish-commands/confirm-version-and-tags.js
+7
-31
@@ -2,29 +2,14 @@
2
3
'use strict';
4
5
-const clear = require('clear');
5
const {readJson} = require('fs-extra');
6
const {join} = require('path');
8
-const {confirm} = require('../utils');
7
const theme = require('../theme');
8
11
-const run = async ({cwd, packages, tags, ci}) => {
12
- clear();
13
-
14
- if (tags.length === 0) {
15
- console.error('Expected at least one tag.');
16
- process.exit(1);
17
- } else if (tags.length === 1) {
18
- console.log(
19
- theme`{spinnerSuccess ✓} You are about the publish the following packages under the tag {tag ${tags}}:`
20
- );
21
- } else {
22
- console.log(
23
- theme`{spinnerSuccess ✓} You are about the publish the following packages under the tags {tag ${tags.join(
24
- ', '
25
- )}}:`
26
- );
27
- }
9
+const run = async ({cwd, packages, tag}) => {
10
+ console.log(
11
+ theme`{spinnerSuccess ✓} You are about the publish the following packages under the tag {tag ${tag}}:`
12
+ );
13
14
for (let i = 0; i < packages.length; i++) {
15
const packageName = packages[i];
@@ -36,20 +21,11 @@ const run = async ({cwd, packages, tags, ci}) => {
21
);
22
const packageJSON = await readJson(packageJSONPath);
23
console.log(
39
- theme`• {package ${packageName}} {version ${packageJSON.version}}`
24
+ `::group::${theme`{package ${packageName}} {version ${packageJSON.version}}`}`
25
);
41
- if (ci) {
42
- console.log(packageJSON);
43
- }
44
- }
45
-
46
- if (!ci) {
47
- await confirm('Do you want to proceed?');
26
+ console.log(packageJSON);
27
+ console.log('::endgroup::');
28
}
49
-
50
- clear();
29
};
30
53
-// Run this directly because it's fast,
54
-// and logPromise would interfere with console prompting.
31
module.exports = run;
scripts/release/publish-commands/parse-params.js
+33
-33
@@ -13,11 +13,12 @@ const paramDefinitions = [
13
defaultValue: false,
14
},
15
{
16
- name: 'tags',
16
+ name: 'tag',
17
type: String,
18
- multiple: true,
19
- description: 'NPM tags to point to the new release.',
20
- defaultValue: ['untagged'],
18
+ description:
19
+ 'NPM dist-tag to attach at publish time. OIDC trusted publishing ' +
20
+ 'authorizes a single tag per publish, so only one value is accepted ' +
21
+ '— passing comma-separated tags or repeating --tag is rejected.',
22
},
23
{
24
name: 'onlyPackages',
@@ -33,40 +34,39 @@ const paramDefinitions = [
34
description: 'Packages to exclude from publishing',
35
defaultValue: [],
36
},
36
- {
37
- name: 'ci',
38
- type: Boolean,
39
- description: 'Run in automated environment, without interactive prompts.',
40
- defaultValue: false,
41
- },
42
- {
43
- name: 'publishVersion',
44
- type: String,
45
- description: 'Version to publish',
46
- },
37
];
38
39
module.exports = () => {
40
const params = commandLineArgs(paramDefinitions);
41
splitCommaParams(params.skipPackages);
42
splitCommaParams(params.onlyPackages);
53
- splitCommaParams(params.tags);
54
- params.tags.forEach(tag => {
55
- switch (tag) {
56
- case 'latest':
57
- case 'canary':
58
- case 'next':
59
- case 'experimental':
60
- case 'alpha':
61
- case 'beta':
62
- case 'rc':
63
- case 'untagged':
64
- break;
65
- default:
66
- console.error('Unsupported tag: "' + tag + '"');
67
- process.exit(1);
68
- break;
69
- }
70
- });
43
+
44
+ // Single-tag invariant. `command-line-args` already collapses multiple
45
+ // --tag occurrences to the last value (since `multiple` is not set), but it
46
+ // happily accepts `--tag a,b` as the literal string "a,b". Reject that
47
+ // here so the failure is loud and obvious instead of being deferred to a
48
+ // later "Unsupported tag" message that doesn't explain the cause.
49
+ if (params.tag == null || params.tag === '') {
50
+ console.error('--tag is required and must be a single dist-tag.');
51
+ process.exit(1);
52
+ }
53
+ if (params.tag.includes(',') || params.tag.includes(' ')) {
54
+ console.error('Only a single --tag is allowed, got: "' + params.tag + '"');
55
+ process.exit(1);
56
+ }
57
+ switch (params.tag) {
58
+ case 'latest':
59
+ case 'canary':
60
+ case 'experimental':
61
+ case 'backport':
62
+ case 'alpha':
63
+ case 'beta':
64
+ case 'rc':
65
+ break;
66
+ default:
67
+ console.error('Unsupported tag: "' + params.tag + '"');
68
+ process.exit(1);
69
+ }
70
+
71
return params;
72
};
scripts/release/publish-commands/print-follow-up-instructions.js
deleted
-129
@@ -1,129 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const clear = require('clear');
6
-const {existsSync} = require('fs');
7
-const {readJsonSync} = require('fs-extra');
8
-const {join} = require('path');
9
-const theme = require('../theme');
10
-const {execRead} = require('../utils');
11
-
12
-const run = async ({cwd, packages, tags}) => {
13
- // Tags are named after the react version.
14
- const {version} = readJsonSync(
15
- `${cwd}/build/node_modules/react/package.json`
16
- );
17
-
18
- clear();
19
-
20
- if (tags.length === 1 && tags[0] === 'next') {
21
- console.log(
22
- theme`{header A "next" release} {version ${version}} {header has been published!}`
23
- );
24
- } else if (tags.length === 1 && tags[0] === 'experimental') {
25
- console.log(
26
- theme`{header An "experimental" release} {version ${version}} {header has been published!}`
27
- );
28
- } else {
29
- const nodeModulesPath = join(cwd, 'build/node_modules');
30
-
31
- console.log(
32
- theme.caution`The release has been published but you're not done yet!`
33
- );
34
-
35
- if (tags.includes('latest')) {
36
- // All packages are built from a single source revision,
37
- // so it is safe to read build info from any one of them.
38
- const arbitraryPackageName = packages[0];
39
- // FIXME: New build script does not output build-info.json. It's only used
40
- // by this post-publish print job, and only for "latest" releases, so I've
41
- // disabled it as a workaround so the publish script doesn't crash for
42
- // "next" and "experimental" pre-releases.
43
- const {commit} = readJsonSync(
44
- join(
45
- cwd,
46
- 'build',
47
- 'node_modules',
48
- arbitraryPackageName,
49
- 'build-info.json'
50
- )
51
- );
52
-
53
- console.log();
54
- console.log(
55
- theme.header`Please review and commit all local, staged changes.`
56
- );
57
-
58
- console.log();
59
- console.log('Version numbers have been updated in the following files:');
60
- for (let i = 0; i < packages.length; i++) {
61
- const packageName = packages[i];
62
- console.log(theme.path`• packages/%s/package.json`, packageName);
63
- }
64
- const status = await execRead(
65
- 'git diff packages/shared/ReactVersion.js',
66
- {cwd}
67
- );
68
- if (status) {
69
- console.log(theme.path`• packages/shared/ReactVersion.js`);
70
- }
71
-
72
- console.log();
73
- console.log(
74
- theme`{header Don't forget to also update and commit the }{path CHANGELOG}`
75
- );
76
-
77
- // Prompt the release engineer to tag the commit and update the CHANGELOG.
78
- // (The script could automatically do this, but this seems safer.)
79
- console.log();
80
- console.log(
81
- theme.header`Tag the source for this release in Git with the following command:`
82
- );
83
- console.log(
84
- theme` {command git tag -a v}{version %s} {command -m "v%s"} {version %s}`,
85
- version,
86
- version,
87
- commit
88
- );
89
- console.log(theme.command` git push origin --tags`);
90
-
91
- console.log();
92
- console.log(theme.header`Lastly, please fill in the release on GitHub.`);
93
- console.log(
94
- theme.link`https://github.com/facebook/react/releases/tag/v%s`,
95
- version
96
- );
97
- console.log(
98
- theme`\nThe GitHub release should also include links to the following artifacts:`
99
- );
100
- for (let i = 0; i < packages.length; i++) {
101
- const packageName = packages[i];
102
- if (existsSync(join(nodeModulesPath, packageName, 'umd'))) {
103
- const {version: packageVersion} = readJsonSync(
104
- join(nodeModulesPath, packageName, 'package.json')
105
- );
106
- console.log(
107
- theme`{path • %s:} {link https://unpkg.com/%s@%s/umd/}`,
108
- packageName,
109
- packageName,
110
- packageVersion
111
- );
112
- }
113
- }
114
-
115
- // Update reactjs.org so the React version shown in the header is up to date.
116
- console.log();
117
- console.log(
118
- theme.header`Once you've pushed changes, update the docs site.`
119
- );
120
- console.log(
121
- 'This will ensure that any newly-added error codes can be decoded.'
122
- );
123
-
124
- console.log();
125
- }
126
- }
127
-};
128
-
129
-module.exports = run;
scripts/release/publish-commands/prompt-for-otp.js
deleted
-23
@@ -1,23 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const prompt = require('prompt-promise');
6
-const theme = require('../theme');
7
-
8
-const run = async () => {
9
- while (true) {
10
- const otp = await prompt('NPM 2-factor auth code: ');
11
- prompt.done();
12
-
13
- if (otp) {
14
- return otp;
15
- } else {
16
- console.log();
17
- console.log(theme.error`Two-factor auth is required to publish.`);
18
- // (Ask again.)
19
- }
20
- }
21
-};
22
-
23
-module.exports = run;
scripts/release/publish-commands/publish-to-npm.js
+32
-77
@@ -3,97 +3,52 @@
3
'use strict';
4
5
const {spawnSync} = require('child_process');
6
-const {exec} = require('child-process-promise');
6
const {readJsonSync} = require('fs-extra');
7
const {join} = require('path');
9
-const {confirm} = require('../utils');
8
const theme = require('../theme');
9
12
-const run = async ({cwd, dry, tags, ci}, packageName, otp) => {
10
+const run = async ({cwd, dry, tag}, packageName) => {
11
const packagePath = join(cwd, 'build/node_modules', packageName);
12
const {version} = readJsonSync(join(packagePath, 'package.json'));
13
14
// Check if this package version has already been published.
15
// If so we might be resuming from a previous run.
18
- // We could infer this by comparing the build-info.json,
19
- // But for now the easiest way is just to ask if this is expected.
20
- const {status} = spawnSync('npm', ['view', `${packageName}@${version}`]);
21
- const packageExists = status === 0;
16
+ const {status: npmViewStatus} = spawnSync('npm', [
17
+ 'view',
18
+ `${packageName}@${version}`,
19
+ ]);
20
+ const packageExists = npmViewStatus === 0;
21
if (packageExists) {
22
console.log(
23
theme`{package ${packageName}} {version ${version}} has already been published.`
24
);
26
- if (!ci) {
27
- await confirm('Is this expected?');
28
- }
29
- } else {
30
- console.log(
31
- theme`{spinnerSuccess ✓} Publishing {package ${packageName}}${dry ? ' (dry-run)' : ''}`
32
- );
33
-
34
- // Publish the package and tag it.
35
- if (!dry) {
36
- if (!ci) {
37
- await exec(`npm publish --tag=${tags[0]} --otp=${otp}`, {
38
- cwd: packagePath,
39
- });
40
- console.log(theme.command(` cd ${packagePath}`));
41
- console.log(
42
- theme.command(` npm publish --tag=${tags[0]} --otp=${otp}`)
43
- );
44
- } else {
45
- await exec(`npm publish --tag=${tags[0]}`, {
46
- cwd: packagePath,
47
- });
48
- console.log(theme.command(` cd ${packagePath}`));
49
- console.log(theme.command(` npm publish --tag=${tags[0]}`));
50
- }
51
- }
52
-
53
- for (let j = 1; j < tags.length; j++) {
54
- if (!dry) {
55
- if (!ci) {
56
- await exec(
57
- `npm dist-tag add ${packageName}@${version} ${tags[j]} --otp=${otp}`,
58
- {cwd: packagePath}
59
- );
60
- console.log(
61
- theme.command(
62
- ` npm dist-tag add ${packageName}@${version} ${tags[j]} --otp=${otp}`
63
- )
64
- );
65
- } else {
66
- await exec(`npm dist-tag add ${packageName}@${version} ${tags[j]}`, {
67
- cwd: packagePath,
68
- });
69
- console.log(
70
- theme.command(
71
- ` npm dist-tag add ${packageName}@${version} ${tags[j]}`
72
- )
73
- );
74
- }
75
- }
76
- }
25
+ return;
26
+ }
27
78
- if (tags.includes('untagged')) {
79
- // npm doesn't let us publish without a tag at all,
80
- // so for one-off publishes we clean it up ourselves.
81
- if (!dry) {
82
- if (!ci) {
83
- await exec(`npm dist-tag rm ${packageName} untagged --otp=${otp}`);
84
- console.log(
85
- theme.command(
86
- ` npm dist-tag rm ${packageName} untagged --otp=${otp}`
87
- )
88
- );
89
- } else {
90
- await exec(`npm dist-tag rm ${packageName} untagged`);
91
- console.log(
92
- theme.command(` npm dist-tag rm ${packageName} untagged`)
93
- );
94
- }
95
- }
96
- }
28
+ console.log(
29
+ `::group::${theme`{spinnerSuccess ✓} Publishing {package ${packageName}}${dry ? ' (--dry-run)' : ''}`}`
30
+ );
31
+ // OIDC trusted publishing authorizes exactly one publish per call, and
32
+ // can't add/remove dist-tags after the fact — so we only ever publish under
33
+ // a single tag, and that tag is the one chosen at workflow dispatch time.
34
+ const args = ['publish', `--tag`, tag];
35
+ if (dry) {
36
+ args.push('--dry-run');
37
+ }
38
+ console.log(theme.command(` cd ${packagePath}`));
39
+ console.log(theme.command(` npm ${args.join(' ')}`));
40
+
41
+ const {error, status: publishStatus} = spawnSync('npm', args, {
42
+ cwd: packagePath,
43
+ stdio: ['ignore', 'inherit', 'inherit'],
44
+ });
45
+ console.log('::endgroup::');
46
+ if (error) {
47
+ throw error;
48
+ } else if (publishStatus !== 0) {
49
+ throw new Error(
50
+ `Failed to publish ${packageName}@${version} with tag ${tag}`
51
+ );
52
}
53
};
54
scripts/release/publish-commands/update-stable-version-numbers.js
deleted
-52
@@ -1,52 +0,0 @@
1
-#!/usr/bin/env node
2
-
3
-'use strict';
4
-
5
-const {readFileSync, writeFileSync} = require('fs');
6
-const {readJson, writeJson} = require('fs-extra');
7
-const {join} = require('path');
8
-
9
-const run = async ({cwd, packages, skipPackages, tags}) => {
10
- if (!tags.includes('latest')) {
11
- // Don't update version numbers for alphas.
12
- return;
13
- }
14
-
15
- const nodeModulesPath = join(cwd, 'build/node_modules');
16
- const packagesPath = join(cwd, 'packages');
17
-
18
- // Update package versions and dependencies (in source) to mirror what was published to NPM.
19
- for (let i = 0; i < packages.length; i++) {
20
- const packageName = packages[i];
21
- const publishedPackageJSON = await readJson(
22
- join(nodeModulesPath, packageName, 'package.json')
23
- );
24
- const sourcePackageJSONPath = join(
25
- packagesPath,
26
- packageName,
27
- 'package.json'
28
- );
29
- const sourcePackageJSON = await readJson(sourcePackageJSONPath);
30
- sourcePackageJSON.version = publishedPackageJSON.version;
31
- sourcePackageJSON.dependencies = publishedPackageJSON.dependencies;
32
- sourcePackageJSON.peerDependencies = publishedPackageJSON.peerDependencies;
33
-
34
- await writeJson(sourcePackageJSONPath, sourcePackageJSON, {spaces: 2});
35
- }
36
-
37
- // Update the shared React version source file.
38
- // (Unless this release does not include an update to React)
39
- if (!skipPackages.includes('react')) {
40
- const sourceReactVersionPath = join(cwd, 'packages/shared/ReactVersion.js');
41
- const {version} = await readJson(
42
- join(nodeModulesPath, 'react', 'package.json')
43
- );
44
- const sourceReactVersion = readFileSync(
45
- sourceReactVersionPath,
46
- 'utf8'
47
- ).replace(/export default '[^']+';/, `export default '${version}';`);
48
- writeFileSync(sourceReactVersionPath, sourceReactVersion);
49
- }
50
-};
51
-
52
-module.exports = run;
scripts/release/publish-commands/validate-tags.js
+13
-23
@@ -6,8 +6,7 @@ const {readJson} = require('fs-extra');
6
const {join} = require('path');
7
const theme = require('../theme');
8
9
-const run = async ({cwd, packages, tags}) => {
10
- // Prevent a "next" release from ever being published as @latest
9
+const run = async ({cwd, packages, tag}) => {
10
// All canaries share a version number, so it's okay to check any of them.
11
const arbitraryPackageName = packages[0];
12
const packageJSONPath = join(
@@ -18,42 +17,33 @@ const run = async ({cwd, packages, tags}) => {
17
'package.json'
18
);
19
const {version} = await readJson(packageJSONPath);
20
+
21
const isExperimentalVersion = version.indexOf('experimental') !== -1;
22
if (version.indexOf('-') !== -1) {
23
- if (tags.includes('latest')) {
24
- if (isExperimentalVersion) {
25
- console.log(
26
- theme`{error Experimental release} {version ${version}} {error cannot be tagged as} {tag latest}`
27
- );
28
- } else {
29
- console.log(
30
- theme`{error Next release} {version ${version}} {error cannot be tagged as} {tag latest}`
31
- );
32
- }
33
- process.exit(1);
34
- }
35
- if (tags.includes('next') && isExperimentalVersion) {
23
+ // Prerelease: canary or experimental.
24
+ if (tag === 'latest' || tag === 'backport') {
25
console.log(
37
- theme`{error Experimental release} {version ${version}} {error cannot be tagged as} {tag next}`
26
+ theme`{error Prerelease} {version ${version}} {error cannot be tagged as} {tag ${tag}}`
27
);
28
process.exit(1);
29
}
41
- if (tags.includes('experimental') && !isExperimentalVersion) {
30
+ if (tag === 'experimental' && !isExperimentalVersion) {
31
console.log(
43
- theme`{error Next release} {version ${version}} {error cannot be tagged as} {tag experimental}`
32
+ theme`{error Canary release} {version ${version}} {error cannot be tagged as} {tag experimental}`
33
);
34
process.exit(1);
35
}
47
- } else {
48
- if (!tags.includes('latest')) {
36
+ if (tag === 'canary' && isExperimentalVersion) {
37
console.log(
50
- theme`{error Stable release} {version ${version}} {error must always be tagged as} {tag latest}`
38
+ theme`{error Experimental release} {version ${version}} {error cannot be tagged as} {tag canary}`
39
);
40
process.exit(1);
41
}
54
- if (tags.includes('experimental')) {
42
+ } else {
43
+ // Semver stable: must publish under @latest or @backport.
44
+ if (tag !== 'latest' && tag !== 'backport') {
45
console.log(
56
- theme`{error Stable release} {version ${version}} {error cannot be tagged as} {tag experimental}`
46
+ theme`{error Stable release} {version ${version}} {error must be tagged as} {tag latest} {error or} {tag backport}`
47
);
48
process.exit(1);
49
}
scripts/release/publish-using-ci-workflow.js
deleted
-148
@@ -1,148 +0,0 @@
1
-'use strict';
2
-
3
-const fetch = require('node-fetch');
4
-
5
-const {logPromise} = require('./utils');
6
-const theme = require('./theme');
7
-
8
-const CIRCLE_TOKEN = process.env.CIRCLE_CI_API_TOKEN;
9
-
10
-if (!CIRCLE_TOKEN) {
11
- console.error(
12
- theme.error(
13
- 'Missing required environment variable: CIRCLE_CI_API_TOKEN\n' +
14
- 'Grab it here: https://app.circleci.com/settings/user/tokens'
15
- )
16
- );
17
- process.exit(1);
18
-}
19
-
20
-function sleep(ms) {
21
- return new Promise(resolve => {
22
- setTimeout(() => resolve(), ms);
23
- });
24
-}
25
-
26
-async function getPublishWorkflowID(pipelineID) {
27
- // Since we just created the pipeline in a POST request, the server may 404.
28
- // Try a few times before giving up.
29
- for (let i = 0; i < 20; i++) {
30
- const pipelineWorkflowsResponse = await fetch(
31
- `https://circleci.com/api/v2/pipeline/${pipelineID}/workflow`
32
- );
33
- if (pipelineWorkflowsResponse.ok) {
34
- const pipelineWorkflowsJSON = await pipelineWorkflowsResponse.json();
35
- const workflows = pipelineWorkflowsJSON.items;
36
- if (workflows.length !== 0) {
37
- return workflows[0].id;
38
- }
39
- }
40
- // CircleCI server may be stale. Wait a sec and try again.
41
- await sleep(1000);
42
- }
43
- return null;
44
-}
45
-
46
-async function pollUntilWorkflowFinishes(workflowID) {
47
- while (true) {
48
- const workflowResponse = await fetch(
49
- `https://circleci.com/api/v2/workflow/${workflowID}`
50
- );
51
- const workflow = await workflowResponse.json();
52
- switch (workflow.status) {
53
- case 'running':
54
- // Workflow still running. Wait a bit then check again.
55
- await sleep(2000);
56
- continue;
57
- case 'success':
58
- // Publish succeeded! Continue.
59
- return;
60
- case 'not_run':
61
- case 'failed':
62
- case 'error':
63
- case 'failing':
64
- case 'on_hold':
65
- case 'canceled':
66
- case 'unauthorized':
67
- default:
68
- console.error(
69
- theme.error(
70
- `Failed to publish. Workflow exited with status: ${workflow.status}`
71
- )
72
- );
73
- console.error(
74
- `Visit https://app.circleci.com/pipelines/workflows/${workflowID} for details.`
75
- );
76
- process.exit(1);
77
- break;
78
- }
79
- }
80
-}
81
-
82
-async function main() {
83
- const headCommitResponse = await fetch(
84
- 'https://api.github.com/repos/facebook/react/commits/main'
85
- );
86
- const headCommitJSON = await headCommitResponse.json();
87
- const headCommitSha = headCommitJSON.sha;
88
-
89
- const pipelineResponse = await fetch(
90
- 'https://circleci.com/api/v2/project/github/facebook/react/pipeline',
91
- {
92
- method: 'post',
93
- body: JSON.stringify({
94
- parameters: {
95
- prerelease_commit_sha: headCommitSha,
96
- },
97
- }),
98
- headers: {
99
- 'Circle-Token': CIRCLE_TOKEN,
100
- 'Content-Type': 'application/json',
101
- },
102
- }
103
- );
104
-
105
- if (!pipelineResponse.ok) {
106
- console.error(
107
- theme.error(
108
- `Failed to access CircleCI. Responded with status: ${pipelineResponse.status}`
109
- )
110
- );
111
- process.exit(1);
112
- }
113
-
114
- const pipelineJSON = await pipelineResponse.json();
115
- const pipelineID = pipelineJSON.id;
116
-
117
- const workflowID = await logPromise(
118
- getPublishWorkflowID(pipelineID),
119
- theme`{header Creating CI workflow}`,
120
- 2 * 1000 // Estimated time: 2 seconds,
121
- );
122
-
123
- if (workflowID === null) {
124
- console.warn(
125
- theme.yellow(
126
- 'Created a CI pipeline to publish the packages, but the script timed ' +
127
- "out when requesting the associated workflow ID. It's still " +
128
- 'possible the workflow was created.\n\n' +
129
- 'Visit ' +
130
- 'https://app.circleci.com/pipelines/github/facebook/react?branch=main ' +
131
- 'for a list of the latest workflows.'
132
- )
133
- );
134
- process.exit(1);
135
- }
136
-
137
- await logPromise(
138
- pollUntilWorkflowFinishes(workflowID),
139
- theme`{header Publishing in CI workflow}: https://app.circleci.com/pipelines/workflows/${workflowID}`,
140
- 2 * 60 * 1000 // Estimated time: 2 minutes,
141
- );
142
-}
143
-
144
-main().catch(error => {
145
- console.error(theme.error('Failed to trigger publish workflow.'));
146
- console.error(error.message);
147
- process.exit(1);
148
-});
scripts/release/publish.js
+19
-51
@@ -3,19 +3,12 @@
3
'use strict';
4
5
const {join} = require('path');
6
-const {readJsonSync} = require('fs-extra');
7
-const clear = require('clear');
6
const {getPublicPackages, handleError} = require('./utils');
7
const theme = require('./theme');
8
11
-const checkNPMPermissions = require('./publish-commands/check-npm-permissions');
12
-const confirmSkippedPackages = require('./publish-commands/confirm-skipped-packages');
9
const confirmVersionAndTags = require('./publish-commands/confirm-version-and-tags');
10
const parseParams = require('./publish-commands/parse-params');
15
-const printFollowUpInstructions = require('./publish-commands/print-follow-up-instructions');
16
-const promptForOTP = require('./publish-commands/prompt-for-otp');
11
const publishToNPM = require('./publish-commands/publish-to-npm');
18
-const updateStableVersionNumbers = require('./publish-commands/update-stable-version-numbers');
12
const validateTags = require('./publish-commands/validate-tags');
13
const validateSkipPackages = require('./publish-commands/validate-skip-packages');
14
@@ -23,10 +16,7 @@ const run = async () => {
16
try {
17
const params = parseParams();
18
26
- const version =
27
- params.publishVersion ??
28
- readJsonSync('./build/node_modules/react/package.json').version;
29
- const isExperimental = version.includes('experimental');
19
+ const isExperimental = params.tag === 'experimental';
20
21
params.cwd = join(__dirname, '..', '..');
22
params.packages = await getPublicPackages(isExperimental);
@@ -59,53 +49,31 @@ const run = async () => {
49
});
50
51
await validateTags(params);
62
- await confirmSkippedPackages(params);
52
await confirmVersionAndTags(params);
53
await validateSkipPackages(params);
65
- await checkNPMPermissions(params);
54
+ // npm ownership / whoami no longer applies — OIDC trusted publishing
55
+ // verifies the publisher via the registry's per-package config, not via a
56
+ // logged-in npm user.
57
58
const packageNames = params.packages;
59
69
- if (params.ci) {
70
- let failed = false;
71
- for (let i = 0; i < packageNames.length; i++) {
72
- try {
73
- const packageName = packageNames[i];
74
- await publishToNPM(params, packageName, null);
75
- } catch (error) {
76
- failed = true;
77
- console.error(error.message);
78
- console.log();
79
- console.log(
80
- theme.error`Publish failed. Will attempt to publish remaining packages.`
81
- );
82
- }
83
- }
84
- if (failed) {
85
- console.log(theme.error`One or more packages failed to publish.`);
86
- process.exit(1);
87
- }
88
- } else {
89
- clear();
90
- let otp = await promptForOTP(params);
91
- for (let i = 0; i < packageNames.length; ) {
60
+ let failed = false;
61
+ for (let i = 0; i < packageNames.length; i++) {
62
+ try {
63
const packageName = packageNames[i];
93
- try {
94
- await publishToNPM(params, packageName, otp);
95
- i++;
96
- } catch (error) {
97
- console.error(error.message);
98
- console.log();
99
- console.log(
100
- theme.error`Publish failed. Enter a fresh otp code to retry.`
101
- );
102
- otp = await promptForOTP(params);
103
- // Try publishing package again
104
- continue;
105
- }
64
+ await publishToNPM(params, packageName, null);
65
+ } catch (error) {
66
+ failed = true;
67
+ console.error(error.message);
68
+ console.log();
69
+ console.log(
70
+ theme.error`Publish failed. Will attempt to publish remaining packages.`
71
+ );
72
}
107
- await updateStableVersionNumbers(params);
108
- await printFollowUpInstructions(params);
73
+ }
74
+ if (failed) {
75
+ console.log(theme.error`One or more packages failed to publish.`);
76
+ process.exit(1);
77
}
78
} catch (error) {
79
handleError(error);
scripts/release/shared-commands/download-build-artifacts.js
+4
-5
@@ -14,8 +14,7 @@ if (process.env.GH_TOKEN == null) {
14
process.exit(1);
15
}
16
17
-const OWNER = 'facebook';
18
-const REPO = 'react';
17
+const REPO = process.env.GITHUB_REPOSITORY || 'facebook/react';
18
const WORKFLOW_ID = 'runtime_build_and_test.yml';
19
const GITHUB_HEADERS = `
20
-H "Accept: application/vnd.github+json" \
@@ -49,7 +48,7 @@ function getWorkflowId() {
48
49
async function getWorkflowRun(commit) {
50
const res = await exec(
52
- `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${OWNER}/${REPO}/actions/workflows/${getWorkflowId()}/runs?head_sha=${commit}`
51
+ `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${REPO}/actions/workflows/${getWorkflowId()}/runs?head_sha=${commit}`
52
);
53
54
const json = JSON.parse(res.stdout);
@@ -67,7 +66,7 @@ async function getWorkflowRun(commit) {
66
67
async function getArtifact(workflowRunId, artifactName) {
68
const res = await exec(
70
- `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${OWNER}/${REPO}/actions/runs/${workflowRunId}/artifacts?per_page=100&name=${artifactName}`
69
+ `curl -L ${GITHUB_HEADERS} https://api.github.com/repos/${REPO}/actions/runs/${workflowRunId}/artifacts?per_page=100&name=${artifactName}`
70
);
71
72
const json = JSON.parse(res.stdout);
@@ -103,7 +102,7 @@ async function processArtifact(artifact, opts) {
102
// Use https://cli.github.com/manual/gh_attestation_verify to verify artifact
103
if (executableIsAvailable('gh')) {
104
await exec(
106
- `gh attestation verify artifacts_combined.zip --repo=${OWNER}/${REPO}`,
105
+ `gh attestation verify artifacts_combined.zip --repo=${REPO}`,
106
{
107
cwd: tmpDir,
108
}