1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Release 6.4.0 (#7250)

* General fixes for release-tool (#7238)

* General fixes for release-tool

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert change to number of PRs retrieved

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Throw on errors in kubectlApplyFolder (#7239)

Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>

* Quick fix for store migration version being wrong (#7243)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Revert "Renderer file logging transport (#6795)" (#7245)

Renderer file logging still caused UI freezing (at least on apple silicon macs) when cluster frame was open and main frame was reloaded.

See #544

This reverts commit ac2d0e46ff.

Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>

* Fix extension install (#7247)

* Fix extension install

- Remove old bundled extension dependencies
- Make sure external extensions are installed as optional

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Ignore ENOENT errors

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Add comment

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Release 6.4.0

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix lint

Signed-off-by: Sebastian Malton <sebastian@malton.name>

---------

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Signed-off-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
Signed-off-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>
Co-authored-by: Panu Horsmalahti <phorsmalahti@mirantis.com>
Co-authored-by: Sami Tiilikainen <97873007+samitiilikainen@users.noreply.github.com>
This commit is contained in:
Sebastian Malton 2023-03-01 06:32:52 -08:00 committed by GitHub
parent b74cc14b72
commit 5ac90e9178
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 269 additions and 300 deletions

View File

@ -4,7 +4,7 @@
"packages": [
"packages/*"
],
"version": "6.4.0-beta.17",
"version": "6.4.0",
"npmClient": "yarn",
"npmClientArgs": [
"--network-timeout=100000"

View File

@ -3,7 +3,7 @@
"productName": "",
"description": "Lens Desktop Core",
"homepage": "https://github.com/lensapp/lens",
"version": "6.4.0-beta.17",
"version": "6.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import kubectlApplyAllInjectable from "../../main/kubectl/kubectl-apply-all.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { KubernetesCluster } from "../catalog-entities";
import readDirectoryInjectable from "../fs/read-directory.injectable";
import readFileInjectable from "../fs/read-file.injectable";
import createResourceStackInjectable from "../k8s/create-resource-stack.injectable";
import appPathsStateInjectable from "../app-paths/app-paths-state.injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
describe("create resource stack tests", () => {
let di: DiContainer;
let cluster: KubernetesCluster;
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
cluster = {
getId: () => "test-cluster",
} as any;
di.override(readDirectoryInjectable, () => () => Promise.resolve(["file1"]) as any);
di.override(readFileInjectable, () => () => Promise.resolve("filecontents"));
di.override(appPathsStateInjectable, () => ({
get: () => ({}),
}));
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
});
describe("kubectlApplyFolder", () => {
it("returns response", async () => {
di.override(kubectlApplyAllInjectable, () => () => Promise.resolve({
callWasSuccessful: true as const,
response: "success",
}));
const createResourceStack = di.inject(createResourceStackInjectable);
const resourceStack = createResourceStack(cluster, "test");
const response = await resourceStack.kubectlApplyFolder("/foo/bar");
expect(response).toEqual("success");
});
it("throws on error", async () => {
di.override(kubectlApplyAllInjectable, () => () => Promise.resolve({
callWasSuccessful: false as const,
error: "No permissions",
}));
const createResourceStack = di.inject(createResourceStackInjectable);
const resourceStack = createResourceStack(cluster, "test");
await expect(() => resourceStack.kubectlApplyFolder("/foo/bar")).rejects.toThrow("No permissions");
});
});
});

View File

@ -7,7 +7,7 @@ import type { ReadOptions } from "fs-extra";
import fse from "fs-extra";
/**
* NOTE: Add corrisponding a corrisponding override of this injecable in `src/test-utils/override-fs-with-fakes.ts`
* NOTE: Add corresponding override of this injectable in `src/test-utils/override-fs-with-fakes.ts`
*/
const fsInjectable = getInjectable({
id: "fs",

View File

@ -51,7 +51,7 @@ export class ResourceStack {
this.dependencies.logger.warn(`[RESOURCE-STACK]: failed to apply resources: ${result.error}`);
return "";
throw new Error(result.error);
}
/**

View File

@ -3,13 +3,20 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { createLogger, format } from "winston";
import type { Logger } from "./logger";
import winstonLoggerInjectable from "./winston-logger.injectable";
import { loggerTransportInjectionToken } from "./logger/transports";
const loggerInjectable = getInjectable({
id: "logger",
instantiate: (di): Logger => {
const baseLogger = di.inject(winstonLoggerInjectable);
const baseLogger = createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
});
return {
debug: (message, ...data) => baseLogger.debug(message, ...data),

View File

@ -3,11 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { applicationInformationToken } from "./application-information-token";
const storeMigrationVersionInjectable = getInjectable({
id: "store-migration-version",
instantiate: (di) => di.inject(applicationInformationToken).version,
instantiate: () => "6.4.0",
});
export default storeMigrationVersionInjectable;

View File

@ -1,20 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { createLogger, format } from "winston";
import { loggerTransportInjectionToken } from "./logger/transports";
const winstonLoggerInjectable = getInjectable({
id: "winston-logger",
instantiate: (di) => createLogger({
format: format.combine(
format.splat(),
format.simple(),
),
transports: di.injectMany(loggerTransportInjectionToken),
}),
});
export default winstonLoggerInjectable;

View File

@ -8,8 +8,8 @@ import extensionLoaderInjectable from "../extension-loader/extension-loader.inje
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionInjectable from "../install-extension/install-extension.injectable";
import extensionPackageRootDirectoryInjectable from "../install-extension/extension-package-root-directory.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import loggerInjectable from "../../common/logger.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable";

View File

@ -7,7 +7,7 @@ import type { FSWatcher } from "chokidar";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable";
import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery";
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
import installExtensionInjectable from "../install-extension/install-extension.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { delay } from "../../renderer/utils";
import { observable, runInAction, when } from "mobx";

View File

@ -1,21 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import pathToNpmCliInjectable from "../../common/app-paths/path-to-npm-cli.injectable";
import loggerInjectable from "../../common/logger.injectable";
import { ExtensionInstaller } from "./extension-installer";
import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory/extension-package-root-directory.injectable";
const extensionInstallerInjectable = getInjectable({
id: "extension-installer",
instantiate: (di) => new ExtensionInstaller({
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
logger: di.inject(loggerInjectable),
pathToNpmCli: di.inject(pathToNpmCliInjectable),
}),
});
export default extensionInstallerInjectable;

View File

@ -1,78 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import AwaitLock from "await-lock";
import child_process from "child_process";
import type { Logger } from "../../common/logger";
const logModule = "[EXTENSION-INSTALLER]";
interface Dependencies {
readonly extensionPackageRootDirectory: string;
readonly logger: Logger;
readonly pathToNpmCli: string;
}
const baseNpmInstallArgs = [
"install",
"--audit=false",
"--fund=false",
// NOTE: we do not omit the `optional` dependencies because that is how we specify the non-bundled extensions
"--omit=dev",
"--omit=peer",
"--prefer-offline",
];
/**
* Installs dependencies for extensions
*/
export class ExtensionInstaller {
private readonly installLock = new AwaitLock();
constructor(private readonly dependencies: Dependencies) {}
/**
* Install single package using npm
*/
installPackage = async (name: string): Promise<void> => {
// Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync();
try {
this.dependencies.logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`);
await this.npm(...baseNpmInstallArgs, name);
this.dependencies.logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`);
} finally {
this.installLock.release();
}
};
private npm(...args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = child_process.fork(this.dependencies.pathToNpmCli, args, {
cwd: this.dependencies.extensionPackageRootDirectory,
silent: true,
env: {},
});
let stderr = "";
child.stderr?.on("data", data => {
stderr += String(data);
});
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(stderr));
} else {
resolve();
}
});
child.on("error", error => {
reject(error);
});
});
}
}

