Skip to content

Commit 39a3e31

Browse files
Merge pull request #8 from nteract/GEN-25_oneshot_command
GEN-25: oneshot command
2 parents fdd7c90 + 318e2f2 commit 39a3e31

23 files changed

Lines changed: 3709 additions & 103 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
steps:
2323
- uses: actions/checkout@v4
2424
- uses: dtolnay/rust-toolchain@stable
25-
- run: cargo test --all --locked
25+
- run: cargo test --all --locked --features test-config-override
2626

2727
doc:
2828
name: cargo doc (allez, condarc)
@@ -65,7 +65,7 @@ jobs:
6565
# Revisit this number at T056/T058 once the full `ephemeral/` module
6666
# (including its Windows-only/`#[cfg(windows)]` paths this Linux job
6767
# can't itself exercise) has landed and real coverage is measured.
68-
- run: cargo llvm-cov --all --locked --fail-under-lines 85
68+
- run: cargo llvm-cov --all --locked --fail-under-lines 85 --features test-config-override
6969

7070
conformance:
7171
name: condarc conformance tests

Cargo.toml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ rattler_digest = "1.3.2"
2828
rattler_virtual_packages = "4.0.0"
2929
rattler_shell = "0.27.11"
3030
ulid = "1"
31-
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync"] }
31+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "sync", "process", "signal", "time"] }
3232
# Pinned to the major version `rattler_networking` (a transitive dependency
3333
# of `rattler`, via `astral-reqwest-middleware`/`ambient-id`) itself
3434
# resolves to -- confirmed via `cargo tree -i reqwest` to be 0.13, not 0.12,
@@ -52,6 +52,7 @@ windows-sys = { version = "0.61.2", features = [
5252
"Win32_Security",
5353
"Win32_Security_Authorization",
5454
"Win32_Storage_FileSystem",
55+
"Win32_System_Console",
5556
"Win32_System_Threading",
5657
] }
5758

