Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Format maintained frontend sources with Vite+.
ffba2136dbc88ef6a570398184c355eaeed5424e
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
vite-plus:
patterns:
- "vite-plus"
- "vite"
- "@voidzero-dev/vite-plus-core"
- "vitest"
- "@vitest/*"
- package-ecosystem: "npm"
directory: "/skeletons/hypervel"
schedule:
interval: "weekly"
cooldown:
default-days: 7
versioning-strategy: "increase-if-necessary"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
Expand Down
163 changes: 113 additions & 50 deletions .github/scripts/submission-cleanup.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
async function permissionFor(login) {
if (!permissions.has(login)) {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
...repository, username: login,
...repository,
username: login,
});
permissions.set(login, data);
}
Expand All @@ -23,22 +24,27 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
if (!user || user.type === 'Bot') return false;
const permission = await permissionFor(user.login);

return ['admin', 'write', 'maintain'].includes(permission.permission)
|| (allowTriage && permission.role_name === 'triage');
return (
['admin', 'write', 'maintain'].includes(permission.permission) ||
(allowTriage && permission.role_name === 'triage')
);
}

function isExempt(item) {
return item.labels.some(label => label.name === EXEMPT_LABEL);
return item.labels.some((label) => label.name === EXEMPT_LABEL);
}

async function commentsFor(number) {
return github.paginate(github.rest.issues.listComments, {
...repository, issue_number: number, per_page: 100,
...repository,
issue_number: number,
per_page: 100,
});
}

async function authorEditAndDraftStart(item) {
const { node } = await github.graphql(`
const { node } = await github.graphql(
`
query($id: ID!) {
node(id: $id) {
... on Issue { lastEditedAt editor { login } }
Expand All @@ -54,7 +60,9 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
}
}
}
`, { id: item.node_id });
`,
{ id: item.node_id },
);

return {
editedAt: node.editor?.login === item.user.login ? node.lastEditedAt : null,
Expand All @@ -63,14 +71,16 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
}

async function recordDraftActivity(pullRequest, comments) {
const previous = comments.find(comment =>
comment.user?.login === 'github-actions[bot]' && ACTIVITY_MARKER.test(comment.body));
const previous = comments.find(
(comment) => comment.user?.login === 'github-actions[bot]' && ACTIVITY_MARKER.test(comment.body),
);
const activity = { head: pullRequest.head.sha, at: now.toISOString() };
const body = `External draft pull requests are closed after seven days without contributor activity. `
+ `Pushes, edits, and author replies count; bot comments do not. `
+ `Maintainers can exempt directed work with the \`${EXEMPT_LABEL}\` label. `
+ `See the [contribution guide](${GUIDE}#pull-request-requirements).\n\n`
+ `<!-- hypervel-draft-activity ${JSON.stringify(activity)} -->`;
const body =
`External draft pull requests are closed after seven days without contributor activity. ` +
`Pushes, edits, and author replies count; bot comments do not. ` +
`Maintainers can exempt directed work with the \`${EXEMPT_LABEL}\` label. ` +
`See the [contribution guide](${GUIDE}#pull-request-requirements).\n\n` +
`<!-- hypervel-draft-activity ${JSON.stringify(activity)} -->`;

core.info(`Record draft activity for #${pullRequest.number}${dryRun ? ' (dry run)' : ''}`);
if (dryRun) return;
Expand All @@ -82,13 +92,20 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
}

async function checkDraft(pullRequest, recordActivity = false) {
if (pullRequest.state !== 'open' || !pullRequest.draft || isExempt(pullRequest)
|| !pullRequest.user || pullRequest.user.type === 'Bot'
|| await isMaintainer(pullRequest.user)) return;
if (
pullRequest.state !== 'open' ||
!pullRequest.draft ||
isExempt(pullRequest) ||
!pullRequest.user ||
pullRequest.user.type === 'Bot' ||
(await isMaintainer(pullRequest.user))
)
return;

const comments = await commentsFor(pullRequest.number);
const notice = comments.find(comment =>
comment.user?.login === 'github-actions[bot]' && ACTIVITY_MARKER.test(comment.body));
const notice = comments.find(
(comment) => comment.user?.login === 'github-actions[bot]' && ACTIVITY_MARKER.test(comment.body),
);
const recorded = notice ? JSON.parse(notice.body.match(ACTIVITY_MARKER)[1]) : null;

// Commit dates do not record push times. Remember observed head changes,
Expand All @@ -99,16 +116,23 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
}

const edits = await authorEditAndDraftStart(pullRequest);
let lastActivity = Math.max(Date.parse(recorded.at), Date.parse(pullRequest.created_at),
let lastActivity = Math.max(
Date.parse(recorded.at),
Date.parse(pullRequest.created_at),
edits.editedAt ? Date.parse(edits.editedAt) : 0,
edits.draftStartedAt ? Date.parse(edits.draftStartedAt) : 0);
edits.draftStartedAt ? Date.parse(edits.draftStartedAt) : 0,
);
if (now.getTime() - lastActivity < 7 * DAY) return;

const reviews = await github.paginate(github.rest.pulls.listReviews, {
...repository, pull_number: pullRequest.number, per_page: 100,
...repository,
pull_number: pullRequest.number,
per_page: 100,
});
const reviewComments = await github.paginate(github.rest.pulls.listReviewComments, {
...repository, pull_number: pullRequest.number, per_page: 100,
...repository,
pull_number: pullRequest.number,
per_page: 100,
});

