release.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. #!/usr/bin/env node
  2. const exec = require("node:child_process").execSync;
  3. const fs = require("node:fs");
  4. const path = require("node:path");
  5. const crypto = require("node:crypto");
  6. const { parseArgs } = require("node:util");
  7. const args = parseArgs({
  8. options: {
  9. version: { type: "string" },
  10. "dry-run": { type: "boolean", default: false },
  11. },
  12. });
  13. const dryRun = args.values["dry-run"];
  14. if (dryRun) {
  15. console.log('Running in "dry-run" mode');
  16. }
  17. const exitWithError = (message) => {
  18. console.error(`Exit with error: ${message}`);
  19. process.exit(1);
  20. };
  21. if (!process.env.CI) {
  22. exitWithError("The script should only be run in CI");
  23. }
  24. exec('echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc');
  25. exec('git config --global user.name "Zihua Li"');
  26. exec('git config --global user.email "635902+luin@users.noreply.github.com"');
  27. /*
  28. * Check that the git working directory is clean
  29. */
  30. if (exec("git status --porcelain").length) {
  31. exitWithError(
  32. "Make sure the git working directory is clean before releasing"
  33. );
  34. }
  35. /*
  36. * Check that the version is valid. Also extract the dist-tag from the version.
  37. */
  38. const [version, distTag] = (() => {
  39. const inputVersion = args.values.version;
  40. if (!inputVersion) {
  41. exitWithError('Missing required argument: "--version <version>"');
  42. }
  43. if (inputVersion === "experimental") {
  44. const randomId = crypto
  45. .randomBytes(Math.ceil(9 / 2))
  46. .toString("hex")
  47. .slice(0, 9);
  48. return [
  49. `0.0.0-experimental-${randomId}-${new Date()
  50. .toISOString()
  51. .slice(0, 10)
  52. .replace(/-/g, "")}`,
  53. "experimental",
  54. ];
  55. }
  56. const match = inputVersion.match(
  57. /^(?:[0-9]+\.){2}(?:[0-9]+)(?:-(dev|alpha|beta|rc)\.[0-9]+)?$/
  58. );
  59. if (!match) {
  60. exitWithError(`Invalid version: ${inputVersion}`);
  61. }
  62. return [inputVersion, match[1] || "latest"];
  63. })();
  64. /*
  65. * Get the current version
  66. */
  67. const currentVersion = JSON.parse(
  68. fs.readFileSync("package.json", "utf-8")
  69. ).version;
  70. console.log(
  71. `Releasing with version: ${currentVersion} -> ${version} and dist-tag: ${distTag}`
  72. );
  73. /*
  74. * Update version in CHANGELOG.md
  75. */
  76. console.log("Updating CHANGELOG.md and bumping versions");
  77. const changelog = fs.readFileSync("CHANGELOG.md", "utf8");
  78. const UNRELEASED_PLACEHOLDER = "# [Unreleased]";
  79. const index = changelog.indexOf(UNRELEASED_PLACEHOLDER);
  80. if (index === -1) {
  81. exitWithError(`Could not find "${UNRELEASED_PLACEHOLDER}" in CHANGELOG.md`);
  82. }
  83. let nextVersionIndex = changelog.indexOf("\n# ", index);
  84. if (nextVersionIndex === -1) {
  85. nextVersionIndex = changelog.length - 1;
  86. }
  87. const releaseNots = changelog
  88. .substring(index + UNRELEASED_PLACEHOLDER.length, nextVersionIndex)
  89. .trim();
  90. fs.writeFileSync(
  91. "CHANGELOG.md",
  92. changelog.replace(
  93. UNRELEASED_PLACEHOLDER,
  94. `${UNRELEASED_PLACEHOLDER}\n\n# ${version}`
  95. )
  96. );
  97. /*
  98. * Bump npm versions
  99. */
  100. exec("git add CHANGELOG.md");
  101. exec(`npm version ${version} --workspaces --force`);
  102. exec("git add **/package.json");
  103. exec(`npm version ${version} --include-workspace-root --force`);
  104. const pushCommand = `git push origin ${process.env.GITHUB_REF_NAME} --follow-tags`;
  105. if (distTag === "experimental") {
  106. console.log(`Skipping: "${pushCommand}" for experimental version`);
  107. } else {
  108. if (dryRun) {
  109. console.log(`Skipping: "${pushCommand}" in dry-run mode`);
  110. } else {
  111. exec(pushCommand);
  112. }
  113. }
  114. /*
  115. * Build Quill package
  116. */
  117. console.log("Building Quill");
  118. exec("npm run build:quill");
  119. /*
  120. * Publish Quill package
  121. */
  122. console.log("Publishing Quill");
  123. const distFolder = "packages/quill/dist";
  124. if (
  125. JSON.parse(fs.readFileSync(path.join(distFolder, "package.json"), "utf-8"))
  126. .version !== version
  127. ) {
  128. exitWithError("Version mismatch between package.json and dist/package.json");
  129. }
  130. exec(`npm publish --tag ${distTag}${dryRun ? " --dry-run" : ""}`, {
  131. cwd: distFolder,
  132. });
  133. /*
  134. * Create GitHub release
  135. */
  136. if (distTag === "experimental") {
  137. console.log("Skipping GitHub release for experimental version");
  138. } else {
  139. const filename = `release-note-${version}-${(Math.random() * 1000) | 0}.txt`;
  140. fs.writeFileSync(filename, releaseNots);
  141. try {
  142. const prereleaseFlag = distTag === "latest" ? "--latest" : " --prerelease";
  143. const releaseCommand = `gh release create v${version} ${prereleaseFlag} -t "Version ${version}" --notes-file "${filename}"`;
  144. if (dryRun) {
  145. console.log(`Skipping: "${releaseCommand}" in dry-run mode`);
  146. console.log(`Release note:\n${releaseNots}`);
  147. } else {
  148. exec(releaseCommand);
  149. }
  150. } finally {
  151. fs.unlinkSync(filename);
  152. }
  153. }
  154. /*
  155. * Create npm package tarball
  156. */
  157. exec("npm pack", { cwd: distFolder });