diff --git a/docs/assets/annotations.png b/docs/assets/annotations.png new file mode 100644 index 0000000000..f210022090 Binary files /dev/null and b/docs/assets/annotations.png differ diff --git a/docs/problem-matchers.md b/docs/problem-matchers.md index 520d26bae5..e496b299bb 100644 --- a/docs/problem-matchers.md +++ b/docs/problem-matchers.md @@ -6,7 +6,7 @@ Problem Matchers are a way to scan the output of actions for a specified regex p Currently, GitHub Actions limit the annotation count in a workflow run. -- 10 warning annotations and 10 error annotations per step +- 10 warning annotations, 10 error annotations, and 10 notice annotations per step - 50 annotations per job (sum of annotations from all the steps) - 50 annotations per run (separate from the job annotations, these annotations aren’t created by users) diff --git a/packages/core/README.md b/packages/core/README.md index deffaa5d87..6681502d3e 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -92,6 +92,8 @@ try { // Do stuff core.info('Output to the actions build log') + + core.notice('This is a message that will also emit an annotation') } catch (err) { core.error(`Error ${err}, action may still succeed though`); @@ -115,6 +117,54 @@ const result = await core.group('Do something async', async () => { }) ``` +#### Annotations + +This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run). +```js +core.error('This is a bad error. This will also fail the build.') + +core.warning('Something went wrong, but it\'s not bad enough to fail the build.') + +core.notice('Something happened that you might want to know about.') +``` + +These will surface to the UI in the Actions page and on Pull Requests. They look something like this: + +![Annotations Image](../../docs/assets/annotations.png) + +These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring. + +These options are: +```typescript +export interface AnnotationProperties { + /** + * A title for the annotation. + */ + title?: string + + /** + * The start line for the annotation. + */ + startLine?: number + + /** + * The end line for the annotation. Defaults to `startLine` when `startLine` is provided. + */ + endLine?: number + + /** + * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. + */ + startColumn?: number + + /** + * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. + * Defaults to `startColumn` when `startColumn` is provided. + */ + endColumn?: number +} +``` + #### Styling output Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported. diff --git a/packages/core/__tests__/core.test.ts b/packages/core/__tests__/core.test.ts index 5be8df4e8f..f5ccec3b29 100644 --- a/packages/core/__tests__/core.test.ts +++ b/packages/core/__tests__/core.test.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' import * as core from '../src/core' +import {toCommandProperties} from '../src/utils' /* eslint-disable @typescript-eslint/unbound-method */ @@ -269,6 +270,20 @@ describe('@actions/core', () => { assertWriteCalls([`::error::Error: ${message}${os.EOL}`]) }) + it('error handles parameters correctly', () => { + const message = 'this is my error message' + core.error(new Error(message), { + title: 'A title', + startColumn: 1, + endColumn: 2, + startLine: 5, + endLine: 5 + }) + assertWriteCalls([ + `::error title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}` + ]) + }) + it('warning sets the correct message', () => { core.warning('Warning') assertWriteCalls([`::warning::Warning${os.EOL}`]) @@ -285,6 +300,38 @@ describe('@actions/core', () => { assertWriteCalls([`::warning::Error: ${message}${os.EOL}`]) }) + it('warning handles parameters correctly', () => { + const message = 'this is my error message' + core.warning(new Error(message), { + title: 'A title', + startColumn: 1, + endColumn: 2, + startLine: 5, + endLine: 5 + }) + assertWriteCalls([ + `::warning title=A title,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}` + ]) + }) + + it('annotations map field names correctly', () => { + const commandProperties = toCommandProperties({ + title: 'A title', + startColumn: 1, + endColumn: 2, + startLine: 5, + endLine: 5 + }) + expect(commandProperties.title).toBe('A title') + expect(commandProperties.col).toBe(1) + expect(commandProperties.endColumn).toBe(2) + expect(commandProperties.line).toBe(5) + expect(commandProperties.endLine).toBe(5) + + expect(commandProperties.startColumn).toBeUndefined() + expect(commandProperties.startLine).toBeUndefined() + }) + it('startGroup starts a new group', () => { core.startGroup('my-group') assertWriteCalls([`::group::my-group${os.EOL}`]) diff --git a/packages/core/src/command.ts b/packages/core/src/command.ts index 05e1261ab3..2796fce9f6 100644 --- a/packages/core/src/command.ts +++ b/packages/core/src/command.ts @@ -6,7 +6,7 @@ import {toCommandValue} from './utils' // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ -interface CommandProperties { +export interface CommandProperties { [key: string]: any } diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index c5c36fa4f5..e57d9f1549 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -1,6 +1,6 @@ import {issue, issueCommand} from './command' import {issueCommand as issueFileCommand} from './file-command' -import {toCommandValue} from './utils' +import {toCommandProperties, toCommandValue} from './utils' import * as os from 'os' import * as path from 'path' @@ -31,6 +31,38 @@ export enum ExitCode { Failure = 1 } +/** + * Optional properties that can be sent with annotatation commands (notice, error, and warning) + * See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations. + */ +export interface AnnotationProperties { + /** + * A title for the annotation. + */ + title?: string + + /** + * The start line for the annotation. + */ + startLine?: number + + /** + * The end line for the annotation. Defaults to `startLine` when `startLine` is provided. + */ + endLine?: number + + /** + * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. + */ + startColumn?: number + + /** + * The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values. + * Defaults to `startColumn` when `startColumn` is provided. + */ + endColumn?: number +} + //----------------------------------------------------------------------- // Variables //----------------------------------------------------------------------- @@ -199,17 +231,49 @@ export function debug(message: string): void { /** * Adds an error issue * @param message error issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. */ -export function error(message: string | Error): void { - issue('error', message instanceof Error ? message.toString() : message) +export function error( + message: string | Error, + properties: AnnotationProperties = {} +): void { + issueCommand( + 'error', + toCommandProperties(properties), + message instanceof Error ? message.toString() : message + ) } /** - * Adds an warning issue + * Adds a warning issue * @param message warning issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. */ -export function warning(message: string | Error): void { - issue('warning', message instanceof Error ? message.toString() : message) +export function warning( + message: string | Error, + properties: AnnotationProperties = {} +): void { + issueCommand( + 'warning', + toCommandProperties(properties), + message instanceof Error ? message.toString() : message + ) +} + +/** + * Adds a notice issue + * @param message notice issue message. Errors will be converted to string via toString() + * @param properties optional properties to add to the annotation. + */ +export function notice( + message: string | Error, + properties: AnnotationProperties = {} +): void { + issueCommand( + 'notice', + toCommandProperties(properties), + message instanceof Error ? message.toString() : message + ) } /** diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 2f1d60ceb3..8596b5f41c 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,6 +1,9 @@ // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ +import {AnnotationProperties} from './core' +import {CommandProperties} from './command' + /** * Sanitizes an input into a string so it can be passed into issueCommand safely * @param input input to sanitize into a string @@ -13,3 +16,25 @@ export function toCommandValue(input: any): string { } return JSON.stringify(input) } + +/** + * + * @param annotationProperties + * @returns The command properties to send with the actual annotation command + * See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646 + */ +export function toCommandProperties( + annotationProperties: AnnotationProperties +): CommandProperties { + if (!Object.keys(annotationProperties).length) { + return {} + } + + return { + title: annotationProperties.title, + line: annotationProperties.startLine, + endLine: annotationProperties.endLine, + col: annotationProperties.startColumn, + endColumn: annotationProperties.endColumn + } +}