Repository navigation
435 lines (405 loc) · 21.3 KB
/
Copy pathoperator-release.yml
File metadata and controls
435 lines (405 loc) · 21.3 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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
name: Operator Image Release
# Builds and pushes the OpenShift operator (controller) image to GHCR.
# The image wraps the published Helm chart via the operator-sdk helm plugin.
# It must be public: the community-operators CI installs it on a real cluster
# and has no credentials for a private registry.
#
# ONE-TIME MANUAL STEP: the very first push creates the GHCR package as
# PRIVATE. After the first successful release, make it public once:
# github.com/orgs/libredb/packages -> libredb-studio-operator ->
# Package settings -> Change visibility -> Public.
# Community catalog submissions FAIL while the image is private.
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: "Operator version tag (e.g. 0.9.44 - no v prefix)"
required: true
type: string
concurrency:
# Keyed on the ref on purpose, even though a global group would serialize
# the submission's "an open submission for another version" guard, which
# reads the catalog's default branch and so cannot see a pull request opened
# seconds earlier by another run.
#
# A global group costs more than that race. GitHub keeps only ONE pending
# run per group: a third release cancels the second while the first is still
# running, `cancel-in-progress: false` or not, and a cancelled run means no
# image and no submission for that version, silently. The race it would
# prevent is loud instead: two submissions naming the same predecessor fail
# the second merge upstream with "multiple channel heads".
group: operator-release-${{ github.ref }}
cancel-in-progress: false
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/libredb-studio-operator
jobs:
build-and-push:
name: Build and Push Operator Image
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
outputs:
# Read by submit-catalogs. `skip` is what makes a Helm chart tag a no-op
# for the whole workflow rather than for this job alone.
version: ${{ steps.version.outputs.version }}
skip: ${{ steps.version.outputs.skip }}
submittable: ${{ steps.version.outputs.submittable }}
image: ${{ steps.image.outputs.name }}
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Verify operator embedded chart is in sync
run: |
if ! diff -r --exclude=charts charts/libredb-studio operator/helm-charts/libredb-studio; then
echo "::error::operator/helm-charts/libredb-studio is out of sync with charts/libredb-studio. The operator image would ship a stale chart. Refresh it with: rm -rf operator/helm-charts/libredb-studio && cp -R charts/libredb-studio operator/helm-charts/libredb-studio"
exit 1
fi
- name: Resolve version (no v prefix)
id: version
env:
EVENT_NAME: ${{ github.event_name }}
INPUT_VERSION: ${{ github.event.inputs.version }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
VERSION="$INPUT_VERSION"
else
VERSION="${RELEASE_TAG#v}"
fi
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z][0-9A-Za-z.-]*)?$'; then
if [ "$EVENT_NAME" = "release" ]; then
# Helm chart releases publish tags like libredb-studio-0.1.19,
# which also emit 'release: published'. They are not operator
# versions - skip quietly instead of failing every chart release.
echo "Tag '$RELEASE_TAG' is not an app semver - skipping operator build"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Version '$VERSION' is not a valid semver (no v prefix) - refusing to tag" >&2
exit 1
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
# The regex above admits a prerelease (0.15.0-rc.1), which is a
# legitimate operator image but not a catalog submission: the
# community catalogs take bare semver directories. Say so here
# rather than letting the submission job fail on it after the image
# is already pushed.
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "submittable=true" >> "$GITHUB_OUTPUT"
else
echo "submittable=false" >> "$GITHUB_OUTPUT"
echo "::notice::'${VERSION}' is a prerelease; the operator image is built but no catalog submission is opened."
fi
- name: Guard version against the checked-out ref
# A manual dispatch (typo'd or historical version) or a manually
# published release whose tag points at a commit with a different
# package.json would otherwise stamp mismatched content with an
# immutable tag. Run the guard for every non-skipped event; the
# checked-out ref carries the authoritative package.json version.
if: steps.version.outputs.skip != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
PKG=$(node -p "require('./package.json').version")
if [ "$VERSION" != "$PKG" ]; then
echo "Version '$VERSION' does not match package.json '$PKG' at the checked-out ref - run this workflow from refs/tags/$VERSION (or the matching commit) instead" >&2
exit 1
fi
- name: Prepare image name (lowercase)
if: steps.version.outputs.skip != 'true'
id: image
run: |
NAME=$(printf '%s' "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
echo "name=${NAME}" >> "$GITHUB_OUTPUT"
- name: Set up Helm
if: steps.version.outputs.skip != 'true'
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
version: v4.1.3
- name: Vendor embedded chart dependencies
# charts/*.tgz is gitignored; without this the image ships the chart
# with its declared postgresql dependency missing, and helm silently
# installs without the subchart when postgresql.enabled=true.
# --skip-refresh: the `helm repo add` in the same block already fetched the index; Helm 4's `dependency build` re-fetches it by default and takes no --timeout.
if: steps.version.outputs.skip != 'true'
run: |
helm repo add bitnami https://charts.bitnami.com/bitnami --timeout 5m
helm dependency build operator/helm-charts/libredb-studio --skip-refresh
- name: Set up QEMU
if: steps.version.outputs.skip != 'true'
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
- name: Set up Docker Buildx
if: steps.version.outputs.skip != 'true'
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Log in to GitHub Container Registry
if: steps.version.outputs.skip != 'true'
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push operator image
if: steps.version.outputs.skip != 'true'
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: operator
push: true
tags: ${{ steps.image.outputs.name }}:${{ steps.version.outputs.version }}
platforms: linux/amd64,linux/arm64
submit-catalogs:
name: Submit ${{ matrix.catalog.label }} bundle PR
# The community catalogs pull the controller image on a real cluster, so
# this cannot start before the image exists and is public (issue #656).
needs: build-and-push
if: needs.build-and-push.outputs.skip != 'true' && needs.build-and-push.outputs.submittable == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
strategy:
# One catalog failing must not cancel the other: they are independent
# submissions with independent review queues.
fail-fast: false
matrix:
catalog:
- label: operatorhub.io
upstream: k8s-operatorhub/community-operators
fork: libredb/community-operators
release_config: false
- label: OpenShift console
upstream: redhat-openshift-ecosystem/community-operators-prod
fork: libredb/community-operators-prod
release_config: true
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: "1.4.2"
- name: Install dependencies
uses: ./.github/actions/bun-install
- name: Resolve the channel switch and the token
id: gate
env:
# Presence only: the secret is never expanded into a shell variable
# that could reach a log, and the steps that actually need it take it
# individually.
TOKEN_PRESENT: ${{ secrets.OPERATOR_CATALOG_TOKEN != '' }}
run: |
set -euo pipefail
# distribution/channels.yaml is the switchboard for every optional
# release channel; this reads the same flag the winget job reads.
CI_ENABLED=$(node scripts/distribution-check.mjs --ci-outputs | sed -n 's/^operatorhub_community=//p')
if [ "${CI_ENABLED:-}" != "true" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::operatorhub-community update.ci_enabled is '${CI_ENABLED:-<unresolved>}' in distribution/channels.yaml - skipping the catalog submission."
exit 0
fi
if [ "${TOKEN_PRESENT}" != "true" ]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::OPERATOR_CATALOG_TOKEN is not configured - skipping the catalog submission."
exit 0
fi
echo "enabled=true" >> "$GITHUB_OUTPUT"
- name: Verify the controller image is anonymously pullable
if: steps.gate.outputs.enabled == 'true'
id: image
env:
IMAGE: ${{ needs.build-and-push.outputs.image }}
VERSION: ${{ needs.build-and-push.outputs.version }}
run: |
set -euo pipefail
# The catalog pipelines pull without credentials, so a private
# package fails there rather than here, late and confusingly. This
# checks it the way they will: an anonymous pull token and the
# manifest, with no credentials of our own.
#
# The manifest is parsed rather than grepped. A single-arch manifest
# has no platform objects at all, and grepping for a substring would
# read "absent" and "present but formatted differently" the same way.
REPO="${IMAGE#ghcr.io/}"
TOKEN=$(curl -sf --retry 3 --retry-connrefused "https://ghcr.io/token?service=ghcr.io&scope=repository:${REPO}:pull" |
node -e 'let raw = ""; process.stdin.on("data", (d) => { raw += d }); process.stdin.on("end", () => { process.stdout.write(JSON.parse(raw).token) })')
# No -f on the manifest read. ghcr hands out a pull token to anyone
# who asks, even for a package they cannot read, so the failure lands
# here - and with -f the step would die on curl's exit 22 before the
# message written for exactly this case could print.
CODE=$(curl -s --retry 3 --retry-connrefused -o manifest.json -w '%{http_code}' \
-H "Authorization: Bearer ${TOKEN}" \
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" \
"https://ghcr.io/v2/${REPO}/manifests/${VERSION}")
case "${CODE}" in
200) ;;
401|403)
echo "::error::${IMAGE}:${VERSION} is not anonymously pullable (HTTP ${CODE}). The catalog CI pulls without credentials - make the GHCR package public: github.com/orgs/libredb/packages"
exit 1 ;;
404)
echo "::error::${IMAGE}:${VERSION} has no manifest (HTTP 404) - the tag was never pushed."
exit 1 ;;
*)
echo "::error::ghcr returned HTTP ${CODE} for ${IMAGE}:${VERSION}."
exit 1 ;;
esac
node -e '
const index = JSON.parse(require("node:fs").readFileSync("manifest.json", "utf8"))
const platforms = (index.manifests ?? []).map((m) => `${m.platform?.os}/${m.platform?.architecture}`)
const missing = ["linux/amd64", "linux/arm64"].filter((p) => !platforms.includes(p))
if (missing.length > 0) {
console.log(`::error::${process.env.IMAGE}:${process.env.VERSION} is missing ${missing.join(", ")}. Found: ${platforms.join(", ") || "no manifest list"}. If the GHCR package is private, make it public before submitting.`)
process.exit(1)
}
console.log(`Image is public and serves ${platforms.join(", ")}.`)
'
rm -f manifest.json
- name: Assert the fork and fast-forward its default branch
if: steps.gate.outputs.enabled == 'true'
env:
GH_TOKEN: ${{ secrets.OPERATOR_CATALOG_TOKEN }}
FORK: ${{ matrix.catalog.fork }}
UPSTREAM: ${{ matrix.catalog.upstream }}
run: |
set -euo pipefail
# Not `gh repo sync --source`: that calls POST /merge-upstream first,
# an endpoint which takes neither a source nor a force parameter, so
# --source is dead on the success path and a fork pointing at the
# WRONG parent would sync from the wrong repository and still exit 0.
# The parent is asserted instead, then merge-upstream is called
# directly, then the result is asserted.
PARENT=$(gh api "repos/${FORK}" --jq .parent.full_name)
if [ "${PARENT}" != "${UPSTREAM}" ]; then
echo "::error::${FORK} is a fork of '${PARENT}', not of '${UPSTREAM}'. A fork's parent cannot be changed - re-create it from the upstream, or transfer the existing fork."
exit 1
fi
gh api -X POST "repos/${FORK}/merge-upstream" -f branch=main --jq .message
# The upstream pipeline rebases the submission branch onto the
# catalog's default branch and fails loudly if it cannot, so a fork
# whose main carries commits the upstream lacks has to stop us here
# rather than upstream.
STATUS=$(gh api "repos/${UPSTREAM}/compare/main...${FORK%%/*}:${FORK##*/}:main" --jq .status)
case "${STATUS}" in
identical|behind) echo "Fork main is ${STATUS} relative to upstream main." ;;
*) echo "::error::${FORK} main is '${STATUS}' relative to ${UPSTREAM} main; it carries commits upstream does not have. Reset it before submitting."; exit 1 ;;
esac
- name: Checkout the catalog repository
if: steps.gate.outputs.enabled == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6
with:
repository: ${{ matrix.catalog.upstream }}
ref: main
path: catalog
persist-credentials: false
- name: Decide whether to submit, and what this bundle replaces
if: steps.gate.outputs.enabled == 'true'
id: decide
env:
# The search API is rate limited hard for anonymous callers, and an
# unread search must fail the step rather than default to "submit".
GITHUB_TOKEN: ${{ secrets.OPERATOR_CATALOG_TOKEN }}
run: |
set -euo pipefail
node scripts/operator-catalog-submission.mjs decide \
--version "${{ needs.build-and-push.outputs.version }}" \
--operator-dir catalog/operators/libredb-studio-operator \
--repo "${{ matrix.catalog.upstream }}" \
--fork "${{ matrix.catalog.fork }}" | tee -a "$GITHUB_OUTPUT"
- name: Report a skip
if: steps.gate.outputs.enabled == 'true' && steps.decide.outputs.enabled != 'true'
run: |
# A warning rather than a notice: every skip here means a release did
# not reach a catalog, and one of them (an older submission still
# open) mutes every later release until somebody acts on it.
echo "::warning::${{ matrix.catalog.upstream }} - ${{ steps.decide.outputs.reason }}"
- name: Stage the bundle
if: steps.decide.outputs.enabled == 'true'
env:
VERSION: ${{ needs.build-and-push.outputs.version }}
IMAGE: ${{ needs.build-and-push.outputs.image }}
REPLACES: ${{ steps.decide.outputs.replaces }}
run: |
set -euo pipefail
DEST="catalog/operators/libredb-studio-operator/${VERSION}"
CSV="operator/bundle/manifests/libredb-studio-operator.clusterserviceversion.yaml"
# The directory is named from the release version while its contents
# come from the committed bundle, and nothing upstream of here proves
# those agree: the bundle-freshness job only runs when a pull request
# touches operator/, so a version bump that skipped `make -C operator
# bundle` would submit the previous bundle under the new number.
grep -qx " version: ${VERSION}" "$CSV" || {
echo "::error::${CSV} does not declare version ${VERSION}. Run 'make -C operator bundle' and commit the result."
exit 1
}
grep -q "containerImage: ${IMAGE}:${VERSION}$" "$CSV" || {
echo "::error::${CSV} does not stamp containerImage ${IMAGE}:${VERSION}. Run 'make -C operator bundle' and commit the result."
exit 1
}
mkdir -p "$DEST"
cp -R operator/bundle/. "$DEST/"
# spec.replaces is absent from the committed bundle on purpose: its
# only correct value names a version in THIS catalog, which this
# repository cannot derive. It is injected here, or omitted entirely
# on a first submission.
if [ -n "${REPLACES}" ]; then
node scripts/operator-catalog-submission.mjs set-replaces \
--csv "$DEST/manifests/libredb-studio-operator.clusterserviceversion.yaml" \
--replaces "${REPLACES}"
else
echo "First submission in this catalog - no replaces to set."
fi
- name: Write release-config.yaml for the FBC catalog
if: steps.decide.outputs.enabled == 'true' && matrix.catalog.release_config
env:
VERSION: ${{ needs.build-and-push.outputs.version }}
REPLACES: ${{ steps.decide.outputs.replaces }}
run: |
set -euo pipefail
# Without this file the release pipeline only prints the manual
# instructions instead of opening the rendered-catalog PR itself.
# `replaces` and not `skipRange`: with a skipRange alone the rendered
# channel keeps two heads and `opm validate` rejects it with
# "multiple channel heads found in graph" (measured on
# community-operators-prod#11106).
CONFIG="catalog/operators/libredb-studio-operator/${VERSION}/release-config.yaml"
{
echo "---"
echo "catalog_templates:"
echo " - template_name: basic.yaml"
echo " channels: [alpha]"
if [ -n "${REPLACES}" ]; then
echo " replaces: ${REPLACES}"
fi
} > "$CONFIG"
cat "$CONFIG"
- name: Open or update the submission PR
if: steps.decide.outputs.enabled == 'true'
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
path: catalog
push-to-fork: ${{ matrix.catalog.fork }}
token: ${{ secrets.OPERATOR_CATALOG_TOKEN }}
base: main
branch: libredb-studio-operator-${{ needs.build-and-push.outputs.version }}
# Never delete the head branch from here: deleting it closes the PR
# and GitHub then refuses to reopen it.
delete-branch: false
add-paths: operators/libredb-studio-operator
# DCO, not GPG: upstream's "signed" requirement is the Developer
# Certificate of Origin, and the trailer email must match the author.
signoff: true
committer: Mehmet Cevheri BOZOGLAN <cevheribozoglan@gmail.com>
author: Mehmet Cevheri BOZOGLAN <cevheribozoglan@gmail.com>
commit-message: |
operator libredb-studio-operator (${{ needs.build-and-push.outputs.version }})
title: operator libredb-studio-operator (${{ needs.build-and-push.outputs.version }})
body: |
Update libredb-studio-operator to ${{ needs.build-and-push.outputs.version }}.
Release notes: https://github.com/libredb/libredb-studio/releases/tag/${{ needs.build-and-push.outputs.version }}
Controller image: `${{ needs.build-and-push.outputs.image }}:${{ needs.build-and-push.outputs.version }}`, public, linux/amd64 and linux/arm64.
${{ steps.decide.outputs.reason }}
Opened by this repository's release workflow.