diff --git a/.changeset/group-align-deps-ci.md b/.changeset/group-align-deps-ci.md new file mode 100644 index 0000000000..7d6d6b631f --- /dev/null +++ b/.changeset/group-align-deps-ci.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/align-deps": patch +--- + +Group align-deps package output in GitHub Actions and Azure DevOps logs. diff --git a/packages/align-deps/src/ci.ts b/packages/align-deps/src/ci.ts new file mode 100644 index 0000000000..08115668be --- /dev/null +++ b/packages/align-deps/src/ci.ts @@ -0,0 +1,79 @@ +type GroupMarkers = { + beginGroup: (title: string) => void; + endGroup: () => void; +}; + +type Output = Pick; + +function noop() { + return undefined; +} + +export function getGroupMarkers( + log: typeof console.log = console.log, + env = process.env +): GroupMarkers { + if (env["GITHUB_ACTIONS"] === "true") { + return { + beginGroup: (title) => log(`::group::${encodeURI(title)}`), + endGroup: () => log("::endgroup::"), + }; + } + + if (env["TF_BUILD"] === "True") { + return { + beginGroup: (title) => log(`##[group]${title}`), + endGroup: () => log("##[endgroup]"), + }; + } + + return { + beginGroup: noop, + endGroup: noop, + }; +} + +export function withLogGroup( + title: string, + fn: () => T, + output: Output = console, + env = process.env +): T { + const originalLog = output.log; + const originalWarn = output.warn; + const originalError = output.error; + const { beginGroup, endGroup } = getGroupMarkers(originalLog, env); + let groupStarted = false; + + const begin = () => { + if (!groupStarted) { + beginGroup(title); + groupStarted = true; + } + }; + + output.log = (...args) => { + begin(); + originalLog(...args); + }; + output.warn = (...args) => { + begin(); + originalWarn(...args); + }; + output.error = (...args) => { + begin(); + originalError(...args); + }; + + try { + return fn(); + } finally { + output.log = originalLog; + output.warn = originalWarn; + output.error = originalError; + + if (groupStarted) { + endGroup(); + } + } +} diff --git a/packages/align-deps/src/cli.ts b/packages/align-deps/src/cli.ts index c8067647b0..812504d19a 100644 --- a/packages/align-deps/src/cli.ts +++ b/packages/align-deps/src/cli.ts @@ -9,6 +9,7 @@ import { } from "@rnx-kit/tools-workspaces"; import * as fs from "node:fs"; import * as path from "node:path"; +import { withLogGroup } from "./ci.ts"; import { makeCheckCommand } from "./commands/check.ts"; import { makeExportCatalogsCommand } from "./commands/exportCatalogs.ts"; import { makeInitializeCommand } from "./commands/initialize.ts"; @@ -266,21 +267,23 @@ export async function cli({ packages, ...args }: Args): Promise { // disk only when everything is in order for the target package. Packages with // invalid or missing configurations are skipped. const errors = manifests.reduce((errors, manifest) => { - try { - const result = command(manifest); - printError(manifest, result); - if (result !== "success" && result !== "excluded") { - return errors + 1; - } - } catch (e) { - if (hasProperty(e, "message")) { - error(`${manifest}: ${e.message}`); - return errors + 1; - } + return withLogGroup(manifest, () => { + try { + const result = command(manifest); + printError(manifest, result); + if (result !== "success" && result !== "excluded") { + return errors + 1; + } + } catch (e) { + if (hasProperty(e, "message")) { + error(`${manifest}: ${e.message}`); + return errors + 1; + } - throw e; - } - return errors; + throw e; + } + return errors; + }); }, 0); process.exitCode = errors; diff --git a/packages/align-deps/test/ci.test.ts b/packages/align-deps/test/ci.test.ts new file mode 100644 index 0000000000..b93dc985e2 --- /dev/null +++ b/packages/align-deps/test/ci.test.ts @@ -0,0 +1,113 @@ +import { deepEqual, equal } from "node:assert/strict"; +import { describe, it } from "node:test"; +import { getGroupMarkers, withLogGroup } from "../src/ci.ts"; + +describe("getGroupMarkers()", () => { + it("returns GitHub Actions group markers", () => { + const output: string[] = []; + const { beginGroup, endGroup } = getGroupMarkers( + (message) => output.push(message), + { GITHUB_ACTIONS: "true" } + ); + + beginGroup("packages/app/package.json"); + endGroup(); + + deepEqual(output, ["::group::packages/app/package.json", "::endgroup::"]); + }); + + it("encodes GitHub Actions group titles", () => { + const output: string[] = []; + const { beginGroup, endGroup } = getGroupMarkers( + (message) => output.push(message), + { GITHUB_ACTIONS: "true" } + ); + + beginGroup("packages/%app\r\n/package.json"); + endGroup(); + + deepEqual(output, [ + "::group::packages/%25app%0D%0A/package.json", + "::endgroup::", + ]); + }); + + it("returns Azure DevOps group markers", () => { + const output: string[] = []; + const { beginGroup, endGroup } = getGroupMarkers( + (message) => output.push(message), + { TF_BUILD: "True" } + ); + + beginGroup("packages/app/package.json"); + endGroup(); + + deepEqual(output, ["##[group]packages/app/package.json", "##[endgroup]"]); + }); + + it("does not group local output", () => { + const output: string[] = []; + const { beginGroup, endGroup } = getGroupMarkers( + (message) => output.push(message), + {} + ); + + beginGroup("packages/app/package.json"); + endGroup(); + + deepEqual(output, []); + }); +}); + +describe("withLogGroup()", () => { + function createConsole() { + const output: string[] = []; + + return { + output, + console: { + log: (...args: unknown[]) => output.push(args.join(" ")), + warn: (...args: unknown[]) => output.push(args.join(" ")), + error: (...args: unknown[]) => output.push(args.join(" ")), + }, + }; + } + + it("does not emit an empty group", () => { + const { console, output } = createConsole(); + + withLogGroup("packages/app/package.json", () => undefined, console, { + GITHUB_ACTIONS: "true", + }); + + deepEqual(output, []); + }); + + it("groups the first emitted log line", () => { + const { console, output } = createConsole(); + + withLogGroup( + "packages/app/package.json", + () => console.log("hello"), + console, + { GITHUB_ACTIONS: "true" } + ); + + deepEqual(output, [ + "::group::packages/app/package.json", + "hello", + "::endgroup::", + ]); + }); + + it("restores the console methods", () => { + const { console } = createConsole(); + const log = console.log; + + withLogGroup("packages/app/package.json", () => undefined, console, { + GITHUB_ACTIONS: "true", + }); + + equal(console.log, log); + }); +});