Skip to content

Commit 5bd9bdc

Browse files
authored
workflow/labels: switch to a scheduled trigger (#416808)
2 parents 65d2429 + 4d53700 commit 5bd9bdc

File tree

2 files changed

+118
-80
lines changed

2 files changed

+118
-80
lines changed

.github/workflows/labels.yml

Lines changed: 118 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,23 @@
66
name: "Label PR"
77

88
on:
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

1620
concurrency:
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

2027
permissions:
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' ||
@@ -98,8 +152,7 @@ jobs:
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' ||

.github/workflows/review-submitted.yml

Lines changed: 0 additions & 17 deletions
This file was deleted.

0 commit comments

Comments
 (0)