Add support for adding & removing labels when no longer stale (#468)
* 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>
This commit is contained in:
parent
52f5648db3
commit
b1da9e1fb1
16
README.md
16
README.md
|
@ -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 | |
|
||||
|
@ -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.
|
||||
|
|
|
@ -44,5 +44,7 @@ export const DefaultProcessorOptions: IIssuesProcessorOptions = Object.freeze({
|
|||
exemptAllAssignees: false,
|
||||
exemptAllIssueAssignees: undefined,
|
||||
exemptAllPrAssignees: undefined,
|
||||
enableStatistics: true
|
||||
enableStatistics: true,
|
||||
labelsToRemoveWhenUnstale: '',
|
||||
labelsToAddWhenUnstale: ''
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
|
@ -299,6 +300,8 @@ 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()) {
|
||||
|
@ -306,7 +309,7 @@ class IssuesProcessor {
|
|||
}
|
||||
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()) {
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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* () {
|
||||
|
@ -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 = {}));
|
||||
|
||||
|
||||
|
@ -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',
|
||||
|
|
|
@ -55,7 +55,9 @@ describe('Issue', (): void => {
|
|||
exemptAllAssignees: false,
|
||||
exemptAllIssueAssignees: undefined,
|
||||
exemptAllPrAssignees: undefined,
|
||||
enableStatistics: false
|
||||
enableStatistics: false,
|
||||
labelsToRemoveWhenUnstale: '',
|
||||
labelsToAddWhenUnstale: ''
|
||||
};
|
||||
issueInterface = {
|
||||
title: 'dummy-title',
|
||||
|
|
|
@ -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;
|
||||
|
@ -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()) {
|
||||
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -438,6 +456,8 @@ export class IssuesProcessor {
|
|||
issue,
|
||||
staleLabel,
|
||||
actor,
|
||||
labelsToAddWhenUnstale,
|
||||
labelsToRemoveWhenUnstale,
|
||||
closeMessage,
|
||||
closeLabel
|
||||
);
|
||||
|
@ -549,6 +569,8 @@ export class IssuesProcessor {
|
|||
issue: Issue,
|
||||
staleLabel: string,
|
||||
actor: string,
|
||||
labelsToAddWhenUnstale: Readonly<string>[],
|
||||
labelsToRemoveWhenUnstale: Readonly<string>[],
|
||||
closeMessage?: string,
|
||||
closeLabel?: string
|
||||
) {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -45,4 +45,6 @@ export interface IIssuesProcessorOptions {
|
|||
exemptAllIssueAssignees: boolean | undefined;
|
||||
exemptAllPrAssignees: boolean | undefined;
|
||||
enableStatistics: boolean;
|
||||
labelsToRemoveWhenUnstale: string;
|
||||
labelsToAddWhenUnstale: string;
|
||||
}
|
||||
|
|
|
@ -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 [
|
||||
|
|
Loading…
Reference in New Issue