1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @format
8 */
9
10'use strict';
11
12/**
13 * This script prepares a release version of react-native and may publish to NPM.
14 * It is supposed to run in CI environment, not on a developer's machine.
15 *
16 * For a dry run (commitly), this script will:
17 *  * Version the commitly of the form `1000.0.0-<commitSha>`
18 *  * Create Android artifacts
19 *  * It will not publish to npm
20 *
21 * For a nightly run, this script will:
22 *  * Version the nightly release of the form `0.0.0-<dateIdentifier>-<commitSha>`
23 *  * Create Android artifacts
24 *  * Publish to npm using `nightly` tag
25 *
26 * For a release run, this script will:
27 *  * Version the release by the tag version that triggered CI
28 *  * Create Android artifacts
29 *  * Publish to npm
30 *     * using `latest` tag if commit is currently tagged `latest`
31 *     * or otherwise `{major}.{minor}-stable`
32 */
33
34const {exec, echo, exit, test, env} = require('shelljs');
35const {parseVersion, isTaggedLatest} = require('./version-utils');
36const fs = require('fs');
37const os = require('os');
38const path = require('path');
39const yargs = require('yargs');
40
41const buildTag = process.env.CIRCLE_TAG;
42const otp = process.env.NPM_CONFIG_OTP;
43
44const argv = yargs
45  .option('n', {
46    alias: 'nightly',
47    type: 'boolean',
48    default: false,
49  })
50  .option('d', {
51    alias: 'dry-run',
52    type: 'boolean',
53    default: false,
54  })
55  .option('h', {
56    alias: 'include-hermes',
57    type: 'boolean',
58    default: false,
59  }).argv;
60const nightlyBuild = argv.nightly;
61const dryRunBuild = argv.dryRun;
62const includeHermes = argv.includeHermes;
63const isCommitly = nightlyBuild || dryRunBuild;
64
65if (includeHermes) {
66  const HERMES_INSTALL_LOCATION = 'sdks';
67  const HERMES_SOURCE_DEST_PATH = `${HERMES_INSTALL_LOCATION}/hermes`;
68
69  let hermesReleaseTag;
70  let hermesReleaseURI;
71  if (isCommitly) {
72    // use latest commit / tarball
73    hermesReleaseURI = 'https://github.com/facebook/hermes/tarball/main';
74  } else {
75    // use one configured in disk
76    fs.readFile(
77      `${HERMES_INSTALL_LOCATION}/.hermesversion`,
78      {
79        encoding: 'utf8',
80        flag: 'r',
81      },
82      function (err, data) {
83        if (err) {
84          echo('Failed to read current Hermes release tag.');
85          // TODO: We'll need to make sure every release going forward has one of these.
86          exit(1);
87        } else {
88          hermesReleaseTag = data.trim();
89          hermesReleaseURI = `https://github.com/facebook/hermes/archive/refs/tags/${hermesReleaseTag}.tar.gz`;
90        }
91      },
92    );
93  }
94
95  const tmpDownloadDir = fs.mkdtempSync(
96    path.join(os.tmpdir(), 'hermes-tarball'),
97  );
98  const tmpExtractDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes'));
99
100  const hermesInstallScript = `
101    mkdir -p ${HERMES_SOURCE_DEST_PATH} && \
102    wget ${hermesReleaseURI} -O ${tmpDownloadDir}/hermes.tar.gz && \
103    tar -xzf ${tmpDownloadDir}/hermes.tar.gz -C ${tmpExtractDir} && \
104    HERMES_SOURCE_EXTRACT_PATH=$(ls -d ${tmpExtractDir}/*) && \
105    mv $HERMES_SOURCE_EXTRACT_PATH ${HERMES_SOURCE_DEST_PATH}
106  `;
107
108  if (fs.existsSync(`${HERMES_SOURCE_DEST_PATH}`)) {
109    if (exec(`rm -rf ./${HERMES_SOURCE_DEST_PATH}`).code) {
110      echo('Failed to clean up previous Hermes installation.');
111      exit(1);
112    }
113  }
114  if (exec(hermesInstallScript).code) {
115    echo('Failed to include Hermes in release.');
116    exit(1);
117  }
118}
119
120// 34c034298dc9cad5a4553964a5a324450fda0385
121const currentCommit = exec('git rev-parse HEAD', {
122  silent: true,
123}).stdout.trim();
124const shortCommit = currentCommit.slice(0, 9);
125
126const rawVersion =
127  // 0.0.0 triggers issues with cocoapods for codegen when building template project.
128  dryRunBuild
129    ? '1000.0.0'
130    : // For nightly we continue to use 0.0.0 for clarity for npm
131    nightlyBuild
132    ? '0.0.0'
133    : // For pre-release and stable releases, we use the git tag of the version we're releasing (set in bump-oss-version)
134      buildTag;
135
136let version,
137  major,
138  minor,
139  prerelease = null;
140try {
141  ({version, major, minor, prerelease} = parseVersion(rawVersion));
142} catch (e) {
143  echo(e.message);
144  exit(1);
145}
146
147let releaseVersion;
148if (dryRunBuild) {
149  releaseVersion = `${version}-${shortCommit}`;
150} else if (nightlyBuild) {
151  // 2021-09-28T05:38:40.669Z -> 20210928-0538
152  const dateIdentifier = new Date()
153    .toISOString()
154    .slice(0, -8)
155    .replace(/[-:]/g, '')
156    .replace(/[T]/g, '-');
157  releaseVersion = `${version}-${dateIdentifier}-${shortCommit}`;
158} else {
159  releaseVersion = version;
160}
161
162// Bump version number in various files (package.json, gradle.properties etc)
163// For stable, pre-release releases, we rely on CircleCI job `prepare_package_for_release` to handle this
164if (isCommitly) {
165  if (
166    exec(`node scripts/set-rn-version.js --to-version ${releaseVersion}`).code
167  ) {
168    echo(`Failed to set version number to ${releaseVersion}`);
169    exit(1);
170  }
171}
172
173// -------- Generating Android Artifacts
174if (exec('./gradlew :ReactAndroid:installArchives').code) {
175  echo('Could not generate artifacts');
176  exit(1);
177}
178
179// -------- Generating the Hermes Engine Artifacts
180env.REACT_NATIVE_HERMES_SKIP_PREFAB = true;
181if (exec('./gradlew :ReactAndroid:hermes-engine:installArchives').code) {
182  echo('Could not generate artifacts');
183  exit(1);
184}
185
186// undo uncommenting javadoc setting
187exec('git checkout ReactAndroid/gradle.properties');
188
189echo('Generated artifacts for Maven');
190
191let artifacts = [
192  '.module',
193  '.pom',
194  '-debug.aar',
195  '-release.aar',
196  '-debug-sources.jar',
197  '-release-sources.jar',
198].map(suffix => {
199  return `react-native-${releaseVersion}${suffix}`;
200});
201
202artifacts.forEach(name => {
203  if (
204    !test(
205      '-e',
206      `./android/com/facebook/react/react-native/${releaseVersion}/${name}`,
207    )
208  ) {
209    echo(`Failing as expected file: ${name} was not correctly generated.`);
210    exit(1);
211  }
212});
213
214if (dryRunBuild) {
215  echo('Skipping `npm publish` because --dry-run is set.');
216  exit(0);
217}
218
219// Running to see if this commit has been git tagged as `latest`
220const isLatest = isTaggedLatest(currentCommit);
221
222const releaseBranch = `${major}.${minor}-stable`;
223
224// Set the right tag for nightly and prerelease builds
225// If a release is not git-tagged as `latest` we use `releaseBranch` to prevent
226// npm from overriding the current `latest` version tag, which it will do if no tag is set.
227const tagFlag = nightlyBuild
228  ? '--tag nightly'
229  : prerelease != null
230  ? '--tag next'
231  : isLatest
232  ? '--tag latest'
233  : `--tag ${releaseBranch}`;
234
235// use otp from envvars if available
236const otpFlag = otp ? `--otp ${otp}` : '';
237
238if (exec(`npm publish ${tagFlag} ${otpFlag}`).code) {
239  echo('Failed to publish package to npm');
240  exit(1);
241} else {
242  echo(`Published to npm ${releaseVersion}`);
243  exit(0);
244}
245