mirror of
https://github.com/stan220/flathub.git
synced 2026-09-08 18:29:08 +00:00
277 lines
10 KiB
YAML
277 lines
10 KiB
YAML
#
|
|
# This workflow aims to reduce the number of PRs that fail basic
|
|
# requirements by automatically flagging and closing those
|
|
# that remain unaddressed for an extended period. Unlike the stale
|
|
# action, this works based on the label application date since the
|
|
# blocking labels are added manually. It ignores push events and updates
|
|
# intentionally as blocking labels are supposed to be manually
|
|
# removed on review once the PR passes basic requirements and neither
|
|
# push events nor updates signal that the PR passes the basic
|
|
# requirements again.
|
|
#
|
|
# This workflow processes open PRs that -
|
|
#
|
|
# 1. Aren't drafts or exempted through EXEMPT_LABEL label, AND
|
|
# 2. Have one or more of LABEL_BLOCKED, LABEL_CHECKS, or LABEL_STALE
|
|
#
|
|
# It tracks how long blocking labels have been applied, then -
|
|
#
|
|
# 1. If the PR no longer has any blocking labels (LABEL_BLOCKED or
|
|
# LABEL_CHECKS), the LABEL_STALE label is removed.
|
|
# 2. If a PR is blocked (LABEL_BLOCKED or LABEL_CHECKS labels) for at
|
|
# least COMMENT_AFTER_DAYS days, a warning comment is posted notifying
|
|
# that it will be closed in CLOSE_AFTER_COMMENT_DAYS more days and
|
|
# the LABEL_STALE label is applied.
|
|
# 3. If a PR is both stale (LABEL_STALE label) and still blocked
|
|
# (LABEL_BLOCKED or LABEL_CHECKS labels), it is closed after
|
|
# CLOSE_AFTER_COMMENT_DAYS days from when the LABEL_STALE label was
|
|
# applied.
|
|
#
|
|
|
|
name: "Close stale blocked PRs"
|
|
|
|
on:
|
|
schedule:
|
|
- cron: "0 0 * * 1"
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: "Dry run"
|
|
type: boolean
|
|
default: false
|
|
|
|
jobs:
|
|
close-blocked:
|
|
permissions:
|
|
pull-requests: write
|
|
issues: write
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Close stale blocked PRs
|
|
# 8.0.0
|
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
|
|
env:
|
|
DRY_RUN: ${{ inputs.dry_run || 'false' }}
|
|
COMMENT_AFTER_DAYS: "5"
|
|
CLOSE_AFTER_COMMENT_DAYS: "3"
|
|
LABEL_BLOCKED: "blocked"
|
|
LABEL_CHECKS: "pr-check-blocked"
|
|
EXEMPT_LABEL: "leave-open"
|
|
LABEL_STALE: "stale-blocked"
|
|
COMMENT: |-
|
|
<!-- stale-blocked-bot -->
|
|
This pull request has been marked as blocked for {days} days
|
|
and is not ready for inclusion. It will be closed in {grace_days}
|
|
days unless the issues are fixed and the {label} label is removed.
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
script: |
|
|
const LABEL_BLOCKED = process.env.LABEL_BLOCKED || "blocked";
|
|
const LABEL_CHECKS = process.env.LABEL_CHECKS || "pr-check-blocked";
|
|
const LABEL_STALE = process.env.LABEL_STALE || "stale-blocked";
|
|
const EXEMPT_LABEL = process.env.EXEMPT_LABEL || "leave-open";
|
|
|
|
const COMMENT_AFTER_DAYS = parseInt(process.env.COMMENT_AFTER_DAYS || "14", 10);
|
|
const CLOSE_AFTER_DAYS = parseInt(
|
|
process.env.CLOSE_AFTER_COMMENT_DAYS || "7",
|
|
10
|
|
);
|
|
|
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
|
const COMMENT_TEMPLATE = process.env.COMMENT?.trim();
|
|
const STALE_MARKER = "<!-- stale-blocked-bot -->";
|
|
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
|
|
if (!COMMENT_TEMPLATE) throw new Error("COMMENT env var is required");
|
|
|
|
const { owner, repo } = context.repo;
|
|
const now = Date.now();
|
|
|
|
function act(description, fn) {
|
|
if (DRY_RUN) {
|
|
console.log(`[DRY RUN] ${description}`);
|
|
return Promise.resolve();
|
|
}
|
|
console.log(description);
|
|
return fn();
|
|
}
|
|
|
|
function latestUnremovedLabelDate(events, label) {
|
|
let applied = null;
|
|
for (const e of events) {
|
|
if (e.event === "labeled" && e.label?.name === label)
|
|
applied = new Date(e.created_at).getTime();
|
|
if (e.event === "unlabeled" && e.label?.name === label) applied = null;
|
|
}
|
|
return applied;
|
|
}
|
|
|
|
function daysSince(ms) {
|
|
return (now - ms) / MS_PER_DAY;
|
|
}
|
|
|
|
try {
|
|
const rate = await github.rest.rateLimit.get();
|
|
console.log(`Rate limit remaining: ${rate.data.rate.remaining}`);
|
|
} catch (e) {
|
|
console.log("Could not fetch rate limit");
|
|
}
|
|
|
|
const prs = await github.paginate(github.rest.pulls.list, {
|
|
owner,
|
|
repo,
|
|
state: "open",
|
|
per_page: 100,
|
|
});
|
|
|
|
for (const pr of prs) {
|
|
try {
|
|
await processPR(pr);
|
|
} catch (err) {
|
|
console.error(
|
|
`Error processing PR #${pr.number} (${pr.html_url}): ${err.message}`
|
|
);
|
|
}
|
|
}
|
|
|
|
async function processPR(pr) {
|
|
const labels = new Set(pr.labels.map((l) => l.name));
|
|
const has = (label) => labels.has(label);
|
|
|
|
const isBlocked = has(LABEL_BLOCKED);
|
|
const isChecks = has(LABEL_CHECKS);
|
|
const isStale = has(LABEL_STALE);
|
|
|
|
if (!isBlocked && !isChecks && !isStale) return;
|
|
if (pr.draft) return;
|
|
if (has(EXEMPT_LABEL)) return;
|
|
|
|
let events;
|
|
try {
|
|
events = await github.paginate(github.rest.issues.listEventsForTimeline, {
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
});
|
|
} catch (e) {
|
|
console.warn(`Failed to fetch timeline for #${pr.number}: ${e.message}`);
|
|
return;
|
|
}
|
|
|
|
if (isStale && !isBlocked && !isChecks) {
|
|
const { data: fresh } = await github.rest.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
});
|
|
const freshLabels = new Set(fresh.labels.map((l) => l.name));
|
|
if (freshLabels.has(LABEL_BLOCKED) || freshLabels.has(LABEL_CHECKS)) return;
|
|
|
|
await act(
|
|
`Removing stale label from #${pr.number} (no longer blocked)`,
|
|
() =>
|
|
github.rest.issues.removeLabel({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
name: LABEL_STALE,
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (isStale && (isBlocked || isChecks)) {
|
|
const staleApplied = latestUnremovedLabelDate(events, LABEL_STALE);
|
|
if (staleApplied === null) return;
|
|
|
|
const diff = daysSince(staleApplied);
|
|
console.log(`[INFO] #${pr.number}: stale for ${Math.floor(diff)} days`);
|
|
if (diff < CLOSE_AFTER_DAYS) return;
|
|
|
|
const { data: fresh } = await github.rest.pulls.get({
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
});
|
|
if (!fresh.labels.some((l) => l.name === LABEL_STALE)) return;
|
|
|
|
await act(
|
|
`Closing PR #${pr.number} — stale and blocked for ${Math.floor(
|
|
diff
|
|
)} days`,
|
|
() =>
|
|
github.rest.pulls.update({
|
|
owner,
|
|
repo,
|
|
pull_number: pr.number,
|
|
state: "closed",
|
|
})
|
|
);
|
|
return;
|
|
}
|
|
|
|
const blockAppliedDates = [
|
|
isBlocked ? latestUnremovedLabelDate(events, LABEL_BLOCKED) : null,
|
|
isChecks ? latestUnremovedLabelDate(events, LABEL_CHECKS) : null,
|
|
].filter(Number.isFinite);
|
|
|
|
if (blockAppliedDates.length === 0) return;
|
|
|
|
const appliedDate = Math.min(...blockAppliedDates);
|
|
const diffDays = daysSince(appliedDate);
|
|
|
|
console.log(`[INFO] #${pr.number}: blocked for ${Math.floor(diffDays)} days`);
|
|
if (diffDays < COMMENT_AFTER_DAYS) return;
|
|
|
|
const labelText = [
|
|
isBlocked ? LABEL_BLOCKED : null,
|
|
isChecks ? LABEL_CHECKS : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" / ");
|
|
|
|
const body = COMMENT_TEMPLATE.replaceAll("{days}", Math.floor(diffDays))
|
|
.replaceAll("{grace_days}", CLOSE_AFTER_DAYS)
|
|
.replaceAll("{label}", labelText);
|
|
|
|
const comments = await github.paginate(github.rest.issues.listComments, {
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
});
|
|
const alreadyCommented = comments.some((c) => c.body?.includes(STALE_MARKER));
|
|
|
|
if (!alreadyCommented) {
|
|
await act(
|
|
`Commenting on PR #${pr.number} (blocked ${Math.floor(
|
|
diffDays
|
|
)} days) and marking stale`,
|
|
async () => {
|
|
await github.rest.issues.createComment({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
body,
|
|
});
|
|
await github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
labels: [LABEL_STALE],
|
|
});
|
|
}
|
|
);
|
|
} else if (!isStale) {
|
|
await act(
|
|
`Re-applying stale label to PR #${pr.number} (comment already present)`,
|
|
() =>
|
|
github.rest.issues.addLabels({
|
|
owner,
|
|
repo,
|
|
issue_number: pr.number,
|
|
labels: [LABEL_STALE],
|
|
})
|
|
);
|
|
}
|
|
}
|