Skip to content

Commit ab8c40c

Browse files
authored
Show message if permissions missing (#422)
* Pin @octokit/request-error version * Add error notice * Move report generation after annotation publication * Styling * Fix error logging in console * Fixed tests
1 parent 0d5fce3 commit ab8c40c

16 files changed

+351
-297
lines changed

dist/index.js

+181-180
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@actions/exec": "^1.1.1",
2323
"@actions/github": "^5.0.3",
2424
"@actions/http-client": "^2.0.1",
25+
"@octokit/request-error": "^2.1.0",
2526
"fs-extra": "^10.0.0",
2627
"markdown-table": "^2.0.0",
2728
"micromatch": "^4.0.4",

src/format/formatErrors.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { formatThresholdResults } from './formatThresholdResults';
22
import { ActionError } from '../typings/ActionError';
3+
import { FailReason } from '../typings/Report';
34
import { ThresholdResult } from '../typings/ThresholdResult';
45
import { getConsoleLink } from '../utils/getConsoleLink';
56
import { i18n } from '../utils/i18n';
@@ -24,6 +25,16 @@ const formatErrorsInner = (
2425
return undefined;
2526
}
2627

28+
if (
29+
errors.some(
30+
(error) =>
31+
error instanceof ActionError &&
32+
error.failReason === FailReason.MISSING_CHECKS_PERMISSION
33+
)
34+
) {
35+
return i18n('errors.missingChecksPermissionDetail');
36+
}
37+
2738
if (errors.length === 1) {
2839
const error = errors[0];
2940

src/format/strings.json

+18-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"baseCoverage": "Base coverage collection",
5353
"switchToBase": "Switching to base branch",
5454
"generateReportContent": "Generating report",
55+
"generateRunReport": "Generating run report",
5556
"publishReport": "Report publish",
5657
"failedTestsAnnotations": "Failed tests' annotations publication",
5758
"coverageAnnotations": "Coverage annotations publication",
@@ -106,7 +107,23 @@
106107
"failedGettingCoverage": "Getting code coverage data failed.",
107108
"reportGenerationError": "Action wasn't able to generate report within GitHub comment limit. If you're facing this issue, please let me know by commenting under [this issue](https://github.com/ArtiomTr/jest-coverage-report-action/issues/404).",
108109
"testFail": "Test run failed",
109-
"coverageFail": "Coverage does not meet threshold"
110+
"coverageFail": "Coverage does not meet threshold",
111+
"missingChecksPermission": "Missing `checks: write` permission",
112+
"missingChecksPermissionDetail": [
113+
"You've enabled `annotations` option, but `checks` permission is missing. To fix this, add this to your `action.yaml` file:",
114+
"```diff",
115+
"jobs:",
116+
" permissions:",
117+
" contents: write",
118+
" pull-requests: write",
119+
"+ checks: write",
120+
" coverage:",
121+
" runs-on: ubuntu-latest",
122+
" steps:",
123+
" - uses: actions/checkout@v3",
124+
" - uses: ArtiomTr/jest-coverage-report-action@v2",
125+
"```"
126+
]
110127
},
111128
"detailsHidden": ":warning: Details were not displayed: the report size has exceeded the limit.",
112129
"summaryTitle": "Coverage report {{ dir }}",

src/run.ts

+61-46
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { generateCommitReport } from './report/generateCommitReport';
1010
import { generatePRReport } from './report/generatePRReport';
1111
import { checkThreshold } from './stages/checkThreshold';
1212
import { createReport } from './stages/createReport';
13+
import { createRunReport } from './stages/createRunReport';
1314
import { getCoverage } from './stages/getCoverage';
1415
import {
1516
checkoutRef,
@@ -170,54 +171,22 @@ export const run = async (
170171
}
171172
);
172173

173-
const [isReportContentGenerated, summaryReport] = await runStage(
174-
'generateReportContent',
174+
const [isRunReportGenerated, runReport] = await runStage(
175+
'generateRunReport',
175176
dataCollector,
176-
async () => {
177-
return createReport(dataCollector, options, thresholdResults ?? []);
178-
}
179-
);
180-
181-
await runStage('publishReport', dataCollector, async (skip) => {
182-
if (!isReportContentGenerated || !options.output.includes('comment')) {
183-
skip();
184-
}
185-
186-
const octokit = getOctokit(options.token);
187-
188-
if (isInPR) {
189-
await generatePRReport(
190-
summaryReport!.text,
191-
options,
192-
context.repo,
193-
options.pullRequest as { number: number },
194-
octokit
195-
);
196-
} else {
197-
await generateCommitReport(
198-
summaryReport!.text,
199-
context.repo,
200-
octokit
201-
);
202-
}
203-
});
204-
205-
await runStage('setOutputs', dataCollector, (skip) => {
206-
if (
207-
!isReportContentGenerated ||
208-
!options.output.includes('report-markdown')
209-
) {
210-
skip();
211-
}
177+
(skip) => {
178+
if (!isHeadCoverageGenerated) {
179+
skip();
180+
}
212181

213-
if (options.output.includes('report-markdown')) {
214-
setOutput('report', summaryReport!.text);
182+
return createRunReport(headCoverage!);
215183
}
216-
});
184+
);
217185

218186
await runStage('failedTestsAnnotations', dataCollector, async (skip) => {
219187
if (
220188
!isHeadCoverageGenerated ||
189+
!isRunReportGenerated ||
221190
!['all', 'failed-tests'].includes(options.annotations)
222191
) {
223192
skip();
@@ -228,11 +197,7 @@ export const run = async (
228197
const octokit = getOctokit(options.token);
229198
await upsertCheck(
230199
octokit,
231-
formatFailedTestsAnnotations(
232-
summaryReport!.runReport,
233-
failedAnnotations,
234-
options
235-
)
200+
formatFailedTestsAnnotations(runReport!, failedAnnotations, options)
236201
);
237202
});
238203

@@ -261,6 +226,56 @@ export const run = async (
261226
);
262227
});
263228

229+
const [isReportContentGenerated, summaryReport] = await runStage(
230+
'generateReportContent',
231+
dataCollector,
232+
async () => {
233+
return createReport(
234+
dataCollector,
235+
runReport,
236+
options,
237+
thresholdResults ?? []
238+
);
239+
}
240+
);
241+
242+
await runStage('publishReport', dataCollector, async (skip) => {
243+
if (!isReportContentGenerated || !options.output.includes('comment')) {
244+
skip();
245+
}
246+
247+
const octokit = getOctokit(options.token);
248+
249+
if (isInPR) {
250+
await generatePRReport(
251+
summaryReport!.text,
252+
options,
253+
context.repo,
254+
options.pullRequest as { number: number },
255+
octokit
256+
);
257+
} else {
258+
await generateCommitReport(
259+
summaryReport!.text,
260+
context.repo,
261+
octokit
262+
);
263+
}
264+
});
265+
266+
await runStage('setOutputs', dataCollector, (skip) => {
267+
if (
268+
!isReportContentGenerated ||
269+
!options.output.includes('report-markdown')
270+
) {
271+
skip();
272+
}
273+
274+
if (options.output.includes('report-markdown')) {
275+
setOutput('report', summaryReport!.text);
276+
}
277+
});
278+
264279
if (dataCollector.get().errors.length > 0) {
265280
setFailed(i18n('failed'));
266281
}

src/stages/createReport.ts

+2-24
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { GITHUB_MESSAGE_SIZE_LIMIT } from '../constants/GITHUB_MESSAGE_SIZE_LIMI
55
import { formatCoverage } from '../format/formatCoverage';
66
import { formatErrors } from '../format/formatErrors';
77
import { formatRunReport } from '../format/formatRunReport';
8-
import { getFailureDetails } from '../format/getFailureDetails';
98
import template from '../format/template.md';
109
import { JsonReport } from '../typings/JsonReport';
1110
import { Options } from '../typings/Options';
@@ -22,6 +21,7 @@ export const getSha = () =>
2221

2322
export const createReport = (
2423
dataCollector: DataCollector<JsonReport>,
24+
runReport: TestRunReport | undefined,
2525
options: Options,
2626
thresholdResults: ThresholdResult[]
2727
): SummaryReport => {
@@ -38,28 +38,7 @@ export const createReport = (
3838
);
3939

4040
const coverage = formatCoverage(headReport, baseReport, undefined, false);
41-
const runReport: TestRunReport = headReport.success
42-
? {
43-
success: true,
44-
title: i18n('testsSuccess'),
45-
summary: i18n('testsSuccessSummary', {
46-
numPassedTests: headReport.numPassedTests,
47-
numPassedTestSuites: headReport.numPassedTestSuites,
48-
ending: headReport.numPassedTestSuites > 1 ? 's' : '',
49-
}),
50-
}
51-
: {
52-
success: false,
53-
title: i18n('testsFail'),
54-
summary: i18n('testsFailSummary', {
55-
numFailedTests: headReport.numFailedTests,
56-
numTotalTests: headReport.numTotalTests,
57-
numFailedTestSuites: headReport.numFailedTestSuites,
58-
numTotalTestSuites: headReport.numTotalTestSuites,
59-
}),
60-
failures: getFailureDetails(headReport),
61-
};
62-
const formattedReport = formatRunReport(runReport);
41+
const formattedReport = runReport ? formatRunReport(runReport) : '';
6342

6443
let templateText = insertArgs(template, {
6544
body: [formattedErrors, coverage, formattedReport].join('\n'),
@@ -108,6 +87,5 @@ export const createReport = (
10887

10988
return {
11089
text: templateText,
111-
runReport,
11290
};
11391
};

src/stages/createRunReport.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getFailureDetails } from '../format/getFailureDetails';
2+
import { JsonReport } from '../typings/JsonReport';
3+
import { TestRunReport } from '../typings/Report';
4+
import { i18n } from '../utils/i18n';
5+
6+
export const createRunReport = (headReport: JsonReport): TestRunReport => {
7+
return headReport.success
8+
? {
9+
success: true,
10+
title: i18n('testsSuccess'),
11+
summary: i18n('testsSuccessSummary', {
12+
numPassedTests: headReport.numPassedTests,
13+
numPassedTestSuites: headReport.numPassedTestSuites,
14+
ending: headReport.numPassedTestSuites > 1 ? 's' : '',
15+
}),
16+
}
17+
: {
18+
success: false,
19+
title: i18n('testsFail'),
20+
summary: i18n('testsFailSummary', {
21+
numFailedTests: headReport.numFailedTests,
22+
numTotalTests: headReport.numTotalTests,
23+
numFailedTestSuites: headReport.numFailedTestSuites,
24+
numTotalTestSuites: headReport.numTotalTestSuites,
25+
}),
26+
failures: getFailureDetails(headReport),
27+
};
28+
};

src/typings/ActionError.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import { FailReason } from './Report';
22
import { i18n } from '../utils/i18n';
33

44
export class ActionError<T> extends Error {
5+
public readonly failReason: FailReason;
6+
57
public constructor(reason: FailReason, details?: T) {
68
super(
79
i18n(
810
`errors.${reason}`,
911
(details as unknown) as Record<string, unknown>
1012
)
1113
);
14+
this.failReason = reason;
1215
}
1316

1417
public toString(): string {

src/typings/Report.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export enum FailReason {
66
REPORT_NOT_FOUND = 'reportNotFound',
77
READING_COVERAGE_FILE_FAILED = 'readingCoverageFileFailed',
88
FAILED_GETTING_COVERAGE = 'failedGettingCoverage',
9+
MISSING_CHECKS_PERMISSION = 'missingChecksPermission',
910
}
1011

1112
export type TestRunReport =
@@ -23,5 +24,4 @@ export type TestRunReport =
2324

2425
export type SummaryReport = {
2526
text: string;
26-
runReport: TestRunReport;
2727
};

src/utils/i18n.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ const icons = (strings.icons as Record<string, Record<string, string>>)[
1313
];
1414

1515
export const i18n = (key: string, args?: Record<string, unknown>) => {
16-
const string = get(strings, key, key) as string;
16+
let string = get(strings, key, key) as string | string[];
17+
18+
if (Array.isArray(string)) {
19+
string = string.join('\n');
20+
}
1721

1822
const normalizedIconsString = string.replace(
1923
iconRegex,

src/utils/upsertCheck.ts

+19-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { getOctokit } from '@actions/github';
2+
import { RequestError } from '@octokit/request-error';
23

34
import { CreateCheckOptions } from '../format/annotations/CreateCheckOptions';
5+
import { ActionError } from '../typings/ActionError';
6+
import { FailReason } from '../typings/Report';
47

58
export const upsertCheck = async (
69
octokit: ReturnType<typeof getOctokit>,
@@ -26,13 +29,21 @@ export const upsertCheck = async (
2629
);
2730
}
2831

29-
if (check_id === undefined) {
30-
await octokit.rest.checks.create(check);
31-
} else {
32-
await octokit.rest.checks.update({
33-
check_run_id: check_id,
34-
...check,
35-
head_sha: undefined,
36-
});
32+
try {
33+
if (check_id === undefined) {
34+
await octokit.rest.checks.create(check);
35+
} else {
36+
await octokit.rest.checks.update({
37+
check_run_id: check_id,
38+
...check,
39+
head_sha: undefined,
40+
});
41+
}
42+
} catch (error) {
43+
if (error instanceof RequestError && error.status === 403) {
44+
throw new ActionError(FailReason.MISSING_CHECKS_PERMISSION);
45+
}
46+
47+
throw error;
3748
}
3849
};

0 commit comments

Comments
 (0)