<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #46790 Example live run off this branch: https://github.com/fleetdm/fleet/actions/runs/27005120262 Example stale issue with comment: https://github.com/fleetdm/fleet/issues/18421 - Added `@author` mention when marking issue as stale - Refactored so that both Fleetie and eng-initiated stale issue bots use the same core JS code - Run Fleetie-initiated workflow on a schedule <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automated stale-issue workflows for engineering-initiated and Fleetie issues with configurable dry-run, max-operations, manual triggers, and scheduled runs. * **Tests** * Added shared test helpers and expanded, tightened test suites covering staleness, closing, unstale, and error/boundary behaviors. * **Refactor** * Introduced a shared stale-issue engine used by thin, author/label-based wrappers for consistent behavior and messaging. * **Chores** * Updated workflow triggers, permissions, and CI test matrix to include the new core and wrappers. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
127 lines
4.3 KiB
JavaScript
127 lines
4.3 KiB
JavaScript
// Shared test harness for the stale-issue closer tests (`stale-eng-issues.test.js` and
|
|
// `stale-fleetie-issues.test.js`). Provides time helpers and mocks for the `github`, `context`,
|
|
// and `core` objects that `actions/github-script` passes to the scripts. Issue factories stay in
|
|
// the individual test files because each wrapper has a different notion of a default-eligible issue.
|
|
|
|
'use strict';
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
const daysAgoIso = (days) => new Date(Date.now() - days * DAY_MS).toISOString();
|
|
|
|
// `at` (ms-since-epoch) takes precedence over `daysAgo` so tests aligning an event to a specific
|
|
// updated_at moment can share the same instant rather than two independent Date.now() reads.
|
|
function makeStaleLabelEvent({ daysAgo = 100, at } = {}) {
|
|
const created_at = at != null ? new Date(at).toISOString() : daysAgoIso(daysAgo);
|
|
return { event: 'labeled', label: { name: 'stale' }, created_at };
|
|
}
|
|
|
|
function makeContext() {
|
|
return { repo: { owner: 'o', repo: 'r' } };
|
|
}
|
|
|
|
function makeCore() {
|
|
const infos = [];
|
|
const warnings = [];
|
|
const summaryCalls = [];
|
|
const summary = new Proxy(
|
|
{},
|
|
{
|
|
get(_target, prop) {
|
|
if (prop === 'write') {
|
|
return async () => {
|
|
summaryCalls.push({ method: 'write' });
|
|
};
|
|
}
|
|
return (...args) => {
|
|
summaryCalls.push({ method: String(prop), args });
|
|
return summary;
|
|
};
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
info: (msg) => infos.push(msg),
|
|
warning: (msg) => warnings.push(msg),
|
|
summary,
|
|
_captured: { infos, warnings, summaryCalls },
|
|
};
|
|
}
|
|
|
|
// `issuesByPage`: array of pages (each page is an array of issues) returned by paginate.iterator.
|
|
// `eventsByIssue`: map from issue.number -> array of event objects returned by paginate(listEvents).
|
|
// `failOn`: optional fault-injection { createComment, addLabels, update, removeLabel, listEvents }
|
|
// values can be 'always', an integer (fail until Nth call), or { status: 404 }.
|
|
function makeGithub({ issuesByPage = [], eventsByIssue = {}, failOn = {} } = {}) {
|
|
const createCommentCalls = [];
|
|
const addLabelsCalls = [];
|
|
const removeLabelCalls = [];
|
|
const updateCalls = [];
|
|
const listEventsCalls = [];
|
|
|
|
const counters = {};
|
|
const shouldFail = (op) => {
|
|
const cfg = failOn[op];
|
|
if (!cfg) return null;
|
|
counters[op] = (counters[op] || 0) + 1;
|
|
if (cfg === 'always') return new Error(`${op} simulated failure`);
|
|
if (typeof cfg === 'number' && counters[op] <= cfg) return new Error(`${op} simulated failure`);
|
|
if (typeof cfg === 'object' && cfg.status && counters[op] === 1) {
|
|
const err = new Error(`${op} simulated failure status ${cfg.status}`);
|
|
err.status = cfg.status;
|
|
return err;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const paginate = async (endpoint, params) => {
|
|
if (endpoint === 'listEvents-sentinel') {
|
|
listEventsCalls.push(params);
|
|
const err = shouldFail('listEvents');
|
|
if (err) throw err;
|
|
return eventsByIssue[params.issue_number] || [];
|
|
}
|
|
if (endpoint === 'listForRepo-sentinel') {
|
|
return issuesByPage.flat();
|
|
}
|
|
return [];
|
|
};
|
|
paginate.iterator = async function* iterator(endpoint) {
|
|
if (endpoint === 'listForRepo-sentinel') {
|
|
for (const page of issuesByPage) yield { data: page };
|
|
}
|
|
};
|
|
|
|
return {
|
|
paginate,
|
|
rest: {
|
|
issues: {
|
|
listForRepo: 'listForRepo-sentinel',
|
|
listEvents: 'listEvents-sentinel',
|
|
createComment: async (params) => {
|
|
const err = shouldFail('createComment');
|
|
if (err) throw err;
|
|
createCommentCalls.push(params);
|
|
},
|
|
addLabels: async (params) => {
|
|
const err = shouldFail('addLabels');
|
|
if (err) throw err;
|
|
addLabelsCalls.push(params);
|
|
},
|
|
removeLabel: async (params) => {
|
|
const err = shouldFail('removeLabel');
|
|
if (err) throw err;
|
|
removeLabelCalls.push(params);
|
|
},
|
|
update: async (params) => {
|
|
const err = shouldFail('update');
|
|
if (err) throw err;
|
|
updateCalls.push(params);
|
|
},
|
|
},
|
|
},
|
|
_captured: { createCommentCalls, addLabelsCalls, removeLabelCalls, updateCalls, listEventsCalls },
|
|
};
|
|
}
|
|
|
|
module.exports = { DAY_MS, daysAgoIso, makeStaleLabelEvent, makeContext, makeCore, makeGithub };
|