Skip to content

Commit

Permalink
generic label/action interface
Browse files Browse the repository at this point in the history
  • Loading branch information
kellertk committed Apr 30, 2022
1 parent 1e9625a commit 79cd238
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 24 deletions.
30 changes: 11 additions & 19 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,23 @@ branding:
icon: 'cloud'
color: 'orange'
inputs:
repo-token:
description: 'Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`'
default: ${{ github.token }}
minimum-upvotes-to-exempt:
description: 'The minimum number of "upvotes" that an issue needs to have before never closing it.'
default: 0
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'
default: |-
''
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'
Expand Down Expand Up @@ -65,22 +76,3 @@ inputs:
runs:
using: 'node16'
main: 'dist/index.js'
env:
REPO_TOKEN: ${{ github.token }}
# ISSUE_TYPES: ${{ inputs.issue-types }}
# ANCIENT_ISSUE_MESSAGE: ${{ inputs.ancient-issue-message }}
# ANCIENT_PR_MESSAGE: ${{ inputs.ancient-pr-message }}
# STALE_ISSUE_MESSAGE: ${{ inputs.stale-issue-message }}
# STALE_PR_MESSAGE: ${{ inputs.stale-pr-message }}
# DAYS_BEFORE_STALE: ${{ inputs.days-before-stale }}
# DAYS_BEFORE_CLOSE: ${{ inputs.days-before-close }}
# DAYS_BEFORE_ANCIENT: ${{ inputs.days-before-ancient }}
# STALE_ISSUE_LABEL: ${{ inputs.stale-issue-label }}
# EXEMPT_ISSUE_LABELS: ${{ inputs.exempt-issue-labels }}
# STALE_PR_LABEL: ${{ inputs.stale-pr-label }}
# EXEMPT_PR_LABELS: ${{ inputs.exempt-pr-labels }}
# RESPONSE_REQUESTED_LABEL: ${{ inputs.response-requested-label }}
# CFS_LABEL: ${{ inputs.closed-for-staleness-label }}
MINIMUM_UPVOTES_TO_EXEMPT: ${{ inputs.minimum-upvotes-to-exempt }}
# LOGLEVEL: ${{ inputs.loglevel }}
DRYRUN: ${{ inputs.dry-run }}
85 changes: 85 additions & 0 deletions src/github.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import * as github from '@actions/github';
import * as core from '@actions/core';
import { args } from './input';

export const labelActions = ['add', 'remove', 'close'] as const;

type Element<T extends unknown[]> = T extends readonly (infer ElementType)[] ? ElementType : never;
export type Issue = Element<Awaited<ReturnType<typeof getIssues>>>;
export type Timeline = Awaited<ReturnType<typeof getIssueLabelTimeline>>;

export async function getIssues(labels: string[], token: string) {
const octokit = github.getOctokit(token);
Expand All @@ -9,3 +17,80 @@ export async function getIssues(labels: string[], token: string) {
labels: labels.join(),
});
}

export async function processIssues(issues: Issue[], args: args) {
issues.forEach(async issue => {
const timeline = await getIssueLabelTimeline(issue.number, args.token);
// 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) {
// These are labels that we apply if an issue hasn't been updated in a specified timeframe
args.expirationLabelMap.forEach(async lam => {
const sourceLabelList = lam.split(':')[0].split(',');
const configuredAction = lam.split(':')[1];
const configuredTime = parseInt(lam.split(':')[2]);

if (sourceLabelList.includes(issueLabel) && issueDateCompare(issue.updated_at, configuredTime)) {
// Issue contains label specified and configured time has elapsed
switch (configuredAction) {
case 'add':
await addLabelToIssue(issue.number, lam.split(':')[3]);
break;
case 'remove':
await removeLabelFromIssue(issue.number, lam.split(':')[3]);
break;
case 'close':
await closeIssue(issue.number);
break;
default:
core.error(`Unknown action ${configuredAction} for issue #${issue.number}, doing nothing`);
}
}
});
}
if (args.updateRemoveLabels) {
// These are labels that need removed if an issue has been updated after they were applied
args.updateRemoveLabels.forEach(async removeMe => {
if (Date.parse(issue.updated_at) > getIssueLabelDate(timeline, removeMe)) {
removeLabelFromIssue(issue.number, removeMe);
}
});
}
}
});
});
}

async function getIssueLabelTimeline(issueNumber: number, token: string) {
const octokit = github.getOctokit(token);
return (
await octokit.paginate(octokit.rest.issues.listEventsForTimeline, {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: issueNumber,
})
).filter(event => event.event === 'labeled');
}

function getIssueLabelDate(timeline: Timeline, label: string) {
// Return when the label was last applied
return timeline.reduce((p, c) => {
if (c.updated_at && c.label?.name === label) {
if (Date.parse(c.updated_at) > p) {
return Date.parse(c.updated_at);
} else {
return p;
}
} else {
return p;
}
}, 0);
}

function issueDateCompare(issueDate: string, configuredDays: number) {
const d = new Date(Date.parse(issueDate));
d.setDate(d.getDate() + configuredDays);
return d.valueOf() < Date.now();
}
24 changes: 19 additions & 5 deletions src/input.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
import * as core from '@actions/core';

import { labelActions } from './github';
export interface args {
dryrun: boolean;
minimumUpvotesToExempt: number;
token: string;
expirationLabelMap?: string[];
updateRemoveLabels?: string[];
}

export function getAndValidateInputs(): args {
const minUpvotes = parseInt(core.getInput('minimumUpvotesToExempt', { required: false }));
// Number inputs
const minUpvotes = parseInt(core.getInput('minimum-upvotes-to-exempt', { required: false }));
for (const numberInput of [minUpvotes]) {
if (isNaN(numberInput)) {
throw Error(`Input ${numberInput} did not parse to a valid integar`);
throw Error(`Input ${numberInput} did not parse to a valid integer`);
}
}

// 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 })
.filter(m => labelValidationRegex.test(m));
core.debug(`Parsed label mapping: ${expirationLabelMap}`);
const updateRemoveLabels = core.getInput('update-remove-labels', { required: false }).split(',');

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

0 comments on commit 79cd238

Please sign in to comment.