Compare commits

..

5 Commits

Author SHA1 Message Date
dependabot[bot]
5923092d78 build(deps-dev): bump handlebars from 4.7.8 to 4.7.9
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-27 19:56:12 +00:00
shamoon
db5d06a4c8 Enhancement: ignore stale labeling events (#1311)
* Ignore updates if only stale label changed after marking

* Initial tests

* test hasOnlyStaleLabelUpdateSince itself

* Actually we should only ignore label events, not unlabel

* Add logger

* Fix test

* Remove trailing whitespace

* Add test for non-label event

* Add boundary test

* Add error handling to hasOnlyStaleLabelAddedSince

* Add comment noting intentional event before boundary

* Remove unneeded optional chaining

* Limit pagination to max 3 calls / 300 events

* Rename method

* Handle invalid timestamps

* Fallback when limit reached

* Oh wow, just realized we already got events...

* Refactor

* Lint

* Build

* Remove events "cache"

* Update index.js
2026-03-18 11:25:18 -05:00
dependabot[bot]
b5d41d4e1d build(deps-dev): bump lodash from 4.17.21 to 4.17.23 (#1313)
* build(deps-dev): bump lodash from 4.17.21 to 4.17.23

Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.17.23.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.17.23)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.17.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

* build(deps): update undici to 6.23.0, fast-xml-parser to 5.3.4, and @actions/http-client to 3.0.2

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: chiranjib-swain <chiranjib-swain@github.com>
2026-02-09 12:47:17 -06:00
Chiranjib Swain
dcd2b9469d Fix punycode and url.parse Deprecation Warnings (#1312)
* chore: update dependencies and add new license files for octokit packages

* chore: update license files and remove obsolete dependencies
2026-02-02 11:24:18 -06:00
dependabot[bot]
d6f8a33132 build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 (#1304)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-21 21:47:42 -06:00
20 changed files with 56290 additions and 1550 deletions

View File

@@ -10,7 +10,6 @@ allowed:
- cc0-1.0
- unlicense
- 0bsd
- blueoak-1.0.0
reviewed:
npm:

View File

@@ -1,6 +1,6 @@
---
name: "@actions/http-client"
version: 2.2.3
version: 2.2.0
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client

View File

@@ -1,6 +1,6 @@
---
name: "@actions/http-client"
version: 3.0.1
version: 3.0.2
type: npm
summary: Actions Http Client
homepage: https://github.com/actions/toolkit/tree/main/packages/http-client

View File

@@ -1,6 +1,6 @@
---
name: "@fastify/busboy"
version: 2.1.1
version: 2.1.0
type: npm
summary: A streaming parser for HTML form data for node.js
homepage:

View File

@@ -1,6 +1,6 @@
---
name: "@octokit/plugin-retry"
version: 5.0.5
version: 4.1.6
type: npm
summary: Automatic retry plugin for octokit
homepage:

View File

@@ -1,6 +1,6 @@
---
name: "@octokit/types"
version: 10.0.0
version: 9.3.2
type: npm
summary: Shared TypeScript definitions for Octokit projects
homepage:

View File

@@ -1,6 +1,6 @@
---
name: debug
version: 4.4.3
version: 4.3.4
type: npm
summary: Lightweight debugging utility for Node.js and the browser
homepage:

View File

@@ -1,6 +1,6 @@
---
name: fast-xml-parser
version: 5.3.3
version: 5.3.4
type: npm
summary: Validate XML, Parse XML, Build XML without C/C++ based libraries
homepage:

View File

@@ -1,6 +1,6 @@
---
name: ms
version: 2.1.3
version: 2.1.2
type: npm
summary: Tiny millisecond conversion utility
homepage:
@@ -10,7 +10,7 @@ licenses:
text: |
The MIT License (MIT)
Copyright (c) 2020 Vercel, Inc.
Copyright (c) 2016 Zeit, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,6 +1,6 @@
---
name: semver
version: 7.7.3
version: 7.6.3
type: npm
summary: The semantic version parser used by npm.
homepage:

View File

@@ -1,16 +1,16 @@
---
name: "@octokit/request-error"
version: 4.0.2
name: undici
version: 6.23.0
type: npm
summary: Error class for Octokit request errors
homepage:
summary: An HTTP/1.1 client, written from scratch for Node.js
homepage: https://undici.nodejs.org
license: mit
licenses:
- sources: LICENSE
text: |
The MIT License
MIT License
Copyright (c) 2019 Octokit contributors
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -19,16 +19,16 @@ licenses:
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
- sources: README.md
text: "[MIT](LICENSE)"
text: MIT
notices: []

View File

@@ -4,6 +4,7 @@ import {IComment} from '../../src/interfaces/comment';
import {IIssuesProcessorOptions} from '../../src/interfaces/issues-processor-options';
import {IPullRequest} from '../../src/interfaces/pull-request';
import {IState} from '../../src/interfaces/state/state';
import {IIssueEvent} from '../../src/interfaces/issue-event';
export class IssuesProcessorMock extends IssuesProcessor {
constructor(
@@ -17,7 +18,15 @@ export class IssuesProcessorMock extends IssuesProcessor {
getLabelCreationDate?: (
issue: Issue,
label: string
) => Promise<string | undefined>,
) =>
| Promise<string | undefined>
| Promise<{creationDate?: string; events: IIssueEvent[]}>,
hasOnlyStaleLabelingEventsSince?: (
issue: Issue,
sinceDate: string,
staleLabel: string,
events: IIssueEvent[]
) => Promise<boolean>,
getPullRequest?: (issue: Issue) => Promise<IPullRequest | undefined | void>
) {
super(options, state);
@@ -31,7 +40,21 @@ export class IssuesProcessorMock extends IssuesProcessor {
}
if (getLabelCreationDate) {
this.getLabelCreationDate = getLabelCreationDate;
this.getLabelCreationDate = async (
issue: Issue,
label: string
): Promise<{creationDate?: string; events: IIssueEvent[]}> => {
const result = await getLabelCreationDate(issue, label);
if (typeof result === 'string' || typeof result === 'undefined') {
return {creationDate: result, events: []};
}
return result;
};
}
if (hasOnlyStaleLabelingEventsSince) {
this.hasOnlyStaleLabelingEventsSince = hasOnlyStaleLabelingEventsSince;
}
if (getPullRequest) {

View File

@@ -129,6 +129,7 @@ class IssuesProcessorBuilder {
async p => (p === 1 ? this._issues : []),
async () => [],
async () => new Date().toDateString(),
undefined,
async (): Promise<IPullRequest> => {
return Promise.resolve({
number: 0,

View File

@@ -0,0 +1,288 @@
import {Issue} from '../src/classes/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';
import {alwaysFalseStateMock} from './classes/state-mock';
import {IState} from '../src/interfaces/state/state';
import {IIssueEvent} from '../src/interfaces/issue-event';
import {IssuesProcessor} from '../src/classes/issues-processor';
describe('remove-stale-when-updated with stale label events', (): void => {
const markedStaleOn = '2025-01-01T00:00:00Z';
const updatedAt = '2025-01-01T00:01:00Z';
let options: IIssuesProcessorOptions;
beforeEach((): void => {
options = {
...DefaultProcessorOptions,
removeStaleWhenUpdated: true
};
});
const buildIssue = (): Issue =>
generateIssue(
options,
1,
'dummy-title',
updatedAt,
markedStaleOn,
false,
false,
['Stale']
);
const buildEvents = (): IIssueEvent[] => [
{
event: 'labeled',
created_at: markedStaleOn,
label: {name: 'Stale'}
}
];
test('does not remove stale label when only stale label events occurred', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const processor = new IssuesProcessorMock(
options,
alwaysFalseStateMock,
async p => (p === 1 ? [issue] : []),
async () => [],
async () => ({creationDate: markedStaleOn, events: buildEvents()}),
async () => true
);
await processor.processIssues();
expect(processor.removedLabelIssues).toHaveLength(0);
});
test('removes stale label when updates are not just stale label events', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const processor = new IssuesProcessorMock(
options,
alwaysFalseStateMock,
async p => (p === 1 ? [issue] : []),
async () => [],
async () => ({creationDate: markedStaleOn, events: buildEvents()}),
async () => false
);
await processor.processIssues();
expect(processor.removedLabelIssues).toHaveLength(1);
});
});
class TestIssuesProcessor extends IssuesProcessor {
constructor(
options: IIssuesProcessorOptions,
state: IState,
events: IIssueEvent[]
) {
super(options, state);
const client = {
rest: {
issues: {
listEvents: {
endpoint: {
merge: () => ({})
}
}
}
},
paginate: {
iterator: async function* () {
yield {data: events};
}
}
};
(this as any).client = client;
}
async callhasOnlyStaleLabelingEventsSince(
issue: Issue,
sinceDate: string,
staleLabel: string,
events: IIssueEvent[]
): Promise<boolean> {
return this.hasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
}
}
describe('hasOnlyStaleLabelingEventsSince', (): void => {
const staleLabel = 'Stale';
const sinceDate = '2025-01-01T00:00:00Z';
const originalRepo = process.env.GITHUB_REPOSITORY;
let options: IIssuesProcessorOptions;
beforeEach((): void => {
process.env.GITHUB_REPOSITORY = 'owner/repo';
options = {
...DefaultProcessorOptions,
staleIssueLabel: staleLabel,
removeStaleWhenUpdated: true
};
});
afterEach((): void => {
if (originalRepo === undefined) {
delete process.env.GITHUB_REPOSITORY;
} else {
process.env.GITHUB_REPOSITORY = originalRepo;
}
});
const buildIssue = (): Issue =>
generateIssue(
options,
1,
'dummy-title',
'2025-01-01T00:02:00Z',
sinceDate,
false,
false,
[staleLabel]
);
test('returns true when only stale label events exist after the since date', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const events: IIssueEvent[] = [
// Event before the sinceDate should be ignored.
{
event: 'labeled',
created_at: '2024-12-31T23:59:00Z',
label: {name: staleLabel}
},
{
event: 'labeled',
created_at: '2025-01-01T00:00:10Z',
label: {name: staleLabel}
}
];
const processor = new TestIssuesProcessor(
options,
alwaysFalseStateMock,
events
);
const result = await processor.callhasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
expect(result).toBe(true);
});
test('returns false when a non-stale label event exists after the since date', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const events: IIssueEvent[] = [
{
event: 'labeled',
created_at: '2025-01-01T00:00:10Z',
label: {name: 'other-label'}
}
];
const processor = new TestIssuesProcessor(
options,
alwaysFalseStateMock,
events
);
const result = await processor.callhasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
expect(result).toBe(false);
});
test('returns false when stale label is removed after the since date', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const events: IIssueEvent[] = [
{
event: 'unlabeled',
created_at: '2025-01-01T00:00:10Z',
label: {name: staleLabel}
}
];
const processor = new TestIssuesProcessor(
options,
alwaysFalseStateMock,
events
);
const result = await processor.callhasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
expect(result).toBe(false);
});
test('returns false when a non-label event exists after the since date', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const events: IIssueEvent[] = [
{
event: 'commented',
created_at: '2025-01-01T00:00:10Z',
label: {name: staleLabel}
}
];
const processor = new TestIssuesProcessor(
options,
alwaysFalseStateMock,
events
);
const result = await processor.callhasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
expect(result).toBe(false);
});
test('includes events that occur exactly at the since date boundary', async (): Promise<void> => {
expect.assertions(1);
const issue = buildIssue();
const events: IIssueEvent[] = [
{
event: 'labeled',
created_at: sinceDate,
label: {name: staleLabel}
}
];
const processor = new TestIssuesProcessor(
options,
alwaysFalseStateMock,
events
);
const result = await processor.callhasOnlyStaleLabelingEventsSince(
issue,
sinceDate,
staleLabel,
events
);
expect(result).toBe(true);
});
});

55412
dist/index.js vendored

File diff suppressed because one or more lines are too long

1987
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,7 +41,7 @@
"@actions/core": "^1.11.1",
"@actions/github": "^7.0.0",
"@octokit/core": "^5.2.0",
"@octokit/plugin-retry": "^5.0.5",
"@octokit/plugin-retry": "^4.1.1",
"lodash.deburr": "^4.1.0",
"semver": "^7.5.4"
},
@@ -61,7 +61,7 @@
"jest": "^29.6.2",
"jest-circus": "^29.5.0",
"jest-silent-reporter": "^0.5.0",
"js-yaml": "^4.1.0",
"js-yaml": "^4.1.1",
"prettier": "^2.8.7",
"standard-version": "^9.3.1",
"terminal-link": "^2.1.1",

View File

@@ -608,7 +608,7 @@ export class IssuesProcessor {
async getLabelCreationDate(
issue: Issue,
label: string
): Promise<string | undefined> {
): Promise<{creationDate?: string; events: IIssueEvent[]}> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(`Checking for label on this $$type`);
@@ -623,6 +623,7 @@ export class IssuesProcessor {
});
const events: IIssueEvent[] = await this.client.paginate(options);
const reversedEvents = events.reverse();
const staleLabeledEvent = reversedEvents.find(
@@ -633,10 +634,51 @@ export class IssuesProcessor {
if (!staleLabeledEvent) {
// Must be old rather than labeled
return undefined;
return {creationDate: undefined, events};
}
return staleLabeledEvent.created_at;
return {creationDate: staleLabeledEvent.created_at, events};
}
protected async hasOnlyStaleLabelingEventsSince(
issue: Issue,
sinceDate: string,
staleLabel: string,
events: IIssueEvent[]
): Promise<boolean> {
const issueLogger: IssueLogger = new IssueLogger(issue);
issueLogger.info(
`Checking if only stale label added events on $$type since: ${LoggerService.cyan(
sinceDate
)}`
);
if (!sinceDate) {
return false;
}
const sinceTimestamp = new Date(sinceDate).getTime();
if (Number.isNaN(sinceTimestamp)) {
return false;
}
const relevantEvents = events.filter(event => {
const eventTimestamp = new Date(event.created_at).getTime();
return !Number.isNaN(eventTimestamp) && eventTimestamp >= sinceTimestamp;
});
if (relevantEvents.length === 0) {
return false;
}
return relevantEvents.every(event => {
if (event.event !== 'labeled') {
return false;
}
return cleanLabel(event.label.name) === cleanLabel(staleLabel);
});
}
async getPullRequest(issue: Issue): Promise<IPullRequest | undefined | void> {
@@ -691,8 +733,11 @@ export class IssuesProcessor {
closeLabel?: string
) {
const issueLogger: IssueLogger = new IssueLogger(issue);
const markedStaleOn: string =
(await this.getLabelCreationDate(issue, staleLabel)) || issue.updated_at;
const {creationDate, events} = await this.getLabelCreationDate(
issue,
staleLabel
);
const markedStaleOn: string = creationDate || issue.updated_at;
issueLogger.info(
`$$type marked stale on: ${LoggerService.cyan(markedStaleOn)}`
);
@@ -744,12 +789,33 @@ export class IssuesProcessor {
// The issue.updated_at and markedStaleOn are not always exactly in sync (they can be off by a second or 2)
// isDateMoreRecentThan makes sure they are not the same date within a certain tolerance (15 seconds in this case)
const issueHasUpdateSinceStale = isDateMoreRecentThan(
let issueHasUpdateSinceStale = isDateMoreRecentThan(
new Date(issue.updated_at),
new Date(markedStaleOn),
15
);
// Check if the only update was the stale label being added
if (
issueHasUpdateSinceStale &&
shouldRemoveStaleWhenUpdated &&
!issue.markedStaleThisRun
) {
const onlyStaleLabelAdded = await this.hasOnlyStaleLabelingEventsSince(
issue,
markedStaleOn,
staleLabel,
events
);
if (onlyStaleLabelAdded) {
issueHasUpdateSinceStale = false;
issueLogger.info(
`Ignoring $$type update since only the stale label was added`
);
}
}
issueLogger.info(
`$$type has been updated since it was marked stale: ${LoggerService.cyan(
issueHasUpdateSinceStale