Skip to content

feat: native container steps for workflows and custom commands - #2626

Merged
Andriy Knysh (aknysh) merged 37 commits into
mainfrom
osterman/container-step-prd
Jun 23, 2026
Merged

Andriy Knysh (aknysh) merged 37 commits into
mainfrom
osterman/container-step-prd

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Jun 18, 2026 •

Copy link
Copy Markdown
Member

what

  • Add a native type: container step (build / push / run) to the shared step library used by both workflows and custom commands, built on the existing pkg/container Docker/Podman runtime (new ephemeral one-shot runner plus image build/tag/push/inspect helpers; ImageInspect added to the Runtime interface, mocks regenerated).
  • Formalize step outputs: every named step exposes value/values/metadata/outputs/skipped/error (command-like steps add stdout/stderr/exit_code), so a build step can publish an image reference consumed by later push/run steps via {{ .steps.<name>.outputs.<key> }}.
  • Support per-step identity for registry auth and Docker Buildx + Buildx Bake builds; Podman uses the native podman build path.
  • Add the examples/container-step example and a hermetic GitHub Actions job ([container-step]) that exercises build → push → run against a registry:2 service on localhost:5000, including failure-propagation.
  • Document the step type (website/docs/workflows), add a changelog blog post, and update the roadmap (container steps + step outputs marked shipped).
  • Land the design PRDs for the follow-on primitives — container-components.md, compose-components.md, and a rewritten membership-based compositions.md — and trim container-actions-and-step-outputs.md to cover only the procedural step. Remove the earlier targets:-based composition scaffolding (pkg/composition, cmd/composition, the composition step, and schema.Composition*) in favor of those PRDs.
  • Split the container-step handler into focused files and reduce complexity to satisfy the lint gate.

why

  • Atmos workflows and custom commands increasingly resemble CI pipelines; running containers natively (build images, push to registries, run tools) removes the need for one-off shell scripts and keeps the same automation usable locally and in CI.
  • A first-class step-outputs contract lets build → push → run/deploy pipelines pass structured values without shell parsing or temporary env files.
  • The procedural container step is the shippable foundation; the component kinds (container, compose) and compositions are specified as PRDs so the broader system can be designed and reviewed before implementation, without blocking this PR.

references

  • PRDs: docs/prd/container-actions-and-step-outputs.md, docs/prd/container-components.md, docs/prd/compose-components.md, docs/prd/compositions.md
  • Changelog: website/blog/2026-06-17-native-container-steps.mdx
  • Roadmap initiative: "Container Composition & Local Development" in website/src/data/roadmap.js

Add a native `type: container` step (build/push/run) to the shared step
library used by workflows and custom commands, plus a formal step-outputs
contract so one step can build an image and later steps push/run the exact
produced artifact. Built on the existing pkg/container Docker/Podman runtime
(ephemeral one-shot runner, image build/tag/push/inspect helpers) with
per-step identity for registry auth and Docker Buildx Bake support.

Includes the examples/container-step example, a hermetic GitHub Actions job
that exercises build -> push -> run against a registry:2 service on
localhost:5000 (plus failure-propagation), workflow step-type docs, a
changelog blog post, and a roadmap update marking container steps and step
outputs shipped.

Also lands the design PRDs for the follow-on primitives (container
components, compose components, and membership-based compositions) and trims
the container-step PRD to cover only the procedural step. The earlier
targets-based composition scaffolding is removed in favor of those PRDs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@atmos-pro

atmos-pro Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@osterman Erik Osterman (Cloud Posse) (osterman) added the minor New features that do not break anything label Jun 18, 2026
@github-actions github-actions Bot added the size/l Large size PR label Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@mergify

mergify Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Important

Cloud Posse Engineering Team Review Required

This pull request modifies files that require Cloud Posse's review. Please be patient, and a core maintainer will review your changes.

To expedite this process, reach out to us on Slack in the #pr-reviews channel.

