summaryrefslogtreecommitdiff
path: root/tools/release/helpers
diff options
context:
space:
mode:
authorDavid Sherret <dsherret@users.noreply.github.com>2021-08-25 09:02:22 -0400
committerGitHub <noreply@github.com>2021-08-25 09:02:22 -0400
commitdce70d32a47801025b3b67a97ec9ebed90dfc8a2 (patch)
treef08176bdad18f931956f24cd7d3b92ba313c4493 /tools/release/helpers
parentdccf4cbe36d66140f9e35a6ee755c3c440d745f9 (diff)
chore: add scripts for helping with a release (#11832)
Diffstat (limited to 'tools/release/helpers')
-rw-r--r--tools/release/helpers/cargo.ts48
-rw-r--r--tools/release/helpers/crates_io.ts22
-rw-r--r--tools/release/helpers/deno_workspace.ts210
-rw-r--r--tools/release/helpers/helpers.ts92
-rw-r--r--tools/release/helpers/mod.ts10
5 files changed, 382 insertions, 0 deletions
diff --git a/tools/release/helpers/cargo.ts b/tools/release/helpers/cargo.ts
new file mode 100644
index 000000000..619d7a0f7
--- /dev/null
+++ b/tools/release/helpers/cargo.ts
@@ -0,0 +1,48 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+import { runCommand } from "./helpers.ts";
+
+export interface CargoMetadata {
+ packages: CargoPackageMetadata[];
+ /** Identifiers in the `packages` array of the workspace members. */
+ "workspace_members": string[];
+ /** The absolute workspace root directory path. */
+ "workspace_root": string;
+}
+
+export interface CargoPackageMetadata {
+ id: string;
+ name: string;
+ version: string;
+ dependencies: CargoDependencyMetadata[];
+ /** Path to Cargo.toml */
+ "manifest_path": string;
+}
+
+export interface CargoDependencyMetadata {
+ name: string;
+ /** Version requrement (ex. ^0.1.0) */
+ req: string;
+}
+
+export async function getMetadata(directory: string) {
+ const result = await runCommand({
+ cwd: directory,
+ cmd: ["cargo", "metadata", "--format-version", "1"],
+ });
+ return JSON.parse(result!) as CargoMetadata;
+}
+
+export async function publishCrate(directory: string) {
+ const p = Deno.run({
+ cwd: directory,
+ cmd: ["cargo", "publish"],
+ stderr: "inherit",
+ stdout: "inherit",
+ });
+
+ const status = await p.status();
+ if (!status.success) {
+ throw new Error("Failed");
+ }
+}
diff --git a/tools/release/helpers/crates_io.ts b/tools/release/helpers/crates_io.ts
new file mode 100644
index 000000000..b26539964
--- /dev/null
+++ b/tools/release/helpers/crates_io.ts
@@ -0,0 +1,22 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+export interface CratesIoMetadata {
+ crate: {
+ id: string;
+ name: string;
+ };
+ versions: {
+ crate: string;
+ num: string;
+ }[];
+}
+
+export async function getCratesIoMetadata(crateName: string) {
+ // rate limit
+ await new Promise((resolve) => setTimeout(resolve, 100));
+
+ const response = await fetch(`https://crates.io/api/v1/crates/${crateName}`);
+ const data = await response.json();
+
+ return data as CratesIoMetadata;
+}
diff --git a/tools/release/helpers/deno_workspace.ts b/tools/release/helpers/deno_workspace.ts
new file mode 100644
index 000000000..f964d24b3
--- /dev/null
+++ b/tools/release/helpers/deno_workspace.ts
@@ -0,0 +1,210 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+import * as path from "https://deno.land/std@0.105.0/path/mod.ts";
+import * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts";
+import {
+ CargoMetadata,
+ CargoPackageMetadata,
+ getMetadata,
+ publishCrate,
+} from "./cargo.ts";
+import { getCratesIoMetadata } from "./crates_io.ts";
+import { withRetries } from "./helpers.ts";
+
+export class DenoWorkspace {
+ #workspaceCrates: readonly DenoWorkspaceCrate[];
+ #workspaceRootDirPath: string;
+
+ static get rootDirPath() {
+ const currentDirPath = path.dirname(path.fromFileUrl(import.meta.url));
+ return path.resolve(currentDirPath, "../../../");
+ }
+
+ static async load(): Promise<DenoWorkspace> {
+ return new DenoWorkspace(await getMetadata(DenoWorkspace.rootDirPath));
+ }
+
+ private constructor(metadata: CargoMetadata) {
+ const crates = [];
+ for (const memberId of metadata.workspace_members) {
+ const pkg = metadata.packages.find((pkg) => pkg.id === memberId);
+ if (!pkg) {
+ throw new Error(`Could not find package with id ${memberId}`);
+ }
+ crates.push(new DenoWorkspaceCrate(this, pkg));
+ }
+
+ this.#workspaceCrates = crates;
+ this.#workspaceRootDirPath = metadata.workspace_root;
+ }
+
+ get crates() {
+ return this.#workspaceCrates;
+ }
+
+ /** Gets the dependency crates used for the first part of the release process. */
+ getDependencyCrates() {
+ return [
+ this.getBenchUtilCrate(),
+ this.getCoreCrate(),
+ ...this.getExtCrates(),
+ this.getRuntimeCrate(),
+ ];
+ }
+
+ getCliCrate() {
+ return this.getCrateByNameOrThrow("deno");
+ }
+
+ getCoreCrate() {
+ return this.getCrateByNameOrThrow("deno_core");
+ }
+
+ getRuntimeCrate() {
+ return this.getCrateByNameOrThrow("deno_runtime");
+ }
+
+ getBenchUtilCrate() {
+ return this.getCrateByNameOrThrow("deno_bench_util");
+ }
+
+ getExtCrates() {
+ const extPath = path.join(this.#workspaceRootDirPath, "ext");
+ return this.#workspaceCrates.filter((c) =>
+ c.manifestPath.startsWith(extPath)
+ );
+ }
+
+ getCrateByNameOrThrow(name: string) {
+ const crate = this.#workspaceCrates.find((c) => c.name === name);
+ if (!crate) {
+ throw new Error(`Could not find crate: ${name}`);
+ }
+ return crate;
+ }
+}
+
+export class DenoWorkspaceCrate {
+ #workspace: DenoWorkspace;
+ #pkg: CargoPackageMetadata;
+ #isUpdatingManifest = false;
+
+ constructor(workspace: DenoWorkspace, pkg: CargoPackageMetadata) {
+ this.#workspace = workspace;
+ this.#pkg = pkg;
+ }
+
+ get manifestPath() {
+ return this.#pkg.manifest_path;
+ }
+
+ get directoryPath() {
+ return path.dirname(this.#pkg.manifest_path);
+ }
+
+ get name() {
+ return this.#pkg.name;
+ }
+
+ get version() {
+ return this.#pkg.version;
+ }
+
+ getDependencies() {
+ const dependencies = [];
+ for (const dependency of this.#pkg.dependencies) {
+ const crate = this.#workspace.crates.find((c) =>
+ c.name === dependency.name
+ );
+ if (crate != null) {
+ dependencies.push(crate);
+ }
+ }
+ return dependencies;
+ }
+
+ async isPublished() {
+ const cratesIoMetadata = await getCratesIoMetadata(this.name);
+ return cratesIoMetadata.versions.some((v) => v.num === this.version);
+ }
+
+ async publish() {
+ if (await this.isPublished()) {
+ console.log(`Already published ${this.name} ${this.version}`);
+ return false;
+ }
+
+ console.log(`Publishing ${this.name} ${this.version}...`);
+
+ // Sometimes a publish may fail due to local caching issues.
+ // Usually it will fix itself after retrying so try a few
+ // times before failing hard.
+ return await withRetries({
+ action: async () => {
+ await publishCrate(this.directoryPath);
+ return true;
+ },
+ retryCount: 3,
+ retryDelaySeconds: 10,
+ });
+ }
+
+ increment(part: "major" | "minor" | "patch") {
+ const newVersion = semver.parse(this.version)!.inc(part).toString();
+ return this.setVersion(newVersion);
+ }
+
+ async setVersion(version: string) {
+ console.log(`Setting ${this.name} to ${version}...`);
+ for (const crate of this.#workspace.crates) {
+ await crate.setDependencyVersion(this.name, version);
+ }
+ await this.#updateManifestVersion(version);
+ }
+
+ async setDependencyVersion(dependencyName: string, version: string) {
+ const dependency = this.#pkg.dependencies.find((d) =>
+ d.name === dependencyName
+ );
+ if (dependency != null) {
+ await this.#updateManifestFile((fileText) => {
+ // simple for now...
+ const findRegex = new RegExp(
+ `^(\\b${dependencyName}\\b\\s.*)"([=\\^])?[0-9]+[^"]+"`,
+ "gm",
+ );
+ return fileText.replace(findRegex, `$1"${version}"`);
+ });
+
+ dependency.req = `^${version}`;
+ }
+ }
+
+ async #updateManifestVersion(version: string) {
+ await this.#updateManifestFile((fileText) => {
+ const findRegex = new RegExp(
+ `^(version\\s*=\\s*)"${this.#pkg.version}"$`,
+ "m",
+ );
+ return fileText.replace(findRegex, `$1"${version}"`);
+ });
+ this.#pkg.version = version;
+ }
+
+ async #updateManifestFile(action: (fileText: string) => string) {
+ if (this.#isUpdatingManifest) {
+ throw new Error("Cannot update manifest while updating manifest.");
+ }
+ this.#isUpdatingManifest = true;
+ try {
+ const originalText = await Deno.readTextFile(this.#pkg.manifest_path);
+ const newText = action(originalText);
+ if (originalText === newText) {
+ throw new Error(`The file didn't change: ${this.manifestPath}`);
+ }
+ await Deno.writeTextFile(this.manifestPath, newText);
+ } finally {
+ this.#isUpdatingManifest = false;
+ }
+ }
+}
diff --git a/tools/release/helpers/helpers.ts b/tools/release/helpers/helpers.ts
new file mode 100644
index 000000000..c034af546
--- /dev/null
+++ b/tools/release/helpers/helpers.ts
@@ -0,0 +1,92 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+import type { DenoWorkspaceCrate } from "./deno_workspace.ts";
+
+export function getCratesPublishOrder(crates: DenoWorkspaceCrate[]) {
+ const pendingCrates = [...crates];
+ const sortedCrates = [];
+
+ while (pendingCrates.length > 0) {
+ for (let i = pendingCrates.length - 1; i >= 0; i--) {
+ const crate = pendingCrates[i];
+ const hasPendingDependency = crate.getDependencies()
+ .some((c) => pendingCrates.includes(c));
+ if (!hasPendingDependency) {
+ sortedCrates.push(crate);
+ pendingCrates.splice(i, 1);
+ }
+ }
+ }
+
+ return sortedCrates;
+}
+
+export function getGitLogFromTag(directory: string, tagName: string) {
+ return runCommand({
+ cwd: directory,
+ cmd: ["git", "log", "--oneline", `${tagName}..`],
+ });
+}
+
+export function formatGitLogForMarkdown(text: string) {
+ return text.split(/\r?\n/)
+ .map((line) => line.replace(/^[a-f0-9]{9} /i, "").trim())
+ .filter((l) => !l.startsWith("chore") && l.length > 0)
+ .sort()
+ .map((line) => `- ${line}`)
+ .join("\n");
+}
+
+export async function runCommand(params: {
+ cwd: string;
+ cmd: string[];
+}) {
+ const p = Deno.run({
+ cwd: params.cwd,
+ cmd: params.cmd,
+ stderr: "piped",
+ stdout: "piped",
+ });
+
+ const [status, stdout, stderr] = await Promise.all([
+ p.status(),
+ p.output(),
+ p.stderrOutput(),
+ ]);
+ p.close();
+
+ if (!status.success) {
+ throw new Error(
+ `Error executing ${params.cmd[0]}: ${new TextDecoder().decode(stderr)}`,
+ );
+ }
+
+ return new TextDecoder().decode(stdout);
+}
+
+export async function withRetries<TReturn>(params: {
+ action: () => Promise<TReturn>;
+ retryCount: number;
+ retryDelaySeconds: number;
+}) {
+ for (let i = 0; i < params.retryCount; i++) {
+ if (i > 0) {
+ console.log(
+ `Failed. Trying again in ${params.retryDelaySeconds} seconds...`,
+ );
+ await delay(params.retryDelaySeconds * 1000);
+ console.log(`Attempt ${i + 1}/${params.retryCount}...`);
+ }
+ try {
+ return await params.action();
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ throw new Error(`Failed after ${params.retryCount} attempts.`);
+}
+
+function delay(ms: number) {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+}
diff --git a/tools/release/helpers/mod.ts b/tools/release/helpers/mod.ts
new file mode 100644
index 000000000..f9d23ac83
--- /dev/null
+++ b/tools/release/helpers/mod.ts
@@ -0,0 +1,10 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+export * from "./cargo.ts";
+export * from "./crates_io.ts";
+export * from "./deno_workspace.ts";
+export {
+ formatGitLogForMarkdown,
+ getCratesPublishOrder,
+ getGitLogFromTag,
+} from "./helpers.ts";