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

GitHub Test Annotations #648

Merged
merged 40 commits into from
Nov 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5e0e896
initial commit
samchungy Nov 7, 2021
26ec1f0
chore: update comment
samchungy Nov 7, 2021
6728ced
refactor: change from map to array of objects
samchungy Nov 7, 2021
60778f1
feat: add unit test coverage
samchungy Nov 7, 2021
7ea56bf
feat: add displayName to Props
samchungy Nov 7, 2021
dddaed9
Create unlucky-walls-drop.md
72636c Nov 7, 2021
57fd67f
Merge branch 'master' of github.com:seek-oss/skuba into github-test-a…
samchungy Nov 11, 2021
7019418
resolve merge conflict
samchungy Nov 11, 2021
fbbc1a1
refactor: chage to inline export
samchungy Nov 11, 2021
726fb78
Apply suggestions from code review
samchungy Nov 11, 2021
5d7ce0e
Merge branches 'github-test-annotations' and 'github-test-annotations…
samchungy Nov 11, 2021
41b503a
feat: use for...of loop
samchungy Nov 11, 2021
5a99d7b
ci: use require resolve
samchungy Nov 11, 2021
2e95d4e
fix: remove unused dep
samchungy Nov 11, 2021
6ee6cb8
feat: switch to flatMap
samchungy Nov 11, 2021
f415137
ci: add displayName to int config
samchungy Nov 11, 2021
234f717
feat: add `projects` to jest mergePreset
samchungy Nov 11, 2021
a9a9332
docs: update documentation
samchungy Nov 11, 2021
152c737
Create ninety-pillows-move.md
72636c Nov 11, 2021
9c16734
Merge branch 'master' into github-test-annotations
72636c Nov 11, 2021
b858310
feat: warn on failure
samchungy Nov 15, 2021
6d40b34
Merge branch 'master' into github-test-annotations
samchungy Nov 15, 2021
396183f
docs: add Jest.mergePreset to project
samchungy Nov 16, 2021
fa79588
docs: attempt to make the displayName functionality clearer
samchungy Nov 16, 2021
0f28615
chore: remove typo
samchungy Nov 16, 2021
d2dfcdb
chore: nitpick docs
samchungy Nov 16, 2021
c79161a
chore: nitpick docs
samchungy Nov 16, 2021
d7f08e0
Align mock workdir
72636c Nov 16, 2021
8d402e8
Use some type imports for transitive deps
72636c Nov 16, 2021
3e3e7a3
Use nullish assignment
72636c Nov 16, 2021
7593d22
Merge branch 'master' into github-test-annotations
72636c Nov 16, 2021
d56e250
Placate new ESLint rule
72636c Nov 16, 2021
b5ac841
Document `JEST_LOCATION_REGEX`
72636c Nov 16, 2021
175def3
Merge branch 'master' into github-test-annotations
72636c Nov 16, 2021
cf12052
Split out a Jest deep dive topic
72636c Nov 16, 2021
8623e8f
Fix a missing reference
72636c Nov 16, 2021
5b32e4a
Link to deep dive
72636c Nov 16, 2021
5787434
Align lint annotater
72636c Nov 16, 2021
3ce557e
Refactor reporter
72636c Nov 16, 2021
ec0ce9e
Fix bad copypasta
72636c Nov 16, 2021
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
7 changes: 7 additions & 0 deletions .changeset/ninety-pillows-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"skuba": minor
---

test: Add GitHub check run annotations

