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
10const VERSION_REGEX = /^v?((\d+)\.(\d+)\.(\d+)(?:-(.+))?)$/;
11
12/**
13 * Parses a version string and performs some checks to verify its validity.
14 * A valid version is in the format vX.Y.Z[-KKK] where X, Y, Z are numbers and KKK can be something else.
15 * The `builtType` is used to enforce that the major version can assume only specific
16 * values.
17 *
18 * Some examples of valid versions are:
19 * - stable: 0.68.1
20 * - stable prerelease: 0.70.0-rc.0
21 * - e2e-test: X.Y.Z-20221116-2018
22 * - nightly: 0.0.0-20221116-2018-0bc4547fc
23 * - dryrun: 1000.0.0
24 *
25 * Parameters:
26 * - @versionStr the string representing a version
27 * - @buildType the build type. It can be of values: `dry-run`, `release`, `nightly`
28 *
29 * Returns: an object with the shape:
30 * ```
31 * {
32 *   version: string,
33 *   major: number,
34 *   minor: number,
35 *   patch: number,
36 *   prerelease: string
37 * }
38 * ```
39 *
40 */
41function parseVersion(versionStr, buildType) {
42  try {
43    validateBuildType(buildType);
44  } catch (e) {
45    throw e;
46  }
47
48  const match = extractMatchIfValid(versionStr);
49  const [, version, major, minor, patch, prerelease] = match;
50
51  const versionObject = {
52    version,
53    major,
54    minor,
55    patch,
56    prerelease,
57  };
58
59  try {
60    validateVersion(versionObject, buildType);
61  } catch (e) {
62    throw e;
63  }
64
65  return versionObject;
66}
67
68function validateBuildType(buildType) {
69  const validBuildTypes = new Set(['release', 'dry-run', 'nightly']);
70  if (!validBuildTypes.has(buildType)) {
71    throw new Error(`Unsupported build type: ${buildType}`);
72  }
73}
74
75function extractMatchIfValid(versionStr) {
76  const match = versionStr.match(VERSION_REGEX);
77  if (!match) {
78    throw new Error(
79      `You must pass a correctly formatted version; couldn't parse ${versionStr}`,
80    );
81  }
82  return match;
83}
84
85function validateVersion(versionObject, buildType) {
86  const map = {
87    release: validateRelease,
88    'dry-run': validateDryRun,
89    nightly: validateNightly,
90  };
91
92  const validationFunction = map[buildType];
93  validationFunction(versionObject);
94}
95
96/**
97 * Releases are in the form of 0.Y.Z[-RC.0]
98 */
99function validateRelease(version) {
100  const validRelease = isStableRelease(version) || isStablePrerelease(version);
101  if (!validRelease) {
102    throw new Error(`Version ${version.version} is not valid for Release`);
103  }
104}
105
106function validateDryRun(version) {
107  const isNightly = isNightlyBuild(version) && version.prerelease != null;
108
109  if (
110    !isMain(version) &&
111    !isNightly &&
112    !isStableRelease(version) &&
113    !isStablePrerelease(version)
114  ) {
115    throw new Error(`Version ${version.version} is not valid for dry-runs`);
116  }
117}
118
119function validateNightly(version) {
120  // a valid nightly is a prerelease
121  const isPrerelease = version.prerelease != null;
122  const isValidNightly = isNightlyBuild(version) && isPrerelease;
123  if (!isValidNightly) {
124    throw new Error(`Version ${version.version} is not valid for nightlies`);
125  }
126}
127
128function isStableRelease(version) {
129  return (
130    version.major === '0' && version.minor !== '0' && version.prerelease == null
131  );
132}
133
134function isStablePrerelease(version) {
135  return (
136    version.major === '0' &&
137    version.minor !== '0' &&
138    version.patch.match(/^\d+$/) &&
139    version.prerelease != null &&
140    (version.prerelease.startsWith('rc.') ||
141      version.prerelease.startsWith('rc-') ||
142      version.prerelease.match(/^(\d{8})-(\d{4})$/))
143  );
144}
145
146function isNightlyBuild(version) {
147  return (
148    version.major === '0' && version.minor === '0' && version.patch === '0'
149  );
150}
151
152function isMain(version) {
153  return (
154    version.major === '1000' && version.minor === '0' && version.patch === '0'
155  );
156}
157
158function isReleaseBranch(branch) {
159  return branch.endsWith('-stable');
160}
161
162module.exports = {
163  validateBuildType,
164  parseVersion,
165  isReleaseBranch,
166  isMain,
167  isStableRelease,
168  isStablePrerelease,
169};
170