Skip to content

feat(cache)!: sort query parameters in HTTP cache keys by default #47

feat(cache)!: sort query parameters in HTTP cache keys by default

feat(cache)!: sort query parameters in HTTP cache keys by default #47

Workflow file for this run

name: PR gate

Check warning on line 1 in .github/workflows/pr-gate.yml

View workflow run for this annotation

GitHub Actions / PR gate

Workflow execution policy warning (evaluate mode)

On November 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# Filters pull requests from outside contributors before a maintainer reads
# them. Many arrive from accounts that open hundreds of unrequested PRs a
# month; the rules below are the ones in docs/CONTRIBUTING.md, so a PR that
# follows the guide passes and one that ignores it is closed with a comment.
#
# pull_request_target runs in the context of the base branch, with a token
# that can comment and close. That is safe only because this workflow never
# checks out or runs code from the pull request: it reads the PR through the
# API and nothing else. Keep it that way.
on:
pull_request_target: # zizmor: ignore[dangerous-triggers] -- no PR code is checked out or run
types: [opened, reopened, edited, synchronize, labeled]
permissions: {}
# Serialize runs per PR, so two quick events (opened + edited) do not both
# find no earlier comment and post two.
concurrency:
group: pr-gate-${{ github.event.pull_request.number }}
cancel-in-progress: false
jobs:
gate:
# Maintainers, collaborators and bots (Renovate, Dependabot) are exempt.
if: >-
github.event.pull_request.user.type != 'Bot' &&
!contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.pull_request.author_association)
runs-on: ubuntu-latest
permissions:
issues: read
pull-requests: write
steps:
- name: Check the contribution rules
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const pr = context.payload.pull_request;
const { owner, repo } = context.repo;
const author = pr.user.login.toLowerCase();
const marker = '<!-- pr-gate -->';
const guide = `https://github.com/${owner}/${repo}/blob/master/docs/CONTRIBUTING.md`;
if (pr.state !== 'open') return;
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number, per_page: 100,
});
const previous = comments.find(
(c) => c.user.login === 'github-actions[bot]' && c.body.startsWith(marker),
);
const upsert = async (body) => {
if (previous) {
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
};
if (pr.labels.some((label) => label.name === 'skip-pr-gate')) {
core.info('skip-pr-gate label present; not checking.');
if (previous) await upsert(`${marker}\nA maintainer waived this check.`);
return;
}
// 1. The PR must close an issue that a maintainer assigned to its author.
const escaped = `${owner}/${repo}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const closing = new RegExp(
`\\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\\s+(?:#|${escaped}#|https?://github\\.com/${escaped}/issues/)(\\d+)\\b`,
'gi',
);
// Capped: each reference costs an API call from the repository's shared quota.
const linked = [...new Set([...(pr.body ?? '').matchAll(closing)].map((m) => Number(m[1])))].slice(0, 10);
const blocking = [];
if (linked.length === 0) {
blocking.push('The description does not close an issue (`Fixes #123`).');
} else {
let assigned = false;
for (const number of linked) {
if (assigned) break;
try {
const { data: issue } = await github.rest.issues.get({ owner, repo, issue_number: number });
if (!issue.pull_request && issue.assignees.some((a) => a.login.toLowerCase() === author)) {
assigned = true;
}
} catch (error) {
if (error.status !== 404 && error.status !== 410) throw error;
}
}
if (!assigned) {
blocking.push(
`You are not assigned to ${linked.map((n) => `#${n}`).join(', ')}. ` +
'Ask on the issue first; a maintainer assigns it to you before you open a PR.',
);
}
}
// 2. A change to the package needs tests and a changelog fragment.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});
const changed = files.filter((f) => f.status !== 'removed').map((f) => f.filename);
const missing = [];
if (files.some((f) => f.filename.startsWith('fastapi_cachex/'))) {
if (!changed.some((name) => name.startsWith('tests/'))) {
missing.push('The change to `fastapi_cachex/` comes without a test under `tests/`.');
}
if (!changed.some((name) => /^changelog\.d\/\d+\.[a-z]+(\.\d+)?\.md$/.test(name))) {
missing.push('The change to `fastapi_cachex/` comes without a changelog fragment (`changelog.d/<issue>.<section>.md`).');
}
}
if (blocking.length === 0 && missing.length === 0) {
if (previous) await upsert(`${marker}\nThe contribution rules are met now. Thanks!`);
return;
}
const list = [...blocking, ...missing].map((line) => `- ${line}`).join('\n');
if (blocking.length > 0) {
await upsert(
`${marker}\nThanks for the pull request. This project only takes pull requests for ` +
`issues that have been assigned to the author, so I am closing this one:\n\n${list}\n\n` +
`See the [contributing guide](${guide}). Once a maintainer has assigned the issue to you, ` +
'open a new pull request.',
);
await github.rest.pulls.update({ owner, repo, pull_number: pr.number, state: 'closed' });
core.setFailed('Closed: the pull request does not close an issue assigned to its author.');
} else {
await upsert(
`${marker}\nThanks for the pull request. Before review it still needs:\n\n${list}\n\n` +
`See the [contributing guide](${guide}). Push the missing pieces to this branch and the check reruns.`,
);
core.setFailed('The pull request is missing tests or a changelog fragment.');
}