chore(dev): reproducible dev container with Android + Flatpak toolchains (#237) #241

Open
james wants to merge 1 commit from 237-devcontainer into main
Owner

Summary

  • Adds a Dev Containers spec (.devcontainer/) that gets a fresh contributor to working pnpm -F @carol/api dev, pnpm -F @carol/client export:web, pnpm -F @carol/client build:android, and pnpm -F @carol/client build:flatpak in one docker pull.
  • Ships the publish workflow (.forgejo/workflows/release-devcontainer.yml) that builds, signs (cosign), and publishes the image, then opens a bot PR to update the digest pin.
  • Adds the contributor-facing "Dev container (recommended)" section to CONTRIBUTING.md and the "Dev container publish flow" paragraph in docs/ci.md.

What changed

  • .devcontainer/Dockerfile — Ubuntu 24.04 base (manifest-list digest pinned), preinstalls every toolchain Carol ships:
    • mise (reads .tool-versions at first attach for Node, pnpm, gitleaks, actionlint).
    • OpenJDK 21, Android cmdline-tools 11076708, platforms;android-35, build-tools;35.0.0 (matches release-android.yml).
    • Rust 1.83.0 + cargo (matches release-flatpak.yml); rust-toolchain.toml doesn't exist yet so this is the floor.
    • Tauri 2 system deps: libwebkit2gtk-4.1-dev, libsoup-3.0-dev, libjavascriptcoregtk-4.1-dev, librsvg2-dev, libayatana-appindicator3-dev, libgtk-3-dev, libxdo-dev, libssl-dev, build-essential, pkg-config.
    • flatpak + flatpak-builder + GNOME Platform 48 + Sdk 48 (installed --user since --system fails in an unprivileged container).
    • Release ergonomics: cosign 2.5.3, git-cliff 2.6.1, gh, jq, ripgrep, fd.
    • Non-root vscode user with passwordless sudo (Dev Containers convention).
  • .devcontainer/devcontainer.json — workspace mount at /workspaces/carol, forwarded ports 3000 / 8081 / 19000-19002, postCreateCommand: mise install && pnpm install --frozen-lockfile && pnpm -F @carol/client export:web, VS Code extension recommendations (ESLint, Prettier, Vitest, Expo Tools, rust-analyzer, Tauri, sqlite-viewer, markdown, yaml). Ships with the image line commented + a local build block so the spec is usable from this branch before the first registry publish.
  • .devcontainer/README.md — what's inside, expected size (8-10 GB uncompressed), how to use it (VS Code / devcontainer CLI / docker run), the publish flow, and out-of-scope items (no Android emulator inside the container, no GitHub Codespaces specifics, etc.).
  • .forgejo/workflows/release-devcontainer.yml — triggers on .devcontainer/** changes, monthly cron (0 0 1 * *), and workflow_dispatch. Builds with docker buildx for linux/amd64, pushes to forge.wynning.tech/james/carol-devcontainer:YYYY-MM-DD + :latest, signs the digest via cosign keypair (same one release.yml uses), then opens a bot PR to update the digest pin via the helper script.
  • scripts/ci/pin-devcontainer.mjs — idempotent in-place pin updater. Handles both the first-publish case (uncomment the sentinel + drop the local build block) and subsequent re-pins. Locally runnable.
  • CONTRIBUTING.md — new "Dev container (recommended)" section at the top of Prerequisites. Existing host-install path stays as the explicit fallback.
  • docs/ci.md — new "Dev container publish flow" section under Release pipeline.

Base image choice + rationale

Ubuntu 24.04 LTS over Fedora 42. The deciding factor was the install-surface parity with release-android.yml (apt-get + sdkmanager) and release-flatpak.yml (Tauri's system deps named libwebkit2gtk-4.1-dev / libsoup-3.0-dev / etc. — Fedora's webkit2gtk4.1-devel / libsoup3-devel would force a parallel install list to maintain). Mirroring the release workflows' install surface keeps "what the dev container ships" and "what CI installs" identical, so a future Android SDK / Flatpak runtime bump is one PR, not two. The Ubuntu 24.04 multi-arch manifest digest is pinned by digest in the same ARG style the production Dockerfile uses for node:24-slim.

Expected image size

~8-10 GB uncompressed. Breakdown:

Layer Size
Ubuntu 24.04 base ~80 MB
JDK 21 + Android SDK + platforms ~4 GB
Rust toolchain ~1.5 GB
Tauri / GTK / webkit2gtk deps ~700 MB
GNOME Platform 48 + SDK ~2 GB
Node, pnpm, gitleaks, actionlint <300 MB

Documented in .devcontainer/README.md so contributors know what they're pulling.

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck / lint / test (556 passed, 107 skipped — Postgres leg)
  • pnpm -F @carol/client typecheck / lint / test (29 passed) / export:web
  • pnpm -F @carol/api-client typecheck / lint
  • actionlint .forgejo/workflows/*.yml — clean
  • jq parse of .devcontainer/devcontainer.json (with JSONC comments stripped) — valid
  • docker buildx build --check .devcontainerCheck complete, no warnings found.
  • node scripts/ci/pin-devcontainer.mjs smoke-tested for first-publish swap (template → real digest + build-block removal), subsequent re-pin, and idempotency (same digest → exit 0 with no diff).
  • Local gitleaks detect on the diff — clean.
  • lefthook pre-commit + commit-msg hooks — all pass.
  • First publish via .forgejo/workflows/release-devcontainer.yml — needs the workflow to run at least once to populate the digest pin. Until then, contributors get the build: block path (Dockerfile + context) instead of the pinned image.
  • End-to-end docker pull from the published registry → devcontainer uppnpm -F @carol/client build:android smoke. Waits on first publish.

Out of scope

  • Android emulator inside the container (KVM passthrough fragility — out per #237).
  • macOS / Windows native builds in the container.
  • GitHub Codespaces / cloud Codespaces specifics (the spec is portable enough).
  • Auto-syncing with #227 (CI runner image) — same dep surface, separate images. Either could layer on the other's base; documented in .devcontainer/README.md and docs/ci.md.

Notes / surprises

  • Flatpak install user vs system. flatpak install --system fails inside an unprivileged container because of bind-mount restrictions on /var/lib/flatpak; switched to --user. Same install lives at $HOME/.local/share/flatpak, works for flatpak-builder invocations from the vscode user. Release workflows use --system (the runner is privileged), so the dev container and CI diverge on this one flag.
  • Android SDK licence acceptance. The Dockerfile uses the same yes 2>/dev/null | sdkmanager --licenses || true dance release-android.yml does. Works deterministically as long as Google doesn't reshape the EULA surface.
  • VS Code extension recommendations. Stayed minimal — ESLint, Prettier, Vitest, Expo Tools, rust-analyzer, Tauri, sqlite-viewer, markdown-all-in-one, yaml. Resisted the urge to recommend the Forgejo / Gitea pack since gh is preinstalled and Carol's docs use it as the generic API client.
  • Devcontainer.json is JSONC. The Dev Containers spec officially supports JSON-with-comments, so the // comments embedded throughout are spec-conformant. Validated by stripping comments and piping to jq empty.
  • Image not actually published. Per the ticket, no registry creds were used. The image line in devcontainer.json is commented out with a <TBD-FILLED-BY-FIRST-PUBLISH> sentinel; the publish workflow rewrites it via scripts/ci/pin-devcontainer.mjs on the first successful run.

Closes #237. See ADR-0014 for the release-pipeline shape this mirrors.

## Summary - Adds a Dev Containers spec (`.devcontainer/`) that gets a fresh contributor to working `pnpm -F @carol/api dev`, `pnpm -F @carol/client export:web`, `pnpm -F @carol/client build:android`, and `pnpm -F @carol/client build:flatpak` in one `docker pull`. - Ships the publish workflow (`.forgejo/workflows/release-devcontainer.yml`) that builds, signs (cosign), and publishes the image, then opens a bot PR to update the digest pin. - Adds the contributor-facing "Dev container (recommended)" section to `CONTRIBUTING.md` and the "Dev container publish flow" paragraph in `docs/ci.md`. ## What changed - `.devcontainer/Dockerfile` — Ubuntu 24.04 base (manifest-list digest pinned), preinstalls every toolchain Carol ships: - mise (reads `.tool-versions` at first attach for Node, pnpm, gitleaks, actionlint). - OpenJDK 21, Android cmdline-tools 11076708, `platforms;android-35`, `build-tools;35.0.0` (matches `release-android.yml`). - Rust 1.83.0 + cargo (matches `release-flatpak.yml`); rust-toolchain.toml doesn't exist yet so this is the floor. - Tauri 2 system deps: `libwebkit2gtk-4.1-dev`, `libsoup-3.0-dev`, `libjavascriptcoregtk-4.1-dev`, `librsvg2-dev`, `libayatana-appindicator3-dev`, `libgtk-3-dev`, `libxdo-dev`, `libssl-dev`, `build-essential`, `pkg-config`. - `flatpak` + `flatpak-builder` + GNOME Platform 48 + Sdk 48 (installed `--user` since `--system` fails in an unprivileged container). - Release ergonomics: cosign 2.5.3, git-cliff 2.6.1, `gh`, `jq`, `ripgrep`, `fd`. - Non-root `vscode` user with passwordless sudo (Dev Containers convention). - `.devcontainer/devcontainer.json` — workspace mount at `/workspaces/carol`, forwarded ports 3000 / 8081 / 19000-19002, `postCreateCommand: mise install && pnpm install --frozen-lockfile && pnpm -F @carol/client export:web`, VS Code extension recommendations (ESLint, Prettier, Vitest, Expo Tools, rust-analyzer, Tauri, sqlite-viewer, markdown, yaml). Ships with the `image` line commented + a local `build` block so the spec is usable from this branch before the first registry publish. - `.devcontainer/README.md` — what's inside, expected size (8-10 GB uncompressed), how to use it (VS Code / `devcontainer` CLI / `docker run`), the publish flow, and out-of-scope items (no Android emulator inside the container, no GitHub Codespaces specifics, etc.). - `.forgejo/workflows/release-devcontainer.yml` — triggers on `.devcontainer/**` changes, monthly cron (`0 0 1 * *`), and `workflow_dispatch`. Builds with `docker buildx` for `linux/amd64`, pushes to `forge.wynning.tech/james/carol-devcontainer:YYYY-MM-DD` + `:latest`, signs the digest via cosign keypair (same one `release.yml` uses), then opens a bot PR to update the digest pin via the helper script. - `scripts/ci/pin-devcontainer.mjs` — idempotent in-place pin updater. Handles both the first-publish case (uncomment the sentinel + drop the local `build` block) and subsequent re-pins. Locally runnable. - `CONTRIBUTING.md` — new "Dev container (recommended)" section at the top of Prerequisites. Existing host-install path stays as the explicit fallback. - `docs/ci.md` — new "Dev container publish flow" section under Release pipeline. ## Base image choice + rationale **Ubuntu 24.04 LTS** over Fedora 42. The deciding factor was the install-surface parity with `release-android.yml` (apt-get + sdkmanager) and `release-flatpak.yml` (Tauri's system deps named `libwebkit2gtk-4.1-dev` / `libsoup-3.0-dev` / etc. — Fedora's `webkit2gtk4.1-devel` / `libsoup3-devel` would force a parallel install list to maintain). Mirroring the release workflows' install surface keeps "what the dev container ships" and "what CI installs" identical, so a future Android SDK / Flatpak runtime bump is one PR, not two. The Ubuntu 24.04 multi-arch manifest digest is pinned by digest in the same `ARG` style the production `Dockerfile` uses for `node:24-slim`. ## Expected image size ~8-10 GB uncompressed. Breakdown: | Layer | Size | | --- | --- | | Ubuntu 24.04 base | ~80 MB | | JDK 21 + Android SDK + platforms | ~4 GB | | Rust toolchain | ~1.5 GB | | Tauri / GTK / webkit2gtk deps | ~700 MB | | GNOME Platform 48 + SDK | ~2 GB | | Node, pnpm, gitleaks, actionlint | <300 MB | Documented in `.devcontainer/README.md` so contributors know what they're pulling. ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` / `lint` / `test` (556 passed, 107 skipped — Postgres leg) - [x] `pnpm -F @carol/client typecheck` / `lint` / `test` (29 passed) / `export:web` - [x] `pnpm -F @carol/api-client typecheck` / `lint` - [x] `actionlint .forgejo/workflows/*.yml` — clean - [x] `jq` parse of `.devcontainer/devcontainer.json` (with JSONC comments stripped) — valid - [x] `docker buildx build --check .devcontainer` — `Check complete, no warnings found.` - [x] `node scripts/ci/pin-devcontainer.mjs` smoke-tested for first-publish swap (template → real digest + build-block removal), subsequent re-pin, and idempotency (same digest → exit 0 with no diff). - [x] Local `gitleaks detect` on the diff — clean. - [x] `lefthook` pre-commit + commit-msg hooks — all pass. - [ ] First publish via `.forgejo/workflows/release-devcontainer.yml` — needs the workflow to run at least once to populate the digest pin. Until then, contributors get the `build:` block path (Dockerfile + context) instead of the pinned image. - [ ] End-to-end `docker pull` from the published registry → `devcontainer up` → `pnpm -F @carol/client build:android` smoke. Waits on first publish. ## Out of scope - Android emulator inside the container (KVM passthrough fragility — out per #237). - macOS / Windows native builds in the container. - GitHub Codespaces / cloud Codespaces specifics (the spec is portable enough). - Auto-syncing with #227 (CI runner image) — same dep surface, separate images. Either could layer on the other's base; documented in `.devcontainer/README.md` and `docs/ci.md`. ## Notes / surprises - **Flatpak install user vs system.** `flatpak install --system` fails inside an unprivileged container because of bind-mount restrictions on `/var/lib/flatpak`; switched to `--user`. Same install lives at `$HOME/.local/share/flatpak`, works for `flatpak-builder` invocations from the `vscode` user. Release workflows use `--system` (the runner is privileged), so the dev container and CI diverge on this one flag. - **Android SDK licence acceptance.** The Dockerfile uses the same `yes 2>/dev/null | sdkmanager --licenses || true` dance `release-android.yml` does. Works deterministically as long as Google doesn't reshape the EULA surface. - **VS Code extension recommendations.** Stayed minimal — ESLint, Prettier, Vitest, Expo Tools, rust-analyzer, Tauri, sqlite-viewer, markdown-all-in-one, yaml. Resisted the urge to recommend the Forgejo / Gitea pack since `gh` is preinstalled and Carol's docs use it as the generic API client. - **Devcontainer.json is JSONC.** The Dev Containers spec officially supports JSON-with-comments, so the `//` comments embedded throughout are spec-conformant. Validated by stripping comments and piping to `jq empty`. - **Image not actually published.** Per the ticket, no registry creds were used. The `image` line in `devcontainer.json` is commented out with a `<TBD-FILLED-BY-FIRST-PUBLISH>` sentinel; the publish workflow rewrites it via `scripts/ci/pin-devcontainer.mjs` on the first successful run. Closes #237. See [ADR-0014](../docs/adr/0014-release-pipeline.md) for the release-pipeline shape this mirrors.
chore(dev): reproducible dev container with Android + Flatpak toolchains (#237)
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 6s
PR / Static analysis (pull_request) Successful in 1m53s
PR / OSV-Scanner (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m59s
PR / pnpm audit (pull_request) Successful in 2m13s
PR / Test (sqlite) (pull_request) Successful in 2m40s
PR / Test (postgres) (pull_request) Successful in 2m40s
PR / OpenAPI (pull_request) Successful in 2m50s
PR / Client (web export smoke) (pull_request) Successful in 2m57s
PR / Package age policy (soft) (pull_request) Successful in 1m11s
PR / Lint (pull_request) Successful in 3m11s
PR / Build (pull_request) Successful in 3m10s
Secrets / gitleaks (pull_request) Successful in 58s
PR / Coverage (soft) (pull_request) Successful in 1m36s
PR / Trivy (image) (pull_request) Failing after 1m45s
aa05815179
Adds a Dev Containers spec under .devcontainer/ that gets a fresh
contributor to working pnpm -F @carol/api dev, pnpm -F @carol/client
export:web, pnpm -F @carol/client build:android, and pnpm -F @carol/client
build:flatpak in one docker pull.

Base image: Ubuntu 24.04 LTS (manifest-list digest pinned). Picked over
Fedora because the Android SDK + Tauri/Flatpak system-dep install
surface matches .forgejo/workflows/release-android.yml and
release-flatpak.yml 1:1 — same package names, same install ordering.
Mirroring the release workflow's install surface keeps "what the
container ships" and "what CI installs" identical.

Preinstalled toolchains, every version pinned to its CI counterpart:
- mise + Carol's .tool-versions baseline (Node 22.23.0, pnpm 10.18.3,
  gitleaks 8.30.1, actionlint 1.7.12)
- OpenJDK 21, Android cmdline-tools 11076708, platforms;android-35,
  build-tools;35.0.0 (matches release-android.yml)
- Rust 1.83.0 + cargo (matches release-flatpak.yml)
- Tauri 2 system deps: libwebkit2gtk-4.1-dev, libsoup-3.0-dev,
  libjavascriptcoregtk-4.1-dev, librsvg2-dev, libayatana-appindicator3-dev,
  libgtk-3-dev, libxdo-dev, libssl-dev, build-essential, pkg-config
- flatpak + flatpak-builder + GNOME Platform 48 + Sdk 48 (--user install;
  --system fails inside an unprivileged container)
- Release-pipeline ergonomics: cosign 2.5.3, git-cliff 2.6.1, gh, jq,
  ripgrep, fd

Image size budget: ~8-10 GB uncompressed. Documented in
.devcontainer/README.md so first-pull contributors know what they're
pulling.

Publish flow (.forgejo/workflows/release-devcontainer.yml):
- Triggers on .devcontainer/** changes, monthly cron, and
  workflow_dispatch.
- Builds with docker buildx for linux/amd64.
- Pushes to forge.wynning.tech/james/carol-devcontainer:YYYY-MM-DD +
  :latest (split-URL pattern from #75 / release.yml).
- Signs the digest with cosign — same keypair release.yml uses.
- Opens a bot PR to update the digest pin in devcontainer.json via
  scripts/ci/pin-devcontainer.mjs (idempotent, locally runnable).

devcontainer.json ships with the published-image line commented out
plus a local build block — so contributors can use the spec from this
branch before the first registry publish. The publish workflow
swaps to a pinned image= on first run.

CONTRIBUTING.md gains a "Dev container (recommended)" section at the
top of Prerequisites; the host-install path remains the explicit
fallback.

docs/ci.md gains a "Dev container publish flow" section under the
Release pipeline.

Closes #237.

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 83.0% ≥ 50%
Branches 75.9% ≥ 75%
Functions 91.3% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 83.0% ✅ | ≥ 50% | | Branches | 75.9% ✅ | ≥ 75% | | Functions | 91.3% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.

Trivy (container image)

Threshold: high  ·  Total findings: 121  ·  At/above threshold: 1

critical high medium low
0 1 50 70
severity id package installed / range fix
high CVE-2026-12151 undici 6.25.0 6.27.0, 7.28.0, 8.5.0
<!-- scanner-comment: trivy --> ### Trivy (container image) **Threshold:** `high` &nbsp;·&nbsp; **Total findings:** 121 &nbsp;·&nbsp; **At/above threshold:** 1 | critical | high | medium | low | |---:|---:|---:|---:| | 0 | 1 | 50 | 70 | | severity | id | package | installed / range | fix | |---|---|---|---|---| | high | [CVE-2026-12151](https://avd.aquasec.com/nvd/cve-2026-12151) | undici | 6.25.0 | `6.27.0, 7.28.0, 8.5.0` |
Some checks failed
Commits / Conventional Commits (pull_request) Successful in 6s
PR / Static analysis (pull_request) Successful in 1m53s
PR / OSV-Scanner (pull_request) Successful in 1m46s
PR / Typecheck (pull_request) Successful in 1m59s
PR / pnpm audit (pull_request) Successful in 2m13s
PR / Test (sqlite) (pull_request) Successful in 2m40s
PR / Test (postgres) (pull_request) Successful in 2m40s
PR / OpenAPI (pull_request) Successful in 2m50s
PR / Client (web export smoke) (pull_request) Successful in 2m57s
PR / Package age policy (soft) (pull_request) Successful in 1m11s
PR / Lint (pull_request) Successful in 3m11s
PR / Build (pull_request) Successful in 3m10s
Secrets / gitleaks (pull_request) Successful in 58s
PR / Coverage (soft) (pull_request) Successful in 1m36s
PR / Trivy (image) (pull_request) Failing after 1m45s
This pull request can be merged automatically.
This branch is out-of-date with the base branch
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin 237-devcontainer:237-devcontainer
git switch 237-devcontainer

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff 237-devcontainer
git switch 237-devcontainer
git rebase main
git switch main
git merge --ff-only 237-devcontainer
git switch 237-devcontainer
git rebase main
git switch main
git merge --no-ff 237-devcontainer
git switch main
git merge --squash 237-devcontainer
git switch main
git merge --ff-only 237-devcontainer
git switch main
git merge 237-devcontainer
git push origin main
Sign in to join this conversation.
No description provided.