Skip to content

Commit 16909e3

Browse files
authored
Merge pull request #29 from os2display/feature/e2e-workflow
B29: E2E CI workflow on release branches
2 parents b9c69f1 + be4550d commit 16909e3

6 files changed

Lines changed: 189 additions & 11 deletions

File tree

.github/workflows/e2e.yaml

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
### ### E2E
2+
###
3+
### End-to-end test of the localhost-cert install recipe, walked
4+
### through the supplied tasks the same way an operator would: bootstrap
5+
### env files via `task env:init`, configure traefik via
6+
### `task env:traefik`, generate a self-signed cert via `task dev:cert`,
7+
### bring the stack up via `task up`, run migrations + JWT + tenant +
8+
### admin user via `task console`, install templates via
9+
### `task templates:install`, and tear down via `task dev:teardown`.
10+
### Curls `/admin/` over HTTPS using the generated cert as the success
11+
### gate.
12+
###
13+
### Triggers on `pull_request` against `release/**` and pushes to
14+
### `release/**` only — too slow + too much docker churn to run on every
15+
### casual PR. Acts as the release-cut quality bar that the static
16+
### `markdown` / `yaml` / `sh` / `compose` / `tasks` workflows can't catch:
17+
### actual stack boot, Doctrine migrations, JWT keypair generation, and
18+
### Traefik routing.
19+
###
20+
### #### Known fragility points
21+
###
22+
### - **`APP_ENV=dev` upstream image bug**: the bundled `/app/.env` still
23+
### defaults to `APP_ENV=dev` until a 3.0.0-rc3+ tag ships the fix
24+
### from display-api-service#435. We sed it to `prod` after `task
25+
### env:init`. Drop the workaround once a fixed image tag is in
26+
### `OS2DISPLAY_VERSION_API`.
27+
### - **`task install` is interactive**: chained `task: <subtask>` calls
28+
### don't reliably pipe stdin to the Symfony console for tenant:add
29+
### and user:add (B23 caveat). We walk the install steps individually
30+
### via `task console -- ARGS` (which is non-interactive when the
31+
### bin/console subcommand accepts CLI args) instead of calling
32+
### `task install` directly.
33+
### - **`scripts/install-secrets.sh` runs as the first step of `task
34+
### install`**: not currently a standalone task, so we invoke the
35+
### script directly. It's tracked under `scripts/` and shellcheck-
36+
### validated; equivalent to running task install's secrets-fill
37+
### step in isolation.
38+
### - **GHCR pulls without auth**: bot quota, can flake on heavy CI days.
39+
40+
name: E2E
41+
42+
on:
43+
pull_request:
44+
branches:
45+
- "release/**"
46+
push:
47+
branches:
48+
- "release/**"
49+
50+
jobs:
51+
install-localhost:
52+
runs-on: ubuntu-latest
53+
timeout-minutes: 15
54+
steps:
55+
- uses: actions/checkout@v6
56+
57+
- name: Install Task
58+
uses: arduino/setup-task@v2
59+
with:
60+
version: 3.x
61+
repo-token: ${{ secrets.GITHUB_TOKEN }}
62+
63+
- name: task env:init (domain prompt piped)
64+
run: printf 'os2display.localhost\n' | task env:init
65+
66+
- name: APP_ENV=prod workaround (upstream image bug — see header)
67+
run: sed -i 's|^APP_ENV=dev|APP_ENV=prod|' .env.symfony
68+
69+
- name: task env:traefik (cert-file branch, prompts piped)
70+
# Choice 2 = cert-file. Domain = traefik.localhost. LE email blank
71+
# (cert-file branch ignores it). Cert filename defaults accepted
72+
# via blank lines. Dashboard admin / password.
73+
run: printf '2\ntraefik.localhost\n\n\nadmin\npassword\n' | task env:traefik
74+
75+
- name: task dev:cert
76+
run: task dev:cert
77+
78+
- name: install-secrets (would be `task install`'s first step)
79+
run: ./scripts/install-secrets.sh
80+
81+
- name: Pre-create bind-mount dirs with container-writable perms
82+
# The os2display container writes JWT keys as UID 1042 (`deploy`)
83+
# per README's deploy-host UID/GID contract, and uploads to
84+
# ./media as the same user. CI runners aren't the operator's
85+
# 1042-group host, so chown the bind-mount sources to match.
86+
run: |
87+
mkdir -p jwt media backup
88+
sudo chown -R 1042:1042 jwt media backup
89+
90+
- name: task up
91+
# `task up` uses `compose up -d --wait`, so this blocks until every
92+
# service's healthcheck passes. Migrations on the next step can
93+
# connect immediately.
94+
run: task up
95+
96+
- name: task console -- app:update (Doctrine migrations)
97+
run: task console -- --no-interaction app:update
98+
99+
- name: task console -- lexik:jwt:generate-keypair
100+
run: task console -- lexik:jwt:generate-keypair --skip-if-exists
101+
102+
- name: task console -- app:tenant:add
103+
run: task console -- app:tenant:add e2e 'E2E Tenant' CI
104+
105+
- name: task console -- app:user:add
106+
run: task console -- app:user:add admin@os2display.localhost e2epassword 'E2E Admin' admin e2e
107+
108+
- name: task templates:install
109+
run: task templates:install
110+
111+
- name: Smoke test — admin route over HTTPS
112+
# --resolve maps os2display.localhost → 127.0.0.1; --cacert trusts
113+
# the generated dev cert (covers os2display.localhost as a SAN).
114+
# -f makes curl exit non-zero on >=400.
115+
run: |
116+
curl -fv \
117+
--resolve os2display.localhost:443:127.0.0.1 \
118+
--cacert traefik/ssl/dev.crt \
119+
https://os2display.localhost/admin/ \
120+
-o /dev/null
121+
122+
- name: task dev:teardown
123+
if: always()
124+
run: task -y dev:teardown

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,27 @@ 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.
62+
- **`task up` / `install` / `update` now use `compose up --wait`** — blocks until every
63+
service's healthcheck passes before returning, instead of returning immediately and
64+
leaving the operator to poll. Matches the operator mental-model "after `task up` the
65+
stack is up". Replaces `task install`'s previous `sleep 20` hack.
66+
- **`docker-compose.yml` `depends_on` graph filled in.** B6 added healthchecks to every
67+
service but only wired one dependency (`nginx-api → os2display: service_healthy`).
68+
Migrations on a fresh install raced the database — `task install` papered over with a
69+
20-second sleep. Now:
70+
- `os2display` waits for `mariadb` (`service_healthy`, `required: false` so the dep is
71+
skipped when an operator runs against an external db / drops the `mariadb` profile)
72+
and `redis` (`service_healthy`).
73+
- `traefik` waits for `socket-proxy` (`service_healthy`) — traefik queries socket-proxy
74+
for docker-label discovery, no point asking before the proxy is reachable.
75+
Combined with `--wait` above, `task up` returns when the stack is genuinely ready, with
76+
startup ordering enforced at the container level rather than via wall-clock guesswork.
5677
- `scripts/` directory: extracted helpers for the longer Taskfile bodies that the upstream
5778
[Taskfile style guide](https://taskfile.dev/styleguide/) recommends moving out
5879
("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.
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.
958966
959-
All four trigger on `pull_request` and pushes to `main` / `develop` / `release/**`.
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
@@ -968,7 +978,7 @@ All four trigger on `pull_request` and pushes to `main` / `develop` / `release/*
968978
Lifecycle
969979
install Install the project — first-time setup (interactive)
970980
update Pull images, recreate containers, run app:update
971-
up Start the stack without recreating containers
981+
up Start the stack; blocks until healthchecks pass
972982
down Remove all containers (preserves named volumes)
973983
stop Stop all containers
974984
purge Remove all containers AND named volumes (prompts)

Taskfile.yml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ tasks:
3131
- echo "Installing"
3232
- ./scripts/install-secrets.sh
3333
- "{{.COMPOSE}} pull"
34-
- "{{.COMPOSE}} up --force-recreate --detach --remove-orphans"
35-
- echo "Waiting for database to be ready"
36-
- sleep 20
34+
- "{{.COMPOSE}} up --force-recreate --detach --remove-orphans --wait"
3735
- echo "Run database migrations and required updates"
3836
- task: console
3937
vars: { CLI_ARGS: "app:update" }
@@ -55,18 +53,18 @@ tasks:
5553
msg: ".env.symfony missing — run 'task env:init' first."
5654
cmds:
5755
- "{{.COMPOSE}} pull"
58-
- "{{.COMPOSE}} up --force-recreate --detach --remove-orphans"
56+
- "{{.COMPOSE}} up --force-recreate --detach --remove-orphans --wait"
5957
- task: console
6058
vars: { EXEC_FLAGS: "--user deploy", CLI_ARGS: "app:update" }
6159
- echo "NB! Run 'task templates:install' if the new image added templates."
6260

6361
up:
64-
desc: Start the stack without recreating containers
62+
desc: Start the stack without recreating containers; blocks until healthchecks pass
6563
preconditions:
6664
- sh: "[ -f .env.symfony ]"
6765
msg: ".env.symfony missing — run 'task env:init' first."
6866
cmds:
69-
- "{{.COMPOSE}} up -d"
67+
- "{{.COMPOSE}} up -d --wait"
7068

7169
down:
7270
desc: Remove all containers (preserves named volumes)

docker-compose.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ services:
4747
required: true
4848
- path: .env.php.local
4949
required: false
50+
depends_on:
51+
# mariadb is profile-gated (`mariadb`). `required: false` lets compose
52+
# skip this dep when an operator runs against an external database.
53+
mariadb:
54+
condition: service_healthy
55+
required: false
56+
redis:
57+
condition: service_healthy
5058
healthcheck:
5159
test: ["CMD", "php", "-r", "exit(@fsockopen('127.0.0.1', 9000) ? 0 : 1);"]
5260
interval: 30s
@@ -147,6 +155,12 @@ services:
147155
required: true
148156
- path: .env.traefik.local
149157
required: false
158+
depends_on:
159+
# Traefik queries the docker socket via socket-proxy for label
160+
# discovery; wait for the proxy to pass its /version healthcheck
161+
# before traefik starts asking it for routes.
162+
socket-proxy:
163+
condition: service_healthy
150164
ports:
151165
- "80:80"
152166
- "443:443"

scripts/install-secrets.sh

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,19 @@ if [ ! -f .env.mariadb ] || [ ! -f .env.symfony ]; then
1818
exit 1
1919
fi
2020

21-
# Sentinel detection via env vars (loaded by Taskfile's `dotenv:` directive).
22-
# If neither password is the CHANGE_ME placeholder, there's nothing to do.
21+
# Source .env.mariadb so $MARIADB_PASSWORD etc. are set whether the
22+
# script runs via `task install` (Taskfile's `dotenv:` already loaded it)
23+
# or stand-alone (CI workflow, manual invocation). Without this, an empty
24+
# env defeats the sentinel check below — both `${VAR:-}` evaluate to
25+
# empty (≠ "CHANGE_ME"), so the script silently exits 0 and mariadb
26+
# starts with the literal CHANGE_ME password from the file.
27+
set -a
28+
# shellcheck disable=SC1091
29+
. ./.env.mariadb
30+
set +a
31+
32+
# Sentinel detection. If neither password is the CHANGE_ME placeholder,
33+
# there's nothing to do.
2334
if [ "${MARIADB_PASSWORD:-}" != "CHANGE_ME" ] && [ "${MARIADB_ROOT_PASSWORD:-}" != "CHANGE_ME" ]; then
2435
exit 0
2536
fi

0 commit comments

Comments
 (0)