dirigent-block-execute 0.17.2__tar.gz → 0.18.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/PKG-INFO +4 -4
  2. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/pyproject.toml +4 -4
  3. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/pyproject.toml.orig +4 -4
  4. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/build.py +2 -6
  5. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/compose.py +11 -22
  6. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/docker.py +21 -11
  7. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/git.py +3 -6
  8. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/messages.py +1 -1
  9. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/shell.py +2 -6
  10. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/subprocess.py +27 -20
  11. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/LICENSE +0 -0
  12. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/README.md +0 -0
  13. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/__init__.py +0 -0
  14. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/capture.py +0 -0
  15. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/environment.py +0 -0
  16. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/py.typed +0 -0
  17. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/reap.py +0 -0
  18. {dirigent_block_execute-0.17.2 → dirigent_block_execute-0.18.0}/src/dirigent_block_execute/secrets.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dirigent-block-execute
3
- Version: 0.17.2
3
+ Version: 0.18.0
4
4
  Summary: The execute block family for dirigent: shell, docker, compose, buildx, and a checkout.
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
7
7
  Classifier: Programming Language :: Python :: 3
8
8
  Classifier: Programming Language :: Python :: 3.13
9
- Requires-Dist: dirigent-block-http==0.17.2
10
- Requires-Dist: dirigent-common==0.17.2
11
- Requires-Dist: dirigent-plugin==0.17.2
9
+ Requires-Dist: dirigent-block-http==0.18.0
10
+ Requires-Dist: dirigent-common==0.18.0
11
+ Requires-Dist: dirigent-plugin==0.18.0
12
12
  Requires-Dist: httpx2>=2.12.0
13
13
  Requires-Python: >=3.13
14
14
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-block-execute"
3
- version = "0.17.2"
3
+ version = "0.18.0"
4
4
  description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,9 +11,9 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-http==0.17.2",
15
- "dirigent-common==0.17.2",
16
- "dirigent-plugin==0.17.2",
14
+ "dirigent-block-http==0.18.0",
15
+ "dirigent-common==0.18.0",
16
+ "dirigent-plugin==0.18.0",
17
17
  "httpx2>=2.12.0",
18
18
  ]
19
19
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dirigent-block-execute"
3
- version = "0.17.2"
3
+ version = "0.18.0"
4
4
  description = "The execute block family for dirigent: shell, docker, compose, buildx, and a checkout."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -11,9 +11,9 @@ classifiers = [
11
11
  "Programming Language :: Python :: 3.13",
12
12
  ]
13
13
  dependencies = [
14
- "dirigent-block-http==0.17.2",
15
- "dirigent-common==0.17.2",
16
- "dirigent-plugin==0.17.2",
14
+ "dirigent-block-http==0.18.0",
15
+ "dirigent-common==0.18.0",
16
+ "dirigent-plugin==0.18.0",
17
17
  "httpx2>=2.12.0",
18
18
  ]
19
19
 
@@ -176,22 +176,18 @@ class DockerBuildOperator(Operator[DockerBuildConfig, DockerBuildOutput]):
176
176
  if config.push and (settings is None or not settings.authenticates):
177
177
  raise BlockFailure(NO_REGISTRY_CREDENTIAL, error_class=ErrorClass.REJECTED)
178
178
  timeout = config.timeout.total_seconds()
179
- artifacts = subprocess.prefix(ctx, "build")
180
- stdout_uri = f"{artifacts}-stdout.txt"
181
- build_log_uri = f"{artifacts}-build.log"
182
179
 
183
180
  with sealed(settings, root, isolate_config=config.push) as material:
184
181
  environ = daemon_environment(
185
182
  subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
186
183
  )
187
184
  write_cli_config(root / ".docker")
