Repository navigation
239 lines (217 loc) · 12.4 KB
/
Copy pathslim-release-published.yml
File metadata and controls
239 lines (217 loc) · 12.4 KB
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
name: Slim Release Published
# Reconciles the stack catalog against supabase/slim-services releases. See
# docs/adr/0026-slim-artifact-mirrors.md, "Hotfix and upgrade pickup".
on:
repository_dispatch:
types:
- slim-release-published
permissions:
contents: read
concurrency:
group: slim-release-published-${{ github.event.client_payload.service }}
cancel-in-progress: false
# The default `single` cancels a pending run when another is queued, losing its release-visibility wait.
queue: max
jobs:
pickup:
runs-on: ubuntu-latest
# Setup (~10 min) + release visibility (<1 min) + the S3-mirror wait (up to ~10 min, normally only
# for the first planned item) + pin, render and push per item + the merge-queue wait (up to 30 min).
timeout-minutes: 75
permissions:
contents: read
steps:
- name: Checkout the default branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
# `Artifacts.ts` and `render-service-dockerfile.ts` import locked workspace dependencies.
- name: Setup
uses: ./.github/actions/setup
with:
dependency-firewall-token: ${{ secrets.DF_FIREWALL_TOKEN }}
- name: Record the base commit
id: base
run: echo "sha=$(git rev-parse HEAD)" >>"$GITHUB_OUTPUT"
# Re-echoes a validate-payload failure so its `::error ::…` line surfaces as an annotation.
- name: Validate payload
id: validate
env:
SERVICE: ${{ github.event.client_payload.service }}
UPSTREAM_VERSION: ${{ github.event.client_payload.upstream_version }}
REVISION: ${{ github.event.client_payload.revision }}
RELEASE_VERSION: ${{ github.event.client_payload.release_version }}
run: |
set -euo pipefail
if ! output="$(bun .github/scripts/sync-artifacts-catalog.ts validate-payload \
--service "$SERVICE" --upstream "$UPSTREAM_VERSION" --revision "$REVISION" --release "$RELEASE_VERSION")"; then
echo "$output"
exit 1
fi
echo "$output" >>"$GITHUB_OUTPUT"
# `--expect-release` waits for the releases API to list the release behind this dispatch.
- name: Plan updates
id: plan
env:
SERVICE: ${{ steps.validate.outputs.service }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
bun .github/scripts/sync-artifacts-catalog.ts plan-updates --service "$SERVICE" --format lines \
--expect-release "$RELEASE_VERSION" --output "${RUNNER_TEMP}/slim-updates.tsv"
count="$(wc -l < "${RUNNER_TEMP}/slim-updates.tsv" | tr -d ' ')"
echo "count=${count}" >>"$GITHUB_OUTPUT"
- name: Nothing to do
if: steps.plan.outputs.count == '0'
env:
SERVICE: ${{ steps.validate.outputs.service }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
run: echo "No catalog updates for ${SERVICE} from ${RELEASE_VERSION}; nothing to do."
- name: Install regctl
if: steps.plan.outputs.count != '0'
run: |
set -euo pipefail
install -d "${RUNNER_TEMP}/regctl-bin"
curl -fsSLo "${RUNNER_TEMP}/regctl-bin/regctl" \
https://github.com/regclient/regclient/releases/download/v0.11.5/regctl-linux-amd64
echo "c93aa7638749f5aaac1a8e01787321889c78f0101809bb2880343478d0ba0467 ${RUNNER_TEMP}/regctl-bin/regctl" | sha256sum -c -
chmod +x "${RUNNER_TEMP}/regctl-bin/regctl"
echo "${RUNNER_TEMP}/regctl-bin" >>"$GITHUB_PATH"
"${RUNNER_TEMP}/regctl-bin/regctl" version
- name: Generate token
if: steps.plan.outputs.count != '0'
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.GH_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
# One branch per planned item, rebuilt from the recorded base commit, never from a previous
# iteration's HEAD; `--release` pins the planned release, never "highest at apply time".
# `bun`/`pnpm` run with the tokens unset: only `git push` and `gh` ever see them.
# A merge-queued branch rejects pushes; see ADR 0026 "Hotfix and upgrade pickup" for the wait and hand-off.
- name: Apply planned updates
if: steps.plan.outputs.count != '0'
env:
APP_TOKEN: ${{ steps.app-token.outputs.token }}
GITHUB_TOKEN: ${{ github.token }}
SERVICE: ${{ steps.validate.outputs.service }}
UPSTREAM_VERSION: ${{ steps.validate.outputs.upstream_version }}
REVISION: ${{ steps.validate.outputs.revision }}
RELEASE_VERSION: ${{ steps.validate.outputs.release_version }}
REPLAY: ${{ github.event.client_payload.replay }}
BASE_SHA: ${{ steps.base.outputs.sha }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
push_url="${PUSH_REMOTE_URL:-https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}.git}"
manual_prefix="bun .github/scripts/sync-artifacts-catalog.ts --service ${SERVICE} --release"
in_merge_queue() {
# shellcheck disable=SC2016 # `$owner`, `$name` and `$branch` are GraphQL variables.
GH_TOKEN="$APP_TOKEN" gh api graphql \
-f query='query($owner: String!, $name: String!, $branch: String!) { repository(owner: $owner, name: $name) { pullRequests(headRefName: $branch, states: OPEN, first: 10) { nodes { isInMergeQueue isCrossRepository } } } }' \
-f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f branch="$1" \
--jq '[.data.repository.pullRequests.nodes[] | select(.isCrossRepository | not) | .isInMergeQueue] | any'
}
# Reads records on fd 3, not stdin, so commands the loop runs (`bun`, `gh`, `git`) never
# consume bytes meant for `read`.
while IFS=$'\x1f' read -r -u 3 kind branch title release from; do
if [ -z "$kind" ] || [ -z "$branch" ] || [ -z "$title" ] || [ -z "$release" ] || [ -z "$from" ]; then
echo "::error ::Malformed plan-updates line: kind='${kind}' branch='${branch}' title='${title}' release='${release}' from='${from}'."
exit 1
fi
git checkout -B "$branch" "$BASE_SHA"
env -u APP_TOKEN bun .github/scripts/sync-artifacts-catalog.ts --service "$SERVICE" --release "$release"
env -u APP_TOKEN -u GITHUB_TOKEN pnpm exec oxfmt --config .oxfmtrc.json packages/stack/src/Artifacts.ts
env -u APP_TOKEN -u GITHUB_TOKEN bun apps/cli/scripts/render-service-dockerfile.ts
generated_files=(
packages/stack/src/Artifacts.ts
apps/cli/src/shared/services/Dockerfile
apps/cli-go/pkg/config/templates/Dockerfile
)
if git diff --quiet -- "${generated_files[@]}"; then
echo "${branch}: catalog and Dockerfiles already match ${release}; skipping."
continue
fi
git add -- "${generated_files[@]}"
git commit -m "$title"
if ! push_output="$(git push --force "$push_url" "HEAD:refs/heads/${branch}" 2>&1)"; then
echo "$push_output"
if ! grep -q "queued for merging cannot be updated" <<<"$push_output"; then
echo "::error ::Failed to push ${branch}. Run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand."
exit 1
fi
manual_steps="run manually: ${manual_prefix} ${release}, regenerate the Dockerfiles with \`bun apps/cli/scripts/render-service-dockerfile.ts\`, then open a pull request by hand."
queued_steps="Once ${branch} leaves the merge queue, ${manual_steps}"
# The app token minted just before this step expires after one hour; capping the wait at
# 50 minutes into the step leaves room for the lookups and the dispatch that follow it.
deadline=$((SECONDS + 1800 < 3000 ? SECONDS + 1800 : 3000))
while :; do
if ! queued="$(in_merge_queue "$branch")"; then
echo "::error ::Failed to read the merge-queue state of ${branch}. ${queued_steps}"
exit 1
fi
case "$queued" in
false) break ;;
true) ;;
*)
echo "::error ::Unexpected merge-queue state '${queued}' for ${branch}. ${queued_steps}"
exit 1
;;
esac
if [ "$SECONDS" -ge "$deadline" ]; then
echo "::error ::${branch} is still in the merge queue. ${queued_steps}"
exit 1
fi
sleep 30
done
# `replay` counts consecutive re-sends; anything but 0-2 stops the chain.
if ! [[ "${REPLAY:-0}" =~ ^[0-2]$ ]]; then
echo "::error ::Stopped re-dispatching after repeated merge-queue rejections of ${branch}; ${manual_steps}"
exit 1
fi
if ! GH_TOKEN="$APP_TOKEN" gh api "repos/${GITHUB_REPOSITORY}/dispatches" \
-f event_type=slim-release-published \
-f "client_payload[service]=${SERVICE}" \
-f "client_payload[upstream_version]=${UPSTREAM_VERSION}" \
-f "client_payload[revision]=${REVISION}" \
-f "client_payload[release_version]=${RELEASE_VERSION}" \
-f "client_payload[replay]=$((${REPLAY:-0} + 1))"; then
echo "::error ::Failed to re-dispatch slim-release-published for ${SERVICE} ${RELEASE_VERSION}. Re-run this job."
exit 1
fi
echo "::warning ::${branch} was held by a merge-queued pull request; re-dispatched slim-release-published so a fresh run re-plans from ${DEFAULT_BRANCH}."
exit 0
fi
echo "$push_output"
if [ "$kind" = "upgrade" ]; then
action="bumps"
else
action="pins"
fi
# Names the release this PR actually pins, not necessarily RELEASE_VERSION: postgres
# can plan a hotfix on one line and an unrelated upgrade on another in one dispatch.
body="$(printf 'This %s %s from %s to %s.\n\nhttps://github.com/supabase/slim-services/releases/tag/%s-%s\n\nPlanned after supabase/slim-services published %s. This branch is rewritten from %s whenever a newer relevant release arrives.\n' \
"$action" "$SERVICE" "$from" "$release" "$SERVICE" "$release" "$RELEASE_VERSION" "$DEFAULT_BRANCH")"
# `--head` matches by branch name only and ignores the owner, so a fork PR with the
# same head branch name would otherwise match too; `isCrossRepository` excludes it.
existing="$(GH_TOKEN="$APP_TOKEN" gh pr list --head "$branch" --base "$DEFAULT_BRANCH" --state open --json number,isCrossRepository --jq 'map(select(.isCrossRepository | not)) | .[0].number // empty')"
if [ -n "$existing" ]; then
if ! GH_TOKEN="$APP_TOKEN" gh pr edit "$existing" --title "$title" --body "$body"; then
echo "::error ::Pushed ${branch} but failed to edit pull request #${existing}. Run manually: ${manual_prefix} ${release}, then edit the pull request from ${branch} by hand."
exit 1
fi
elif ! GH_TOKEN="$APP_TOKEN" gh pr create \
--title "$title" \
--body "$body" \
--head "$branch" \
--base "$DEFAULT_BRANCH"; then
echo "::error ::Pushed ${branch} but failed to open a pull request. Run manually: ${manual_prefix} ${release}, then open a pull request from ${branch} against ${DEFAULT_BRANCH} by hand."
exit 1
fi
done 3< "${RUNNER_TEMP}/slim-updates.tsv"