Repository navigation
278 lines (249 loc) · 11.4 KB
/
Copy pathcube-image.yml
File metadata and controls
278 lines (249 loc) · 11.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
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
name: Cube Image
# Builds ghcr.io/formbricks/cube from docker/cube-image (see its README.md). Pull requests and the merge
# queue build it for amd64, gate it on Trivy and run the smoke test. Pushes to main and manual runs on
# main also publish it for amd64 and arm64 under its immutable tag plus `latest`, and sign it with cosign.
on:
pull_request:
branches:
- main
paths:
- "docker/cube-image/**"
- "docker/cube/**"
# smoke-test.sh runs the cube service exactly as this file declares it.
- "docker/docker-compose.yml"
- ".github/workflows/cube-image.yml"
# merge_group has no `paths` filter; the verify job diffs against the queue's base instead.
merge_group:
branches:
- main
push:
branches:
- main
paths:
- "docker/cube-image/**"
- "docker/cube/**"
# smoke-test.sh runs the cube service exactly as this file declares it.
- "docker/docker-compose.yml"
- ".github/workflows/cube-image.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
# Never cancel a run on main: it may be halfway through pushing and signing.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
IMAGE: ghcr.io/formbricks/cube
jobs:
verify:
name: Build, scan and smoke-test
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Detect Cube changes
id: changes
shell: bash
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before }}
run: |
set -euo pipefail
# A manual run, or a push with no previous commit to compare against, checks everything.
if [[ "$EVENT_NAME" == "workflow_dispatch" || -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then
echo "relevant=true" >> "$GITHUB_OUTPUT"
echo "image=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git fetch --no-tags --depth=1 origin "$BASE_SHA"
if git diff --quiet "$BASE_SHA" HEAD -- docker/cube-image docker/cube docker/docker-compose.yml \
.github/workflows/cube-image.yml; then
echo "No Cube image changes; skipping."
echo "relevant=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "relevant=true" >> "$GITHUB_OUTPUT"
# What goes into the image. Only a change here (or to the gate itself) runs the builds for both
# architectures and the Trivy gate, so a schema or Compose edit gets the smoke test without going
# red over a base-image CVE it did not introduce.
image_inputs=(docker/cube-image/Dockerfile docker/cube-image/package.json
docker/cube-image/package-lock.json docker/cube-image/native.sha256)
if git diff --quiet "$BASE_SHA" HEAD -- "${image_inputs[@]}" docker/cube-image/.trivyignore.yaml \
.github/workflows/cube-image.yml; then
echo "image=false" >> "$GITHUB_OUTPUT"
else
echo "image=true" >> "$GITHUB_OUTPUT"
fi
# The published tag is immutable, so a change to what goes into the image must also change the
# tag, or main would have nothing new to publish (see docker/cube-image/README.md). Pull requests
# and the merge queue enforce it; a push to main has already passed it.
if [[ "$EVENT_NAME" == "push" ]] || git diff --quiet "$BASE_SHA" HEAD -- "${image_inputs[@]}"; then
exit 0
fi
base_tag="$(docker/cube-image/image-tag.sh "$BASE_SHA")"
head_tag="$(docker/cube-image/image-tag.sh)"
if [[ "$base_tag" == "$head_tag" ]]; then
echo "::error file=docker/cube-image/REVISION::The image changed but its tag is still ${head_tag}. Bump docker/cube-image/REVISION, or reset it to 1 when the Cube version changes."
exit 1
fi
echo "Image tag changes from ${base_tag:-<none>} to ${head_tag}."
- name: Set up Docker Buildx
if: steps.changes.outputs.relevant == 'true'
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
# The build stage cross-builds (see the Dockerfile), so arm64 needs no emulation. Building it here
# catches a bad arm64 checksum before main tries to publish.
- name: Build linux/arm64
if: steps.changes.outputs.image == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: docker/cube-image
platforms: linux/arm64
outputs: type=cacheonly
- name: Build linux/amd64
if: steps.changes.outputs.relevant == 'true'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: docker/cube-image
platforms: linux/amd64
load: true
tags: formbricks-cube:ci
- name: Trivy gate (CRITICAL and HIGH with a fix)
if: steps.changes.outputs.image == 'true'
uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
with:
image-ref: formbricks-cube:ci
scanners: vuln
severity: "CRITICAL,HIGH"
ignore-unfixed: true
trivyignores: docker/cube-image/.trivyignore.yaml
exit-code: "1"
format: table
version: "v0.71.2"
- name: Set up Node.js
if: steps.changes.outputs.relevant == 'true'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Smoke test
if: steps.changes.outputs.relevant == 'true'
run: bash docker/cube-image/smoke-test.sh formbricks-cube:ci
publish:
name: Publish ghcr.io/formbricks/cube
# Only from main: a manual run on another branch would publish unreviewed code under the real tag.
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
packages: write
# Keyless cosign signing through Sigstore's OIDC identity.
id-token: write
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Resolve image tag
id: tag
shell: bash
run: |
set -euo pipefail
# Assigned first: inside `echo "tag=$(...)"` a failing image-tag.sh would still exit 0.
tag="$(docker/cube-image/image-tag.sh)"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Log in to GitHub Container Registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# An existing immutable tag is never pushed again: a run whose image inputs did not change (a schema
# edit under docker/cube/, a README change, a re-run) has nothing new to publish. Any lookup error
# other than "absent" fails the job rather than risk overwriting a published tag.
- name: Refuse to overwrite a published tag
id: existing
shell: bash
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
if output="$(docker buildx imagetools inspect "$IMAGE:$TAG" 2>&1)"; then
echo "::notice::$IMAGE:$TAG is already published and immutable, so nothing is pushed. Bump docker/cube-image/REVISION to publish a changed image."
echo "exists=true" >> "$GITHUB_OUTPUT"
elif grep -qiE "not found|403 Forbidden|denied" <<<"$output"; then
# A 403 is what GHCR answers before the package's first push. Without read access the push
# below fails too, so treating it as absent cannot overwrite anything.
echo "exists=false" >> "$GITHUB_OUTPUT"
else
echo "$output" >&2
echo "::error::Could not tell whether $IMAGE:$TAG is already published; refusing to push."
exit 1
fi
- name: Set up Docker Buildx
if: steps.existing.outputs.exists == 'false'
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build and push linux/amd64 and linux/arm64
id: build
if: steps.existing.outputs.exists == 'false'
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: docker/cube-image
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}
${{ env.IMAGE }}:latest
labels: |
org.opencontainers.image.version=${{ steps.tag.outputs.tag }}
org.opencontainers.image.revision=${{ github.sha }}
- name: Install cosign
if: steps.existing.outputs.exists == 'false'
uses: sigstore/cosign-installer@3454372f43399081ed03b604cb2d021dabca52bb # v3.8.2
# Mirrors .github/actions/build-and-push-docker: sign every pushed tag by the digest it points at.
- name: Sign the image
if: steps.existing.outputs.exists == 'false'
shell: bash
env:
TAGS: |
${{ env.IMAGE }}:${{ steps.tag.outputs.tag }}
${{ env.IMAGE }}:latest
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
echo "${TAGS}" | sed '/^$/d' | xargs -I {} cosign sign --yes "{}@${DIGEST}"
# GitHub creates a new package as private, and a private image fails every install that pulls it
# without credentials. Check the tag the way a self-hoster pulls it, on every run, so a re-run after
# the first publish stays red until the package is made public.
- name: Check the image is publicly pullable
shell: bash
env:
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
DOCKER_CONFIG="$(mktemp -d)"
export DOCKER_CONFIG
if ! docker manifest inspect "$IMAGE:$TAG" > /dev/null; then
echo "::error::$IMAGE:$TAG cannot be pulled without credentials. Set the formbricks/cube package to public in its GitHub package settings, then re-run this job."
exit 1
fi
- name: Summary
if: steps.existing.outputs.exists == 'false'
shell: bash
env:
TAG: ${{ steps.tag.outputs.tag }}
DIGEST: ${{ steps.build.outputs.digest }}
run: echo "Published \`$IMAGE:$TAG\` (and \`latest\`) at \`$DIGEST\`." >> "$GITHUB_STEP_SUMMARY"