@mergify mergify Bot added the needs-cloudposse Needs Cloud Posse assistance label Jun 18, 2026
lychee resolves /core-concepts/* as repo-relative file paths, which do not
exist; use plain text in the PRD intro instead of website-route links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 022867dc-6c2d-4aed-b8c7-c6e624c0adb6

📥 Commits

Reviewing files that changed from the base of the PR and between 1b13b91 and b1e2e23.

📒 Files selected for processing (3)
  • lychee.toml
  • pkg/schema/schema.go
  • website/docs/cli/configuration/secrets.mdx
✅ Files skipped from review due to trivial changes (2)
  • lychee.toml
  • website/docs/cli/configuration/secrets.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/schema/schema.go

📝 Walkthrough

Walkthrough

Adds native container execution, workflow container sandboxes, step output propagation, and the supporting schema, runtime, example, CI, and documentation updates.

Changes

Container step and workflow container feature

Layer / File(s) Summary
Contracts and runtime primitives
pkg/container/*, pkg/schema/*, pkg/config/*, pkg/datafetcher/schema/config/global/1.0.json, cmd/cmd_utils.go, pkg/devcontainer/runtime.go
Adds container runtime interfaces, build/push/inspect metadata, ephemeral and sandbox helpers, runtime detection, environment forwarding, top-level container config, workflow/step schema fields, and env/output plumbing.
Workflow and step execution wiring
pkg/runner/step/*, pkg/runner/runner.go, pkg/workflow/*, internal/exec/workflow_utils.go
Registers container steps, wires build/push/run/inspect actions, records step outputs, supports retry and dry-run behavior, and routes shell steps through workflow-level or step-level container execution.
Examples, CI, and tests
examples/container-step/*, examples/container-sandbox/*, .github/workflows/test.yml, tests/testhelpers/*, tests/snapshots/*, *_test.go
Adds examples, a CI job that exercises local build→push→run flows, fake runtime helpers, snapshot updates, and unit/integration coverage for runtime, handler, executor, and sandbox behavior.
Docs, PRDs, and navigation
docs/prd/*, website/docs/..., website/blog/..., website/sidebars.js, website/src/data/roadmap.js, examples/README.md, lychee.toml, .claude/skills/docs/SKILL.md
Documents container steps, workflow containers, step outputs, command/workflow references, updates sidebar and roadmap navigation, and refreshes related docs/blog/link targets.

Sequence Diagram(s)

sequenceDiagram
  participant Executor
  participant StepRegistry
  participant ContainerHandler
  participant ContainerSession
  Executor->>StepRegistry: resolve extended step type
  StepRegistry-->>Executor: handler
  Executor->>ContainerHandler: Validate and Execute
  ContainerHandler->>ContainerSession: run or inspect inside workflow container
  ContainerHandler-->>Executor: StepResult with outputs
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • cloudposse/atmos#1899: Extends the registered step execution path and output propagation used by workflow and custom-command step handlers.
  • cloudposse/atmos#1849: Touches the same workflow executor layer that now dispatches registered container steps and workflow container execution.
  • cloudposse/atmos#1229: Modifies internal/exec/workflow_utils.go, which this PR also updates for workflow step routing and cleanup behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly summarizes the main change: introducing native container steps for workflows and custom commands, which is the central feature of this large changeset.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 osterman/container-step-prd

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (3)
examples/container-step/Dockerfile (1)

1-1: ⚡ Quick win

Pin the base image instead of alpine:latest.

Line 1 is using a mutable tag, so builds can drift unexpectedly over time. Pin to a fixed version (ideally with digest) for reproducibility.

Suggested change
-FROM alpine:latest
+FROM alpine:3.22.1
🤖 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 `@examples/container-step/Dockerfile` at line 1, The Dockerfile is using the
mutable tag alpine:latest for the FROM instruction, which causes builds to be
non-reproducible as the base image can change over time. Replace alpine:latest
with a pinned version tag (such as alpine:3.18 or the latest stable version
number) to ensure consistent and reproducible builds. For maximum
reproducibility, also include the image digest (the sha256 hash) after the
version tag.
.github/workflows/test.yml (1)

684-697: ⚡ Quick win

Use an immutable registry image reference for the service container.

Lines 684-687 describe this job as hermetic, but Line 695 uses mutable registry:2. Pinning by digest keeps runs reproducible and avoids silent upstream drift.

Suggested change
-        image: registry:2
+        image: registry:2@sha256:<pinned-digest>
🤖 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 @.github/workflows/test.yml around lines 684 - 697, The registry service
container in the container-step job uses a mutable tag `registry:2` which
contradicts the hermetic nature described in the comments and can cause silent
upstream drift. Replace the mutable `registry:2` tag in the image field of the
registry service with an immutable digest reference by pinning it to a specific
SHA256 digest (e.g., registry:2@sha256:...). This ensures the workflow remains
reproducible across runs.
pkg/container/podman_test.go (1)

793-801: ⚡ Quick win

Broaden this unsupported-path test to assert error type and both guard branches.

Right now this covers only Bake != nil and checks message text. Add a case for Engine: "buildx" and assert the wrapped sentinel (ErrContainerRuntimeOperation) so error-contract regressions are caught early.

💡 Suggested update
+import errUtils "github.com/cloudposse/atmos/errors"
+
 func TestPodmanRuntime_BuildBakeUnsupported(t *testing.T) {
 	runtime := NewPodmanRuntime()
-	err := runtime.Build(context.Background(), &BuildConfig{
-		Bake: &BakeConfig{File: "docker-bake.hcl"},
-	})
-
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "Docker Buildx requires Docker")
+
+	t.Run("bake config", func(t *testing.T) {
+		err := runtime.Build(context.Background(), &BuildConfig{
+			Bake: &BakeConfig{File: "docker-bake.hcl"},
+		})
+		require.Error(t, err)
+		assert.ErrorIs(t, err, errUtils.ErrContainerRuntimeOperation)
+		assert.Contains(t, err.Error(), "Docker Buildx requires Docker")
+	})
+
+	t.Run("buildx engine", func(t *testing.T) {
+		err := runtime.Build(context.Background(), &BuildConfig{
+			Engine: "buildx",
+		})
+		require.Error(t, err)
+		assert.ErrorIs(t, err, errUtils.ErrContainerRuntimeOperation)
+		assert.Contains(t, err.Error(), "Docker Buildx requires Docker")
+	})
 }

As per coding guidelines: “All errors MUST be wrapped using static errors… and use errors.Is() for error checking,” and “All features need tests.”

🤖 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 `@pkg/container/podman_test.go` around lines 793 - 801, The test
TestPodmanRuntime_BuildBakeUnsupported currently only covers the Bake field
branch and uses message text assertion instead of error type checking. Extend
this test to also cover the Engine: "buildx" branch by adding another
BuildConfig case with just Engine set to "buildx", and replace the
assert.Contains check with errors.Is to verify that both branches return the
wrapped ErrContainerRuntimeOperation sentinel error rather than relying on error
message text.

Source: Coding guidelines

🤖 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 @.github/workflows/test.yml:
- Around line 726-733: The current test for the failing-check workflow only
validates that it returns a non-zero exit code, but does not verify the actual
reason for failure, which could allow false positives if the workflow breaks for
unrelated reasons. Capture the output from the atmos workflow failing-check
command execution and add an assertion that checks for expected marker text
within that output to confirm the workflow failed for the intended reason, not
due to incidental breakage.

In `@docs/prd/compose-components.md`:
- Around line 165-168: Remove the stale placeholder text `Plugins` `",remain"`
from the first bullet point where it registers compose in schema.Components.
This phrase appears corrupted and unclear; simply delete the parenthetical
phrase that contains it (starting with "may begin via") and end the line cleanly
after listing the sibling components
(terraform/helmfile/packer/ansible/container) to improve clarity and readability
of the implementation note.

In `@docs/prd/compositions.md`:
- Around line 219-221: The documentation in the compositions.md file contains a
stale and incorrect example path reference. Find the mention of
`examples/containers/storefront` in the text and replace it with the correct
path `examples/container-step`, which is the actual runnable example that exists
in the repository and is consistently referenced throughout the PR materials.

In `@pkg/container/detector.go`:
- Around line 107-117: The inner condition in
DetectRuntimeWithPreferenceAndRecovery checks Docker availability instead of
Podman availability. When preferred is set to Podman via environment variable,
the recovery should trigger only if Podman is not available, not Docker. Change
the condition from checking !isAvailable(ctx, TypeDocker) to !isAvailable(ctx,
TypePodman) so that TryRecoverPodmanRuntime is invoked when Podman is actually
unavailable and needs to be recovered, regardless of Docker's availability
status.

In `@pkg/container/ephemeral_test.go`:
- Around line 57-81: Add a new test function that serves as the negative-path
counterpart to TestRunEphemeralContainer_PullMissingRetriesCreate. This test
should verify that the pull recovery mechanism does NOT trigger when the
condition is absent. Set up a similar mock environment using gomock.InOrder, but
configure the EphemeralConfig with PullPolicy set to PullNever. Expect the
runtime.Create call to fail with an error, and crucially, do NOT include an
expectation for runtime.Pull in the gomock.InOrder chain. Verify that
RunEphemeralContainer returns the error without attempting to pull the image,
confirming that recovery is correctly skipped when the pull policy forbids
pulling.

In `@pkg/container/ephemeral.go`:
- Around line 77-85: Raw runtime errors from runtime.Pull() and
createEphemeralContainer() are being returned directly without wrapping, which
breaks the project's typed-error contract. Wrap all error returns in the
ephemeral container creation flow (including errors from runtime.Pull,
createEphemeralContainer, and other runtime operations) using the appropriate
container sentinel error defined in errors/errors.go before returning them, and
ensure all upstream error checking uses errors.Is() for proper error type
validation.
- Around line 90-93: The deferred cleanup block calls runtime.Remove with the
original ctx parameter, which may be cancelled or have an expired deadline by
the time cleanup executes, causing the removal to fail and leak containers.
Replace the ctx argument in the runtime.Remove call within the defer function
with a fresh, non-cancelled context created from context.Background() or a new
context derived from Background, ensuring the cleanup operation is not affected
by the original context's cancellation or deadline.

---

Nitpick comments:
In @.github/workflows/test.yml:
- Around line 684-697: The registry service container in the container-step job
uses a mutable tag `registry:2` which contradicts the hermetic nature described
in the comments and can cause silent upstream drift. Replace the mutable
`registry:2` tag in the image field of the registry service with an immutable
digest reference by pinning it to a specific SHA256 digest (e.g.,
registry:2@sha256:...). This ensures the workflow remains reproducible across
runs.

In `@examples/container-step/Dockerfile`:
- Line 1: The Dockerfile is using the mutable tag alpine:latest for the FROM
instruction, which causes builds to be non-reproducible as the base image can
change over time. Replace alpine:latest with a pinned version tag (such as
alpine:3.18 or the latest stable version number) to ensure consistent and
reproducible builds. For maximum reproducibility, also include the image digest
(the sha256 hash) after the version tag.

In `@pkg/container/podman_test.go`:
- Around line 793-801: The test TestPodmanRuntime_BuildBakeUnsupported currently
only covers the Bake field branch and uses message text assertion instead of
error type checking. Extend this test to also cover the Engine: "buildx" branch
by adding another BuildConfig case with just Engine set to "buildx", and replace
the assert.Contains check with errors.Is to verify that both branches return the
wrapped ErrContainerRuntimeOperation sentinel error rather than relying on error
message text.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ce993397-1338-4a3b-ba3c-19e5b6413037

📥 Commits

Reviewing files that changed from the base of the PR and between dbbe23e and d618c5c.

📒 Files selected for processing (45)
  • .github/workflows/test.yml
  • cmd/cmd_utils.go
  • docs/prd/compose-components.md
  • docs/prd/compositions.md
  • docs/prd/container-actions-and-step-outputs.md
  • docs/prd/container-components.md
  • examples/container-step/Dockerfile
  • examples/container-step/README.md
  • examples/container-step/atmos.yaml
  • examples/container-step/docker-bake.hcl
  • examples/container-step/workflows/container-step.yaml
  • internal/exec/workflow_utils.go
  • pkg/container/common.go
  • pkg/container/common_test.go
  • pkg/container/detector.go
  • pkg/container/docker.go
  • pkg/container/ephemeral.go
  • pkg/container/ephemeral_test.go
  • pkg/container/image.go
  • pkg/container/mock_runtime_test.go
  • pkg/container/podman.go
  • pkg/container/podman_test.go
  • pkg/container/runtime.go
  • pkg/devcontainer/mock_runtime_test.go
  • pkg/devcontainer/runtime.go
  • pkg/runner/runner.go
  • pkg/runner/step/command_handlers_test.go
  • pkg/runner/step/container.go
  • pkg/runner/step/container_build.go
  • pkg/runner/step/container_push.go
  • pkg/runner/step/container_run.go
  • pkg/runner/step/container_test.go
  • pkg/runner/step/executor.go
  • pkg/runner/step/types.go
  • pkg/runner/step/variables.go
  • pkg/runner/step/variables_test.go
  • pkg/runner/task_test.go
  • pkg/schema/task.go
  • pkg/schema/workflow.go
  • pkg/workflow/executor.go
  • pkg/workflow/executor_test.go
  • website/blog/2026-06-17-native-container-steps.mdx
  • website/docs/workflows/_partials/_step-types.mdx
  • website/docs/workflows/workflows.mdx
  • website/src/data/roadmap.js

Comment thread .github/workflows/test.yml
Comment thread docs/prd/compose-components.md
Comment thread docs/prd/compositions.md Outdated
Comment thread pkg/container/detector.go
Comment thread pkg/container/ephemeral_test.go
Comment thread pkg/container/ephemeral.go Outdated
Comment thread pkg/container/ephemeral.go
…tput

DockerRuntime.Create() returned the full CombinedOutput of `docker create`
as the container ID. When the image was missing locally, `docker create`
pulled it inline and the pull progress was captured alongside the ID, so the
multi-line blob was passed to `docker start`, producing
"Error response from daemon: page not found" (the container-step example job).

Extract the container ID as the last non-empty line of output via a shared
extractContainerID() helper, used by both the Docker and Podman runtimes
(de-duplicating Podman's inline loop). Add an empty-ID guard and a regression
test covering the exact inline-pull output from CI.

Also bundles related container refinements already in the worktree: runtime
resolution/recovery ordering in detector.go, error wrapping and
cancellation-safe cleanup in ephemeral.go, and PRD doc link fixes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/container/ephemeral.go (1)

250-259: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Quote preview args before joining.

The preview is not equivalent when an arg contains spaces: []string{"/bin/sh", "-lc", "echo ok"} renders as ... -lc echo ok. Route all preview builders through one small quote-aware join helper.

Preview join helper
+func joinPreviewArgs(args []string) string {
+	quoted := make([]string, len(args))
+	for i, arg := range args {
+		quoted[i] = quotePreviewArg(arg)
+	}
+	return strings.Join(quoted, spaceSeparator)
+}
+
+func quotePreviewArg(arg string) string {
+	if arg == "" {
+		return "''"
+	}
+	if !strings.ContainsAny(arg, " \t\n'\"\\$`!&;()<>|*?[]{}") {
+		return arg
+	}
+	return "'" + strings.ReplaceAll(arg, "'", "'\"'\"'") + "'"
+}
+
 func BuildEphemeralPreview(runtimeName string, config *EphemeralConfig) string {
@@
-	return strings.Join(args, spaceSeparator)
+	return joinPreviewArgs(args)
 }
@@
-	return strings.Join(args, spaceSeparator)
+	return joinPreviewArgs(args)
 }
@@
-	return strings.Join(args, spaceSeparator)
+	return joinPreviewArgs(args)
 }
@@
-	return strings.Join(args, spaceSeparator)
+	return joinPreviewArgs(args)
 }

Also applies to: 307-308, 318-319, 329-330

🤖 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 `@pkg/container/ephemeral.go` around lines 250 - 259, The current preview
output uses strings.Join with a simple space separator, which fails to properly
quote arguments containing spaces (e.g., an argument "echo ok" renders as "echo
ok" instead of being treated as a single argument). Create a new quote-aware
join helper function that properly quotes arguments when joining them, and
replace the strings.Join(args, spaceSeparator) call in the return statement of
this function with the new helper. Apply the same helper function to all other
preview builders mentioned (at lines 307-308, 318-319, and 329-330) to ensure
consistent quote-aware argument handling across all preview outputs.
🧹 Nitpick comments (1)
pkg/container/docker.go (1)

63-68: Add validation for container ID format and test the warning-after-ID edge case.

The backward scan is already implemented and handles inline pull output correctly. However, the function lacks ID format validation—it returns any non-empty line without confirming it's actually a hex container ID. Add a regex check (docker IDs are 64-char hex) to guard against edge cases where trailing output isn't an ID, then add a test case: <valid-id>\nWARNING: ...\n to ensure the function rejects warnings that might appear after the ID.

🤖 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 `@pkg/container/docker.go` around lines 63 - 68, The extractContainerID
function currently accepts any non-empty line as a valid container ID without
validating its format. Add a regex validation check to ensure the extracted
containerID is a 64-character hexadecimal string before accepting it as
valid—this guards against edge cases where trailing output (like warnings) might
be incorrectly identified as a container ID. Additionally, add a test case for
the extractContainerID function that covers the edge case where a valid
container ID is followed by a WARNING line to verify the function correctly
identifies and validates the true container ID while rejecting any non-ID
output.
🤖 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 `@pkg/container/ephemeral.go`:
- Around line 157-159: The fmt.Errorf call in the error handling block uses two
%w verbs in a single format string, which is not the recommended pattern for
combining multiple errors. Replace this with errors.Join to explicitly combine
the create error (err) and pull error (pullErr). Wrap each individual error with
its context label using fmt.Errorf, then join them together, and finally wrap
the joined result with the overall error message "failed to create container and
pull image" to provide the full context.

---

Outside diff comments:
In `@pkg/container/ephemeral.go`:
- Around line 250-259: The current preview output uses strings.Join with a
simple space separator, which fails to properly quote arguments containing
spaces (e.g., an argument "echo ok" renders as "echo ok" instead of being
treated as a single argument). Create a new quote-aware join helper function
that properly quotes arguments when joining them, and replace the
strings.Join(args, spaceSeparator) call in the return statement of this function
with the new helper. Apply the same helper function to all other preview
builders mentioned (at lines 307-308, 318-319, and 329-330) to ensure consistent
quote-aware argument handling across all preview outputs.

---

Nitpick comments:
In `@pkg/container/docker.go`:
- Around line 63-68: The extractContainerID function currently accepts any
non-empty line as a valid container ID without validating its format. Add a
regex validation check to ensure the extracted containerID is a 64-character
hexadecimal string before accepting it as valid—this guards against edge cases
where trailing output (like warnings) might be incorrectly identified as a
container ID. Additionally, add a test case for the extractContainerID function
that covers the edge case where a valid container ID is followed by a WARNING
line to verify the function correctly identifies and validates the true
container ID while rejecting any non-ID output.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f301098d-6b4f-4494-a196-a47ba25e1da5

📥 Commits

Reviewing files that changed from the base of the PR and between d618c5c and 70974c8.

📒 Files selected for processing (9)
  • docs/prd/compose-components.md
  • docs/prd/compositions.md
  • pkg/container/common.go
  • pkg/container/common_test.go
  • pkg/container/detector.go
  • pkg/container/docker.go
  • pkg/container/ephemeral.go
  • pkg/container/ephemeral_test.go
  • pkg/container/podman.go
✅ Files skipped from review due to trivial changes (2)
  • docs/prd/compose-components.md
  • docs/prd/compositions.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/container/detector.go
  • pkg/container/podman.go
  • pkg/container/common.go

Comment thread pkg/container/ephemeral.go
Adds an EnvSetter seam so container runtimes (Docker/Podman) run their CLI
subprocesses with a materialized environment (e.g. DOCKER_CONFIG for ECR
login) instead of always inheriting os.Environ(). Wires the env through the
runner container steps (build/push/run) and updates the container-step
example, step-type docs, and the native-container-steps blog post.

Includes new env_test.go and container_env_test.go coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
pkg/container/podman.go (1)

357-362: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap PodmanRuntime.Logs failures with the static runtime error.

Line 361 returns cmd.Run() directly, which makes this method inconsistent with the runtime error-wrapping contract used elsewhere.

Proposed fix.
 func (p *PodmanRuntime) Logs(ctx context.Context, containerID string, follow bool, tail string, stdout, stderr io.Writer) error {
 	defer perf.Track(nil, "container.PodmanRuntime.Logs")()
@@
 	cmd := p.command(ctx, args...)
 	cmd.Stdout = stdout
 	cmd.Stderr = stderr
 
-	return cmd.Run()
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("%w: podman logs failed: %w", errUtils.ErrContainerRuntimeOperation, err)
+	}
+	return nil
 }

As per coding guidelines, “All errors MUST be wrapped using static errors defined in errors/errors.go.”

🤖 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 `@pkg/container/podman.go` around lines 357 - 362, The Logs method in
PodmanRuntime returns the error from cmd.Run() directly without wrapping it,
which violates the coding guideline that all errors must be wrapped using static
errors defined in errors/errors.go. Instead of returning cmd.Run() directly,
capture the error result from cmd.Run(), wrap it using an appropriate static
error wrapper from the errors package (likely following the pattern used
elsewhere in the codebase for similar runtime failures), and return the wrapped
error.

Source: Coding guidelines

pkg/container/docker.go (1)

395-399: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wrap DockerRuntime.Logs failures with the static runtime error.

Line 399 returns cmd.Run() directly, so this path breaks the runtime error contract and cannot be matched consistently with errors.Is(..., errUtils.ErrContainerRuntimeOperation).

Proposed fix.
 func (d *DockerRuntime) Logs(ctx context.Context, containerID string, follow bool, tail string, stdout, stderr io.Writer) error {
 	defer perf.Track(nil, "container.DockerRuntime.Logs")()
@@
 	cmd := d.command(ctx, args...)
 	cmd.Stdout = stdout
 	cmd.Stderr = stderr
 
-	return cmd.Run()
+	if err := cmd.Run(); err != nil {
+		return fmt.Errorf("%w: docker logs failed: %w", errUtils.ErrContainerRuntimeOperation, err)
+	}
+	return nil
 }

As per coding guidelines, “All errors MUST be wrapped using static errors defined in errors/errors.go.”

🤖 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 `@pkg/container/docker.go` around lines 395 - 399, The code returns cmd.Run()
directly without wrapping the error, violating the runtime error contract
required by the coding guidelines. Instead of directly returning cmd.Run(),
assign its result to an error variable, then wrap it with
errUtils.ErrContainerRuntimeOperation before returning. This ensures all errors
from the DockerRuntime.Logs method are consistently wrapped with the static
runtime error and can be matched using errors.Is().

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@pkg/container/docker.go`:
- Around line 395-399: The code returns cmd.Run() directly without wrapping the
error, violating the runtime error contract required by the coding guidelines.
Instead of directly returning cmd.Run(), assign its result to an error variable,
then wrap it with errUtils.ErrContainerRuntimeOperation before returning. This
ensures all errors from the DockerRuntime.Logs method are consistently wrapped
with the static runtime error and can be matched using errors.Is().

In `@pkg/container/podman.go`:
- Around line 357-362: The Logs method in PodmanRuntime returns the error from
cmd.Run() directly without wrapping it, which violates the coding guideline that
all errors must be wrapped using static errors defined in errors/errors.go.
Instead of returning cmd.Run() directly, capture the error result from
cmd.Run(), wrap it using an appropriate static error wrapper from the errors
package (likely following the pattern used elsewhere in the codebase for similar
runtime failures), and return the wrapped error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfbad11d-202e-42a7-af71-239962a1abfb

📥 Commits

Reviewing files that changed from the base of the PR and between 70974c8 and 494d47e.

📒 Files selected for processing (16)
  • examples/container-step/README.md
  • examples/container-step/atmos.yaml
  • examples/container-step/workflows/container-step.yaml
  • pkg/container/common.go
  • pkg/container/docker.go
  • pkg/container/env_test.go
  • pkg/container/podman.go
  • pkg/container/runtime.go
  • pkg/runner/step/container.go
  • pkg/runner/step/container_build.go
  • pkg/runner/step/container_env_test.go
  • pkg/runner/step/container_push.go
  • pkg/runner/step/container_run.go
  • pkg/runner/step/variables.go
  • website/blog/2026-06-17-native-container-steps.mdx
  • website/docs/workflows/_partials/_step-types.mdx
✅ Files skipped from review due to trivial changes (3)
  • examples/container-step/README.md
  • examples/container-step/atmos.yaml
  • website/docs/workflows/_partials/_step-types.mdx
🚧 Files skipped from review as they are similar to previous changes (8)
  • examples/container-step/workflows/container-step.yaml
  • website/blog/2026-06-17-native-container-steps.mdx
  • pkg/runner/step/container_push.go
  • pkg/container/runtime.go
  • pkg/runner/step/container_build.go
  • pkg/runner/step/variables.go
  • pkg/runner/step/container.go
  • pkg/runner/step/container_run.go

The linux Acceptance Tests job runs `make testacc-cover` with
`-coverpkg=./...` across the whole repo, which is memory-heavy. It ran on the
`terraform` runner whose RunsOn config has an 8 GiB RAM floor (`ram: [8, 64]`),
so spot instances as small as 8 GiB were provisioned. The coverage build was
killed (exit 137, "runner received a shutdown signal") at the coverage step on
consecutive runs — every test package passed; only the coverage collection OOMed.

Switch the linux matrix entry to `runner=large` (`ram: [16, 128]`, `disk: large`),
the same runner the `release` job already uses. macos/windows run plain
`testacc` (no coverage instrumentation) and are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
createEphemeralContainer used a single fmt.Errorf with two %w verbs to wrap the
create and pull errors. Per the project error-handling guidelines, combine
multiple error causes with errors.Join: wrap each cause with its context label
and join them under the outer message. Preserves both error chains for errors.Is.

Addresses CodeRabbit review on PR #2626.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…step handlers

Custom-command step dispatch had a broad pre-check that sent every *registered*
step type (except `exec`) — including `shell` and `atmos` — through the new
pkg/runner/step handlers, making the legacy switch cases dead code. That
regressed two ways:

- Windows: the shell handler hardcodes `exec.Command("sh", "-c", ...)`, so
  shell-based custom commands (greet/echo/deploy/show) failed with
  `exit status 126`, breaking their golden snapshots.
- Linux: the handler path spawned `atmos` grandchildren without process-group
  cleanup, leaking orphaned `atmos` processes (32 in one acceptance run vs 0 on
  main). Accumulated coverage-instrumented `atmos` processes exhausted memory and
  the on-demand runner was torn down (exit 137 / "runner received a shutdown
  signal") — which looked like an OOM/spot flake but was a process leak.

Gate on stepPkg.IsExtendedStepType instead (matching internal/exec/workflow_utils.go
and pkg/workflow/executor.go): shell/atmos/exec keep the legacy, cross-platform,
child-reaping paths; only genuinely-extended types (container, input, …) route
through the registered handlers via the default case, which now also carries the
resolved step env. Removes the now-dead runRegisteredStepHandler helper.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 18, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.29916% with 317 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.20%. Comparing base (49ce1d3) to head (b3986ed).

Files with missing lines Patch % Lines
pkg/workflow/executor.go 69.56% 37 Missing and 12 partials ⚠️
internal/exec/workflow_utils.go 40.62% 38 Missing ⚠️
pkg/workflow/container.go 88.08% 24 Missing and 14 partials ⚠️
pkg/container/sandbox.go 77.97% 25 Missing and 12 partials ⚠️
pkg/runner/step/container_run.go 86.60% 15 Missing and 15 partials ⚠️
pkg/runner/step/container_build.go 84.68% 10 Missing and 7 partials ⚠️
pkg/runner/runner.go 0.00% 15 Missing ⚠️
pkg/runner/step/output_mode.go 73.58% 13 Missing and 1 partial ⚠️
pkg/runner/step/container_push.go 86.74% 8 Missing and 3 partials ⚠️
pkg/runner/step/variables.go 80.76% 5 Missing and 5 partials ⚠️
... and 12 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2626      +/-   ##
==========================================
+ Coverage   80.05%   80.20%   +0.14%     
==========================================
  Files        1361     1371      +10     
  Lines      127594   129506    +1912     
==========================================
+ Hits       102150   103873    +1723     
- Misses      19783    19877      +94     
- Partials     5661     5756      +95     
Flag Coverage Δ
unittests 80.20% <84.29%> (+0.14%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/config/load.go 83.94% <100.00%> (+0.22%) ⬆️
pkg/container/common.go 94.35% <100.00%> (+4.70%) ⬆️
pkg/container/runtime.go 100.00% <ø> (ø)
pkg/devcontainer/runtime.go 98.70% <100.00%> (-0.03%) ⬇️
pkg/schema/schema.go 87.70% <ø> (ø)
pkg/schema/task.go 95.71% <100.00%> (+1.00%) ⬆️
pkg/config/casemap/casemap.go 96.82% <88.88%> (-3.18%) ⬇️
pkg/runner/step/executor.go 94.33% <0.00%> (-1.86%) ⬇️
pkg/runner/step/types.go 93.33% <71.42%> (-6.67%) ⬇️
pkg/container/detector.go 84.41% <90.00%> (+3.46%) ⬆️
... and 18 more

... and 12 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mergify

mergify Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

💥 This pull request now has conflicts. Could you fix it Erik Osterman (Cloud Posse) (@osterman)? 🙏

@mergify mergify Bot added the conflict This PR has conflicts label Jun 21, 2026
…ep-prd

# Conflicts:
#	website/docs/cli/configuration/commands.mdx
#	website/plugins/file-browser/index.js
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jun 21, 2026
The "Check Markdown Links" job failed with a network connection error to
https://no-color.org/ (referenced from docs/prd/help-system-architecture.md and
docs/prd/io-handling-strategy.md). The URL is the canonical NO_COLOR spec site
and resolves in a real browser; it just intermittently refuses connections from
CI runners. Add it to the lychee exclude list, consistent with the existing
entries for gnu.org, tldp.org, kubernetes.io, nx.dev, and other valid-but-flaky
external links.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mergify

mergify Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

💥 This pull request now has conflicts. Could you fix it Erik Osterman (Cloud Posse) (@osterman)? 🙏

@mergify mergify Bot added the conflict This PR has conflicts label Jun 21, 2026
…ep-prd

# Conflicts:
#	lychee.toml
#	pkg/schema/schema.go
@mergify mergify Bot removed the conflict This PR has conflicts label Jun 22, 2026
@aknysh
Andriy Knysh (aknysh) merged commit c61e38b into main Jun 23, 2026
63 checks passed
@atmos-pro

atmos-pro Bot commented Jun 23, 2026 •

Copy link
Copy Markdown
Contributor

Tip

Atmos Pro  

No affected stacks workflow was detected for this pull request.
If this is expected, no action is needed.
Learn More. Ask AI.

@aknysh
Andriy Knysh (aknysh) deleted the osterman/container-step-prd branch June 23, 2026 23:54
@github-actions

Copy link
Copy Markdown

Warning

Release Documentation Required

This PR is labeled minor or major and requires documentation updates:

  • Changelog entry - Add a blog post in website/blog/YYYY-MM-DD-feature-name.mdx
  • Roadmap update - Update website/src/data/roadmap.js with the new milestone

Alternatively: If this change doesn't require release documentation, remove the minor or major label.

@github-actions

Copy link
Copy Markdown

These changes were released in v1.222.0-rc.8.

This branch was successfully deployed

1 active deployment
preview — b3986ed3 Deployed Jun 23, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor New features that do not break anything size/xxl

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants