Skip to content

feat(getting-started): Dockerfiles and a container e2e workflow - #42

Merged
intech merged 2 commits into
mainfrom
feat/container-e2e
Aug 3, 2026
Merged

feat(getting-started): Dockerfiles and a container e2e workflow#42
intech merged 2 commits into
mainfrom
feat/container-e2e

Conversation

@intech

@intech intech commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Two gaps this closes

The quickstart had no Dockerfile — while the documentation described containerising a Connectum service. The recommended 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.

Nothing tested a container. Everything in either repository exercises these services in-process — the existing e2e test opens a real socket but keeps client and server together — so the HEALTHCHECK, the production dependency closure with devDependencies stripped, and SIGTERM handling as PID 1 were all untested.

The Dockerfiles

Two files rather than one parameterised by build args: this example exists to be copied, and a file with five ARGs 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 scenario

scripts/container-e2e.sh runs from the host against the published port and asserts response bodies against the documented contract, not merely that a call did not fail:

# Check
1 the container reaches its own HEALTHCHECK
2 /healthz returns SERVING over h2c
3 an unknown path returns 404 — this is what proves the probe is not the vacuous kind
4 reflection lists the greeter and health methods a client would look up
5 SayHello returns "Hello, Ada!"
6 grpc.health.v1.Health/Check returns SERVING
7 SayGoodbye returns "Goodbye, Ada!"
8 SIGTERM as PID 1 exits 0 inside the grace window, rather than being SIGKILLed

Both runtimes pass all eleven assertions locally (Node and Bun images built and run).

