diff options
author | David Sherret <dsherret@users.noreply.github.com> | 2021-08-25 09:02:22 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-08-25 09:02:22 -0400 |
commit | dce70d32a47801025b3b67a97ec9ebed90dfc8a2 (patch) | |
tree | f08176bdad18f931956f24cd7d3b92ba313c4493 /tools/release/helpers | |
parent | dccf4cbe36d66140f9e35a6ee755c3c440d745f9 (diff) |
chore: add scripts for helping with a release (#11832)
Diffstat (limited to 'tools/release/helpers')
-rw-r--r-- | tools/release/helpers/cargo.ts | 48 | ||||
-rw-r--r-- | tools/release/helpers/crates_io.ts | 22 | ||||
-rw-r--r-- | tools/release/helpers/deno_workspace.ts | 210 | ||||
-rw-r--r-- | tools/release/helpers/helpers.ts | 92 | ||||
-rw-r--r-- | tools/release/helpers/mod.ts | 10 |
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"; |