Skip to content

Commit 64f2783

Browse files
turegjorupclaude
andcommitted
Add E2E CI workflow on release branches
Static lint + config-sanity checks (markdown, yaml, sh, compose, tasks) catch a lot, but not "does the stack actually boot, run migrations, and serve /admin/?". Adds a GitHub Actions workflow that does exactly that on release-branch PRs. The workflow walks the localhost-cert install recipe end to end: 1. task env:init (domain prompt piped: os2display.localhost) 2. APP_ENV=prod sed (until upstream display-api-service ships PR #435 in a tagged release; the image's bundled .env defaults to dev mode but the image is built --no-dev) 3. task env:traefik (cert-file branch, prompts piped) 4. task dev:cert 5. install-secrets.sh + compose pull + compose up --wait 6. bin/console app:update + lexik:jwt:generate-keypair 7. bin/console app:tenant:add + app:user:add (direct compose exec — task install's nested task: subtasks don't pipe stdin reliably per B23) 8. curl /admin/ via --cacert traefik/ssl/dev.crt + --resolve. Asserts 200. 9. compose down --volumes (always-runs cleanup) Triggers on pull_request against release/** and push to release/**. Doesn't run on every PR — too slow (~5 min) and too much docker churn for the casual review cycle. Acts as the release-cut quality bar. Known fragility points documented in the workflow comments for the next operator who has to debug a flake: the APP_ENV workaround, GHCR rate limits, and the task-install stdin-piping caveat. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9c69f1 commit 64f2783

3 files changed

Lines changed: 136 additions & 3 deletions

File tree

.github/workflows/e2e.yaml

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
### ### E2E
2+
###
3+
### End-to-end test of the localhost-cert install recipe: bootstrap env
4+
### files via `task env:init`, generate the self-signed cert via
5+
### `task dev:cert`, bring the stack up, create a tenant + admin user,
6+
### and curl the admin route over HTTPS using the generated cert.
7+
###
8+
### Triggers on `pull_request` against `release/**` and pushes to
9+
### `release/**` only — too slow + too much docker churn to run on every
10+
### casual PR. Acts as the release-cut quality bar that the static
11+
### `markdown` / `yaml` / `sh` / `compose` / `tasks` workflows can't catch:
12+
### actual stack boot, Doctrine migrations, JWT keypair generation, and
13+
### Traefik routing.
14+
###
15+
### #### Known fragility points
16+
###
17+
### - `APP_ENV=dev` workaround: the upstream display-api-service image's
18+
### bundled .env still defaults to `APP_ENV=dev` until a 3.0.0-rc3+ tag
19+
### ships the fix from display-api-service#435. We sed it to `prod`
20+
### before bringing the stack up. Drop the override once a fixed tag
21+
### is in `OS2DISPLAY_VERSION_API`.
22+
### - `task install` chains `task: <subtask>` calls that don't reliably
23+
### pipe stdin to the Symfony console (B23 caveat). The workflow
24+
### sidesteps by running the install steps directly via
25+
### `docker compose exec`.
26+
### - GHCR pulls without auth: bot quota, can flake on heavy CI days.
27+
28+
name: E2E
29+
30+
on:
31+
pull_request:
32+
branches:
33+
- "release/**"
34+
push:
35+
branches:
36+
- "release/**"
37+
38+
jobs:
39+
install-localhost:
40+
runs-on: ubuntu-latest
41+
timeout-minutes: 15
42+
steps:
43+
- uses: actions/checkout@v6
44+
45+
- name: Install Task
46+
uses: arduino/setup-task@v2
47+
with:
48+
version: 3.x
49+
repo-token: ${{ secrets.GITHUB_TOKEN }}
50+
51+
- name: Bootstrap env files (domain prompt piped)
52+
# `task env:init` prompts for the public domain; pipe the localhost
53+
# default. The script also extracts .env.symfony from the API image,
54+
# auto-generates APP_SECRET + JWT_PASSPHRASE, and bumps serverVersion.
55+
run: printf 'os2display.localhost\n' | task env:init
56+
57+
- name: APP_ENV=prod workaround (upstream image bug)
58+
# The image's bundled .env defaults to APP_ENV=dev but the image is
59+
# built --no-dev, so dev-mode boot fails on a missing WebProfilerBundle.
60+
# Drop this step once the fix from display-api-service#435 ships
61+
# in a 3.0.0-rc3+ tag.
62+
run: sed -i 's|^APP_ENV=dev|APP_ENV=prod|' .env.symfony
63+
64+
- name: Configure traefik (cert-file branch, prompts piped)
65+
# Choice 2 = cert-file. Domain = traefik.localhost. LE email
66+
# skipped via blank line (cert-file branch ignores it). Cert
67+
# filename defaults accepted via blank lines. Dashboard
68+
# admin / password.
69+
run: |
70+
printf '2\ntraefik.localhost\n\n\nadmin\npassword\n' | task env:traefik
71+
72+
- name: Generate self-signed cert
73+
run: task dev:cert
74+
75+
- name: Bring stack up
76+
# `task install`'s chained `task: <subtask>` calls don't pipe
77+
# stdin reliably for tenant:add / user:add (B23 caveat), so
78+
# we step through manually below. install-secrets.sh
79+
# (auto-fills MARIADB_*=CHANGE_ME sentinels) gets called first
80+
# to match the real `task install` order.
81+
run: |
82+
./scripts/install-secrets.sh
83+
docker compose --env-file .env --env-file .env.traefik pull
84+
docker compose --env-file .env --env-file .env.traefik up --detach --wait
85+
86+
- name: Run database migrations
87+
run: |
88+
docker compose --env-file .env --env-file .env.traefik exec -T os2display \
89+
bin/console --no-interaction app:update
90+
91+
- name: Generate JWT keypair
92+
run: |
93+
docker compose --env-file .env --env-file .env.traefik exec -T os2display \
94+
bin/console lexik:jwt:generate-keypair --skip-if-exists
95+
96+
- name: Create tenant + admin user
97+
run: |
98+
docker compose --env-file .env --env-file .env.traefik exec -T os2display \
99+
bin/console app:tenant:add e2e 'E2E Tenant' 'CI'
100+
docker compose --env-file .env --env-file .env.traefik exec -T os2display \
101+
bin/console app:user:add admin@os2display.localhost e2epassword 'E2E Admin' admin e2e
102+
103+
- name: Smoke test — admin route over HTTPS
104+
# --resolve maps os2display.localhost → 127.0.0.1; --cacert trusts
105+
# the generated dev cert (covers os2display.localhost as a SAN).
106+
# -f makes curl exit non-zero on >=400.
107+
run: |
108+
curl -fv \
109+
--resolve os2display.localhost:443:127.0.0.1 \
110+
--cacert traefik/ssl/dev.crt \
111+
https://os2display.localhost/admin/ \
112+
-o /dev/null
113+
114+
- name: Tear down
115+
if: always()
116+
run: |
117+
docker compose --env-file .env --env-file .env.traefik down --volumes --remove-orphans

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ and aligned to the v3 image's env contract. **For 1.x → 3.x operators: see
5353
aggregate) — set-membership comparison of `task --list` against the README block. Section
5454
headings, descriptions, and alias notes in the README stay human-curated; only the SET of
5555
task names is checked. Caught the missing `dev:teardown` entry in the existing block.
56+
- New **`E2E`** workflow on release branches: bootstraps env files via `task env:init`,
57+
generates a self-signed cert via `task dev:cert`, brings the full stack up, runs Doctrine
58+
migrations, creates a tenant + admin user, and curls `/admin/` over HTTPS via the dev cert.
59+
Catches install-path regressions the static checks can't (image boot, migration replay,
60+
Traefik routing). Triggers on `pull_request` against `release/**` and pushes to
61+
`release/**` — too slow (~5 min wall-clock) for every casual PR.
5662
- `scripts/` directory: extracted helpers for the longer Taskfile bodies that the upstream
5763
[Taskfile style guide](https://taskfile.dev/styleguide/) recommends moving out
5864
("Prefer using external scripts instead of multi-line commands"). `host-resources.sh`,

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -939,14 +939,17 @@ file-copy header at the top of each lints config preserves provenance for sync.
939939

940940
### CI workflows
941941

942-
`.github/workflows/`, four files:
942+
`.github/workflows/`, five static checks plus a release-branch e2e:
943943

944944
- **`markdown.yaml`** — runs markdownlint via `docker compose --profile dev run --rm
945945
markdownlint`. Catches doc rot.
946946
- **`yaml.yaml`** — runs prettier via the same pattern. Catches YAML drift.
947947
- **`sh.yaml`** — runs shellcheck against `scripts/*.sh` via the `shellcheck` dev-profile
948948
service. Catches shell footguns in the extracted helpers (unquoted vars, masked exit
949949
codes, etc.).
950+
- **`tasks.yaml`** — runs `scripts/check-tasks-readme.sh`, asserting the set of task
951+
names in the README's "All tasks" reference block matches `task --list`. Section
952+
labels, descriptions, and alias notes stay human-curated; only membership is checked.
950953
- **`compose.yaml`** — three jobs:
951954
1. `compose-config` — synthesises the stack with example env files + a stub `.env.symfony`.
952955
Catches typos and dangling `${VAR}` references.
@@ -955,8 +958,15 @@ file-copy header at the top of each lints config preserves provenance for sync.
955958
3. `env-coverage` — every bare `${VAR}` reference in `docker-compose.yml` is declared in
956959
`.env.example` or `.env.traefik.example`. Catches the silent-empty
957960
substitution case.
958-
959-
All four trigger on `pull_request` and pushes to `main` / `develop` / `release/**`.
961+
- **`e2e.yaml`** — release-branch only (`pull_request` against `release/**`, push to
962+
`release/**`). Bootstraps env files via `task env:init`, generates a self-signed cert,
963+
brings the full stack up, runs migrations, creates a tenant + admin user, and curls
964+
`/admin/` over HTTPS via the cert. Catches install-path regressions the static checks
965+
can't (image boot, migration replay, Traefik routing). ~5 minutes wall-clock.
966+
967+
All static checks trigger on `pull_request` and pushes to `main` / `develop` /
968+
`release/**`. The e2e workflow runs on release branches only — too slow + too much docker
969+
churn for the casual review cycle.
960970
961971
---
962972

0 commit comments

Comments
 (0)