mirror of
https://github.com/dessant/lock-threads.git
synced 2026-03-13 01:27:03 -04:00
feat: lock discussions
BREAKING CHANGE: Discussions are also processed by default,
set the `process-only` input parameter to preserve the old behavior
```yaml
steps:
- uses: dessant/lock-threads@v5
with:
process-only: 'issues, prs'
```
Closes #25.
This commit is contained in:
166
README.md
166
README.md
@@ -1,9 +1,9 @@
|
||||
# Lock Threads
|
||||
|
||||
Lock Threads is a GitHub Action that locks closed issues
|
||||
and pull requests after a period of inactivity.
|
||||
Lock Threads is a GitHub Action that locks closed issues,
|
||||
pull requests and discussions after a period of inactivity.
|
||||
|
||||
<img width="800" src="https://raw.githubusercontent.com/dessant/lock-threads/master/assets/screenshot.png">
|
||||
<img width="800" src="https://raw.githubusercontent.com/dessant/lock-threads/main/assets/screenshot.png">
|
||||
|
||||
## Supporting the Project
|
||||
|
||||
@@ -16,13 +16,13 @@ please consider contributing with
|
||||
|
||||
## Usage
|
||||
|
||||
Create the `lock.yml` workflow file in the `.github/workflows` directory,
|
||||
use one of the [example workflows](#examples) to get started.
|
||||
Create the `lock-threads.yml` workflow file in the `.github/workflows`
|
||||
directory, use one of the [example workflows](#examples) to get started.
|
||||
|
||||
### Inputs
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
The action can be configured using [input parameters](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswith).
|
||||
The action can be configured using [input parameters](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith).
|
||||
|
||||
- **`github-token`**
|
||||
- GitHub access token, value must be `${{ github.token }}` or an encrypted
|
||||
@@ -146,9 +146,65 @@ The action can be configured using [input parameters](https://docs.github.com/en
|
||||
- Reason for locking a pull request, value must be one
|
||||
of `resolved`, `off-topic`, `too heated`, `spam` or `''`
|
||||
- Optional, defaults to `resolved`
|
||||
- **`discussion-inactive-days`**
|
||||
- Number of days of inactivity before a closed discussion is locked
|
||||
- Optional, defaults to `365`
|
||||
- **`exclude-discussion-created-before`**
|
||||
- Do not lock discussions created before a given date,
|
||||
value must follow ISO 8601, ignored
|
||||
when `exclude-discussion-created-between` is set
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-discussion-created-after`**
|
||||
- Do not lock discussions created after a given date,
|
||||
value must follow ISO 8601, ignored
|
||||
when `exclude-discussion-created-between` is set
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-discussion-created-between`**
|
||||
- Do not lock discussions created in a given time interval,
|
||||
value must follow ISO 8601
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-discussion-closed-before`**
|
||||
- Do not lock discussions closed before a given date,
|
||||
value must follow ISO 8601, ignored
|
||||
when `exclude-discussion-closed-between` is set
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-discussion-closed-after`**
|
||||
- Do not lock discussions closed after a given date,
|
||||
value must follow ISO 8601, ignored
|
||||
when `exclude-discussion-closed-between` is set
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-discussion-closed-between`**
|
||||
- Do not lock discussions closed in a given time interval,
|
||||
value must follow ISO 8601
|
||||
- Optional, defaults to `''`
|
||||
- **`include-any-discussion-labels`**
|
||||
- Only lock discussions with any of these labels, value must be
|
||||
a comma separated list of labels or `''`, ignored
|
||||
when `include-all-discussion-labels` is set
|
||||
- Optional, defaults to `''`
|
||||
- **`include-all-discussion-labels`**
|
||||
- Only lock discussions with all these labels, value must be
|
||||
a comma separated list of labels or `''`
|
||||
- Optional, defaults to `''`
|
||||
- **`exclude-any-discussion-labels`**
|
||||
- Do not lock discussions with any of these labels, value must be
|
||||
a comma separated list of labels or `''`
|
||||
- Optional, defaults to `''`
|
||||
- **`add-discussion-labels`**
|
||||
- Labels to add before locking a discussion, value must be
|
||||
a comma separated list of labels or `''`
|
||||
- Optional, defaults to `''`
|
||||
- **`remove-discussion-labels`**
|
||||
- Labels to remove before locking a discussion, value must be
|
||||
a comma separated list of labels or `''`
|
||||
- Optional, defaults to `''`
|
||||
- **`discussion-comment`**
|
||||
- Comment to post before locking a discussion
|
||||
- Optional, defaults to `''`
|
||||
- **`process-only`**
|
||||
- Limit locking to only issues or pull requests, value must be
|
||||
one of `issues`, `prs` or `''`
|
||||
- Only lock issues, pull requests or discussions,
|
||||
value must be a comma separated list, list items must be
|
||||
one of `issues`, `prs` or `discussions`
|
||||
- Optional, defaults to `''`
|
||||
- **`log-output`**
|
||||
- Log output parameters, value must be either `true` or `false`
|
||||
@@ -165,11 +221,15 @@ The action can be configured using [input parameters](https://docs.github.com/en
|
||||
- Pull requests that have been locked, value is a JSON string in the form
|
||||
of `[{"owner": "actions", "repo": "toolkit", "issue_number": 1}]`
|
||||
- Defaults to `''`
|
||||
- **`discussions`**
|
||||
- Discussions that have been locked, value is a JSON string in the form
|
||||
of `[{"owner": "actions", "repo": "toolkit", "discussion_number": 1}]`
|
||||
- Defaults to `''`
|
||||
|
||||
## Examples
|
||||
|
||||
The following workflow will search once an hour for closed issues
|
||||
and pull requests that have not had any activity
|
||||
The following workflow will search once an hour for closed issues,
|
||||
pull requests and discussions that have not had any activity
|
||||
in the past year and can be locked.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
@@ -184,19 +244,20 @@ on:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
```
|
||||
|
||||
Edit the workflow after the initial backlog of issues and pull requests
|
||||
has been processed to reduce the frequency of scheduled runs.
|
||||
Edit the workflow after the initial backlog of issues, pull requests
|
||||
and discussions has been processed to reduce the frequency of scheduled runs.
|
||||
Running the workflow only once a day helps reduce resource usage.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
@@ -223,15 +284,16 @@ on:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-inactive-days: '365'
|
||||
@@ -262,11 +324,24 @@ jobs:
|
||||
remove-pr-labels: ''
|
||||
pr-comment: ''
|
||||
pr-lock-reason: 'resolved'
|
||||
discussion-inactive-days: '365'
|
||||
exclude-discussion-created-before: ''
|
||||
exclude-discussion-created-after: ''
|
||||
exclude-discussion-created-between: ''
|
||||
exclude-discussion-closed-before: ''
|
||||
exclude-discussion-closed-after: ''
|
||||
exclude-discussion-closed-between: ''
|
||||
include-any-discussion-labels: ''
|
||||
include-all-discussion-labels: ''
|
||||
exclude-any-discussion-labels: ''
|
||||
add-discussion-labels: ''
|
||||
remove-discussion-labels: ''
|
||||
discussion-comment: ''
|
||||
process-only: ''
|
||||
log-output: false
|
||||
```
|
||||
|
||||
### Filtering issues and pull requests
|
||||
### Filtering issues, pull requests and discussions
|
||||
|
||||
This step will lock only issues, and exclude issues created before 2018,
|
||||
or those with the `upstream` or `help-wanted` labels applied.
|
||||
@@ -274,7 +349,7 @@ or those with the `upstream` or `help-wanted` labels applied.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
exclude-issue-created-before: '2018-01-01T00:00:00Z'
|
||||
exclude-any-issue-labels: 'upstream, help-wanted'
|
||||
@@ -287,7 +362,7 @@ with the `wip` label applied.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
exclude-any-pr-labels: 'wip'
|
||||
process-only: 'prs'
|
||||
@@ -299,7 +374,7 @@ or those created in 2018 and 2019.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
exclude-issue-created-between: '2018-01-01T00:00:00Z/2019-12-31T23:59:59.999Z'
|
||||
exclude-issue-closed-before: '2018-01-01T00:00:00Z'
|
||||
@@ -313,22 +388,24 @@ labels applied.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
include-any-issue-labels: 'incomplete, invalid'
|
||||
include-all-pr-labels: 'qa: done, published'
|
||||
process-only: 'issues, prs'
|
||||
|
||||
```
|
||||
|
||||
This step will lock issues that have not had any activity in the past 180 days.
|
||||
This step will lock discussions that have not had any activity
|
||||
in the past 180 days.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
issue-inactive-days: '180'
|
||||
process-only: 'issues'
|
||||
discussion-inactive-days: '180'
|
||||
process-only: 'discussions'
|
||||
|
||||
```
|
||||
|
||||
@@ -340,7 +417,7 @@ and apply the `outdated` label to issues.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
add-issue-labels: 'outdated'
|
||||
issue-comment: >
|
||||
@@ -351,6 +428,7 @@ and apply the `outdated` label to issues.
|
||||
This pull request has been automatically locked since there
|
||||
has not been any recent activity after it was closed.
|
||||
Please open a new issue for related bugs.
|
||||
process-only: 'issues, prs'
|
||||
```
|
||||
|
||||
This step will apply the `qa: done` and `archived` labels,
|
||||
@@ -360,10 +438,11 @@ before locking issues.
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
add-issue-labels: 'qa: done, archived'
|
||||
remove-issue-labels: 'qa: primary, needs: user feedback'
|
||||
process-only: 'issues'
|
||||
```
|
||||
|
||||
### Using a personal access token
|
||||
@@ -372,39 +451,38 @@ The action uses an installation access token by default to interact with GitHub.
|
||||
You may also authenticate with a personal access token to perform actions
|
||||
as a GitHub user instead of the `github-actions` app.
|
||||
|
||||
Create a [personal access token](https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token)
|
||||
Create a [personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic)
|
||||
with the `repo` or `public_repo` scopes enabled, and add the token as an
|
||||
[encrypted secret](https://docs.github.com/en/actions/reference/encrypted-secrets#creating-encrypted-secrets-for-a-repository)
|
||||
[encrypted secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-a-repository)
|
||||
for the repository or organization, then provide the action with the secret
|
||||
using the `github-token` input parameter.
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
```yaml
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4
|
||||
- uses: dessant/lock-threads@v5
|
||||
with:
|
||||
github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
```
|
||||
|
||||
## How are issues and pull requests determined to be inactive?
|
||||
## How are issues, pull requests and discussions determined to be inactive?
|
||||
|
||||
The action uses GitHub's [updated](https://help.github.com/en/github/searching-for-information-on-github/searching-issues-and-pull-requests#search-by-when-an-issue-or-pull-request-was-created-or-last-updated)
|
||||
search qualifier to determine inactivity. Any change to an issue or pull request
|
||||
is considered an update, including comments, changing labels,
|
||||
applying or removing milestones, or pushing commits.
|
||||
The action uses GitHub's [updated](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests#search-by-when-an-issue-or-pull-request-was-created-or-last-updated)
|
||||
search qualifier to determine inactivity. Any change to an issue, pull request
|
||||
or discussion is considered an update, including new comments,
|
||||
or changing labels.
|
||||
|
||||
An easy way to check and see which issues or pull requests will initially
|
||||
be locked is to add the `updated` search qualifier to either the issue
|
||||
or pull request search field for your repository:
|
||||
An easy way to see which threads will initially be locked is to add
|
||||
the `updated` search qualifier to the issue, pull request or discussion
|
||||
search field for your repository, adjust the date based on the value
|
||||
of the `*-inactive-days` input parameter:
|
||||
`is:closed is:unlocked updated:<2018-12-20`.
|
||||
Adjust the date to be 365 days ago (or whatever you set for `*-inactive-days`)
|
||||
to see which issues or pull requests will be locked.
|
||||
|
||||
## Why are only some issues and pull requests processed?
|
||||
## Why are only some issues, pull requests and discussions processed?
|
||||
|
||||
To avoid triggering abuse prevention mechanisms on GitHub, only 50 issues
|
||||
and pull requests will be handled at once. If your repository has more
|
||||
than that, it will just take a few hours or days to process them all.
|
||||
To avoid triggering abuse prevention mechanisms on GitHub, only 50 threads
|
||||
will be handled at a time. If your repository has more than that,
|
||||
it will take a few hours or days to process them all.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
45
action.yml
45
action.yml
@@ -1,5 +1,5 @@
|
||||
name: 'Lock Threads'
|
||||
description: 'Lock closed issues and pull requests after a period of inactivity'
|
||||
description: 'Lock closed issues, pull requests and discussions after a period of inactivity'
|
||||
author: 'Armin Sebastian'
|
||||
inputs:
|
||||
github-token:
|
||||
@@ -89,8 +89,47 @@ inputs:
|
||||
pr-lock-reason:
|
||||
description: 'Reason for locking a pull request, value must be one of `resolved`, `off-topic`, `too heated` or `spam`'
|
||||
default: 'resolved'
|
||||
discussion-inactive-days:
|
||||
description: 'Number of days of inactivity before a closed discussion is locked'
|
||||
default: '365'
|
||||
exclude-discussion-created-before:
|
||||
description: 'Do not lock discussions created before a given date, value must follow ISO 8601'
|
||||
default: ''
|
||||
exclude-discussion-created-after:
|
||||
description: 'Do not lock discussions created after a given date, value must follow ISO 8601'
|
||||
default: ''
|
||||
exclude-discussion-created-between:
|
||||
description: 'Do not lock discussions created in a given time interval, value must follow ISO 8601'
|
||||
default: ''
|
||||
exclude-discussion-closed-before:
|
||||
description: 'Do not lock discussions closed before a given date, value must follow ISO 8601'
|
||||
default: ''
|
||||
exclude-discussion-closed-after:
|
||||
description: 'Do not lock discussions closed after a given date, value must follow ISO 8601'
|
||||
default: ''
|
||||
exclude-discussion-closed-between:
|
||||
description: 'Do not lock discussions closed in a given time interval, value must follow ISO 8601'
|
||||
default: ''
|
||||
include-any-discussion-labels:
|
||||
description: 'Only lock issues with any of these labels, value must be a comma separated list of labels'
|
||||
default: ''
|
||||
include-all-discussion-labels:
|
||||
description: 'Only lock discussions with all these labels, value must be a comma separated list of labels'
|
||||
default: ''
|
||||
exclude-any-discussion-labels:
|
||||
description: 'Do not lock discussions with any of these labels, value must be a comma separated list of labels'
|
||||
default: ''
|
||||
add-discussion-labels:
|
||||
description: 'Labels to add before locking a discussion, value must be a comma separated list of labels'
|
||||
default: ''
|
||||
remove-discussion-labels:
|
||||
description: 'Labels to remove before locking a discussion, value must be a comma separated list of labels'
|
||||
default: ''
|
||||
discussion-comment:
|
||||
description: 'Comment to post before locking a discussion'
|
||||
default: ''
|
||||
process-only:
|
||||
description: 'Limit locking to only issues or pull requests, value must be one of `issues` or `prs`'
|
||||
description: 'Only lock issues, pull requests or discussions, value must be a comma separated list, list items must be one of `issues`, `prs` or `discussions`'
|
||||
default: ''
|
||||
log-output:
|
||||
description: 'Log output parameters, value must be either `true` or `false`'
|
||||
@@ -100,6 +139,8 @@ outputs:
|
||||
description: 'Issues that have been locked, value is a JSON string'
|
||||
prs:
|
||||
description: 'Pull requests that have been locked, value is a JSON string'
|
||||
discussions:
|
||||
description: 'Discussions that have been locked, value is a JSON string'
|
||||
runs:
|
||||
using: 'node20'
|
||||
main: 'dist/index.js'
|
||||
|
||||
113
src/data.js
Normal file
113
src/data.js
Normal file
@@ -0,0 +1,113 @@
|
||||
const addDiscussionCommentQuery = `
|
||||
mutation ($discussionId: ID!, $body: String!) {
|
||||
addDiscussionComment(input: {discussionId: $discussionId, body: $body}) {
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getLabelQuery = `
|
||||
query ($owner: String!, $repo: String!, $label: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
label(name: $label) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const createLabelQuery = `
|
||||
mutation ($repositoryId: ID!, $name: String!, $color: String!) {
|
||||
createLabel(input: {repositoryId: $repositoryId, name: $name, , color: $color}) {
|
||||
label {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const getDiscussionLabelsQuery = `
|
||||
query ($owner: String!, $repo: String!, $discussion: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
discussion(number: $discussion) {
|
||||
number
|
||||
labels(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const addLabelsToLabelableQuery = `
|
||||
mutation ($labelableId: ID!, $labelIds: [ID!]!) {
|
||||
addLabelsToLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
|
||||
labelable {
|
||||
labels(first: 0) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const removeLabelsFromLabelableQuery = `
|
||||
mutation ($labelableId: ID!, $labelIds: [ID!]!) {
|
||||
removeLabelsFromLabelable(input: {labelableId: $labelableId, labelIds: $labelIds}) {
|
||||
labelable {
|
||||
labels(first: 0) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const lockLockableQuery = `
|
||||
mutation ($lockableId: ID!) {
|
||||
lockLockable(input: {lockableId: $lockableId}) {
|
||||
lockedRecord {
|
||||
locked
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const searchDiscussionsQuery = `
|
||||
query ($q: String!) {
|
||||
search(type: DISCUSSION, first: 50, query: $q) {
|
||||
nodes {
|
||||
... on Discussion {
|
||||
id
|
||||
number
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export {
|
||||
searchDiscussionsQuery,
|
||||
addDiscussionCommentQuery,
|
||||
getLabelQuery,
|
||||
createLabelQuery,
|
||||
getDiscussionLabelsQuery,
|
||||
addLabelsToLabelableQuery,
|
||||
removeLabelsFromLabelableQuery,
|
||||
lockLockableQuery
|
||||
};
|
||||
269
src/index.js
269
src/index.js
@@ -1,8 +1,17 @@
|
||||
import core from '@actions/core';
|
||||
import github from '@actions/github';
|
||||
|
||||
import {schema} from './schema.js';
|
||||
import {getClient} from './utils.js';
|
||||
import {getConfig, getClient} from './utils.js';
|
||||
import {
|
||||
searchDiscussionsQuery,
|
||||
addDiscussionCommentQuery,
|
||||
getLabelQuery,
|
||||
createLabelQuery,
|
||||
getDiscussionLabelsQuery,
|
||||
addLabelsToLabelableQuery,
|
||||
removeLabelsFromLabelableQuery,
|
||||
lockLockableQuery
|
||||
} from './data.js';
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
@@ -23,10 +32,10 @@ class App {
|
||||
}
|
||||
|
||||
async lockThreads() {
|
||||
const type = this.config['process-only'];
|
||||
const processOnly = this.config['process-only'];
|
||||
const logOutput = this.config['log-output'];
|
||||
|
||||
const threadTypes = type ? [type] : ['issue', 'pr'];
|
||||
const threadTypes = processOnly || ['issue', 'pr', 'discussion'];
|
||||
for (const item of threadTypes) {
|
||||
const threads = await this.lock(item);
|
||||
|
||||
@@ -44,93 +53,173 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
async lock(type) {
|
||||
const repo = github.context.repo;
|
||||
const addLabels = this.config[`add-${type}-labels`];
|
||||
const removeLabels = this.config[`remove-${type}-labels`];
|
||||
const comment = this.config[`${type}-comment`];
|
||||
const lockReason = this.config[`${type}-lock-reason`];
|
||||
async lock(threadType) {
|
||||
const {owner, repo} = github.context.repo;
|
||||
|
||||
const addLabels = this.config[`add-${threadType}-labels`];
|
||||
const removeLabels = this.config[`remove-${threadType}-labels`];
|
||||
const comment = this.config[`${threadType}-comment`];
|
||||
const lockReason = this.config[`${threadType}-lock-reason`];
|
||||
|
||||
const threads = [];
|
||||
|
||||
const results = await this.search(type);
|
||||
const results = await this.search(threadType);
|
||||
|
||||
for (const result of results) {
|
||||
const issue = {...repo, issue_number: result.number};
|
||||
const thread =
|
||||
threadType === 'discussion'
|
||||
? {owner, repo, discussion_number: result.number}
|
||||
: {owner, repo, issue_number: result.number};
|
||||
const threadNumber = thread.discussion_number || thread.issue_number;
|
||||
const discussionId = result.id;
|
||||
|
||||
if (comment) {
|
||||
core.debug(`Commenting (${type}: ${issue.issue_number})`);
|
||||
try {
|
||||
await this.client.rest.issues.createComment({
|
||||
...issue,
|
||||
core.debug(`Commenting (${threadType}: ${threadNumber})`);
|
||||
|
||||
if (threadType === 'discussion') {
|
||||
await this.client.graphql(addDiscussionCommentQuery, {
|
||||
discussionId,
|
||||
body: comment
|
||||
});
|
||||
} catch (err) {
|
||||
if (!/cannot be modified.*discussion/i.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addLabels || removeLabels) {
|
||||
const {data: issueData} = await this.client.rest.issues.get({...issue});
|
||||
|
||||
if (addLabels) {
|
||||
const currentLabels = issueData.labels.map(label => label.name);
|
||||
const newLabels = addLabels.filter(
|
||||
label => !currentLabels.includes(label)
|
||||
);
|
||||
|
||||
if (newLabels.length) {
|
||||
core.debug(`Labeling (${type}: ${issue.issue_number})`);
|
||||
await this.client.rest.issues.addLabels({
|
||||
...issue,
|
||||
labels: newLabels
|
||||
} else {
|
||||
try {
|
||||
await this.client.rest.issues.createComment({
|
||||
...thread,
|
||||
body: comment
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (removeLabels) {
|
||||
const currentLabels = issueData.labels.map(label => label.name);
|
||||
const matchingLabels = currentLabels.filter(label =>
|
||||
removeLabels.includes(label)
|
||||
);
|
||||
if (matchingLabels.length) {
|
||||
core.debug(`Unlabeling (${type}: ${issue.issue_number})`);
|
||||
for (const label of matchingLabels) {
|
||||
await this.client.rest.issues.removeLabel({
|
||||
...issue,
|
||||
name: label
|
||||
});
|
||||
} catch (err) {
|
||||
if (!/cannot be modified.*discussion/i.test(err.message)) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
core.debug(`Locking (${type}: ${issue.issue_number})`);
|
||||
if (addLabels || removeLabels) {
|
||||
let currentLabels;
|
||||
if (threadType === 'discussion') {
|
||||
({
|
||||
repository: {
|
||||
discussion: {
|
||||
labels: {nodes: currentLabels}
|
||||
}
|
||||
}
|
||||
} = await this.client.graphql(getDiscussionLabelsQuery, {
|
||||
owner,
|
||||
repo,
|
||||
discussion: thread.discussion_number
|
||||
}));
|
||||
} else {
|
||||
({
|
||||
data: {labels: currentLabels}
|
||||
} = await this.client.rest.issues.get({...thread}));
|
||||
}
|
||||
|
||||
const params = {...issue};
|
||||
if (addLabels) {
|
||||
const currentLabelNames = currentLabels.map(label => label.name);
|
||||
const newLabels = addLabels.filter(
|
||||
label => !currentLabelNames.includes(label)
|
||||
);
|
||||
|
||||
if (lockReason) {
|
||||
params.lock_reason = lockReason;
|
||||
if (newLabels.length) {
|
||||
core.debug(`Labeling (${threadType}: ${threadNumber})`);
|
||||
|
||||
if (threadType === 'discussion') {
|
||||
const labels = [];
|
||||
for (const labelName of newLabels) {
|
||||
let {
|
||||
repository: {label}
|
||||
} = await this.client.graphql(getLabelQuery, {
|
||||
owner,
|
||||
repo,
|
||||
label: labelName
|
||||
});
|
||||
|
||||
if (!label) {
|
||||
({
|
||||
createLabel: {label}
|
||||
} = await this.client.graphql(createLabelQuery, {
|
||||
repositoryId: github.context.payload.repository.node_id,
|
||||
name: labelName,
|
||||
color: 'ffffff',
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.bane-preview+json'
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
labels.push(label);
|
||||
}
|
||||
|
||||
await this.client.graphql(addLabelsToLabelableQuery, {
|
||||
labelableId: discussionId,
|
||||
labelIds: labels.map(label => label.id)
|
||||
});
|
||||
} else {
|
||||
await this.client.rest.issues.addLabels({
|
||||
...thread,
|
||||
labels: newLabels
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removeLabels) {
|
||||
const matchingLabels = currentLabels.filter(label =>
|
||||
removeLabels.includes(label.name)
|
||||
);
|
||||
|
||||
if (matchingLabels.length) {
|
||||
core.debug(`Unlabeling (${threadType}: ${threadNumber})`);
|
||||
|
||||
if (threadType === 'discussion') {
|
||||
await this.client.graphql(removeLabelsFromLabelableQuery, {
|
||||
labelableId: discussionId,
|
||||
labelIds: matchingLabels.map(label => label.id)
|
||||
});
|
||||
} else {
|
||||
for (const label of matchingLabels) {
|
||||
await this.client.rest.issues.removeLabel({
|
||||
...thread,
|
||||
name: label.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.client.rest.issues.lock(params);
|
||||
core.debug(`Locking (${threadType}: ${threadNumber})`);
|
||||
|
||||
threads.push(issue);
|
||||
if (threadType === 'discussion') {
|
||||
await this.client.graphql(lockLockableQuery, {
|
||||
lockableId: discussionId
|
||||
});
|
||||
} else {
|
||||
const params = {...thread};
|
||||
|
||||
if (lockReason) {
|
||||
params.lock_reason = lockReason;
|
||||
}
|
||||
|
||||
await this.client.rest.issues.lock(params);
|
||||
}
|
||||
|
||||
threads.push(thread);
|
||||
}
|
||||
|
||||
return threads;
|
||||
}
|
||||
|
||||
async search(type) {
|
||||
async search(threadType) {
|
||||
const {owner, repo} = github.context.repo;
|
||||
const updatedTime = this.getUpdatedTimestamp(
|
||||
this.config[`${type}-inactive-days`]
|
||||
this.config[`${threadType}-inactive-days`]
|
||||
);
|
||||
let query = `repo:${owner}/${repo} updated:<${updatedTime} is:closed is:unlocked`;
|
||||
|
||||
const includeAnyLabels = this.config[`include-any-${type}-labels`];
|
||||
const includeAllLabels = this.config[`include-all-${type}-labels`];
|
||||
const includeAnyLabels = this.config[`include-any-${threadType}-labels`];
|
||||
const includeAllLabels = this.config[`include-all-${threadType}-labels`];
|
||||
|
||||
if (includeAllLabels) {
|
||||
query += ` ${includeAllLabels
|
||||
@@ -140,13 +229,13 @@ class App {
|
||||
query += ` label:${includeAnyLabels.join(',')}`;
|
||||
}
|
||||
|
||||
const excludeAnyLabels = this.config[`exclude-any-${type}-labels`];
|
||||
const excludeAnyLabels = this.config[`exclude-any-${threadType}-labels`];
|
||||
if (excludeAnyLabels) {
|
||||
query += ` -label:${excludeAnyLabels.join(',')}`;
|
||||
}
|
||||
|
||||
const excludeCreatedQuery = this.getFilterByDateQuery({
|
||||
type,
|
||||
threadType,
|
||||
qualifier: 'created'
|
||||
});
|
||||
if (excludeCreatedQuery) {
|
||||
@@ -154,37 +243,48 @@ class App {
|
||||
}
|
||||
|
||||
const excludeClosedQuery = this.getFilterByDateQuery({
|
||||
type,
|
||||
threadType,
|
||||
qualifier: 'closed'
|
||||
});
|
||||
if (excludeClosedQuery) {
|
||||
query += ` ${excludeClosedQuery}`;
|
||||
}
|
||||
|
||||
if (type === 'issue') {
|
||||
if (threadType === 'issue') {
|
||||
query += ' is:issue';
|
||||
} else {
|
||||
} else if (threadType === 'pr') {
|
||||
query += ' is:pr';
|
||||
}
|
||||
|
||||
core.debug(`Searching (${type}s)`);
|
||||
const results = (
|
||||
await this.client.rest.search.issuesAndPullRequests({
|
||||
core.debug(`Searching (${threadType}s)`);
|
||||
|
||||
let results;
|
||||
if (threadType === 'discussion') {
|
||||
({
|
||||
search: {nodes: results}
|
||||
} = await this.client.graphql(searchDiscussionsQuery, {q: query}));
|
||||
} else {
|
||||
({
|
||||
data: {items: results}
|
||||
} = await this.client.rest.search.issuesAndPullRequests({
|
||||
q: query,
|
||||
sort: 'updated',
|
||||
order: 'desc',
|
||||
per_page: 50
|
||||
})
|
||||
).data.items;
|
||||
}));
|
||||
|
||||
// results may include locked issues
|
||||
return results.filter(issue => !issue.locked);
|
||||
// results may include locked threads
|
||||
results = results.filter(item => !item.locked);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
getFilterByDateQuery({type, qualifier = 'created'} = {}) {
|
||||
const beforeDate = this.config[`exclude-${type}-${qualifier}-before`];
|
||||
const afterDate = this.config[`exclude-${type}-${qualifier}-after`];
|
||||
const betweenDates = this.config[`exclude-${type}-${qualifier}-between`];
|
||||
getFilterByDateQuery({threadType, qualifier = 'created'} = {}) {
|
||||
const beforeDate = this.config[`exclude-${threadType}-${qualifier}-before`];
|
||||
const afterDate = this.config[`exclude-${threadType}-${qualifier}-after`];
|
||||
const betweenDates =
|
||||
this.config[`exclude-${threadType}-${qualifier}-between`];
|
||||
|
||||
if (betweenDates) {
|
||||
return `-${qualifier}:${betweenDates
|
||||
@@ -212,17 +312,4 @@ class App {
|
||||
}
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
const input = Object.fromEntries(
|
||||
Object.keys(schema.describe().keys).map(item => [item, core.getInput(item)])
|
||||
);
|
||||
|
||||
const {error, value} = schema.validate(input, {abortEarly: false});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
@@ -58,13 +58,23 @@ const extendedJoi = Joi.extend(joi => {
|
||||
.extend(joi => {
|
||||
return {
|
||||
type: 'processOnly',
|
||||
base: joi.string(),
|
||||
base: joi.array(),
|
||||
coerce: {
|
||||
from: 'string',
|
||||
method(value, helpers) {
|
||||
method(value) {
|
||||
value = value.trim();
|
||||
if (['issues', 'prs'].includes(value)) {
|
||||
value = value.slice(0, -1);
|
||||
|
||||
if (value) {
|
||||
value = value
|
||||
.split(',')
|
||||
.map(item => {
|
||||
item = item.trim();
|
||||
if (['issues', 'prs', 'discussions'].includes(item)) {
|
||||
item = item.slice(0, -1);
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
return {value};
|
||||
@@ -74,10 +84,7 @@ const extendedJoi = Joi.extend(joi => {
|
||||
});
|
||||
|
||||
const joiDate = Joi.alternatives().try(
|
||||
Joi.date()
|
||||
// .iso()
|
||||
.min('1970-01-01T00:00:00Z')
|
||||
.max('2970-12-31T23:59:59Z'),
|
||||
Joi.date().iso().min('1970-01-01T00:00:00Z').max('2970-12-31T23:59:59Z'),
|
||||
Joi.string().trim().valid('')
|
||||
);
|
||||
|
||||
@@ -169,9 +176,46 @@ const schema = Joi.object({
|
||||
.valid('resolved', 'off-topic', 'too heated', 'spam', '')
|
||||
.default('resolved'),
|
||||
|
||||
'process-only': extendedJoi
|
||||
.processOnly()
|
||||
.valid('issue', 'pr', '')
|
||||
'discussion-inactive-days': Joi.number()
|
||||
.min(0)
|
||||
.max(3650)
|
||||
.precision(9)
|
||||
.default(365),
|
||||
|
||||
'exclude-discussion-created-before': joiDate.default(''),
|
||||
|
||||
'exclude-discussion-created-after': joiDate.default(''),
|
||||
|
||||
'exclude-discussion-created-between': joiTimeInterval.default(''),
|
||||
|
||||
'exclude-discussion-closed-before': joiDate.default(''),
|
||||
|
||||
'exclude-discussion-closed-after': joiDate.default(''),
|
||||
|
||||
'exclude-discussion-closed-between': joiTimeInterval.default(''),
|
||||
|
||||
'include-any-discussion-labels': joiLabels.default(''),
|
||||
|
||||
'include-all-discussion-labels': joiLabels.default(''),
|
||||
|
||||
'exclude-any-discussion-labels': joiLabels.default(''),
|
||||
|
||||
'add-discussion-labels': joiLabels.default(''),
|
||||
|
||||
'remove-discussion-labels': joiLabels.default(''),
|
||||
|
||||
'discussion-comment': Joi.string().trim().max(10000).allow('').default(''),
|
||||
|
||||
'process-only': Joi.alternatives()
|
||||
.try(
|
||||
extendedJoi
|
||||
.processOnly()
|
||||
.items(Joi.string().valid('issue', 'pr', 'discussion'))
|
||||
.min(1)
|
||||
.max(3)
|
||||
.unique(),
|
||||
Joi.string().trim().valid('')
|
||||
)
|
||||
.default(''),
|
||||
|
||||
'log-output': Joi.boolean().default(false)
|
||||
|
||||
17
src/utils.js
17
src/utils.js
@@ -3,6 +3,21 @@ import github from '@actions/github';
|
||||
import {retry} from '@octokit/plugin-retry';
|
||||
import {throttling} from '@octokit/plugin-throttling';
|
||||
|
||||
import {schema} from './schema.js';
|
||||
|
||||
function getConfig() {
|
||||
const input = Object.fromEntries(
|
||||
Object.keys(schema.describe().keys).map(item => [item, core.getInput(item)])
|
||||
);
|
||||
|
||||
const {error, value} = schema.validate(input, {abortEarly: false});
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getClient(token) {
|
||||
const requestRetries = 3;
|
||||
|
||||
@@ -34,4 +49,4 @@ function getClient(token) {
|
||||
return github.getOctokit(token, options, retry, throttling);
|
||||
}
|
||||
|
||||
export {getClient};
|
||||
export {getConfig, getClient};
|
||||
|
||||
Reference in New Issue
Block a user