diff --git a/README.md b/README.md index 54940c40..48c9cee2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # :calendar: GitHub Issue Due Dates Action -Add due dates to GitHub issues - issues are automatically tagged with `Overdue` and `Due in 1 week` labels. +Add due dates to GitHub issues - issues are automatically tagged with labels +when they pass certain date thresholds, as defined by you. ## How it works: 1. Add the following snippet to the top of issues you'd like to assign due dates to: @@ -9,7 +10,7 @@ due: 2019-09-19 --- ``` 2. Create a `.github/workflows/workflow.yml` file with the following contents: -``` +```yaml name: Main Workflow on: schedule: @@ -23,6 +24,31 @@ jobs: uses: alexleventer/github-issue-due-dates-action@1.0.12 with: GH_TOKEN: "${{ secrets.GH_TOKEN }}" + OVERDUE_LABEL: OVERDUE! + INTERVALS: >- + - days: 30 + label: Due in 1 month + - days: 14 + label: Due in 2 weeks + - days: 7 + label: Due in 1 week + - days: 1 + label: DUE TOMORROW ``` 3. Generate a [personal access GitHub token](https://github.com/settings/tokens). -4. Add the following environment variable to your repository secrets: `GH_TOKEN={{your personal access token}}`. +4. Add the following environment variable to your repository + secrets: `GH_TOKEN={{your personal access token}}`. + +## Defining intervals + +Intervals are defined as a sequence (list) with a number of `days` and +a `label` to be set. + +You can define intervals and labels to meet your requirements; the +above is simply a guide. + +### Note the block syntax + +The value of the `INTERVALS` key must be interpreted as a string, +and must be a valid YAML block. Your action will fail if you do not +set the block indicator. diff --git a/__tests__/Octokit.test.ts b/__tests__/Octokit.test.ts index 64c94344..8956e597 100644 --- a/__tests__/Octokit.test.ts +++ b/__tests__/Octokit.test.ts @@ -21,19 +21,19 @@ describe('Octokit', () => { await gh.addLabelToIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[1].number, ['Test']); const updatedIssue = await gh.get(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[1].number); expect(updatedIssue.labels.map(label => label.name).join(', ')).toContain('Test'); - await gh.removeLabelFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, 'Test', issues[1].number); + await gh.removeLabelsFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[1].number, ['Test']); }); - it('should remove label from issue without label', async () => { + it('should remove labels from issue without label', async () => { const issues = await gh.listAllOpenIssues(TEST_REPO_AUTHOR, TEST_REPO_NAME); - const results = await gh.removeLabelFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, 'Test', issues[0].number); + const results = await gh.removeLabelsFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[0].number, ['Test']); expect(results).toHaveLength(0); }); it('should remove label from issue with label', async () => { const issues = await gh.listAllOpenIssues(TEST_REPO_AUTHOR, TEST_REPO_NAME); await gh.addLabelToIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[1].number, ['Test']); - const results = await gh.removeLabelFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, 'Test', issues[1].number); + const results = await gh.removeLabelsFromIssue(TEST_REPO_AUTHOR, TEST_REPO_NAME, issues[1].number, ['Test']); expect(results).toHaveLength(1); }); diff --git a/action.yml b/action.yml index d8a459c7..9ab0f048 100644 --- a/action.yml +++ b/action.yml @@ -5,6 +5,14 @@ inputs: GH_TOKEN: description: GitHub token used to make API requests required: true + INTERVALS: + description: >- + A list containing a `label` to set when + the ticket is due in the specified `days` + required: true + OVERDUE_LABEL: + description: The label to set when an issue is overdue + required: false branding: icon: 'calendar' color: 'purple' diff --git a/package.json b/package.json index 6f6cd968..f34958b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "main": "dist/app.js", - "version": "1.0.12", + "version": "1.0.13", "license": "MIT", "scripts": { "build": "tsc", @@ -20,6 +20,7 @@ "@actions/github": "^2.2.0", "@slack/client": "^5.0.2", "front-matter": "^3.1.0", - "moment": "^2.25.3" + "moment": "^2.25.3", + "yaml": "^1.10.0" } } diff --git a/src/app.ts b/src/app.ts index 8a488930..c835ae65 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,34 +1,61 @@ import * as core from '@actions/core'; import {GitHub, context} from '@actions/github'; import Octokit from './integrations/Octokit'; -import {datesToDue} from './utils/dateUtils'; -import {OVERDUE_TAG_NAME, NEXT_WEEK_TAG_NAME} from './constants'; +import {datesToDue, byDays} from './utils/dateUtils'; +import YAML from 'yaml' export const run = async () => { try { const githubToken = core.getInput('GH_TOKEN'); + const inputIntervals = core.getInput('INTERVALS'); + const overdueLabel = core.getInput('OVERDUE_LABEL') || "OVERDUE"; if (!githubToken) { throw new Error('Missing GH_TOKEN environment variable'); } + if (!inputIntervals) { + throw new Error('Missing INTERVALS environment variable'); + } + const ok = new Octokit(githubToken); const issues = await ok.listAllOpenIssues(context.repo.owner, context.repo.repo); + console.log(`Found ${issues.length} open issue(s)`); + const results = await ok.getIssuesWithDueDate(issues); + console.log(`Found ${results.length} issue(s) with due dates`); + + const intervals = YAML.parse(inputIntervals).sort(byDays); + const intervalLabels = intervals.map(interval => interval.label); + console.log(`Found ${intervals.length} defined intervals`); + results.forEach(async issue => { + console.log(`Processing issue #${issue.number} with due date of ${issue.due}`); const daysUtilDueDate = await datesToDue(issue.due); - if (daysUtilDueDate <= 7 && daysUtilDueDate > 0) { - await ok.addLabelToIssue(context.repo.owner, context.repo.repo, issue.number, [NEXT_WEEK_TAG_NAME]); - } else if (daysUtilDueDate <= 0) { - await ok.removeLabelFromIssue(context.repo.owner, context.repo.repo, NEXT_WEEK_TAG_NAME, issue.number); - await ok.addLabelToIssue(context.repo.owner, context.repo.repo, issue.number, [OVERDUE_TAG_NAME]); + + if (daysUtilDueDate <= 0) { + await ok.removeLabelsFromIssue(context.repo.owner, context.repo.repo, issue.number, intervalLabels); + await ok.addLabelToIssue(context.repo.owner, context.repo.repo, issue.number, [overdueLabel]); + console.log(`Marked issue #${issue.number} as overdue`); + } else { + for(interval of intervals) { + if (daysUtilDueDate <= interval.days) { + await ok.removeLabelsFromIssue(context.repo.owner, context.repo.repo, issue.number, intervalLabels); + await ok.addLabelToIssue(context.repo.owner, context.repo.repo, issue.number, [interval.label]); + console.log(`Marked issue #${issue.number} with label: ${interval.label}`); + break; // don't process any more intervals + } + } } + }); + return { ok: true, issuesProcessed: results.length, } + } catch (e) { core.setFailed(e.message); throw e; diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 7162753e..00000000 --- a/src/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const OVERDUE_TAG_NAME = 'Overdue'; -export const NEXT_WEEK_TAG_NAME = 'Due in next week'; diff --git a/src/integrations/Octokit.ts b/src/integrations/Octokit.ts index 660c7db1..3bbecc2c 100644 --- a/src/integrations/Octokit.ts +++ b/src/integrations/Octokit.ts @@ -27,29 +27,31 @@ export default class Octokit { return data; } - async addLabelToIssue(owner: string, repo: string, issueNumber: number, labels: string[]) { + async addLabelToIssue(owner: string, repo: string, issue_number: number, labels: string[]) { const {data} = await this.client.issues.addLabels({ owner, repo, - issue_number: issueNumber, + issue_number, labels, }); return data; } - async removeLabelFromIssue(owner: string, repo: string, name: string, issue_number: number) { - try { - const {data} = await this.client.issues.removeLabel({ - owner, - repo, - name, - issue_number, - }); - return data; - } catch (e) { - // Do not throw error - return []; - } + async removeLabelsFromIssue(owner: string, repo: string, issue_number: number, labels: string[]) { + labels.forEach(async label => { + try { + const {data} = await this.client.issues.removeLabel({ + owner, + repo, + issue_number, + label, + }); + return data; + } catch (e) { + // Do not throw error + return []; + } + }); } async getIssuesWithDueDate(rawIssues: any[]) { diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts index 36cbd2eb..686f843a 100644 --- a/src/utils/dateUtils.ts +++ b/src/utils/dateUtils.ts @@ -5,3 +5,14 @@ export const datesToDue = (date: string) => { const today = moment(); return eventDate.diff(today, 'days'); }; + +export const byDays = (a: object, b: object) => { + switch(true) { + case a.days < b.days: + return -1; + case a.days > b.days: + return 1; + default: + return 0; + } +};