feat(session)!: reject JWT HMAC secrets shorter than the hash output #91
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PR gate | ||
|
Check warning on line 1 in .github/workflows/pr-gate.yml
|
||
| # 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.'); | ||
| } | ||