Skip to content

feat: implement typed lifecycle command format support#13

Merged
pofallon merged 1 commit into
mainfrom
012-fix-lifecycle-formats
Feb 22, 2026
Merged

feat: implement typed lifecycle command format support#13
pofallon merged 1 commit into
mainfrom
012-fix-lifecycle-formats

Conversation

@pofallon
Copy link
Copy Markdown
Contributor

Summary

Implements full support for three lifecycle command formats per the DevContainer specification:

  • String format: Shell-interpreted commands (/bin/sh -c in container, sh -c on host)
  • Array format: Exec-style direct OS invocation without shell wrapping
  • Object format: Named parallel commands executing concurrently with full error aggregation

What's Changed

Core Type System

  • Introduce LifecycleCommandValue enum with Shell(String), Exec(Vec<String>), Parallel(IndexMap<String, LifecycleCommandValue>) variants
  • Implement spec-compliant parsing via from_json_value() with type validation
  • Thread typed commands through aggregation and execution pipeline for all 6 lifecycle phases

Execution

  • Format-aware execution in both container and host paths
  • Parallel execution via tokio::JoinSet — all entries run concurrently, all complete before phase result reported (no early cancellation per spec)
  • Variable substitution applied element-wise for arrays, recursively for objects
  • Capture stdout/stderr from parallel commands for diagnostics
  • Output attributed via [key] prefixes in progress events per entry

Code Quality Fixes

  • Replace 4 unreachable!() calls in runtime paths with proper DeaconError::Lifecycle returns
  • Return validation errors for invalid array entries inside objects (matches top-level behavior)
  • Filter empty strings/arrays in object entries as no-ops at parse time
  • Document serde_json preserve_order dependency for object key ordering
  • Extract duplicated workspace folder derivation to shared::derive_container_workspace_folder()
  • Log warn! on poisoned progress tracker mutex instead of silent swallow
  • Use sentinel values in host-only dummy config instead of empty strings
  • Remove initializeCommand from run-user-commands (host-only, belongs to up only)
  • Document feature lifecycle aggregation gap in run-user-commands with TODO

Tests

  • 4 new concurrency tests proving parallel execution is truly concurrent (~500ms for two 500ms tasks)
  • Error aggregation tests verifying all parallel entries complete despite individual failures
  • Comprehensive format × phase integration tests
  • 1758 tests passing, 0 failures

Known Limitations

  • run-user-commands feature gap: Feature-defined lifecycle commands are not yet aggregated (documented with TODO). Requires resolving features from container image metadata labels.
  • Parallel output terminal prefixing: Output captured in CommandResult.stdout/stderr but not line-prefixed to terminal in real-time (inherited stdio model constraint).

Spec Compliance

✓ All 3 formats work for all 6 lifecycle phases
✓ Exec-style arrays bypass shell — arguments passed literally
✓ Parallel objects run concurrently, wait for all, aggregate failures
✓ Variable substitution preserves structure (element-wise / recursive)
✓ Empty/null commands filtered as no-ops at every level
✓ Per-entry progress events with {phase}-{key} command IDs

🤖 Generated with Claude Code

Adds support for three lifecycle command formats per DevContainer spec:
- String: shell-interpreted (/bin/sh -c in container, sh -c on host)
- Array: exec-style, passed directly to OS without shell wrapping
- Object: named parallel commands, each value is Shell or Exec

## Core Changes

- Introduce LifecycleCommandValue enum (Shell, Exec, Parallel variants)
- Implement format-aware parsing with spec-compliant validation
- Wire typed commands through aggregation and execution pipeline
- Support variable substitution element-wise for arrays, recursively for objects
- Implement parallel execution via tokio::JoinSet with error aggregation
- Capture stdout/stderr in parallel execution for diagnostics
- Add output attribution via [key] prefixes for parallel commands

## Fixes & Quality Improvements

- Replace 4 unreachable!() in runtime paths with fallible DeaconError
- Return validation errors for invalid array entries in object format
- Filter empty strings/arrays as no-ops at parse time
- Document serde_json preserve_order dependency
- Extract duplicate workspace folder derivation to shared helper
- Log warnings on poisoned progress tracker mutex
- Use sentinel values in dummy container config
- Add concurrency tests verifying parallel execution is truly concurrent
- Add comprehensive format × phase integration tests

## Limitations

- run-user-commands: feature lifecycle commands not yet aggregated (documented gap)
- Parallel output prefixing requires inherited stdio (captured in CommandResult.stdout/stderr but not line-prefixed to terminal)

Fixes issues from spec review: format parsing, exec semantics, parallel execution,
variable substitution, all six lifecycle phases, empty/null handling.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions github-actions Bot added build Build system changes deps Dependency updates labels Feb 22, 2026
@pofallon pofallon merged commit d04ea84 into main Feb 22, 2026
7 checks passed
@pofallon pofallon deleted the 012-fix-lifecycle-formats branch February 22, 2026 13:07
pofallon added a commit that referenced this pull request May 26, 2026
…ONTRIBUTING (#47)

Bundles three P2 polish items from the post-1.0 audit:

## SECURITY.md (gap #10 policy half)

New top-level file documenting:
- Supported versions
- Private reporting channels (GitHub Security Advisory + email)
- Scope (in-scope: command injection, path traversal, secret leakage,
  TLS/OCI auth, runtime privilege escalation; out-of-scope:
  upstream Docker/Podman bugs, third-party features, CLI-process DoS)
- Coordinated disclosure window
- Pointer to the CI security gates

## CodeQL workflow (gap #10 scanning half)

New `.github/workflows/codeql.yml`: Rust analysis on PR + push to main
+ weekly schedule (Monday 06:00 UTC, staggered from cargo-deny's 07:00
UTC daily). Uses the workspace MSRV. Builds `--workspace --all-targets`
so the full subcommand surface is analyzed.

## Cargo manifest metadata (gap #12)

Added to `[workspace.package]` and per-package `[package]`:
- `homepage`, `documentation`, `keywords`, `categories`
- Per-package `description` and `readme` for crates.io rendering

This unblocks publishing both crates to crates.io with proper search
discoverability.

## CONTRIBUTING.md refresh (gap #13)

The Quick Start and Common Tasks sections centered on raw `cargo test`,
but the repo standardized on `cargo-nextest` + `make` targets long ago.
Updated to:
- Lead with `make dev-fast` / `make test-nextest-fast` in the Quick Start
- Replace the Common Tasks table with the make-target taxonomy
- Drop the long e2e-by-name list (replaced with general "filter by name"
  guidance — the e2e suite has grown past the original 7 named tests)
- CI/CD section now reflects the actual workflows on main: ci.yml +
  codeql.yml + cargo-deny, with Conventional-Commits PR-title gate.

## Verification

- `cargo build` ✓
- `cargo fmt --all -- --check` ✓

Refs: gaps #10, #12, #13 from the post-1.0 audit.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Build system changes deps Dependency updates

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants