-
Notifications
You must be signed in to change notification settings - Fork 2
138 lines (127 loc) · 6.79 KB
/
Copy pathci-compose-validate.yml
File metadata and controls
138 lines (127 loc) · 6.79 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
name: Validate Compose
# The self-hosted stack (docker/self-host/) composes the reference deployment's
# service definitions via `include`, so a change to docker/compose.*.yaml can
# break a stranger's install without touching a single self-host file. Rendering
# both stacks here turns interpolation- and merge-level breakage into a red check
# instead of a bad first boot. (Semantic breakage — a renamed service silently
# joining the stack, an inherited runtime bug — still needs a real boot.)
#
# Runs on every PR (not just docker/ changes) so it can be a required check,
# matching how verify-changesets.yml is wired. It is cheap: only `docker compose
# config`, no image pulls.
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
concurrency:
group: validate-compose-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
validate:
name: "Render compose stacks"
timeout-minutes: 15
runs-on: ubuntu-latest
env:
COMPOSE_ENV_FILES: .env,release-lock.env
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Render the self-hosted stack
working-directory: docker/self-host
run: |
set -euo pipefail
# Fill the required values the way an operator would; `docker compose
# config` fails on any variable the stack requires but .env.example
# does not carry, which is exactly the drift we want to catch.
cp .env.example .env
sed -i \
-e 's|^APP_HOSTNAME=$|APP_HOSTNAME=hephaestus.example.com|' \
-e 's|^ACME_EMAIL=$|ACME_EMAIL=operator@example.com|' \
-e 's|^POSTGRES_PASSWORD=$|POSTGRES_PASSWORD=ci-not-a-real-password|' \
-e 's|^HEPHAESTUS_SECURITY_ENCRYPTION_KEY=$|HEPHAESTUS_SECURITY_ENCRYPTION_KEY=0123456789abcdef0123456789abcdef|' \
-e 's|^HEPHAESTUS_AUTH_STATE_COOKIE_KEY=$|HEPHAESTUS_AUTH_STATE_COOKIE_KEY=Y2ktbm90LWEtcmVhbC1zdGF0ZS1jb29raWUta2V5|' \
-e 's|^WEBHOOK_SECRET=$|WEBHOOK_SECRET=ci000000000000000000000000000000000|' \
-e 's|^NATS_USERNAME=$|NATS_USERNAME=ci|' \
-e 's|^NATS_PASSWORD=$|NATS_PASSWORD=ci-not-a-real-password|' \
.env
while IFS= read -r image; do
name="HEPHAESTUS_IMAGE_${image^^}"
name=${name//-/_}
printf '%s=%s@sha256:%s\n' "$name" "example.invalid/${image}" "$(printf '0%.0s' {1..64})" >> release-lock.env
done < <(jq -r '.images[], .upstream[].name' ../../security/release-images.json)
printf 'IMAGE_TAG=0.0.0\nHEPHAESTUS_RELEASE=v0.0.0\nHEPHAESTUS_RELEASE_COMMIT=%s\n' \
"$(printf '0%.0s' {1..40})" >> release-lock.env
docker compose config > /dev/null
echo "Rendered services:"
docker compose config --services | sort
- name: Fail on unset variables
working-directory: docker/self-host
run: |
set -euo pipefail
# `config` only warns about variables missing from .env; a warning here
# means .env.example has fallen behind the stack it renders.
if docker compose config 2>&1 >/dev/null | grep "variable is not set"; then
echo "::error::docker/self-host/.env.example is missing variables the stack references (see warnings above)"
exit 1
fi
- name: Assert the merged stack matches single-host intent
working-directory: docker/self-host
run: |
set -euo pipefail
services=$(docker compose config --services)
for unwanted in application-worker maintenance; do
grep -qx "$unwanted" <<< "$services" && {
echo "::error::'$unwanted' is a reference-deployment service and must not run on a single host"; exit 1; } || true
done
for required in application-server webhook-server postgres nats-server webapp reverse-proxy; do
grep -qx "$required" <<< "$services" || {
echo "::error::'$required' is missing from the self-hosted stack"; exit 1; }
done
# Compose 2.21-2.23 parse `!override` but silently ignore it, which would
# publish the reference's dashboard port and keep the maintainers' ACME
# email. Assert the merged result rather than trust the runner's version.
rendered=$(docker compose config)
for required in \
'entrypoints.https.http.middlewares=security-headers@docker' \
'request-body-limit@docker' \
'providers.file.filename=/etc/traefik/dynamic.yml' \
'contentsecuritypolicy' \
'default-src' \
'stsseconds' \
'2592000' \
'maxrequestbodybytes' \
'26214400' \
'minversion' \
'versiontls12'; do
grep -Fiq "$required" <<< "$rendered" || {
echo "::error::runtime edge envelope is missing '$required'"; exit 1; }
done
edge_csp=$(sed -n 's/.*contentSecurityPolicy=\(.*\)"/\1/p' ../compose.proxy.yaml)
selfhost_csp=$(sed -n 's/.*contentSecurityPolicy=\(.*\)"/\1/p' compose.single-host.yaml)
nginx_csp=$(sed -n 's/add_header Content-Security-Policy "\(.*\)" always;/\1/p' ../../webapp/docker/security-headers.conf)
[ "$edge_csp" = "$nginx_csp" ] || {
echo "::error::Traefik and nginx Content-Security-Policy values differ"; exit 1; }
[ "$edge_csp" = "$selfhost_csp" ] || {
echo "::error::reference and single-host Content-Security-Policy values differ"; exit 1; }
grep -q "admin@tum.de" <<< "$rendered" && {
echo "::error::the maintainers' ACME email survived the override — Compose is too old to honour !override"; exit 1; } || true
published=$(docker compose config --format json \
| jq -r '.services["reverse-proxy"].ports[].published' | sort -n | tr '\n' ' ')
[ "$published" = "80 443 " ] || {
echo "::error::reverse-proxy publishes '$published', expected '80 443 ' — !override was ignored"; exit 1; }
- name: Set up the repository's Node.js and pnpm versions
uses: ./.github/actions/setup-node-pnpm
with:
install: "none"
- name: Render the preview stack and assert it stays sandboxed
run: pnpm run check:preview-stack
- name: Render the reference deployment
working-directory: docker
run: |
set -euo pipefail
# Values mirror what the deploy workflow supplies; only proves the files
# still render, not that they are correct for production.
docker compose \
-f compose.proxy.yaml -f compose.core.yaml -f compose.app.yaml \
--env-file self-host/.env --env-file self-host/release-lock.env config --quiet