Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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;
}