Skip to content

Commit

Permalink
add PR support
Browse files Browse the repository at this point in the history
  • Loading branch information
kellertk committed Apr 30, 2022
1 parent 79cd238 commit aa3cf88
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 65 deletions.
64 changes: 10 additions & 54 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,64 +14,20 @@ inputs:
dry-run:
description: 'Set to true to not perform repository changes'
default: false
expiration-label-map:
description: 'A multiline input mapping labels with actions and destination labels, applied after an expiration time. See README.md'
issue-expiration-label-map:
description: 'A multiline input mapping labels with actions and destination labels for issues, applied after an expiration time. See README.md'
default: |-
''
update-remove-labels:
issue-update-remove-labels:
description: 'A comma-seperated list of labels that should be removed on issue update. See README.md'
default: ''


#name: "'Stale Issue Cleanup' Action for GitHub Actions"
#description: 'Close issues and pull requests with no recent activity'
#branding:
# icon: 'cloud'
# color: 'orange'
#inputs:
# repo-token:
# description: 'Token for the repository. Can be passed in using {{ secrets.GITHUB_TOKEN }}'
# required: true
# issue-types:
# description: 'Issue types to process ("issues", "pull_requests", or "issues,pull_requests")'
# default: 'issues,pull_requests'
# stale-issue-message:
# description: 'The message to post on the issue when tagging it. If none provided, will not mark issues stale.'
# stale-pr-message:
# description: 'The message to post on the pr when tagging it. If none provided, will not mark pull requests stale.'
# days-before-stale:
# description: 'The number of days old an issue can be before marking it stale.'
# default: 60
# days-before-close:
# description: 'The number of days to wait to close an issue or pull request after it being marked stale.'
# default: 7
# stale-issue-label:
# description: 'The label to apply when an issue is stale.'
# default: 'Stale'
# exempt-issue-labels:
# description: 'The labels to apply when an issue is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
# stale-pr-label:
# description: 'The label to apply when a pull request is stale.'
# default: 'Stale'
# exempt-pr-labels:
# description: 'The labels to apply when a pull request is exempt from being marked stale. Separate multiple labels with commas (eg. "label1,label2")'
# ancient-issue-message:
# description: 'The message to post when an issue is very old.'
# ancient-pr-message:
# description: 'The message to post when a pr is very old.'
# days-before-ancient:
# description: 'The number of days old an issue can be before marking it ancient.'
# default: 360
# response-requested-label:
# description: 'The label that gets applied when a response is requested.'
# closed-for-staleness-label:
# description: 'The label that gets applied when an issue is closed for staleness.'
# minimum-upvotes-to-exempt:
# description: 'The minimum number of "upvotes" that an issue needs to have before not marking as ancient.'
# loglevel:
# description: 'Set to DEBUG to enable debug logging'
# dry-run:
# description: 'Set to true to not perform repository changes'
pr-expiration-label-map:
description: 'A multiline input mapping labels with actions and destination labels for PRs, applied after an expiration time. See README.md'
default: |-
''
pr-update-remove-labels:
description: 'A comma-seperated list of labels that should be removed on PR update. See README.md'
default: ''

runs:
using: 'node16'
Expand Down
62 changes: 54 additions & 8 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,28 @@ export async function getIssues(labels: string[], token: string) {
});
}

