diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index fd3404f07a3b..04b210146b86 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -24,6 +24,10 @@ on: # not evaluate untrusted code. NIXPKGS_MANUAL_EDIT_CHECK_APP_PRIVATE_KEY: required: false + # Can be provided in pull requests because the job it is used in does + # not evaluate untrusted code. + NIXPKGS_CI_APP_PRIVATE_KEY: + required: false # Should only be provided in the merge queue, not in pull requests, # where we're evaluating untrusted code. CACHIX_AUTH_TOKEN_GHA: @@ -136,6 +140,52 @@ jobs: GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} run: gh api /rate_limit | jq + reminders: + if: inputs.baseBranch && inputs.headBranch + permissions: + pull-requests: write + runs-on: ubuntu-24.04-arm + timeout-minutes: 8 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false + path: trusted + sparse-checkout: | + ci/github-script + + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + if: github.event_name != 'pull_request' && vars.NIXPKGS_CI_CLIENT_ID + id: app-token + with: + client-id: ${{ vars.NIXPKGS_CI_CLIENT_ID }} + private-key: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }} + permission-pull-requests: write + + - name: Log current API rate limits + env: + GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} + run: gh api /rate_limit | jq + + - name: Post reminders + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + github-token: ${{ steps.app-token.outputs.token || github.token }} + script: | + const { default: postReminders } = await import('${{ github.workspace }}/trusted/ci/github-script/reminders.ts') + await postReminders({ + github, + context, + core, + dry: context.eventName == 'pull_request', + repoPath: 'trusted', + }) + + - name: Log current API rate limits + env: + GH_TOKEN: ${{ steps.app-token.outputs.token || github.token }} + run: gh api /rate_limit | jq + github-script: runs-on: ubuntu-24.04-arm timeout-minutes: 5 diff --git a/.github/workflows/pull-request-target.yml b/.github/workflows/pull-request-target.yml index 48f74b6dd930..aa42175a0cc1 100644 --- a/.github/workflows/pull-request-target.yml +++ b/.github/workflows/pull-request-target.yml @@ -82,6 +82,7 @@ jobs: secrets: NIXPKGS_COMMIT_CHECK_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_COMMIT_CHECK_APP_PRIVATE_KEY }} NIXPKGS_MANUAL_EDIT_CHECK_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_MANUAL_EDIT_CHECK_APP_PRIVATE_KEY }} + NIXPKGS_CI_APP_PRIVATE_KEY: ${{ secrets.NIXPKGS_CI_APP_PRIVATE_KEY }} with: baseBranch: ${{ needs.prepare.outputs.baseBranch }} headBranch: ${{ needs.prepare.outputs.headBranch }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8ea1194f879..1404d29785d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,6 +88,7 @@ jobs: 'ci/github-script/package.json', 'ci/github-script/package-lock.json', 'ci/github-script/prepare.js', + 'ci/github-script/reminders.ts', 'ci/github-script/reviewers.js', 'ci/github-script/reviews.js', 'ci/github-script/supportedBranches.js', @@ -95,7 +96,7 @@ jobs: 'ci/github-script/withRateLimit.js', 'ci/pinned.json', 'pkgs/top-level/release-supported-systems.json', - ].includes(file))) core.setOutput('pr', true) + ].includes(file) || file.startsWith('ci/github-script/reminders/'))) core.setOutput('pr', true) merge-group: if: needs.prepare.outputs.merge-group diff --git a/ci/github-script/README.md b/ci/github-script/README.md index 52b78c79d79f..d47559081746 100644 --- a/ci/github-script/README.md +++ b/ci/github-script/README.md @@ -15,3 +15,9 @@ Run `./run commits OWNER REPO PR`, where OWNER is your username or "NixOS", REPO ## Labeler Run `./run labels OWNER REPO`, where OWNER is your username or "NixOS" and REPO the name of your fork or "nixpkgs". + +## Reminders + +Run `./run reminders OWNER REPO PR` to post the reminders that apply to the paths a pull request touches. + +To add a reminder, write the review body to `reminders/KEY.md` and list KEY with its path regexes in `reminders.js`. diff --git a/ci/github-script/reminders.ts b/ci/github-script/reminders.ts new file mode 100644 index 000000000000..49431c752149 --- /dev/null +++ b/ci/github-script/reminders.ts @@ -0,0 +1,111 @@ +import fs from 'node:fs' +import path from 'node:path' +import type * as actionsCore from '@actions/core' +import type { context as actionsContext } from '@actions/github' +import type { GitHub } from '@actions/github/lib/utils' +import { getCommitDetailsForPR } from './get-pr-commit-details.js' +import { dismissReviews, postReview } from './reviews.js' +import { classify } from './supportedBranches.js' + +/** + * Reminders to post as a non-blocking review when a pull request touches + * certain matching paths. + */ +export const reminders: { key: string; paths: RegExp[] }[] = [ + { + key: 'docs-styleguide', + paths: [/^doc\//, /^nixos\/doc\//, /^nixos\/modules\/.*\.md$/], + }, +] + +export function matchesAny(changedPaths: string[], patterns: RegExp[]) { + return changedPaths.some((changedPath) => + patterns.some((pattern) => pattern.test(changedPath)), + ) +} + +function reminderBody(key: string) { + return fs + .readFileSync(path.join(import.meta.dirname, 'reminders', `${key}.md`), { + encoding: 'utf8', + }) + .trim() +} + +export default async function postReminders({ + github, + context, + core, + repoPath, + dry, +}: { + github: InstanceType + context: typeof actionsContext + core: typeof actionsCore + repoPath?: string + dry: boolean +}) { + const pull_number = context.payload.pull_request?.number + if (!pull_number) { + core.info('This is not a pull request. Skipping reminders.') + return + } + + const pr = ( + await github.rest.pulls.get({ + ...context.repo, + pull_number, + }) + ).data + + if (pr.user.login.endsWith('[bot]')) { + core.info('This is a bot, so reminders do not apply.') + return + } + + const baseBranchType = classify( + pr.base.ref.replace(/^refs\/heads\//, ''), + ).type + const headBranchType = classify( + pr.head.ref.replace(/^refs\/heads\//, ''), + ).type + + if ( + baseBranchType.includes('development') && + headBranchType.includes('development') && + pr.base.repo.id === pr.head.repo?.id + ) { + // This matches, for example, PRs from NixOS:staging-next to NixOS:master, or vice versa. + // We ignore them, we should only care about PRs introducing new commits. + // We still want to run on PRs from, e.g., Someone:master to NixOS:master though. + core.info( + 'This PR is from one development branch to another. Skipping reminders.', + ) + return + } + + const details = await getCommitDetailsForPR({ core, pr, repoPath }) + const changedPaths = details.flatMap(({ changedPaths }) => changedPaths) + + for (const { key, paths } of reminders) { + if (matchesAny(changedPaths, paths)) { + await postReview({ + github, + context, + core, + dry, + event: 'COMMENT', + body: reminderBody(key), + reviewKey: key, + }) + } else { + await dismissReviews({ + github, + context, + core, + dry, + reviewKey: key, + }) + } + } +} diff --git a/ci/github-script/reminders/docs-styleguide.md b/ci/github-script/reminders/docs-styleguide.md new file mode 100644 index 000000000000..d42fc5265e11 --- /dev/null +++ b/ci/github-script/reminders/docs-styleguide.md @@ -0,0 +1,12 @@ +Thanks for contributing to the documentation + +Make sure you follow the [documentation styleguide](https://github.com/NixOS/nixpkgs/blob/master/doc/styleguide.md), most notably: + +- Show, don't tell: lead with a minimal working example; explanation follows the code. +- No meta-commentary: don't write "This section explains how to…", just do it. +- Imperative mood and active voice: "Run the command", not "The user should run the following command". +- Present tense: "This creates a folder", not "This will create a folder". +- Be confident: no hedging with "should", "might", "typically", "usually". +- Cut filler words: "simply", "just", "easily", "basically"; "to", not "in order to". + +For larger changes, like adding or removing whole sections, ask the NixOS documentation team for a review. diff --git a/ci/github-script/run b/ci/github-script/run index 1fbdd0c24a65..a47220f2c33a 100755 --- a/ci/github-script/run +++ b/ci/github-script/run @@ -127,4 +127,15 @@ program await run(checkManualFileEdits, owner, repo, pr, options) }) +program + .command('reminders') + .description('Post reminders for the paths a PR touches') + .argument('', 'Owner of the GitHub repository to run on (Example: NixOS)') + .argument('', 'Name of the GitHub repository to run on (Example: nixpkgs)') + .argument('', 'Number of the Pull Request to run on') + .action(async (owner, repo, pr, options) => { + const postReminders = (await import('./reminders.ts')).default + await run(postReminders, owner, repo, pr, options) + }) + await program.parse()