feat(statistics): display some stats in the logs (#337)

* test: add more coverage

* docs: reorder and enhance typo

* docs(contributing): add more information about the npm scripts

* feat(statistics): add simple statistics

* feat(statistics): add more stats

* refactor(issues-processor): remove some options from the constructor

it should have been only useful for the tests

* feat(statistics): add stats for new stale or undo stale issues

* chore(rebase): handle rebase conflicts
This commit is contained in:
Geoffrey Testelin 2021-03-01 21:34:35 +01:00 committed by GitHub
parent 63ae8ac024
commit 419a53bc05
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1021 additions and 679 deletions

114
README.md
View File

@ -4,56 +4,51 @@ Warns and then closes issues and PRs that have had no activity for a specified a
### Arguments
| Input | Description | Usage |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | -------- |
| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional |
| `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional |
| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional |
| `days-before-pr-stale` | Idle number of days before marking an PR as stale (override `days-before-stale`). | Optional |
| `days-before-close` | Idle number of days before closing an stale issue/PR. _Defaults to **7**_ | Optional |
| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional |
| `days-before-pr-close` | Idle number of days before closing an stale PR (override `days-before-close`). | Optional |
| `stale-issue-message` | Message to post on the stale issue. | Optional |
| `stale-pr-message` | Message to post on the stale PR. | Optional |
| `close-issue-message` | Message to post on the stale issue while closing it. | Optional |
| `close-pr-message` | Message to post on the stale PR while closing it. | Optional |
| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **Stale**_ | Optional |
| `close-issue-label` | Label to apply on closing issue (automatically removed if no longer closed nor locked). | Optional |
| `stale-pr-label` | Label to apply on the stale PR. _Defaults to **Stale**_ | Optional |
| `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional |
| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional |
| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional |
| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional |
| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `only-labels` | Only issues and PRs with ALL these labels are checked. Separate multiple labels with commas (eg. "question,answered"). | Optional |
| `only-labels` | Only labels checked for stale issue/PR. | Optional |
| `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional |
| `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional |
| `any-of-labels` | Only issues and PRs with ANY of these labels are checked. Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | Optional |
| `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional |
| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional |
| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional |
| `ascending` | Order to get issues/PR. _Defaults to **false**_ | Optional |
| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional |
| `skip-stale-pr-message` | Skip adding stale message on stale PR. _Defaults to **false**_ | Optional |
| `start-date` | The date used to skip the stale action on issue/PR created before it (ISO 8601 or RFC 2822). | Optional |
| `delete-branch` | Delete the git branch after closing a stale pull request. _Defaults to **false**_ | Optional |
| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional |
| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale (override `exempt-assignees`). | Optional |
| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale (override `exempt-assignees`). | Optional |
| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale. (priority over `exempt-assignees` rules) | Optional |
| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional |
| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional |
| Input | Description | Usage |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| `repo-token` | PAT(Personal Access Token) for authorizing repository. _Defaults to **${{ github.token }}**_ | Optional |
| `days-before-stale` | Idle number of days before marking an issue/PR as stale. _Defaults to **60**_ | Optional |
| `days-before-issue-stale` | Idle number of days before marking an issue as stale (override `days-before-stale`). | Optional |
| `days-before-pr-stale` | Idle number of days before marking an PR as stale (override `days-before-stale`). | Optional |
| `days-before-close` | Idle number of days before closing an stale issue/PR. _Defaults to **7**_ | Optional |
| `days-before-issue-close` | Idle number of days before closing an stale issue (override `days-before-close`). | Optional |
| `days-before-pr-close` | Idle number of days before closing an stale PR (override `days-before-close`). | Optional |
| `stale-issue-message` | Message to post on the stale issue. | Optional |
| `stale-pr-message` | Message to post on the stale PR. | Optional |
| `close-issue-message` | Message to post on the stale issue while closing it. | Optional |
| `close-pr-message` | Message to post on the stale PR while closing it. | Optional |
| `stale-issue-label` | Label to apply on the stale issue. _Defaults to **Stale**_ | Optional |
| `close-issue-label` | Label to apply on closing issue (automatically removed if no longer closed nor locked). | Optional |
| `stale-pr-label` | Label to apply on the stale PR. _Defaults to **Stale**_ | Optional |
| `close-pr-label` | Label to apply on the closing PR (automatically removed if no longer closed nor locked). | Optional |
| `exempt-issue-labels` | Labels on an issue exempted from being marked as stale. | Optional |
| `exempt-pr-labels` | Labels on the PR exempted from being marked as stale. | Optional |
| `only-labels` | Only issues and PRs with ALL these labels are checked. Separate multiple labels with commas (eg. "question,answered"). | Optional |
| `only-labels` | Only labels checked for stale issue/PR. | Optional |
| `only-issue-labels` | Only labels checked for stale issue (override `only-labels`). | Optional |
| `only-pr-labels` | Only labels checked for stale PR (override `only-labels`). | Optional |
| `any-of-labels` | Only issues and PRs with ANY of these labels are checked. Separate multiple labels with commas (eg. "incomplete,waiting-feedback"). | Optional |
| `operations-per-run` | Maximum number of operations per run (GitHub API CRUD related). _Defaults to **30**_ | Optional |
| `remove-stale-when-updated` | Remove stale label from issue/PR on updates or comments. _Defaults to **true**_ | Optional |
| `debug-only` | Dry-run on action. _Defaults to **false**_ | Optional |
| `ascending` | Order to get issues/PR. _Defaults to **false**_ | Optional |
| `skip-stale-issue-message` | Skip adding stale message on stale issue. _Defaults to **false**_ | Optional |
| `skip-stale-pr-message` | Skip adding stale message on stale PR. _Defaults to **false**_ | Optional |
| `start-date` | The date used to skip the stale action on issue/PR created before it (ISO 8601 or RFC 2822). | Optional |
| `delete-branch` | Delete the git branch after closing a stale pull request. _Defaults to **false**_ | Optional |
| `exempt-milestones` | Milestones on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-milestones` | Milestones on an issue exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-pr-milestones` | Milestones on the PR exempted from being marked as stale (override `exempt-milestones`). | Optional |
| `exempt-all-milestones` | Exempt all issues and PRs with milestones from being marked as stale. (priority over `exempt-milestones` rules) | Optional |
| `exempt-all-issue-milestones` | Exempt all issues with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-all-pr-milestones` | Exempt all PRs with milestones from being marked as stale. (override `exempt-all-milestones`). | Optional |
| `exempt-assignees` | Assignees on an issue or a PR exempted from being marked as stale. | Optional |
| `exempt-issue-assignees` | Assignees on an issue exempted from being marked as stale (override `exempt-assignees`). | Optional |
| `exempt-pr-assignees` | Assignees on the PR exempted from being marked as stale (override `exempt-assignees`). | Optional |
| `exempt-all-assignees` | Exempt all issues and PRs with assignees from being marked as stale. (priority over `exempt-assignees` rules) | Optional |
| `exempt-all-issue-assignees` | Exempt all issues with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional |
| `exempt-all-pr-assignees` | Exempt all PRs with assignees from being marked as stale. (override `exempt-all-assignees`). | Optional |
| `enable-statistics` | Display some statistics at the end of the logs regarding the stale workflow (only when the logs are enabled). _Defaults to **true**_ | Optional |
### Usage
@ -275,10 +270,25 @@ jobs:
### Debugging
**Logs:**
To see the debug output from this action, you must set the secret `ACTIONS_STEP_DEBUG` to `true` in your repository.
There is a lot of logs so this can be very helpful!
**Statistics:**
If the logs are enabled, you can also enable the statistics log which will be visible at the end of the logs once all issues were processed.
This is very helpful to have a quick understanding of the whole stale workflow.
Set `enable-statistics` to `true` in your workflow configuration file.
**Dry-run:**
You can run this action in debug only mode (no actions will be taken on your issues and pull requests) by passing `debug-only` to `true` as an argument to the action.
You can also increase the maximum number of operations per run by passing `operations-per-run` to `100` for example.
Finally, you could also change the cron job frequency in the stale workflow to run stale more often.
**More operations:**
You can increase the maximum number of operations per run by passing `operations-per-run` to `1000` for example which will help you to handle more operations in a single stale workflow run.
If the `debug-only` option is enabled, this is very helpful because the workflow will (almost) never reach the GitHub API rate, and you will be able to deep-dive into the logs.
**Job frequency:**
You could change the cron job frequency in the stale workflow to run the stale workflow more often.
Usually this is not very helpful though.
### Contributing

View File

@ -1,6 +1,6 @@
import {Issue} from '../src/classes/issue';
import {IssuesProcessor} from '../src/classes/issues-processor';
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
import {IssuesProcessorMock} from './classes/issues-processor-mock';
import {DefaultProcessorOptions} from './constants/default-processor-options';
import {generateIssue} from './functions/generate-issue';
@ -95,8 +95,8 @@ class IssuesProcessorBuilder {
return this;
}
build(): IssuesProcessor {
return new IssuesProcessor(
build(): IssuesProcessorMock {
return new IssuesProcessorMock(
this._options,
async () => 'abot',
async p => (p === 1 ? this._issues : []),

View File

@ -1,6 +1,6 @@
import {Issue} from '../src/classes/issue';
import {IssuesProcessor} from '../src/classes/issues-processor';
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
import {IssuesProcessorMock} from './classes/issues-processor-mock';
import {DefaultProcessorOptions} from './constants/default-processor-options';
import {generateIssue} from './functions/generate-issue';
@ -21,7 +21,7 @@ interface ITestData {
describe('assignees options', (): void => {
let opts: IIssuesProcessorOptions;
let testIssueList: Issue[];
let processor: IssuesProcessor;
let processor: IssuesProcessorMock;
const setTestIssueList = (
isPullRequest: boolean,
@ -46,7 +46,7 @@ describe('assignees options', (): void => {
};
const setProcessor = () => {
processor = new IssuesProcessor(
processor = new IssuesProcessorMock(
opts,
async () => 'abot',
async p => (p === 1 ? testIssueList : []),

View File

@ -0,0 +1,38 @@
import {Issue} from '../../src/classes/issue';
import {IssuesProcessor} from '../../src/classes/issues-processor';
import {IComment} from '../../src/interfaces/comment';
import {IIssuesProcessorOptions} from '../../src/interfaces/issues-processor-options';
export class IssuesProcessorMock extends IssuesProcessor {
constructor(
options: IIssuesProcessorOptions,
getActor?: () => Promise<string>,
getIssues?: (page: number) => Promise<Issue[]>,
listIssueComments?: (
issueNumber: number,
sinceDate: string
) => Promise<IComment[]>,
getLabelCreationDate?: (
issue: Issue,
label: string
) => Promise<string | undefined>
) {
super(options);
if (getActor) {
this.getActor = getActor;
}
if (getIssues) {
this.getIssues = getIssues;
}
if (listIssueComments) {
this.listIssueComments = listIssueComments;
}
if (getLabelCreationDate) {
this.getLabelCreationDate = getLabelCreationDate;
}
}
}

View File

@ -41,5 +41,6 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
exemptPrAssignees: '',
exemptAllAssignees: false,
exemptAllIssueAssignees: undefined,
exemptAllPrAssignees: undefined
exemptAllPrAssignees: undefined,
enableStatistics: false
});

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import {Issue} from '../src/classes/issue';
import {IssuesProcessor} from '../src/classes/issues-processor';
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
import {IssuesProcessorMock} from './classes/issues-processor-mock';
import {DefaultProcessorOptions} from './constants/default-processor-options';
import {generateIssue} from './functions/generate-issue';
@ -14,7 +14,7 @@ interface ITestData {
describe('milestones options', (): void => {
let opts: IIssuesProcessorOptions;
let testIssueList: Issue[];
let processor: IssuesProcessor;
let processor: IssuesProcessorMock;
const setTestIssueList = (
isPullRequest: boolean,
@ -37,7 +37,7 @@ describe('milestones options', (): void => {
};
const setProcessor = () => {
processor = new IssuesProcessor(
processor = new IssuesProcessorMock(
opts,
async () => 'abot',
async p => (p === 1 ? testIssueList : []),

View File

@ -1,12 +1,12 @@
import {Issue} from '../src/classes/issue';
import {IssuesProcessor} from '../src/classes/issues-processor';
import {IIssue} from '../src/interfaces/issue';
import {IIssuesProcessorOptions} from '../src/interfaces/issues-processor-options';
import {IssuesProcessorMock} from './classes/issues-processor-mock';
import {DefaultProcessorOptions} from './constants/default-processor-options';
import {generateIssue} from './functions/generate-issue';
let issuesProcessorBuilder: IssuesProcessorBuilder;
let issuesProcessor: IssuesProcessor;
let issuesProcessor: IssuesProcessorMock;
describe('only-labels option', (): void => {
beforeEach((): void => {
@ -1140,8 +1140,8 @@ class IssuesProcessorBuilder {
return this;
}
build(): IssuesProcessor {
return new IssuesProcessor(
build(): IssuesProcessorMock {
return new IssuesProcessorMock(
this._options,
async () => 'abot',
async p => (p === 1 ? this._issues : []),

View File

@ -156,6 +156,10 @@ inputs:
description: 'Exempt all pull requests with assignees from being marked as stale. Override "exempt-all-assignees" option regarding only the pull requests.'
default: ''
required: false
enable-statistics:
description: 'Display some statistics at the end regarding the stale workflow (only when the logs are enabled).'
default: 'true'
required: false
runs:
using: 'node12'
main: 'dist/index.js'

425
dist/index.js vendored
View File

@ -206,7 +206,6 @@ const github_1 = __nccwpck_require__(5438);
const get_humanized_date_1 = __nccwpck_require__(965);
const is_date_more_recent_than_1 = __nccwpck_require__(1473);
const is_valid_date_1 = __nccwpck_require__(891);
const get_issue_type_1 = __nccwpck_require__(5153);
const is_labeled_1 = __nccwpck_require__(6792);
const is_pull_request_1 = __nccwpck_require__(5400);
const should_mark_when_stale_1 = __nccwpck_require__(2461);
@ -216,11 +215,12 @@ const issue_1 = __nccwpck_require__(4783);
const issue_logger_1 = __nccwpck_require__(2984);
const logger_1 = __nccwpck_require__(6212);
const milestones_1 = __nccwpck_require__(4601);
const statistics_1 = __nccwpck_require__(3334);
/***
* Handle processing of issues for staleness/closure.
*/
class IssuesProcessor {
constructor(options, getActor, getIssues, listIssueComments, getLabelCreationDate) {
constructor(options) {
this._logger = new logger_1.Logger();
this._operationsLeft = 0;
this.staleIssues = [];
@ -228,23 +228,14 @@ class IssuesProcessor {
this.deletedBranchIssues = [];
this.removedLabelIssues = [];
this.options = options;
this._operationsLeft = options.operationsPerRun;
this.client = github_1.getOctokit(options.repoToken);
if (getActor) {
this._getActor = getActor;
}
if (getIssues) {
this._getIssues = getIssues;
}
if (listIssueComments) {
this._listIssueComments = listIssueComments;
}
if (getLabelCreationDate) {
this._getLabelCreationDate = getLabelCreationDate;
}
this._operationsLeft = this.options.operationsPerRun;
this.client = github_1.getOctokit(this.options.repoToken);
if (this.options.debugOnly) {
this._logger.warning('Executing in debug mode. Debug output will be written but no issues will be processed.');
}
if (this.options.enableStatistics) {
this._statistics = new statistics_1.Statistics(this.options);
}
}
static _updatedSince(timestamp, num_days) {
const daysInMillis = 1000 * 60 * 60 * 24 * num_days;
@ -252,18 +243,20 @@ class IssuesProcessor {
return millisSinceLastUpdated <= daysInMillis;
}
processIssues(page = 1) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
// get the next batch of issues
const issues = yield this._getIssues(page);
this._operationsLeft -= 1;
const actor = yield this._getActor();
const issues = yield this.getIssues(page);
const actor = yield this.getActor();
if (issues.length <= 0) {
this._logger.info('---');
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.setOperationsLeft(this._operationsLeft).logStats();
this._logger.info('No more issues found to process. Exiting.');
return this._operationsLeft;
}
for (const issue of issues.values()) {
const issueLogger = new issue_logger_1.IssueLogger(issue);
(_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementProcessedIssuesCount();
issueLogger.info(`Found this $$type last updated ${issue.updated_at}`);
// calculate string based messages for this issue
const staleMessage = issue.isPullRequest
@ -281,7 +274,6 @@ class IssuesProcessor {
const skipMessage = issue.isPullRequest
? this.options.skipStalePrMessage
: this.options.skipStaleIssueMessage;
const issueType = get_issue_type_1.getIssueType(issue.isPullRequest);
const daysBeforeStale = issue.isPullRequest
? this._getDaysBeforePrStale()
: this._getDaysBeforeIssueStale();
@ -353,7 +345,7 @@ class IssuesProcessor {
const anyOfLabels = words_to_list_1.wordsToList(this.options.anyOfLabels);
if (anyOfLabels.length &&
!anyOfLabels.some((label) => is_labeled_1.isLabeled(issue, label))) {
issueLogger.info(`Skipping ${issueType} because it does not have any of the required labels`);
issueLogger.info(`Skipping $$type because it does not have any of the required labels`);
continue; // don't process issues without any of the required labels
}
const milestones = new milestones_1.Milestones(this.options, issue);
@ -379,7 +371,7 @@ class IssuesProcessor {
// process the issue if it was marked stale
if (issue.isStale) {
issueLogger.info(`Found a stale $$type`);
yield this._processStaleIssue(issue, issueType, staleLabel, actor, closeMessage, closeLabel);
yield this._processStaleIssue(issue, staleLabel, actor, closeMessage, closeLabel);
}
}
if (this._operationsLeft <= 0) {
@ -390,11 +382,97 @@ class IssuesProcessor {
return this.processIssues(page + 1);
});
}
// handle all of the stale issue logic when we find a stale issue
_processStaleIssue(issue, issueType, staleLabel, actor, closeMessage, closeLabel) {
// grab comments for an issue since a given date
listIssueComments(issueNumber, sinceDate) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// find any comments since date on the given issue
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesCommentsCount();
const comments = yield this.client.issues.listComments({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issueNumber,
since: sinceDate
});
return comments.data;
}
catch (error) {
this._logger.error(`List issue comments error: ${error.message}`);
return Promise.resolve([]);
}
});
}
// get the actor from the GitHub token or context
getActor() {
return __awaiter(this, void 0, void 0, function* () {
let actor;
try {
this._operationsLeft -= 1;
actor = yield this.client.users.getAuthenticated();
}
catch (error) {
return github_1.context.actor;
}
return actor.data.login;
});
}
// grab issues from github in batches of 100
getIssues(page) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
// generate type for response
const endpoint = this.client.issues.listForRepo;
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesCount();
const issueResult = yield this.client.issues.listForRepo({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
state: 'open',
per_page: 100,
direction: this.options.ascending ? 'asc' : 'desc',
page
});
return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue));
}
catch (error) {
this._logger.error(`Get issues for repo error: ${error.message}`);
return Promise.resolve([]);
}
});
}
// returns the creation date of a given label on an issue (or nothing if no label existed)
///see https://developer.github.com/v3/activity/events/
getLabelCreationDate(issue, label) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
const markedStaleOn = (yield this._getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
issueLogger.info(`Checking for label on $$type`);
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedIssuesEventsCount();
const options = this.client.issues.listEvents.endpoint.merge({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
per_page: 100,
issue_number: issue.number
});
const events = yield this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(event => event.event === 'labeled' && event.label.name === label);
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
}
return staleLabeledEvent.created_at;
});
}
// handle all of the stale issue logic when we find a stale issue
_processStaleIssue(issue, staleLabel, actor, closeMessage, closeLabel) {
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
const markedStaleOn = (yield this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
issueLogger.info(`$$type marked stale on: ${markedStaleOn}`);
const issueHasComments = yield this._hasCommentsSince(issue, markedStaleOn, actor);
issueLogger.info(`$$type has been commented on: ${issueHasComments}`);
@ -436,74 +514,20 @@ class IssuesProcessor {
return true;
}
// find any comments since the date
const comments = yield this._listIssueComments(issue.number, sinceDate);
const comments = yield this.listIssueComments(issue.number, sinceDate);
const filteredComments = comments.filter(comment => comment.user.type === 'User' && comment.user.login !== actor);
issueLogger.info(`Comments not made by actor or another bot: ${filteredComments.length}`);
// if there are any user comments returned
return filteredComments.length > 0;
});
}
// grab comments for an issue since a given date
_listIssueComments(issueNumber, sinceDate) {
return __awaiter(this, void 0, void 0, function* () {
// find any comments since date on the given issue
try {
const comments = yield this.client.issues.listComments({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issueNumber,
since: sinceDate
});
return comments.data;
}
catch (error) {
this._logger.error(`List issue comments error: ${error.message}`);
return Promise.resolve([]);
}
});
}
// get the actor from the GitHub token or context
_getActor() {
return __awaiter(this, void 0, void 0, function* () {
let actor;
try {
actor = yield this.client.users.getAuthenticated();
}
catch (error) {
return github_1.context.actor;
}
return actor.data.login;
});
}
// grab issues from github in batches of 100
_getIssues(page) {
return __awaiter(this, void 0, void 0, function* () {
// generate type for response
const endpoint = this.client.issues.listForRepo;
try {
const issueResult = yield this.client.issues.listForRepo({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
state: 'open',
per_page: 100,
direction: this.options.ascending ? 'asc' : 'desc',
page
});
return issueResult.data.map((issue) => new issue_1.Issue(this.options, issue));
}
catch (error) {
this._logger.error(`Get issues for repo error: ${error.message}`);
return Promise.resolve([]);
}
});
}
// Mark an issue as stale with a comment and a label
_markStale(issue, staleMessage, staleLabel, skipMessage) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Marking $$type as stale`);
this.staleIssues.push(issue);
this._operationsLeft -= 2;
// if the issue is being marked stale, the updated date should be changed to right now
// so that close calculations work correctly
const newUpdatedAtDate = new Date();
@ -513,6 +537,8 @@ class IssuesProcessor {
}
if (!skipMessage) {
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedComment();
yield this.client.issues.createComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -525,6 +551,9 @@ class IssuesProcessor {
}
}
try {
this._operationsLeft -= 1;
(_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedLabel();
(_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementStaleIssuesCount();
yield this.client.issues.addLabels({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -539,16 +568,18 @@ class IssuesProcessor {
}
// Close an issue based on staleness
_closeIssue(issue, closeMessage, closeLabel) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Closing $$type for being stale`);
this.closedIssues.push(issue);
this._operationsLeft -= 1;
if (this.options.debugOnly) {
return;
}
if (closeMessage) {
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedComment();
yield this.client.issues.createComment({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -562,6 +593,8 @@ class IssuesProcessor {
}
if (closeLabel) {
try {
this._operationsLeft -= 1;
(_b = this._statistics) === null || _b === void 0 ? void 0 : _b.incrementAddedLabel();
yield this.client.issues.addLabels({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -574,6 +607,8 @@ class IssuesProcessor {
}
}
try {
this._operationsLeft -= 1;
(_c = this._statistics) === null || _c === void 0 ? void 0 : _c.incrementClosedIssuesCount();
yield this.client.issues.update({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -587,10 +622,15 @@ class IssuesProcessor {
});
}
_getPullRequest(issue) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
this._operationsLeft -= 1;
if (this.options.debugOnly) {
return;
}
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementFetchedPullRequestsCount();
const pullRequest = yield this.client.pulls.get({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -605,21 +645,23 @@ class IssuesProcessor {
}
// Delete the branch on closed pull request
_deleteBranch(issue) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Delete branch from closed $$type - ${issue.title}`);
if (this.options.debugOnly) {
return;
}
const pullRequest = yield this._getPullRequest(issue);
if (!pullRequest) {
issueLogger.info(`Not deleting branch as pull request not found for this $$type`);
return;
}
if (this.options.debugOnly) {
return;
}
const branch = pullRequest.head.ref;
issueLogger.info(`Deleting branch ${branch} from closed $$type`);
this._operationsLeft -= 1;
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedBranchesCount();
yield this.client.git.deleteRef({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -633,16 +675,18 @@ class IssuesProcessor {
}
// Remove a label from an issue
_removeLabel(issue, label) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Removing label "${label}" from $$type`);
this.removedLabelIssues.push(issue);
this._operationsLeft -= 1;
// @todo remove the debug only to be able to test the code below
if (this.options.debugOnly) {
return;
}
try {
this._operationsLeft -= 1;
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedLabelsCount();
yield this.client.issues.removeLabel({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
@ -655,29 +699,6 @@ class IssuesProcessor {
}
});
}
// returns the creation date of a given label on an issue (or nothing if no label existed)
///see https://developer.github.com/v3/activity/events/
_getLabelCreationDate(issue, label) {
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Checking for label on $$type`);
this._operationsLeft -= 1;
const options = this.client.issues.listEvents.endpoint.merge({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
per_page: 100,
issue_number: issue.number
});
const events = yield this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(event => event.event === 'labeled' && event.label.name === label);
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
}
return staleLabeledEvent.created_at;
});
}
_getDaysBeforeIssueStale() {
return isNaN(this.options.daysBeforeIssueStale)
? this.options.daysBeforeStale
@ -712,13 +733,16 @@ class IssuesProcessor {
return this.options.onlyLabels;
}
_removeStaleLabel(issue, staleLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`The $$type is no longer stale. Removing the stale label...`);
return this._removeLabel(issue, staleLabel);
yield this._removeLabel(issue, staleLabel);
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementUndoStaleIssuesCount();
});
}
_removeCloseLabel(issue, closeLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`The $$type is not closed nor locked. Trying to remove the close label...`);
@ -728,7 +752,8 @@ class IssuesProcessor {
}
if (is_labeled_1.isLabeled(issue, closeLabel)) {
issueLogger.info(`The $$type has a close label "${closeLabel}". Removing the close label...`);
return this._removeLabel(issue, closeLabel);
yield this._removeLabel(issue, closeLabel);
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementDeletedCloseLabelsCount();
}
});
}
@ -938,18 +963,157 @@ exports.Milestones = Milestones;
/***/ }),
/***/ 9639:
/***/ ((__unused_webpack_module, exports) => {
/***/ 3334:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.IssueType = void 0;
var IssueType;
(function (IssueType) {
IssueType["Issue"] = "issue";
IssueType["PullRequest"] = "pr";
})(IssueType = exports.IssueType || (exports.IssueType = {}));
exports.Statistics = void 0;
const logger_1 = __nccwpck_require__(6212);
class Statistics {
constructor(options) {
this._logger = new logger_1.Logger();
this._processedIssuesCount = 0;
this._staleIssuesCount = 0;
this._undoStaleIssuesCount = 0;
this._operationsCount = 0;
this._closedIssuesCount = 0;
this._deletedLabelsCount = 0;
this._deletedCloseLabelsCount = 0;
this._deletedBranchesCount = 0;
this._addedLabelsCount = 0;
this._addedCommentsCount = 0;
this._fetchedIssuesCount = 0;
this._fetchedIssuesEventsCount = 0;
this._fetchedIssuesCommentsCount = 0;
this._fetchedPullRequestsCount = 0;
this._options = options;
}
incrementProcessedIssuesCount(increment = 1) {
this._processedIssuesCount += increment;
return this;
}
incrementStaleIssuesCount(increment = 1) {
this._staleIssuesCount += increment;
return this;
}
incrementUndoStaleIssuesCount(increment = 1) {
this._undoStaleIssuesCount += increment;
return this;
}
setOperationsLeft(operationsLeft) {
this._operationsCount = this._options.operationsPerRun - operationsLeft;
return this;
}
incrementClosedIssuesCount(increment = 1) {
this._closedIssuesCount += increment;
return this;
}
incrementDeletedLabelsCount(increment = 1) {
this._deletedLabelsCount += increment;
return this;
}
incrementDeletedCloseLabelsCount(increment = 1) {
this._deletedCloseLabelsCount += increment;
return this;
}
incrementDeletedBranchesCount(increment = 1) {
this._deletedBranchesCount += increment;
return this;
}
incrementAddedLabel(increment = 1) {
this._addedLabelsCount += increment;
return this;
}
incrementAddedComment(increment = 1) {
this._addedCommentsCount += increment;
return this;
}
incrementFetchedIssuesCount(increment = 1) {
this._fetchedIssuesCount += increment;
return this;
}
incrementFetchedIssuesEventsCount(increment = 1) {
this._fetchedIssuesEventsCount += increment;
return this;
}
incrementFetchedIssuesCommentsCount(increment = 1) {
this._fetchedIssuesCommentsCount += increment;
return this;
}
incrementFetchedPullRequestsCount(increment = 1) {
this._fetchedPullRequestsCount += increment;
return this;
}
logStats() {
this._logger.info('Statistics');
this._logProcessedIssuesCount();
this._logStaleIssuesCount();
this._logUndoStaleIssuesCount();
this._logOperationsCount();
this._logClosedIssuesCount();
this._logDeletedLabelsCount();
this._logDeletedCloseLabelsCount();
this._logDeletedBranchesCount();
this._logAddedLabelsCount();
this._logAddedCommentsCount();
this._logFetchedIssuesCount();
this._logFetchedIssuesEventsCount();
this._logFetchedIssuesCommentsCount();
this._logFetchedPullRequestsCount();
this._logger.info('---');
return this;
}
_logProcessedIssuesCount() {
this._logCount('Processed issues/PRs', this._processedIssuesCount);
}
_logStaleIssuesCount() {
this._logCount('New stale issues/PRs', this._staleIssuesCount);
}
_logUndoStaleIssuesCount() {
this._logCount('No longer stale issues/PRs', this._undoStaleIssuesCount);
}
_logOperationsCount() {
this._logCount('Operations performed', this._operationsCount);
}
_logClosedIssuesCount() {
this._logCount('Closed issues', this._closedIssuesCount);
}
_logDeletedLabelsCount() {
this._logCount('Deleted labels', this._deletedLabelsCount);
}
_logDeletedCloseLabelsCount() {
this._logCount('Deleted close labels', this._deletedCloseLabelsCount);
}
_logDeletedBranchesCount() {
this._logCount('Deleted branches', this._deletedBranchesCount);
}
_logAddedLabelsCount() {
this._logCount('Added labels', this._addedLabelsCount);
}
_logAddedCommentsCount() {
this._logCount('Added comments', this._addedCommentsCount);
}
_logFetchedIssuesCount() {
this._logCount('Fetched issues', this._fetchedIssuesCount);
}
_logFetchedIssuesEventsCount() {
this._logCount('Fetched issues events', this._fetchedIssuesEventsCount);
}
_logFetchedIssuesCommentsCount() {
this._logCount('Fetched issues comments', this._fetchedIssuesCommentsCount);
}
_logFetchedPullRequestsCount() {
this._logCount('Fetched pull requests', this._fetchedPullRequestsCount);
}
_logCount(name, count) {
if (count > 0) {
this._logger.info(`${name}: ${count}`);
}
}
}
exports.Statistics = Statistics;
/***/ }),
@ -1020,22 +1184,6 @@ function isValidDate(date) {
exports.isValidDate = isValidDate;
/***/ }),
/***/ 5153:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getIssueType = void 0;
const issue_type_1 = __nccwpck_require__(9639);
function getIssueType(isPullRequest) {
return isPullRequest ? issue_type_1.IssueType.PullRequest : issue_type_1.IssueType.Issue;
}
exports.getIssueType = getIssueType;
/***/ }),
/***/ 6792:
@ -1229,7 +1377,8 @@ function _getAndValidateArgs() {
exemptPrAssignees: core.getInput('exempt-pr-assignees'),
exemptAllAssignees: core.getInput('exempt-all-assignees') === 'true',
exemptAllIssueAssignees: _toOptionalBoolean('exempt-all-issue-assignees'),
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees')
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees'),
enableStatistics: core.getInput('enable-statistics') === 'true'
};
for (const numberInput of [
'days-before-stale',

View File

@ -52,7 +52,8 @@ describe('Issue', (): void => {
exemptPrAssignees: '',
exemptAllAssignees: false,
exemptAllIssueAssignees: undefined,
exemptAllPrAssignees: undefined
exemptAllPrAssignees: undefined,
enableStatistics: false
};
issueInterface = {
title: 'dummy-title',

View File

@ -1,11 +1,9 @@
import {context, getOctokit} from '@actions/github';
import {GitHub} from '@actions/github/lib/utils';
import {GetResponseTypeFromEndpointMethod} from '@octokit/types';
import {IssueType} from '../enums/issue-type';
import {getHumanizedDate} from '../functions/dates/get-humanized-date';
import {isDateMoreRecentThan} from '../functions/dates/is-date-more-recent-than';
import {isValidDate} from '../functions/dates/is-valid-date';
import {getIssueType} from '../functions/get-issue-type';
import {isLabeled} from '../functions/is-labeled';
import {isPullRequest} from '../functions/is-pull-request';
import {shouldMarkWhenStale} from '../functions/should-mark-when-stale';
@ -20,6 +18,7 @@ import {Issue} from './issue';
import {IssueLogger} from './loggers/issue-logger';
import {Logger} from './loggers/logger';
import {Milestones} from './milestones';
import {Statistics} from './statistics';
/***
* Handle processing of issues for staleness/closure.
@ -34,6 +33,7 @@ export class IssuesProcessor {
}
private readonly _logger: Logger = new Logger();
private readonly _statistics: Statistics | undefined;
private _operationsLeft = 0;
readonly client: InstanceType<typeof GitHub>;
readonly options: IIssuesProcessorOptions;
@ -42,61 +42,38 @@ export class IssuesProcessor {
readonly deletedBranchIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
constructor(
options: IIssuesProcessorOptions,
getActor?: () => Promise<string>,
getIssues?: (page: number) => Promise<Issue[]>,
listIssueComments?: (
issueNumber: number,
sinceDate: string
) => Promise<IComment[]>,
getLabelCreationDate?: (
issue: Issue,
label: string
) => Promise<string | undefined>
) {
constructor(options: IIssuesProcessorOptions) {
this.options = options;
this._operationsLeft = options.operationsPerRun;
this.client = getOctokit(options.repoToken);
if (getActor) {
this._getActor = getActor;
}
if (getIssues) {
this._getIssues = getIssues;
}
if (listIssueComments) {
this._listIssueComments = listIssueComments;
}
if (getLabelCreationDate) {
this._getLabelCreationDate = getLabelCreationDate;
}
this._operationsLeft = this.options.operationsPerRun;
this.client = getOctokit(this.options.repoToken);
if (this.options.debugOnly) {
this._logger.warning(
'Executing in debug mode. Debug output will be written but no issues will be processed.'
);
}
if (this.options.enableStatistics) {
this._statistics = new Statistics(this.options);
}
}
async processIssues(page = 1): Promise<number> {
// get the next batch of issues
const issues: Issue[] = await this._getIssues(page);
this._operationsLeft -= 1;
const actor: string = await this._getActor();
const issues: Issue[] = await this.getIssues(page);
const actor: string = await this.getActor();
if (issues.length <= 0) {
this._logger.info('---');
this._statistics?.setOperationsLeft(this._operationsLeft).logStats();
this._logger.info('No more issues found to process. Exiting.');
return this._operationsLeft;
}
for (const issue of issues.values()) {
const issueLogger: IssueLogger = new IssueLogger(issue);
this._statistics?.incrementProcessedIssuesCount();
issueLogger.info(`Found this $$type last updated ${issue.updated_at}`);
@ -116,7 +93,6 @@ export class IssuesProcessor {
const skipMessage = issue.isPullRequest
? this.options.skipStalePrMessage
: this.options.skipStaleIssueMessage;
const issueType: IssueType = getIssueType(issue.isPullRequest);
const daysBeforeStale: number = issue.isPullRequest
? this._getDaysBeforePrStale()
: this._getDaysBeforeIssueStale();
@ -238,7 +214,7 @@ export class IssuesProcessor {
)
) {
issueLogger.info(
`Skipping ${issueType} because it does not have any of the required labels`
`Skipping $$type because it does not have any of the required labels`
);
continue; // don't process issues without any of the required labels
}
@ -282,7 +258,6 @@ export class IssuesProcessor {
issueLogger.info(`Found a stale $$type`);
await this._processStaleIssue(
issue,
issueType,
staleLabel,
actor,
closeMessage,
@ -302,10 +277,108 @@ export class IssuesProcessor {
return this.processIssues(page + 1);
}
// grab comments for an issue since a given date
async listIssueComments(
issueNumber: number,
sinceDate: string
): Promise<IComment[]> {
// find any comments since date on the given issue
try {
this._operationsLeft -= 1;
this._statistics?.incrementFetchedIssuesCommentsCount();
const comments = await this.client.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
since: sinceDate
});
return comments.data;
} catch (error) {
this._logger.error(`List issue comments error: ${error.message}`);
return Promise.resolve([]);
}
}
// get the actor from the GitHub token or context
async getActor(): Promise<string> {
let actor;
try {
this._operationsLeft -= 1;
actor = await this.client.users.getAuthenticated();
} catch (error) {
return context.actor;
}
return actor.data.login;
}
// grab issues from github in batches of 100
async getIssues(page: number): Promise<Issue[]> {
// generate type for response
const endpoint = this.client.issues.listForRepo;
type OctoKitIssueList = GetResponseTypeFromEndpointMethod<typeof endpoint>;
try {
this._operationsLeft -= 1;
this._statistics?.incrementFetchedIssuesCount();
const issueResult: OctoKitIssueList = await this.client.issues.listForRepo(
{
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
direction: this.options.ascending ? 'asc' : 'desc',
page
}
);
return issueResult.data.map(
(issue: Readonly<IIssue>): Issue => new Issue(this.options, issue)
);
} catch (error) {
this._logger.error(`Get issues for repo error: ${error.message}`);
return Promise.resolve([]);
}
}
// returns the creation date of a given label on an issue (or nothing if no label existed)
///see https://developer.github.com/v3/activity/events/
async getLabelCreationDate(
issue: Issue,
label: string
): Promise<string | undefined> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Checking for label on $$type`);
this._operationsLeft -= 1;
this._statistics?.incrementFetchedIssuesEventsCount();
const options = this.client.issues.listEvents.endpoint.merge({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
issue_number: issue.number
});
const events: IIssueEvent[] = await this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(
event => event.event === 'labeled' && event.label.name === label
);
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
}
return staleLabeledEvent.created_at;
}
// handle all of the stale issue logic when we find a stale issue
private async _processStaleIssue(
issue: Issue,
issueType: IssueType,
staleLabel: string,
actor: string,
closeMessage?: string,
@ -313,7 +386,7 @@ export class IssuesProcessor {
) {
const issueLogger: IssueLogger = new IssueLogger(issue);
const markedStaleOn: string =
(await this._getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
(await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
issueLogger.info(`$$type marked stale on: ${markedStaleOn}`);
const issueHasComments: boolean = await this._hasCommentsSince(
@ -381,7 +454,7 @@ export class IssuesProcessor {
}
// find any comments since the date
const comments = await this._listIssueComments(issue.number, sinceDate);
const comments = await this.listIssueComments(issue.number, sinceDate);
const filteredComments = comments.filter(
comment => comment.user.type === 'User' && comment.user.login !== actor
@ -395,65 +468,6 @@ export class IssuesProcessor {
return filteredComments.length > 0;
}
// grab comments for an issue since a given date
private async _listIssueComments(
issueNumber: number,
sinceDate: string
): Promise<IComment[]> {
// find any comments since date on the given issue
try {
const comments = await this.client.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
since: sinceDate
});
return comments.data;
} catch (error) {
this._logger.error(`List issue comments error: ${error.message}`);
return Promise.resolve([]);
}
}
// get the actor from the GitHub token or context
private async _getActor(): Promise<string> {
let actor;
try {
actor = await this.client.users.getAuthenticated();
} catch (error) {
return context.actor;
}
return actor.data.login;
}
// grab issues from github in batches of 100
private async _getIssues(page: number): Promise<Issue[]> {
// generate type for response
const endpoint = this.client.issues.listForRepo;
type OctoKitIssueList = GetResponseTypeFromEndpointMethod<typeof endpoint>;
try {
const issueResult: OctoKitIssueList = await this.client.issues.listForRepo(
{
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100,
direction: this.options.ascending ? 'asc' : 'desc',
page
}
);
return issueResult.data.map(
(issue: Readonly<IIssue>): Issue => new Issue(this.options, issue)
);
} catch (error) {
this._logger.error(`Get issues for repo error: ${error.message}`);
return Promise.resolve([]);
}
}
// Mark an issue as stale with a comment and a label
private async _markStale(
issue: Issue,
@ -464,11 +478,8 @@ export class IssuesProcessor {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Marking $$type as stale`);
this.staleIssues.push(issue);
this._operationsLeft -= 2;
// if the issue is being marked stale, the updated date should be changed to right now
// so that close calculations work correctly
const newUpdatedAtDate: Date = new Date();
@ -480,6 +491,8 @@ export class IssuesProcessor {
if (!skipMessage) {
try {
this._operationsLeft -= 1;
this._statistics?.incrementAddedComment();
await this.client.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@ -492,6 +505,9 @@ export class IssuesProcessor {
}
try {
this._operationsLeft -= 1;
this._statistics?.incrementAddedLabel();
this._statistics?.incrementStaleIssuesCount();
await this.client.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@ -512,17 +528,16 @@ export class IssuesProcessor {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Closing $$type for being stale`);
this.closedIssues.push(issue);
this._operationsLeft -= 1;
if (this.options.debugOnly) {
return;
}
if (closeMessage) {
try {
this._operationsLeft -= 1;
this._statistics?.incrementAddedComment();
await this.client.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
@ -536,6 +551,8 @@ export class IssuesProcessor {
if (closeLabel) {
try {
this._operationsLeft -= 1;
this._statistics?.incrementAddedLabel();
await this.client.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
@ -548,6 +565,8 @@ export class IssuesProcessor {
}
try {
this._operationsLeft -= 1;
this._statistics?.incrementClosedIssuesCount();
await this.client.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
@ -561,11 +580,16 @@ export class IssuesProcessor {
private async _getPullRequest(
issue: Issue
): Promise<IPullRequest | undefined> {
): Promise<IPullRequest | undefined | void> {
const issueLogger: IssueLogger = new IssueLogger(issue);
this._operationsLeft -= 1;
if (this.options.debugOnly) {
return;
}
try {
this._operationsLeft -= 1;
this._statistics?.incrementFetchedPullRequestsCount();
const pullRequest = await this.client.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
@ -584,10 +608,6 @@ export class IssuesProcessor {
issueLogger.info(`Delete branch from closed $$type - ${issue.title}`);
if (this.options.debugOnly) {
return;
}
const pullRequest = await this._getPullRequest(issue);
if (!pullRequest) {
@ -597,12 +617,16 @@ export class IssuesProcessor {
return;
}
if (this.options.debugOnly) {
return;
}
const branch = pullRequest.head.ref;
issueLogger.info(`Deleting branch ${branch} from closed $$type`);
this._operationsLeft -= 1;
try {
this._operationsLeft -= 1;
this._statistics?.incrementDeletedBranchesCount();
await this.client.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
@ -620,17 +644,16 @@ export class IssuesProcessor {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Removing label "${label}" from $$type`);
this.removedLabelIssues.push(issue);
this._operationsLeft -= 1;
// @todo remove the debug only to be able to test the code below
if (this.options.debugOnly) {
return;
}
try {
this._operationsLeft -= 1;
this._statistics?.incrementDeletedLabelsCount();
await this.client.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
@ -642,40 +665,6 @@ export class IssuesProcessor {
}
}
// returns the creation date of a given label on an issue (or nothing if no label existed)
///see https://developer.github.com/v3/activity/events/
private async _getLabelCreationDate(
issue: Issue,
label: string
): Promise<string | undefined> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Checking for label on $$type`);
this._operationsLeft -= 1;
const options = this.client.issues.listEvents.endpoint.merge({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100,
issue_number: issue.number
});
const events: IIssueEvent[] = await this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(
event => event.event === 'labeled' && event.label.name === label
);
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
}
return staleLabeledEvent.created_at;
}
private _getDaysBeforeIssueStale(): number {
return isNaN(this.options.daysBeforeIssueStale)
? this.options.daysBeforeStale
@ -724,7 +713,8 @@ export class IssuesProcessor {
`The $$type is no longer stale. Removing the stale label...`
);
return this._removeLabel(issue, staleLabel);
await this._removeLabel(issue, staleLabel);
this._statistics?.incrementUndoStaleIssuesCount();
}
private async _removeCloseLabel(
@ -748,7 +738,8 @@ export class IssuesProcessor {
`The $$type has a close label "${closeLabel}". Removing the close label...`
);
return this._removeLabel(issue, closeLabel);
await this._removeLabel(issue, closeLabel);
this._statistics?.incrementDeletedCloseLabelsCount();
}
}
}

200
src/classes/statistics.ts Normal file
View File

@ -0,0 +1,200 @@
import {IIssuesProcessorOptions} from '../interfaces/issues-processor-options';
import {Logger} from './loggers/logger';
export class Statistics {
private readonly _logger: Logger = new Logger();
private readonly _options: IIssuesProcessorOptions;
private _processedIssuesCount = 0;
private _staleIssuesCount = 0;
private _undoStaleIssuesCount = 0;
private _operationsCount = 0;
private _closedIssuesCount = 0;
private _deletedLabelsCount = 0;
private _deletedCloseLabelsCount = 0;
private _deletedBranchesCount = 0;
private _addedLabelsCount = 0;
private _addedCommentsCount = 0;
private _fetchedIssuesCount = 0;
private _fetchedIssuesEventsCount = 0;
private _fetchedIssuesCommentsCount = 0;
private _fetchedPullRequestsCount = 0;
constructor(options: IIssuesProcessorOptions) {
this._options = options;
}
incrementProcessedIssuesCount(increment: Readonly<number> = 1): Statistics {
this._processedIssuesCount += increment;
return this;
}
incrementStaleIssuesCount(increment: Readonly<number> = 1): Statistics {
this._staleIssuesCount += increment;
return this;
}
incrementUndoStaleIssuesCount(increment: Readonly<number> = 1): Statistics {
this._undoStaleIssuesCount += increment;
return this;
}
setOperationsLeft(operationsLeft: Readonly<number>): Statistics {
this._operationsCount = this._options.operationsPerRun - operationsLeft;
return this;
}
incrementClosedIssuesCount(increment: Readonly<number> = 1): Statistics {
this._closedIssuesCount += increment;
return this;
}
incrementDeletedLabelsCount(increment: Readonly<number> = 1): Statistics {
this._deletedLabelsCount += increment;
return this;
}
incrementDeletedCloseLabelsCount(
increment: Readonly<number> = 1
): Statistics {
this._deletedCloseLabelsCount += increment;
return this;
}
incrementDeletedBranchesCount(increment: Readonly<number> = 1): Statistics {
this._deletedBranchesCount += increment;
return this;
}
incrementAddedLabel(increment: Readonly<number> = 1): Statistics {
this._addedLabelsCount += increment;
return this;
}
incrementAddedComment(increment: Readonly<number> = 1): Statistics {
this._addedCommentsCount += increment;
return this;
}
incrementFetchedIssuesCount(increment: Readonly<number> = 1): Statistics {
this._fetchedIssuesCount += increment;
return this;
}
incrementFetchedIssuesEventsCount(
increment: Readonly<number> = 1
): Statistics {
this._fetchedIssuesEventsCount += increment;
return this;
}
incrementFetchedIssuesCommentsCount(
increment: Readonly<number> = 1
): Statistics {
this._fetchedIssuesCommentsCount += increment;
return this;
}
incrementFetchedPullRequestsCount(
increment: Readonly<number> = 1
): Statistics {
this._fetchedPullRequestsCount += increment;
return this;
}
logStats(): Statistics {
this._logger.info('Statistics');
this._logProcessedIssuesCount();
this._logStaleIssuesCount();
this._logUndoStaleIssuesCount();
this._logOperationsCount();
this._logClosedIssuesCount();
this._logDeletedLabelsCount();
this._logDeletedCloseLabelsCount();
this._logDeletedBranchesCount();
this._logAddedLabelsCount();
this._logAddedCommentsCount();
this._logFetchedIssuesCount();
this._logFetchedIssuesEventsCount();
this._logFetchedIssuesCommentsCount();
this._logFetchedPullRequestsCount();
this._logger.info('---');
return this;
}
private _logProcessedIssuesCount(): void {
this._logCount('Processed issues/PRs', this._processedIssuesCount);
}
private _logStaleIssuesCount(): void {
this._logCount('New stale issues/PRs', this._staleIssuesCount);
}
private _logUndoStaleIssuesCount(): void {
this._logCount('No longer stale issues/PRs', this._undoStaleIssuesCount);
}
private _logOperationsCount(): void {
this._logCount('Operations performed', this._operationsCount);
}
private _logClosedIssuesCount(): void {
this._logCount('Closed issues', this._closedIssuesCount);
}
private _logDeletedLabelsCount(): void {
this._logCount('Deleted labels', this._deletedLabelsCount);
}
private _logDeletedCloseLabelsCount(): void {
this._logCount('Deleted close labels', this._deletedCloseLabelsCount);
}
private _logDeletedBranchesCount(): void {
this._logCount('Deleted branches', this._deletedBranchesCount);
}
private _logAddedLabelsCount(): void {
this._logCount('Added labels', this._addedLabelsCount);
}
private _logAddedCommentsCount(): void {
this._logCount('Added comments', this._addedCommentsCount);
}
private _logFetchedIssuesCount(): void {
this._logCount('Fetched issues', this._fetchedIssuesCount);
}
private _logFetchedIssuesEventsCount(): void {
this._logCount('Fetched issues events', this._fetchedIssuesEventsCount);
}
private _logFetchedIssuesCommentsCount(): void {
this._logCount('Fetched issues comments', this._fetchedIssuesCommentsCount);
}
private _logFetchedPullRequestsCount(): void {
this._logCount('Fetched pull requests', this._fetchedPullRequestsCount);
}
private _logCount(name: Readonly<string>, count: Readonly<number>): void {
if (count > 0) {
this._logger.info(`${name}: ${count}`);
}
}
}

View File

@ -1,33 +0,0 @@
import {getIssueType} from './get-issue-type';
describe('getIssueType()', (): void => {
let isPullRequest: boolean;
describe('when the issue is a not pull request', (): void => {
beforeEach((): void => {
isPullRequest = false;
});
it('should return that the issue is really an issue', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('issue');
});
});
describe('when the issue is a pull request', (): void => {
beforeEach((): void => {
isPullRequest = true;
});
it('should return that the issue is a pull request', (): void => {
expect.assertions(1);
const result = getIssueType(isPullRequest);
expect(result).toStrictEqual('pr');
});
});
});

View File

@ -1,5 +0,0 @@
import {IssueType} from '../enums/issue-type';
export function getIssueType(isPullRequest: Readonly<boolean>): IssueType {
return isPullRequest ? IssueType.PullRequest : IssueType.Issue;
}

View File

@ -42,4 +42,5 @@ export interface IIssuesProcessorOptions {
exemptAllAssignees: boolean;
exemptAllIssueAssignees: boolean | undefined;
exemptAllPrAssignees: boolean | undefined;
enableStatistics: boolean;
}

View File

@ -68,7 +68,8 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
exemptPrAssignees: core.getInput('exempt-pr-assignees'),
exemptAllAssignees: core.getInput('exempt-all-assignees') === 'true',
exemptAllIssueAssignees: _toOptionalBoolean('exempt-all-issue-assignees'),
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees')
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees'),
enableStatistics: core.getInput('enable-statistics') === 'true'
};
for (const numberInput of [