Skip to content

Commit a62e73b

Browse files
feat: make the Containerfile and devcontainer runnable (wave-2 3/8) (#44)
## What changed & why Finalizes wave-2 work order `.claude/tasks/prod-readiness/w2-3-containers.md`. The implementation commit (`3d43043`) was already on `feat/containers-runnable` and pushed to origin, but no PR had been opened, so the new `.github/workflows/container-build.yml` had never actually executed in CI (`gh run list --branch feat/containers-runnable` showed only an unrelated, skipped "Push email notification" run — the workflow only triggers on `push`-to-`main` or `pull_request`-to-`main`, neither of which had happened). This PR's only change vs. the prior state of the branch is opening it, which is required for the acceptance criterion "CI build job green on the PR" to be checkable at all. - `Containerfile` — rebased off `docker.io/library/julia:1.10` pinned by digest (matches the `julia_version` pinned in the committed `Manifest.toml`/`.tool-versions`), instantiates + precompiles the committed Manifest, runs as a non-root `statistikles` user, and sets `ENTRYPOINT ["julia", "--project=/app", "-e", "using Statistikles; main()"]` — the real entry point in `src/tools/chat.jl`. No commented-out template blocks remain. - `.dockerignore` — new, trims the build context (`.git/`, `.github/`, `docs/`, `proofs/`, `ffi/`, etc.). - `.devcontainer/devcontainer.json` — adds the `ghcr.io/julialang/devcontainer-features/julia:1` feature pinned to the `1.10` channel, and switches `postCreateCommand` from the wave-1-era no-op `just deps` to the real `just setup` (wave-1 PR #34 made `just setup` a real `Pkg.instantiate()`). `.devcontainer/Containerfile` (Wolfi-based, `remoteUser: nonroot`) and its existing git/just/nickel features are untouched — they're real and out of scope. - `.github/workflows/container-build.yml` — new. Builds the root `Containerfile` with `docker build` on `pull_request`/`push` to `main` (path-filtered to `Containerfile`, `.dockerignore`, `Project.toml`, `Manifest.toml`, `src/**`, and the workflow file itself) plus `workflow_dispatch`. Build only, no push/registry login. `actions/checkout` is SHA-pinned with a version comment; no job-level `hashFiles()`/`secrets` conditionals. A smoke step then runs the built image and greps its stdout for the documented offline-demo banner lines. - `QUICKSTART-USER.adoc` / `QUICKSTART-MAINTAINER.adoc` — reconciled with the now-real image: exact `podman build`/`podman run` commands (docker works identically), plus a documented caveat (verified by testing both the container and a native WSL Julia run) that `STATISTIKLES_LM_URL` set at `run` time has no effect on a precompiled image/install, because `Pkg.precompile()` bakes the `ENV`-derived `BASE_URL` constant (`src/tools/lmstudio.jl`) into the precompile cache — a pre-existing `lmstudio.jl` behavior, out of scope for this task, documented rather than silently left broken. ## Process note (raised by review, not a code defect) The existing commit's trailer reads `Co-Authored-By: Claude Opus 4.8 (1M context)`, while the wave-2 routing table in `.claude/tasks/prod-readiness/README.md` assigns `impl=sonnet`/`verify=sonnet` to this task. Checked and worth recording: the fleet's execution contract fixes that exact trailer string across all tasks regardless of which model actually executes (it is not a literal per-task model attestation) — this finalize pass itself runs under the same fixed-trailer convention. No code change was needed or made for this. ## Verification run & result - No new code changes were required — the branch was already fully pushed and matched the work order. Confirmed via `git fetch origin feat/containers-runnable` + `git diff origin/main..HEAD --stat` (7 files changed, matches the commit's own description) and `git status` (clean, nothing to commit). - Confirmed via `gh pr list --repo hyperpolymath/statistikles --head feat/containers-runnable --state all` (empty) that no PR previously existed, and via `gh run list --repo hyperpolymath/statistikles --branch feat/containers-runnable` that no `Container Build` run had ever executed on this branch — consistent with the reviewer's finding. - Re-read `Containerfile`, `.github/workflows/container-build.yml`, `.devcontainer/devcontainer.json`, and the `.adoc` diffs in full: no commented-out template blocks remain, the workflow is path-filtered and SHA-pinned, and the devcontainer's separate Wolfi-based `.devcontainer/Containerfile` (`remoteUser: nonroot`) is unrelated to and unaffected by the root `Containerfile`'s new `statistikles` non-root user. - Did not re-run `docker build`/`docker run` myself in this pass — the reviewer's own note states their local WSL `docker build`/`docker run` reproduction already succeeded and matched the PR's claims exactly. This PR now lets `.github/workflows/container-build.yml` run for real in CI as the authoritative, checkable confirmation of the acceptance criterion. - Opening this PR is expected to trigger the `Container Build` workflow via its `pull_request`-to-`main` trigger (paths touched: `Containerfile`, `.dockerignore`, `.github/workflows/container-build.yml`). Not merging, per the execution contract. ## Skipped / out of scope Publishing images to a registry, multi-arch builds, and Guix packaging (separate task `w2-2-guix-package.md`, already merged as #39) — all explicitly out of scope per the work order. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 01eee40 commit a62e73b

7 files changed

Lines changed: 173 additions & 41 deletions

File tree

.devcontainer/README.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
== Overview
77

8-
This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, and nickel automatically.
8+
This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, and just pre-installed. Dev container features add git, just, nickel, and Julia 1.10 (via Juliaup) automatically.
99

1010
== VS Code (Local)
1111

@@ -25,4 +25,4 @@ This dev container uses `cgr.dev/chainguard/wolfi-base` with git, curl, bash, an
2525

2626
== Customization
2727

28-
Replace `Statistikles` placeholders in both `devcontainer.json` and `Containerfile` with your actual project name. Run `just deps` to verify the environment after first launch.
28+
Run `just setup` to verify the environment after first launch (already run automatically by `postCreateCommand`).

.devcontainer/devcontainer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
"version": "latest"
1919
},
2020
"ghcr.io/jdx/devcontainer-features/just:1": {},
21-
"ghcr.io/nickel-lang/devcontainer-feature:0": {}
21+
"ghcr.io/nickel-lang/devcontainer-feature:0": {},
22+
"ghcr.io/julialang/devcontainer-features/julia:1": {
23+
"channel": "1.10"
24+
}
2225
},
2326

