7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
220 lines
10 KiB
YAML
220 lines
10 KiB
YAML
name: PR triage labels
|
|
|
|
# Auto-applies author-class labels at PR open and toggles lifecycle labels
|
|
# on review/synchronize events. Labels are defined in .github/labels.yml
|
|
# and synced by .github/workflows/labels-sync.yml.
|
|
#
|
|
# Also applies path-derived topic labels (documentation, tests, docker,
|
|
# github-actions, ...) that feed release-notes categorization. These were
|
|
# historically applied by the AI code reviewer, which became label-only
|
|
# opt-in (#4955), so triage now owns the mechanical subset. Topic labels
|
|
# are applied on opened and ready_for_review only, and are additive-only:
|
|
# a label a maintainer removed is not re-added on every push, and
|
|
# semantic labels (bugfix, feature, security, performance...) stay
|
|
# human/AI-applied.
|
|
#
|
|
# Uses pull_request (not pull_request_target) — fork PRs get a read-only
|
|
# token, so label calls return 403. The script catches 403 and continues
|
|
# so fork PRs don't show a failing check; maintainers can apply labels
|
|
# manually for fork PRs that need them. We accept the no-op behavior on
|
|
# forks vs the security cost of pull_request_target running with secrets
|
|
# on fork code.
|
|
#
|
|
# CODEOWNERS list below is mirrored in .github/CODEOWNERS global owners
|
|
# (line 6) — keep both in sync when the maintainer roster changes.
|
|
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, ready_for_review]
|
|
pull_request_review:
|
|
types: [submitted, dismissed]
|
|
|
|
# No concurrency group — intentionally omitted.
|
|
# Previous attempt (#3554, reverted #3599) used cancel-in-progress which
|
|
# killed in-progress PR runs before they produced useful results. Race
|
|
# between synchronize and pull_request_review can produce transient
|
|
# label flapping in rare cases; maintainers can correct manually.
|
|
|
|
permissions: {} # Minimal top-level for OSSF Scorecard Token-Permissions
|
|
|
|
jobs:
|
|
triage:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 5
|
|
permissions:
|
|
issues: write # PR labels live on the underlying issue resource
|
|
pull-requests: read # listFiles for path-derived topic labels
|
|
steps:
|
|
- name: Harden the runner (Audit all outbound calls)
|
|
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
|
|
with:
|
|
egress-policy: audit
|
|
|
|
- name: Apply triage labels
|
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
with:
|
|
script: |
|
|
// Keep CODEOWNERS list in sync with .github/CODEOWNERS line 6.
|
|
const CODEOWNERS = ['LearningCircuit', 'hashedviking', 'djpetti'];
|
|
// Known AI-bot accounts that don't carry the [bot] suffix.
|
|
const KNOWN_BOTS = ['moltenbot000', 'mseep-ai', 'Nexus-Digital-Automations'];
|
|
|
|
const pr = context.payload.pull_request;
|
|
const issueNumber = pr.number;
|
|
const author = pr.user.login;
|
|
const association = pr.author_association;
|
|
const isBot = author.endsWith('[bot]') || KNOWN_BOTS.includes(author);
|
|
const isInternal = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(association);
|
|
|
|
// 403 on label calls is the expected outcome for fork PRs —
|
|
// pull_request gives forks a read-only token by design. We
|
|
// log and continue so the workflow run stays green and a
|
|
// maintainer can apply labels manually instead of seeing
|
|
// a red check on every fork contribution.
|
|
const isReadOnlyTokenError = (err) => {
|
|
if (err.status !== 403) return false;
|
|
console.log(`Label call returned 403 (read-only token, likely a fork PR). Skipping.`);
|
|
return true;
|
|
};
|
|
|
|
const addLabels = async (labels) => {
|
|
if (!labels.length) return;
|
|
try {
|
|
await github.rest.issues.addLabels({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
labels,
|
|
});
|
|
} catch (err) {
|
|
if (!isReadOnlyTokenError(err)) throw err;
|
|
}
|
|
};
|
|
|
|
const removeLabel = async (name) => {
|
|
try {
|
|
await github.rest.issues.removeLabel({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
issue_number: issueNumber,
|
|
name,
|
|
});
|
|
} catch (err) {
|
|
if (err.status === 404) return; // label wasn't applied
|
|
if (!isReadOnlyTokenError(err)) throw err;
|
|
}
|
|
};
|
|
|
|
// Path-derived topic labels for release-notes categorization.
|
|
// Mechanical mappings only — a rule belongs here iff the label
|
|
// follows from *which files* changed, never from what the
|
|
// change means. First-match-per-rule over all changed files.
|
|
const TOPIC_RULES = [
|
|
[/^\.github\/workflows\//, 'github-actions'],
|
|
[/^(\.github\/|\.pre-commit-hooks\/|\.pre-commit-config\.yaml$|scripts\/ci\/)/, 'ci-cd'],
|
|
[/^(Dockerfile|docker-compose|cookiecutter-docker\/|unraid-templates\/)/, 'docker'],
|
|
[/^(docs\/|[^/]+\.md$)/, 'documentation'],
|
|
[/^tests\//, 'tests'],
|
|
[/^src\/local_deep_research\/database\//, 'database'],
|
|
[/^src\/local_deep_research\/(defaults|settings)\//, 'configuration'],
|
|
[/^(pyproject\.toml|pdm\.lock|package\.json|package-lock\.json)$/, 'dependencies'],
|
|
[/^src\/local_deep_research\/(advanced_search_system|web_search_engines)\//, 'research'],
|
|
[/^src\/.*\.py$/, 'python'],
|
|
[/^src\/.*\.(js|mjs)$/, 'javascript'],
|
|
[/^src\/.*\.css$/, 'css'],
|
|
];
|
|
|
|
const topicLabels = async () => {
|
|
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
pull_number: issueNumber,
|
|
per_page: 100,
|
|
});
|
|
const labels = new Set();
|
|
for (const f of files) {
|
|
for (const [pattern, label] of TOPIC_RULES) {
|
|
if (pattern.test(f.filename)) labels.add(label);
|
|
}
|
|
}
|
|
return [...labels];
|
|
};
|
|
|
|
const action = context.payload.action;
|
|
const eventName = context.eventName;
|
|
|
|
if (eventName === 'pull_request' && action === 'opened') {
|
|
const labels = [];
|
|
if (isBot) labels.push('bot');
|
|
if (!isInternal && !isBot) labels.push('external-contributor');
|
|
if (association === 'FIRST_TIME_CONTRIBUTOR') labels.push('first-time-contributor');
|
|
if (!isInternal) labels.push('needs-codeowner-review');
|
|
labels.push(...await topicLabels());
|
|
await addLabels(labels);
|
|
return;
|
|
}
|
|
|
|
if (eventName === 'pull_request' && (action === 'synchronize' || action === 'ready_for_review')) {
|
|
const current = pr.labels.map((l) => l.name);
|
|
if (current.includes('awaiting-author')) {
|
|
await removeLabel('awaiting-author');
|
|
await addLabels(['awaiting-codeowner']);
|
|
}
|
|
// Draft → ready is a deliberate "the diff is final" moment, so
|
|
// recompute topic labels once there. Deliberately NOT done on
|
|
// synchronize: that would re-add labels a maintainer removed,
|
|
// on every push.
|
|
if (action === 'ready_for_review') {
|
|
await addLabels(await topicLabels());
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (eventName === 'pull_request_review' && action === 'submitted') {
|
|
const review = context.payload.review;
|
|
const reviewer = review.user.login;
|
|
// Strict CODEOWNERS-only check. The hardcoded list above must
|
|
// mirror .github/CODEOWNERS line 6. We deliberately do NOT
|
|
// accept any OWNER/MEMBER/COLLABORATOR association as a
|
|
// codeowner: branch protection here may later adopt
|
|
// require_code_owner_reviews=true, at which point clearing
|
|
// needs-codeowner-review on a non-codeowner MEMBER review
|
|
// would be a security-relevant mislabel.
|
|
if (!CODEOWNERS.includes(reviewer)) return;
|
|
|
|
if (review.state === 'approved') {
|
|
await removeLabel('needs-codeowner-review');
|
|
await removeLabel('awaiting-codeowner');
|
|
// Also clear awaiting-author: a codeowner can resolve
|
|
// their own changes_requested review by approving without
|
|
// an intervening synchronize (e.g., the author convinced
|
|
// them via comments).
|
|
await removeLabel('awaiting-author');
|
|
} else if (review.state === 'changes_requested') {
|
|
await removeLabel('needs-codeowner-review');
|
|
await removeLabel('awaiting-codeowner');
|
|
await addLabels(['awaiting-author']);
|
|
}
|
|
// 'commented' → no-op.
|
|
return;
|
|
}
|
|
|
|
if (eventName === 'pull_request_review' && action === 'dismissed') {
|
|
// GitHub sets review.state to "dismissed" on this event — the
|
|
// original state is not preserved (github/docs#20216). Use the
|
|
// awaiting-author label as the discriminator: it's only set by
|
|
// a codeowner's changes_requested review, so its presence is
|
|
// proof the dismissal is the one we care about. Dismissals of
|
|
// approval/comment reviews are naturally no-ops because
|
|
// awaiting-author won't be present.
|
|
const review = context.payload.review;
|
|
if (!CODEOWNERS.includes(review.user.login)) return;
|
|
|
|
const current = pr.labels.map((l) => l.name);
|
|
if (current.includes('awaiting-author')) {
|
|
await removeLabel('awaiting-author');
|
|
await addLabels(['needs-codeowner-review']);
|
|
}
|
|
return;
|
|
}
|