// Issue processing steps
// Step 1: Skip closed/merged/locked
// Step 2: If the issue is a PR, use the PR configuration, else use the issue configuration
// Step 3: Iterate all labels in the issue. If labeled, iterate over the configured labels and see if the issue's labels
// match the configured ones.
// Step 4: If they do, take the action specified in the configuration line, and repeat for all configuration lines
// Step 5: Do step 4 but for the updateRemoveLabels
export async function processIssues(issues: Issue[], args: args) {
issues.forEach(async issue => {
// Skip closed and locked issues
if (issue.state === 'closed' || issue.state === 'merged' || issue.locked) return;

const timeline = await getIssueLabelTimeline(issue.number, args.token);
const expirationLabelMap = isPr(issue) ? args.prExpirationLabelMap : args.expirationLabelMap;
const removeLabelMap = isPr(issue) ? args.prUpdateRemoveLabels : args.updateRemoveLabels;
// Enumerate labels in issue and check if each matches our action list
issue.labels.forEach(label => {
const issueLabel = typeof label === 'string' ? label : label.name;
if (issueLabel) {
if (args.expirationLabelMap) {
if (expirationLabelMap) {
// These are labels that we apply if an issue hasn't been updated in a specified timeframe
args.expirationLabelMap.forEach(async lam => {
expirationLabelMap.forEach(async lam => {
const sourceLabelList = lam.split(':')[0].split(',');
const configuredAction = lam.split(':')[1];
const configuredTime = parseInt(lam.split(':')[2]);
Expand All @@ -36,25 +48,25 @@ export async function processIssues(issues: Issue[], args: args) {
// Issue contains label specified and configured time has elapsed
switch (configuredAction) {
case 'add':
await addLabelToIssue(issue.number, lam.split(':')[3]);
await addLabelToIssue(issue.number, lam.split(':')[3], args.token);
break;
case 'remove':
await removeLabelFromIssue(issue.number, lam.split(':')[3]);
await removeLabelFromIssue(issue.number, lam.split(':')[3], args.token);
break;
case 'close':
await closeIssue(issue.number);
await closeIssue(issue.number, args.token);
break;
default:
core.error(`Unknown action ${configuredAction} for issue #${issue.number}, doing nothing`);
}
}
});
}
if (args.updateRemoveLabels) {
if (removeLabelMap) {
// These are labels that need removed if an issue has been updated after they were applied
args.updateRemoveLabels.forEach(async removeMe => {
removeLabelMap.forEach(async removeMe => {
if (Date.parse(issue.updated_at) > getIssueLabelDate(timeline, removeMe)) {
removeLabelFromIssue(issue.number, removeMe);
removeLabelFromIssue(issue.number, removeMe, args.token);
}
});
}
Expand All @@ -74,6 +86,36 @@ async function getIssueLabelTimeline(issueNumber: number, token: string) {
).filter(event => event.event === 'labeled');
}

async function addLabelToIssue(issue: number, label: string, token: string) {
const octokit = github.getOctokit(token);
await octokit.rest.issues.addLabels({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue,
labels: [label],
});
}

async function removeLabelFromIssue(issue: number, label: string, token: string) {
const octokit = github.getOctokit(token);
await octokit.rest.issues.removeLabel({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue,
name: label,
});
}

async function closeIssue(issue: number, token: string) {
const octokit = github.getOctokit(token);
await octokit.rest.issues.update({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issue,
state: 'closed',
});
}

function getIssueLabelDate(timeline: Timeline, label: string) {
// Return when the label was last applied
return timeline.reduce((p, c) => {
Expand All @@ -94,3 +136,7 @@ function issueDateCompare(issueDate: string, configuredDays: number) {
d.setDate(d.getDate() + configuredDays);
return d.valueOf() < Date.now();
}

function isPr(issue: Issue) {
return !!issue.pull_request;
}
15 changes: 12 additions & 3 deletions src/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export interface args {
token: string;
expirationLabelMap?: string[];
updateRemoveLabels?: string[];
prExpirationLabelMap?: string[];
prUpdateRemoveLabels?: string[];
}

export function getAndValidateInputs(): args {
Expand All @@ -20,16 +22,23 @@ export function getAndValidateInputs(): args {
// Action map
const labelValidationRegex = new RegExp(`^[A-Za-z0-9_.-,]+:(${labelActions.join('|')}):\\d+(:[A-Za-z0-9_.-,]+)?/i`);
const expirationLabelMap = core
.getMultilineInput('expiration-label-map', { required: false })
.getMultilineInput('issue-expiration-label-map', { required: false })
.filter(m => labelValidationRegex.test(m));
core.debug(`Parsed label mapping: ${expirationLabelMap}`);
const updateRemoveLabels = core.getInput('update-remove-labels', { required: false }).split(',');
core.debug(`Parsed issue label mapping: ${expirationLabelMap}`);
const prExpirationLabelMap = core
.getMultilineInput('pr-expiration-label-map', { required: false })
.filter(m => labelValidationRegex.test(m));
core.debug(`Parsed PR label mapping: ${prExpirationLabelMap}`);
const updateRemoveLabels = core.getInput('issue-update-remove-labels', { required: false }).split(',');
const prUpdateRemoveLabels = core.getInput('pr-update-remove-labels', { required: false }).split(',');

return {
dryrun: core.getBooleanInput('dry-run', { required: false }),
minimumUpvotesToExempt: minUpvotes,
token: core.getInput('repo-token'),
expirationLabelMap,
updateRemoveLabels,
prExpirationLabelMap,
prUpdateRemoveLabels,
};
}

0 comments on commit aa3cf88

Please sign in to comment.