From 0a0976f3ded51c88c6128dd96dd115c9f14fa764 Mon Sep 17 00:00:00 2001 From: dessant Date: Tue, 14 Nov 2023 07:03:53 +0200 Subject: [PATCH] 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. --- README.md | 166 +++++++++++++++++++++++++---------- action.yml | 45 +++++++++- src/data.js | 113 ++++++++++++++++++++++++ src/index.js | 235 ++++++++++++++++++++++++++++++++++---------------- src/schema.js | 66 +++++++++++--- src/utils.js | 17 +++- 6 files changed, 510 insertions(+), 132 deletions(-) create mode 100644 src/data.js diff --git a/README.md b/README.md index 1e01ffd..2d0bf3a 100644 --- a/README.md +++ b/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. - + ## 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 -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. @@ -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. @@ -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. ```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. ```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. ```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. ```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. ```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. ```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. ```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. ```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 diff --git a/action.yml b/action.yml index 36aff90..881d669 100644 --- a/action.yml +++ b/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' diff --git a/src/data.js b/src/data.js new file mode 100644 index 0000000..740d1d9 --- /dev/null +++ b/src/data.js @@ -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 +}; diff --git a/src/index.js b/src/index.js index bda1cb8..acf48f6 100644 --- a/src/index.js +++ b/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; + } else { + try { + await this.client.rest.issues.createComment({ + ...thread, + 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}); + 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})); + } if (addLabels) { - const currentLabels = issueData.labels.map(label => label.name); + const currentLabelNames = currentLabels.map(label => label.name); const newLabels = addLabels.filter( - label => !currentLabels.includes(label) + label => !currentLabelNames.includes(label) ); if (newLabels.length) { - core.debug(`Labeling (${type}: ${issue.issue_number})`); - await this.client.rest.issues.addLabels({ - ...issue, - labels: newLabels - }); + 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 currentLabels = issueData.labels.map(label => label.name); const matchingLabels = currentLabels.filter(label => - removeLabels.includes(label) + removeLabels.includes(label.name) ); + if (matchingLabels.length) { - core.debug(`Unlabeling (${type}: ${issue.issue_number})`); - for (const label of matchingLabels) { - await this.client.rest.issues.removeLabel({ - ...issue, - name: label + 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 + }); + } } } } } - core.debug(`Locking (${type}: ${issue.issue_number})`); + core.debug(`Locking (${threadType}: ${threadNumber})`); - const params = {...issue}; + if (threadType === 'discussion') { + await this.client.graphql(lockLockableQuery, { + lockableId: discussionId + }); + } else { + const params = {...thread}; - if (lockReason) { - params.lock_reason = lockReason; - } + if (lockReason) { + params.lock_reason = lockReason; + } - await this.client.rest.issues.lock(params); + await this.client.rest.issues.lock(params); + } - threads.push(issue); + 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(); diff --git a/src/schema.js b/src/schema.js index 95d9f68..5e119c7 100644 --- a/src/schema.js +++ b/src/schema.js @@ -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) diff --git a/src/utils.js b/src/utils.js index cf21b95..9d40057 100644 --- a/src/utils.js +++ b/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};