mirror of
https://github.com/denoland/deno.git
synced 2024-12-22 07:14:47 -05:00
chore: add scripts for helping with a release (#11832)
This commit is contained in:
parent
dccf4cbe36
commit
dce70d32a4
9 changed files with 475 additions and 35 deletions
|
@ -16,57 +16,40 @@ cut.**
|
|||
|
||||
## Updating the main repo
|
||||
|
||||
1. Create a PR that does a minor version bump of all crates in `bench_util`,
|
||||
`core`, `ext`, `runtime` directories.
|
||||
1. Run `./tools/release/01_bump_dependency_crate_versions.ts` to increase the
|
||||
minor versions of all crates in the `bench_util`, `core`, `ext`, and
|
||||
`runtime` directories.
|
||||
|
||||
2. Make sure CI pipeline passes.
|
||||
2. Create a PR for this change.
|
||||
|
||||
3. Publish all bumped crates to `crates.io`
|
||||
3. Make sure CI pipeline passes (DO NOT merge yet).
|
||||
|
||||
4. Run `./tools/release/02_publish_dependency_crates.ts` to publish these bumped
|
||||
crates to `crates.io`
|
||||
|
||||
**Make sure that `cargo` is logged on with a user that has permissions to
|
||||
publish those crates.**
|
||||
|
||||
This is done by running `cargo publish` in each crate, because of dependencies
|
||||
between the crates, it must be done in specific order:
|
||||
|
||||
- `deno_core` - all crates depend on `deno_core` so it must always be published
|
||||
first
|
||||
- `bench_util`
|
||||
- crates in `ext/` directory, publish in the following order:
|
||||
- broadcast_channel
|
||||
- console
|
||||
- ffi
|
||||
- tls
|
||||
- web
|
||||
- webgpu
|
||||
- webidl
|
||||
- websocket
|
||||
- webstorage
|
||||
- crypto
|
||||
- fetch
|
||||
- http
|
||||
- net
|
||||
- url
|
||||
- timers
|
||||
- `runtime` - this crate depends on `deno_core` and all crates in `ext/`
|
||||
directory
|
||||
|
||||
If there are any problems when you publish, that require you to change the code,
|
||||
then after applying the fixes they should be committed and pushed to the PR.
|
||||
|
||||
4. Once all crates are published merge the PR.
|
||||
|
||||
5. Create a PR that bumps `cli` crate version and updates `Releases.md`.
|
||||
5. Run `./tools/release/03_bump_cli_version.ts` to bump the CLI version.
|
||||
|
||||
6. Make sure CI pipeline passes.
|
||||
6. Use the output of the above command to update `Releases.md`
|
||||
|
||||
7. Publish `cli` crate to `crates.io`
|
||||
7. Create a PR for these changes.
|
||||
|
||||
8. Merge the PR.
|
||||
8. Make sure CI pipeline passes.
|
||||
|
||||
9. Create a tag with the version number (with `v` prefix).
|
||||
9. Publish `cli` crate to `crates.io`
|
||||
|
||||
10. Wait for CI pipeline on the created tag branch to pass.
|
||||
10. Merge the PR.
|
||||
|
||||
11. Create a tag with the version number (with `v` prefix).
|
||||
|
||||
12. Wait for CI pipeline on the created tag branch to pass.
|
||||
|
||||
The CI pipeline will create a release draft on GitHub
|
||||
(https://github.com/denoland/deno/releases).
|
||||
|
|
9
tools/release/01_bump_dependency_crate_versions.ts
Normal file
9
tools/release/01_bump_dependency_crate_versions.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run=cargo
|
||||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
import { DenoWorkspace } from "./helpers/mod.ts";
|
||||
|
||||
const workspace = await DenoWorkspace.load();
|
||||
|
||||
for (const crate of workspace.getDependencyCrates()) {
|
||||
await crate.increment("minor");
|
||||
}
|
11
tools/release/02_publish_depenency_crates.ts
Normal file
11
tools/release/02_publish_depenency_crates.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run=cargo --allow-net=crates.io
|
||||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
import { DenoWorkspace, getCratesPublishOrder } from "./helpers/mod.ts";
|
||||
|
||||
const workspace = await DenoWorkspace.load();
|
||||
|
||||
const dependencyCrates = workspace.getDependencyCrates();
|
||||
|
||||
for (const crate of getCratesPublishOrder(dependencyCrates)) {
|
||||
await crate.publish();
|
||||
}
|
55
tools/release/03_bump_cli_version.ts
Normal file
55
tools/release/03_bump_cli_version.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-run="cargo,git"
|
||||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
import {
|
||||
DenoWorkspace,
|
||||
formatGitLogForMarkdown,
|
||||
getGitLogFromTag,
|
||||
} from "./helpers/mod.ts";
|
||||
|
||||
const workspace = await DenoWorkspace.load();
|
||||
|
||||
const cliCrate = workspace.getCliCrate();
|
||||
const originalVersion = cliCrate.version;
|
||||
|
||||
// increment the version
|
||||
await cliCrate.increment(getVersionIncrement());
|
||||
|
||||
// output the Releases.md markdown text
|
||||
console.log(
|
||||
"You may use the following as a template for updating Releases.md:\n",
|
||||
);
|
||||
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,
|
||||
`v${originalVersion}`,
|
||||
);
|
||||
const formattedGitLog = formatGitLogForMarkdown(gitLogOutput);
|
||||
const formattedDate = getFormattedDate(new Date());
|
||||
|
||||
return `### ${cliCrate.version} / ${formattedDate}\n\n` +
|
||||
`${formattedGitLog}`;
|
||||
|
||||
function getFormattedDate(date: Date) {
|
||||
const formattedMonth = padTwoDigit(date.getMonth() + 1);
|
||||
const formattedDay = padTwoDigit(date.getDate());
|
||||
return `${date.getFullYear()}.${formattedMonth}.${formattedDay}`;
|
||||
|
||||
function padTwoDigit(val: number) {
|
||||
return val.toString().padStart(2, "0");
|
||||
}
|
||||
}
|
||||
}
|
48
tools/release/helpers/cargo.ts
Normal file
48
tools/release/helpers/cargo.ts
Normal file
|
@ -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");
|
||||
}
|
||||
}
|
22
tools/release/helpers/crates_io.ts
Normal file
22
tools/release/helpers/crates_io.ts
Normal file
|
@ -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;
|
||||
}
|
210
tools/release/helpers/deno_workspace.ts
Normal file
210
tools/release/helpers/deno_workspace.ts
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
92
tools/release/helpers/helpers.ts
Normal file
92
tools/release/helpers/helpers.ts
Normal file
|
@ -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));
|
||||
}
|
10
tools/release/helpers/mod.ts
Normal file
10
tools/release/helpers/mod.ts
Normal file
|
@ -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";
|
Loading…
Reference in a new issue