Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add notice annotation and support more annotation fields #855

Merged
merged 9 commits into from Jul 28, 2021
Merged
Binary file added docs/assets/annotations.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/problem-matchers.md
Expand Up @@ -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)

Expand Down
50 changes: 50 additions & 0 deletions packages/core/README.md
Expand Up @@ -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`);
Expand All @@ -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).
luketomlinson marked this conversation as resolved.
Show resolved Hide resolved
```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
luketomlinson marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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.
Expand Down
47 changes: 47 additions & 0 deletions packages/core/__tests__/core.test.ts
Expand Up @@ -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 */

Expand Down Expand Up @@ -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}`])
Expand All @@ -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}`])
Expand Down
78 changes: 71 additions & 7 deletions packages/core/src/core.ts
@@ -1,6 +1,6 @@
import {issue, issueCommand} from './command'
import {issue, issueCommand} from './internal-command'
import {issueCommand as issueFileCommand} from './file-command'
import {toCommandValue} from './utils'
import {toCommandProperties, toCommandValue} from './internal-utils'

import * as os from 'os'
import * as path from 'path'
Expand Down Expand Up @@ -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
//-----------------------------------------------------------------------
Expand Down Expand Up @@ -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(
thboop marked this conversation as resolved.
Show resolved Hide resolved
message: string | Error,
properties: AnnotationProperties = {}
): void {
issueCommand(
thboop marked this conversation as resolved.
Show resolved Hide resolved
'notice',
toCommandProperties(properties),
message instanceof Error ? message.toString() : message
)
}

/**
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/file-command.ts
Expand Up @@ -5,7 +5,7 @@

import * as fs from 'fs'
import * as os from 'os'
import {toCommandValue} from './utils'
import {toCommandValue} from './internal-utils'

export function issueCommand(command: string, message: any): void {
const filePath = process.env[`GITHUB_${command}`]
Expand Down
@@ -1,12 +1,12 @@
import * as os from 'os'
import {toCommandValue} from './utils'
import {toCommandValue} from './internal-utils'

// For internal use, subject to change.

// We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */

interface CommandProperties {
export interface CommandProperties {
[key: string]: any
}

Expand Down
40 changes: 40 additions & 0 deletions packages/core/src/internal-utils.ts
@@ -0,0 +1,40 @@
// We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */

import {AnnotationProperties} from './core'
import {CommandProperties} from './internal-command'

/**
* Sanitizes an input into a string so it can be passed into issueCommand safely
* @param input input to sanitize into a string
*/
export function toCommandValue(input: any): string {
if (input === null || input === undefined) {
return ''
} else if (typeof input === 'string' || input instanceof String) {
return input as 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
}
}
15 changes: 0 additions & 15 deletions packages/core/src/utils.ts

This file was deleted.