`skuba test` can now automatically annotate GitHub commits when you [propagate CI environment variables and a GitHub API token](https://github.com/seek-oss/skuba/blob/master/docs/deep-dives/github.md#github-annotations). These annotations also appear inline with code under the “Files changed” tab in pull requests.
5 changes: 5 additions & 0 deletions .changeset/unlucky-walls-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"skuba": patch
---

Jest.mergePreset: Allow `displayName` and `projects`
2 changes: 1 addition & 1 deletion docs/cli/lint.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ you can limit this with the `--serial` flag.
`skuba lint` can automatically emit annotations in CI.

- [Buildkite annotations] are enabled when Buildkite environment variables and the `buildkite-agent` binary are present.
- [GitHub annotations] are enabled when Buildkite and GitHub environment variables are present.
- [GitHub annotations] are enabled when CI and GitHub environment variables are present.

[`skuba format`]: #skuba-format
[buildkite annotations]: ../deep-dives/buildkite.md#buildkite-annotations
Expand Down
17 changes: 17 additions & 0 deletions docs/cli/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,21 @@ Arguments are passed through to the Jest CLI:
skuba test --coverage path/to/file.test.ts
```

### Annotations

`skuba test` can automatically emit annotations in CI.

- [Buildkite annotations] are planned in future.
- [GitHub annotations] are enabled when CI and GitHub environment variables are present.

GitHub check runs are created with a default title of `skuba/test`.
You can further qualify this by providing a [displayName] in your Jest config;
for example, the display name `integration` will result in the title `skuba/test (integration)`.

See our [Jest guide] for a more detailed configuration breakdown.

[displayname]: https://jestjs.io/docs/configuration#displayname-string-object
[github annotations]: ../deep-dives/github.md#github-annotations
[jest]: https://jestjs.io
[jest guide]: ../deep-dives/jest.md
[projects]: https://jestjs.io/docs/configuration#projects-arraystring--projectconfig
4 changes: 1 addition & 3 deletions docs/deep-dives/github.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This topic details GitHub integration features baked into **skuba**.

## GitHub annotations

**skuba** can annotate the first 50 issues detected by [`skuba lint`] via the [GitHub Checks API].
**skuba** can annotate the first 50 issues detected by [`skuba lint`] and [`skuba test`] via the [GitHub Checks API].

This can be enabled by propagating Buildkite environment variables and a GitHub API token (at SEEK, this token can be configured through Build Agency).
For example, with the Docker plugin:
Expand Down Expand Up @@ -62,8 +62,6 @@ propagate the following environment variables to achieve the same effect:
- `GITHUB_RUN_NUMBER`
- `GITHUB_TOKEN`

This feature is also planned for [`skuba test`] in future.

**skuba**'s development API includes a [GitHub.createCheckRun] function.
You can use this to create your own check runs from other JavaScript code running in your CI workflow.

Expand Down
78 changes: 78 additions & 0 deletions docs/deep-dives/jest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
parent: Deep dives
---

# Jest

---

## Configuring a single test suite

In the typical case, your project has a single command to run all of its tests:

```console
skuba test --coverage
```

This works well out-of-the-box with the [annotation] features built in to [`skuba test`].

---

## Configuring multiple test suites

If you have multiple test suites in your project,
you may have multiple corresponding config files and commands:

- `skuba test --config jest.config.ts`
- `skuba test --config jest.config.int.ts`

In this scenario, declare unique [displayName]s so that [`skuba test`] can differentiate between the test suites when annotating your builds.

For example, if you set the following display names:

```typescript
// jest.config.ts
import { Jest } from 'skuba';

export default Jest.mergePreset({
displayName: 'unit',
testPathIgnorePatterns: ['\\.int\\.test\\.ts'],
});
```

```typescript
// jest.config.int.ts
import { Jest } from 'skuba';

export default Jest.mergePreset({
displayName: 'integration',
testPathIgnorePatterns: ['\\.unit\\.test\\.ts'],
});
```

**skuba** will generate two GitHub check runs titled `skuba/test (unit)` and `skuba/test (integration)` respectively.

Alternatively, you can declare multiple [projects] in a single config file:

```typescript
// jest.config.ts
import { Jest } from 'skuba';

export default Jest.mergePreset({
projects: [
Jest.mergePreset({
displayName: 'unit',
testPathIgnorePatterns: ['\\.int\\.test\\.ts'],
}),
Jest.mergePreset({
displayName: 'integration',
testPathIgnorePatterns: ['\\.unit\\.test\\.ts'],
}),
],
});
```

[`skuba test`]: ../cli/test.md
[annotation]: ../cli/test.md#annotations
[displayname]: https://jestjs.io/docs/configuration#displayname-string-object
[projects]: https://jestjs.io/docs/configuration#projects-arraystring--projectconfig
1 change: 1 addition & 0 deletions jest-preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ module.exports = {
'/node_modules.*/',
'<rootDir>/(coverage|dist|lib|tmp).*/',
],
reporters: ['default', require.resolve('./lib/cli/test/reporters/github')],
};
1 change: 1 addition & 0 deletions jest.config.int.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Jest } from './src';

export default Jest.mergePreset({
displayName: 'integration',
setupFiles: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['<rootDir>/template/', '/test\\.ts'],
watchPathIgnorePatterns: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"devDependencies": {
"@changesets/cli": "2.18.0",
"@changesets/get-github-info": "0.5.0",
"@jest/reporters": "27.3.1",
"@types/concurrently": "6.3.0",
"@types/ejs": "3.1.0",
"@types/express": "4.17.13",
Expand Down
2 changes: 2 additions & 0 deletions src/api/jest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ type Props = Pick<
| 'collectCoverageOnlyFrom'
| 'coveragePathIgnorePatterns'
| 'coverageThreshold'
| 'displayName'
| 'globals'
| 'globalSetup'
| 'globalTeardown'
| 'projects'
| 'setupFiles'
| 'setupFilesAfterEnv'
| 'snapshotSerializers'
Expand Down
3 changes: 1 addition & 2 deletions src/cli/lint/annotate/github/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ export const createGitHubAnnotations = async (
];

const isOk = eslint.ok && prettier.ok && tscOk;
const conclusion = isOk ? 'success' : 'failure';

const summary = isOk
? '`skuba lint` passed.'
Expand All @@ -40,7 +39,7 @@ export const createGitHubAnnotations = async (
name: 'skuba/lint',
summary,
annotations,
conclusion,
conclusion: isOk ? 'success' : 'failure',
title: `${build} ${isOk ? 'passed' : 'failed'}`,
});
};
4 changes: 1 addition & 3 deletions src/cli/test.ts → src/cli/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { run } from 'jest';

export const test = () => {
// This is usually set in `jest-cli`'s binary wrapper
if (process.env.NODE_ENV === undefined) {
process.env.NODE_ENV = 'test';
}
process.env.NODE_ENV ??= 'test';

const argv = process.argv.slice(2);

Expand Down
153 changes: 153 additions & 0 deletions src/cli/test/reporters/github/annotations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { SerializableError, TestResult } from '@jest/test-result';

import { createAnnotations } from './annotations';

jest.spyOn(process, 'cwd').mockReturnValue('/workdir/skuba');

it('should create annotations from Jest test results', () => {
const testResult = {
leaks: false,
numFailingTests: 1,
numPassingTests: 0,
numPendingTests: 0,
numTodoTests: 0,
openHandles: [],
perfStats: {
end: 1636247736632,
runtime: 2646,
slow: false,
start: 1636247733986,
},
skipped: false,
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0,
},
testFilePath: '/workdir/skuba/src/test.test.ts',
testResults: [
{
ancestorTitles: [],
duration: 3,
failureDetails: [
{
matcherResult: {
actual: 'b',
expected: 'a',
message:
'\u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m"a"\u001b[39m\nReceived: \u001b[31m"b"\u001b[39m',
name: 'toBe',
pass: false,
},
},
],
failureMessages: [
'Error: \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\nExpected: \u001b[32m"a"\u001b[39m\nReceived: \u001b[31m"b"\u001b[39m\n at Object.<anonymous> (/workdir/skuba/src/test.test.ts:2:15)\n at Promise.then.completed (/workdir/skuba/node_modules/jest-circus/build/utils.js:390:28)\n at new Promise (<anonymous>)\n at callAsyncCircusFn (/workdir/skuba/node_modules/jest-circus/build/utils.js:315:10)\n at _callCircusTest (/workdir/skuba/node_modules/jest-circus/build/run.js:218:40)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at _runTest (/workdir/skuba/node_modules/jest-circus/build/run.js:155:3)\n at _runTestsForDescribeBlock (/workdir/skuba/node_modules/jest-circus/build/run.js:66:9)\n at run (/workdir/skuba/node_modules/jest-circus/build/run.js:25:3)\n at runAndTransformResultsToJestFormat (/workdir/skuba/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:167:21)',
],
fullName: 'should output a',
invocations: 1,
location: null,
numPassingAsserts: 0,
status: 'failed',
title: 'should output a',
},
],
failureMessage:
"\u001b[1m\u001b[31m \u001b[1m● \u001b[22m\u001b[1mshould output a\u001b[39m\u001b[22m\n\n \u001b[2mexpect(\u001b[22m\u001b[31mreceived\u001b[39m\u001b[2m).\u001b[22mtoBe\u001b[2m(\u001b[22m\u001b[32mexpected\u001b[39m\u001b[2m) // Object.is equality\u001b[22m\n\n Expected: \u001b[32m\"a\"\u001b[39m\n Received: \u001b[31m\"b\"\u001b[39m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 1 |\u001b[39m it(\u001b[32m'should output a'\u001b[39m\u001b[33m,\u001b[39m () \u001b[33m=>\u001b[39m {\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[2m\u001b[39m\u001b[90m 2 |\u001b[39m expect(\u001b[32m'b'\u001b[39m)\u001b[33m.\u001b[39mtoBe(\u001b[32m'a'\u001b[39m)\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m |\u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[2m\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 3 |\u001b[39m })\u001b[33m;\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m \u001b[0m \u001b[90m 4 |\u001b[39m\u001b[0m\u001b[22m\n\u001b[2m\u001b[22m\n\u001b[2m \u001b[2mat Object.<anonymous> (\u001b[22m\u001b[2m\u001b[0m\u001b[36msrc/test.test.ts\u001b[39m\u001b[0m\u001b[2m:2:15)\u001b[22m\u001b[2m\u001b[22m\n",
} as TestResult;

const annotations = createAnnotations([testResult]);
expect(annotations).toStrictEqual([
{
annotation_level: 'failure',
path: 'src/test.test.ts',
start_line: 2,
end_line: 2,
start_column: 15,
end_column: 15,
message:
'Error: \x1B[2mexpect(\x1B[22m\x1B[31mreceived\x1B[39m\x1B[2m).\x1B[22mtoBe\x1B[2m(\x1B[22m\x1B[32mexpected\x1B[39m\x1B[2m) // Object.is equality\x1B[22m\n' +
'\n' +
'Expected: \x1B[32m"a"\x1B[39m\n' +
'Received: \x1B[31m"b"\x1B[39m\n' +
' at Object.<anonymous> (/workdir/skuba/src/test.test.ts:2:15)\n' +
' at Promise.then.completed (/workdir/skuba/node_modules/jest-circus/build/utils.js:390:28)\n' +
' at new Promise (<anonymous>)\n' +
' at callAsyncCircusFn (/workdir/skuba/node_modules/jest-circus/build/utils.js:315:10)\n' +
' at _callCircusTest (/workdir/skuba/node_modules/jest-circus/build/run.js:218:40)\n' +
' at processTicksAndRejections (node:internal/process/task_queues:96:5)\n' +
' at _runTest (/workdir/skuba/node_modules/jest-circus/build/run.js:155:3)\n' +
' at _runTestsForDescribeBlock (/workdir/skuba/node_modules/jest-circus/build/run.js:66:9)\n' +
' at run (/workdir/skuba/node_modules/jest-circus/build/run.js:25:3)\n' +
' at runAndTransformResultsToJestFormat (/workdir/skuba/node_modules/jest-circus/build/legacy-code-todo-rewrite/jestAdapterInit.js:167:21)',
title: 'Jest',
},
]);
});

it('should create annotations from Jest exec errors', () => {
const error = {
message:
"\x1B[96msrc/test.ts\x1B[0m:\x1B[93m1\x1B[0m:\x1B[93m1\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS6133: \x1B[0m'a' is declared but its value is never read.\n" +
'\n' +
"\x1B[7m1\x1B[0m import { a } from 'b';\n" +
'\x1B[7m \x1B[0m \x1B[91m~~~~~~~~~~~~~~~~~~~~~~\x1B[0m\n' +
"\x1B[96msrc/test.ts\x1B[0m:\x1B[93m1\x1B[0m:\x1B[93m19\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS2307: \x1B[0mCannot find module 'b' or its corresponding type declarations.\n" +
'\n' +
"\x1B[7m1\x1B[0m import { a } from 'b';\n" +
'\x1B[7m \x1B[0m \x1B[91m ~~~\x1B[0m',
} as SerializableError;
const testResult = {
failureMessage:
" \u001b[1m● \u001b[22mTest suite failed to run\n\n \u001b[96msrc/test.ts\u001b[0m:\u001b[93m1\u001b[0m:\u001b[93m1\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS6133: \u001b[0m'a' is declared but its value is never read.\n\n \u001b[7m1\u001b[0m import { a } from 'b';\n \u001b[7m \u001b[0m \u001b[91m~~~~~~~~~~~~~~~~~~~~~~\u001b[0m\n \u001b[96msrc/test.ts\u001b[0m:\u001b[93m1\u001b[0m:\u001b[93m19\u001b[0m - \u001b[91merror\u001b[0m\u001b[90m TS2307: \u001b[0mCannot find module 'b' or its corresponding type declarations.\n\n \u001b[7m1\u001b[0m import { a } from 'b';\n \u001b[7m \u001b[0m \u001b[91m ~~~\u001b[0m\n",
leaks: false,
numFailingTests: 0,
numPassingTests: 0,
numPendingTests: 0,
numTodoTests: 0,
openHandles: [],
perfStats: {
end: 0,
runtime: 0,
slow: false,
start: 0,
},
skipped: false,
snapshot: {
added: 0,
fileDeleted: false,
matched: 0,
unchecked: 0,
uncheckedKeys: [],
unmatched: 0,
updated: 0,
},
testExecError: error,
testFilePath: '/workdir/skuba/src/test.test.ts',
testResults: [],
} as TestResult;

const annotations = createAnnotations([testResult]);
expect(annotations).toStrictEqual([
{
annotation_level: 'failure',
path: 'src/test.test.ts',
start_line: 1,
end_line: 1,
message:
"\x1B[96msrc/test.ts\x1B[0m:\x1B[93m1\x1B[0m:\x1B[93m1\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS6133: \x1B[0m'a' is declared but its value is never read.\n" +
'\n' +
"\x1B[7m1\x1B[0m import { a } from 'b';\n" +
'\x1B[7m \x1B[0m \x1B[91m~~~~~~~~~~~~~~~~~~~~~~\x1B[0m\n' +
"\x1B[96msrc/test.ts\x1B[0m:\x1B[93m1\x1B[0m:\x1B[93m19\x1B[0m - \x1B[91merror\x1B[0m\x1B[90m TS2307: \x1B[0mCannot find module 'b' or its corresponding type declarations.\n" +
'\n' +
"\x1B[7m1\x1B[0m import { a } from 'b';\n" +
'\x1B[7m \x1B[0m \x1B[91m ~~~\x1B[0m',
title: 'Jest',
},
]);
});
Loading