release.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. const fs = require("fs");
  2. const path = require("path");
  3. const { execSync } = require("child_process");
  4. const updateChangelog = require("./updateChangelog");
  5. // skipping utils for now, as it has independent release process
  6. const PACKAGES = ["common", "math", "element", "excalidraw"];
  7. const PACKAGES_DIR = path.resolve(__dirname, "../packages");
  8. /**
  9. * Returns the arguments for the release script.
  10. *
  11. * Usage examples:
  12. * - yarn release --help -> prints this help message
  13. * - yarn release -> publishes `@excalidraw` packages with "test" tag and "-[hash]" version suffix
  14. * - yarn release --tag=test -> same as above
  15. * - yarn release --tag=next -> publishes `@excalidraw` packages with "next" tag and version "-[hash]" suffix
  16. * - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
  17. * - yarn release --tag=latest --version=0.19.0 -> publishes `@excalidraw` packages with "latest" tag and version "0.19.0" & prepares changelog for the release
  18. *
  19. * @returns [tag, version, nonInteractive]
  20. */
  21. const getArguments = () => {
  22. let tag = "test";
  23. let version = "";
  24. let nonInteractive = false;
  25. for (const argument of process.argv.slice(2)) {
  26. if (/--help/.test(argument)) {
  27. console.info(`Available arguments:
  28. --tag=<tag> -> (optional) "test" (default), "next" for auto release, "latest" for stable release
  29. --version=<version> -> (optional) for "next" and "test", (required) for "latest" i.e. "0.19.0"
  30. --non-interactive -> (optional) disables interactive prompts`);
  31. console.info(`\nUsage examples:
  32. - yarn release -> publishes \`@excalidraw\` packages with "test" tag and "-[hash]" version suffix
  33. - yarn release --tag=test -> same as above
  34. - yarn release --tag=next -> publishes \`@excalidraw\` packages with "next" tag and version "-[hash]" suffix
  35. - yarn release --tag=next --non-interactive -> skips interactive prompts (runs on CI/CD), otherwise same as above
  36. - yarn release --tag=latest --version=0.19.0 -> publishes \`@excalidraw\` packages with "latest" tag and version "0.19.0" & prepares changelog for the release`);
  37. process.exit(0);
  38. }
  39. if (/--tag=/.test(argument)) {
  40. tag = argument.split("=")[1];
  41. }
  42. if (/--version=/.test(argument)) {
  43. version = argument.split("=")[1];
  44. }
  45. if (/--non-interactive/.test(argument)) {
  46. nonInteractive = true;
  47. }
  48. }
  49. if (tag !== "latest" && tag !== "next" && tag !== "test") {
  50. console.error(`Unsupported tag "${tag}", use "latest", "next" or "test".`);
  51. process.exit(1);
  52. }
  53. if (tag === "latest" && !version) {
  54. console.error("Pass the version to make the latest stable release!");
  55. process.exit(1);
  56. }
  57. if (!version) {
  58. // set the next version based on the excalidraw package version + commit hash
  59. const excalidrawPackageVersion = require(getPackageJsonPath(
  60. "excalidraw",
  61. )).version;
  62. const hash = getShortCommitHash();
  63. if (!excalidrawPackageVersion.includes(hash)) {
  64. version = `${excalidrawPackageVersion}-${hash}`;
  65. } else {
  66. // ensuring idempotency
  67. version = excalidrawPackageVersion;
  68. }
  69. }
  70. console.info(`Running with tag "${tag}" and version "${version}"...`);
  71. return [tag, version, nonInteractive];
  72. };
  73. const validatePackageName = (packageName) => {
  74. if (!PACKAGES.includes(packageName)) {
  75. console.error(`Package "${packageName}" not found!`);
  76. process.exit(1);
  77. }
  78. };
  79. const getPackageJsonPath = (packageName) => {
  80. validatePackageName(packageName);
  81. return path.resolve(PACKAGES_DIR, packageName, "package.json");
  82. };
  83. const updatePackageJsons = (nextVersion) => {
  84. const packageJsons = new Map();
  85. for (const packageName of PACKAGES) {
  86. const pkg = require(getPackageJsonPath(packageName));
  87. pkg.version = nextVersion;
  88. if (pkg.dependencies) {
  89. for (const dependencyName of PACKAGES) {
  90. if (!pkg.dependencies[`@excalidraw/${dependencyName}`]) {
  91. continue;
  92. }
  93. pkg.dependencies[`@excalidraw/${dependencyName}`] = nextVersion;
  94. }
  95. }
  96. packageJsons.set(packageName, `${JSON.stringify(pkg, null, 2)}\n`);
  97. }
  98. // modify once, to avoid inconsistent state
  99. for (const packageName of PACKAGES) {
  100. const content = packageJsons.get(packageName);
  101. fs.writeFileSync(getPackageJsonPath(packageName), content, "utf-8");
  102. }
  103. };
  104. const getShortCommitHash = () => {
  105. return execSync("git rev-parse --short HEAD").toString().trim();
  106. };
  107. const askToCommit = (tag, nextVersion) => {
  108. if (tag !== "latest") {
  109. return Promise.resolve();
  110. }
  111. return new Promise((resolve) => {
  112. const rl = require("readline").createInterface({
  113. input: process.stdin,
  114. output: process.stdout,
  115. });
  116. rl.question(
  117. "Do you want to commit these changes to git? (Y/n): ",
  118. (answer) => {
  119. rl.close();
  120. if (answer.toLowerCase() === "y") {
  121. execSync(`git add -u`);
  122. execSync(
  123. `git commit -m "chore: release @excalidraw/excalidraw@${nextVersion} 🎉"`,
  124. );
  125. } else {
  126. console.warn(
  127. "Skipping commit. Don't forget to commit manually later!",
  128. );
  129. }
  130. resolve();
  131. },
  132. );
  133. });
  134. };
  135. const buildPackages = () => {
  136. console.info("Running yarn install...");
  137. execSync(`yarn --frozen-lockfile`, { stdio: "inherit" });
  138. console.info("Removing existing build artifacts...");
  139. execSync(`yarn rm:build`, { stdio: "inherit" });
  140. for (const packageName of PACKAGES) {
  141. console.info(`Building "@excalidraw/${packageName}"...`);
  142. execSync(`yarn run build:esm`, {
  143. cwd: path.resolve(PACKAGES_DIR, packageName),
  144. stdio: "inherit",
  145. });
  146. }
  147. };
  148. const askToPublish = (tag, version) => {
  149. return new Promise((resolve) => {
  150. const rl = require("readline").createInterface({
  151. input: process.stdin,
  152. output: process.stdout,
  153. });
  154. rl.question(
  155. "Do you want to publish these changes to npm? (Y/n): ",
  156. (answer) => {
  157. rl.close();
  158. if (answer.toLowerCase() === "y") {
  159. publishPackages(tag, version);
  160. } else {
  161. console.info("Skipping publish.");
  162. }
  163. resolve();
  164. },
  165. );
  166. });
  167. };
  168. const publishPackages = (tag, version) => {
  169. for (const packageName of PACKAGES) {
  170. execSync(`yarn publish --tag ${tag}`, {
  171. cwd: path.resolve(PACKAGES_DIR, packageName),
  172. stdio: "inherit",
  173. });
  174. console.info(
  175. `Published "@excalidraw/${packageName}@${tag}" with version "${version}"! 🎉`,
  176. );
  177. }
  178. };
  179. /** main */
  180. (async () => {
  181. const [tag, version, nonInteractive] = getArguments();
  182. buildPackages();
  183. if (tag === "latest") {
  184. await updateChangelog(version);
  185. }
  186. updatePackageJsons(version);
  187. if (nonInteractive) {
  188. publishPackages(tag, version);
  189. } else {
  190. await askToCommit(tag, version);
  191. await askToPublish(tag, version);
  192. }
  193. })();