From bd7e8b992b0d3b3dee10ebfa9163e5fcdab873ed Mon Sep 17 00:00:00 2001 From: Michael Daniels Date: Sun, 12 Apr 2026 14:14:19 -0400 Subject: [PATCH] ci/github-script/manual-file-edits: init Blocks manual edits to github-teams.json --- .github/workflows/check.yml | 35 +++++++++++++++++ ci/github-script/manual-file-edits.js | 55 +++++++++++++++++++++++++++ ci/github-script/run | 11 ++++++ 3 files changed, 101 insertions(+) create mode 100644 ci/github-script/manual-file-edits.js diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 577823d61f55..d246f8ad061e 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -71,6 +71,41 @@ jobs: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq + manual-file-edits: + if: inputs.baseBranch && inputs.headBranch + permissions: + pull-requests: write + runs-on: ubuntu-slim + timeout-minutes: 3 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + path: trusted + sparse-checkout: | + ci/github-script + + - name: Log current API rate limits + env: + GH_TOKEN: ${{ github.token }} + run: gh api /rate_limit | jq + + - name: Discourage manual edits to certain files + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + require('./trusted/ci/github-script/manual-file-edits.js')({ + github, + context, + core, + repoPath: 'trusted', + }) + + - name: Log current API rate limits + env: + GH_TOKEN: ${{ github.token }} + run: gh api /rate_limit | jq + owners: runs-on: ubuntu-24.04-arm timeout-minutes: 5 diff --git a/ci/github-script/manual-file-edits.js b/ci/github-script/manual-file-edits.js new file mode 100644 index 000000000000..e40d6decbb7d --- /dev/null +++ b/ci/github-script/manual-file-edits.js @@ -0,0 +1,55 @@ +// @ts-check +const { getCommitDetailsForPR } = require('./get-pr-commit-details') + +/** + * @param {{ + * github: InstanceType, + * context: import('@actions/github/lib/context').Context, + * core: import('@actions/core'), + * repoPath?: string, + * }} CheckManualFileEditsProps + */ +async function checkManualFileEdits({ github, context, core, repoPath }) { + const pull_number = context.payload.pull_request?.number + if (!pull_number) { + core.info('This is not a pull request. Skipping checks.') + 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 these checks do not apply.') + return + } + + const details = await getCommitDetailsForPR({ core, pr, repoPath }) + + if ( + details.some(({ changedPaths }) => + changedPaths.includes('maintainers/github-teams.json'), + ) + ) { + core.setFailed( + [ + 'maintainers/github-teams.json is supposed to accurately reflect the state of the teams in GitHub.\n', + 'Therefore, it should not be edited manually.\n', + 'All changes to teams listed in maintainers/github-teams.json should be performed in GitHub by a team maintainer.\n', + "Team maintainers are listed in the github-teams.json file and in GitHub's UI.\n", + 'If there is no team maintainer available, an org owner can make the needed change, please contact one by', + 'following the instructions at https://github.com/NixOS/org/blob/main/doc/github-org-owners.md#how-to-contact-the-team.\n', + 'Thank you!', + ].reduce( + (prev, curr) => prev + (!prev || prev.endsWith('\n') ? '' : ' ') + curr, + '', + ), + ) + } +} + +module.exports = checkManualFileEdits diff --git a/ci/github-script/run b/ci/github-script/run index 744e49f018a4..1ac5075dab1b 100755 --- a/ci/github-script/run +++ b/ci/github-script/run @@ -116,4 +116,15 @@ program await run(checkCommitMessages, owner, repo, pr, options) }) +program + .command('manual-file-edits') + .description("Error when files that shouldn't be edited manually are") + .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 checkManualFileEdits = (await import('./manual-file-edits.js')).default + await run(checkManualFileEdits, owner, repo, pr, options) + }) + await program.parse()