@@ -69,6 +70,16 @@ windows-sys = { version = "0.61.2", features = [
6970
[features]
7071
conformance-tests = []
7172
network-tests = []
73+
# Gates a test-only `ALLEZ_CONDARC_PATH` environment-variable check in
74+
# `channel_config::default_condarc_path()` (see
75+
# specs/GEN-25_oneshot_command/research.md § Test strategy), plus a
76+
# test-only `ephemeral::test_create_owner_only_directory` seam
77+
# `tests/oneshot_exec.rs`'s Windows harness uses to pre-create an
78+
# already-owner-only `ALLEZ_EPHEMERAL_ROOT` for its root-reuse scenarios.
79+
# Not part of any `default = [...]` list -- there is none in this
80+
# `Cargo.toml` today -- so a release build has no code path that reads
81+
# that env var, or exposes that seam, at all.
82+
test-config-override = []
7283

7384
# Explicit target so `required-features` can gate it; cargo's test
7485
# autodiscovery still picks up every other file under tests/ (e.g.
@@ -78,6 +89,16 @@ name = "condarc_conformance"
7889
path = "tests/condarc_conformance.rs"
7990
required-features = ["conformance-tests"]
8091

92+
# Gated the same way as `condarc_conformance` above, but for a different
93+
# reason: `test-config-override` exists for the `ALLEZ_CONDARC_PATH`
94+
# security concern (research.md), not test speed -- see Makefile's `test`
95+
# target and .github/workflows/ci.yml's `test`/`coverage` jobs, both of
96+
# which pass `--features test-config-override` on their default invocation.
97+
[[test]]
98+
name = "oneshot_exec"
99+
path = "tests/oneshot_exec.rs"
100+
required-features = ["test-config-override"]
101+
81102
[dev-dependencies]
82103
assert_cmd = "2.2.2"
83104
jsonschema = { version = "0.48.5", default-features = false }

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ help: ## Show this help
1010
@grep -E '^[a-zA-Z_-]+:.*## ' $(MAKEFILE_LIST) | sort | awk -F ':.*## ' '{printf "%-24s %s\n", $$1, $$2}'
1111

1212
test: ## Run the full cargo test suite
13-
cargo test --all
13+
cargo test --all --features test-config-override
1414

1515
# Scoped to `-p condarc --lib -p allez --lib` rather than `--workspace`:
1616
# `--workspace` would also try (and fail, for unrelated reasons) to build
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Interface Contract: `allez oneshot` CLI surface
2+
3+
This feature exposes a **CLI subcommand contract**: exit codes, JSON/
4+
human stdout/stderr shapes, and the `tracing` observability schema
5+
extension — not a new Rust library API (that's GEN-23/GEN-24's contracts,
6+
consumed here unmodified). This is what an automated caller (spec.md's
7+
Operating Context: an AI agent, not a human at a terminal) can rely on.
8+
9+
## Invocation shape (unchanged from GEN-22's existing scaffold)
10+
11+
```text
12+
allez oneshot [PACKAGES]... -- <COMMAND> [ARGS...]
13+
```
14+
15+
- `PACKAGES`: zero or more package-name tokens before `--`. Zero is valid
16+
(FR-001's Acceptance Scenario 2) — resolves to the configured default/
17+
override package set, not an error.
18+
- `-- <COMMAND> [ARGS...]`: required (FR-011); rejected as a usage error
19+
(exit `2`, category `missing_pass_through_command`) if `--` is absent,
20+
or present with nothing after it — enforced at the dispatch layer by the
21+
existing `cli::validate_pass_through`, unchanged by this ticket.
22+
23+
## Exit codes (FR-006)
24+
25+
| Code | Meaning | Caller-facing category (stderr) | FR-012 observability category |
26+
|---|---|---|---|
27+
| `0` | Pass-through command started and exited `0`. | N/A | N/A |
28+
| `1..=255` (pass-through's own code) | Pass-through command started and exited normally with that code. **Authoritative once the command starts** — even if it happens to equal one of the values below (FR-006's own explicit caveat). | None — FR-013: no envelope at all once started. | None (a normal exit carries no `failure_category`). |
29+
| `1` | Environment creation failed (FR-010), activation of an already-created environment failed, **or** registering a signal listener failed (this plan's `activation_failed`/`signal_setup_failed`; see `research.md`). | One of `unresolvable_package`, `integrity_verification_failed`, `unwritable_location`, `no_channels_configured`, `activation_failed`, or `signal_setup_failed`. `teardown_failed` never appears here — it appears only as `cleanup_category` in the dual-failure case below, alongside one of the other four `EphemeralEnvError` categories. | Same category, mirrored. |
30+
| `2` | Usage error (FR-011) — no `--`, or `--` with nothing after it. | `missing_pass_through_command`. | N/A — this case never reaches `oneshot::run` at all; covered instead by `main.rs`'s own pre-existing, schema-versioned usage-rejection record (see Observability contract below). |
31+
| `126` | Pass-through program found but could not be executed (FR-008). | `pass_through_not_executable`. | Same category, mirrored. |
32+
| `127` | Pass-through program's name could not be found (FR-008). | `pass_through_not_found`. | Same category, mirrored. |
33+
| `128 + N` | Pass-through program terminated by signal `N`, on a platform where that concept exists (FR-007). | **None** — the command already started, so FR-013 forbids any caller-facing message or category for this outcome; `pass_through_terminated_by_signal` exists *only* as an FR-012 observability category, never on stderr. | `pass_through_terminated_by_signal`. |
34+
35+
Every other subcommand's own exit-code surface is unaffected (FR-006).
36+
37+
## stdout/stderr contract
38+
39+
**Before the pass-through program starts** (usage error, environment-
40+
creation failure, or pass-through-not-found/not-executable/activation
41+
failure): identical to every other existing `allez` subcommand's failure
42+
convention — `output::render_error` (or the new, additive `output::
43+
render_ephemeral_creation_failure` for the dual-failure case below) on
44+
stderr; stdout is empty. JSON is the default; `--human` selects the
45+
human-readable form. Shape (JSON default):
46+
47+
```json
48+
{"schema_version": "0.1.0-unstable", "category": "unresolvable_package", "message": "could not resolve package `nonexistent-pkg-xyz`"}
49+
```
50+
51+
**FR-010 dual-failure case** (creation failed *and* its own rollback also
52+
failed) — additive, optional fields, so this remains backward-compatible
53+
with every existing single-failure JSON consumer that only reads
54+
`category`/`message`:
55+
56+
```json
57+
{
58+
"schema_version": "0.1.0-unstable",
59+
"category": "unwritable_location",
60+
"message": "ephemeral environment location is unwritable",
61+
"cleanup_category": "teardown_failed",
62+
"cleanup_message": "ephemeral environment teardown failed"
63+
}
64+
```
65+
66+
`--human` mode renders both via one line, reusing `CreationFailure`'s own
67+
existing `Display` impl (GEN-24) verbatim — it already produces `"{error}
68+
(cleanup also failed: {cleanup})"` — so no separate human-mode dual-field
69+
logic is needed; only the JSON path needs the two new optional fields.
70+
71+
**Once the pass-through program has successfully started** (FR-013):
72+
`allez` writes no further caller-facing result payload of its own to
73+
stdout or stderr, ever again, for this invocation. The pass-through
74+
program's own stdout/stderr — streamed live, each stream kept separate
75+
(FR-004) — *is* the entire visible output; `allez`'s own final exit code
76+
is the entire machine-actionable signal. This is the one documented
77+
exception to Constitution III's dual-format convention (spec.md FR-013
78+
itself says so) — there is no JSON/human rendering to reconcile because
79+
there is no separate `allez`-authored result payload at all for this
80+
outcome. The `RUST_LOG`-gated `tracing` channel (Observability contract,
81+
below) is not this result payload: it is silent unless the caller
82+
explicitly opts in via `RUST_LOG`, and even then carries only the fixed,
83+
schema-versioned `OneshotOutcomeEvent` fields — never a second copy of,
84+
or a substitute for, the pass-through program's own output or exit code.
85+
86+
## Observability contract (FR-012, additive extension)
87+
88+
The "rejected as a usage error, never attempted" case (no `--`, or `--`
89+
with nothing after it) is already covered by `main.rs`'s existing
90+
`exit_on_invalid_pass_through`/`tracing::warn!(operation, category, ...)`
91+
call site — that rejection happens before `oneshot::run` (and therefore
92+
before any `EnvironmentId` exists to correlate a new event by) is ever
93+
reached, so it needs no new event type. This ticket adds one small,
94+
additive field to that pre-existing call, `schema_version` (the same
95+
value `OneshotOutcomeEvent` below uses), so the record satisfies FR-012's
96+
"carrying its own documented schema version" clause explicitly; no
97+
correlation identifier is added, since a rejected invocation never
98+
produces more than this one record, and FR-012's correlation requirement
99+
exists to tie multiple records together.
100+
101+
For every invocation that passes usage validation, exactly one `tracing`
102+
event (in addition to whatever `ephemeral::mod.rs`'s own `create`/
103+
`install`/`teardown` events already emit — unchanged by this ticket):
104+
105+
1. Immediately after `create_ephemeral_environment` resolves — carries
106+
`pass_through_started: false` if it failed (with `failure_category`/
107+
`message`, plus `cleanup_category`/`cleanup_message` for the dual-
108+
failure case). Emitted only on failure; a success is not separately
109+
recorded here, since the terminal event below already reports
110+
`pass_through_started: true` for that case, and this ticket emits
111+
exactly one event per invocation that reaches `oneshot::run`, never
112+
two.
113+
2. Immediately after the pass-through program's own outcome is known
114+
(started-and-exited, started-and-signaled, or could-not-start) —
115+
`pass_through_started: true` unless the failure is `NotFound`/
116+
`NotExecutable`/`ActivationFailed`/`SignalSetupFailed`, all four of
117+
which are pre-start.
118+
119+
Exactly one of the two above ever fires for a given invocation — never
120+
both, and never during the pass-through program's own execution. Both are
121+
emitted strictly outside the FR-004 streaming window — never while the
122+
pass-through program is running — per `OneshotOutcomeEvent`
123+
(`data-model.md`). `RUST_LOG` gates visibility identically to every other
124+
`tracing::*!` call site already in this codebase (`observability.rs`,
125+
unchanged): silent unless explicitly set, matching the existing "never
126+
interleave with the single-JSON-object stderr error contract" guarantee
127+
`tests/cli_scaffold.rs`'s `t063`/`t063a`/`t063b` already lock in for other
128+
subcommands.
129+
130+
## Non-goals of this contract
131+
132+
- No change to `create`/`list`/`remove`'s own exit-code/JSON surface.
133+
- No change to `run`/`sandbox`'s pass-through behavior — this ticket wires
134+
only `oneshot`; `pass_through.rs`'s functions are written as plain
135+
`pub(crate)` building blocks, but wiring `run`/`sandbox` to them is
136+
explicitly out of this ticket's scope, and no future consumer is
137+
assumed.
138+
- No environment-removal/teardown surface of any kind (FR-009) — GEN-24
139+
exposes no environment-removal API at all for this ticket to call.
140+
- No new CLI flag — `oneshot`'s argument shape (`PackagesAndCommandArgs`)
141+
is unchanged from GEN-22's existing scaffold. `ALLEZ_CONDARC_PATH` (see
142+
`research.md` § Test strategy) is a test-only internal environment
143+
variable, gated behind the non-default `test-config-override` Cargo
144+
feature — a release build of `allez` has no code path that reads it at
145+
all, and it is not part of this contract.

0 commit comments

Comments
 (0)