diff --git a/.github/workflow-scripts/__tests__/createDraftRelease-test.js b/.github/workflow-scripts/__tests__/createDraftRelease-test.js new file mode 100644 index 00000000000000..77901d4df0999e --- /dev/null +++ b/.github/workflow-scripts/__tests__/createDraftRelease-test.js @@ -0,0 +1,305 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const { + _verifyTagExists, + _extractChangelog, + _computeBody, + _createDraftReleaseOnGitHub, +} = require('../createDraftRelease'); + +const fs = require('fs'); + +const silence = () => {}; +const mockFetch = jest.fn(); + +jest.mock('../utils.js', () => ({ + log: silence, +})); + +global.fetch = mockFetch; + +describe('Create Draft Release', () => { + beforeEach(jest.clearAllMocks); + + describe('#_verifyTagExists', () => { + it('throws if the tag does not exists', async () => { + const token = 'token'; + mockFetch.mockReturnValueOnce(Promise.resolve({status: 404})); + + await expect(_verifyTagExists('0.77.1')).rejects.toThrowError( + `Tag v0.77.1 does not exist`, + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + ); + }); + }); + + describe('#_extractChangelog', () => { + it(`extracts changelog from CHANGELOG.md`, async () => { + const mockedReturnValue = `# Changelog + +## v0.77.2 + +- [PR #1234](https://github.com/facebook/react-native/pull/1234) - Some change +- [PR #5678](https://github.com/facebook/react-native/pull/5678) - Some other change + + +## v0.77.1 +### Breaking Changes +- [PR #9012](https://github.com/facebook/react-native/pull/9012) - Some other change + +#### Android +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change +- [PR #3457](https://github.com/facebook/react-native/pull/3457) - Some other change + +#### iOS +- [PR #3436](https://github.com/facebook/react-native/pull/3436) - Some other change +- [PR #3437](https://github.com/facebook/react-native/pull/3437) - Some other change + +### Fixed +- [PR #9012](https://github.com/facebook/react-native/pull/9012) - Some other change + +#### Android +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change + +#### iOS +- [PR #3437](https://github.com/facebook/react-native/pull/3437) - Some other change + + +## v0.77.0 + +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change + +## v0.76.0 + +- [PR #7890](https://github.com/facebook/react-native/pull/7890) - Some other change`; + + jest.spyOn(fs, 'readFileSync').mockImplementationOnce(func => { + return mockedReturnValue; + }); + const changelog = _extractChangelog('0.77.1'); + expect(changelog).toEqual(`## v0.77.1 +### Breaking Changes +- [PR #9012](https://github.com/facebook/react-native/pull/9012) - Some other change + +#### Android +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change +- [PR #3457](https://github.com/facebook/react-native/pull/3457) - Some other change + +#### iOS +- [PR #3436](https://github.com/facebook/react-native/pull/3436) - Some other change +- [PR #3437](https://github.com/facebook/react-native/pull/3437) - Some other change + +### Fixed +- [PR #9012](https://github.com/facebook/react-native/pull/9012) - Some other change + +#### Android +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change + +#### iOS +- [PR #3437](https://github.com/facebook/react-native/pull/3437) - Some other change`); + }); + + it('does not extract changelog for rc.0', async () => { + const changelog = _extractChangelog('0.77.0-rc.0'); + expect(changelog).toEqual(''); + }); + + it('does not extract changelog for 0.X.0', async () => { + const changelog = _extractChangelog('0.77.0'); + expect(changelog).toEqual(''); + }); + }); + + describe('#_computeBody', () => { + it('computes body for release', async () => { + const version = '0.77.1'; + const changelog = `## v${version} +### Breaking Changes +- [PR #9012](https://github.com/facebook/react-native/pull/9012) - Some other change + +#### Android +- [PR #3456](https://github.com/facebook/react-native/pull/3456) - Some other change +- [PR #3457](https://github.com/facebook/react-native/pull/3457) - Some other change + +#### iOS +- [PR #3436](https://github.com/facebook/react-native/pull/3436) - Some other change +- [PR #3437](https://github.com/facebook/react-native/pull/3437) - Some other change`; + const body = _computeBody(version, changelog); + + expect(body).toEqual(`${changelog} + +--- + +Hermes dSYMS: +- [Debug](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-framework-dSYM-debug.tar.gz) +- [Release](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-framework-dSYM-release.tar.gz) + +ReactNativeDependencies dSYMs: +- [Debug](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-reactnative-dependencies-dSYM-debug.tar.gz) +- [Release](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-reactnative-dependencies-dSYM-release.tar.gz) + +--- + +You can file issues or pick requests against this release [here](https://github.com/reactwg/react-native-releases/issues/new/choose). + +--- + +To help you upgrade to this version, you can use the [Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) ⚛️. + +--- + +View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/react-native/blob/main/CHANGELOG.md).`); + }); + }); + + describe('#_createDraftReleaseOnGitHub', () => { + it('creates a draft release on GitHub', async () => { + const version = '0.77.1'; + const url = 'https://api.github.com/repos/facebook/react-native/releases'; + const token = 'token'; + const headers = { + Accept: 'Accept: application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + Authorization: `Bearer ${token}`, + }; + const body = `Draft release body`; + const latest = true; + const fetchBody = JSON.stringify({ + tag_name: `v${version}`, + name: `${version}`, + body: body, + draft: true, + prerelease: false, + make_latest: `${latest}`, + }); + + mockFetch.mockReturnValueOnce( + Promise.resolve({ + status: 201, + json: () => + Promise.resolve({ + html_url: + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + }), + }), + ); + const response = await _createDraftReleaseOnGitHub( + version, + body, + latest, + token, + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + `https://api.github.com/repos/facebook/react-native/releases`, + { + method: 'POST', + headers: headers, + body: fetchBody, + }, + ); + expect(response).toEqual( + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + ); + }); + + it('creates a draft release for prerelease on GitHub', async () => { + const version = '0.77.0-rc.2'; + const url = 'https://api.github.com/repos/facebook/react-native/releases'; + const token = 'token'; + const headers = { + Accept: 'Accept: application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + Authorization: `Bearer ${token}`, + }; + const body = `Draft release body`; + const latest = true; + const fetchBody = JSON.stringify({ + tag_name: `v${version}`, + name: `${version}`, + body: body, + draft: true, + prerelease: true, + make_latest: `${latest}`, + }); + + mockFetch.mockReturnValueOnce( + Promise.resolve({ + status: 201, + json: () => + Promise.resolve({ + html_url: + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + }), + }), + ); + const response = await _createDraftReleaseOnGitHub( + version, + body, + latest, + token, + ); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + `https://api.github.com/repos/facebook/react-native/releases`, + { + method: 'POST', + headers: headers, + body: fetchBody, + }, + ); + expect(response).toEqual( + 'https://github.com/facebook/react-native/releases/tag/v0.77.1', + ); + }); + + it('throws if the post failes', async () => { + const version = '0.77.0-rc.2'; + const url = 'https://api.github.com/repos/facebook/react-native/releases'; + const token = 'token'; + const headers = { + Accept: 'Accept: application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + Authorization: `Bearer ${token}`, + }; + const body = `Draft release body`; + const latest = true; + const fetchBody = JSON.stringify({ + tag_name: `v${version}`, + name: `${version}`, + body: body, + draft: true, + prerelease: true, + make_latest: `${latest}`, + }); + + mockFetch.mockReturnValueOnce( + Promise.resolve({ + status: 401, + }), + ); + await expect( + _createDraftReleaseOnGitHub(version, body, latest, token), + ).rejects.toThrowError(); + expect(mockFetch).toHaveBeenCalledTimes(1); + expect(mockFetch).toHaveBeenCalledWith( + `https://api.github.com/repos/facebook/react-native/releases`, + { + method: 'POST', + headers: headers, + body: fetchBody, + }, + ); + }); + }); +}); diff --git a/.github/workflow-scripts/createDraftRelease.js b/.github/workflow-scripts/createDraftRelease.js new file mode 100644 index 00000000000000..f8737c3cbfda4c --- /dev/null +++ b/.github/workflow-scripts/createDraftRelease.js @@ -0,0 +1,137 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const {log, run} = require('./utils'); +const fs = require('fs'); + +function _headers(token) { + return { + Accept: 'Accept: application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + Authorization: `Bearer ${token}`, + }; +} + +function _extractChangelog(version) { + if (version.endsWith('.0')) { + // for RC.0 and for the release of a new stable minor, the changelog is too long + // to be added in a release. The release body is usually something shorter. + // See for example the release for 0.76.0 or 0.77.0: + // 0.76: https://github.com/facebook/react-native/releases/tag/v0.76.0 + // 0.77: https://github.com/facebook/react-native/releases/tag/v0.77.0 + return ''; + } + const changelog = String(fs.readFileSync('CHANGELOG.md', 'utf8')).split('\n'); + const changelogStarts = changelog.indexOf(`## v${version}`); + let changelogEnds = changelogStarts; + // Scan the changelog to find the next version + for (var line = changelogStarts + 1; line < changelog.length; line++) { + if (changelog[line].startsWith('## ')) { + changelogEnds = line; + break; + } + } + return changelog.slice(changelogStarts, changelogEnds).join('\n').trim(); +} + +function _computeBody(version, changelog) { + return `${changelog} + +--- + +Hermes dSYMS: +- [Debug](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-framework-dSYM-debug.tar.gz) +- [Release](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-hermes-framework-dSYM-release.tar.gz) + +ReactNativeDependencies dSYMs: +- [Debug](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-reactnative-dependencies-dSYM-debug.tar.gz) +- [Release](https://repo1.maven.org/maven2/com/facebook/react/react-native-artifacts/${version}/react-native-artifacts-${version}-reactnative-dependencies-dSYM-release.tar.gz) + +--- + +You can file issues or pick requests against this release [here](https://github.com/reactwg/react-native-releases/issues/new/choose). + +--- + +To help you upgrade to this version, you can use the [Upgrade Helper](https://react-native-community.github.io/upgrade-helper/) ⚛️. + +--- + +View the whole changelog in the [CHANGELOG.md file](https://github.com/facebook/react-native/blob/main/CHANGELOG.md).`; +} + +async function _verifyTagExists(version) { + const url = `https://github.com/facebook/react-native/releases/tag/v${version}`; + + const response = await fetch(url); + if (response.status === 404) { + throw new Error(`Tag v${version} does not exist`); + } +} + +async function _createDraftReleaseOnGitHub(version, body, latest, token) { + const url = 'https://api.github.com/repos/facebook/react-native/releases'; + const method = 'POST'; + const headers = _headers(token); + const fetchBody = JSON.stringify({ + tag_name: `v${version}`, + name: `${version}`, + body: body, + draft: true, // NEVER CHANGE this value to false. If false, it will publish the release, and send a GH notification to all the subscribers. + prerelease: version.includes('-rc.') ? true : false, + make_latest: `${latest}`, + }); + + const response = await fetch(url, { + method, + headers, + body: fetchBody, + }); + + if (response.status !== 201) { + throw new Error( + `Failed to create the release: ${response.status} ${response.statusText}`, + ); + } + + const data = await response.json(); + return data.html_url; +} + +function moveToChangelogBranch(version) { + log(`Moving to changelog branch: changelog/v${version}`); + run(`git checkout -b changelog/v${version}`); +} + +async function createDraftRelease(version, latest, token) { + if (version.startsWith('v')) { + version = version.substring(1); + } + + _verifyTagExists(version); + moveToChangelogBranch(version); + const changelog = _extractChangelog(version); + const body = _computeBody(version, changelog); + const release = await _createDraftReleaseOnGitHub( + version, + body, + latest, + token, + ); + log(`Created draft release: ${release}`); +} + +module.exports = { + createDraftRelease, + // Exported for testing purposes + _verifyTagExists, + _extractChangelog, + _computeBody, + _createDraftReleaseOnGitHub, +}; diff --git a/.github/workflows/create-draft-release.yml b/.github/workflows/create-draft-release.yml new file mode 100644 index 00000000000000..dd17c5bc1c3fb8 --- /dev/null +++ b/.github/workflows/create-draft-release.yml @@ -0,0 +1,29 @@ +name: Create Draft Release + +on: + workflow_call: + +jobs: + create-draft-release: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Configure Git + shell: bash + run: | + git config --local user.email "bot@reactnative.dev" + git config --local user.name "React Native Bot" + - name: Create draft release + uses: actions/github-script@v6 + with: + script: | + const {createDraftRelease} = require('./.github/workflow-scripts/createDraftRelease.js'); + const version = ${{ github.ref_name }}'; + const {isLatest} = require('./.github/workflow-scripts/publishTemplate.js'); + await createDraftRelease(version, isLatest(), '${{secrets.REACT_NATIVE_BOT_GITHUB_TOKEN}}'); diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index c6724c8253081e..be8c89f779c9b4 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -243,7 +243,12 @@ jobs: uses: ./.github/workflows/generate-changelog.yml secrets: inherit - bump-podfile-lock: + bump_podfile_lock: needs: build_npm_package uses: ./.github/workflows/bump-podfile-lock.yml secrets: inherit + + create_draft_release: + needs: generate_changelog + uses: ./.github/workflows/create-draft-release.yml + secrets: inherit