Repository Maintenance #5
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: 'Repository Maintenance' | |
on: | |
schedule: | |
- cron: '0 3 * * *' | |
workflow_dispatch: | |
permissions: | |
issues: write | |
pull-requests: write | |
discussions: write | |
concurrency: | |
group: lock | |
jobs: | |
stale: | |
name: 'Stale' | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/stale@v9 | |
with: | |
days-before-stale: 7 | |
days-before-close: 14 | |
stale-issue-label: stale | |
stale-pr-label: stale | |
stale-issue-message: > | |
This issue has been automatically marked as stale because it has not had | |
recent activity. It will be closed if no further activity occurs. Thank you | |
for your contributions. | |
lock-threads: | |
name: 'Lock Old Threads' | |
runs-on: ubuntu-latest | |
steps: | |
- uses: dessant/lock-threads@v5 | |
with: | |
issue-inactive-days: '30' | |
pr-inactive-days: '30' | |
discussion-inactive-days: '30' | |
log-output: true | |
issue-comment: > | |
This issue has been automatically locked since there | |
has not been any recent activity after it was closed. | |
Please open a new discussion or issue for related concerns. | |
pr-comment: > | |
This pull request has been automatically locked since there | |
has not been any recent activity after it was closed. | |
Please open a new discussion or issue for related concerns. | |
discussion-comment: > | |
This discussion has been automatically locked since there | |
has not been any recent activity after it was closed. | |
Please open a new discussion for related concerns. | |
close-answered-discussions: | |
name: 'Close Answered Discussions' | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/github-script@v7 | |
with: | |
script: | | |
function sleep(ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
const query = `query($owner:String!, $name:String!) { | |
repository(owner:$owner, name:$name){ | |
discussions(first:100, answered:true, states:[OPEN]) { | |
nodes { | |
id, | |
number | |
} | |
} | |
} | |
}`; | |
const variables = { | |
owner: context.repo.owner, | |
name: context.repo.repo, | |
} | |
const result = await github.graphql(query, variables) | |
console.log(`Found ${result.repository.discussions.nodes.length} open answered discussions`) | |
for (const discussion of result.repository.discussions.nodes) { | |
console.log(`Closing discussion #${discussion.number} (${discussion.id})`) | |
const addCommentMutation = `mutation($discussion:ID!, $body:String!) { | |
addDiscussionComment(input:{discussionId:$discussion, body:$body}) { | |
clientMutationId | |
} | |
}`; | |
const commentVariables = { | |
discussion: discussion.id, | |
body: 'This discussion has been automatically closed because it was marked as answered.', | |
} | |
await github.graphql(addCommentMutation, commentVariables) | |
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) { | |
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) { | |
clientMutationId | |
} | |
}`; | |
const closeVariables = { | |
discussion: discussion.id, | |
reason: "RESOLVED", | |
} | |
await github.graphql(closeDiscussionMutation, closeVariables) | |
await sleep(1000) | |
} | |
close-outdated-discussions: | |
name: 'Close Outdated Discussions' | |
runs-on: ubuntu-latest | |
steps: | |
- uses: actions/github-script@v7 | |
with: | |
script: | | |
function sleep(ms) { | |
return new Promise(resolve => setTimeout(resolve, ms)); | |
} | |
const CUTOFF_DAYS = 180; | |
const cutoff = new Date(); | |
cutoff.setDate(cutoff.getDate() - CUTOFF_DAYS); | |
const query = `query( | |
$owner:String!, | |
$name:String!, | |
$supportCategory:ID!, | |
$generalCategory:ID!, | |
) { | |
supportDiscussions: repository(owner:$owner, name:$name){ | |
discussions( | |
categoryId:$supportCategory, | |
last:50, | |
answered:false, | |
states:[OPEN], | |
) { | |
nodes { | |
id, | |
number, | |
updatedAt | |
} | |
}, | |
}, | |
generalDiscussions: repository(owner:$owner, name:$name){ | |
discussions( | |
categoryId:$generalCategory, | |
last:50, | |
states:[OPEN], | |
) { | |
nodes { | |
id, | |
number, | |
updatedAt | |
} | |
} | |
} | |
}`; | |
const variables = { | |
owner: context.repo.owner, | |
name: context.repo.repo, | |
supportCategory: "DIC_kwDOH31rQM4CRErR", | |
generalCategory: "DIC_kwDOH31rQM4CRErQ" | |
} | |
const result = await github.graphql(query, variables); | |
const combinedDiscussions = [ | |
...result.supportDiscussions.discussions.nodes, | |
...result.generalDiscussions.discussions.nodes, | |
] | |
console.log(`Checking ${combinedDiscussions.length} open discussions`); | |
for (const discussion of combinedDiscussions) { | |
if (new Date(discussion.updatedAt) < cutoff) { | |
console.log(`Closing outdated discussion #${discussion.number} (${discussion.id}), last updated at ${discussion.updatedAt}`); | |
const addCommentMutation = `mutation($discussion:ID!, $body:String!) { | |
addDiscussionComment(input:{discussionId:$discussion, body:$body}) { | |
clientMutationId | |
} | |
}`; | |
const commentVariables = { | |
discussion: discussion.id, | |
body: 'This discussion has been automatically closed due to inactivity.', | |
} | |
await github.graphql(addCommentMutation, commentVariables); | |
const closeDiscussionMutation = `mutation($discussion:ID!, $reason:DiscussionCloseReason!) { | |
closeDiscussion(input:{discussionId:$discussion, reason:$reason}) { | |
clientMutationId | |
} | |
}`; | |
const closeVariables = { | |
discussion: discussion.id, | |
reason: "OUTDATED", | |
} | |
await github.graphql(closeDiscussionMutation, closeVariables); | |
await sleep(1000); | |
} | |
} |