diff --git a/packages/helpers/LICENSE.md b/packages/helpers/LICENSE.md new file mode 100644 index 0000000000..b6ddf8f7d2 --- /dev/null +++ b/packages/helpers/LICENSE.md @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright 2023 GitHub + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/helpers/__tests__/logger.test.ts b/packages/helpers/__tests__/logger.test.ts new file mode 100644 index 0000000000..55d5241264 --- /dev/null +++ b/packages/helpers/__tests__/logger.test.ts @@ -0,0 +1,397 @@ +import * as core from '@actions/core' +import {Logger} from '../src/logger' +import os from 'os' + +const logSpy = jest.spyOn(core, 'info') +const cnSpy = jest.spyOn(process.stdout, 'write') +const isDebugSpy = jest.spyOn(core, 'isDebug') + +const createCounter = + (initial = 0) => + () => + initial++ + +let count = createCounter(1) + +describe('logger', () => { + beforeEach(() => { + jest.resetModules() + count = createCounter(1) + + cnSpy.mockImplementation(() => { + // uncomment to debug + // process.stderr.write('write:' + line + '\n'); + return false + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it('should log a message', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.info('test') + + logger.startContext('test') + + logger.info('test') + + logger.endContext() + + expect(logSpy).toHaveBeenCalledTimes(2) + + expect(logSpy).toHaveBeenNthCalledWith(count(), 'test') + expect(logSpy).toHaveBeenNthCalledWith(count(), '[test] test') + }) + + it('should log different levels', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.error('test') + logger.warning('test') + logger.notice('test') + logger.info('test') + + expect(cnSpy).toHaveBeenCalledTimes(4) + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::error::test${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::warning::test${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::notice::test${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `test${os.EOL}`) + + isDebugSpy.mockReturnValue(true) + + logger.debug('test') + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::debug::test${os.EOL}`) + + isDebugSpy.mockReturnValue(false) + expect(cnSpy).toHaveBeenCalledTimes(5) + }) + + it('should not log messages above the log level', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.WARNING, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.error('test') + logger.warning('test') + logger.notice('test') + logger.info('test') + + expect(cnSpy).toHaveBeenCalledTimes(2) + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::error::test${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::warning::test${os.EOL}`) + }) + + it('should group messages', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.info('outside group') + logger.startGroup('group') + logger.info('within group') + logger.endGroup() + logger.info('outside group') + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `outside group${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::group::group${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `within group${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::endgroup::${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `outside group${os.EOL}`) + }) + + it('should group messages without group support', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput(false)] + }) + + logger.info('outside group') + logger.startGroup('group') + logger.info('within group') + logger.endGroup() + logger.info('outside group') + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `outside group${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[group] within group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `outside group${os.EOL}`) + }) + + it('supports complex grouping', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.startGroup('Hello group') + logger.info('Hello, world!') + logger.endGroup() + + logger.startContext('Outside context') + logger.info('Hello, world!') + + logger.startGroup('Group') + logger.startContext('Inside context') + logger.info('Hello, world!') + logger.startContext('Inside context 2') + logger.info('Hello, world!') + logger.endContext() + logger.info('Hello, world!') + logger.endGroup() + + logger.info('Hello, world!') + logger.endContext() + logger.info('Hello, world!') + + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `::group::Hello group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `Hello, world!${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::endgroup::${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Outside context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `::group::[Outside context] Group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Inside context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Inside context] [Inside context 2] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Inside context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::endgroup::${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Outside context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `Hello, world!${os.EOL}`) + }) + + it('supports locking contexts', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.info('Hello, world!') + + logger.withContextSync('Locked context', () => { + logger.info('Hello, world!') + + logger.startContext('Not locked context') + + logger.info('Hello, world!') + + // removes second context + logger.endContext() + + // has no effect + logger.endContext() + + logger.info('Hello, world!') + + logger.withContextSync('Locked context 2', () => { + logger.info('Hello, world!') + }) + + logger.info('Hello, world!') + }) + + logger.info('Hello, world!') + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `Hello, world!${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] [Not locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] [Locked context 2] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `Hello, world!${os.EOL}`) + }) + + it('supports locking contexts in async functions', async () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + logger.info('Hello, world!') + + await logger.withContext('Locked context', async () => { + logger.info('Hello, world!') + + logger.startContext('Not locked context') + + logger.info('Hello, world!') + + // removes second context + logger.endContext() + + // has no effect + logger.endContext() + + logger.info('Hello, world!') + + await logger.withContext('Locked context 2', () => { + logger.info('Hello, world!') + }) + + logger.info('Hello, world!') + }) + + logger.withGroupSync('Locked group', () => { + logger.info('Message in locked group') + }) + + expect(cnSpy).toHaveBeenNthCalledWith(count(), `Hello, world!${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] [Not locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] [Locked context 2] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Hello, world!${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `::group::Locked group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `Message in locked group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::endgroup::${os.EOL}`) + }) + + it('supports locking groups', async () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput()] + }) + + await logger.withGroup('Locked group', async () => { + logger.info('Message in locked group') + + await logger.withContext('Locked context', async () => { + logger.info('Message in locked group in locked context') + + await logger.withContext('Locked context 2', () => { + logger.info('Message in locked group in locked context 2') + }) + + // has no effect + logger.endContext() + + logger.info('Message in locked group in locked context') + }) + + // has no effect + logger.endGroup() + + logger.info('Message in locked group') + }) + + logger.info('Message outside locked group') + + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `::group::Locked group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `Message in locked group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Message in locked group in locked context${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] [Locked context 2] Message in locked group in locked context 2${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[Locked context] Message in locked group in locked context${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `Message in locked group${os.EOL}` + ) + expect(cnSpy).toHaveBeenNthCalledWith(count(), `::endgroup::${os.EOL}`) + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `Message outside locked group${os.EOL}` + ) + }) + + it('supports sequential group declaration', () => { + const logger = new Logger({ + level: Logger.LOG_LEVELS.INFO, + outputs: [new Logger.OUTPUTS.CoreOutput(false)] + }) + + logger.startGroup('group 1') + + logger.startContext('context 1') + + logger.startGroup('group 2') + + logger.info('Hello, world!') + + expect(cnSpy).toHaveBeenNthCalledWith( + count(), + `[group 2] Hello, world!${os.EOL}` + ) + }) +}) diff --git a/packages/helpers/package-lock.json b/packages/helpers/package-lock.json new file mode 100644 index 0000000000..3e83649421 --- /dev/null +++ b/packages/helpers/package-lock.json @@ -0,0 +1,72 @@ +{ + "name": "@actions/helpers", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@actions/helpers", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/exec": "^1.1.1" + }, + "devDependencies": { + "@types/node": "^20.5.0" + } + }, + "node_modules/@actions/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", + "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", + "dependencies": { + "@actions/http-client": "^2.0.1", + "uuid": "^8.3.2" + } + }, + "node_modules/@actions/exec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "dependencies": { + "@actions/io": "^1.0.1" + } + }, + "node_modules/@actions/http-client": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz", + "integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==", + "dependencies": { + "tunnel": "^0.0.6" + } + }, + "node_modules/@actions/io": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", + "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + }, + "node_modules/@types/node": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.5.0.tgz", + "integrity": "sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==", + "dev": true + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + } + } +} diff --git a/packages/helpers/package.json b/packages/helpers/package.json new file mode 100644 index 0000000000..c4a5448611 --- /dev/null +++ b/packages/helpers/package.json @@ -0,0 +1,46 @@ +{ + "name": "@actions/helpers", + "version": "1.0.0", + "description": "Helpers for creating actions", + "main": "lib/helpers.js", + "types": "lib/helpers.d.ts", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib", + "!.DS_Store" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json", + "test": "echo \"Error: run tests from root\" && exit 1", + "tsc": "tsc -p tsconfig.json" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/actions/toolkit.git", + "directory": "packages/helpers" + }, + "keywords": [ + "github", + "actions", + "helpers" + ], + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/actions/toolkit/issues" + }, + "homepage": "https://github.com/actions/toolkit#readme", + "devDependencies": { + "@types/node": "^20.5.0" + }, + "dependencies": { + "@actions/core": "^1.10.0", + "@actions/exec": "^1.1.1" + } +} diff --git a/packages/helpers/src/helpers.ts b/packages/helpers/src/helpers.ts new file mode 100644 index 0000000000..648a345eae --- /dev/null +++ b/packages/helpers/src/helpers.ts @@ -0,0 +1 @@ +import './logger' diff --git a/packages/helpers/src/logger/index.ts b/packages/helpers/src/logger/index.ts new file mode 100644 index 0000000000..a2f75f33e5 --- /dev/null +++ b/packages/helpers/src/logger/index.ts @@ -0,0 +1 @@ +export {Logger} from './logger' diff --git a/packages/helpers/src/logger/logger.ts b/packages/helpers/src/logger/logger.ts new file mode 100644 index 0000000000..fe2c788947 --- /dev/null +++ b/packages/helpers/src/logger/logger.ts @@ -0,0 +1,209 @@ +import {AnnotationProperties} from '@actions/core' +import {LogContext, LogLevel, Output} from './types' +import {LOG_LEVELS} from './static' +import {CoreOutput} from './outputs' + +export class Logger { + static OUTPUTS = { + CoreOutput + } + + static LOG_LEVELS = { + [LOG_LEVELS[0]]: 0, + [LOG_LEVELS[1]]: 1, + [LOG_LEVELS[2]]: 2, + [LOG_LEVELS[3]]: 3 + } as const + + private logLevel: LogLevel = Logger.LOG_LEVELS.INFO + private contexts: LogContext[] = [] + private outputs: Output[] = [] + + private activeGroup = -1 + private lockedContexts: number[] = [] + private groupLocked = false + + constructor(options: {level: LogLevel; outputs: Output[]}) { + this.logLevel = options.level + this.outputs = options.outputs + } + + startContext = (title: string): void => { + this.contexts.push({value: title, isGroup: false}) + + for (const output of this.outputs) { + output.update({ + contexts: this.contexts, + activeGroup: this.activeGroup, + type: 'context-start', + title + }) + } + } + + startGroup = (title: string): void => { + if (this.groupLocked) return + + if (this.activeGroup !== -1) { + this.contexts.splice(this.activeGroup) + } + + this.contexts.push({value: title, isGroup: true}) + this.activeGroup = this.contexts.length - 1 + + for (const output of this.outputs) { + output.update({ + contexts: this.contexts, + activeGroup: this.activeGroup, + type: 'group-start', + title + }) + } + } + + endContext = (): void => { + const lockedContext = this.lockedContexts.at(-1) + + if ( + lockedContext !== undefined && + this.contexts.length - 1 <= lockedContext + ) { + return + } + + const latestContext = this.contexts.at(-1) + + if (latestContext === undefined) return + if (latestContext.isGroup) return + + this.contexts.pop() + + for (const output of this.outputs) { + output.update({ + contexts: this.contexts, + activeGroup: this.activeGroup, + type: 'context-end' + }) + } + } + + endGroup = (): void => { + if (this.groupLocked) return + if (this.activeGroup === -1) return + + this.contexts.splice(this.activeGroup) + this.activeGroup = -1 + + for (const output of this.outputs) { + output.update({ + contexts: this.contexts, + activeGroup: this.activeGroup, + type: 'group-end' + }) + } + } + + async withContext(title: string, fn: () => T): Promise { + this.startContext(title) + this.lockedContexts.push(this.contexts.length - 1) + + try { + return await new Promise(resolve => resolve(fn())) + } finally { + this.contexts.splice((this.lockedContexts.pop() ?? -1) + 1) + this.endContext() + } + } + + withContextSync(title: string, fn: () => T): T { + this.startContext(title) + this.lockedContexts.push(this.contexts.length - 1) + + try { + return fn() + } finally { + this.contexts.splice((this.lockedContexts.pop() ?? -1) + 1) + this.endContext() + } + } + + async withGroup(title: string, fn: () => T): Promise { + if (this.groupLocked) return await new Promise(() => fn()) + + this.startGroup(title) + this.groupLocked = true + + try { + return await new Promise(resolve => resolve(fn())) + } finally { + this.groupLocked = false + this.endGroup() + } + } + + withGroupSync(title: string, fn: () => T): T { + if (this.groupLocked) return fn() + + this.startGroup(title) + this.groupLocked = true + + try { + return fn() + } finally { + this.groupLocked = false + this.endGroup() + } + } + + log = ( + level: LogLevel, + message: string | Error, + properties?: AnnotationProperties + ): void => { + if (level > this.logLevel) return + + for (const output of this.outputs) { + output.message({ + contexts: this.contexts, + activeGroup: this.activeGroup, + level, + payload: message, + properties + }) + } + } + + debug = (message: string): void => { + for (const output of this.outputs) { + output.message({ + contexts: this.contexts, + activeGroup: this.activeGroup, + level: 0, + payload: message, + isDebug: true + }) + } + } + + error = ( + message: string | Error, + properties?: AnnotationProperties + ): void => { + this.log(Logger.LOG_LEVELS.ERROR, message, properties) + } + + warning = ( + message: string | Error, + properties?: AnnotationProperties + ): void => { + this.log(Logger.LOG_LEVELS.WARNING, message, properties) + } + + notice = (message: string): void => { + this.log(Logger.LOG_LEVELS.NOTICE, message) + } + + info = (message: string): void => { + this.log(Logger.LOG_LEVELS.INFO, message) + } +} diff --git a/packages/helpers/src/logger/outputs.ts b/packages/helpers/src/logger/outputs.ts new file mode 100644 index 0000000000..b7c0013395 --- /dev/null +++ b/packages/helpers/src/logger/outputs.ts @@ -0,0 +1,84 @@ +import * as core from '@actions/core' +import {Output} from './types' +import {Logger} from './logger' + +export class CoreOutput implements Output { + constructor(private enableGrouping = true) {} + + message: Output['message'] = ({ + contexts, + activeGroup, + level, + payload, + properties, + isDebug + }) => { + let prefix = '' + + if (this.enableGrouping) { + for (let i = activeGroup + 1; i < contexts.length; i++) { + prefix += `[${contexts[i].value.toString()}] ` + } + } else { + for (const context of contexts) { + prefix += `[${context.value.toString()}] ` + } + } + + const output = `${prefix}${payload.toString()}`.trim() + + if (isDebug) { + core.debug(output) + + return + } + + if (level === Logger.LOG_LEVELS.INFO) { + core.info(output) + + return + } + + if (level === Logger.LOG_LEVELS.NOTICE) { + core.notice(output) + + return + } + + if (level === Logger.LOG_LEVELS.WARNING) { + core.warning(output, properties) + + return + } + + if (level === Logger.LOG_LEVELS.ERROR) { + core.error(output, properties) + + return + } + } + + update: Output['update'] = ({contexts, type, title}) => { + if (type === 'group-start' && this.enableGrouping) { + let prefix = '' + + for (const context of contexts) { + if (context.isGroup) { + break + } + + prefix += `[${context.value.toString()}] ` + } + + core.startGroup((prefix + (title ?? '')).trim()) + + return + } + + if (type === 'group-end' && this.enableGrouping) { + core.endGroup() + + return + } + } +} diff --git a/packages/helpers/src/logger/static.ts b/packages/helpers/src/logger/static.ts new file mode 100644 index 0000000000..29744e9ddf --- /dev/null +++ b/packages/helpers/src/logger/static.ts @@ -0,0 +1 @@ +export const LOG_LEVELS = ['ERROR', 'WARNING', 'NOTICE', 'INFO'] as const diff --git a/packages/helpers/src/logger/types.ts b/packages/helpers/src/logger/types.ts new file mode 100644 index 0000000000..d29aef9b84 --- /dev/null +++ b/packages/helpers/src/logger/types.ts @@ -0,0 +1,26 @@ +import {AnnotationProperties} from '@actions/core' + +export type LogLevel = 0 | 1 | 2 | 3 + +export type LogContext = { + value: string | Error + isGroup: boolean +} + +export interface Output { + message: (messageData: { + contexts: LogContext[] + activeGroup: number + level: LogLevel + payload: string | Error + properties?: AnnotationProperties + isDebug?: boolean + }) => void + + update: (updateData: { + contexts: LogContext[] + activeGroup: number + type: 'context-start' | 'group-start' | 'context-end' | 'group-end' + title?: string + }) => void +} diff --git a/packages/helpers/tsconfig.json b/packages/helpers/tsconfig.json new file mode 100644 index 0000000000..3bb20cf8eb --- /dev/null +++ b/packages/helpers/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./lib", + "declaration": true, + "rootDir": "./src" + }, + "include": [ + "./src" + ] +} \ No newline at end of file