pyplines-builder 2026.9.2a1__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 (25) hide show
  1. pyplines_builder-2026.9.2a1/PKG-INFO +114 -0
  2. pyplines_builder-2026.9.2a1/README.md +100 -0
  3. pyplines_builder-2026.9.2a1/pyplines_builder/__init__.py +1 -0
  4. pyplines_builder-2026.9.2a1/pyplines_builder/app.py +347 -0
  5. pyplines_builder-2026.9.2a1/pyplines_builder/build.schema.json +28 -0
  6. pyplines_builder-2026.9.2a1/pyplines_builder/builds.py +150 -0
  7. pyplines_builder-2026.9.2a1/pyplines_builder/distributions.py +278 -0
  8. pyplines_builder-2026.9.2a1/pyplines_builder/documents.py +122 -0
  9. pyplines_builder-2026.9.2a1/pyplines_builder/errors.py +25 -0
  10. pyplines_builder-2026.9.2a1/pyplines_builder/flow.py +113 -0
  11. pyplines_builder-2026.9.2a1/pyplines_builder/images.py +430 -0
  12. pyplines_builder-2026.9.2a1/pyplines_builder/publication.py +293 -0
  13. pyplines_builder-2026.9.2a1/pyplines_builder/runtime.py +240 -0
  14. pyplines_builder-2026.9.2a1/pyplines_builder/schemas.json +2645 -0
  15. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/PKG-INFO +114 -0
  16. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/SOURCES.txt +23 -0
  17. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/dependency_links.txt +1 -0
  18. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/entry_points.txt +2 -0
  19. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/requires.txt +7 -0
  20. pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/top_level.txt +1 -0
  21. pyplines_builder-2026.9.2a1/pyproject.toml +30 -0
  22. pyplines_builder-2026.9.2a1/setup.cfg +4 -0
  23. pyplines_builder-2026.9.2a1/tests/test_builder.py +304 -0
  24. pyplines_builder-2026.9.2a1/tests/test_docker.py +110 -0
  25. pyplines_builder-2026.9.2a1/uv.lock +1135 -0
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyplines-builder
3
+ Version: 2026.9.2a1
4
+ Summary: Build and sign Pyplines Actions and Distributions
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: typer>=0.24
8
+ Requires-Dist: docker<8,>=7.1
9
+ Requires-Dist: cryptography>=44
10
+ Requires-Dist: packaging>=25
11
+ Requires-Dist: PyYAML>=6
12
+ Requires-Dist: jsonschema>=4.23
13
+ Requires-Dist: referencing>=0.36
14
+
15
+ # Pyplines Builder
16
+
17
+ `pyplines-builder` is the Python/Typer authoring tool. `pyplines` remains the
18
+ operational CLI: it installs and operates the resulting Distribution. Neither
19
+ tool depends on the other. The obsolete Go/Buildah Action Builder is not used.
20
+
21
+ Install with `pip install pyplines-builder`. Development: `uv sync --project builder`.
22
+ Docker must provide Linux containers. The builder uses the Docker API, never a
23
+ Docker/Buildah shell command. Source inspection runs in the target image using
24
+ the SDK, not in the host interpreter.
25
+
26
+ ## Commands
27
+
28
+ ```text
29
+ pyplines-builder version
30
+ pyplines-builder check pyplines-build.yaml
31
+ pyplines-builder keygen ./signing --source https://example.com --publisher example
32
+ pyplines-builder build pyplines-build.yaml --registry registry.example.com/example \
33
+ --signing-key ./signing/publisher.pem --output ./hello-world-1.0.0.tar.gz
34
+ pyplines-builder inspect ./hello-world-1.0.0.tar.gz
35
+ pyplines-builder verify ./hello-world-1.0.0.tar.gz --trust-file ./signing/trust.json
36
+ pyplines-builder action build ./action --platform linux/arm64
37
+ pyplines-builder action inspect sha256:IMAGE_ID
38
+ pyplines-builder action run sha256:IMAGE_ID --input-file ./input.yaml
39
+ ```
40
+
41
+ Every command supports `--json`. Data goes to stdout, failures to stderr;
42
+ failures have nonzero exit codes. No prompts, animations, or credential values
43
+ in diagnostics. Local image IDs are reported by `action build`. `action run`
44
+ reports runtime logs and the terminal InvocationResult; it does not replace
45
+ testing against the Server. `--allow-network` explicitly enables test egress.
46
+
47
+ ## Build document
48
+
49
+ See [Hello World](../example/v1alpha1/distributions/hello-world). The tooling-only
50
+ `pyplines.dev/build/v1alpha1` document references workspace-neutral, standard
51
+ Procedure resources and Action sources (or immutable prebuilt image references).
52
+ IDs in Procedure references match IDs in the build document. Exactly one root
53
+ Procedure is required; every included resource must be reachable from it.
54
+ There is no additional Action Package format and no new platform resource.
55
+
56
+ `check` performs offline structural checks; it does not import Action source or
57
+ claim to validate runtime schemas. `build` validates the complete included
58
+ resource graph. Server installation remains authoritative for semantic planning,
59
+ authorization, trust and runtime compatibility.
60
+
61
+ Each Action's `pyproject.toml` supplies static `project.name`, `version`,
62
+ `requires-python`, and the `pyplines.action` handler entry point. Include source
63
+ files explicitly with `[tool.pyplines.builder] include = ["hello_world.py"]`
64
+ (default: `src`). Include required README/license/package data there too. No
65
+ symlinks, hidden directories, environments or private-key files are copied.
66
+ Review this allowlist: an ordinary source file can still contain a hardcoded
67
+ secret. Building executes author-controlled packaging code and must only be
68
+ done for trusted source on a suitable Docker engine.
69
+
70
+ Python 3.11–3.14 is supported; the newest compatible minor is selected, or use
71
+ `--python`. `--platform auto` uses the Docker engine's architecture. Cross-platform
72
+ builds require engine-provided emulation. A build targets one architecture;
73
+ produce a separately signed Distribution for each platform. Multi-platform OCI
74
+ index assembly is not provided. `--base-image` permits an explicitly pinned Python
75
+ base. Otherwise the selected Python slim tag is resolved to a digest before
76
+ building. Base, source, SDK and recipe attribution is embedded at
77
+ `/opt/pyplines/action/build.json` and returned in the build report. Docker provides
78
+ layer caching. Unlocked third-party dependencies can change between builds;
79
+ pin dependencies for reproducibility. Distribution bytes are deterministic for
80
+ the same resource documents and signing key; source-to-image reproducibility is
81
+ not claimed for unpinned dependencies.
82
+
83
+ Released builders pin the matching published `pyplines` SDK. A development builder
84
+ requires an explicit `--sdk-wheel`; it never silently includes the repository.
85
+ Keep a private signing key outside Action sources. Keys are Ed25519 PKCS8 PEM,
86
+ mode 0600 on POSIX. Existing keys and output archives are never overwritten.
87
+ Existing registry version tags are reused only for identical images. Enable
88
+ registry-side immutable tags as well to protect against concurrent publishers.
89
+ The separate `trust.json` is public and can be supplied to Server trust
90
+ configuration; the builder does not install trust or weaken verification.
91
+
92
+ Private registries use `--registry-credentials /path/to/docker-auth.json`, a
93
+ portable Docker `auths` file. Credential helpers/keychains are not required or
94
+ invoked. Never put credentials in the build document or image reference.
95
+ The default is anonymous registry access. Docker daemon insecure-registry
96
+ configuration is needed for a local HTTP registry; the builder does not alter it.
97
+
98
+ Local runtime tests use the same entrypoint, Unix socket transport, non-root UID,
99
+ read-only root, capability restrictions and scratch mounts as the appliance.
100
+ Test limits are 1 CPU, 256 MiB, 128 PIDs, 16 MiB scratch, 64 KiB test input/secrets,
101
+ 4 MiB collected logs and a configurable 1–3600 second deadline. Only the test's
102
+ own containers/volume are removed. Build images/cache remain for reuse.
103
+
104
+ ## Development
105
+
106
+ ```sh
107
+ python3 scripts/sync_builder_contracts.py --check
108
+ uv run --project builder pytest -q builder/tests
109
+ uv run --project builder black --check builder/pyplines_builder/app.py
110
+ uv build --project builder
111
+ ```
112
+
113
+ The generated readers and schema snapshot come from the same sources as the
114
+ operational CLI. Edit those sources and regenerate, not the bundled copies.
@@ -0,0 +1,100 @@
1
+ # Pyplines Builder
2
+
3
+ `pyplines-builder` is the Python/Typer authoring tool. `pyplines` remains the
4
+ operational CLI: it installs and operates the resulting Distribution. Neither
5
+ tool depends on the other. The obsolete Go/Buildah Action Builder is not used.
6
+
7
+ Install with `pip install pyplines-builder`. Development: `uv sync --project builder`.
8
+ Docker must provide Linux containers. The builder uses the Docker API, never a
9
+ Docker/Buildah shell command. Source inspection runs in the target image using
10
+ the SDK, not in the host interpreter.
11
+
12
+ ## Commands
13
+
14
+ ```text
15
+ pyplines-builder version
16
+ pyplines-builder check pyplines-build.yaml
17
+ pyplines-builder keygen ./signing --source https://example.com --publisher example
18
+ pyplines-builder build pyplines-build.yaml --registry registry.example.com/example \
19
+ --signing-key ./signing/publisher.pem --output ./hello-world-1.0.0.tar.gz
20
+ pyplines-builder inspect ./hello-world-1.0.0.tar.gz
21
+ pyplines-builder verify ./hello-world-1.0.0.tar.gz --trust-file ./signing/trust.json
22
+ pyplines-builder action build ./action --platform linux/arm64
23
+ pyplines-builder action inspect sha256:IMAGE_ID
24
+ pyplines-builder action run sha256:IMAGE_ID --input-file ./input.yaml
25
+ ```
26
+
27
+ Every command supports `--json`. Data goes to stdout, failures to stderr;
28
+ failures have nonzero exit codes. No prompts, animations, or credential values
29
+ in diagnostics. Local image IDs are reported by `action build`. `action run`
30
+ reports runtime logs and the terminal InvocationResult; it does not replace
31
+ testing against the Server. `--allow-network` explicitly enables test egress.
32
+
33
+ ## Build document
34
+
35
+ See [Hello World](../example/v1alpha1/distributions/hello-world). The tooling-only
36
+ `pyplines.dev/build/v1alpha1` document references workspace-neutral, standard
37
+ Procedure resources and Action sources (or immutable prebuilt image references).
38
+ IDs in Procedure references match IDs in the build document. Exactly one root
39
+ Procedure is required; every included resource must be reachable from it.
40
+ There is no additional Action Package format and no new platform resource.
41
+
42
+ `check` performs offline structural checks; it does not import Action source or
43
+ claim to validate runtime schemas. `build` validates the complete included
44
+ resource graph. Server installation remains authoritative for semantic planning,
45
+ authorization, trust and runtime compatibility.
46
+
47
+ Each Action's `pyproject.toml` supplies static `project.name`, `version`,
48
+ `requires-python`, and the `pyplines.action` handler entry point. Include source
49
+ files explicitly with `[tool.pyplines.builder] include = ["hello_world.py"]`
50
+ (default: `src`). Include required README/license/package data there too. No
51
+ symlinks, hidden directories, environments or private-key files are copied.
52
+ Review this allowlist: an ordinary source file can still contain a hardcoded
53
+ secret. Building executes author-controlled packaging code and must only be
54
+ done for trusted source on a suitable Docker engine.
55
+
56
+ Python 3.11–3.14 is supported; the newest compatible minor is selected, or use
57
+ `--python`. `--platform auto` uses the Docker engine's architecture. Cross-platform
58
+ builds require engine-provided emulation. A build targets one architecture;
59
+ produce a separately signed Distribution for each platform. Multi-platform OCI
60
+ index assembly is not provided. `--base-image` permits an explicitly pinned Python
61
+ base. Otherwise the selected Python slim tag is resolved to a digest before
62
+ building. Base, source, SDK and recipe attribution is embedded at
63
+ `/opt/pyplines/action/build.json` and returned in the build report. Docker provides
64
+ layer caching. Unlocked third-party dependencies can change between builds;
65
+ pin dependencies for reproducibility. Distribution bytes are deterministic for
66
+ the same resource documents and signing key; source-to-image reproducibility is
67
+ not claimed for unpinned dependencies.
68
+
69
+ Released builders pin the matching published `pyplines` SDK. A development builder
70
+ requires an explicit `--sdk-wheel`; it never silently includes the repository.
71
+ Keep a private signing key outside Action sources. Keys are Ed25519 PKCS8 PEM,
72
+ mode 0600 on POSIX. Existing keys and output archives are never overwritten.
73
+ Existing registry version tags are reused only for identical images. Enable
74
+ registry-side immutable tags as well to protect against concurrent publishers.
75
+ The separate `trust.json` is public and can be supplied to Server trust
76
+ configuration; the builder does not install trust or weaken verification.
77
+
78
+ Private registries use `--registry-credentials /path/to/docker-auth.json`, a
79
+ portable Docker `auths` file. Credential helpers/keychains are not required or
80
+ invoked. Never put credentials in the build document or image reference.
81
+ The default is anonymous registry access. Docker daemon insecure-registry
82
+ configuration is needed for a local HTTP registry; the builder does not alter it.
83
+
84
+ Local runtime tests use the same entrypoint, Unix socket transport, non-root UID,
85
+ read-only root, capability restrictions and scratch mounts as the appliance.
86
+ Test limits are 1 CPU, 256 MiB, 128 PIDs, 16 MiB scratch, 64 KiB test input/secrets,
87
+ 4 MiB collected logs and a configurable 1–3600 second deadline. Only the test's
88
+ own containers/volume are removed. Build images/cache remain for reuse.
89
+
90
+ ## Development
91
+
92
+ ```sh
93
+ python3 scripts/sync_builder_contracts.py --check
94
+ uv run --project builder pytest -q builder/tests
95
+ uv run --project builder black --check builder/pyplines_builder/app.py
96
+ uv build --project builder
97
+ ```
98
+
99
+ The generated readers and schema snapshot come from the same sources as the
100
+ operational CLI. Edit those sources and regenerate, not the bundled copies.
@@ -0,0 +1 @@
1
+ """Authoring tools for the v1alpha1 platform. No operational CLI dependency."""
@@ -0,0 +1,347 @@
1
+ """Small, static Typer interface over reusable builder services."""
2
+
3
+ from importlib.metadata import version as package_version
4
+ import json
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ import typer
9
+ from typer.core import TyperCommand, TyperGroup
10
+
11
+ try:
12
+ from typer import _click as click
13
+ except ImportError:
14
+ import click
15
+
16
+ try:
17
+ from typer.exceptions import Abort
18
+ except ImportError:
19
+ from typer._click.exceptions import Abort
20
+
21
+ from .builds import build_document, procedures, build_distribution
22
+ from .documents import read_document
23
+ from .distributions import inspect_file
24
+ from .errors import ClientError
25
+ from .images import (
26
+ connection,
27
+ platform_name,
28
+ build_action,
29
+ image_manifest,
30
+ source_files,
31
+ push_action,
32
+ )
33
+ from .publication import keygen as generate_key, verify as verify_publication
34
+ from .runtime import run_action
35
+
36
+
37
+ class PlainCommand(TyperCommand):
38
+ def format_help(self, ctx, formatter):
39
+ self.format_usage(ctx, formatter)
40
+ self.format_help_text(ctx, formatter)
41
+ self.format_options(ctx, formatter)
42
+ self.format_epilog(ctx, formatter)
43
+
44
+
45
+ class PlainGroup(TyperGroup):
46
+ format_help = PlainCommand.format_help
47
+
48
+ def main(self, args=None, standalone_mode=True, **kwargs):
49
+ arguments = list(sys.argv[1:] if args is None else args)
50
+ try:
51
+ result = super().main(args=arguments, standalone_mode=False, **kwargs)
52
+ if standalone_mode:
53
+ raise SystemExit(result if isinstance(result, int) else 0)
54
+ return result
55
+ except click.ClickException as exc:
56
+ if isinstance(exc, click.exceptions.NoArgsIsHelpError):
57
+ sys.stdout.write(exc.ctx.get_help() + "\n")
58
+ return 0
59
+ error = ClientError("invalid-command", exc.format_message(), 2)
60
+ sys.stderr.write(
61
+ (
62
+ json.dumps(error.document())
63
+ if "--json" in arguments
64
+ else "Error: " + error.message
65
+ )
66
+ + "\n"
67
+ )
68
+ raise SystemExit(2) from None
69
+ except ClientError as exc:
70
+ if "--json" in arguments:
71
+ sys.stderr.write(json.dumps(exc.document()) + "\n")
72
+ else:
73
+ sys.stderr.write("Error: " + exc.message + "\n")
74
+ raise SystemExit(exc.exit_code) from None
75
+ except (KeyboardInterrupt, Abort):
76
+ error = ClientError(
77
+ "interrupted",
78
+ "Interrupted; check Docker if the connection failed during cleanup",
79
+ 130,
80
+ )
81
+ sys.stderr.write(
82
+ (
83
+ json.dumps(error.document())
84
+ if "--json" in arguments
85
+ else "Error: " + error.message
86
+ )
87
+ + "\n"
88
+ )
89
+ raise SystemExit(130) from None
90
+ except (OSError, ValueError, KeyError, TypeError) as exc:
91
+ error = ClientError(
92
+ "builder-failed",
93
+ "Builder input or operation failed ("
94
+ + type(exc).__name__
95
+ + "); check document fields, file permissions, signing trust and Docker availability",
96
+ )
97
+ sys.stderr.write(
98
+ (
99
+ json.dumps(error.document())
100
+ if "--json" in arguments
101
+ else "Error: " + error.message
102
+ )
103
+ + "\n"
104
+ )
105
+ raise SystemExit(1) from None
106
+
107
+
108
+ app = typer.Typer(
109
+ cls=PlainGroup,
110
+ help="Build and sign Pyplines Actions and Distributions.",
111
+ add_completion=False,
112
+ no_args_is_help=True,
113
+ pretty_exceptions_enable=False,
114
+ )
115
+ actions = typer.Typer(
116
+ cls=PlainGroup,
117
+ help="Build and test Actions using Docker, without an installation.",
118
+ no_args_is_help=True,
119
+ )
120
+ app.add_typer(actions, name="action")
121
+
122
+
123
+ @app.callback()
124
+ def options(
125
+ ctx: typer.Context,
126
+ json_output: bool = typer.Option(
127
+ False, "--json", help="Emit machine-readable JSON."
128
+ ),
129
+ ):
130
+ ctx.ensure_object(dict)
131
+ ctx.obj["json"] = json_output
132
+
133
+
134
+ def emit(ctx, value, json_output=False):
135
+ if json_output or ctx.obj.get("json"):
136
+ typer.echo(json.dumps(value, ensure_ascii=False, allow_nan=False))
137
+ else:
138
+ for key, item in value.items():
139
+ text = (
140
+ json.dumps(item, indent=2, ensure_ascii=False)
141
+ if isinstance(item, (dict, list))
142
+ else str(item)
143
+ )
144
+ typer.echo(f"{key.replace('_', ' ').capitalize()}: {text}")
145
+
146
+
147
+ @app.command(cls=PlainCommand)
148
+ def version(ctx: typer.Context, json_output: bool = typer.Option(False, "--json")):
149
+ """Show the installed builder version."""
150
+ emit(ctx, {"version": package_version("pyplines-builder")}, json_output)
151
+
152
+
153
+ @app.command(cls=PlainCommand)
154
+ def keygen(
155
+ ctx: typer.Context,
156
+ directory: str,
157
+ source: str = typer.Option(...),
158
+ publisher: str = typer.Option(...),
159
+ key_id: str = typer.Option("publisher", "--key-id"),
160
+ json_output: bool = typer.Option(False, "--json"),
161
+ ):
162
+ """Create a private signing key and separate public Server trust file."""
163
+ if not source.startswith("https://") or source.endswith("/"):
164
+ raise ClientError(
165
+ "invalid-source", "Use an HTTPS source without a trailing slash", 2
166
+ )
167
+ emit(ctx, generate_key(directory, source, publisher, key_id), json_output)
168
+
169
+
170
+ @app.command("inspect", cls=PlainCommand)
171
+ def inspect_distribution(
172
+ ctx: typer.Context,
173
+ distribution: str,
174
+ json_output: bool = typer.Option(False, "--json"),
175
+ ):
176
+ """Inspect a Distribution offline; does not assert publisher trust."""
177
+ emit(ctx, inspect_file(distribution).summary(), json_output)
178
+
179
+
180
+ @app.command(cls=PlainCommand)
181
+ def verify(
182
+ ctx: typer.Context,
183
+ distribution: str,
184
+ trust_file: str = typer.Option(..., "--trust-file"),
185
+ json_output: bool = typer.Option(False, "--json"),
186
+ ):
187
+ """Verify resource digests and every Ed25519 publisher signature."""
188
+ from cryptography.exceptions import InvalidSignature
189
+
190
+ try:
191
+ value = verify_publication(distribution, trust_file)
192
+ except (InvalidSignature, KeyError, ValueError):
193
+ raise ClientError(
194
+ "invalid-publication",
195
+ "Publication is unsigned, untrusted, or has an invalid signature",
196
+ ) from None
197
+ emit(ctx, value, json_output)
198
+
199
+
200
+ @app.command(cls=PlainCommand)
201
+ def check(
202
+ ctx: typer.Context,
203
+ document: str = typer.Argument("pyplines-build.yaml"),
204
+ json_output: bool = typer.Option(False, "--json"),
205
+ ):
206
+ """Check build-document/source structure without executing author code."""
207
+ directory, value = build_document(document)
208
+ resources = procedures(directory, value)
209
+ for action in value["actions"]:
210
+ if "source" in action:
211
+ source_files(directory / action["source"])
212
+ emit(
213
+ ctx,
214
+ {
215
+ "valid": True,
216
+ "procedures": len(resources),
217
+ "actions": len(value["actions"]),
218
+ "note": "Action schemas and the complete dependency graph are checked during build.",
219
+ },
220
+ json_output,
221
+ )
222
+
223
+
224
+ @app.command(cls=PlainCommand)
225
+ def build(
226
+ ctx: typer.Context,
227
+ document: str = typer.Argument("pyplines-build.yaml"),
228
+ output: str = typer.Option(..., "--output", "-o"),
229
+ signing_key: str = typer.Option(..., "--signing-key"),
230
+ registry: str = typer.Option("", "--registry"),
231
+ registry_credentials: str | None = typer.Option(None, "--registry-credentials"),
232
+ platform: str = "auto",
233
+ sdk_wheel: str | None = typer.Option(None, "--sdk-wheel"),
234
+ python: str | None = None,
235
+ base_image: str | None = typer.Option(None, "--base-image"),
236
+ json_output: bool = typer.Option(False, "--json"),
237
+ ):
238
+ """Build/push Actions and emit one signed, installable Distribution."""
239
+ with connection(registry_credentials) as client:
240
+ result = build_distribution(
241
+ client,
242
+ document,
243
+ platform_name(client, platform),
244
+ registry,
245
+ signing_key,
246
+ output,
247
+ sdk_wheel,
248
+ python,
249
+ base_image,
250
+ )
251
+ emit(ctx, result, json_output)
252
+
253
+
254
+ @actions.command("build", cls=PlainCommand)
255
+ def action_build(
256
+ ctx: typer.Context,
257
+ source: str = typer.Argument("."),
258
+ platform: str = "auto",
259
+ sdk_wheel: str | None = typer.Option(None, "--sdk-wheel"),
260
+ python: str | None = None,
261
+ base_image: str | None = typer.Option(None, "--base-image"),
262
+ repository: str | None = None,
263
+ registry_credentials: str | None = typer.Option(None, "--registry-credentials"),
264
+ json_output: bool = typer.Option(False, "--json"),
265
+ ):
266
+ """Build a canonical Action image; --repository additionally pushes it."""
267
+ with connection(registry_credentials) as client:
268
+ result = build_action(
269
+ client,
270
+ source,
271
+ platform_name(client, platform),
272
+ sdk_wheel,
273
+ base_image,
274
+ python,
275
+ )
276
+ if repository:
277
+ result["published_image"] = push_action(
278
+ client,
279
+ result["image"],
280
+ repository,
281
+ result["manifest"]["action"]["version"],
282
+ )
283
+ emit(ctx, result, json_output)
284
+
285
+
286
+ @actions.command("inspect", cls=PlainCommand)
287
+ def action_inspect(
288
+ ctx: typer.Context,
289
+ image: str,
290
+ platform: str = "auto",
291
+ json_output: bool = typer.Option(False, "--json"),
292
+ ):
293
+ """Inspect a local built image's identity and input/output schemas without executing it."""
294
+ with connection() as client:
295
+ value = image_manifest(client, image, platform_name(client, platform))
296
+ emit(ctx, value, json_output)
297
+
298
+
299
+ @actions.command("run", cls=PlainCommand)
300
+ def action_run(
301
+ ctx: typer.Context,
302
+ image: str,
303
+ input_file: str = typer.Option(..., "--input-file"),
304
+ secrets_file: str | None = typer.Option(None, "--secrets-file"),
305
+ timeout: int = 60,
306
+ allow_network: bool = typer.Option(False, "--allow-network"),
307
+ platform: str = "auto",
308
+ json_output: bool = typer.Option(False, "--json"),
309
+ ):
310
+ """Test a built image through the real runtime protocol; networking is opt-in."""
311
+ with connection() as client:
312
+ manifest = image_manifest(client, image, platform_name(client, platform))
313
+ value = run_action(
314
+ client,
315
+ image,
316
+ manifest,
317
+ read_document(input_file),
318
+ timeout,
319
+ allow_network,
320
+ read_document(secrets_file) if secrets_file else None,
321
+ )
322
+ emit(ctx, value, json_output)
323
+ if value["result"]["status"] != "completed" or value["container_exit_code"] != 0:
324
+ raise typer.Exit(1)
325
+
326
+
327
+ def main():
328
+ # Docker errors contain registry URLs/credentials and are not safe to echo.
329
+ from docker.errors import DockerException
330
+ from requests.exceptions import RequestException
331
+
332
+ try:
333
+ app()
334
+ except (DockerException, RequestException):
335
+ error = ClientError(
336
+ "docker-operation-failed",
337
+ "Docker operation failed; check engine availability, image/platform compatibility and registry access",
338
+ )
339
+ sys.stderr.write(
340
+ (
341
+ json.dumps(error.document())
342
+ if "--json" in sys.argv
343
+ else "Error: " + error.message
344
+ )
345
+ + "\n"
346
+ )
347
+ raise SystemExit(1) from None
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Pyplines Builder document (tooling, not a platform resource)",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["kind", "source", "publisher", "key_id", "root", "actions", "procedures"],
7
+ "properties": {
8
+ "kind": {"const": "pyplines.dev/build/v1alpha1"},
9
+ "source": {"type": "string", "format": "uri", "pattern": "^https://[^/?#@]+$"},
10
+ "publisher": {"$ref": "https://v1alpha1.pyplines.dev/schemas/SchemaCatalog.json#/$defs/slug"},
11
+ "key_id": {"type": "string", "minLength": 1, "maxLength": 128, "pattern": "^[a-zA-Z0-9._-]+$"},
12
+ "root": {"$ref": "https://v1alpha1.pyplines.dev/schemas/SchemaCatalog.json#/$defs/uuid"},
13
+ "procedures": {"type": "array", "minItems": 1, "maxItems": 256, "uniqueItems": true, "items": {"type": "string", "minLength": 1}},
14
+ "actions": {"type": "array", "maxItems": 255, "items": {
15
+ "type": "object", "additionalProperties": false, "required": ["id"],
16
+ "oneOf": [{"required": ["source"]}, {"required": ["image"]}],
17
+ "properties": {
18
+ "id": {"$ref": "https://v1alpha1.pyplines.dev/schemas/SchemaCatalog.json#/$defs/uuid"},
19
+ "source": {"type": "string", "minLength": 1},
20
+ "image": {"type": "string", "pattern": "^[^\\s@]+@sha256:[a-f0-9]{64}$"},
21
+ "size": {"enum": ["small", "medium", "large"]},
22
+ "network_required": {"type": "boolean"},
23
+ "retry_safety": {"enum": ["safe", "unsafe", "idempotency-key-required"]},
24
+ "secret_aliases": {"type": "array", "uniqueItems": true, "items": {"$ref": "https://v1alpha1.pyplines.dev/schemas/SchemaCatalog.json#/$defs/alias"}}
25
+ }
26
+ }}
27
+ }
28
+ }