oxlint-suggestion-action 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/changeDirectory.d.ts +1 -0
- package/dist/changeDirectory.js +12 -0
- package/dist/getIndexedModifiedLines.d.ts +4 -0
- package/dist/getIndexedModifiedLines.js +41 -0
- package/dist/getOctokit.d.ts +6 -0
- package/dist/getOctokit.js +39 -0
- package/dist/getPullRequestMetadata.d.ts +7 -0
- package/dist/getPullRequestMetadata.js +22 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +57 -0
- package/dist/parseOxlintOutput.d.ts +29 -0
- package/dist/parseOxlintOutput.js +19 -0
- package/dist/pullRequest.d.ts +7 -0
- package/dist/pullRequest.js +161 -0
- package/dist/runOxlint.d.ts +5 -0
- package/dist/runOxlint.js +38 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Cat Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# oxlint-suggestion-action
|
|
2
|
+
|
|
3
|
+
[](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/build.yml)
|
|
4
|
+
[](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/test.yml)
|
|
5
|
+
[](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/oxlint.yml)
|
|
6
|
+
[](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/codeql.yml)
|
|
7
|
+
|
|
8
|
+
This GitHub Action runs Oxlint and provides inline feedback on the changes in a pull request. Features:
|
|
9
|
+
|
|
10
|
+
1. It posts review comments for Oxlint diagnostics on modified lines.
|
|
11
|
+
2. It only provides feedback for lines changed in the pull request, so pre-existing issues outside the diff do not add noise.
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
When there is any Oxlint warning or error, this action will leave a comment:
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Set up a GitHub Action like this:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
name: Oxlint
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
push:
|
|
28
|
+
branches: [main] # or [master] if that's the name of the main branch
|
|
29
|
+
pull_request:
|
|
30
|
+
branches: [main] # or [master] if that's the name of the main branch
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
oxlint:
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
pull-requests: write
|
|
38
|
+
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v6
|
|
41
|
+
- uses: actions/setup-node@v6
|
|
42
|
+
with:
|
|
43
|
+
node-version: '24'
|
|
44
|
+
check-latest: true
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: yarn install # or npm ci if you use npm and have package-lock.json
|
|
48
|
+
|
|
49
|
+
- uses: CatChen/oxlint-suggestion-action@main
|
|
50
|
+
with:
|
|
51
|
+
request-changes: true # optional
|
|
52
|
+
fail-check: false # optional
|
|
53
|
+
github-token: ${{ secrets.GITHUB_TOKEN }} # optional
|
|
54
|
+
directory: './' # optional
|
|
55
|
+
targets: '.' # optional
|
|
56
|
+
oxlint-bin-path: './node_modules/.bin/oxlint' # optional
|
|
57
|
+
config-path: '' # optional
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Save the file to `.github/workflows/oxlint.yml`. It will start working on new pull requests.
|
|
61
|
+
|
|
62
|
+
## Options
|
|
63
|
+
|
|
64
|
+
### `request-changes`
|
|
65
|
+
|
|
66
|
+
This option determines whether this GitHub Action should request changes if Oxlint does not pass. This option has no effect when the workflow is not triggered by a `pull_request` event. The default value is `true`.
|
|
67
|
+
|
|
68
|
+
### `fail-check`
|
|
69
|
+
|
|
70
|
+
This option determines whether the GitHub workflow should fail if Oxlint does not pass. The default value is `false`.
|
|
71
|
+
|
|
72
|
+
### `github-token`
|
|
73
|
+
|
|
74
|
+
The default value is `${{ github.token }}`, which is the GitHub token generated for this workflow. You can [create a different token with a different set of permissions](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and use it here as well.
|
|
75
|
+
|
|
76
|
+
### `directory`
|
|
77
|
+
|
|
78
|
+
The default value is `'./'`. This action runs Oxlint from this directory.
|
|
79
|
+
|
|
80
|
+
### `targets`
|
|
81
|
+
|
|
82
|
+
The default value is `'.'`. For example, it could be `'src'` or `'src/**/*.ts'` for a typical TypeScript project. You can use glob patterns to match multiple directories, for example `'{src,lib}'`.
|
|
83
|
+
|
|
84
|
+
### `oxlint-bin-path`
|
|
85
|
+
|
|
86
|
+
The default value is `'./node_modules/.bin/oxlint'`. This action uses the Oxlint binary from this path.
|
|
87
|
+
|
|
88
|
+
### `config-path`
|
|
89
|
+
|
|
90
|
+
The default value is an empty string. Oxlint's default config discovery is used when this value is empty. If your config file is in a non-default location, set this option.
|
|
91
|
+
|
|
92
|
+
## FAQ
|
|
93
|
+
|
|
94
|
+
### Can I have GitHub suggestions outside of the scope?
|
|
95
|
+
|
|
96
|
+
No, mostly not. GitHub only allows review comments inside diff hunks (changed lines and a small surrounding context). For consistency, this action only comments on changed lines in the pull request.
|
|
97
|
+
|
|
98
|
+
### How can I avoid having annotations in generated code inside a project?
|
|
99
|
+
|
|
100
|
+
Follow [GitHub's documentation](https://github.com/github/linguist/blob/master/docs/overrides.md#generated-code) and use `.gitattributes` to mark generated files and directories correctly. GitHub will hide those files in pull requests.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function changeDirectory(directory: string): void;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { chdir, cwd } from 'node:process';
|
|
3
|
+
import { info } from '@actions/core';
|
|
4
|
+
const DEFAULT_WORKING_DIRECTORY = cwd();
|
|
5
|
+
export function changeDirectory(directory) {
|
|
6
|
+
info(`Working directory is: ${DEFAULT_WORKING_DIRECTORY}`);
|
|
7
|
+
const absoluteDirectory = path.resolve(DEFAULT_WORKING_DIRECTORY, directory);
|
|
8
|
+
if (absoluteDirectory !== DEFAULT_WORKING_DIRECTORY) {
|
|
9
|
+
info(`Working directory is changed to: ${absoluteDirectory}`);
|
|
10
|
+
chdir(absoluteDirectory);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { info } from '@actions/core';
|
|
2
|
+
const HUNK_HEADER_PATTERN = /^@@ -\d+(,\d+)? \+(\d+)(,(\d+))? @@/;
|
|
3
|
+
export function getIndexedModifiedLines(file) {
|
|
4
|
+
var _a;
|
|
5
|
+
const modifiedLines = [];
|
|
6
|
+
const indexedModifiedLines = {};
|
|
7
|
+
let currentLine = 0;
|
|
8
|
+
let remainingLinesInHunk = 0;
|
|
9
|
+
const lines = (_a = file.patch) === null || _a === void 0 ? void 0 : _a.split('\n');
|
|
10
|
+
if (lines) {
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (remainingLinesInHunk === 0) {
|
|
13
|
+
const matches = line.match(HUNK_HEADER_PATTERN);
|
|
14
|
+
currentLine = parseInt((matches === null || matches === void 0 ? void 0 : matches[2]) || '1');
|
|
15
|
+
remainingLinesInHunk = parseInt((matches === null || matches === void 0 ? void 0 : matches[4]) || '1');
|
|
16
|
+
if (!currentLine || !remainingLinesInHunk) {
|
|
17
|
+
throw new Error(`Expecting hunk header in ${file.filename} but seeing ${line}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
else if (line[0] === '-') {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
if (line[0] === '+') {
|
|
25
|
+
modifiedLines.push(currentLine);
|
|
26
|
+
indexedModifiedLines[currentLine] = true;
|
|
27
|
+
}
|
|
28
|
+
currentLine++;
|
|
29
|
+
remainingLinesInHunk--;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
info(` File modified lines: ${modifiedLines.join()}`);
|
|
34
|
+
if (file.patch !== undefined) {
|
|
35
|
+
info(` File patch: \n${file.patch
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((line) => ' ' + line)
|
|
38
|
+
.join('\n')}\n`);
|
|
39
|
+
}
|
|
40
|
+
return indexedModifiedLines;
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Octokit } from '@octokit/core';
|
|
2
|
+
import type { PaginateInterface } from '@octokit/plugin-paginate-rest';
|
|
3
|
+
import type { Api } from '@octokit/plugin-rest-endpoint-methods';
|
|
4
|
+
export declare function getOctokit(githubToken: string): Octokit & Api & {
|
|
5
|
+
paginate: PaginateInterface;
|
|
6
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { GitHub, getOctokitOptions } from '@actions/github/lib/utils';
|
|
2
|
+
import { retry } from '@octokit/plugin-retry';
|
|
3
|
+
import { throttling } from '@octokit/plugin-throttling';
|
|
4
|
+
export function getOctokit(githubToken) {
|
|
5
|
+
const Octokit = GitHub.plugin(throttling, retry);
|
|
6
|
+
const octokit = new Octokit(getOctokitOptions(githubToken, {
|
|
7
|
+
throttle: {
|
|
8
|
+
onRateLimit: (retryAfter, options, _, retryCount) => {
|
|
9
|
+
if (retryCount === 0) {
|
|
10
|
+
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`);
|
|
11
|
+
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
octokit.log.error(`Request quota exhausted for request ${options.method} ${options.url}`);
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
onSecondaryRateLimit: (retryAfter, options, _, retryCount) => {
|
|
19
|
+
if (retryCount === 0) {
|
|
20
|
+
octokit.log.warn(`Abuse detected for request ${options.method} ${options.url}`);
|
|
21
|
+
octokit.log.info(`Retrying after ${retryAfter} seconds!`);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
octokit.log.warn(`Abuse detected for request ${options.method} ${options.url}`);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
retry: {
|
|
30
|
+
doNotRetry: [429],
|
|
31
|
+
},
|
|
32
|
+
}));
|
|
33
|
+
octokit.graphql = octokit.graphql.defaults({
|
|
34
|
+
headers: {
|
|
35
|
+
'X-GitHub-Next-Global-ID': 1,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
return octokit;
|
|
39
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { info } from '@actions/core';
|
|
2
|
+
import { context } from '@actions/github';
|
|
3
|
+
export function getPullRequestMetadata() {
|
|
4
|
+
const pullRequest = context.payload.pull_request;
|
|
5
|
+
const owner = context.repo.owner;
|
|
6
|
+
const repo = context.repo.repo;
|
|
7
|
+
const pullRequestNumber = pullRequest.number;
|
|
8
|
+
const baseSha = pullRequest.base.sha;
|
|
9
|
+
const headSha = pullRequest.head.sha;
|
|
10
|
+
info(`Owner: ${owner}`);
|
|
11
|
+
info(`Repo: ${repo}`);
|
|
12
|
+
info(`Pull Request number: ${pullRequestNumber}`);
|
|
13
|
+
info(`Base SHA: ${baseSha}`);
|
|
14
|
+
info(`Head SHA: ${headSha}`);
|
|
15
|
+
return {
|
|
16
|
+
owner,
|
|
17
|
+
repo,
|
|
18
|
+
pullRequestNumber,
|
|
19
|
+
baseSha,
|
|
20
|
+
headSha,
|
|
21
|
+
};
|
|
22
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function oxlintSuggestion({ requestChanges, failCheck, githubToken, directory, targets, oxlintBinPath, configPath, }: {
|
|
2
|
+
requestChanges: boolean;
|
|
3
|
+
failCheck: boolean;
|
|
4
|
+
githubToken: string;
|
|
5
|
+
directory: string;
|
|
6
|
+
targets: string;
|
|
7
|
+
oxlintBinPath: string;
|
|
8
|
+
configPath: string;
|
|
9
|
+
}): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { endGroup, getBooleanInput, getInput, info, setFailed, startGroup, } from '@actions/core';
|
|
11
|
+
import { context } from '@actions/github';
|
|
12
|
+
import { changeDirectory } from './changeDirectory.js';
|
|
13
|
+
import { getOctokit } from './getOctokit.js';
|
|
14
|
+
import { getPullRequestMetadata } from './getPullRequestMetadata.js';
|
|
15
|
+
import { parseOxlintOutput } from './parseOxlintOutput.js';
|
|
16
|
+
import { handlePullRequest } from './pullRequest.js';
|
|
17
|
+
import { runOxlint } from './runOxlint.js';
|
|
18
|
+
export function oxlintSuggestion(_a) {
|
|
19
|
+
return __awaiter(this, arguments, void 0, function* ({ requestChanges, failCheck, githubToken, directory, targets, oxlintBinPath, configPath, }) {
|
|
20
|
+
startGroup('Oxlint');
|
|
21
|
+
changeDirectory(directory);
|
|
22
|
+
const output = yield runOxlint({
|
|
23
|
+
oxlintBinPath,
|
|
24
|
+
targets,
|
|
25
|
+
configPath,
|
|
26
|
+
});
|
|
27
|
+
const parsedOutput = parseOxlintOutput(output);
|
|
28
|
+
endGroup();
|
|
29
|
+
info(`Event name: ${context.eventName}`);
|
|
30
|
+
switch (context.eventName) {
|
|
31
|
+
case 'pull_request':
|
|
32
|
+
case 'pull_request_target':
|
|
33
|
+
yield (() => __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const octokit = getOctokit(githubToken);
|
|
35
|
+
const { owner, repo, pullRequestNumber, headSha } = getPullRequestMetadata();
|
|
36
|
+
yield handlePullRequest(octokit, parsedOutput.diagnostics, owner, repo, pullRequestNumber, headSha, failCheck, requestChanges);
|
|
37
|
+
}))();
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function run() {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
yield oxlintSuggestion({
|
|
47
|
+
requestChanges: getBooleanInput('request-changes'),
|
|
48
|
+
failCheck: getBooleanInput('fail-check'),
|
|
49
|
+
githubToken: getInput('github-token'),
|
|
50
|
+
directory: getInput('directory'),
|
|
51
|
+
targets: getInput('targets'),
|
|
52
|
+
oxlintBinPath: getInput('oxlint-bin-path'),
|
|
53
|
+
configPath: getInput('config-path'),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
run().catch((error) => setFailed(error));
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type OxlintSeverity = 'warning' | 'error';
|
|
2
|
+
type OxlintSpan = {
|
|
3
|
+
offset: number;
|
|
4
|
+
length: number;
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
};
|
|
8
|
+
type OxlintLabel = {
|
|
9
|
+
label?: string;
|
|
10
|
+
span: OxlintSpan;
|
|
11
|
+
};
|
|
12
|
+
export type OxlintDiagnostic = {
|
|
13
|
+
message: string;
|
|
14
|
+
code?: string;
|
|
15
|
+
severity: OxlintSeverity;
|
|
16
|
+
url?: string;
|
|
17
|
+
help?: string;
|
|
18
|
+
filename: string;
|
|
19
|
+
labels: OxlintLabel[];
|
|
20
|
+
};
|
|
21
|
+
export type OxlintOutput = {
|
|
22
|
+
diagnostics: OxlintDiagnostic[];
|
|
23
|
+
number_of_files: number;
|
|
24
|
+
number_of_rules: number;
|
|
25
|
+
threads_count: number;
|
|
26
|
+
start_time: number;
|
|
27
|
+
};
|
|
28
|
+
export declare function parseOxlintOutput(output: string): OxlintOutput;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { info } from '@actions/core';
|
|
2
|
+
export function parseOxlintOutput(output) {
|
|
3
|
+
var _a, _b, _c, _d;
|
|
4
|
+
var _e;
|
|
5
|
+
const parsed = JSON.parse(output);
|
|
6
|
+
const indexedDiagnostics = {};
|
|
7
|
+
for (const diagnostic of parsed.diagnostics) {
|
|
8
|
+
(_a = indexedDiagnostics[_e = diagnostic.filename]) !== null && _a !== void 0 ? _a : (indexedDiagnostics[_e] = []);
|
|
9
|
+
(_b = indexedDiagnostics[diagnostic.filename]) === null || _b === void 0 ? void 0 : _b.push(diagnostic);
|
|
10
|
+
}
|
|
11
|
+
for (const [file, diagnostics] of Object.entries(indexedDiagnostics)) {
|
|
12
|
+
info(`File name: ${file}`);
|
|
13
|
+
for (const diagnostic of diagnostics) {
|
|
14
|
+
const line = (_d = (_c = diagnostic.labels[0]) === null || _c === void 0 ? void 0 : _c.span.line) !== null && _d !== void 0 ? _d : 1;
|
|
15
|
+
info(` (${diagnostic.severity}) ${diagnostic.message} @ ${line}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return parsed;
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OxlintDiagnostic } from './parseOxlintOutput.js';
|
|
2
|
+
import type { Octokit } from '@octokit/core';
|
|
3
|
+
import type { PaginateInterface } from '@octokit/plugin-paginate-rest';
|
|
4
|
+
import type { Api } from '@octokit/plugin-rest-endpoint-methods';
|
|
5
|
+
export declare function handlePullRequest(octokit: Octokit & Api & {
|
|
6
|
+
paginate: PaginateInterface;
|
|
7
|
+
}, diagnostics: OxlintDiagnostic[], owner: string, repo: string, pullRequestNumber: number, headSha: string, failCheck: boolean, requestChanges: boolean): Promise<void>;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { endGroup, error, info, notice, startGroup } from '@actions/core';
|
|
11
|
+
import { getIndexedModifiedLines } from './getIndexedModifiedLines.js';
|
|
12
|
+
const REVIEW_BODY = "Oxlint doesn't pass. Please fix all Oxlint issues.";
|
|
13
|
+
const GITHUB_ACTIONS_BOT_ID = 41898282;
|
|
14
|
+
function getPullRequestFiles(octokit, owner, repo, pullRequestNumber) {
|
|
15
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
16
|
+
const files = yield octokit.paginate(octokit.rest.pulls.listFiles, {
|
|
17
|
+
owner,
|
|
18
|
+
repo,
|
|
19
|
+
pull_number: pullRequestNumber,
|
|
20
|
+
per_page: 100,
|
|
21
|
+
});
|
|
22
|
+
info(`Files: (${files.length})`);
|
|
23
|
+
return files;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function getReviewComments(octokit, owner, repo, pullRequestNumber) {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const reviews = yield octokit.paginate(octokit.rest.pulls.listReviews, {
|
|
29
|
+
owner,
|
|
30
|
+
repo,
|
|
31
|
+
pull_number: pullRequestNumber,
|
|
32
|
+
per_page: 100,
|
|
33
|
+
});
|
|
34
|
+
const reviewComments = yield octokit.paginate(octokit.rest.pulls.listReviewComments, {
|
|
35
|
+
owner,
|
|
36
|
+
repo,
|
|
37
|
+
pull_number: pullRequestNumber,
|
|
38
|
+
per_page: 100,
|
|
39
|
+
});
|
|
40
|
+
const relevantReviews = reviews.filter((review) => { var _a; return ((_a = review.user) === null || _a === void 0 ? void 0 : _a.id) === GITHUB_ACTIONS_BOT_ID && review.body === REVIEW_BODY; });
|
|
41
|
+
const relevantReviewIds = relevantReviews.map((review) => review.id);
|
|
42
|
+
const relevantReviewComments = reviewComments.filter((reviewComment) => reviewComment.user.id === GITHUB_ACTIONS_BOT_ID &&
|
|
43
|
+
reviewComment.pull_request_review_id !== null &&
|
|
44
|
+
relevantReviewIds.includes(reviewComment.pull_request_review_id));
|
|
45
|
+
info(`Existing review comments: (${relevantReviewComments.length})`);
|
|
46
|
+
return relevantReviewComments;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function getDiagnosticLines(diagnostic) {
|
|
50
|
+
const lines = diagnostic.labels.map((label) => label.span.line);
|
|
51
|
+
return [...new Set(lines)];
|
|
52
|
+
}
|
|
53
|
+
function getReviewCommentFromDiagnostic(diagnostic, line, path) {
|
|
54
|
+
const ruleInfo = diagnostic.code
|
|
55
|
+
? diagnostic.url
|
|
56
|
+
? `[\`${diagnostic.code}\`](${diagnostic.url})`
|
|
57
|
+
: `\`${diagnostic.code}\``
|
|
58
|
+
: '';
|
|
59
|
+
const trailingRuleInfo = ruleInfo ? ` ${ruleInfo}` : '';
|
|
60
|
+
return {
|
|
61
|
+
body: `**${diagnostic.message}**${trailingRuleInfo}`,
|
|
62
|
+
path,
|
|
63
|
+
side: 'RIGHT',
|
|
64
|
+
line,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function matchReviewComments(reviewComments, reviewComment) {
|
|
68
|
+
const matchedNodeIds = [];
|
|
69
|
+
for (const existingReviewComment of reviewComments) {
|
|
70
|
+
if (existingReviewComment.path === reviewComment.path &&
|
|
71
|
+
existingReviewComment.line === reviewComment.line &&
|
|
72
|
+
existingReviewComment.side === reviewComment.side &&
|
|
73
|
+
existingReviewComment.body === reviewComment.body) {
|
|
74
|
+
matchedNodeIds.push(existingReviewComment.node_id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return matchedNodeIds;
|
|
78
|
+
}
|
|
79
|
+
export function handlePullRequest(octokit, diagnostics, owner, repo, pullRequestNumber, headSha, failCheck, requestChanges) {
|
|
80
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
81
|
+
var _a, _b, _c;
|
|
82
|
+
var _d;
|
|
83
|
+
startGroup('GitHub Pull Request');
|
|
84
|
+
const files = yield getPullRequestFiles(octokit, owner, repo, pullRequestNumber);
|
|
85
|
+
const existingReviewComments = yield getReviewComments(octokit, owner, repo, pullRequestNumber);
|
|
86
|
+
const indexedDiagnostics = {};
|
|
87
|
+
for (const diagnostic of diagnostics) {
|
|
88
|
+
(_a = indexedDiagnostics[_d = diagnostic.filename]) !== null && _a !== void 0 ? _a : (indexedDiagnostics[_d] = []);
|
|
89
|
+
(_b = indexedDiagnostics[diagnostic.filename]) === null || _b === void 0 ? void 0 : _b.push(diagnostic);
|
|
90
|
+
}
|
|
91
|
+
let commentsCounter = 0;
|
|
92
|
+
let outOfScopeResultsCounter = 0;
|
|
93
|
+
const reviewComments = [];
|
|
94
|
+
for (const file of files) {
|
|
95
|
+
info(` File name: ${file.filename}`);
|
|
96
|
+
info(` File status: ${file.status}`);
|
|
97
|
+
if (file.status === 'removed') {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const indexedModifiedLines = getIndexedModifiedLines(file);
|
|
101
|
+
const fileDiagnostics = (_c = indexedDiagnostics[file.filename]) !== null && _c !== void 0 ? _c : [];
|
|
102
|
+
for (const diagnostic of fileDiagnostics) {
|
|
103
|
+
const lines = getDiagnosticLines(diagnostic);
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
if (indexedModifiedLines[line]) {
|
|
106
|
+
info(` Matched line: ${line}`);
|
|
107
|
+
const reviewComment = getReviewCommentFromDiagnostic(diagnostic, line, file.filename);
|
|
108
|
+
const matchedComments = matchReviewComments(existingReviewComments, reviewComment);
|
|
109
|
+
commentsCounter++;
|
|
110
|
+
if (matchedComments.length === 0) {
|
|
111
|
+
reviewComments.push(reviewComment);
|
|
112
|
+
info(` Comment queued`);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
info(` Comment skipped`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
outOfScopeResultsCounter++;
|
|
120
|
+
info(` Out of scope line: ${line}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (outOfScopeResultsCounter > 0) {
|
|
126
|
+
info(`Out of scope results: ${outOfScopeResultsCounter}`);
|
|
127
|
+
}
|
|
128
|
+
endGroup();
|
|
129
|
+
startGroup('Feedback');
|
|
130
|
+
if (commentsCounter > 0) {
|
|
131
|
+
try {
|
|
132
|
+
yield octokit.rest.pulls.createReview({
|
|
133
|
+
owner,
|
|
134
|
+
repo,
|
|
135
|
+
body: REVIEW_BODY,
|
|
136
|
+
pull_number: pullRequestNumber,
|
|
137
|
+
commit_id: headSha,
|
|
138
|
+
event: requestChanges ? 'REQUEST_CHANGES' : 'COMMENT',
|
|
139
|
+
comments: reviewComments,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (_e) {
|
|
143
|
+
throw new Error(`Failed to create review with ${reviewComments.length} comment(s).`);
|
|
144
|
+
}
|
|
145
|
+
if (commentsCounter - reviewComments.length > 0) {
|
|
146
|
+
info(`Review comments existed and skipped: ${commentsCounter - reviewComments.length}`);
|
|
147
|
+
}
|
|
148
|
+
info(`Review comments submitted: ${reviewComments.length}`);
|
|
149
|
+
if (failCheck) {
|
|
150
|
+
throw new Error('Oxlint fails. Please review comments.');
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
error('Oxlint fails');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
notice('Oxlint passes');
|
|
158
|
+
}
|
|
159
|
+
endGroup();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { cwd } from 'node:process';
|
|
13
|
+
import { notice } from '@actions/core';
|
|
14
|
+
import { getExecOutput } from '@actions/exec';
|
|
15
|
+
import { globSync } from 'glob';
|
|
16
|
+
export function runOxlint(_a) {
|
|
17
|
+
return __awaiter(this, arguments, void 0, function* ({ oxlintBinPath, targets, configPath, }) {
|
|
18
|
+
const absoluteOxlintBinPath = resolve(cwd(), oxlintBinPath);
|
|
19
|
+
if (!existsSync(absoluteOxlintBinPath)) {
|
|
20
|
+
throw new Error(`Oxlint binary cannot be found at ${absoluteOxlintBinPath}`);
|
|
21
|
+
}
|
|
22
|
+
notice(`Using Oxlint from: ${absoluteOxlintBinPath}`);
|
|
23
|
+
const args = [...globSync(targets), '--format=json'];
|
|
24
|
+
const absoluteConfigPath = configPath ? resolve(cwd(), configPath) : null;
|
|
25
|
+
if (absoluteConfigPath) {
|
|
26
|
+
if (!existsSync(absoluteConfigPath)) {
|
|
27
|
+
throw new Error(`Oxlint config cannot be found at ${absoluteConfigPath}`);
|
|
28
|
+
}
|
|
29
|
+
notice(`Using Oxlint config from: ${absoluteConfigPath}`);
|
|
30
|
+
args.push(`--config=${absoluteConfigPath}`);
|
|
31
|
+
}
|
|
32
|
+
const oxlintOutput = yield getExecOutput(absoluteOxlintBinPath, args, {
|
|
33
|
+
ignoreReturnCode: true,
|
|
34
|
+
silent: true,
|
|
35
|
+
});
|
|
36
|
+
return oxlintOutput.stdout;
|
|
37
|
+
});
|
|
38
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oxlint-suggestion-action",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A template to create custom GitHub Action with TypeScript/JavaScript.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"codegen": "rm -rf src/__graphql__ && graphql-codegen-esm --config codegen.ts",
|
|
10
|
+
"build": "rm -rf dist && yarn tsc",
|
|
11
|
+
"bundle": "rm -rf bundle && yarn ncc build src/index.ts --source-map --license licenses.txt --out bundle",
|
|
12
|
+
"format": "oxfmt --write .",
|
|
13
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
14
|
+
"lint": "oxlint src",
|
|
15
|
+
"prepublishOnly": "pinst --disable && yarn build",
|
|
16
|
+
"postpublish": "pinst --enable",
|
|
17
|
+
"prepare": "is-ci || husky"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/CatChen/oxlint-suggestion-action.git"
|
|
22
|
+
},
|
|
23
|
+
"author": "Cat Chen",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/CatChen/oxlint-suggestion-action/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/CatChen/oxlint-suggestion-action#readme",
|
|
29
|
+
"funding": "https://github.com/CatChen/oxlint-suggestion-action?sponsor=1",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@0no-co/graphqlsp": "^1.12.12",
|
|
32
|
+
"@graphql-codegen/cli": "6.1.2",
|
|
33
|
+
"@graphql-codegen/near-operation-file-preset": "^4.0.0",
|
|
34
|
+
"@graphql-codegen/typescript": "5.0.8",
|
|
35
|
+
"@graphql-codegen/typescript-document-nodes": "5.0.8",
|
|
36
|
+
"@types/node": "^25.0.0",
|
|
37
|
+
"@vercel/ncc": "^0.38.0",
|
|
38
|
+
"graphql": "^16.9.0",
|
|
39
|
+
"husky": "^9.0.5",
|
|
40
|
+
"is-ci": "^4.1.0",
|
|
41
|
+
"lint-staged": "^16.0.0",
|
|
42
|
+
"oxfmt": "^0.35.0",
|
|
43
|
+
"oxlint": "^1.24.3",
|
|
44
|
+
"pinst": "^3.0.0",
|
|
45
|
+
"typescript": "^5.0.2"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@actions/core": "^3.0.0",
|
|
49
|
+
"@actions/exec": "^3.0.0",
|
|
50
|
+
"@actions/github": "^9.0.0",
|
|
51
|
+
"@graphql-typed-document-node/core": "^3.2.0",
|
|
52
|
+
"@octokit/plugin-retry": "^8.0.1",
|
|
53
|
+
"@octokit/plugin-throttling": "^11.0.1",
|
|
54
|
+
"glob": "^13.0.6"
|
|
55
|
+
},
|
|
56
|
+
"lint-staged": {
|
|
57
|
+
"*.{ts,js}": [
|
|
58
|
+
"yarn format",
|
|
59
|
+
"yarn lint --fix"
|
|
60
|
+
],
|
|
61
|
+
"*.{json,yml,yaml,md,markdown}": "yarn format"
|
|
62
|
+
}
|
|
63
|
+
}
|