workflow/labels: switch to a scheduled trigger (#416808)

authored by philiptaron.tngl.sh and committed by GitHub 5bd9bdc6 65d2429d

+117 -79
+117 -62
.github/workflows/labels.yml
··· 6 name: "Label PR" 7 8 on: 9 workflow_call: 10 - workflow_run: 11 - workflows: 12 - - Review dismissed 13 - - Review submitted 14 - types: [completed] 15 16 concurrency: 17 - group: labels-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }} 18 - cancel-in-progress: true 19 20 permissions: 21 issues: write # needed to create *new* labels ··· 31 runs-on: ubuntu-24.04-arm 32 if: "!contains(github.event.pull_request.title, '[skip treewide]')" 33 steps: 34 - - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 35 - id: eval 36 - with: 37 - script: | 38 - const run_id = (await github.rest.actions.listWorkflowRuns({ 39 - owner: context.repo.owner, 40 - repo: context.repo.repo, 41 - workflow_id: 'eval.yml', 42 - event: 'pull_request_target', 43 - head_sha: context.payload.pull_request?.head.sha ?? context.payload.workflow_run.head_sha 44 - })).data.workflow_runs[0]?.id 45 - core.setOutput('run-id', run_id) 46 - 47 - - name: Download the comparison results 48 - if: steps.eval.outputs.run-id 49 - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 50 - with: 51 - run-id: ${{ steps.eval.outputs.run-id }} 52 - github-token: ${{ github.token }} 53 - pattern: comparison 54 - path: comparison 55 - merge-multiple: true 56 57 - - name: Labels from eval 58 - if: steps.eval.outputs.run-id && github.event_name != 'pull_request' 59 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 60 with: 61 script: | 62 const { readFile } = require('node:fs/promises') 63 64 - let pull_requests 65 - if (context.payload.workflow_run) { 66 - // PRs from forks don't have any PRs associated by default. 67 - // Thus, we request the PR number with an API call *to* the fork's repo. 68 - // Multiple pull requests can be open from the same head commit, either via 69 - // different base branches or head branches. 70 - const { head_repository, head_sha, repository } = context.payload.workflow_run 71 - pull_requests = (await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, { 72 - owner: head_repository.owner.login, 73 - repo: head_repository.name, 74 - commit_sha: head_sha 75 - })).filter(pull_request => pull_request.base.repo.id == repository.id) 76 - } else { 77 - pull_requests = [ context.payload.pull_request ] 78 } 79 80 - await Promise.all( 81 - pull_requests.map(async (pull_request) => { 82 - const pr = { 83 - owner: context.repo.owner, 84 - repo: context.repo.repo, 85 - issue_number: pull_request.number 86 - } 87 88 // Get all currently set labels that we manage 89 const before = 90 - (await github.paginate(github.rest.issues.listLabelsOnIssue, pr)) 91 - .map(({ name }) => name) 92 .filter(name => 93 name.startsWith('10.rebuild') || 94 name == '11.by: package-maintainer' || ··· 98 99 const approvals = new Set( 100 (await github.paginate(github.rest.pulls.listReviews, { 101 - owner: context.repo.owner, 102 - repo: context.repo.repo, 103 pull_number: pull_request.number 104 })) 105 .filter(review => review.state == 'APPROVED') ··· 119 await Promise.all( 120 before.filter(name => !after.includes(name)) 121 .map(name => github.rest.issues.removeLabel({ 122 - ...pr, 123 name 124 })) 125 ) ··· 128 const added = after.filter(name => !before.includes(name)) 129 if (added.length > 0) { 130 await github.rest.issues.addLabels({ 131 - ...pr, 132 labels: added 133 }) 134 } 135 - }) 136 ) 137 138 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 139 name: Labels from touched files 140 if: | 141 - github.event_name != 'workflow_run' && 142 github.event.pull_request.head.repo.owner.login != 'NixOS' || !( 143 github.head_ref == 'haskell-updates' || 144 github.head_ref == 'python-updates' || ··· 153 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 154 name: Labels from touched files (no sync) 155 if: | 156 - github.event_name != 'workflow_run' && 157 github.event.pull_request.head.repo.owner.login != 'NixOS' || !( 158 github.head_ref == 'haskell-updates' || 159 github.head_ref == 'python-updates' || ··· 171 # This is to avoid the mass of labels there, which is mostly useless - and really annoying for 172 # the backport labels. 173 if: | 174 - github.event_name != 'workflow_run' && 175 github.event.pull_request.head.repo.owner.login == 'NixOS' && ( 176 github.head_ref == 'haskell-updates' || 177 github.head_ref == 'python-updates' ||
··· 6 name: "Label PR" 7 8 on: 9 + schedule: 10 + - cron: '37 * * * *' 11 workflow_call: 12 + workflow_dispatch: 13 + inputs: 14 + updatedWithin: 15 + description: 'Updated within [hours]' 16 + type: number 17 + required: false 18 + default: 0 # everything since last run 19 20 concurrency: 21 + # This explicitly avoids using `run_id` for the concurrency key to make sure that only 22 + # *one* non-PR run can run at a time. 23 + group: labels-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number }} 24 + # PR- and manually-triggered runs will be cancelled, but scheduled runs will be queued. 25 + cancel-in-progress: ${{ github.event_name != 'schedule' }} 26 27 permissions: 28 issues: write # needed to create *new* labels ··· 38 runs-on: ubuntu-24.04-arm 39 if: "!contains(github.event.pull_request.title, '[skip treewide]')" 40 steps: 41 + - name: Install dependencies 42 + run: npm install @actions/artifact 43 44 + - name: Labels from API data and Eval results 45 uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 46 + env: 47 + UPDATED_WITHIN: ${{ inputs.updatedWithin }} 48 with: 49 script: | 50 + const path = require('node:path') 51 + const { DefaultArtifactClient } = require('@actions/artifact') 52 const { readFile } = require('node:fs/promises') 53 54 + const artifactClient = new DefaultArtifactClient() 55 + 56 + if (process.env.UPDATED_WITHIN && !/^\d+$/.test(process.env.UPDATED_WITHIN)) 57 + throw new Error('Please enter "updated within" as integer in hours.') 58 + 59 + const cutoff = new Date(await (async () => { 60 + // Always run for Pull Request triggers, no cutoff since there will be a single 61 + // response only anyway. 0 is the Unix epoch, so always smaller. 62 + if (context.payload.pull_request?.number) return 0 63 + 64 + // Manually triggered via UI when updatedWithin is set. Will fallthrough to the last 65 + // option if the updatedWithin parameter is set to 0, which is the default. 66 + const updatedWithin = Number.parseInt(process.env.UPDATED_WITHIN, 10) 67 + if (updatedWithin) return new Date().getTime() - updatedWithin * 60 * 60 * 1000 68 + 69 + // Normally a scheduled run, but could be workflow_dispatch, see above. Go back as far 70 + // as the last successful run of this workflow to make sure we are not leaving anyone 71 + // behind on GHA failures. 72 + return (await github.rest.actions.listWorkflowRuns({ 73 + ...context.repo, 74 + workflow_id: 'labels.yml', 75 + event: 'schedule', 76 + status: 'success', 77 + exclude_pull_requests: true 78 + })).data.workflow_runs[0]?.created_at 79 + })()) 80 + core.info('cutoff timestamp: ' + cutoff.toISOString()) 81 + 82 + // To simplify this action's logic we fetch the pull_request data again below, even if 83 + // we are already in a pull_request event's context and would have the data readily 84 + // available. We do this by filtering the list of pull requests with head and base 85 + // branch - there can only be a single open Pull Request for any such combination. 86 + const prEventCondition = !context.payload.pull_request ? undefined : { 87 + // "label" is in the format of `user:branch` or `org:branch` 88 + head: context.payload.pull_request.head.label, 89 + base: context.payload.pull_request.base.ref 90 } 91 92 + await github.paginate( 93 + github.rest.pulls.list, 94 + { 95 + ...context.repo, 96 + state: 'open', 97 + sort: 'updated', 98 + direction: 'desc', 99 + ...prEventCondition 100 + }, 101 + async (response, done) => await Promise.all(response.data.map(async (pull_request) => { 102 + const log = (k,v) => core.info(`PR #${pull_request.number} - ${k}: ${v}`) 103 + 104 + log('Last updated at', pull_request.updated_at) 105 + if (new Date(pull_request.updated_at) < cutoff) return done() 106 + 107 + const run_id = (await github.rest.actions.listWorkflowRuns({ 108 + ...context.repo, 109 + workflow_id: 'eval.yml', 110 + event: 'pull_request_target', 111 + // For PR events, the workflow run is still in progress with this job itself. 112 + status: prEventCondition ? 'in_progress' : 'success', 113 + exclude_pull_requests: true, 114 + head_sha: pull_request.head.sha 115 + })).data.workflow_runs[0]?.id 116 + 117 + // Newer PRs might not have run Eval to completion, yet. We can skip them, because this 118 + // job will be run as part of that Eval run anyway. 119 + log('Last eval run', run_id) 120 + if (!run_id) return; 121 + 122 + const artifact = (await github.rest.actions.listWorkflowRunArtifacts({ 123 + ...context.repo, 124 + run_id, 125 + name: 'comparison' 126 + })).data.artifacts[0] 127 + 128 + // Instead of checking the boolean artifact.expired, we will give us a minute to 129 + // actually download the artifact in the next step and avoid that race condition. 130 + log('Artifact expires at', artifact.expires_at) 131 + if (new Date(artifact.expires_at) < new Date(new Date().getTime() + 60 * 1000)) return; 132 + 133 + await artifactClient.downloadArtifact(artifact.id, { 134 + findBy: { 135 + repositoryName: context.repo.repo, 136 + repositoryOwner: context.repo.owner, 137 + token: core.getInput('github-token') 138 + }, 139 + path: path.resolve('comparison'), 140 + expectedHash: artifact.digest 141 + }) 142 143 // Get all currently set labels that we manage 144 const before = 145 + pull_request.labels.map(({ name }) => name) 146 .filter(name => 147 name.startsWith('10.rebuild') || 148 name == '11.by: package-maintainer' || ··· 152 153 const approvals = new Set( 154 (await github.paginate(github.rest.pulls.listReviews, { 155 + ...context.repo, 156 pull_number: pull_request.number 157 })) 158 .filter(review => review.state == 'APPROVED') ··· 172 await Promise.all( 173 before.filter(name => !after.includes(name)) 174 .map(name => github.rest.issues.removeLabel({ 175 + ...context.repo, 176 + issue_number: pull_request.number 177 name 178 })) 179 ) ··· 182 const added = after.filter(name => !before.includes(name)) 183 if (added.length > 0) { 184 await github.rest.issues.addLabels({ 185 + ...context.repo, 186 + issue_number: pull_request.number 187 labels: added 188 }) 189 } 190 + })) 191 ) 192 193 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 194 name: Labels from touched files 195 if: | 196 + github.event_name == 'pull_request_target' && 197 github.event.pull_request.head.repo.owner.login != 'NixOS' || !( 198 github.head_ref == 'haskell-updates' || 199 github.head_ref == 'python-updates' || ··· 208 - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 209 name: Labels from touched files (no sync) 210 if: | 211 + github.event_name == 'pull_request_target' && 212 github.event.pull_request.head.repo.owner.login != 'NixOS' || !( 213 github.head_ref == 'haskell-updates' || 214 github.head_ref == 'python-updates' || ··· 226 # This is to avoid the mass of labels there, which is mostly useless - and really annoying for 227 # the backport labels. 228 if: | 229 + github.event_name == 'pull_request_target' && 230 github.event.pull_request.head.repo.owner.login == 'NixOS' && ( 231 github.head_ref == 'haskell-updates' || 232 github.head_ref == 'python-updates' ||
-17
.github/workflows/review-submitted.yml
··· 1 - name: Review submitted 2 - 3 - on: 4 - pull_request_review: 5 - types: [submitted] 6 - 7 - permissions: {} 8 - 9 - defaults: 10 - run: 11 - shell: bash 12 - 13 - jobs: 14 - trigger: 15 - runs-on: ubuntu-24.04-arm 16 - steps: 17 - - run: echo This is a no-op only used as a trigger for workflow_run.
···