188
- code, out, err = await subprocess.run(
185
+ code, out, err, stdout_uri, build_log_uri = await subprocess.run(
189
186
  directory=root,
190
187
  ctx=ctx,
191
- stdout_uri=stdout_uri,
192
- stderr_uri=build_log_uri,
193
188
  timeout_seconds=timeout,
194
189
  environ=environ,
190
+ stderr_name="build.log",
195
191
  argv=build_argv(config, context, iidfile),
196
192
  what="docker buildx build",
197
193
  redact=material.secrets,
@@ -141,10 +141,9 @@ class DockerComposeUpConfig(BlockModel):
141
141
  pipeline document itself."""
142
142
 
143
143
  project_name: str | None = None
144
- """The compose project (``-p``). Defaults to a deterministic name derived from the run id,
145
-
146
- so this ``up``, the steps that drive it, and a later ``down`` all address the same project,
147
- and two runs never collide."""
144
+ """The compose project (``-p``). Defaults to a deterministic name derived from the run id, so
145
+ this ``up``, the steps that drive it, and a later ``down`` all address the same project, and
146
+ two runs never collide."""
148
147
 
149
148
  profiles: list[str] = Field(default_factory=list[str])
150
149
  """Compose profiles to activate (``--profile``)."""
@@ -213,13 +212,11 @@ class DockerComposeDownConfig(BlockModel):
213
212
 
214
213
  project_name: str | None = None
215
214
  """The compose project (``-p``). Defaults to the same deterministic name ``up`` derives from
216
-
217
- the run id, so a teardown step in the same run addresses the stack the ``up`` created with
218
- no wiring from the author."""
215
+ the run id, so a teardown step in the same run addresses the stack the ``up`` created with no
216
+ wiring from the author."""
219
217
 
220
218
  file: str | None = None
221
219
  """A compose file, only if the CLI needs ``-f`` to resolve the project; a project tears down
222
-
223
220
  by its label alone otherwise. As a path inside the run's work directory."""
224
221
 
225
222
  content: str | None = None
@@ -342,7 +339,6 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
342
339
  assert compose_file is not None
343
340
  project = config.project_name or _default_project(ctx)
344
341
  timeout = config.timeout.total_seconds()
345
- stdout_uri, stderr_uri = _artifact_uris(ctx, "up")
346
342
 
347
343
  with sealed(_connection(config.connection, ctx), root) as material:
348
344
  environ = daemon_environment(
@@ -350,13 +346,13 @@ class DockerComposeUpOperator(Operator[DockerComposeUpConfig, DockerComposeUpOut
350
346
  )
351
347
  write_cli_config(root / ".docker")
352
348
  try:
353
- code, out, err = await subprocess.run(
349
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
354
350
  directory=root,
355
351
  ctx=ctx,
356
- stdout_uri=stdout_uri,
357
- stderr_uri=stderr_uri,
358
352
  timeout_seconds=timeout,
359
353
  environ=environ,
354
+ stdout_name="up-stdout",
355
+ stderr_name="up-stderr",
360
356
  argv=up_argv(config, project, compose_file, root),
361
357
  what="docker compose up",
362
358
  )
@@ -411,20 +407,19 @@ class DockerComposeDownOperator(Operator[DockerComposeDownConfig, DockerComposeD
411
407
  compose_file = _compose_file(config.file, config.content, ctx, root)
412
408
  project = config.project_name or _default_project(ctx)
413
409
  timeout = config.timeout.total_seconds()
414
- stdout_uri, stderr_uri = _artifact_uris(ctx, "down")
415
410
 
416
411
  with sealed(_connection(config.connection, ctx), root) as material:
417
412
  environ = daemon_environment(
418
413
  subprocess.environment([*DAEMON_ENV, *config.env_allowlist], config.env, root), material
419
414
  )
420
415
  write_cli_config(root / ".docker")
421
- code, out, err = await subprocess.run(
416
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
422
417
  directory=root,
423
418
  ctx=ctx,
424
- stdout_uri=stdout_uri,
425
- stderr_uri=stderr_uri,
426
419
  timeout_seconds=timeout,
427
420
  environ=environ,
421
+ stdout_name="down-stdout",
422
+ stderr_name="down-stderr",
428
423
  argv=down_argv(config, project, compose_file, root),
429
424
  what="docker compose down",
430
425
  )
@@ -656,12 +651,6 @@ def _work_relative(path: Path, root: Path) -> str:
656
651
  return str(path.relative_to(root))
657
652
 
658
653
 
659
- def _artifact_uris(ctx: StepContext, action: str) -> tuple[str, str]:
660
- """Where the CLI's two streams are written for this attempt."""
661
- root = f"{subprocess.prefix(ctx, 'compose')}-{action}"
662
- return f"{root}-stdout.txt", f"{root}-stderr.txt"
663
-
664
-
665
654
  def _default_project(ctx: StepContext) -> str:
666
655
  """A project name that is the same across a run's up, drive and down, and unique between runs."""
667
656
  return f"dirigent-{ctx.run_id.hex}"
@@ -109,8 +109,8 @@ NOT_FOUND = 404
109
109
  CONFLICT = 409
110
110
 
111
111
 
112
- #: The schemes a daemon URL may be written in, which are the ones the docker CLI accepts.
113
- DAEMON_SCHEMES = ("tcp://", "ssh://", "unix://")
112
+ #: The schemes a daemon URL may be written in, which are the ones the worker speaks.
113
+ DAEMON_SCHEMES = ("tcp://", "unix://")
114
114
 
115
115
  #: What a registry credential that names no registry authenticates to.
116
116
  DOCKER_HUB = "docker.io"
@@ -123,7 +123,7 @@ class DockerConnectionConfig(BlockModel):
123
123
  """A daemon to reach, a registry to authenticate to, or both."""
124
124
 
125
125
  host: str = ""
126
- """The daemon, as ``tcp://host:2376``, ``ssh://user@host`` or ``unix:///var/run/docker.sock``.
126
+ """The daemon, as ``tcp://host:2376`` or ``unix:///var/run/docker.sock``.
127
127
 
128
128
  Left empty, a step using this connection reaches the daemon the worker's own environment
129
129
  names, which is what a step naming no connection does."""
@@ -157,7 +157,7 @@ class DockerConnectionConfig(BlockModel):
157
157
  def _check_shape(self) -> "DockerConnectionConfig":
158
158
  """Refuse a daemon URL of an unknown form, half a TLS triple, and half a credential."""
159
159
  if self.host and not self.host.startswith(DAEMON_SCHEMES):
160
- raise ValueError(NOT_A_DAEMON_SCHEME.render(schemes=", ".join(DAEMON_SCHEMES), host=repr(self.host)))
160
+ raise ValueError(NOT_A_DAEMON_SCHEME.render(schemes=" or ".join(DAEMON_SCHEMES), host=repr(self.host)))
161
161
  pems = [self.tls_ca, self.tls_cert, self.tls_key]
162
162
  if any(pem is not None for pem in pems) and not all(pem is not None for pem in pems):
163
163
  raise ValueError(TLS_IS_ALL_THREE.render())
@@ -493,6 +493,7 @@ class DockerRunOutput(BlockModel):
493
493
  """What the container did, with its full streams and produced files addressable as artifacts."""
494
494
 
495
495
  exit_code: int
496
+ """The status the container exited with."""
496
497
 
497
498
  stdout: str
498
499
  """The head of what the container printed, cut at the instance's inline capture size.
@@ -521,7 +522,11 @@ class DockerRunOutput(BlockModel):
521
522
  """Whether ``stderr`` above is short of the stream, which is an independent question."""
522
523
 
523
524
  container_id: str
525
+ """The container that ran, by the id the daemon gave it."""
526
+
524
527
  image: str
528
+ """The image the container ran."""
529
+
525
530
  outputs: dict[str, str] = Field(default_factory=dict[str, str])
526
531
  """Where each declared output was written, by the name the config gave it: the storage URI,
527
532
  or the path relative to the run's work directory it landed at."""
@@ -582,21 +587,16 @@ class DockerRunOperator(Operator[DockerRunConfig, DockerRunOutput]):
582
587
 
583
588
  async def fetch(self, handle: RemoteHandle, config: DockerRunConfig, ctx: StepContext) -> DockerRunOutput:
584
589
  """Collect the exit code, the streams, and the declared files, then drop the container."""
585
- artifacts = subprocess.prefix(ctx, "docker")
586
- stdout_uri = f"{artifacts}-stdout.txt"
587
- stderr_uri = f"{artifacts}-stderr.txt"
588
590
  with _material(config, ctx) as environ:
589
591
  async with _daemon(config, environ) as daemon:
590
592
  inspected = await daemon.inspect(handle.ref)
591
593
  if inspected is None:
592
594
  raise BlockFailure(CONTAINER_GONE, error_class=ErrorClass.TRANSIENT, container=handle.ref[:12])
593
- async with (
594
- ctx.storage.open_write(stdout_uri) as out_sink,
595
- ctx.storage.open_write(stderr_uri) as err_sink,
596
- ):
595
+ async with ctx.capture("stdout") as out_sink, ctx.capture("stderr") as err_sink:
597
596
  stdout, stderr = await drain_logs(
598
597
  daemon, handle.ref, ctx.inline_capture, sinks=(out_sink, err_sink)
599
598
  )
599
+ stdout_uri, stderr_uri = out_sink.uri, err_sink.uri
600
600
  collected = await _collect_outputs(config, handle, ctx)
601
601
  # The run log gets only what the probes have not already streamed: the segment
602
602
  # between the last committed cursor and the exit. The full read above is for
@@ -1153,6 +1153,9 @@ def resolve_endpoint(explicit_socket: str | None, environ: Mapping[str, str] | N
1153
1153
  pipeline's containers off the host. ``DOCKER_TLS_VERIFY`` with ``DOCKER_CERT_PATH`` turns
1154
1154
  that into https with client certificates, the way the docker CLI reads the same variables.
1155
1155
 
1156
+ A ``DOCKER_HOST`` in any other scheme is refused instead of resolving to the local socket:
1157
+ the daemon is reached over HTTP here, and nothing tunnels an ``ssh://`` one.
1158
+
1156
1159
  ``environ`` is what a step with a ``docker`` connection reads instead of the worker's own.
1157
1160
  """
1158
1161
  source = os.environ if environ is None else environ
@@ -1173,6 +1176,13 @@ def resolve_endpoint(explicit_socket: str | None, environ: Mapping[str, str] | N
1173
1176
  if host.startswith("unix://"):
1174
1177
  socket = host.removeprefix("unix://") or DEFAULT_SOCKET_PATH
1175
1178
  return DaemonEndpoint(DAEMON_BASE_URL, socket, True, None, socket)
1179
+ if host:
1180
+ raise BlockFailure(
1181
+ NOT_A_DAEMON_SCHEME,
1182
+ error_class=ErrorClass.REJECTED,
1183
+ schemes=" or ".join(DAEMON_SCHEMES),
1184
+ host=repr(host),
1185
+ )
1176
1186
  return DaemonEndpoint(DAEMON_BASE_URL, DEFAULT_SOCKET_PATH, True, None, DEFAULT_SOCKET_PATH)
1177
1187
 
1178
1188
 
@@ -381,16 +381,13 @@ class _Git:
381
381
  artifact rather than a buffer in the worker. Each command writes its own pair, named for
382
382
  its subcommand; the pair the last one wrote is what the output carries.
383
383
  """
384
- artifacts = f"{subprocess.prefix(self.ctx, 'git')}-{argv[0]}"
385
- stdout_uri = f"{artifacts}-stdout.txt"
386
- stderr_uri = f"{artifacts}-stderr.txt"
387
- code, out, err = await subprocess.run(
384
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
388
385
  directory=directory,
389
386
  ctx=self.ctx,
390
- stdout_uri=stdout_uri,
391
- stderr_uri=stderr_uri,
392
387
  timeout_seconds=self.timeout,
393
388
  environ=self.environ,
389
+ stdout_name=f"{argv[0]}-stdout",
390
+ stderr_name=f"{argv[0]}-stderr",
394
391
  argv=[self.binary, *argv],
395
392
  what=f"git {argv[0]}",
396
393
  redact=self.secrets,
@@ -125,7 +125,7 @@ CWD_STAYS_INSIDE = EXECUTE.define(
125
125
 
126
126
  NOT_A_DAEMON_SCHEME = EXECUTE.define(
127
127
  "not_a_daemon_scheme",
128
- "a docker host is one of {schemes}, and {host} is none of them",
128
+ "a docker host is {schemes}, and {host} is not one of them",
129
129
  )
130
130
 
131
131
  TLS_IS_ALL_THREE = EXECUTE.define(
@@ -77,6 +77,7 @@ class ShellRunOutput(BlockModel):
77
77
  """What the command did, with its full streams addressable as artifacts."""
78
78
 
79
79
  exit_code: int
80
+ """The status the command exited with."""
80
81
 
81
82
  stdout: str
82
83
  """The head of what the command printed, cut at the instance's inline capture size.
@@ -136,14 +137,9 @@ class ShellRunOperator(Operator[ShellRunConfig, ShellRunOutput]):
136
137
  workspace = subprocess.workspace(ctx, "shell")
137
138
  directory = workspace / config.cwd if config.cwd else workspace
138
139
  directory.mkdir(parents=True, exist_ok=True)
139
- artifacts = subprocess.prefix(ctx, "shell")
140
- stdout_uri = f"{artifacts}-stdout.txt"
141
- stderr_uri = f"{artifacts}-stderr.txt"
142
- code, out, err = await subprocess.run(
140
+ code, out, err, stdout_uri, stderr_uri = await subprocess.run(
143
141
  directory=directory,
144
142
  ctx=ctx,
145
- stdout_uri=stdout_uri,
146
- stderr_uri=stderr_uri,
147
143
  timeout_seconds=config.timeout.total_seconds(),
148
144
  environ=_environment(config, directory),
149
145
  argv=config.argv or None,
@@ -14,6 +14,7 @@ import os
14
14
  import signal
15
15
  from collections.abc import Sequence
16
16
  from pathlib import Path
17
+ from typing import NamedTuple
17
18
 
18
19
  from dirigent_block_execute.capture import Drained, LiveLines, drain
19
20
  from dirigent_block_execute.environment import allowed
@@ -24,22 +25,26 @@ from dirigent_plugin import BlockFailure, ErrorClass, StepContext
24
25
  BASELINE_ENV = ("PATH", "LANG", "LC_ALL", "TZ")
25
26
 
26
27
 
28
+ class Ran(NamedTuple):
29
+ """What one child process left behind: how it exited, both streams, and where they were written."""
30
+
31
+ code: int
32
+ out: Drained
33
+ err: Drained
34
+ stdout_uri: str
35
+ stderr_uri: str
36
+
37
+
27
38
  def segment(ctx: StepContext, name: str) -> str:
28
- """The path this attempt's files sit at, under either of the run's two roots.
39
+ """The path this attempt's files sit at, inside the run's work directory.
29
40
 
30
- ``{name}/{step}[/{item}]/attempt-{n}``. Both roots are the run's, so the step key and,
31
- inside a fan-out, the item id are what keep two steps of one run and two items of one step
32
- out of each other's files and working directories.
41
+ ``{name}/{step}[/{item}]/attempt-{n}``. The step key, and inside a fan-out the item id,
42
+ are what keep two steps of one run and two items of one step out of each other's files.
33
43
  """
34
44
  item = f"/{ctx.run_item_id}" if ctx.run_item_id is not None else ""
35
45
  return f"{name}/{ctx.step}{item}/attempt-{ctx.attempt}"
36
46
 
37
47
 
38
- def prefix(ctx: StepContext, name: str) -> str:
39
- """The storage URI this attempt's artifact names are built on, under the run's scratch prefix."""
40
- return f"{ctx.scratch.rstrip('/')}/{segment(ctx, name)}"
41
-
42
-
43
48
  def workspace(ctx: StepContext, name: str) -> Path:
44
49
  """This attempt's working directory inside the run's work directory, made if absent."""
45
50
  directory = ctx.work / segment(ctx, name)
@@ -59,22 +64,24 @@ async def run(
59
64
  *,
60
65
  directory: Path,
61
66
  ctx: StepContext,
62
- stdout_uri: str,
63
- stderr_uri: str,
64
67
  timeout_seconds: float,
65
68
  environ: dict[str, str],
69
+ stdout_name: str = "stdout",
70
+ stderr_name: str = "stderr",
66
71
  argv: list[str] | None = None,
67
72
  command: str | None = None,
68
73
  what: str = "the command",
69
74
  redact: Sequence[str] = (),
70
- ) -> tuple[int, Drained, Drained]:
75
+ ) -> Ran:
71
76
  """Start the process, drain it under the timeout, and leave nothing of it behind.
72
77
 
73
- Both pipes are read as the process writes them and written straight through to storage,
74
- so what is held is two bounded ends of each stream rather than all of it: a process that
75
- prints more than the worker has memory for is a file, not a dead worker. Each stream's
76
- first lines go into the run log where they are printed, so a long command is visible
77
- working; ``redact`` names the strings it was handed that must not reach one of them.
78
+ Both pipes are read as the process writes them and written straight through to a capture
79
+ the context opens and names, so what is held is two bounded ends of each stream rather
80
+ than all of it: a process that prints more than the worker has memory for is a file, not
81
+ a dead worker. The two names distinguish one command's streams from another's where a
82
+ step runs more than one. Each stream's first lines go into the run log where they are
83
+ printed, so a long command is visible working; ``redact`` names the strings it was
84
+ handed that must not reach one of them.
78
85
 
79
86
  The process leads a session of its own, so what it starts is killable with it: a command
80
87
  is usually ``sh -c``, and killing the shell alone leaves the children doing the work.
@@ -106,7 +113,7 @@ async def run(
106
113
  pgid = group_of(process)
107
114
  try:
108
115
  async with asyncio.timeout(timeout_seconds):
109
- async with ctx.storage.open_write(stdout_uri) as out_sink, ctx.storage.open_write(stderr_uri) as err_sink:
116
+ async with ctx.capture(stdout_name) as out_sink, ctx.capture(stderr_name) as err_sink:
110
117
  out, err = await asyncio.gather(
111
118
  drain(
112
119
  process.stdout,
@@ -126,7 +133,7 @@ async def run(
126
133
  raise BlockFailure(TIMED_OUT, error_class=ErrorClass.TRANSIENT, what=what, seconds=timeout_seconds) from error
127
134
  finally:
128
135
  await end(process, pgid)
129
- return process.returncode or 0, out, err
136
+ return Ran(process.returncode or 0, out, err, out_sink.uri, err_sink.uri)
130
137
 
131
138
 
132
139
  async def output(
@@ -207,12 +214,12 @@ def kill(pgid: int, pid: int) -> None:
207
214
 
208
215
  __all__ = [
209
216
  "BASELINE_ENV",
217
+ "Ran",
210
218
  "end",
211
219
  "environment",
212
220
  "group_of",
213
221
  "kill",
214
222
  "output",
215
- "prefix",
216
223
  "run",
217
224
  "segment",
218
225
  "workspace",