24-
"postCreateCommand": "just deps",
27+
"postCreateCommand": "just setup",
2528

2629
"remoteUser": "nonroot",
2730

@@ -34,6 +37,7 @@
3437
"vscode": {
3538
"extensions": [
3639
"EditorConfig.EditorConfig",
40+
"julialang.language-julia",
3741
"eamodio.gitlens",
3842
"streetsidesoftware.code-spell-checker",
3943
"timonwong.shellcheck",

.dockerignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Build-context excludes for `docker build`/`podman build` (Containerfile,
3+
# .devcontainer/Containerfile). Podman falls back to this file when no
4+
# .containerignore is present.
5+
6+
.git/
7+
.github/
8+
.devcontainer/
9+
docs/
10+
proofs/
11+
ffi/
12+
benches/
13+
examples/
14+
generated/
15+
.claude/
16+
node_modules/
17+
*.log
18+
/tmp/
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# SPDX-License-Identifier: MPL-2.0
2+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
#
4+
# Statistikles — Container Build
5+
#
6+
# Builds the root Containerfile so it cannot silently rot back into a
7+
# template stub. Build only — no push, no registry login.
8+
9+
name: Container Build
10+
11+
on:
12+
push:
13+
branches: [main]
14+
paths:
15+
- 'Containerfile'
16+
- '.dockerignore'
17+
- 'Project.toml'
18+
- 'Manifest.toml'
19+
- 'src/**'
20+
- '.github/workflows/container-build.yml'
21+
pull_request:
22+
branches: [main]
23+
paths:
24+
- 'Containerfile'
25+
- '.dockerignore'
26+
- 'Project.toml'
27+
- 'Manifest.toml'
28+
- 'src/**'
29+
- '.github/workflows/container-build.yml'
30+
workflow_dispatch:
31+
32+
permissions: read-all
33+
34+
concurrency:
35+
group: container-build-${{ github.ref }}
36+
cancel-in-progress: true
37+
38+
jobs:
39+
build:
40+
name: docker build .
41+
runs-on: ubuntu-latest
42+
timeout-minutes: 30
43+
44+
steps:
45+
- name: Checkout
46+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
47+
48+
- name: Build Containerfile
49+
run: docker build -f Containerfile -t statistikles:ci .
50+
51+
- name: Smoke — run the entrypoint
52+
run: |
53+
# No LM Studio endpoint is reachable from the runner, so main()
54+
# (src/tools/chat.jl) takes its documented offline-demo fallback
55+
# and exits 0 — see the Containerfile's ENTRYPOINT comment and
56+
# QUICKSTART-USER.adoc "First Run".
57+
docker run --rm statistikles:ci | tee /tmp/container-run.log
58+
grep -q "Statistikles v0.1.0" /tmp/container-run.log
59+
grep -q "Cannot connect to LM Studio" /tmp/container-run.log
60+
grep -q "All numbers above were computed by Julia, not an LLM." /tmp/container-run.log

Containerfile

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,39 @@
55
# Build: podman build -t statistikles:latest -f Containerfile .
66
# Run: podman run --rm -it statistikles:latest
77
# Seal: selur seal statistikles:latest
8-
9-
# --- Build stage ---
10-
FROM cgr.dev/chainguard/wolfi-base:latest AS build
11-
12-
# TODO: Install build dependencies for your stack
13-
# Examples:
14-
# RUN apk add --no-cache rust cargo # Rust
15-
# RUN apk add --no-cache elixir erlang # Elixir
16-
# RUN apk add --no-cache zig # Zig
17-
18-
WORKDIR /build
19-
COPY . .
20-
21-
# TODO: Replace with your build command
22-
# Examples:
23-
# RUN cargo build --release
24-
# RUN mix deps.get && MIX_ENV=prod mix release
25-
# RUN zig build -Doptimize=ReleaseSafe
26-
27-
# --- Runtime stage ---
28-
FROM cgr.dev/chainguard/static:latest
29-
30-
# Copy built artifact from build stage
31-
# TODO: Replace with your binary/artifact path
32-
# Examples:
33-
# COPY --from=build /build/target/release/statistikles /usr/local/bin/
34-
# COPY --from=build /build/_build/prod/rel/statistikles /app/
35-
# COPY --from=build /build/zig-out/bin/statistikles /usr/local/bin/
36-
37-
# Non-root user (chainguard images default to nonroot)
38-
USER nonroot
39-
40-
# TODO: Replace with your entrypoint
41-
# ENTRYPOINT ["/usr/local/bin/statistikles"]
8+
#
9+
# Statistikles is a pure Julia package with no compiled binary artifact (see
10+
# "Build from Source" in QUICKSTART-MAINTAINER.adoc) — the `julia` executable
11+
# itself is the runtime, so there is nothing a separate build stage could
12+
# hand off that the runtime doesn't also need. A single stage on the official
13+
# Julia image is therefore correct here, not a shortcut. Chainguard ships no
14+
# Julia package, so the org-default Wolfi/static base (docs/AI-CONVENTIONS.md)
15+
# does not apply to this stack.
16+
17+
# docker.io/library/julia:1.10 == Julia 1.10.11 (Debian trixie), matching the
18+
# `julia_version` pinned in the committed Manifest.toml and .tool-versions.
19+
FROM docker.io/library/julia:1.10@sha256:12477d07306333f0c59c04274196432a3834f8c44a0a10585de659fbf8ab0e54
20+
21+
# Non-root runtime user
22+
RUN useradd --create-home --uid 1000 --shell /bin/bash statistikles
23+
WORKDIR /app
24+
COPY --chown=statistikles:statistikles . .
25+
USER statistikles
26+
ENV JULIA_DEPOT_PATH=/home/statistikles/.julia
27+
28+
# Resolve the committed Manifest.toml exactly (no re-resolution) and
29+
# precompile so the entrypoint starts promptly.
30+
RUN julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.precompile()'
31+
32+
# `main()` (src/tools/chat.jl) prints the startup banner, then tries to reach
33+
# an LM Studio-compatible endpoint (STATISTIKLES_LM_URL, default
34+
# http://localhost:1234/v1). With no LLM endpoint reachable from the
35+
# container it prints "Cannot connect..." and runs the offline
36+
# `run_examples()` demo instead, then exits 0 — it does not hang waiting for
37+
# input. Run with `--network=host` to reach a host-side LM Studio at the
38+
# default address for the interactive chat session instead. NOTE: setting
39+
# STATISTIKLES_LM_URL at `docker/podman run` time has no effect — Julia's
40+
# Pkg.precompile() above already baked the default URL into the package's
41+
# precompile cache; to change it, set STATISTIKLES_LM_URL as an image build
42+
# ENV before the RUN Pkg.precompile() line and rebuild.
43+
ENTRYPOINT ["julia", "--project=/app", "-e", "using Statistikles; main()"]

QUICKSTART-MAINTAINER.adoc

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,38 @@ the full Julia dependency closure from source — the closure is instantiated on
4545
from the pinned `Manifest.toml` into a writable depot on first use (see the
4646
header comment in `guix.scm` for the rationale and the exact command).
4747

48-
The container (`Containerfile`) packaging surface exists in the repository but is
49-
not yet functional — do not base a container distribution on it until it is
50-
completed.
48+
The `Containerfile` also builds a runnable image (see "Container" below).
49+
50+
== Container
51+
52+
[source,bash]
53+
----
54+
podman build -t statistikles:latest -f Containerfile .
55+
podman run --rm -it statistikles:latest
56+
----
57+
58+
(`docker` works identically in place of `podman`.) The image is built from
59+
the official `docker.io/library/julia:1.10` base (pinned by digest in the
60+
Containerfile), runs as a non-root user, and its `ENTRYPOINT` is the same
61+
`main()` entry point as `just run`. With no LLM endpoint reachable from the
62+
container it falls back to the offline `run_examples()` demo and exits 0 —
63+
see "First Run" in QUICKSTART-USER.adoc for the exact expected output. To
64+
reach a host-side LM Studio instance at the default address instead:
65+
66+
[source,bash]
67+
----
68+
podman run --rm -it --network=host statistikles:latest
69+
----
70+
71+
NOTE: `STATISTIKLES_LM_URL` set at `podman run` time has **no effect** on a
72+
precompiled image or a precompiled native install — verified by testing both.
73+
`Pkg.precompile()` evaluates and caches `BASE_URL` (`src/tools/lmstudio.jl`)
74+
into the package's precompile image at the time it runs; loading the package
75+
afterwards deserializes that cache rather than re-reading `ENV`. This is not
76+
container-specific — the same applies to `just build && just run` natively.
77+
To point a built image at a non-default endpoint, set `STATISTIKLES_LM_URL`
78+
as a build-time `ENV` *before* the `Pkg.precompile()` step in the
79+
Containerfile and rebuild.
5180

5281
== Configuration
5382

QUICKSTART-USER.adoc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Before you begin, ensure you have:
3737

3838
== Install
3939

40+
=== Option 1: Native Julia
41+
4042
[source,bash]
4143
----
4244
# Clone and set up
@@ -56,6 +58,23 @@ If you prefer not to use just, the equivalent raw command is:
5658
julia --project=. -e 'using Pkg; Pkg.instantiate()'
5759
----
5860

61+
=== Option 2: Container
62+
63+
No local Julia install required — just Podman or Docker:
64+
65+
[source,bash]
66+
----
67+
git clone https://github.com/hyperpolymath/statistikles.git
68+
cd statistikles
69+
podman build -t statistikles:latest -f Containerfile .
70+
podman run --rm -it statistikles:latest
71+
----
72+
73+
(`docker` works identically in place of `podman`.) This builds the official
74+
`docker.io/library/julia:1.10` base image with the project instantiated and
75+
precompiled, then runs it as a non-root user. Expected output matches
76+
"First Run" below.
77+
5978
== First Run
6079

6180
[source,bash]

0 commit comments

Comments
 (0)