Convert action to TypeScript
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
FROM node:20.10-buster-slim
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN npm install --production
|
||||
|
||||
ENTRYPOINT ["node", "/lib/main.js"]
|
||||
51
README.md
51
README.md
@@ -1,6 +1,18 @@
|
||||
# First Interaction
|
||||
|
||||
An action for filtering pull requests and issues from first-time contributors.
|
||||
[](https://github.com/super-linter/super-linter)
|
||||

|
||||
[](https://github.com/actions/first-interaction/actions/workflows/check-dist.yml)
|
||||
[](https://github.com/actions/first-interaction/actions/workflows/codeql-analysis.yml)
|
||||
[](./badges/coverage.svg)
|
||||
|
||||
An action for filtering pull requests (PRs) and issues from first-time
|
||||
contributors.
|
||||
|
||||
When a first-time contributor opens a PR or issue, this action will add a
|
||||
comment to the PR or issue with a message of your choice. This action is useful
|
||||
for welcoming first-time contributors to your project and providing them with
|
||||
information about how to contribute effectively.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -9,23 +21,32 @@ See [action.yml](action.yml)
|
||||
```yaml
|
||||
name: Greetings
|
||||
|
||||
on: [pull_request, issues]
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
greeting:
|
||||
name: Greet First-Time Contributors
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-message: |
|
||||
# Message with markdown.
|
||||
This is the message that will be displayed on users' first issue.
|
||||
pr-message: |
|
||||
Message that will be displayed on users' first pr.
|
||||
Look, a `code block` for markdown.
|
||||
- uses: actions/first-interaction@vX.Y.Z # Set this to the latest release
|
||||
with:
|
||||
issue-message: |
|
||||
# Issue Message with Markdown
|
||||
|
||||
This is the message that will be displayed!
|
||||
pr-message: |
|
||||
# PR Message with Markdown
|
||||
|
||||
This is the message that will be displayed!
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||
|
||||
10
__fixtures__/@actions/core.ts
Normal file
10
__fixtures__/@actions/core.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type * as core from '@actions/core'
|
||||
import { jest } from '@jest/globals'
|
||||
|
||||
export const debug = jest.fn<typeof core.debug>()
|
||||
export const error = jest.fn<typeof core.error>()
|
||||
export const info = jest.fn<typeof core.info>()
|
||||
export const getInput = jest.fn<typeof core.getInput>()
|
||||
export const setOutput = jest.fn<typeof core.setOutput>()
|
||||
export const setFailed = jest.fn<typeof core.setFailed>()
|
||||
export const warning = jest.fn<typeof core.warning>()
|
||||
27
__fixtures__/@actions/github.ts
Normal file
27
__fixtures__/@actions/github.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as octokit from '../@octokit/rest.js'
|
||||
|
||||
export const getOctokit = () => octokit
|
||||
|
||||
export const context = {
|
||||
eventName: 'pull_request',
|
||||
issue: {
|
||||
number: 10
|
||||
},
|
||||
payload: {
|
||||
number: 10,
|
||||
issue: {
|
||||
number: 10
|
||||
},
|
||||
pull_request: {
|
||||
number: 10
|
||||
},
|
||||
sender: {
|
||||
login: 'mona'
|
||||
}
|
||||
},
|
||||
action: 'opened',
|
||||
repo: {
|
||||
owner: 'actions',
|
||||
repo: 'first-interaction'
|
||||
}
|
||||
}
|
||||
20
__fixtures__/@octokit/rest.ts
Normal file
20
__fixtures__/@octokit/rest.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import { Endpoints } from '@octokit/types'
|
||||
|
||||
export const graphql = jest.fn()
|
||||
export const paginate = jest.fn()
|
||||
export const rest = {
|
||||
issues: {
|
||||
createComment:
|
||||
jest.fn<
|
||||
() => Endpoints['POST /repos/{owner}/{repo}/issues/{issue_number}/comments']['response']
|
||||
>(),
|
||||
listForRepo:
|
||||
jest.fn<() => Endpoints['GET /repos/{owner}/{repo}/issues']['response']>()
|
||||
},
|
||||
pulls: {
|
||||
list: jest.fn<
|
||||
() => Endpoints['GET /repos/{owner}/{repo}/pulls']['response']
|
||||
>()
|
||||
}
|
||||
}
|
||||
304
__tests__/main.test.ts
Normal file
304
__tests__/main.test.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { jest } from '@jest/globals'
|
||||
import * as core from '../__fixtures__/@actions/core.js'
|
||||
import * as github from '../__fixtures__/@actions/github.js'
|
||||
import * as octokit from '../__fixtures__/@octokit/rest.js'
|
||||
|
||||
jest.unstable_mockModule('@actions/core', () => core)
|
||||
jest.unstable_mockModule('@actions/github', () => github)
|
||||
jest.unstable_mockModule('@octokit/rest', async () => {
|
||||
class Octokit {
|
||||
constructor() {
|
||||
return octokit
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
Octokit
|
||||
}
|
||||
})
|
||||
|
||||
const main = await import('../src/main.js')
|
||||
|
||||
const { Octokit } = await import('@octokit/rest')
|
||||
const mocktokit = jest.mocked(new Octokit())
|
||||
|
||||
describe('main.ts', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// "Reset" the github context.
|
||||
github.context.eventName = 'pull_request'
|
||||
github.context.action = 'opened'
|
||||
github.context.payload.issue = undefined as any
|
||||
github.context.payload.pull_request = {
|
||||
number: 10
|
||||
}
|
||||
github.context.payload.sender = {
|
||||
login: 'mona'
|
||||
}
|
||||
|
||||
// Set the action's inputs as return values from core.getInput().
|
||||
core.getInput
|
||||
.mockReturnValueOnce('ISSUE_MESSAGE')
|
||||
.mockReturnValueOnce('PR_MESSAGE')
|
||||
.mockReturnValueOnce('REPO_TOKEN')
|
||||
})
|
||||
|
||||
describe('run()', () => {
|
||||
it('Skips invalid events', async () => {
|
||||
github.context.eventName = 'push'
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Skipping...Not an Issue/PR Event')
|
||||
})
|
||||
|
||||
it('Skips invalid actions', async () => {
|
||||
github.context.action = 'edited'
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith('Skipping...Not an Opened Event')
|
||||
})
|
||||
|
||||
it('Fails if no sender is present', async () => {
|
||||
github.context.payload.sender = undefined as any
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'Internal Error...No Sender Provided by GitHub'
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if neither PR nor issue are provided', async () => {
|
||||
github.context.payload.issue = undefined as any
|
||||
github.context.payload.pull_request = undefined as any
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'Internal Error...No Issue or PR Provided by GitHub'
|
||||
)
|
||||
})
|
||||
|
||||
it('Fails if both PR and issue are provided', async () => {
|
||||
github.context.payload.issue = {
|
||||
number: 20
|
||||
}
|
||||
github.context.payload.pull_request = {
|
||||
number: 10
|
||||
}
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.setFailed).toHaveBeenCalledWith(
|
||||
'Internal Error...Both Issue and PR Provided by GitHub'
|
||||
)
|
||||
})
|
||||
|
||||
it('Skips adding a message if this is not the first contribution', async () => {
|
||||
mocktokit.paginate
|
||||
// Issues
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
},
|
||||
{
|
||||
number: 5
|
||||
}
|
||||
])
|
||||
// PRs
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
number: 3
|
||||
}
|
||||
])
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(core.info).toHaveBeenCalledWith(
|
||||
'Skipping...Not First Contribution'
|
||||
)
|
||||
expect(mocktokit.rest.issues.createComment).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Adds an issue message if this is the first contribution', async () => {
|
||||
github.context.payload.issue = {
|
||||
number: 10
|
||||
}
|
||||
github.context.payload.pull_request = undefined as any
|
||||
|
||||
mocktokit.paginate
|
||||
// Issues
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
// PRs
|
||||
.mockResolvedValueOnce([])
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(mocktokit.rest.issues.createComment).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('Adds a PR message if this is the first contribution', async () => {
|
||||
github.context.payload.issue = undefined as any
|
||||
github.context.payload.pull_request = {
|
||||
number: 10
|
||||
}
|
||||
|
||||
mocktokit.paginate
|
||||
// Issues
|
||||
.mockResolvedValueOnce([])
|
||||
// PRs
|
||||
.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
|
||||
await main.run()
|
||||
|
||||
expect(mocktokit.rest.issues.createComment).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFirstIssue()', () => {
|
||||
beforeEach(() => {
|
||||
github.context.payload.issue = {
|
||||
number: 10
|
||||
}
|
||||
})
|
||||
|
||||
it('Returns true if no issues are present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([])
|
||||
|
||||
const result = await main.isFirstIssue(mocktokit)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Returns true if only the current issue is present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstIssue(mocktokit)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Returns false if older issues are present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
},
|
||||
{
|
||||
number: 5
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstIssue(mocktokit)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Ignores pull requests', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
pull_request: {}
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstIssue(mocktokit)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Returns false if there is an error', async () => {
|
||||
mocktokit.paginate.mockRejectedValueOnce(new Error('Error'))
|
||||
|
||||
const result = await main.isFirstIssue(mocktokit)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFirstPullRequest()', () => {
|
||||
beforeEach(() => {
|
||||
github.context.payload.pull_request = {
|
||||
number: 10
|
||||
}
|
||||
})
|
||||
|
||||
it('Returns true if no PRs are present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([])
|
||||
|
||||
const result = await main.isFirstPullRequest(mocktokit)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Returns true if only the current PR is present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstPullRequest(mocktokit)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('Returns false if older PRs are present', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
},
|
||||
{
|
||||
number: 5
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstPullRequest(mocktokit)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Does not ignore pull requests', async () => {
|
||||
mocktokit.paginate.mockResolvedValueOnce([
|
||||
{
|
||||
number: 10
|
||||
},
|
||||
{
|
||||
number: 5,
|
||||
pull_request: {}
|
||||
}
|
||||
])
|
||||
|
||||
const result = await main.isFirstPullRequest(mocktokit)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Returns false if there is an error', async () => {
|
||||
mocktokit.paginate.mockRejectedValueOnce(new Error('Error'))
|
||||
|
||||
const result = await main.isFirstPullRequest(mocktokit)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
26
action.yml
26
action.yml
@@ -1,15 +1,17 @@
|
||||
name: 'First interaction'
|
||||
description: 'Greet new contributors when they create their first issue or open their first pull request'
|
||||
author: 'GitHub'
|
||||
name: First Interaction
|
||||
description: Greet first-time contributors when they open an issue or PR
|
||||
author: GitHub
|
||||
|
||||
inputs:
|
||||
repo-token:
|
||||
description: 'Token for the repository. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
|
||||
issue_message:
|
||||
description: Comment to post on an individual's first issue
|
||||
pr_message:
|
||||
description: Comment to post on an individual's first pull request
|
||||
repo_token:
|
||||
description: Token with permissions to post issue and PR comments
|
||||
required: true
|
||||
issue-message:
|
||||
description: 'Comment to post on an individual''s first issue'
|
||||
pr-message:
|
||||
description: 'Comment to post on an individual''s first pull request'
|
||||
default: ${{ github.token }}
|
||||
|
||||
runs:
|
||||
using: 'docker'
|
||||
image: 'Dockerfile'
|
||||
|
||||
using: node20
|
||||
main: dist/index.js
|
||||
|
||||
1
badges/coverage.svg
Normal file
1
badges/coverage.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="106" height="20" role="img" aria-label="Coverage: 100%"><title>Coverage: 100%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="106" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="43" height="20" fill="#4c1"/><rect width="106" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="835" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="330">100%</text><text x="835" y="140" transform="scale(.1)" fill="#fff" textLength="330">100%</text></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
1
dist/index.d.ts
generated
vendored
Normal file
1
dist/index.d.ts
generated
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
34859
dist/index.js
generated
vendored
Normal file
34859
dist/index.js
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/index.js.map
generated
vendored
Normal file
1
dist/index.js.map
generated
vendored
Normal file
File diff suppressed because one or more lines are too long
16
dist/main.d.ts
generated
vendored
Normal file
16
dist/main.d.ts
generated
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
export declare function run(): Promise<void>;
|
||||
/**
|
||||
* Checks if this is the user's first issue.
|
||||
*
|
||||
* @param octokit Octokit instance
|
||||
* @returns true if this is the user's first issue
|
||||
*/
|
||||
export declare function isFirstIssue(octokit: Octokit): Promise<boolean>;
|
||||
/**
|
||||
* Checks if this is the user's first pull request.
|
||||
*
|
||||
* @param octokit Octokit instance
|
||||
* @returns true if this is the user's first pull request
|
||||
*/
|
||||
export declare function isFirstPullRequest(octokit: Octokit): Promise<boolean>;
|
||||
155
lib/main.js
155
lib/main.js
@@ -1,155 +0,0 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; } });
|
||||
}) : (function (o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function (o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function (o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const core = __importStar(require("@actions/core"));
|
||||
const github = __importStar(require("@actions/github"));
|
||||
function run() {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
try {
|
||||
const issueMessage = core.getInput('issue-message');
|
||||
const prMessage = core.getInput('pr-message');
|
||||
if (!issueMessage && !prMessage) {
|
||||
throw new Error('Action must have at least one of issue-message or pr-message set');
|
||||
}
|
||||
// Get client and context
|
||||
const client = github.getOctokit(core.getInput('repo-token', { required: true }));
|
||||
const context = github.context;
|
||||
if (context.payload.action !== 'opened') {
|
||||
console.log('No issue or PR was opened, skipping');
|
||||
return;
|
||||
}
|
||||
// Do nothing if its not a pr or issue
|
||||
const isIssue = !!context.payload.issue;
|
||||
if (!isIssue && !context.payload.pull_request) {
|
||||
console.log('The event that triggered this action was not a pull request or issue, skipping.');
|
||||
return;
|
||||
}
|
||||
// Do nothing if its not their first contribution
|
||||
console.log('Checking if its the users first contribution');
|
||||
if (!context.payload.sender) {
|
||||
throw new Error('Internal error, no sender provided by GitHub');
|
||||
}
|
||||
const sender = context.payload.sender.login;
|
||||
const issue = context.issue;
|
||||
let firstContribution = false;
|
||||
if (isIssue) {
|
||||
firstContribution = yield isFirstIssue(client, issue.owner, issue.repo, sender, issue.number);
|
||||
}
|
||||
else {
|
||||
firstContribution = yield isFirstPull(client, issue.owner, issue.repo, sender, issue.number);
|
||||
}
|
||||
if (!firstContribution) {
|
||||
console.log('Not the users first contribution');
|
||||
return;
|
||||
}
|
||||
// Do nothing if no message set for this type of contribution
|
||||
const message = isIssue ? issueMessage : prMessage;
|
||||
if (!message) {
|
||||
console.log('No message provided for this type of contribution');
|
||||
return;
|
||||
}
|
||||
const issueType = isIssue ? 'issue' : 'pull request';
|
||||
// Add a comment to the appropriate place
|
||||
console.log(`Adding message: ${message} to ${issueType} ${issue.number}`);
|
||||
if (isIssue) {
|
||||
yield client.rest.issues.createComment({
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
issue_number: issue.number,
|
||||
body: message
|
||||
});
|
||||
}
|
||||
else {
|
||||
yield client.rest.pulls.createReview({
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
pull_number: issue.number,
|
||||
body: message,
|
||||
event: 'COMMENT'
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
core.setFailed(error.message);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
function isFirstIssue(client, owner, repo, sender, curIssueNumber) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
const { status, data: issues } = yield client.rest.issues.listForRepo({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
creator: sender,
|
||||
state: 'all'
|
||||
});
|
||||
if (status !== 200) {
|
||||
throw new Error(`Received unexpected API status code ${status}`);
|
||||
}
|
||||
if (issues.length === 0) {
|
||||
return true;
|
||||
}
|
||||
for (const issue of issues) {
|
||||
if (issue.number < curIssueNumber && !issue.pull_request) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
// No way to filter pulls by creator
|
||||
function isFirstPull(client, owner, repo, sender, curPullNumber, page = 1) {
|
||||
var _a;
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
// Provide console output if we loop for a while.
|
||||
console.log('Checking...');
|
||||
const { status, data: pulls } = yield client.rest.pulls.list({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
per_page: 100,
|
||||
page: page,
|
||||
state: 'all'
|
||||
});
|
||||
if (status !== 200) {
|
||||
throw new Error(`Received unexpected API status code ${status}`);
|
||||
}
|
||||
if (pulls.length === 0) {
|
||||
return true;
|
||||
}
|
||||
for (const pull of pulls) {
|
||||
const login = (_a = pull.user) === null || _a === void 0 ? void 0 : _a.login;
|
||||
if (login === sender && pull.number < curPullNumber) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return yield isFirstPull(client, owner, repo, sender, curPullNumber, page + 1);
|
||||
});
|
||||
}
|
||||
run();
|
||||
11879
package-lock.json
generated
Normal file
11879
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
95
package.json
95
package.json
@@ -1,46 +1,77 @@
|
||||
{
|
||||
"name": "first-interaction-action",
|
||||
"version": "1.3.0",
|
||||
"description": "An action for greeting first time contributors.",
|
||||
"main": "lib/main.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"test": "jest"
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"author": "GitHub",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/actions/first-interaction",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/first-interaction.git"
|
||||
},
|
||||
"keywords": [
|
||||
"actions",
|
||||
"container",
|
||||
"toolkit",
|
||||
"first",
|
||||
"interaction"
|
||||
],
|
||||
"author": "GitHub",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/first-interaction/issues"
|
||||
},
|
||||
"homepage": "https://github.com/actions/first-interaction#readme",
|
||||
"keywords": [
|
||||
"actions",
|
||||
"first",
|
||||
"interaction"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"bundle": "npm run format:write && npm run package",
|
||||
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
|
||||
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
|
||||
"format:write": "npx prettier --write .",
|
||||
"format:check": "npx prettier --check .",
|
||||
"lint": "npx eslint .",
|
||||
"local-action": "npx local-action . src/main.ts .env",
|
||||
"package": "npx rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
|
||||
"package:watch": "npm run package -- --watch",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
|
||||
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "file:toolkit/actions-core-1.10.0.tgz",
|
||||
"@actions/exec": "file:toolkit/actions-exec-1.1.1.tgz",
|
||||
"@actions/github": "file:toolkit/actions-github-5.1.1.tgz",
|
||||
"@actions/http-client": "^2.2.0",
|
||||
"@actions/io": "file:toolkit/actions-io-1.1.2.tgz",
|
||||
"@actions/tool-cache": "file:toolkit/actions-tool-cache-2.0.1.tgz",
|
||||
"@octokit/rest": "file:toolkit/octokit-rest.js-20.0.2.tgz"
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/github": "^6.0.0",
|
||||
"@octokit/rest": "^21.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/node": "^12.0.4",
|
||||
"jest": "^24.8.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"prettier": "^1.17.1",
|
||||
"ts-jest": "^24.0.2",
|
||||
"typescript": "^3.5.1"
|
||||
"@eslint/compat": "^1.2.6",
|
||||
"@github/local-action": "^2.6.2",
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@octokit/types": "^13.8.0",
|
||||
"@rollup/plugin-commonjs": "^28.0.2",
|
||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
||||
"@typescript-eslint/parser": "^8.24.1",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-import-resolver-typescript": "^3.8.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-jest": "^28.11.0",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"jest": "^29.7.0",
|
||||
"make-coverage-badge": "^1.2.0",
|
||||
"prettier": "^3.5.1",
|
||||
"prettier-eslint": "^16.3.0",
|
||||
"rollup": "^4.34.8",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-jest-resolver": "^2.0.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-linux-x64-gnu": "*"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
rollup.config.ts
Normal file
22
rollup.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// See: https://rollupjs.org/introduction/
|
||||
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import nodeResolve from '@rollup/plugin-node-resolve'
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
|
||||
const config = {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
esModule: true,
|
||||
file: 'dist/index.js',
|
||||
format: 'es',
|
||||
sourcemap: true
|
||||
},
|
||||
plugins: [
|
||||
typescript({ sourceMap: true }),
|
||||
nodeResolve({ preferBuiltins: true }),
|
||||
commonjs()
|
||||
]
|
||||
}
|
||||
|
||||
export default config
|
||||
8
src/index.ts
Normal file
8
src/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* The entrypoint for the action. This file simply imports and runs the action's
|
||||
* main logic.
|
||||
*/
|
||||
import { run } from './main.js'
|
||||
|
||||
/* istanbul ignore next */
|
||||
run()
|
||||
261
src/main.ts
261
src/main.ts
@@ -1,170 +1,111 @@
|
||||
import * as core from '@actions/core';
|
||||
import * as github from '@actions/github';
|
||||
import * as core from '@actions/core'
|
||||
import * as github from '@actions/github'
|
||||
import { Octokit } from '@octokit/rest'
|
||||
|
||||
async function run() {
|
||||
export async function run() {
|
||||
core.info('Running actions/first-interaction!')
|
||||
|
||||
// Skip if this is not an issue or PR event.
|
||||
if (
|
||||
github.context.eventName !== 'issues' &&
|
||||
github.context.eventName !== 'pull_request'
|
||||
)
|
||||
return core.info('Skipping...Not an Issue/PR Event')
|
||||
|
||||
// Skip if this is not an issue/PR open event.
|
||||
if (github.context.action !== 'opened')
|
||||
return core.info('Skipping...Not an Opened Event')
|
||||
|
||||
// Confirm the sender data is present.
|
||||
if (!github.context.payload.sender)
|
||||
return core.setFailed('Internal Error...No Sender Provided by GitHub')
|
||||
|
||||
// Check if this is an issue or PR event.
|
||||
const isIssue = github.context.payload.issue !== undefined
|
||||
const isPullRequest = github.context.payload.pull_request !== undefined
|
||||
|
||||
// Confirm that only one of the two is present.
|
||||
if (!isIssue && !isPullRequest)
|
||||
return core.setFailed('Internal Error...No Issue or PR Provided by GitHub')
|
||||
if (isIssue && isPullRequest)
|
||||
return core.setFailed(
|
||||
'Internal Error...Both Issue and PR Provided by GitHub'
|
||||
)
|
||||
|
||||
// Get the action inputs.
|
||||
const issueMessage: string = core.getInput('issue_message', {
|
||||
required: true
|
||||
})
|
||||
const prMessage: string = core.getInput('pr_message', { required: true })
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: core.getInput('repo_token', { required: true })
|
||||
})
|
||||
|
||||
// Check if this is the user's first contribution.
|
||||
if (!(await isFirstIssue(octokit)) && !(await isFirstPullRequest(octokit)))
|
||||
return core.info('Skipping...Not First Contribution')
|
||||
|
||||
core.info(`Adding Message to #${github.context.issue.number}`)
|
||||
|
||||
await octokit.rest.issues.createComment({
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
issue_number: github.context.issue.number,
|
||||
body: isIssue ? issueMessage : prMessage
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this is the user's first issue.
|
||||
*
|
||||
* @param octokit Octokit instance
|
||||
* @returns true if this is the user's first issue
|
||||
*/
|
||||
export async function isFirstIssue(octokit: Octokit): Promise<boolean> {
|
||||
try {
|
||||
const issueMessage: string = core.getInput('issue-message');
|
||||
const prMessage: string = core.getInput('pr-message');
|
||||
if (!issueMessage && !prMessage) {
|
||||
throw new Error(
|
||||
'Action must have at least one of issue-message or pr-message set'
|
||||
);
|
||||
}
|
||||
// Get client and context
|
||||
const client = github.getOctokit(
|
||||
core.getInput('repo-token', {required: true})
|
||||
);
|
||||
const context = github.context;
|
||||
const issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
creator: github.context.payload.sender!.login,
|
||||
state: 'all'
|
||||
})
|
||||
|
||||
if (context.payload.action !== 'opened') {
|
||||
console.log('No issue or PR was opened, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing if its not a pr or issue
|
||||
const isIssue: boolean = !!context.payload.issue;
|
||||
if (!isIssue && !context.payload.pull_request) {
|
||||
console.log(
|
||||
'The event that triggered this action was not a pull request or issue, skipping.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing if its not their first contribution
|
||||
console.log('Checking if its the users first contribution');
|
||||
if (!context.payload.sender) {
|
||||
throw new Error('Internal error, no sender provided by GitHub');
|
||||
}
|
||||
const sender: string = context.payload.sender!.login;
|
||||
const issue: {owner: string; repo: string; number: number} = context.issue;
|
||||
let firstContribution: boolean = false;
|
||||
if (isIssue) {
|
||||
firstContribution = await isFirstIssue(
|
||||
client,
|
||||
issue.owner,
|
||||
issue.repo,
|
||||
sender,
|
||||
issue.number
|
||||
);
|
||||
} else {
|
||||
firstContribution = await isFirstPull(
|
||||
client,
|
||||
issue.owner,
|
||||
issue.repo,
|
||||
sender,
|
||||
issue.number
|
||||
);
|
||||
}
|
||||
if (!firstContribution) {
|
||||
console.log('Not the users first contribution');
|
||||
return;
|
||||
}
|
||||
|
||||
// Do nothing if no message set for this type of contribution
|
||||
const message: string = isIssue ? issueMessage : prMessage;
|
||||
if (!message) {
|
||||
console.log('No message provided for this type of contribution');
|
||||
return;
|
||||
}
|
||||
|
||||
const issueType: string = isIssue ? 'issue' : 'pull request';
|
||||
// Add a comment to the appropriate place
|
||||
console.log(`Adding message: ${message} to ${issueType} ${issue.number}`);
|
||||
if (isIssue) {
|
||||
await client.rest.issues.createComment({
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
issue_number: issue.number,
|
||||
body: message
|
||||
});
|
||||
} else {
|
||||
await client.rest.pulls.createReview({
|
||||
owner: issue.owner,
|
||||
repo: issue.repo,
|
||||
pull_number: issue.number,
|
||||
body: message,
|
||||
event: 'COMMENT'
|
||||
});
|
||||
}
|
||||
return (
|
||||
issues
|
||||
// Filter out PRs.
|
||||
.filter((issue) => issue.pull_request === undefined)
|
||||
// Filter out any issue that are newer than the current issue.
|
||||
.filter((issue) => issue.number < github.context.issue.number)
|
||||
.length === 0
|
||||
)
|
||||
} catch (error) {
|
||||
core.setFailed((error as any).message);
|
||||
return;
|
||||
core.setFailed((error as any).message)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
async function isFirstIssue(
|
||||
client: ReturnType<typeof github.getOctokit>,
|
||||
owner: string,
|
||||
repo: string,
|
||||
sender: string,
|
||||
curIssueNumber: number
|
||||
): Promise<boolean> {
|
||||
const {status, data: issues} = await client.rest.issues.listForRepo({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
creator: sender,
|
||||
state: 'all'
|
||||
});
|
||||
/**
|
||||
* Checks if this is the user's first pull request.
|
||||
*
|
||||
* @param octokit Octokit instance
|
||||
* @returns true if this is the user's first pull request
|
||||
*/
|
||||
export async function isFirstPullRequest(octokit: Octokit): Promise<boolean> {
|
||||
try {
|
||||
const pulls = await octokit.paginate(octokit.rest.pulls.list, {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
state: 'all'
|
||||
})
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error(`Received unexpected API status code ${status}`);
|
||||
return (
|
||||
// Filter out any PRs that are newer than the current one.
|
||||
pulls.filter((pull) => pull.number < github.context.issue.number)
|
||||
.length === 0
|
||||
)
|
||||
} catch (error) {
|
||||
core.setFailed((error as any).message)
|
||||
return false
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const issue of issues) {
|
||||
if (issue.number < curIssueNumber && !issue.pull_request) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// No way to filter pulls by creator
|
||||
async function isFirstPull(
|
||||
client: ReturnType<typeof github.getOctokit>,
|
||||
owner: string,
|
||||
repo: string,
|
||||
sender: string,
|
||||
curPullNumber: number,
|
||||
page: number = 1
|
||||
): Promise<boolean> {
|
||||
// Provide console output if we loop for a while.
|
||||
console.log('Checking...');
|
||||
const {status, data: pulls} = await client.rest.pulls.list({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
per_page: 100,
|
||||
page: page,
|
||||
state: 'all'
|
||||
});
|
||||
|
||||
if (status !== 200) {
|
||||
throw new Error(`Received unexpected API status code ${status}`);
|
||||
}
|
||||
|
||||
if (pulls.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const pull of pulls) {
|
||||
const login = pull.user?.login;
|
||||
if (login === sender && pull.number < curPullNumber) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return await isFirstPull(
|
||||
client,
|
||||
owner,
|
||||
repo,
|
||||
sender,
|
||||
curPullNumber,
|
||||
page + 1
|
||||
);
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
Binary file not shown.
22
tsconfig.base.json
Normal file
22
tsconfig.base.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"lib": ["ES2022"],
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"newLine": "lf",
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": false,
|
||||
"pretty": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"target": "ES2022"
|
||||
}
|
||||
}
|
||||
17
tsconfig.eslint.json
Normal file
17
tsconfig.eslint.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"exclude": ["dist", "node_modules"],
|
||||
"include": [
|
||||
"__fixtures__",
|
||||
"__tests__",
|
||||
"src",
|
||||
"eslint.config.mjs",
|
||||
"jest.config.js",
|
||||
"rollup.config.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,66 +1,11 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"**/*.test.ts"
|
||||
]
|
||||
"exclude": ["__fixtures__", "__tests__", "coverage", "dist", "node_modules"],
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user