Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 68 additions & 5 deletions .github/actions/docker-smoke/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@
# under its real hostname (e.g. Vite's preview `allowedHosts` rejecting an
# unlisted Host with 403) — caught by curling with `Host: <host>`.
#
# Used as a STEP inside a job named `ci` so the required-status-check context
# stays exactly `ci` (a reusable workflow would produce `ci / <job>` and break
# the org ruleset's required-check match).
# Optionally, `smoke-command` runs a deeper check against the running container
# (e.g. a crawl of every page), and `fail-on-log-pattern` fails on server log
# lines such as a missing module that a page may swallow while still answering
# 200. Whenever a step fails, the last 200 lines of the container log are
# printed.
#
# Used as a STEP inside the job whose name is the required status check (`ci`
# under the org ruleset) so the context stays exactly that name (a reusable
# workflow would produce `ci / <job>` and break the required-check match).
name: Docker image smoke test
description: Build the Docker image, run it, and assert it serves the production Host with the expected HTTP status.

Expand Down Expand Up @@ -50,6 +56,22 @@ inputs:
the Dockerfile needs) without requiring the container to serve.
required: false
default: "false"
smoke-command:
description: >-
Shell command run with bash after the Host-header probe succeeds, from the
workspace root, with SMOKE_BASE_URL=http://localhost:<port> in its
environment. A non-zero exit fails the action. Empty (the default) skips
it. Ignored when build-only is "true".
required: false
default: ""
fail-on-log-pattern:
description: >-
Extended regex (grep -E). After the probe and smoke-command, the action
fails if the container log matches it, e.g.
"MODULE_NOT_FOUND|Cannot find module". Empty (the default) skips the
check. Ignored when build-only is "true".
required: false
default: ""

runs:
using: composite
Expand Down Expand Up @@ -88,10 +110,51 @@ runs:
sleep 3
done
echo "::error::Smoke test failed: got HTTP ${code} for Host: ${SMOKE_HOST}${SMOKE_PATH} (expected ${SMOKE_EXPECTED})"
echo "=== container logs ==="
docker logs ci-smoke || true
exit 1

# The command is caller-supplied, so it goes through `env` and is run by an
# inner bash, never spliced into this script body.
- name: Run smoke command
if: ${{ inputs.build-only != 'true' && inputs.smoke-command != '' }}
shell: bash
env:
SMOKE_COMMAND: ${{ inputs.smoke-command }}
SMOKE_BASE_URL: http://localhost:${{ inputs.port }}
run: bash -eo pipefail -c "$SMOKE_COMMAND"

- name: Check container log
if: ${{ inputs.build-only != 'true' && inputs.fail-on-log-pattern != '' }}
shell: bash
env:
SMOKE_LOG_PATTERN: ${{ inputs.fail-on-log-pattern }}
run: |
set -uo pipefail
if ! logs=$(docker logs ci-smoke 2>&1); then
echo "::error::Could not read the container log"
exit 1
fi
# The runner's bash has -e, so capture grep's status (1 = no match)
# without letting it end the script.
matches=$(printf '%s\n' "$logs" | grep -E -- "$SMOKE_LOG_PATTERN") && rc=0 || rc=$?
if [ "$rc" -eq 0 ]; then
printf '%s\n' "$matches"
echo "::error::The container log matches fail-on-log-pattern: ${SMOKE_LOG_PATTERN}"
exit 1
elif [ "$rc" -ne 1 ]; then
echo "::error::grep failed on fail-on-log-pattern: ${SMOKE_LOG_PATTERN}"
exit 1
fi
echo "✓ No container log line matches: ${SMOKE_LOG_PATTERN}"

- name: Container logs
if: ${{ failure() && inputs.build-only != 'true' }}
shell: bash
run: |
if docker container inspect ci-smoke >/dev/null 2>&1; then
echo "=== last 200 lines of the container log ==="
docker logs --tail 200 ci-smoke 2>&1 || true
fi