View File

@ -1,13 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import extensionInstallerInjectable from "../extension-installer.injectable";
const installExtensionInjectable = getInjectable({
id: "install-extension",
instantiate: (di) => di.inject(extensionInstallerInjectable).installPackage,
});
export default installExtensionInjectable;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackageRootDirectoryInjectable = getInjectable({
id: "extension-package-root-directory",

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { fork } from "child_process";
import AwaitLock from "await-lock";
import pathToNpmCliInjectable from "../../common/app-paths/path-to-npm-cli.injectable";
import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory.injectable";
import prefixedLoggerInjectable from "../../common/logger/prefixed-logger.injectable";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import type { PackageJson } from "../common-api";
import writeJsonFileInjectable from "../../common/fs/write-json-file.injectable";
import { once } from "lodash";
import { isErrnoException } from "../../common/utils";
const baseNpmInstallArgs = [
"install",
"--save-optional",
"--audit=false",
"--fund=false",
// NOTE: we do not omit the `optional` dependencies because that is how we specify the non-bundled extensions
"--omit=dev",
"--omit=peer",
"--prefer-offline",
];
export type InstallExtension = (name: string) => Promise<void>;
const installExtensionInjectable = getInjectable({
id: "install-extension",
instantiate: (di): InstallExtension => {
const pathToNpmCli = di.inject(pathToNpmCliInjectable);
const extensionPackageRootDirectory = di.inject(extensionPackageRootDirectoryInjectable);
const readJsonFile = di.inject(readJsonFileInjectable);
const writeJsonFile = di.inject(writeJsonFileInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const logger = di.inject(prefixedLoggerInjectable, "EXTENSION-INSTALLER");
const forkNpm = (...args: string[]) => new Promise<void>((resolve, reject) => {
const child = fork(pathToNpmCli, args, {
cwd: extensionPackageRootDirectory,
silent: true,
env: {},
});
let stderr = "";
child.stderr?.on("data", data => {
stderr += String(data);
});
child.on("close", (code) => {
if (code !== 0) {
reject(new Error(stderr));
} else {
resolve();
}
});
child.on("error", error => {
reject(error);
});
});
const packageJsonPath = joinPaths(extensionPackageRootDirectory, "package.json");
/**
* NOTES:
* - We have to keep the `package.json` because `npm install` removes files from `node_modules`
* if they are no longer in the `package.json`
* - In v6.2.X we saved bundled extensions as `"dependencies"` and external extensions as
* `"optionalDependencies"` at startup. This was done because `"optionalDependencies"` can
* fail to install and that is OK.
* - We continue to maintain this behavior here by only installing new dependencies as
* `"optionalDependencies"`
*/
const fixupPackageJson = once(async () => {
try {
const packageJson = await readJsonFile(packageJsonPath) as PackageJson;
delete packageJson.dependencies;
await writeJsonFile(packageJsonPath, packageJson);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
return;
}
throw error;
}
});
const installLock = new AwaitLock();
return async (name) => {
await installLock.acquireAsync();
await fixupPackageJson();
try {
logger.info(`installing package for extension "${name}"`);
await forkNpm(...baseNpmInstallArgs, name);
logger.info(`installed package for extension "${name}"`);
} finally {
installLock.release();
}
};
},
});
export default installExtensionInjectable;

View File

@ -46,9 +46,7 @@ export async function bootstrap(di: DiContainer) {
}
try {
await initializeApp(() => {
unmountComponentAtNode(rootElem);
});
await initializeApp(() => unmountComponentAtNode(rootElem));
} catch (error) {
console.error(`[BOOTSTRAP]: view initialization error: ${error}`, {
origin: location.href,

View File

@ -12,7 +12,6 @@ import emitAppEventInjectable from "../../../../common/app-event-bus/emit-event.
import loadExtensionsInjectable from "../../load-extensions.injectable";
import loggerInjectable from "../../../../common/logger.injectable";
import showErrorNotificationInjectable from "../../../components/notifications/show-error-notification.injectable";
import closeRendererLogFileInjectable from "../../../logger/close-renderer-log-file.injectable";
const initClusterFrameInjectable = getInjectable({
id: "init-cluster-frame",
@ -30,7 +29,6 @@ const initClusterFrameInjectable = getInjectable({
emitAppEvent: di.inject(emitAppEventInjectable),
logger: di.inject(loggerInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable),
closeFileLogging: di.inject(closeRendererLogFileInjectable),
});
},
});

View File

@ -2,7 +2,6 @@
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { once } from "lodash";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { CatalogEntityRegistry } from "../../../api/catalog/entity/registry";
import type { ShowNotification } from "../../../components/notifications";
@ -19,7 +18,6 @@ interface Dependencies {
emitAppEvent: EmitAppEvent;
logger: Logger;
showErrorNotification: ShowNotification;
closeFileLogging: () => void;
}
const logPrefix = "[CLUSTER-FRAME]:";
@ -32,7 +30,6 @@ export const initClusterFrame = ({
emitAppEvent,
logger,
showErrorNotification,
closeFileLogging,
}: Dependencies) =>
async (unmountRoot: () => void) => {
// TODO: Make catalogEntityRegistry already initialized when passed as dependency
@ -76,14 +73,11 @@ export const initClusterFrame = ({
});
});
const onCloseFrame = once(() => {
window.onbeforeunload = () => {
logger.info(
`${logPrefix} Unload dashboard, clusterId=${(hostedCluster.id)}, frameId=${frameRoutingId}`,
);
closeFileLogging();
unmountRoot();
});
window.addEventListener("beforeunload", onCloseFrame);
window.addEventListener("pagehide", onCloseFrame);
unmountRoot();
};
};

View File

@ -13,7 +13,6 @@ import loggerInjectable from "../../../common/logger.injectable";
import { delay } from "../../../common/utils";
import { broadcastMessage } from "../../../common/ipc";
import { bundledExtensionsLoaded } from "../../../common/ipc/extension-handling";
import closeRendererLogFileInjectable from "../../logger/close-renderer-log-file.injectable";
const initRootFrameInjectable = getInjectable({
id: "init-root-frame",
@ -25,7 +24,6 @@ const initRootFrameInjectable = getInjectable({
const lensProtocolRouterRenderer = di.inject(lensProtocolRouterRendererInjectable);
const catalogEntityRegistry = di.inject(catalogEntityRegistryInjectable);
const logger = di.inject(loggerInjectable);
const closeRendererLogFile = di.inject(closeRendererLogFileInjectable);
return async (unmountRoot: () => void) => {
catalogEntityRegistry.init();
@ -61,7 +59,7 @@ const initRootFrameInjectable = getInjectable({
window.addEventListener("beforeunload", () => {
logger.info("[ROOT-FRAME]: Unload app");
closeRendererLogFile();
unmountRoot();
});
};

View File

@ -1,22 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import winstonLoggerInjectable from "../../common/winston-logger.injectable";
import rendererFileLoggerTransportInjectable from "./file-transport.injectable";
const closeRendererLogFileInjectable = getInjectable({
id: "close-renderer-log-file",
instantiate: (di) => {
const winstonLogger = di.inject(winstonLoggerInjectable);
const fileLoggingTransport = di.inject(rendererFileLoggerTransportInjectable);
return () => {
fileLoggingTransport.close?.();
winstonLogger.remove(fileLoggingTransport);
};
},
});
export default closeRendererLogFileInjectable;

View File

@ -1,44 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { transports } from "winston";
import directoryForLogsInjectable from "../../common/app-paths/directory-for-logs.injectable";
import { loggerTransportInjectionToken } from "../../common/logger/transports";
import windowLocationInjectable from "../../common/k8s-api/window-location.injectable";
import currentlyInClusterFrameInjectable from "../routes/currently-in-cluster-frame.injectable";
import { getClusterIdFromHost } from "../utils";
const rendererFileLoggerTransportInjectable = getInjectable({
id: "renderer-file-logger-transport",
instantiate: (di) => {
let frameId: string;
const currentlyInClusterFrame = di.inject(
currentlyInClusterFrameInjectable,
);
if (currentlyInClusterFrame) {
const { host } = di.inject(windowLocationInjectable);
const clusterId = getClusterIdFromHost(host);
frameId = clusterId ? `cluster-${clusterId}` : "cluster";
} else {
frameId = "main";
}
return new transports.File({
handleExceptions: false,
level: "info",
filename: `lens-renderer-${frameId}.log`,
dirname: di.inject(directoryForLogsInjectable),
maxsize: 1024 * 1024,
maxFiles: 2,
tailable: true,
});
},
injectionToken: loggerTransportInjectionToken,
});
export default rendererFileLoggerTransportInjectable;

View File

@ -2,7 +2,7 @@
"name": "@k8slens/extensions",
"productName": "OpenLens extensions",
"description": "OpenLens - Open Source Kubernetes IDE: extensions",
"version": "6.4.0-beta.17",
"version": "6.4.0",
"copyright": "© 2022 OpenLens Authors",
"license": "MIT",
"main": "dist/extension-api.js",
@ -26,7 +26,7 @@
"prepare:dev": "yarn run build"
},
"dependencies": {
"@k8slens/core": "^6.4.0-beta.17"
"@k8slens/core": "^6.4.0"
},
"devDependencies": {
"@types/node": "^16.18.6",

View File

@ -4,7 +4,7 @@
"productName": "OpenLens",
"description": "OpenLens - Open Source IDE for Kubernetes",
"homepage": "https://github.com/lensapp/lens",
"version": "6.4.0-beta.17",
"version": "6.4.0",
"repository": {
"type": "git",
"url": "git+https://github.com/lensapp/lens.git"
@ -192,7 +192,7 @@
}
},
"dependencies": {
"@k8slens/core": "^6.4.0-beta.17",
"@k8slens/core": "^6.4.0",
"@k8slens/ensure-binaries": "^6.4.0-beta.16",
"@k8slens/generate-tray-icons": "^6.4.0-beta.16",
"@ogre-tools/fp": "^12.0.1",

View File

@ -1,6 +1,6 @@
{
"name": "@k8slens/release-tool",
"version": "6.4.0-beta.17",
"version": "6.4.0",
"description": "Release tool for lens monorepo",
"main": "dist/index.mjs",
"license": "MIT",

View File

@ -5,7 +5,7 @@
*/
import assert from "assert";
import chalk from "chalk";
import child_process from "child_process";
import child_process, { spawn } from "child_process";
import { readFile } from "fs/promises";
import inquirer from "inquirer";
import { createInterface, ReadLine } from "readline";
@ -16,9 +16,21 @@ type SemVer = semver.SemVer;
const { SemVer } = semver;
const exec = promisify(child_process.exec);
const spawn = promisify(child_process.spawn);
const execFile = promisify(child_process.execFile);
async function pipeExecFile(file: string, args: string[], opts?: { stdin: string }) {
const p = execFile(file, args);
p.child.stdout?.pipe(process.stdout);
p.child.stderr?.pipe(process.stderr);
if (opts) {
p.child.stdin?.end(opts.stdin);
}
await p;
}
interface GithubPrData {
author: {
login: string;
@ -65,9 +77,35 @@ async function fetchAllGitTags(): Promise<string[]> {
.map(line => line.trim());
}
async function bumpPackageVersions(): Promise<void> {
await spawn("npm", ["run", "bump-version"], {
stdio: "inherit",
function bumpPackageVersions() {
const bumpPackages = spawn("npm", ["run", "bump-version"], {
stdio: "inherit"
});
const cleaners: (() => void)[] = [
() => bumpPackages.stdout?.unpipe(),
() => bumpPackages.stderr?.unpipe(),
];
const cleanup = () => cleaners.forEach(clean => clean());
return new Promise<void>((resolve, reject) => {
const onExit = (code: number | null) => {
cleanup();
if (code) {
reject(new Error(`"npm run bump-version" failed with code ${code}`));
} else {
resolve();
}
};
const onError = (error: Error) => {
cleanup();
reject(error);
};
bumpPackages.once("error", onError);
cleaners.push(() => bumpPackages.off("error", onError));
bumpPackages.once("exit", onExit);
cleaners.push(() => bumpPackages.off("exit", onExit));
});
}
@ -110,20 +148,12 @@ function formatSemverForMilestone(version: SemVer): string {
async function createReleaseBranchAndCommit(prBase: string, version: SemVer, prBody: string): Promise<void> {
const prBranch = `release/v${version.format()}`;
await spawn("git", ["checkout", "-b", prBranch], {
stdio: "inherit",
});
await spawn("git", ["add", "lerna.json", "packages/*/package.json"], {
stdio: "inherit",
});
await spawn("git", ["commit", "-sm", `"Release ${version.format()}"`], {
stdio: "inherit",
});
await spawn("git", ["push", "--set-upstream", "origin", prBranch], {
stdio: "inherit",
});
await pipeExecFile("git", ["checkout", "-b", prBranch]);
await pipeExecFile("git", ["add", "lerna.json", "packages/*/package.json"]);
await pipeExecFile("git", ["commit", "-sm", `Release ${version.format()}`]);
await pipeExecFile("git", ["push", "--set-upstream", "origin", prBranch]);
await spawn("gh", [
await pipeExecFile("gh", [
"pr",
"create",
"--base", prBase,
@ -131,9 +161,9 @@ async function createReleaseBranchAndCommit(prBase: string, version: SemVer, prB
"--label", "skip-changelog",
"--label", "release",
"--milestone", formatSemverForMilestone(version),
"--body-file", prBody,
"--body-file", "-",
], {
stdio: "inherit"
stdin: prBody,
});
}
@ -153,7 +183,7 @@ function sortExtendedGithubPrData(left: ExtendedGithubPrData, right: ExtendedGit
}
async function getRelevantPRs(milestone: string, previousReleasedVersion: string): Promise<ExtendedGithubPrData[]> {
console.log("retreiving previous 500 PRs...");
console.log("retrieving previous 200 PRs...");
const getMergedPrsArgs = [
"gh",
@ -167,14 +197,14 @@ async function getRelevantPRs(milestone: string, previousReleasedVersion: string
const mergedPrs = JSON.parse((await exec(getMergedPrsArgs.join(" "), { encoding: "utf-8" })).stdout) as GithubPrData[];
const milestoneRelevantPrs = mergedPrs.filter(pr => pr.milestone?.title === milestone);
const relaventPrsQuery = await Promise.all(
const relevantPrsQuery = await Promise.all(
milestoneRelevantPrs.map(async pr => ({
pr,
stdout: (await exec(`git tag v${previousReleasedVersion} --no-contains ${pr.mergeCommit.oid}`)).stdout,
})),
);
return relaventPrsQuery
return relevantPrsQuery
.filter(query => query.stdout)
.map(query => query.pr)
.filter(pr => pr.labels.every(label => label.name !== "skip-changelog"))
@ -189,40 +219,11 @@ function formatPrEntry(pr: ExtendedGithubPrData) {
const isEnhancementPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === "enhancement");
const isBugfixPr = (pr: ExtendedGithubPrData) => pr.labels.some(label => label.name === "bug");
const cherrypickCommitWith = (rl: ReadLine) => async (commit: string) => {
const cherryPickCommitWith = (rl: ReadLine) => async (commit: string) => {
try {
const cherryPick = child_process.spawn("git", ["cherry-pick", commit]);
cherryPick.stdout.pipe(process.stdout);
cherryPick.stderr.pipe(process.stderr);
await new Promise<void>((resolve, reject) => {
const cleaners: (() => void)[] = [];
const cleanup = () => cleaners.forEach(cleaner => cleaner());
const onExit = (code: number | null) => {
if (code) {
reject(new Error(`git cherry-pick failed with exit code ${code}`));
cleanup();
}
resolve();
cleanup();
};
cherryPick.once("exit", onExit);
cleaners.push(() => cherryPick.off("exit", onExit));
const onError = (error: Error) => {
cleanup();
reject(error);
};
cherryPick.once("error", onError);
cleaners.push(() => cherryPick.off("error", onError));
});
await pipeExecFile("git", ["cherry-pick", commit]);
} catch {
console.error(chalk.bold("Please resolve conflicts in a seperate terminal and then press enter here..."));
console.error(chalk.bold("Please resolve conflicts in a separate terminal and then press enter here..."));
await new Promise<void>(resolve => rl.once("line", () => resolve()));
}
};
@ -249,7 +250,7 @@ async function pickWhichPRsToUse(prs: ExtendedGithubPrData[]): Promise<ExtendedG
function formatChangelog(previousReleasedVersion: string, prs: ExtendedGithubPrData[]): string {
const enhancementPrLines: string[] = [];
const bugPrLines: string[] = [];
const maintenencePrLines: string[] = [];
const maintenancePrLines: string[] = [];
for (const pr of prs) {
if (isEnhancementPr(pr)) {
@ -257,7 +258,7 @@ function formatChangelog(previousReleasedVersion: string, prs: ExtendedGithubPrD
} else if (isBugfixPr(pr)) {
bugPrLines.push(formatPrEntry(pr));
} else {
maintenencePrLines.push(formatPrEntry(pr));
maintenancePrLines.push(formatPrEntry(pr));
}
}
@ -271,9 +272,9 @@ function formatChangelog(previousReleasedVersion: string, prs: ExtendedGithubPrD
bugPrLines.push("");
}
if (maintenencePrLines.length > 0) {
maintenencePrLines.unshift("## 🧰 Maintenance", "");
maintenencePrLines.push("");
if (maintenancePrLines.length > 0) {
maintenancePrLines.unshift("## 🧰 Maintenance", "");
maintenancePrLines.push("");
}
return [
@ -281,22 +282,22 @@ function formatChangelog(previousReleasedVersion: string, prs: ExtendedGithubPrD
"",
...enhancementPrLines,
...bugPrLines,
...maintenencePrLines,
...maintenancePrLines,
].join("\n");
}
async function cherrypickCommits(prs: ExtendedGithubPrData[]): Promise<void> {
async function cherryPickCommits(prs: ExtendedGithubPrData[]): Promise<void> {
const rl = createInterface(process.stdin);
const cherrypickCommit = cherrypickCommitWith(rl);
const cherryPickCommit = cherryPickCommitWith(rl);
for (const pr of prs) {
await cherrypickCommit(pr.mergeCommit.oid);
await cherryPickCommit(pr.mergeCommit.oid);
}
rl.close();
}
async function pickRelaventPrs(prs: ExtendedGithubPrData[], isMasterBranch: boolean): Promise<ExtendedGithubPrData[]> {
async function pickRelevantPrs(prs: ExtendedGithubPrData[], isMasterBranch: boolean): Promise<ExtendedGithubPrData[]> {
if (isMasterBranch) {
return prs;
}
@ -307,7 +308,7 @@ async function pickRelaventPrs(prs: ExtendedGithubPrData[], isMasterBranch: bool
selectedPrs = await pickWhichPRsToUse(prs);
} while (selectedPrs.length === 0 && (console.warn("[WARNING]: must pick at least once commit"), true));
await cherrypickCommits(selectedPrs);
await cherryPickCommits(selectedPrs);
return selectedPrs;
}
@ -326,8 +327,8 @@ async function createRelease(): Promise<void> {
}
const prMilestone = formatSemverForMilestone(await getCurrentVersionOfSubPackage("core"));
const relaventPrs = await getRelevantPRs(prMilestone, previousReleasedVersion);
const selectedPrs = await pickRelaventPrs(relaventPrs, isMasterBranch);
const relevantPrs = await getRelevantPRs(prMilestone, previousReleasedVersion);
const selectedPrs = await pickRelevantPrs(relevantPrs, isMasterBranch);
const prBody = formatChangelog(previousReleasedVersion, selectedPrs);
if (!isMasterBranch) {