for (const response of [...comments, ...reviews, ...reviewComments]) {
Expand All @@ -120,77 +144,116 @@ module.exports = async function cleanup({ github, context, core, now = new Date(
if (now.getTime() - lastActivity < 7 * DAY) return;

const { data: current } = await github.rest.pulls.get({
...repository, pull_number: pullRequest.number,
...repository,
pull_number: pullRequest.number,
});
if (current.state !== 'open' || !current.draft || isExempt(current)
|| current.head.sha !== pullRequest.head.sha || current.updated_at !== pullRequest.updated_at) return;
if (
current.state !== 'open' ||
!current.draft ||
isExempt(current) ||
current.head.sha !== pullRequest.head.sha ||
current.updated_at !== pullRequest.updated_at
)
return;

core.info(`Close inactive external draft #${current.number}${dryRun ? ' (dry run)' : ''}`);
if (dryRun) return;
await github.rest.issues.createComment({
...repository, issue_number: current.number,
body: `Closing this external draft after seven days without contributor activity. `
+ `Bot comments do not extend the deadline. You can reopen it when you resume the work. `
+ `See the [contribution guide](${GUIDE}#pull-request-requirements).`,
...repository,
issue_number: current.number,
body:
`Closing this external draft after seven days without contributor activity. ` +
`Bot comments do not extend the deadline. You can reopen it when you resume the work. ` +
`See the [contribution guide](${GUIDE}#pull-request-requirements).`,
});
await github.rest.pulls.update({ ...repository, pull_number: current.number, state: 'closed' });
}

async function checkInformationRequest(issue) {
if (issue.pull_request || isExempt(issue) || !issue.user || issue.user.type === 'Bot') return;
const events = await github.paginate(github.rest.issues.listEventsForTimeline, {
...repository, issue_number: issue.number, per_page: 100,
...repository,
issue_number: issue.number,
per_page: 100,
});
const request = events.filter(event => event.event === 'labeled' && event.label.name === INFO_LABEL)
const request = events
.filter((event) => event.event === 'labeled' && event.label.name === INFO_LABEL)
.sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at))[0];
if (!request || now.getTime() - Date.parse(request.created_at) < 14 * DAY
|| !await isMaintainer(request.actor, true)) return;
if (
!request ||
now.getTime() - Date.parse(request.created_at) < 14 * DAY ||
!(await isMaintainer(request.actor, true))
)
return;

const requestedAt = Date.parse(request.created_at);
const comments = await commentsFor(issue.number);
const authorResponded = comments.some(comment => comment.user?.login === issue.user.login
&& comment.user.type !== 'Bot'
&& Math.max(Date.parse(comment.created_at), Date.parse(comment.updated_at)) >= requestedAt);
const authorReopened = events.some(event => event.event === 'reopened'
&& event.actor?.login === issue.user.login && Date.parse(event.created_at) >= requestedAt);
const authorResponded = comments.some(
(comment) =>
comment.user?.login === issue.user.login &&
comment.user.type !== 'Bot' &&
Math.max(Date.parse(comment.created_at), Date.parse(comment.updated_at)) >= requestedAt,
);
const authorReopened = events.some(
(event) =>
event.event === 'reopened' &&
event.actor?.login === issue.user.login &&
Date.parse(event.created_at) >= requestedAt,
);
const edits = await authorEditAndDraftStart(issue);
if (authorResponded || authorReopened || (edits.editedAt && Date.parse(edits.editedAt) >= requestedAt)) return;

const { data: current } = await github.rest.issues.get({ ...repository, issue_number: issue.number });
if (current.state !== 'open' || isExempt(current)
|| !current.labels.some(label => label.name === INFO_LABEL) || current.updated_at !== issue.updated_at) return;
if (
current.state !== 'open' ||
isExempt(current) ||
!current.labels.some((label) => label.name === INFO_LABEL) ||
current.updated_at !== issue.updated_at
)
return;

core.info(`Close unanswered information request #${current.number}${dryRun ? ' (dry run)' : ''}`);
if (dryRun) return;
await github.rest.issues.createComment({
...repository, issue_number: current.number,
body: `Closing this issue because the reporter has not responded within 14 days of the maintainer's `
+ `request for information. This does not mean the reported problem is fixed. `
+ `Please provide the requested information here so the issue can be reconsidered. `
+ `See the [contribution guide](${GUIDE}#bug-reports).`,
...repository,
issue_number: current.number,
body:
`Closing this issue because the reporter has not responded within 14 days of the maintainer's ` +
`request for information. This does not mean the reported problem is fixed. ` +
`Please provide the requested information here so the issue can be reconsidered. ` +
`See the [contribution guide](${GUIDE}#bug-reports).`,
});
await github.rest.issues.update({
...repository, issue_number: current.number, state: 'closed', state_reason: 'not_planned',
...repository,
issue_number: current.number,
state: 'closed',
state_reason: 'not_planned',
});
}

if (context.eventName === 'pull_request_target') {
if (context.payload.sender?.type === 'Bot') return;
const { data: pullRequest } = await github.rest.pulls.get({
...repository, pull_number: context.payload.pull_request.number,
...repository,
pull_number: context.payload.pull_request.number,
});
await checkDraft(pullRequest, true);
return;
}

for await (const { data: pullRequests } of github.paginate.iterator(github.rest.pulls.list, {
...repository, state: 'open', per_page: 100,
...repository,
state: 'open',
per_page: 100,
})) {
for (const pullRequest of pullRequests) await checkDraft(pullRequest);
}

for await (const { data: issues } of github.paginate.iterator(github.rest.issues.listForRepo, {
...repository, state: 'open', labels: INFO_LABEL, per_page: 100,
...repository,
state: 'open',
labels: INFO_LABEL,
per_page: 100,
})) {
for (const issue of issues) await checkInformationRequest(issue);
}
Expand Down
Loading
Loading