Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions core/cli/bin/run
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,17 @@ const argv = require('minimist')(process.argv.slice(2), {
})

const { rootLogger } = require('@dotcom-tool-kit/logger')
const { TelemetryProcess } = require('@dotcom-tool-kit/telemetry')
const { formatError } = require('../lib/messages')

async function main() {
const metricsProcess = new TelemetryProcess(rootLogger)
const environment = process.env.CI ? 'ci' : 'local'
const metrics = metricsProcess.root({ environment })
try {
if (argv.install) {
const installHooks = require('../lib/install').default
await installHooks(rootLogger)
await installHooks(rootLogger, metrics)
} else if (argv.listPlugins) {
const { listPlugins } = require('../lib')
await listPlugins(rootLogger)
Expand All @@ -25,7 +29,7 @@ async function main() {
await printConfig(rootLogger)
} else if (argv.printMergedOptions) {
const { printMergedOptions } = require('../lib')
await printMergedOptions(rootLogger)
await printMergedOptions(rootLogger, metrics)
} else if (argv.help || argv._.length === 0) {
const showHelp = require('../lib/help').default
await showHelp(rootLogger, argv._)
Expand All @@ -39,14 +43,16 @@ async function main() {
// the command becomes something like `dotcom-tool-kit test:staged --
// index.js`. When this command is executed it runs the configured task
// where the file path arguments would then be extracted.
await runCommands(rootLogger, argv._, argv['--'])
await runCommands(rootLogger, metrics, argv._, argv['--'])
} else {
await runCommands(rootLogger, argv._)
await runCommands(rootLogger, metrics, argv._)
}
}
} catch (error) {
rootLogger.error(formatError(error), { skipFormat: true })
process.exitCode = error.exitCode || 1
} finally {
metricsProcess.disconnect()
}
}

Expand Down
1 change: 1 addition & 0 deletions core/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@dotcom-tool-kit/plugin": "^2.0.0",
"@dotcom-tool-kit/schemas": "^1.9.0",
"@dotcom-tool-kit/state": "^5.0.0",
"@dotcom-tool-kit/telemetry": "^1.0.0",
"@dotcom-tool-kit/validated": "^2.0.0",
"endent": "^2.1.0",
"lodash": "^4.17.21",
Expand Down
8 changes: 6 additions & 2 deletions core/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type { Logger } from 'winston'
import util from 'util'
import { formatPluginTree } from './messages'
import { loadHookInstallations } from './install'
import { enableTelemetry } from './telemetry'
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import { TelemetryRecorder } from '@dotcom-tool-kit/telemetry'

export { runCommands } from './tasks'
export { shouldDisableNativeFetch } from './fetch'
Expand All @@ -22,9 +25,10 @@ export async function printConfig(logger: Logger): Promise<void> {
logger.info(util.inspect(config, { depth: null, colors: true }))
}

export async function printMergedOptions(logger: Logger): Promise<void> {
export async function printMergedOptions(logger: Logger, metrics: TelemetryRecorder): Promise<void> {
const config = await loadConfig(logger, { validate: true, root: process.cwd() })
const hookInstallations = (await loadHookInstallations(logger, config)).unwrap('invalid hooks')
enableTelemetry(metrics, config.pluginOptions['app root'].options as RootOptions)
const hookInstallations = (await loadHookInstallations(logger, metrics, config)).unwrap('invalid hooks')

const mergedOptions = {
hooks: hookInstallations.map((h) => h.options),
Expand Down
32 changes: 27 additions & 5 deletions core/cli/src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ import { loadConfig } from './config'
import { hasConfigChanged, updateHashes } from './config/hash'
import type { ValidConfig } from '@dotcom-tool-kit/config'
import { Hook, HookClass } from '@dotcom-tool-kit/base'
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import { Validated, invalid, reduceValidated, valid } from '@dotcom-tool-kit/validated'
import { HookModule, reducePluginHookInstallations } from './plugin/reduce-installations'
import { findConflicts, withoutConflicts } from '@dotcom-tool-kit/conflict'
import { formatUninstalledHooks } from './messages'
import { importEntryPoint } from './plugin/entry-point'
import { runInit } from './init'
import { guessSystemCode } from './systemCode'
import { enableTelemetry } from './telemetry'
import { TelemetryRecorder } from '@dotcom-tool-kit/telemetry'

// implementation of the Array#every method that supports asynchronous predicates
async function asyncEvery<T>(arr: T[], pred: (x: T) => Promise<boolean>): Promise<boolean> {
Expand Down Expand Up @@ -51,6 +55,7 @@ export const loadHookEntrypoints = async (

export const loadHookInstallations = async (
logger: Logger,
metrics: TelemetryRecorder,
config: ValidConfig
): Promise<Validated<Hook[]>> => {
const hookClassResults = await loadHookEntrypoints(logger, config)
Expand Down Expand Up @@ -83,17 +88,27 @@ export const loadHookInstallations = async (
return installationsWithoutConflicts.map((installations) => {
return installations.map(({ hookConstructor, forHook, options }) => {
const hookPlugin = config.hooks[forHook].plugin
return new hookConstructor(logger, forHook, options, config.pluginOptions[hookPlugin.id]?.options)
return new hookConstructor(
logger,
forHook,
options,
config.pluginOptions[hookPlugin.id]?.options,
metrics
)
})
})
}

export async function checkInstall(logger: Logger, config: ValidConfig): Promise<void> {
export async function checkInstall(
logger: Logger,
metrics: TelemetryRecorder,
config: ValidConfig
): Promise<void> {
if (!(await hasConfigChanged(logger, config))) {
return
}

const hooks = (await loadHookInstallations(logger, config)).unwrap(
const hooks = (await loadHookInstallations(logger, metrics, config)).unwrap(
'hooks were found to be invalid when checking install'
)

Expand All @@ -110,13 +125,20 @@ export async function checkInstall(logger: Logger, config: ValidConfig): Promise
await updateHashes(config)
}

export default async function installHooks(logger: Logger): Promise<ValidConfig> {
export default async function installHooks(logger: Logger, metrics: TelemetryRecorder): Promise<ValidConfig> {
const config = await loadConfig(logger, { root: process.cwd() })
enableTelemetry(metrics, config.pluginOptions['app root'].options as RootOptions)

await runInit(logger, config)

const systemCode = await guessSystemCode(config)
let scoped = metrics
if (systemCode) {
scoped = metrics.scoped({ systemCode })
}

const errors: Error[] = []
const hooks = (await loadHookInstallations(logger, config)).unwrap(
const hooks = (await loadHookInstallations(logger, scoped, config)).unwrap(
'hooks were found to be invalid when installing'
)

Expand Down
28 changes: 28 additions & 0 deletions core/cli/src/systemCode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { readFile } from 'node:fs/promises'

import type { ValidConfig } from '@dotcom-tool-kit/config'
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import type DopplerSchema from '@dotcom-tool-kit/doppler/src/schema'

import type * as z from 'zod'

export async function guessSystemCode(config: ValidConfig): Promise<string | undefined> {
const systemCodeFromRoot = (config.pluginOptions['app root']?.options as RootOptions).systemCode
if (systemCodeFromRoot) {
return systemCodeFromRoot
}

const systemCodeFromDoppler = (
config.pluginOptions['@dotcom-tool-kit/doppler']?.options as z.infer<typeof DopplerSchema>
).project
if (systemCodeFromDoppler && !systemCodeFromDoppler.startsWith('repo_')) {
return systemCodeFromDoppler
}

try {
const packageJson = JSON.parse(await readFile('package.json', 'utf8'))
if (packageJson.name) {
return `npm:${packageJson.name}`
}
} catch {}
}
47 changes: 39 additions & 8 deletions core/cli/src/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ import { styles } from '@dotcom-tool-kit/logger'
import { shouldDisableNativeFetch } from './fetch'
import { runInit } from './init'
import { formatInvalidOption } from './messages'
import { guessSystemCode } from './systemCode'
import { enableTelemetry } from './telemetry'
import { type TaskOptions, TaskSchemas } from '@dotcom-tool-kit/schemas'
import { OptionsForTask } from '@dotcom-tool-kit/plugin'
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import pluralize from 'pluralize'
import type { ReadonlyDeep } from 'type-fest'
import { type TelemetryRecorder, MockTelemetryClient } from '@dotcom-tool-kit/telemetry'

export type ErrorSummary = {
task: string
Expand All @@ -24,7 +27,11 @@ export type ErrorSummary = {
export async function loadTasks(
logger: Logger,
tasks: OptionsForTask[],
config: ReadonlyDeep<ValidConfig>
config: ReadonlyDeep<ValidConfig>,
// TODO:IM:20251215 make this a required parameter in the next major version.
// this function is currently used by the parallel plugin and so
// can't be changed yet.
metrics?: TelemetryRecorder
): Promise<Validated<Task[]>> {
const taskResults = await Promise.all(
tasks.map(async ({ task: taskId, options, plugin }) => {
Expand All @@ -42,12 +49,14 @@ export async function loadTasks(
}

if (parsedOptions.success) {
const scoped = metrics?.scoped({ plugin: entryPoint.plugin.id })
const task = new (taskModule.baseClass as unknown as TaskConstructor)(
logger,
taskId,
config.pluginOptions[entryPoint.plugin.id]?.options ?? {},
parsedOptions.data,
plugin
plugin,
scoped
)
return valid(task)
} else {
Expand All @@ -72,6 +81,7 @@ export function handleTaskErrors(errors: ErrorSummary[], command: string) {

export async function runTasks(
logger: Logger,
metrics: TelemetryRecorder,
config: ValidConfig,
tasks: Task[],
command: string,
Expand All @@ -84,9 +94,11 @@ export async function runTasks(
}

for (const task of tasks) {
const scoped = metrics.scoped({ task: task.id })
try {
logger.info(styles.taskHeader(`running ${styles.task(task.id)} task`))
await task.run({ files, command, cwd: config.root, config })
scoped.recordEvent('tasks.completed', { success: true })
} catch (error) {
// if there's an exit code, that's a request from the task to exit early
if (error instanceof ToolKitError && error.exitCode) {
Expand All @@ -99,6 +111,7 @@ export async function runTasks(
task: task.id,
error: error as Error
})
scoped.recordEvent('tasks.completed', { success: false })
}
}

Expand All @@ -111,10 +124,14 @@ export async function runCommandsFromConfig(
logger: Logger,
config: ValidConfig,
commands: string[],
files?: string[]
files?: string[],
// TODO:IM:20251215 make this a required parameter in the next major version.
// this function is currently used by the monorepo plugin and so can't be
// changed yet.
metrics: TelemetryRecorder = new MockTelemetryClient()
): Promise<void> {
await runInit(logger, config)
await checkInstall(logger, config)
await checkInstall(logger, metrics, config)

if (
shouldDisableNativeFetch(config.pluginOptions['app root'].options as RootOptions) &&
Expand All @@ -127,7 +144,8 @@ export async function runCommandsFromConfig(
await Promise.all(
commands.map(async (command) => {
const tasks = config.commandTasks[command]?.tasks ?? []
const validatedTaskInstances = await loadTasks(logger, tasks, config)
const scoped = metrics.scoped({ command })
const validatedTaskInstances = await loadTasks(logger, tasks, config, scoped)

return validatedTaskInstances.map((taskInstances) => ({ command, tasks: taskInstances }))
})
Expand All @@ -139,12 +157,25 @@ export async function runCommandsFromConfig(
Object.freeze(config)

for (const { command, tasks } of commandTasks) {
await runTasks(logger, config, tasks, command, files)
const scoped = metrics.scoped({ command })
await runTasks(logger, scoped, config, tasks, command, files)
}
}

export async function runCommands(logger: Logger, commands: string[], files?: string[]): Promise<void> {
export async function runCommands(
logger: Logger,
metrics: TelemetryRecorder,
commands: string[],
files?: string[]
): Promise<void> {
const config = await loadConfig(logger, { root: process.cwd() })
enableTelemetry(metrics, config.pluginOptions['app root'].options as RootOptions)

const systemCode = await guessSystemCode(config)
let scoped = metrics
if (systemCode) {
scoped = metrics.scoped({ systemCode })
}

return runCommandsFromConfig(logger, config, commands, files)
return runCommandsFromConfig(logger, config, commands, files, scoped)
}
11 changes: 11 additions & 0 deletions core/cli/src/telemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { RootOptions } from '@dotcom-tool-kit/plugin/src/root-schema'
import type { TelemetryProcess, TelemetryRecorder } from '@dotcom-tool-kit/telemetry'

// TODO:IM:20260113 in the future this type signature could be changed to allow
// enabling telemetry even without a valid config (and the root options derived
// from it,) so that we can track metrics related to invalid configurations.
export function enableTelemetry(metrics: TelemetryProcess | TelemetryRecorder, options: RootOptions) {
if (options.enableTelemetry) {
metrics.enable()
}
}
3 changes: 3 additions & 0 deletions core/cli/test/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -1702,6 +1702,7 @@ exports[`loadConfig should load a config from a root 1`] = `
},
"options": {
"allowNativeFetch": false,
"enableTelemetry": false,
},
"plugin": {
"children": [
Expand Down Expand Up @@ -4885,6 +4886,7 @@ exports[`loadConfig should load a config from a root and validate it 1`] = `
},
"options": {
"allowNativeFetch": false,
"enableTelemetry": false,
},
"plugin": {
"children": [
Expand Down Expand Up @@ -16502,6 +16504,7 @@ and pipeline.event.action != "closed"))
},
"options": {
"allowNativeFetch": false,
"enableTelemetry": false,
},
"plugin": {
"children": [
Expand Down
4 changes: 3 additions & 1 deletion core/cli/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ToolKitError } from '@dotcom-tool-kit/error'
import type { Valid } from '@dotcom-tool-kit/validated'
import type { Plugin } from '@dotcom-tool-kit/plugin'
import type { ValidPluginsConfig } from '@dotcom-tool-kit/config'
import { MockTelemetryClient } from '@dotcom-tool-kit/telemetry'
import { describe, expect, it, jest } from '@jest/globals'
import * as path from 'path'
import winston, { Logger } from 'winston'
Expand Down Expand Up @@ -137,7 +138,8 @@ describe('cli', () => {
resolvePlugin((plugin as Valid<Plugin>).value, validPluginConfig, logger)

const validConfig = validateConfig(validPluginConfig)
const hooks = await loadHookInstallations(logger, validConfig)
const metrics = new MockTelemetryClient()
const hooks = await loadHookInstallations(logger, metrics, validConfig)
expect(hooks.valid).toBe(true)
})
})
3 changes: 3 additions & 0 deletions core/cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
{
"path": "../../lib/state"
},
{
"path": "../../lib/telemetry"
},
{
"path": "../../lib/base"
}
Expand Down
1 change: 1 addition & 0 deletions lib/base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"dependencies": {
"@dotcom-tool-kit/conflict": "^2.0.0",
"@dotcom-tool-kit/logger": "^5.0.0",
"@dotcom-tool-kit/telemetry": "^1.0.0",
"@dotcom-tool-kit/validated": "^2.0.0",
"semver": "^7.7.3",
"winston": "^3.17.0"
Expand Down
Loading