Skip to content

Commit

Permalink
Add support for adding & removing labels when no longer stale (#468)
Browse files Browse the repository at this point in the history
* Add support for adding & removing labels when no longer stale

* Add remove/addLabelsWhenUpdatedFromStale to relevant spec files. Modify arguments to remove ambiguity in 'labels' var & parameter

* Change parameters for clarity, let autoformat do its thing

* PR feedback: More useful logging when removing labels

* Wrap client calls in try catches

* Use Unstale in variable names

* Don't run add label logic under debug

* Add test for labels added to unstale issues

* PR Feedback: logging

* Update README

* Rename vars to labels-to-add/remove-when-unstale

* Apply doc suggestions from code review

Co-authored-by: Geoffrey Testelin <geoffrey.testelin@gmail.com>

* PR Feedback

Co-authored-by: Geoffrey Testelin <geoffrey.testelin@gmail.com>
  • Loading branch information
benvillalobos and C0ZEN committed Jun 8, 2021
1 parent 52f5648 commit b1da9e1
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 11 deletions.
16 changes: 16 additions & 0 deletions README.md
Expand Up @@ -43,6 +43,8 @@ Every argument is optional.
| [remove-stale-when-updated](#remove-stale-when-updated) | Remove stale label from issues/PRs on updates/comments | `true` |
| [remove-issue-stale-when-updated](#remove-issue-stale-when-updated) | Remove stale label from issues on updates/comments | |
| [remove-pr-stale-when-updated](#remove-pr-stale-when-updated) | Remove stale label from PRs on updates/comments | |
| [labels-to-add-when-unstale](#labels-to-add-when-unstale) | Add specified labels from issues/PRs when they become unstale | |
| [labels-to-remove-when-unstale](#labels-to-remove-when-unstale) | Remove specified labels from issues/PRs when they become unstale | |
| [debug-only](#debug-only) | Dry-run | `false` |
| [ascending](#ascending) | Order to get issues/PRs | `false` |
| [start-date](#start-date) | Skip stale action for issues/PRs created before it | |
Expand Down Expand Up @@ -307,6 +309,20 @@ Override [remove-stale-when-updated](#remove-stale-when-updated) but only to aut

Default value: unset

#### labels-to-add-when-unstale

A comma delimited list of labels to add when a stale issue or pull request receives activity and has the [stale-issue-label](#stale-issue-label) or [stale-pr-label](#stale-pr-label) removed from it.

Default value: unset

#### labels-to-remove-when-unstale

A comma delimited list of labels to remove when a stale issue or pull request receives activity and has the [stale-issue-label](#stale-issue-label) or [stale-pr-label](#stale-pr-label) removed from it.

Warning: each label results in a unique API call which can drastically consume the limit of [operations-per-run](#operations-per-run).

Default value: unset

#### debug-only

Run the stale workflow as dry-run.
Expand Down
4 changes: 3 additions & 1 deletion __tests__/constants/default-processor-options.ts
Expand Up @@ -44,5 +44,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
exemptAllAssignees: false,
exemptAllIssueAssignees: undefined,
exemptAllPrAssignees: undefined,
enableStatistics: true
enableStatistics: true,
labelsToRemoveWhenUnstale: '',
labelsToAddWhenUnstale: ''
});
44 changes: 44 additions & 0 deletions __tests__/main.spec.ts
Expand Up @@ -1255,6 +1255,50 @@ test('stale label should be removed if a comment was added to a stale issue', as
expect(processor.removedLabelIssues).toHaveLength(1);
});

test('when the option "labelsToAddWhenUnstale" is set, the labels should be added when unstale', async () => {
expect.assertions(4);
const opts = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true,
labelsToAddWhenUnstale: 'test'
};
const TestIssueList: Issue[] = [
generateIssue(
opts,
1,
'An issue that should have labels added to it when unstale',
'2020-01-01T17:00:00Z',
'2020-01-01T17:00:00Z',
false,
['Stale']
)
];
const processor = new IssuesProcessorMock(
opts,
async () => 'abot',
async p => (p === 1 ? TestIssueList : []),
async () => [
{
user: {
login: 'notme',
type: 'User'
}
}
], // return a fake comment to indicate there was an update
async () => new Date().toDateString()
);

// process our fake issue list
await processor.processIssues(1);

expect(processor.closedIssues).toHaveLength(0);
expect(processor.staleIssues).toHaveLength(0);
// Stale should have been removed
expect(processor.removedLabelIssues).toHaveLength(1);
// Some label should have been added
expect(processor.addedLabelIssues).toHaveLength(1);
});

test('stale label should not be removed if a comment was added by the bot (and the issue should be closed)', async () => {
const opts = {...DefaultProcessorOptions, removeStaleWhenUpdated: true};
github.context.actor = 'abot';
Expand Down
58 changes: 53 additions & 5 deletions dist/index.js
Expand Up @@ -253,6 +253,7 @@ class IssuesProcessor {
this.closedIssues = [];
this.deletedBranchIssues = [];
this.removedLabelIssues = [];
this.addedLabelIssues = [];
this.options = options;
this.client = github_1.getOctokit(this.options.repoToken);
this.operations = new stale_operations_1.StaleOperations(this.options);
Expand Down Expand Up @@ -299,14 +300,16 @@ class IssuesProcessor {
else {
this._logger.info(`${logger_service_1.LoggerService.yellow('Processing the batch of issues')} ${logger_service_1.LoggerService.cyan(`#${page}`)} ${logger_service_1.LoggerService.yellow('containing')} ${logger_service_1.LoggerService.cyan(issues.length)} ${logger_service_1.LoggerService.yellow(`issue${issues.length > 1 ? 's' : ''}...`)}`);
}
const labelsToAddWhenUnstale = words_to_list_1.wordsToList(this.options.labelsToAddWhenUnstale);
const labelsToRemoveWhenUnstale = words_to_list_1.wordsToList(this.options.labelsToRemoveWhenUnstale);
for (const issue of issues.values()) {
// Stop the processing if no more operations remains
if (!this.operations.hasRemainingOperations()) {
break;
}
const issueLogger = new issue_logger_1.IssueLogger(issue);
yield issueLogger.grouping(`$$type #${issue.number}`, () => __awaiter(this, void 0, void 0, function* () {
yield this.processIssue(issue, actor);
yield this.processIssue(issue, actor, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale);
}));
}
if (!this.operations.hasRemainingOperations()) {
Expand All @@ -320,7 +323,7 @@ class IssuesProcessor {
return this.processIssues(page + 1);
});
}
processIssue(issue, actor) {
processIssue(issue, actor, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementProcessedItemsCount(issue);
Expand Down Expand Up @@ -469,7 +472,7 @@ class IssuesProcessor {
// Process the issue if it was marked stale
if (issue.isStale) {
issueLogger.info(`This $$type is already stale`);
yield this._processStaleIssue(issue, staleLabel, actor, closeMessage, closeLabel);
yield this._processStaleIssue(issue, staleLabel, actor, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, closeMessage, closeLabel);
}
IssuesProcessor._endIssueProcessing(issue);
});
Expand Down Expand Up @@ -561,7 +564,7 @@ class IssuesProcessor {
});
}
// handle all of the stale issue logic when we find a stale issue
_processStaleIssue(issue, staleLabel, actor, closeMessage, closeLabel) {
_processStaleIssue(issue, staleLabel, actor, labelsToAddWhenUnstale, labelsToRemoveWhenUnstale, 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;
Expand All @@ -586,6 +589,9 @@ class IssuesProcessor {
if (shouldRemoveStaleWhenUpdated && issueHasComments) {
issueLogger.info(`Remove the stale label since the $$type has a comment and the workflow should remove the stale label when updated`);
yield this._removeStaleLabel(issue, staleLabel);
// Are there labels to remove or add when an issue is no longer stale?
yield this._removeLabelsWhenUnstale(issue, labelsToRemoveWhenUnstale);
yield this._addLabelsWhenUnstale(issue, labelsToAddWhenUnstale);
issueLogger.info(`Skipping the process since the $$type is now un-stale`);
return; // Nothing to do because it is no longer stale
}
Expand Down Expand Up @@ -854,6 +860,44 @@ class IssuesProcessor {
}
return this.options.removeStaleWhenUpdated;
}
_removeLabelsWhenUnstale(issue, removeLabels) {
return __awaiter(this, void 0, void 0, function* () {
if (!removeLabels.length) {
return;
}
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Removing all the labels specified via the ${this._logger.createOptionLink(option_1.Option.LabelsToRemoveWhenUnstale)} option.`);
for (const label of removeLabels.values()) {
yield this._removeLabel(issue, label);
}
});
}
_addLabelsWhenUnstale(issue, labelsToAdd) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
if (!labelsToAdd.length) {
return;
}
const issueLogger = new issue_logger_1.IssueLogger(issue);
issueLogger.info(`Adding all the labels specified via the ${this._logger.createOptionLink(option_1.Option.LabelsToAddWhenUnstale)} option.`);
this.addedLabelIssues.push(issue);
try {
this.operations.consumeOperation();
(_a = this._statistics) === null || _a === void 0 ? void 0 : _a.incrementAddedItemsLabel(issue);
if (!this.options.debugOnly) {
yield this.client.issues.addLabels({
owner: github_1.context.repo.owner,
repo: github_1.context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
}
}
catch (error) {
this._logger.error(`Error when adding labels after updated from stale: ${error.message}`);
}
});
}
_removeStaleLabel(issue, staleLabel) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
Expand Down Expand Up @@ -1692,6 +1736,8 @@ var Option;
Option["ExemptAllIssueAssignees"] = "exempt-all-issue-assignees";
Option["ExemptAllPrAssignees"] = "exempt-all-pr-assignees";
Option["EnableStatistics"] = "enable-statistics";
Option["LabelsToRemoveWhenUnstale"] = "labels-to-remove-when-unstale";
Option["LabelsToAddWhenUnstale"] = "labels-to-add-when-unstale";
})(Option = exports.Option || (exports.Option = {}));


Expand Down Expand Up @@ -1975,7 +2021,9 @@ function _getAndValidateArgs() {
exemptAllAssignees: core.getInput('exempt-all-assignees') === 'true',
exemptAllIssueAssignees: _toOptionalBoolean('exempt-all-issue-assignees'),
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees'),
enableStatistics: core.getInput('enable-statistics') === 'true'
enableStatistics: core.getInput('enable-statistics') === 'true',
labelsToRemoveWhenUnstale: core.getInput('labels-to-remove-when-unstale'),
labelsToAddWhenUnstale: core.getInput('labels-to-add-when-unstale')
};
for (const numberInput of [
'days-before-stale',
Expand Down
4 changes: 3 additions & 1 deletion src/classes/issue.spec.ts
Expand Up @@ -55,7 +55,9 @@ describe('Issue', (): void => {
exemptAllAssignees: false,
exemptAllIssueAssignees: undefined,
exemptAllPrAssignees: undefined,
enableStatistics: false
enableStatistics: false,
labelsToRemoveWhenUnstale: '',
labelsToAddWhenUnstale: ''
};
issueInterface = {
title: 'dummy-title',
Expand Down
87 changes: 85 additions & 2 deletions src/classes/issues-processor.ts
Expand Up @@ -75,6 +75,7 @@ export class IssuesProcessor {
readonly closedIssues: Issue[] = [];
readonly deletedBranchIssues: Issue[] = [];
readonly removedLabelIssues: Issue[] = [];
readonly addedLabelIssues: Issue[] = [];

constructor(options: IIssuesProcessorOptions) {
this.options = options;
Expand Down Expand Up @@ -127,6 +128,13 @@ export class IssuesProcessor {
);
}

const labelsToAddWhenUnstale: string[] = wordsToList(
this.options.labelsToAddWhenUnstale
);
const labelsToRemoveWhenUnstale: string[] = wordsToList(
this.options.labelsToRemoveWhenUnstale
);

for (const issue of issues.values()) {
// Stop the processing if no more operations remains
if (!this.operations.hasRemainingOperations()) {
Expand All @@ -135,7 +143,12 @@ export class IssuesProcessor {

const issueLogger: IssueLogger = new IssueLogger(issue);
await issueLogger.grouping(`$$type #${issue.number}`, async () => {
await this.processIssue(issue, actor);
await this.processIssue(
issue,
actor,
labelsToAddWhenUnstale,
labelsToRemoveWhenUnstale
);
});
}

Expand Down Expand Up @@ -169,7 +182,12 @@ export class IssuesProcessor {
return this.processIssues(page + 1);
}

async processIssue(issue: Issue, actor: string): Promise<void> {
async processIssue(
issue: Issue,
actor: string,
labelsToAddWhenUnstale: Readonly<string>[],
labelsToRemoveWhenUnstale: Readonly<string>[]
): Promise<void> {
this._statistics?.incrementProcessedItemsCount(issue);

const issueLogger: IssueLogger = new IssueLogger(issue);
Expand Down Expand Up @@ -438,6 +456,8 @@ export class IssuesProcessor {
issue,
staleLabel,
actor,
labelsToAddWhenUnstale,
labelsToRemoveWhenUnstale,
closeMessage,
closeLabel
);
Expand Down Expand Up @@ -549,6 +569,8 @@ export class IssuesProcessor {
issue: Issue,
staleLabel: string,
actor: string,
labelsToAddWhenUnstale: Readonly<string>[],
labelsToRemoveWhenUnstale: Readonly<string>[],
closeMessage?: string,
closeLabel?: string
) {
Expand Down Expand Up @@ -608,6 +630,10 @@ export class IssuesProcessor {
);
await this._removeStaleLabel(issue, staleLabel);

// Are there labels to remove or add when an issue is no longer stale?
await this._removeLabelsWhenUnstale(issue, labelsToRemoveWhenUnstale);
await this._addLabelsWhenUnstale(issue, labelsToAddWhenUnstale);

issueLogger.info(`Skipping the process since the $$type is now un-stale`);

return; // Nothing to do because it is no longer stale
Expand Down Expand Up @@ -956,6 +982,63 @@ export class IssuesProcessor {
return this.options.removeStaleWhenUpdated;
}

private async _removeLabelsWhenUnstale(
issue: Issue,
removeLabels: Readonly<string>[]
): Promise<void> {
if (!removeLabels.length) {
return;
}

const issueLogger: IssueLogger = new IssueLogger(issue);

issueLogger.info(
`Removing all the labels specified via the ${this._logger.createOptionLink(
Option.LabelsToRemoveWhenUnstale
)} option.`
);

for (const label of removeLabels.values()) {
await this._removeLabel(issue, label);
}
}

private async _addLabelsWhenUnstale(
issue: Issue,
labelsToAdd: Readonly<string>[]
): Promise<void> {
if (!labelsToAdd.length) {
return;
}

const issueLogger: IssueLogger = new IssueLogger(issue);

issueLogger.info(
`Adding all the labels specified via the ${this._logger.createOptionLink(
Option.LabelsToAddWhenUnstale
)} option.`
);

this.addedLabelIssues.push(issue);

try {
this.operations.consumeOperation();
this._statistics?.incrementAddedItemsLabel(issue);
if (!this.options.debugOnly) {
await this.client.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
labels: labelsToAdd
});
}
} catch (error) {
this._logger.error(
`Error when adding labels after updated from stale: ${error.message}`
);
}
}

private async _removeStaleLabel(
issue: Issue,
staleLabel: Readonly<string>
Expand Down
4 changes: 3 additions & 1 deletion src/enums/option.ts
Expand Up @@ -40,5 +40,7 @@ export enum Option {
ExemptAllAssignees = 'exempt-all-assignees',
ExemptAllIssueAssignees = 'exempt-all-issue-assignees',
ExemptAllPrAssignees = 'exempt-all-pr-assignees',
EnableStatistics = 'enable-statistics'
EnableStatistics = 'enable-statistics',
LabelsToRemoveWhenUnstale = 'labels-to-remove-when-unstale',
LabelsToAddWhenUnstale = 'labels-to-add-when-unstale'
}
2 changes: 2 additions & 0 deletions src/interfaces/issues-processor-options.ts
Expand Up @@ -45,4 +45,6 @@ export interface IIssuesProcessorOptions {
exemptAllIssueAssignees: boolean | undefined;
exemptAllPrAssignees: boolean | undefined;
enableStatistics: boolean;
labelsToRemoveWhenUnstale: string;
labelsToAddWhenUnstale: string;
}
4 changes: 3 additions & 1 deletion src/main.ts
Expand Up @@ -81,7 +81,9 @@ function _getAndValidateArgs(): IIssuesProcessorOptions {
exemptAllAssignees: core.getInput('exempt-all-assignees') === 'true',
exemptAllIssueAssignees: _toOptionalBoolean('exempt-all-issue-assignees'),
exemptAllPrAssignees: _toOptionalBoolean('exempt-all-pr-assignees'),
enableStatistics: core.getInput('enable-statistics') === 'true'
enableStatistics: core.getInput('enable-statistics') === 'true',
labelsToRemoveWhenUnstale: core.getInput('labels-to-remove-when-unstale'),
labelsToAddWhenUnstale: core.getInput('labels-to-add-when-unstale')
};

for (const numberInput of [
Expand Down

0 comments on commit b1da9e1

Please sign in to comment.