66name : " Label PR"
77
88on :
9+ schedule :
10+ - cron : ' 37 * * * *'
911 workflow_call :
10- workflow_run :
11- workflows :
12- - Review dismissed
13- - Review submitted
14- types : [completed]
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
1519
1620concurrency :
17- group : labels-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.run_id }}
18- cancel-in-progress : true
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' }}
1926
2027permissions :
2128 issues : write # needed to create *new* labels
@@ -31,64 +38,111 @@ jobs:
3138 runs-on : ubuntu-24.04-arm
3239 if : " !contains(github.event.pull_request.title, '[skip treewide]')"
3340 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'
41+ - name : Install dependencies
42+ run : npm install @actions/artifact
43+
44+ - name : Labels from API data and Eval results
5945 uses : actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
46+ env :
47+ UPDATED_WITHIN : ${{ inputs.updatedWithin }}
6048 with :
6149 script : |
50+ const path = require('node:path')
51+ const { DefaultArtifactClient } = require('@actions/artifact')
6252 const { readFile } = require('node:fs/promises')
6353
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 ]
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
7890 }
7991
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- }
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+ })
87142
88143 // Get all currently set labels that we manage
89144 const before =
90- (await github.paginate(github.rest.issues.listLabelsOnIssue, pr))
91- .map(({ name }) => name)
145+ pull_request.labels.map(({ name }) => name)
92146 .filter(name =>
93147 name.startsWith('10.rebuild') ||
94148 name == '11.by: package-maintainer' ||
98152
99153 const approvals = new Set(
100154 (await github.paginate(github.rest.pulls.listReviews, {
101- owner: context.repo.owner,
102- repo: context.repo.repo,
155+ ...context.repo,
103156 pull_number: pull_request.number
104157 }))
105158 .filter(review => review.state == 'APPROVED')
@@ -119,7 +172,8 @@ jobs:
119172 await Promise.all(
120173 before.filter(name => !after.includes(name))
121174 .map(name => github.rest.issues.removeLabel({
122- ...pr,
175+ ...context.repo,
176+ issue_number: pull_request.number
123177 name
124178 }))
125179 )
@@ -128,17 +182,18 @@ jobs:
128182 const added = after.filter(name => !before.includes(name))
129183 if (added.length > 0) {
130184 await github.rest.issues.addLabels({
131- ...pr,
185+ ...context.repo,
186+ issue_number: pull_request.number
132187 labels: added
133188 })
134189 }
135- })
190+ }))
136191 )
137192
138193 - uses : actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
139194 name : Labels from touched files
140195 if : |
141- github.event_name != 'workflow_run ' &&
196+ github.event_name == 'pull_request_target ' &&
142197 github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
143198 github.head_ref == 'haskell-updates' ||
144199 github.head_ref == 'python-updates' ||
@@ -153,7 +208,7 @@ jobs:
153208 - uses : actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
154209 name : Labels from touched files (no sync)
155210 if : |
156- github.event_name != 'workflow_run ' &&
211+ github.event_name == 'pull_request_target ' &&
157212 github.event.pull_request.head.repo.owner.login != 'NixOS' || !(
158213 github.head_ref == 'haskell-updates' ||
159214 github.head_ref == 'python-updates' ||
@@ -171,7 +226,7 @@ jobs:
171226 # This is to avoid the mass of labels there, which is mostly useless - and really annoying for
172227 # the backport labels.
173228 if : |
174- github.event_name != 'workflow_run ' &&
229+ github.event_name == 'pull_request_target ' &&
175230 github.event.pull_request.head.repo.owner.login == 'NixOS' && (
176231 github.head_ref == 'haskell-updates' ||
177232 github.head_ref == 'python-updates' ||
0 commit comments