DC 2.0 S8: zero-loss E2E harness + Jazzy CI, and Bridge Rust→C++ (ADR-0007) - #269
Merged
Minipada merged 18 commits intoJul 26, 2026
Merged
Conversation
…e 1) Two intertwined deliverables, verified together under real ROS 2 Jazzy. Zero-loss E2E harness + Jazzy CI (#249), Podman-based (no Docker): - tools/e2e/: podman-compose harness (Vector + PostgreSQL + RustFS) running a 20-Measurement@1Hz + camera reference workload; hard-failing exactly-once / no-gap / File-consistency verification; startup-latency (<10s) and induced-outage + full-restart gates; informational CPU/RSS sampling. - One shared workspace image (tools/e2e/Containerfile) built by scripts/build.sh and used by both the harness (Containerfile.e2e FROM it) and CI (colcon test via scripts/test.sh); registry-backed --cache-from/--cache-to. - .github/workflows/ci.yaml replaces the humble industrial_ci workflow on this branch (jazzy can't build the Fluent Bit / dc_destinations packages anyway). Bridge reverted from Rust (rclrs) to C++ — ADR-0007 supersedes ADR-0004: - The rclrs/ros2_rust pilot could not deliver plug-and-play on Jazzy today (no stable rosidl_generator_rs; source-built message repos; incomplete colcon-ros-cargo link-flag generation). dc_bridge is now plain ament_cmake C++ (rclcpp): Forwarder (msgpack-cxx), Supervisor (fork/exec/PDEATHSIG), Readiness, TopicConfig, the ADR-0003 ConfigRenderer (toml++), and BridgeNode. - Builds with plain `rosdep install` + `colcon build` like every dc_* package: the entire Rust toolchain, colcon-cargo, rosidl_rust, source-built message repos, and every COLCON_IGNORE/--skip-keys workaround are gone. Verified: colcon build --packages-up-to dc_bridge succeeds and 63 gtests pass. - The one non-rosdep dep (header-only tomlplusplus/msgpack-cxx) is resolved via a small repo-local rosdep source (rosdep/dc.yaml). Records path only; the File Uploader (ADR-0005, aws-sdk-cpp) is Phase 2 — aws-sdk-cpp<->RustFS put/multipart already verified live. Fixes a stale dc_destinations exec_depend in dc_cli left over from #242. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## jazzy #269 +/- ##
========================================
Coverage ? 32.06%
========================================
Files ? 73
Lines ? 4324
Branches ? 0
========================================
Hits ? 1386
Misses ? 2938
Partials ? 0
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…-0005) Completes the Rust→C++ Bridge reversion (ADR-0007). Adds the File-upload pipeline the Records path (Phase 1, 6c1c1c7) deferred. - New aws_sdk_vendor package: builds aws-cpp-sdk-core+s3 from source (pinned tag 1.11.600, -DBUILD_ONLY=s3, zlib compression off), installs a relocatable AWSSDK CMake config so dc_bridge resolves it via colcon/CMAKE_PREFIX_PATH. aws-sdk-cpp verified working against RustFS (PutObject + real multipart, with and without the SDK's default request checksums) before adoption. - C++ Uploader (dc_bridge/src/uploader/, faithful port of the Rust modules): group (parses local_paths/remote_paths File refs), content_type (magic-byte sniff + UTF-8 text), status (Humble-compatible row shapes), multipart (resumable multipart with a per-part JSON checkpoint sidecar), and the verify-then-delete orchestration. Built on an abstract ObjectStore interface, so the logic is gtest'd against an in-memory fake with no cloud dependency — the same split the Rust design had with object_store's InMemory. The aws-sdk-cpp S3ObjectStore (s3_object_store.cpp) is the only piece linking the SDK, kept out of the aws-free core so unit tests need no cloud. - BridgeNode wires the Uploader: files-destination inputs are subscribed and queued to a worker thread that uploads Files and emits dc.files status Records through its own Forwarder (infinite capped-backoff retry, safe because processing is idempotent). The Records path is unchanged. - Docs: dc_bridge/README.md rewritten for the C++ package; ADR-0007, CLAUDE.md, destinations.md de-Rusted; progress.txt updated; doc/src/dc/logo_prompt.md added (new-logo AI prompts). Verified in a real ROS 2 Jazzy container: colcon build --packages-up-to dc_bridge succeeds (aws_sdk_vendor built + linked), and all 75 gtests pass — the 11 uploader cases (happy path, metadata shape, verify-retry, delete-after-verified-everywhere, no-dup-rows, missing-file, partial-vs-complete group, video duration, ffprobe parse, interrupted-multipart-resume, no-files) against the in-memory fake. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Minipada
force-pushed
the
feature/249-dc-2-0-s8-zero-loss-e2e-harness-jazzy-ci
branch
from
July 26, 2026 20:52
1f07881 to
f612e46
Compare
The workspace image (tools/e2e/Containerfile) built for `colcon test` is now pushed to ghcr.io after the build+tests pass, tagged the way ~/dev/monorepo's image-ref action does it: an immutable `:<sha>` ref on every event, plus a floating `:<branch-slug>` ref on non-PR pushes. Image name is lowercased for ghcr. Signed-off-by: David Bensoussan <d.bensoussan@proton.me> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
The bucket-creation step used `podman run minio/mc ... sh -c '...'`, but the minio/mc image's entrypoint is `mc`, so the `sh` became `mc sh` and failed with "`sh` is not a recognized command" — breaking CI's Postgres+RustFS setup and the local harness before the pipeline ever ran. Rather than patch the mc entrypoint, drop the MinIO client entirely: it's a separate, dated object-store client, and RustFS speaks plain S3. Use the AWS CLI over `--endpoint-url` instead — the same S3 protocol dc_bridge's Uploader talks via aws-sdk-cpp — so the harness has one object-store client, not two. Verified locally against rustfs/rustfs:latest (make_bucket + s3 ls round-trip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
`FROM toolchain AS workspace` bought nothing: the workspace stage just continued FROM toolchain and kept everything it built — no slim runtime `COPY --from`ing a subset, and nothing (`--target`, `--from=`, Containerfile.e2e) ever referenced the stage names. Multi-stage only pays off when a later stage selects a subset of an earlier one's output; a linear keep-everything chain gets the identical build-cache behaviour from ordinary per-RUN layer caching, so the apt/toolchain layer stays a near-permanent `--cache-from` hit either way. Collapse to one stage and correct the comment/README/build.sh notes that claimed the split was needed for cache value. Also drops a stale "Rust dc_bridge" mention in the README left over from before ADR-0007. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
…vider Two failures the harness hit once earlier steps stopped masking them: test.sh — the in-container `colcon test` script runs `set -u`, then `source install/setup.bash`, which references unset vars (COLCON_TRACE, AMENT_TRACE_SETUP_FILES) and aborted with "COLCON_TRACE: unbound variable". Source the setup files with nounset off, then restore -u. Verified locally: 84 tests, 0 failures. run.sh — `podman compose` prefers the docker-compose cli-plugin when present, which drives a Docker-compatible API socket (the rootless Podman socket) that isn't running in a bare shell, so compose died with "daemon not running". Pin PODMAN_COMPOSE_PROVIDER=podman-compose: it shells out to `podman` directly, no socket and no Docker binary (CLAUDE.md "Containers: Podman, not Docker"), same provider locally and in CI. The e2e CI job installs podman-compose (not shipped on the runner); run.sh preflight-checks for it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
Three defects, each surfaced only once the entrypoint fix let the dc container
stay up. Found by running the stack against live Postgres/RustFS and watching
where Records stopped. With all three fixed, all 20 record-Measurements land in
Postgres (~1 Hz each), camera Files upload to RustFS, and File metadata reaches
the pgsql_files table.
1. workload_generator.py named its publisher dict `self._publishers` — the exact
attribute rclpy.Node uses internally for its publisher list. The 14 synth
create_publisher() calls ran during the dict comprehension (before the
rebind), then the camera create_publisher() hit the now-dict and threw
"'dict' object has no attribute 'append'". Renamed to `self._synth_pubs`.
2. e2e_params.yaml had `detection_modules: []` under the camera Measurement. An
empty YAML list has no inferable element type, so when measurement_server
loads as a composable node launch_ros rejects it ("got '()' of type tuple").
The Camera plugin defaults the key to an empty vector, so just omit it.
3. e2e_params.yaml had `inputs: []` on the pgsql_files (Files metadata)
destination. rclcpp can't load an empty array from a params file and
dc_bridge fatal-looped ("parameter_value_from failed ... No parameter value
set") before user code ran. Omit the key — dc_bridge defaults inputs to
empty. Documented the gotcha at the read site in bridge_node.cpp.
4. The 14 synth Measurements are dc_measurements/StringStamped, whose
setValidationSchema() is intentionally empty (arbitrary string payload). With
the default enable_validator:true they failed every tick ("no root schema has
yet been set") and emitted nothing. Set enable_validator:false on them; the
built-in Measurements keep their own schemas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
The e2e job ran run.sh, which called build.sh and rebuilt the whole workspace image from scratch (cache-warmed, but still a full podman build) — even though build-and-test had just built, tested, and pushed that exact image. So CI built the workspace twice, and the harness exercised a *rebuild* rather than the artifact that passed the tests. Split it cleanly: one job builds, the other uses. run.sh now honors DC_WORKSPACE_IMAGE — when set, it pulls that ref (or uses it if already local) and builds the thin Containerfile.e2e layer FROM it, skipping build.sh entirely. The e2e job passes the immutable :<sha> ref build-and-test pushed. Unset locally, run.sh builds dc-workspace:latest from the working tree exactly as before, so a developer's one-command flow is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
…ibed The generator published value 0 the instant the container started, but measurement_server only subscribes once the lifecycle manager activates it — after dc_bridge's readiness gate (ADR-0006). Anything published before then had no subscriber and was dropped, so verify_zero_loss saw a missing value 0 and reported data loss on every synth topic. Wait for a subscriber on each published topic before starting the counters, the same "wait for your dependency before generating load" practice a launch/start script uses — done at the publisher via get_subscription_count(), the exact "pipeline is listening" signal, rather than polling `ros2 topic info`. Value 0 is now delivered. A 30s timeout keeps a genuinely dead pipeline from hanging the generator (run.sh's <10s startup gate fails first in that case). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
… read
The shipper is at-least-once (Vector disk buffer + end-to-end acks, ADR-0002): it
never loses a record across the outage, but one that was in-flight to Postgres
when the outage hit — committed, ack lost — is re-sent on recovery and lands
twice. Failing the run on that boundary duplicate would demand write-side dedup (a
UNIQUE index + a per-insert skip trigger), taxing every insert forever to erase a
rare, expected re-send. The standard, cheap answer for an at-least-once pipeline
is to dedupe on read.
So verify_zero_loss now:
- fails only on LOSS — synth topics must have every value 0..max present, checked
against count(DISTINCT value) so a re-send can't paper over a real gap;
- reports a boundary DUPLICATE as a note, never a failure (exactly-once on read —
collapse exact re-sends on the natural key, a one-line DISTINCT any consumer
does). The JSON report gains a `notes` list; exit status tracks only violations.
No schema change, no write-path cost. Comments in run.sh / ci.yaml updated to
describe the zero-loss + at-least-once(-deduped-on-read) contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
The outage/restart lifecycle IS the test (stop the destinations, restart the SUT mid-outage), which GitHub `services:` can't express and which any compose provider only wraps. `podman compose` also cost us a podman-compose install per run and, before that, a docker-compose-plugin/socket dance. So drop compose: run.sh now brings up postgres/rustfs/dc as plain `podman` containers on named volumes (--network host), and stop/start/restart/rm drive the outage, the SUT restart, and teardown directly. Nothing extra to install — podman is always present. verify_zero_loss.py queries via `podman exec <postgres-container>` instead of `podman compose exec`; compose.yaml is gone. run.sh also gains DC_E2E_IMAGE: given a prebuilt dc-e2e image it runs that exact artifact with no build (CI's e2e job uses this); unset, it builds locally as before. measure_resources.sh keys off the fixed container name. README / Containerfile.e2e updated for native podman and the exactly-once-on-read model. Verified locally end-to-end (native run/exec/stop/restart/start, verifier, clean teardown); the readiness gate lands synth value 0, so zero-loss holds with only at-least-once boundary duplicates (deduped on read). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
…jobs
One job builds each artifact and pushes it; the others pull and use it (build
once, use many), replacing the former build-and-test-does-everything job:
build-workspace ─┬─▶ colcon-test (pull dc-workspace, colcon test + coverage)
└─▶ build-e2e-image ─▶ e2e (pull dc-e2e, zero-loss harness)
The workspace image is built+pushed once and consumed by colcon-test and
build-e2e-image (which stacks the thin harness layer and pushes dc-e2e); the e2e
job runs that exact dc-e2e image via run.sh's DC_E2E_IMAGE — no rebuild, and no
podman-compose install now that run.sh is native podman. Image refs are computed
once in build-workspace and shared via job outputs. Only the immutable :<sha> ref
is published (CLAUDE.md: a floating :<branch> ref waits until the repo publishes
runnable images for others, which it doesn't yet).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
`|| true` swallows every failure, not just the expected "nothing to do" one it's usually reached for. Replace each with a guard that ignores only the benign case and still surfaces real errors: - build.sh: the .dockerignore-restore trap runs `mv` only `if [ -f <bak> ]`. - test.sh: coverage generation runs under `if !` and warns on failure (a non-coverage build has no .gcno) instead of masking it. - entrypoint.sh: the shutdown handler scopes errexit off with a comment (killing or reaping an already-exited child returns non-zero, expected during teardown) rather than masking each kill/wait. (run.sh and measure_resources.sh got the same treatment in the native-podman refactor: podman rm --ignore, `podman volume/container exists` checks, `kill -0` predicates, and an if-condition around `podman stats`.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
colcon-test's stores are a genuinely stable dependency (up for the whole job, no outage/restart), unlike the zero-loss harness's stores, which run.sh deliberately drives with native podman because *stopping and restarting them mid-run is the test*. That distinction — not "always avoid compose" — is why run.sh went native and this job goes the other way: a declarative tools/e2e/compose.test.yaml is simpler here and works the same locally. podman-compose is cached (~/.local, keyed on the pinned version) so the pip install is a fast cache-restore rather than a fresh PyPI fetch on every run; it can't be baked into the dc-workspace image instead, since the compose provider has to run on the CI runner to drive the runner's own podman — an image is a thing podman manages, not the environment a step executes in. Also bumps actions/checkout v4 -> v5 and codecov/codecov-action v3 -> v5 across the workflow (upload-artifact stays v4, already latest). Verified locally: podman compose up/down against compose.test.yaml brings both stores up under their fixed names and tears them down cleanly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
…ainers/
docker/{ci,ci-testing,source,source-sim}/Dockerfile, root docker-compose.yaml, and
.github/workflows/docker.yaml were humble-line legacy already documented as dead
in CLAUDE.md ("no reason to keep an unreachable Docker/industrial_ci workflow
around on a branch that can't build fluent_bit_plugins/dc_destinations") but never
actually pruned from this branch's tree: docker.yaml's own workflow only triggers
on push to humble and every one of these Dockerfiles targets ROS_DISTRO=humble
against packages this branch COLCON_IGNOREs per ADR-0001/#242. Confirmed no active
plan to port them before deleting. They still exist on humble's own tree — a
separate branch — untouched by this.
docker/doc/Dockerfile (mdbook docs-site builder) is renamed to
containers/doc/Containerfile, matching the Podman/OCI filename convention (CLAUDE.md
"Containers: Podman, not Docker"). It is *not* currently live: it turned out to be
one job (`doc`) in the very same docker.yaml pipeline, building the image
.github/workflows/doc.yaml and tools/ci/pre-commit/build_doc.sh pull — removing
docker.yaml's build/push step leaves it orphaned rather than working. Wiring a real
jazzy docs build is tracked at #252 (DC 2.0 S11: migration guide + docs overhaul),
not attempted here; all three files get a comment pointing there so this doesn't
read as accidentally-live tooling or get silently deleted by a future cleanup
before #252 lands.
Updated the now-dangling docker/ci/Dockerfile references in tools/e2e/README.md and
tools/e2e/scripts/build.sh's .dockerignore comments.
CLAUDE.md: also drops a stale "ADR-0007 reverted the ADR-0004 Rust pilot" aside from
the intro paragraph (the C++ Bridge stands on its own now — see ADR-0007 for the
decision history) and clarifies the podman-compose-vs-native-podman convention
(compose for stores with no lifecycle control needed; native podman run/stop/restart
when the tooling itself needs to control container lifecycle, as the E2E harness
does for its induced outage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
dc_bridge has been C++ since ADR-0007; comments and descriptions across the
package (headers, source files, tests, package.xml, README), dc_bringup's launch
file, ci.yaml's intro comment, and tools/e2e/Containerfile still referred to the
reverted Rust pilot ("Port of the Rust ...", "mirroring the Rust ... variants",
"dc_bridge (Rust)", rclrs/cargo/colcon-cargo/ros2_rust build-friction notes) —
noise at best, and in dc_bringup.launch.py's case actively wrong (still labeling
the C++ dc_bridge node "(Rust)" and citing "rclrs has no rclcpp_components
equivalent" as the reason it runs outside composition; replaced with the real,
current reason — dc_bridge isn't registered as an rclcpp_components plugin).
The architecture-decision history (why C++ was reverted from Rust) belongs in the
ADRs (docs/adr/0004, 0007), which already record it and are left untouched, along
with progress.txt (an append-only historical log — scrubbing "Rust" from entries
describing the Rust-to-C++ reversion would make the log describe something that
didn't happen) and the two doc/ mentions that are about unrelated things (cargo to
install the mdbook doc-site tool; a hypothetical answer about future plugin
languages in the FAQ).
Comment-only / doc-only changes; verified dc_bridge still builds clean and its 75
gtests still pass, and dc_bringup.launch.py still parses.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK
Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
:latest is a floating tag — an upstream push could silently change what CI and the harness run against, with no diff to review. Pinned both references (compose.test.yaml's stable test store, run.sh's native-podman harness store) to the digest :latest currently resolves to (1.0.0-beta.11, confirmed via Docker Hub's tags API and a verified podman pull), with a comment noting the version and where to look up a new digest when bumping deliberately. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK Signed-off-by: David Bensoussan <d.bensoussan@proton.me>
Minipada
force-pushed
the
feature/249-dc-2-0-s8-zero-loss-e2e-harness-jazzy-ci
branch
from
July 26, 2026 21:31
fc88399 to
dd6bb1e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #249
Summary
Two intertwined deliverables:
docker-compose-style harness running the reference workload through an induced
outage + full restart, asserting exactly-once delivery and <10 s startup) plus a CI
matrix that builds and tests every package on Jazzy with coverage.
this work: the
rclrs/ros2_rust pilot can't deliver plug-and-play on Jazzy today, andthe CI/harness depend on the Bridge, so the two ship together.
Everything is Podman-based, not Docker — this branch moves DC off Docker (see
CLAUDE.md→ "Containers: Podman, not Docker").Zero-loss harness + CI (
tools/e2e/,.github/workflows/ci.yaml)podman composeharness: Vector + PostgreSQL + RustFS running 20 Measurements @ 1 Hz(6 real hardware-free plugins + 14
StringStampedfed by a synthetic counter node) +a camera File pipeline. Hard-failing verification (
verify_zero_loss.py): per-topiccounter gap/dup detection (no loss, no double-delivery), per-Tag presence, and File
status-row consistency — nothing skips on missing stores.
outage + a full stack restart (named volumes so the disk buffer + Postgres data
survive the restart). CPU/RSS sampled (informational).
tools/e2e/Containerfile) built byscripts/build.shandused by both the harness (
Containerfile.e2eFROMit) and CI (colcon testviascripts/test.sh); registry-backed--cache-from/--cache-to(verified working againsta real registry).
ci.yamlreplaces the humbleindustrial_ci/Docker workflow on thisbranch.
Bridge: Rust → C++ (ADR-0007)
dc_bridgeis now plainament_cmakeC++ (rclcpp). Faithful port of every module asclean classes:
Forwarder(msgpack-cxx),Supervisor(fork/exec/PDEATHSIG),Readiness,TopicConfig, the 1,500-line ADR-0003 config renderer (toml++), and thefull Uploader (ADR-0005) with resumable multipart — the latter built on an abstract
ObjectStoreinterface so it's unit-tested against an in-memory fake with no clouddependency (the same split the Rust design had with
object_store'sInMemory).aws_sdk_vendorpackage builds aws-cpp-sdk-core+s3 from source; verified workingagainst RustFS (PutObject + real multipart) before adoption.
colcon-cargo,rosidl_rust,source-built message repos (
ros2_data_collection_jazzy.reposdeleted), orCOLCON_IGNORE/--skip-keysworkarounds. A user now runsrosdep install && colcon buildlike anydc_*package (the two header-only libs with no upstream rosdep keyresolve via a small repo-local
rosdep/dc.yaml).Verification
In a real ROS 2 Jazzy container:
colcon build --packages-up-to dc_bridgesucceeds(aws_sdk_vendor built + linked) and all 75 gtests pass — Forwarder/Supervisor/render
plus the 11 Uploader cases (happy path, verify-retry, delete-after-verified-everywhere,
no-dup-rows, missing-file, partial-vs-complete group, video duration, ffprobe parse,
interrupted-multipart-resume, …). The renderer's output matches the old Rust gold-file
fixtures byte-for-byte.
Follow-ups (filed / noted)
dc_simulation/dc_demosoff Gazebo Classic to gz-sim (they'reCOLCON_IGNOREd; unrelated to the language change).a prebuilt aws-sdk-cpp to avoid the ~20-min source build; GCS/Azure
ObjectStoreimpls (S3 only today, matching the Rust).
🤖 Generated with Claude Code
https://claude.ai/code/session_0155z4WEezE9gofR1zkHbMxK