From a9bc7f6082a7a8eb1fc005748fdcbe406803ffcd Mon Sep 17 00:00:00 2001 From: intech Date: Tue, 4 Aug 2026 02:27:08 +0400 Subject: [PATCH 1/2] feat(getting-started): Dockerfiles and a container e2e workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quickstart had no Dockerfile, while the documentation described containerising a Connectum service — so the recommended container setup was never built, let alone run. It also meant `connectum init` handed users a project with no way to containerise it: the CLI clones this example and passes every file through except `pnpm-workspace.yaml` and `.pnpmfile.cjs`, so these Dockerfiles reach every scaffolded project too. Two Dockerfiles rather than one parameterised by build args: this example exists to be copied, and a file with five `ARG`s reads badly for that. Each is three stages — generate (`gen/` is not committed and `buf` is a devDependency), production dependencies, runtime — ending in a non-root user and an h2c-aware healthcheck. The new workflow closes a real gap. Everything in either repository exercises these services in-process; the existing e2e test opens a socket but keeps client and server together, so nothing covered the container itself. The scenario asserts response *bodies* against the documented contract: - the container reaches its own HEALTHCHECK - `/healthz` returns `SERVING` over h2c, and an unknown path returns 404 — the second assertion is what proves the probe is not the vacuous kind - reflection lists the greeter and health methods a client would look up - `SayHello` and `SayGoodbye` return the documented payloads - `grpc.health.v1.Health/Check` returns `SERVING` - SIGTERM as PID 1 exits 0 inside the grace window, rather than being SIGKILLed Both runtimes pass all eleven checks locally. The nightly schedule exists because the images install `@connectum/*` from npm at build time, so a published regression can break this without anything here changing. Two checks were written expecting behaviour this example does not have and were corrected rather than reported: reflection does not advertise itself, and the quickstart proto declares no constraints, so there is no validation path to assert. Not covered, named rather than silently dropped: the tsx execution model (tsx is a devDependency and there is no tsx Dockerfile), the other examples' images, and any broker-backed flow. --- .github/workflows/container-e2e.yml | 66 ++++++++++++++++++++++ getting-started/.dockerignore | 10 ++++ getting-started/Dockerfile | 45 +++++++++++++++ getting-started/Dockerfile.bun | 46 +++++++++++++++ getting-started/README.md | 21 +++++++ scripts/container-e2e.sh | 87 +++++++++++++++++++++++++++++ 6 files changed, 275 insertions(+) create mode 100644 .github/workflows/container-e2e.yml create mode 100644 getting-started/.dockerignore create mode 100644 getting-started/Dockerfile create mode 100644 getting-started/Dockerfile.bun create mode 100755 scripts/container-e2e.sh diff --git a/.github/workflows/container-e2e.yml b/.github/workflows/container-e2e.yml new file mode 100644 index 0000000..5c0bed8 --- /dev/null +++ b/.github/workflows/container-e2e.yml @@ -0,0 +1,66 @@ +name: Container E2E + +# Builds the quickstart image and runs the full scenario against it over the wire: +# HEALTHCHECK, /healthz, gRPC reflection, a real unary RPC, gRPC health, and SIGTERM as +# PID 1. Nothing else in either repository covers the container -- the existing e2e tests +# keep client and server in one process, so the production dependency closure (with +# devDependencies stripped), the healthcheck and signal handling are all untested there. +# +# `connectum init` clones getting-started as its base and passes every file through +# except `pnpm-workspace.yaml` and `.pnpmfile.cjs`, so these Dockerfiles are also what a +# scaffolded project gets -- testing them here tests what the CLI hands to users. +# +# NOT covered, named rather than silently dropped: the tsx execution model (tsx is a +# devDependency and there is no tsx Dockerfile), the other examples' images, and any +# broker-backed flow. + +on: + pull_request: + paths: + - "getting-started/**" + - "scripts/container-e2e.sh" + - ".github/workflows/container-e2e.yml" + push: + branches: [main] + paths: + - "getting-started/**" + - "scripts/container-e2e.sh" + schedule: + # The images install @connectum/* from npm at build time, so a published regression + # can break this without anything in the repository changing. + - cron: "0 3 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + container-e2e: + name: "e2e ${{ matrix.runtime.name }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + runtime: + - { name: node, dockerfile: Dockerfile } + - { name: bun, dockerfile: Dockerfile.bun } + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up buf (for `buf curl`) + uses: bufbuild/buf-action@fd21066df7214747548607aaa45548ba2b9bc1ff # v1 + with: + setup_only: true + + - name: Build the image + working-directory: getting-started + run: docker build -f ${{ matrix.runtime.dockerfile }} -t quickstart:${{ matrix.runtime.name }} . + + - name: Run the wire-level scenario + run: ./scripts/container-e2e.sh quickstart:${{ matrix.runtime.name }} ${{ matrix.runtime.name }} + + - name: Container logs on failure + if: failure() + run: docker ps -a --filter "name=e2e-" --format '{{.Names}}' | xargs -r -n1 docker logs --tail 100 diff --git a/getting-started/.dockerignore b/getting-started/.dockerignore new file mode 100644 index 0000000..13a68c5 --- /dev/null +++ b/getting-started/.dockerignore @@ -0,0 +1,10 @@ +node_modules +gen +tests +*.md +.gitignore +.pnpmfile.cjs +pnpm-lock.yaml +pnpm-workspace.yaml +Dockerfile* +.dockerignore diff --git a/getting-started/Dockerfile b/getting-started/Dockerfile new file mode 100644 index 0000000..906ec8f --- /dev/null +++ b/getting-started/Dockerfile @@ -0,0 +1,45 @@ +# The quickstart service, containerised. `engines.node` is >=25.2.0, so TypeScript +# runs directly via native type stripping -- no build step and no flags. +# +# `gen/` is not committed, and `buf` is a devDependency, so code generation happens in +# its own stage with the full dependency tree; the runtime image gets production +# dependencies plus the generated code, and never needs buf. +# +# The Bun variant of this file is Dockerfile.bun. Both are exercised by the +# `example-e2e` workflow in the connectum repository. + +# ── build: full dependencies, then generate the proto code ────────────────── +FROM node:25-slim AS build +WORKDIR /app +COPY package.json ./ +RUN npm install --no-audit --no-fund +COPY buf.yaml buf.gen.yaml ./ +COPY proto/ ./proto/ +RUN npx buf generate + +# ── deps: production dependencies only ────────────────────────────────────── +FROM node:25-slim AS deps +WORKDIR /app +COPY package.json ./ +RUN npm install --omit=dev --no-audit --no-fund + +# ── runtime ───────────────────────────────────────────────────────────────── +FROM node:25-slim AS runtime +# curl, not wget: this service serves plaintext h2c (`allowHTTP1: false`), and wget +# speaks HTTP/1.1 only. Against an h2c listener `wget --spider` gets an empty status +# line yet still exits 0, so it would report a dead or NOT_SERVING service as healthy. +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/gen ./gen +COPY package.json ./ +COPY src/ ./src/ +ENV NODE_ENV=production PORT=5000 +EXPOSE 5000 +# `-f` fails on a non-2xx status, which is what makes this a health check rather than a +# port check: /healthz answers 200 for SERVING and 503 for NOT_SERVING and UNKNOWN. +HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \ + CMD curl -fsS --http2-prior-knowledge http://localhost:${PORT:-5000}/healthz || exit 1 +# Run as the built-in non-root `node` user. +USER node +CMD ["node", "src/index.ts"] diff --git a/getting-started/Dockerfile.bun b/getting-started/Dockerfile.bun new file mode 100644 index 0000000..0bfeab5 --- /dev/null +++ b/getting-started/Dockerfile.bun @@ -0,0 +1,46 @@ +# The quickstart service on Bun. Bun transpiles TypeScript itself, so as with the Node +# variant there is no build step. +# +# Bun is used here as both the runtime and the package manager, which is the common +# pairing but not a requirement: `bun install` lays out an ordinary `node_modules`, and a +# project installed with npm runs under Bun unchanged. +# +# `gen/` is not committed, and `buf` is a devDependency, so code generation happens in +# its own stage with the full dependency tree; the runtime image gets production +# dependencies plus the generated code, and never needs buf. + +# ── build: full dependencies, then generate the proto code ────────────────── +FROM oven/bun:1-slim AS build +WORKDIR /app +COPY package.json ./ +RUN bun install +COPY buf.yaml buf.gen.yaml ./ +COPY proto/ ./proto/ +RUN bunx buf generate + +# ── deps: production dependencies only ────────────────────────────────────── +FROM oven/bun:1-slim AS deps +WORKDIR /app +COPY package.json ./ +RUN bun install --production + +# ── runtime ───────────────────────────────────────────────────────────────── +FROM oven/bun:1-slim AS runtime +# curl, not wget: this service serves plaintext h2c (`allowHTTP1: false`), and wget +# speaks HTTP/1.1 only. Against an h2c listener `wget --spider` gets an empty status +# line yet still exits 0, so it would report a dead or NOT_SERVING service as healthy. +RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=build /app/gen ./gen +COPY package.json ./ +COPY src/ ./src/ +ENV NODE_ENV=production PORT=5000 +EXPOSE 5000 +# `-f` fails on a non-2xx status, which is what makes this a health check rather than a +# port check: /healthz answers 200 for SERVING and 503 for NOT_SERVING and UNKNOWN. +HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \ + CMD curl -fsS --http2-prior-knowledge http://localhost:${PORT:-5000}/healthz || exit 1 +# Run as the built-in non-root `bun` user. +USER bun +CMD ["bun", "run", "src/index.ts"] diff --git a/getting-started/README.md b/getting-started/README.md index 30feaf5..b10a48c 100644 --- a/getting-started/README.md +++ b/getting-started/README.md @@ -47,6 +47,27 @@ pnpm start:bun # Bun pnpm start:tsx # tsx ``` +## In a container + +Two Dockerfiles, one per runtime. Both generate the proto code during the build (`gen/` +is not committed and `buf` is a devDependency), then ship production dependencies only: + +```bash +docker build -t quickstart . # Node.js +docker build -f Dockerfile.bun -t quickstart . # Bun + +docker run --rm -p 5000:5000 quickstart +curl -fsS --http2-prior-knowledge http://localhost:5000/healthz +``` + +The probe needs `--http2-prior-knowledge` because the service is plaintext h2c +(`allowHTTP1: false`); `wget` cannot see it at all and would report a dead service as +healthy. + +`scripts/container-e2e.sh` runs the full scenario against a built image — healthcheck, +`/healthz`, reflection, a real RPC, gRPC health and SIGTERM as PID 1 — and CI runs it for +both runtimes. + ## Next - [hris](../hris/) — the same codebase running as a monolith **or** as diff --git a/scripts/container-e2e.sh b/scripts/container-e2e.sh new file mode 100755 index 0000000..90d3a5e --- /dev/null +++ b/scripts/container-e2e.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Full wire-level scenario against a containerised quickstart service. +# +# Everything else in this repository exercises the services in-process: the e2e tests +# open a real socket, but client and server share one process, so nothing covers the +# container itself -- the HEALTHCHECK, the production dependency closure with +# devDependencies stripped, or SIGTERM handling as PID 1. +# +# The probes run from the host against the published port and assert response *bodies* +# against the documented contract, not merely that a call did not fail. +# +# Requires: docker, curl, and `buf` on PATH (for `buf curl`). +# Usage: scripts/container-e2e.sh