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 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
+ [![Build](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/build.yml/badge.svg?branch=main&event=push)](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/build.yml)
4
+ [![Test](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/test.yml/badge.svg?branch=main&event=push)](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/test.yml)
5
+ [![Oxlint](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/oxlint.yml/badge.svg?branch=main&event=push)](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/oxlint.yml)
6
+ [![CodeQL](https://github.com/CatChen/oxlint-suggestion-action/actions/workflows/codeql.yml/badge.svg?branch=main&event=schedule)](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
+ ![example-screenshot](https://github.com/user-attachments/assets/e1f9dd35-1768-477f-b769-a936e7066940)
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,4 @@
1
+ import type { components } from '@octokit/openapi-types/types.js';
2
+ export declare function getIndexedModifiedLines(file: components['schemas']['diff-entry']): {
3
+ [line: string]: true;
4
+ };
@@ -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,7 @@
1
+ export declare function getPullRequestMetadata(): {
2
+ owner: string;
3
+ repo: string;
4
+ pullRequestNumber: number;
5
+ baseSha: string;
6
+ headSha: string;
7
+ };
@@ -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
+ }
@@ -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,5 @@
1
+ export declare function runOxlint({ oxlintBinPath, targets, configPath, }: {
2
+ oxlintBinPath: string;
3
+ targets: string;
4
+ configPath: string;
5
+ }): Promise<string>;
@@ -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
+ }