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.
- pyplines_builder-2026.9.2a1/PKG-INFO +114 -0
- pyplines_builder-2026.9.2a1/README.md +100 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/__init__.py +1 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/app.py +347 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/build.schema.json +28 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/builds.py +150 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/distributions.py +278 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/documents.py +122 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/errors.py +25 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/flow.py +113 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/images.py +430 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/publication.py +293 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/runtime.py +240 -0
- pyplines_builder-2026.9.2a1/pyplines_builder/schemas.json +2645 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/PKG-INFO +114 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/SOURCES.txt +23 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/dependency_links.txt +1 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/entry_points.txt +2 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/requires.txt +7 -0
- pyplines_builder-2026.9.2a1/pyplines_builder.egg-info/top_level.txt +1 -0
- pyplines_builder-2026.9.2a1/pyproject.toml +30 -0
- pyplines_builder-2026.9.2a1/setup.cfg +4 -0
- pyplines_builder-2026.9.2a1/tests/test_builder.py +304 -0
- pyplines_builder-2026.9.2a1/tests/test_docker.py +110 -0
- 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
|
+
}
|