-
Notifications
You must be signed in to change notification settings - Fork 9
187 lines (181 loc) · 7.19 KB
/
dispatch.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# Trigger the execution of workflow in batches.
# This workflow is needed since GitHub Actions limits the matrix size to 256 jobs.
# We use one job per repository per batch.
name: Dispatch
on:
workflow_dispatch:
inputs:
workflow:
description: "Workflow to dispatch"
required: true
inputs:
description: "Workflow inputs"
required: false
default: '{}'
filter:
description: "Filter to apply to the list of repositories"
required: false
default: 'true'
dry-run:
description: "Whether to run in dry run mode"
required: false
default: 'false'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
# Number of repositories in a batch.
# Matrix jobs within a workflow run are run in parallel.
# 256 is the upper limit on the number of matrix jobs.
# Batching too many repositories together can result in
# could not create workflow dispatch event: HTTP 422: inputs are too large.
# This value should be higher than max-parallel in workflow.
MAX_REPOS_PER_WORKFLOW: 100
# Number of seconds to wait before starting to watch workflow run.
# Unfortunately, the interval on the watch is not configurable.
# The delay helps us save on GH API requests.
WORKFLOW_COMPLETION_CHECK_DELAY: 60
jobs:
matrix:
name: Batch targets
runs-on: ubuntu-latest
outputs:
batches: ${{ steps.matrix.outputs.result }}
steps:
- id: matrix
uses: actions/github-script@v6
env:
FILTER: ${{ github.event.inputs.filter }}
with:
github-token: ${{ secrets.UCI_GITHUB_TOKEN }}
retries: 0
script: |
const request = async function(req, opts) {
try {
return await req(opts)
} catch(err) {
opts._attempt = (opts._attempt || 0) + 1
if (err.status === 403) {
if (err.response.headers['x-ratelimit-remaining'] === '0') {
const retryAfter = err.response.headers['x-ratelimit-reset'] - Math.floor(Date.now() / 1000) || 1
core.info(`Rate limit exceeded, retrying in ${retryAfter} seconds`)
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
return request(req, opts)
}
if (err.message.toLowerCase().includes('secondary rate limit')) {
const retryAfter = Math.pow(2, opts._attempt)
core.info(`Secondary rate limit exceeded, retrying in ${retryAfter} seconds`)
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
return request(req, opts)
}
}
throw err
}
}
github.hook.wrap('request', request)
core.info(`Looking for repositories the user has direct access to`)
const items = await github.paginate(github.rest.repos.listForAuthenticatedUser, {
affiliation: 'collaborator'
})
const maxReposPerWorkflow = parseInt(process.env.MAX_REPOS_PER_WORKFLOW)
const batches = []
let batch = []
for (const item of items) {
if (item.archived) {
core.info(`Skipping archived repository ${item.full_name}`)
continue
}
try {
await exec.exec('jq', ['-e', '-n', `${JSON.stringify(item)} | ${process.env.FILTER}`])
} catch(e) {
core.info(`Skipping repository ${item.full_name} due to filter`)
continue
}
batch.push(item.full_name)
if (batch.length === maxReposPerWorkflow) {
batches.push({
key: batches.length,
value: batch
})
batch = []
}
}
if (batch.length > 0) {
batches.push({
key: batches.length,
value: batch
})
}
return batches
dispatch:
needs: [ matrix ]
name: Dispatch workflow(batch ${{ matrix.cfg.key }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# We end up with multiple "dispatch" jobs,
# one per BATCHES "key" chunk above with a "value" array.
# For each "dispatch" job, matrix.cfg.value is an array, like:
#
# [
# "repo1",
# "repo2"
# ]
#
# The triggered workflow runs use that final array as their matrix.
# Since max-parallel here is 1, we'll end up with at most max-parallel from workflow + 1 parallel jobs.
# 20 is the upper limit on parallel jobs on a free plan.
cfg: ${{ fromJSON(needs.matrix.outputs.batches) }}
max-parallel: 1
env:
GITHUB_TOKEN: ${{ github.token }}
WORKFLOW: ${{ github.event.inputs.workflow }}
REPO: ${{ github.repository }}
steps:
- id: dispatch
name: Dispatch workflow
env:
TARGETS: ${{ toJSON(matrix.cfg.value) }}
INPUTS: ${{ github.event.inputs.inputs }}
DRY_RUN: ${{ github.event.inputs.dry-run }}
run: |
start_date="$(date +%s)"
args=()
for key in $(jq -r 'keys[]' <<< "$INPUTS"); do
args+=("--field" "$key=$(jq -rc '.["'"$key"'"]' <<< "$INPUTS")")
done
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY RUN: gh workflow run $WORKFLOW --ref $GITHUB_REF --repo $REPO --field targets=$TARGETS ${args[@]}"
else
gh workflow run "$WORKFLOW" --ref "$GITHUB_REF" --repo "$REPO" --field "targets=$TARGETS" ${args[@]}
fi
echo "start_date=$start_date" | tee -a $GITHUB_OUTPUT
- id: run
name: Wait for workflow run to start
if: github.event.inputs.dry-run == 'false'
env:
START_DATE: ${{ steps.dispatch.outputs.start_date }}
run: |
# checks every 3 seconds until the most recent workflow run's created_at is later than this job's start_date
while sleep 3; do
run="$(gh api "/repos/$REPO/actions/workflows/$WORKFLOW/runs?per_page=1" --jq '.workflow_runs[0]')"
# nothing to check if no workflow run was returned
if [[ ! -z "$run" ]]; then
run_start_date="$(date --date="$(jq -r '.created_at' <<< "$run")" +%s)"
if [[ "$run_start_date" > "$START_DATE" ]]; then
echo "id=$(jq -r '.id' <<< "$run")" | tee -a $GITHUB_OUTPUT
break
fi
fi
done
- name: Wait for workflow run to complete
if: github.event.inputs.dry-run == 'false'
env:
RUN_ID: ${{ steps.run.outputs.id }}
run: |
# delays checking workflow's run status to save on GH API requests
sleep $WORKFLOW_COMPLETION_CHECK_DELAY
# checks every 3 seconds until the workflow run's status is completed
# redirects the stdout to /dev/null because it is very chatty
gh run watch "$RUN_ID" --repo "$REPO" > /dev/null