diff --git a/tools/release/01_bump_dependency_crate_versions.ts b/tools/release/01_bump_dependency_crate_versions.ts index 61555f8310..931b7d199b 100755 --- a/tools/release/01_bump_dependency_crate_versions.ts +++ b/tools/release/01_bump_dependency_crate_versions.ts @@ -1,6 +1,6 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-run=cargo // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -import { DenoWorkspace } from "./helpers/mod.ts"; +import { DenoWorkspace } from "./deno_workspace.ts"; const workspace = await DenoWorkspace.load(); @@ -8,4 +8,5 @@ for (const crate of workspace.getDependencyCrates()) { await crate.increment("minor"); } -await workspace.updateLockFile(); +// update the lock file +await workspace.getCliCrate().cargoCheck(); diff --git a/tools/release/02_publish_dependency_crates.ts b/tools/release/02_publish_dependency_crates.ts index 44b5fe9692..d210971a14 100755 --- a/tools/release/02_publish_dependency_crates.ts +++ b/tools/release/02_publish_dependency_crates.ts @@ -1,6 +1,7 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-run=cargo --allow-net=crates.io // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -import { DenoWorkspace, getCratesPublishOrder } from "./helpers/mod.ts"; +import { DenoWorkspace } from "./deno_workspace.ts"; +import { getCratesPublishOrder } from "./deps.ts"; const workspace = await DenoWorkspace.load(); diff --git a/tools/release/03_bump_cli_version.ts b/tools/release/03_bump_cli_version.ts index e2a64ede07..bd1f3d1c63 100755 --- a/tools/release/03_bump_cli_version.ts +++ b/tools/release/03_bump_cli_version.ts @@ -1,19 +1,17 @@ #!/usr/bin/env -S deno run --allow-read --allow-write --allow-run=cargo,git // Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. -import { - DenoWorkspace, - formatGitLogForMarkdown, - getGitLogFromTag, -} from "./helpers/mod.ts"; +import { DenoWorkspace } from "./deno_workspace.ts"; const workspace = await DenoWorkspace.load(); +const repo = workspace.repo; const cliCrate = workspace.getCliCrate(); const originalVersion = cliCrate.version; // increment the version -await cliCrate.increment(getVersionIncrement()); -await workspace.updateLockFile(); +await cliCrate.promptAndIncrement(); +// update the lock file +await cliCrate.cargoCheck(); // output the Releases.md markdown text console.log( @@ -21,24 +19,13 @@ console.log( ); console.log(await getReleasesMdText()); -function getVersionIncrement() { - if (confirm("Increment patch?")) { - return "patch"; - } else if (confirm("Increment minor?")) { - return "minor"; - } else if (confirm("Increment major?")) { - return "major"; - } else { - throw new Error("No decision."); - } -} - async function getReleasesMdText() { - const gitLogOutput = await getGitLogFromTag( - DenoWorkspace.rootDirPath, + const gitLog = await repo.getGitLogFromTags( + "upstream", `v${originalVersion}`, + undefined, ); - const formattedGitLog = formatGitLogForMarkdown(gitLogOutput); + const formattedGitLog = gitLog.formatForReleaseMarkdown(); const formattedDate = getFormattedDate(new Date()); return `### ${cliCrate.version} / ${formattedDate}\n\n` + diff --git a/tools/release/deno_workspace.ts b/tools/release/deno_workspace.ts new file mode 100644 index 0000000000..725d647df4 --- /dev/null +++ b/tools/release/deno_workspace.ts @@ -0,0 +1,70 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +import { path, Repo } from "./deps.ts"; + +export class DenoWorkspace { + #repo: Repo; + + static get rootDirPath() { + const currentDirPath = path.dirname(path.fromFileUrl(import.meta.url)); + return path.resolve(currentDirPath, "../../"); + } + + static async load(): Promise { + return new DenoWorkspace( + await Repo.load("deno", DenoWorkspace.rootDirPath), + ); + } + + private constructor(repo: Repo) { + this.#repo = repo; + } + + get repo() { + return this.#repo; + } + + get crates() { + return this.#repo.crates; + } + + /** Gets the dependency crates used for the first part of the release process. */ + getDependencyCrates() { + return [ + this.getBenchUtilCrate(), + this.getSerdeV8Crate(), + this.getCoreCrate(), + ...this.getExtCrates(), + this.getRuntimeCrate(), + ]; + } + + getSerdeV8Crate() { + return this.getCrate("serde_v8"); + } + + getCliCrate() { + return this.getCrate("deno"); + } + + getCoreCrate() { + return this.getCrate("deno_core"); + } + + getRuntimeCrate() { + return this.getCrate("deno_runtime"); + } + + getBenchUtilCrate() { + return this.getCrate("deno_bench_util"); + } + + getExtCrates() { + const extPath = path.join(this.#repo.folderPath, "ext"); + return this.crates.filter((c) => c.manifestPath.startsWith(extPath)); + } + + getCrate(name: string) { + return this.#repo.getCrate(name); + } +} diff --git a/tools/release/deps.ts b/tools/release/deps.ts new file mode 100644 index 0000000000..df7a4fc605 --- /dev/null +++ b/tools/release/deps.ts @@ -0,0 +1,3 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +export * from "https://raw.githubusercontent.com/denoland/automation/0.2.0/mod.ts"; diff --git a/tools/release/helpers/cargo.ts b/tools/release/helpers/cargo.ts deleted file mode 100644 index 15dd0c5b61..0000000000 --- a/tools/release/helpers/cargo.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2018-2022 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 function publishCrate(directory: string) { - return runCargoSubCommand({ - directory, - args: ["publish"], - }); -} - -export function build(directory: string) { - return runCargoSubCommand({ - directory, - args: ["build", "-vv"], - }); -} - -export function check(directory: string) { - return runCargoSubCommand({ - directory, - args: ["check"], - }); -} - -async function runCargoSubCommand(params: { - args: string[]; - directory: string; -}) { - const p = Deno.run({ - cwd: params.directory, - cmd: ["cargo", ...params.args], - 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 deleted file mode 100644 index af26f55af0..0000000000 --- a/tools/release/helpers/crates_io.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2018-2022 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 deleted file mode 100644 index 6d7a03cb74..0000000000 --- a/tools/release/helpers/deno_workspace.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright 2018-2022 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 * as cargo 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 { - return new DenoWorkspace( - await cargo.getMetadata(DenoWorkspace.rootDirPath), - ); - } - - private constructor(metadata: cargo.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.getSerdeV8Crate(), - this.getCoreCrate(), - ...this.getExtCrates(), - this.getRuntimeCrate(), - ]; - } - - getSerdeV8Crate() { - return this.getCrateByNameOrThrow("serde_v8"); - } - - 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; - } - - build() { - return cargo.build(DenoWorkspace.rootDirPath); - } - - updateLockFile() { - return cargo.check(DenoWorkspace.rootDirPath); - } -} - -export class DenoWorkspaceCrate { - #workspace: DenoWorkspace; - #pkg: cargo.CargoPackageMetadata; - #isUpdatingManifest = false; - - constructor(workspace: DenoWorkspace, pkg: cargo.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 the crates.io index - // not being updated yet. Usually it will be resolved after - // retrying, so try a few times before failing hard. - return await withRetries({ - action: async () => { - await cargo.publishCrate(this.directoryPath); - return true; - }, - retryCount: 5, - retryDelaySeconds: 10, - }); - } - - build() { - return cargo.build(this.directoryPath); - } - - updateLockFile() { - return cargo.check(this.directoryPath); - } - - 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 deleted file mode 100644 index 454a663731..0000000000 --- a/tools/release/helpers/helpers.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2018-2022 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}..`], - }); -} - -const IGNORED_COMMIT_PREFIX = [ - "build", - "chore", - "ci", - "docs", - "refactor", - "test", -]; - -export function formatGitLogForMarkdown(text: string) { - return text.split(/\r?\n/) - .map((line) => line.replace(/^[a-f0-9]{9} /i, "").trim()) - .filter((l) => { - return !IGNORED_COMMIT_PREFIX.some((prefix) => l.startsWith(prefix)) && - 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(params: { - action: () => Promise; - 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 deleted file mode 100644 index 2cf00c3527..0000000000 --- a/tools/release/helpers/mod.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2018-2022 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";