The nightly schedule exists because the images install @connectum/* from npm at build time, so a published regression can break this without anything in the repository changing.

Honesty notes

Two checks were written expecting behaviour this example does not have, and were corrected rather than reported as findings: gRPC reflection does not advertise itself (normal), and the quickstart proto declares no constraints and the package has no validation dependency, 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.

This is the first test workflow in this repository — until now it only had auto-label.

Summary by CodeRabbit

  • New Features

    • Added production-ready Docker images for the getting-started service using Node.js and Bun.
    • Added health checks, non-root runtime execution, and HTTP/2 support.
    • Added container end-to-end validation covering health, gRPC behavior, graceful shutdown, and error responses.
  • Documentation

    • Added instructions for building, running, and testing the Node.js and Bun containers.
  • Tests

    • Added automated container testing in continuous integration, including scheduled and manual runs.

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-actions github-actions Bot added the type:feature New feature or enhancement request label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@intech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce78e979-4c0a-4f07-83b7-9f22fee58b22

📥 Commits

Reviewing files that changed from the base of the PR and between a9bc7f6 and 50f8481.

📒 Files selected for processing (3)
  • getting-started/Dockerfile
  • getting-started/Dockerfile.bun
  • scripts/container-e2e.sh
📝 Walkthrough

Walkthrough

Added Node and Bun Docker images for the getting-started service, a container E2E script, container usage documentation, and scheduled or event-driven GitHub Actions coverage.

Changes

Container E2E Coverage

Layer / File(s) Summary
Build Node and Bun runtime images
getting-started/.dockerignore, getting-started/Dockerfile, getting-started/Dockerfile.bun
The multi-stage images generate protobuf code, install production dependencies, expose port 5000, define health checks, and run as non-root users.
Validate container behavior
scripts/container-e2e.sh
The script checks health, h2c HTTP behavior, gRPC reflection, RPC responses, gRPC health, graceful shutdown, and cleanup.
Run and document container E2E checks
.github/workflows/container-e2e.yml, getting-started/README.md
The workflow builds and tests Node and Bun images on pull requests, pushes, schedules, and manual dispatch. The README documents container usage and coverage.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant ContainerE2E as container-e2e.sh
  participant Docker
  participant Service as getting-started service
  GitHubActions->>Docker: build Node or Bun image
  GitHubActions->>ContainerE2E: run wire-level E2E test
  ContainerE2E->>Docker: start image
  Docker->>Service: launch service
  ContainerE2E->>Service: validate health, HTTP/2, reflection, RPC, and gRPC health
  ContainerE2E->>Docker: send SIGTERM
  Docker-->>ContainerE2E: return exit code 0
  GitHubActions-->>GitHubActions: print logs if the test fails
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: Dockerfiles for the getting-started example and a container end-to-end workflow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/container-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@getting-started/.dockerignore`:
- Line 7: Stop excluding pnpm-lock.yaml in getting-started/.dockerignore, and
update both dependency-install stages in getting-started/Dockerfile (lines 14-15
and 23-24) and getting-started/Dockerfile.bun (lines 15-16 and 24-25) to use the
project lockfile with frozen pnpm installs for full and production dependencies.

In `@scripts/container-e2e.sh`:
- Around line 24-25: Update scripts/container-e2e.sh lines 24-25 so cleanup
preserves the e2e-* container on failure, or captures its logs before removal;
retain cleanup on successful exits. Update .github/workflows/container-e2e.yml
lines 64-66 to collect logs from the preserved container during failure
handling, then remove it if needed.
- Around line 78-83: Update the shutdown checks after docker stop in
scripts/container-e2e.sh to capture its result and verify the container’s
.State.Status is exited before evaluating exit_code or marking shutdown checks
successful. Treat a failed stop or non-exited status as a failure, and only
accept the existing exit-code and elapsed-time assertions after that validation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41a3933a-b36c-4110-9491-01214002ba98

📥 Commits

Reviewing files that changed from the base of the PR and between 6b8b306 and a9bc7f6.

📒 Files selected for processing (6)
  • .github/workflows/container-e2e.yml
  • getting-started/.dockerignore
  • getting-started/Dockerfile
  • getting-started/Dockerfile.bun
  • getting-started/README.md
  • scripts/container-e2e.sh

*.md
.gitignore
.pnpmfile.cjs
pnpm-lock.yaml

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files of interest =="
git ls-files | rg '(^|/)getting-started/(Dockerfile|Dockerfile\.bun|\.dockerignore)|^package\.json$|pnpm-lock\.yaml$'

echo
echo "== getting-started/.dockerignore =="
if [ -f getting-started/.dockerignore ]; then cat -n getting-started/.dockerignore; fi

echo
echo "== getting-started/Dockerfile =="
if [ -f getting-started/Dockerfile ]; then cat -n getting-started/Dockerfile; fi

echo
echo "== getting-started/Dockerfile.bun =="
if [ -f getting-started/Dockerfile.bun ]; then cat -n getting-started/Dockerfile.bun; fi

echo
echo "== root package manager files =="
for f in package.json pnpm-lock.yaml getting-started/package.json; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done

echo
echo "== package scripts/packageManager fields =="
python3 - <<'PY'
import json, pathlib
for p in [pathlib.Path('package.json'), pathlib.Path('getting-started/package.json')]:
    if not p.exists():
        continue
    data=json.loads(p.read_text())
    print(p, "packageManager=", data.get("packageManager"), "scripts=", data.get("scripts"))
PY

Repository: Connectum-Framework/examples

Length of output: 7485


🌐 Web query:

Bun package manager install lockfile mode lockfile only package.json lockfile generation

💡 Result:

To generate a lockfile in the Bun package manager without installing dependencies to node_modules, you can use the --lockfile-only flag [1][2]. When you run bun install --lockfile-only, Bun will resolve dependencies, update the lockfile, and save it to disk (named bun.lock) [1][3]. Note that this process still populates your global install cache with registry metadata and relevant dependency files (git/tarballs) to ensure accuracy [1][3]. Regarding lockfile generation and management: 1. Default Format: As of Bun v1.2.0, the default lockfile is the human-readable, text-based bun.lock [1][4]. 2. Migration: If you have an older binary lockfile (bun.lockb), you can migrate to the new text format by running bun install --save-text-lockfile --frozen-lockfile --lockfile-only, and then deleting the old bun.lockb file [1][2]. 3. Frozen Lockfile: To ensure reproducible builds, use the --frozen-lockfile flag (often used in CI environments with the equivalent command bun ci). This instructs Bun to use the existing lockfile versions exclusively and fail if the package.json and lockfile are out of sync [2][5]. 4. Alternative Formats: If you require a yarn.lock file for compatibility, you can generate it alongside your bun.lock by using the --yarn flag or by configuring it in your bunfig.toml file [1][6]. If you want to prevent the creation of a lockfile entirely during installation, you can use the --no-save flag [1][3].

Citations:


Use a lockfile for the Docker dependency stages.

getting-started/.dockerignore excludes pnpm-lock.yaml, so both image stages resolve packages again from package.json. A rebuild can get a different dependency graph, and npm can generate a different lockfile than pnpm. Include a project lockfile in the build context, then install from it in frozen mode for both full and production dependency stages.

📍 Affects 3 files
  • getting-started/.dockerignore#L7-L7 (this comment)
  • getting-started/Dockerfile#L14-L15
  • getting-started/Dockerfile#L23-L24
  • getting-started/Dockerfile.bun#L15-L16
  • getting-started/Dockerfile.bun#L24-L25
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@getting-started/.dockerignore` at line 7, Stop excluding pnpm-lock.yaml in
getting-started/.dockerignore, and update both dependency-install stages in
getting-started/Dockerfile (lines 14-15 and 23-24) and
getting-started/Dockerfile.bun (lines 15-16 and 24-25) to use the project
lockfile with frozen pnpm installs for full and production dependencies.

Comment thread scripts/container-e2e.sh Outdated
Comment on lines +24 to +25
cleanup() { docker rm -f "$NAME" >/dev/null 2>&1 || true; }
trap cleanup EXIT

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Retain failed test containers until log collection completes.

The EXIT trap removes every e2e-* container before the workflow failure step runs. The docker logs command therefore finds no container and produces no diagnostics.

  • scripts/container-e2e.sh#L24-L25: preserve the container when the script exits with a failure status, or capture its logs before removal.
  • .github/workflows/container-e2e.yml#L64-L66: collect the preserved container logs, then remove the container if required.
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 24-24: This function is never invoked. Check usage (or ignored if invoked indirectly).

(SC2329)

📍 Affects 2 files
  • scripts/container-e2e.sh#L24-L25 (this comment)
  • .github/workflows/container-e2e.yml#L64-L66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/container-e2e.sh` around lines 24 - 25, Update
scripts/container-e2e.sh lines 24-25 so cleanup preserves the e2e-* container on
failure, or captures its logs before removal; retain cleanup on successful
exits. Update .github/workflows/container-e2e.yml lines 64-66 to collect logs
from the preserved container during failure handling, then remove it if needed.

Comment thread scripts/container-e2e.sh Outdated
…on failure

Review found three issues; two were real defects in this script.

**The shutdown check could pass on a container that never stopped.** It read
`.State.ExitCode` without checking `.State.Status`, and `docker inspect` reports
`ExitCode: 0` for a *running* container -- verified directly. So if `docker stop`
failed or timed out at the daemon level, both shutdown assertions passed while
the service was still up. That is exactly the vacuous-check class this harness
exists to catch, so it now requires `docker stop` to succeed and the status to be
`exited` before the exit code means anything. Verified both ways: a container
ignoring SIGTERM is SIGKILLed and caught by the exit code (137), and a simulated
failed stop is caught by the new status guard where the old logic passed.

**A failing run destroyed its own evidence.** The `EXIT` trap removed the
container, so the workflow's log-capture step -- and anyone debugging locally --
found nothing. Logs are now dumped before removal when a check has failed.

Assertions moved through a single `check` helper, replacing the `A && B || C`
pattern that is not if-then-else (SC2015).

The third comment asked for a committed lockfile with frozen installs. Declined:
car-sharing and hris deliberately ship without one so the images track the latest
compatible `@connectum/*` rather than being bit-reproducible. Both Dockerfiles now
carry that rationale, as those two already did, so the choice reads as deliberate.
@intech

intech commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — two of the three were real defects in the script, and one is a deliberate project convention. All addressed.

Shutdown assertion could pass on a container that never stopped — valid, and the more serious of the two. I verified the premise directly: docker inspect reports ExitCode: 0 for a running container. So if docker stop failed or timed out at the daemon level, both shutdown assertions passed while the service was still up. That is precisely the vacuous-check class this harness exists to catch, which makes it worth stating plainly rather than quietly fixing.

It now requires docker stop to succeed and .State.Status to be exited before the exit code is trusted. Verified both directions:

Scenario Old logic New logic
container ignores SIGTERM → SIGKILL, ExitCode=137 FAIL (caught) FAIL (caught)
docker stop fails, container still running, ExitCode=0 PASS — false pass FAIL (caught)

Cleanup destroyed the evidence — valid. The EXIT trap removed the container, so the workflow's log-capture step, and anyone debugging locally, found nothing. Logs are now dumped before removal whenever a check has failed.

Assertions also moved through a single check helper, replacing the A && B || C pattern flagged as SC2015.

Lockfile with frozen installs — declined, with the rationale now written into both Dockerfiles. car-sharing/Dockerfile and hris/Dockerfile deliberately ship without a committed lockfile so the images resolve the latest compatible @connectum/* from the caret ranges; they carry a comment saying the image is "example-grade, not bit-reproducible" and that production services should commit a lockfile and use --frozen-lockfile. These two Dockerfiles followed that convention but did not explain it, which is why it read as an oversight. They now do.

@intech
intech merged commit 85b7087 into main Aug 3, 2026
7 checks passed
@intech
intech deleted the feat/container-e2e branch August 3, 2026 23:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:feature New feature or enhancement request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant