Loading .github/workflows/pull-request-target.yml +0 −12 Original line number Diff line number Diff line Loading @@ -112,18 +112,6 @@ jobs: with: headBranch: ${{ needs.prepare.outputs.headBranch }} reviewers: name: Reviewers needs: [prepare, eval] if: | needs.prepare.outputs.targetSha && !contains(fromJSON(needs.prepare.outputs.headBranch).type, 'development') uses: ./.github/workflows/reviewers.yml secrets: OWNER_APP_PRIVATE_KEY: ${{ secrets.OWNER_APP_PRIVATE_KEY }} with: artifact-prefix: ${{ inputs.artifact-prefix }} build: name: Build needs: [prepare] Loading .github/workflows/reviewers.ymldeleted 100644 → 0 +0 −157 Original line number Diff line number Diff line # This workflow will request reviews from the maintainers of each package # listed in the PR's most recent eval comparison artifact. name: Reviewers on: pull_request_target: types: [ready_for_review] workflow_call: inputs: artifact-prefix: required: true type: string secrets: OWNER_APP_PRIVATE_KEY: required: true concurrency: group: reviewers-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true permissions: {} defaults: run: shell: bash jobs: request: runs-on: ubuntu-24.04-arm timeout-minutes: 20 steps: - name: Check out the PR at the base commit uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false path: trusted sparse-checkout: ci - name: Install Nix uses: cachix/install-nix-action@456688f15bc354bef6d396e4a35f4f89d40bf2b7 # v31 - name: Build the requestReviews derivation run: nix-build trusted/ci -A requestReviews # For requesting reviewers, this job depends on a GitHub App with the following permissions: # - Permissions: # - Repository > Administration: read-only # - Organization > Members: read-only # - Repository > Pull Requests: read-write # - Install App on this repository, setting these variables: # - OWNER_APP_ID (variable) # - OWNER_APP_PRIVATE_KEY (secret) # # Can't use the token received from permissions above, because it can't get enough permissions. - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 if: github.event_name == 'pull_request_target' && vars.OWNER_APP_ID id: app-token with: app-id: ${{ vars.OWNER_APP_ID }} private-key: ${{ secrets.OWNER_APP_PRIVATE_KEY }} permission-administration: read permission-members: read permission-pull-requests: write - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq # In the regular case, this workflow is called via workflow_call from the eval workflow directly. # In the more special case, when a PR is undrafted an eval run will have started already. - name: Wait for comparison to be done uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 id: eval env: ARTIFACT: ${{ inputs.artifact-prefix }}comparison with: script: | const run_id = (await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: context.eventName === 'pull_request' ? 'test.yml' : 'pull-request-target.yml', event: context.eventName, head_sha: context.payload.pull_request.head.sha })).data.workflow_runs[0].id core.setOutput('run-id', run_id) // Waiting 120 * 5 sec = 10 min. max. // The extreme case is an Eval run that just started when the PR is undrafted. // Eval takes max 5-6 minutes, normally. for (let i = 0; i < 120; i++) { const result = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id, name: process.env.ARTIFACT, }) if (result.data.total_count > 0) return await new Promise(resolve => setTimeout(resolve, 5000)) } throw new Error("No comparison artifact found.") - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq - name: Download the comparison results uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: run-id: ${{ steps.eval.outputs.run-id }} github-token: ${{ github.token }} pattern: ${{ inputs.artifact-prefix }}comparison path: comparison merge-multiple: true - name: Log current API rate limits (app-token) if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: gh api /rate_limit | jq - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq - name: Requesting reviews if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ github.token }} APP_GH_TOKEN: ${{ steps.app-token.outputs.token }} REPOSITORY: ${{ github.repository }} NUMBER: ${{ github.event.number }} AUTHOR: ${{ github.event.pull_request.user.login }} # Don't request reviewers on draft PRs DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }} run: | # maintainers.json contains GitHub IDs. Look up handles to request reviews from. # There appears to be no API to request reviews based on GitHub IDs jq -r 'keys[]' comparison/maintainers.json \ | while read -r id; do gh api /user/"$id" --jq .login; done \ | cat comparison/owners.txt - \ | GH_TOKEN="$APP_GH_TOKEN" result/bin/request-reviewers.sh "$REPOSITORY" "$NUMBER" "$AUTHOR" - name: Log current API rate limits (app-token) if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: gh api /rate_limit | jq - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq .github/workflows/test.yml +0 −1 Original line number Diff line number Diff line Loading @@ -69,7 +69,6 @@ jobs: '.github/workflows/eval.yml', '.github/workflows/lint.yml', '.github/workflows/pull-request-target.yml', '.github/workflows/reviewers.yml', '.github/workflows/test.yml', 'ci/github-script/bot.js', 'ci/github-script/merge.js', Loading ci/default.nix +0 −1 Original line number Diff line number Diff line Loading @@ -156,7 +156,6 @@ let in rec { inherit pkgs fmt; requestReviews = pkgs.callPackage ./request-reviews { }; codeownersValidator = pkgs.callPackage ./codeowners-validator { }; # FIXME(lf-): it might be useful to test other Nix implementations Loading ci/github-script/bot.js +48 −13 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ module.exports = async ({ github, context, core, dry }) => { const withRateLimit = require('./withRateLimit.js') const { classify } = require('../supportedBranches.js') const { handleMerge } = require('./merge.js') const { handleReviewers } = require('./reviewers.js') const artifactClient = new DefaultArtifactClient() Loading Loading @@ -209,10 +210,16 @@ module.exports = async ({ github, context, core, dry }) => { } } const reviews = await github.paginate(github.rest.pulls.listReviews, { // Check for any human reviews other than GitHub actions and other GitHub apps. // Accounts could be deleted as well, so don't count them. const reviews = ( await github.paginate(github.rest.pulls.listReviews, { ...context.repo, pull_number, }) ).filter( (r) => r.user && !r.user.login.endsWith('[bot]') && r.user.type !== 'Bot', ) const approvals = new Set( reviews Loading Loading @@ -282,13 +289,6 @@ module.exports = async ({ github, context, core, dry }) => { log('Last eval run', run_id ?? '<n/a>') if (conclusion === 'success') { // Check for any human reviews other than GitHub actions and other GitHub apps. // Accounts could be deleted as well, so don't count them. const humanReviews = reviews.filter( (r) => r.user && !r.user.login.endsWith('[bot]') && r.user.type !== 'Bot', ) Object.assign(prLabels, { // We only set this label if the latest eval run was successful, because if it was not, it // *could* have requested reviewers. We will let the PR author fix CI first, before "escalating" Loading @@ -301,7 +301,7 @@ module.exports = async ({ github, context, core, dry }) => { '9.needs: reviewer': !pull_request.draft && pull_request.requested_reviewers.length === 0 && humanReviews.length === 0, reviews.length === 0, }) } Loading Loading @@ -373,6 +373,41 @@ module.exports = async ({ github, context, core, dry }) => { maintainers[pkg]?.some((m) => approvals.has(m)), ), }) if (!pull_request.draft) { let owners = [] try { // TODO: Create owner map similar to maintainer map. owners = (await readFile(`${pull_number}/owners.txt`, 'utf-8')).split( '\n', ) } catch (e) { // Older artifacts don't have the owners.txt, yet. if (e.code !== 'ENOENT') throw e } // We set this label earlier already, but the current PR state can be very different // after handleReviewers has requested reviews, so update it in this case to prevent // this label from flip-flopping. prLabels['9.needs: reviewer'] = await handleReviewers({ github, context, core, log, dry, pull_request, reviews, // TODO: Use maintainer map instead of the artifact. maintainers: Object.keys( JSON.parse( await readFile(`${pull_number}/maintainers.json`, 'utf-8'), ), ).map((id) => parseInt(id)), owners, getTeamMembers, getUser, }) } } return prLabels Loading Loading @@ -521,7 +556,7 @@ module.exports = async ({ github, context, core, dry }) => { const hasChanges = Object.keys(after).some( (name) => (before[name] ?? false) !== after[name], ) if (log('Has changes', hasChanges, !hasChanges)) return if (log('Has label changes', hasChanges, !hasChanges)) return // Skipping labeling on a pull_request event, because we have no privileges. const labels = Object.entries(after) Loading Loading
.github/workflows/pull-request-target.yml +0 −12 Original line number Diff line number Diff line Loading @@ -112,18 +112,6 @@ jobs: with: headBranch: ${{ needs.prepare.outputs.headBranch }} reviewers: name: Reviewers needs: [prepare, eval] if: | needs.prepare.outputs.targetSha && !contains(fromJSON(needs.prepare.outputs.headBranch).type, 'development') uses: ./.github/workflows/reviewers.yml secrets: OWNER_APP_PRIVATE_KEY: ${{ secrets.OWNER_APP_PRIVATE_KEY }} with: artifact-prefix: ${{ inputs.artifact-prefix }} build: name: Build needs: [prepare] Loading
.github/workflows/reviewers.ymldeleted 100644 → 0 +0 −157 Original line number Diff line number Diff line # This workflow will request reviews from the maintainers of each package # listed in the PR's most recent eval comparison artifact. name: Reviewers on: pull_request_target: types: [ready_for_review] workflow_call: inputs: artifact-prefix: required: true type: string secrets: OWNER_APP_PRIVATE_KEY: required: true concurrency: group: reviewers-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }} cancel-in-progress: true permissions: {} defaults: run: shell: bash jobs: request: runs-on: ubuntu-24.04-arm timeout-minutes: 20 steps: - name: Check out the PR at the base commit uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false path: trusted sparse-checkout: ci - name: Install Nix uses: cachix/install-nix-action@456688f15bc354bef6d396e4a35f4f89d40bf2b7 # v31 - name: Build the requestReviews derivation run: nix-build trusted/ci -A requestReviews # For requesting reviewers, this job depends on a GitHub App with the following permissions: # - Permissions: # - Repository > Administration: read-only # - Organization > Members: read-only # - Repository > Pull Requests: read-write # - Install App on this repository, setting these variables: # - OWNER_APP_ID (variable) # - OWNER_APP_PRIVATE_KEY (secret) # # Can't use the token received from permissions above, because it can't get enough permissions. - uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4 if: github.event_name == 'pull_request_target' && vars.OWNER_APP_ID id: app-token with: app-id: ${{ vars.OWNER_APP_ID }} private-key: ${{ secrets.OWNER_APP_PRIVATE_KEY }} permission-administration: read permission-members: read permission-pull-requests: write - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq # In the regular case, this workflow is called via workflow_call from the eval workflow directly. # In the more special case, when a PR is undrafted an eval run will have started already. - name: Wait for comparison to be done uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 id: eval env: ARTIFACT: ${{ inputs.artifact-prefix }}comparison with: script: | const run_id = (await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: context.eventName === 'pull_request' ? 'test.yml' : 'pull-request-target.yml', event: context.eventName, head_sha: context.payload.pull_request.head.sha })).data.workflow_runs[0].id core.setOutput('run-id', run_id) // Waiting 120 * 5 sec = 10 min. max. // The extreme case is an Eval run that just started when the PR is undrafted. // Eval takes max 5-6 minutes, normally. for (let i = 0; i < 120; i++) { const result = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id, name: process.env.ARTIFACT, }) if (result.data.total_count > 0) return await new Promise(resolve => setTimeout(resolve, 5000)) } throw new Error("No comparison artifact found.") - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq - name: Download the comparison results uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: run-id: ${{ steps.eval.outputs.run-id }} github-token: ${{ github.token }} pattern: ${{ inputs.artifact-prefix }}comparison path: comparison merge-multiple: true - name: Log current API rate limits (app-token) if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: gh api /rate_limit | jq - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq - name: Requesting reviews if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ github.token }} APP_GH_TOKEN: ${{ steps.app-token.outputs.token }} REPOSITORY: ${{ github.repository }} NUMBER: ${{ github.event.number }} AUTHOR: ${{ github.event.pull_request.user.login }} # Don't request reviewers on draft PRs DRY_MODE: ${{ github.event.pull_request.draft && '1' || '' }} run: | # maintainers.json contains GitHub IDs. Look up handles to request reviews from. # There appears to be no API to request reviews based on GitHub IDs jq -r 'keys[]' comparison/maintainers.json \ | while read -r id; do gh api /user/"$id" --jq .login; done \ | cat comparison/owners.txt - \ | GH_TOKEN="$APP_GH_TOKEN" result/bin/request-reviewers.sh "$REPOSITORY" "$NUMBER" "$AUTHOR" - name: Log current API rate limits (app-token) if: ${{ steps.app-token.outputs.token }} env: GH_TOKEN: ${{ steps.app-token.outputs.token }} run: gh api /rate_limit | jq - name: Log current API rate limits (github.token) env: GH_TOKEN: ${{ github.token }} run: gh api /rate_limit | jq
.github/workflows/test.yml +0 −1 Original line number Diff line number Diff line Loading @@ -69,7 +69,6 @@ jobs: '.github/workflows/eval.yml', '.github/workflows/lint.yml', '.github/workflows/pull-request-target.yml', '.github/workflows/reviewers.yml', '.github/workflows/test.yml', 'ci/github-script/bot.js', 'ci/github-script/merge.js', Loading
ci/default.nix +0 −1 Original line number Diff line number Diff line Loading @@ -156,7 +156,6 @@ let in rec { inherit pkgs fmt; requestReviews = pkgs.callPackage ./request-reviews { }; codeownersValidator = pkgs.callPackage ./codeowners-validator { }; # FIXME(lf-): it might be useful to test other Nix implementations Loading
ci/github-script/bot.js +48 −13 Original line number Diff line number Diff line Loading @@ -5,6 +5,7 @@ module.exports = async ({ github, context, core, dry }) => { const withRateLimit = require('./withRateLimit.js') const { classify } = require('../supportedBranches.js') const { handleMerge } = require('./merge.js') const { handleReviewers } = require('./reviewers.js') const artifactClient = new DefaultArtifactClient() Loading Loading @@ -209,10 +210,16 @@ module.exports = async ({ github, context, core, dry }) => { } } const reviews = await github.paginate(github.rest.pulls.listReviews, { // Check for any human reviews other than GitHub actions and other GitHub apps. // Accounts could be deleted as well, so don't count them. const reviews = ( await github.paginate(github.rest.pulls.listReviews, { ...context.repo, pull_number, }) ).filter( (r) => r.user && !r.user.login.endsWith('[bot]') && r.user.type !== 'Bot', ) const approvals = new Set( reviews Loading Loading @@ -282,13 +289,6 @@ module.exports = async ({ github, context, core, dry }) => { log('Last eval run', run_id ?? '<n/a>') if (conclusion === 'success') { // Check for any human reviews other than GitHub actions and other GitHub apps. // Accounts could be deleted as well, so don't count them. const humanReviews = reviews.filter( (r) => r.user && !r.user.login.endsWith('[bot]') && r.user.type !== 'Bot', ) Object.assign(prLabels, { // We only set this label if the latest eval run was successful, because if it was not, it // *could* have requested reviewers. We will let the PR author fix CI first, before "escalating" Loading @@ -301,7 +301,7 @@ module.exports = async ({ github, context, core, dry }) => { '9.needs: reviewer': !pull_request.draft && pull_request.requested_reviewers.length === 0 && humanReviews.length === 0, reviews.length === 0, }) } Loading Loading @@ -373,6 +373,41 @@ module.exports = async ({ github, context, core, dry }) => { maintainers[pkg]?.some((m) => approvals.has(m)), ), }) if (!pull_request.draft) { let owners = [] try { // TODO: Create owner map similar to maintainer map. owners = (await readFile(`${pull_number}/owners.txt`, 'utf-8')).split( '\n', ) } catch (e) { // Older artifacts don't have the owners.txt, yet. if (e.code !== 'ENOENT') throw e } // We set this label earlier already, but the current PR state can be very different // after handleReviewers has requested reviews, so update it in this case to prevent // this label from flip-flopping. prLabels['9.needs: reviewer'] = await handleReviewers({ github, context, core, log, dry, pull_request, reviews, // TODO: Use maintainer map instead of the artifact. maintainers: Object.keys( JSON.parse( await readFile(`${pull_number}/maintainers.json`, 'utf-8'), ), ).map((id) => parseInt(id)), owners, getTeamMembers, getUser, }) } } return prLabels Loading Loading @@ -521,7 +556,7 @@ module.exports = async ({ github, context, core, dry }) => { const hasChanges = Object.keys(after).some( (name) => (before[name] ?? false) !== after[name], ) if (log('Has changes', hasChanges, !hasChanges)) return if (log('Has label changes', hasChanges, !hasChanges)) return // Skipping labeling on a pull_request event, because we have no privileges. const labels = Object.entries(after) Loading