- name: Tear down container
if: ${{ always() && inputs.build-only != 'true' }}
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ uses: WemoveEU/ci-workflows/.github/workflows/docker-build.yml@v14
- **deploy-strapi.yml** — Orchestrates a Strapi deploy by calling `docker-build.yml` twice: backend first (`backend/Dockerfile`, image `<repo>/backend`), then frontend (`frontend/Dockerfile`, image `<repo>/frontend`). Between them, `wait-for-backend` polls the live backend until ready.
- **notify.yml** — Posts a Slack notification (green on success, red on failure) via `slackapi/slack-github-action`. Requires the `slack_webhook_url` secret. Call it as a final step from a caller workflow.
- **python-build.yml** — Builds a Python package with `uv` (`astral-sh/setup-uv` + `uv build`).
- **docker-smoke** (`.github/actions/docker-smoke`) — Composite action (not a reusable workflow). Builds the Docker image, runs it, and probes it with the production `Host` header to catch build breaks and hostname-dependent serving failures. Used as a step inside a job named `ci` (so the required-status-check context stays exactly `ci`). Supports `build-only` for apps that can't boot standalone in CI.
- **docker-smoke** (`.github/actions/docker-smoke`) — Composite action (not a reusable workflow). Builds the Docker image, runs it, and probes it with the production `Host` header to catch build breaks and hostname-dependent serving failures. Used as a step inside a job named `ci` (so the required-status-check context stays exactly `ci`). Supports `build-only` for apps that can't boot standalone in CI, and optional `smoke-command` / `fail-on-log-pattern` for deeper checks after the probe.

## Architecture Notes (the non-obvious parts)

Expand Down
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,13 @@ image-build breaks (e.g. a base-image bump dropping a tool the Dockerfile needs)
runtime serving breaks that only appear under the real hostname (e.g. a preview server's
`allowedHosts` rejecting an unlisted Host with 403).

Use it as a **step** inside a job named `ci` so the required-status-check context stays
exactly `ci` (a reusable workflow would produce `ci / <job>` and break an org ruleset's
required-check match).
Use it as a **step** inside the job whose name is the required status check (`ci` under the
org ruleset) so the context stays exactly that name (a reusable workflow would produce
`ci / <job>` and break the required-check match).

With `smoke-command` it also runs a deeper check against the running container, and with
`fail-on-log-pattern` it fails on server log lines that a page can swallow while still
answering 200. Whenever a step fails, the last 200 lines of the container log are printed.

### Inputs

Expand All @@ -246,6 +250,8 @@ required-check match).
- **context**: Docker build context. Default `.`.
- **startup-timeout**: Seconds to wait for the container to start responding. Default `90`.
- **build-only**: When `"true"`, only build the image and skip running/probing it. Use for apps that can't boot standalone in CI (need runtime secrets, a database, etc.). Default `"false"`.
- **smoke-command**: Shell command run with bash after the probe succeeds, from the workspace root, with `SMOKE_BASE_URL=http://localhost:<port>` in its environment. A non-zero exit fails the action. Default `""` (skipped).
- **fail-on-log-pattern**: Extended regex (`grep -E`). After the probe and `smoke-command`, the action fails if the container log matches it, e.g. `"MODULE_NOT_FOUND|Cannot find module"`. Default `""` (skipped).

### Sample usage

Expand All @@ -261,13 +267,28 @@ jobs:
port: "3000"
```

With a smoke script and a log check (a Next.js frontend in `frontend/`):

```yaml
- uses: WeMoveEU/ci-workflows/.github/actions/docker-smoke@v14
with:
dockerfile: frontend/Dockerfile
context: frontend
host: example.org
port: "3000"
path: /en
smoke-command: node frontend/scripts/smoke-test.mjs "$SMOKE_BASE_URL"
fail-on-log-pattern: "MODULE_NOT_FOUND|Cannot find module"
```

---

## Releases

Version tags `v1`–`v13` predate the current scheme and are frozen point releases. The
floating-major convention (see [`RELEASING.md`](RELEASING.md)) starts at **`v14`**.

- **v14.8** — `docker-smoke`: optional `smoke-command` (runs after the probe with `SMOKE_BASE_URL` set; its failure fails the action) and `fail-on-log-pattern` (fails when the container log matches an extended regex). The last 200 lines of the container log are now printed when any step fails, not only the probe. Existing callers are unaffected.
- **v14.6** — `pin-override` action: a first pass re-resolves every alerted package inside the ranges the tree already declares (`yarn up -R <pkg>`, lockfile only, manifest untouched) before any resolution is written. This is what clears a package installed at several versions, which Dependabot cannot fix, and a fix a hand-closed Dependabot PR taught Dependabot to ignore. Measured across the fleet on 2026-09-11: 220 audit findings to 102. New `bumped` output; `changed` is true when either pass changed something.
- **v14.5** — `pin-override` composite action: writes Yarn `resolutions` for the transitive advisories Dependabot cannot fix when a parent pins the vulnerable package exactly.
- **v14.2** — `dependabot-auto-merge.yml`: arming is now retried (5 attempts) with a direct-merge fallback, so a transient GraphQL error no longer strands a PR and an already-green PR still merges. Consolidates logic that existed only in the inline copies in wemove.eu, youmove, wemove-charity.eu and pubstatic, which this lets them drop.
Expand Down
Loading