Metadata-Version: 2.4
Name: oras-pants-plugin
Version: 0.3.2
Summary: A Pantsbuild plugin that packages the outputs of other targets as OCI artifacts and publishes them to OCI registries with oras.
Author: Zach Gottesman
Maintainer: zach-overflow
License-Expression: Apache-2.0
Project-URL: Bug Tracker, https://github.com/zach-overflow/pants-plugin-depot/issues
Project-URL: Changelog, https://github.com/zach-overflow/pants-plugin-depot/blob/main/provides/oras-pants-plugin/CHANGELOG.md
Project-URL: Repository, https://github.com/zach-overflow/pants-plugin-depot
Keywords: oci,oci artifact,oras,package,pants,pants plugin,pantsbuild,pex,publish,registry
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: System :: Software Distribution
Classifier: Typing :: Typed
Requires-Python: >=3.14
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pants-plugin-developer-utils>=0.1.0
Dynamic: license-file

# oras-pants-plugin

![PyPI Version](https://img.shields.io/pypi/v/oras-pants-plugin?style=flat&logo=python&logoColor=white&logoSize=auto&color=rgb(162%2C%200%2C%20255))

A [Pantsbuild](https://www.pantsbuild.org/) plugin that wraps the outputs of `package`-able targets, such as
`pex_binary`, as [OCI artifacts](https://oras.land/docs/concepts/artifact) and publishes them to OCI registries
with [`oras`](https://oras.land/). Any registry that stores OCI artifacts works: GitHub Container Registry,
Docker Hub, Artifact Registry, ECR, ACR, Harbor, a local `registry:2`, and so on.

## Installation

Add the plugin to `pants.toml`:

```toml
[GLOBAL]
plugins = ["oras-pants-plugin==<version>"]

[oras.registries.ghcr]
address = "ghcr.io/my-org"
default = true
```

Wrap the targets to publish in an `oci_artifact` target, like the builtin `archive` target does:

```python
# src/app/BUILD
pex_binary(name="app", entry_point="main.py")

oci_artifact(
    name="app-artifact",
    packages=[":app"],
    oci_repository="app",
    oci_tags=["1.2.0", "latest"],
    oci_artifact_type="application/vnd.python.pex.v1",
)
```

Build it, or build and push it:

```bash
pants package src/app:app-artifact   # writes the OCI image layout to dist/src.app/app-artifact/
pants publish src/app:app-artifact   # copies it to ghcr.io/my-org/app:1.2.0 and :latest
```

Pull it back anywhere with `oras pull ghcr.io/my-org/app:1.2.0`, which restores `src.app/app.pex`.

## How it works

`oras` is downloaded as a static binary; nothing is installed into a Pants resolve.

`pants package` builds every `packages` target as `pants package` would, then runs `oras push --oci-layout` to
write one [OCI image layout](https://github.com/opencontainers/image-spec/blob/main/image-layout.md) directory
to `output_path` under `--distdir`. Every file or directory the `packages` targets produce becomes one layer,
titled with its own `--distdir`-relative path (a `packed` or `loose` layout PEX is a directory, pushed as a
gzipped tar layer that `oras pull` unpacks again). The artifact is tagged with every `oci_tags` entry and the
manifest's `artifactType` is `oci_artifact_type`.

`pants publish` packages the target the same way, then runs one `oras cp --from-oci-layout` per registry, in the
foreground so `oras` can show progress. The copy addresses the layout's manifest by digest, so the published
artifact has exactly the digest `pants package` reported. A target without `oci_repository`, that resolves to no
registry, with `skip_oras=True`, or run with `[oras].skip = true` or `[oras].allow_publish = false` is skipped
before it is packaged. Each copy is reported as published, failed or skipped like every other publisher;
`--publish-output` writes the same as JSON, with the `registry`, `repository`, `tags` and `digest` of each copy.

### Tags from files

`oci_tag_files` lists targets whose files hold further tags, one per line, appended to `oci_tags`. Any `file` or
`resource` target works, and so does any code generator producing them, which is how a version derived from git
becomes a tag: the [`vcs-versioning-pants-plugin`](https://pypi.org/project/vcs-versioning-pants-plugin/)'s
`scm_version_file` target generates a one-line version file from the git tags at build time.

```python
scm_version_file(name="version", tag_prefix="app-v", generate_to="src/app/VERSION", local_scheme="no-local-version")

oci_artifact(
    name="app-artifact",
    packages=[":app"],
    oci_repository="app",
    oci_tags=["latest"],
    oci_tag_files=[":version"],  # + `1.2.0` on a checkout of the `app-v1.2.0` tag, `1.2.1.dev3` after it
)
```

The files are hydrated, and generators run, every time the artifact is packaged or published, so the tag follows
the git state. An OCI tag cannot contain `+`, hence `local_scheme="no-local-version"` for dev builds.

### `oci_artifact` fields

| Field               | Purpose                                                                                                                                                                                                                                                       |
| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `packages`          | Addresses of targets `pants package` can build, e.g. `[":app"]`. Each output becomes a layer.                                                                                                                                                                 |
| `output_path`       | Where `pants package` writes the layout directory, relative to `--distdir`. Default: `<spec path with dots>/<target name>`.                                                                                                                                   |
| `oci_tags`          | Tags applied in the layout and in every registry. Default: `["latest"]`. `[]` leaves the artifact untagged, addressable by digest only.                                                                                                                      |
| `oci_tag_files`     | Addresses of `file`, `resource` or code generator targets (e.g. `scm_version_file`, `shell_command`) whose files hold further tags, one per non-blank line, appended to `oci_tags`. Duplicates are dropped.                                                   |
| `oci_artifact_type` | The `artifactType` of the manifest, passed as `--artifact-type`, e.g. `application/vnd.python.pex.v1`. Default: `oras`' `application/vnd.unknown.artifact.v1`.                                                                                               |
| `oci_artifact_platform` | The artifact's platform as `os[/arch][/variant][:os_version]`, e.g. `linux/amd64`, passed as `--artifact-platform` (experimental in `oras`): recorded in the manifest's config as `{"os": ..., "architecture": ...}`. Unset means platform-independent.      |
| `oci_registries`    | Registry addresses (`ghcr.io/my-org`, `localhost:5000`) or `@aliases` of `[oras].registries` entries to publish to. Default: every `[oras].registries` entry with `default = true`.                                                                           |
| `oci_repository`    | The repository within each registry, e.g. `team/app`; the published reference is `<registry>/<oci_repository>:<tag>`. Unset skips the target in `pants publish`.                                                                                             |
| `skip_oras`         | If true, the target is skipped by `pants publish`.                                                                                                                                                                                                            |

## Configuration

| Option                  | Default         | Purpose                                                                                                                                                                                                  |
| :---------------------- | :-------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `[oras].registries`     | `{}`            | Registries by alias: `{"alias": {"address": "host[:port][/prefix]", "default": bool}}`. An entry aliased `default` is a default too.                                                                     |
| `[oras].skip`           | `false`         | Skip `oras` during `pants publish`.                                                                                                                                                                      |
| `[oras].allow_publish`  | `true`          | Whether `pants publish` may push `oci_artifact` targets at all. Set it to `false` in `pants.toml` and to `true` in a CI-only config (`PANTS_CONFIG_FILES=pants.ci.toml`), so only the release pipeline publishes. |
| `[oras].args`           | `[]`            | Extra arguments for every `oras cp`, e.g. `--to-plain-http` or `--to-insecure`. The registry is the copy's destination, so its flags are the `--to-*` ones. They follow the plugin's own flags, so a repeated flag overrides it. |
| `[oras].config`         | unset           | A registry authentication file in the `~/.docker/config.json` format, passed as `--to-registry-config`. Relative paths are resolved from the build root.                                                 |
| `[oras].extra_env_vars` | `[]`            | Extra environment variables for `oras cp`, as `NAME=value` or `NAME` to copy from Pants' environment, e.g. `HTTPS_PROXY` or `SSL_CERT_FILE`.                                                             |
| `[oras].version`        | bundled default | `oras` release to download.                                                                                                                                                                              |
| `[oras].known_versions` | bundled default | Checksums of downloadable releases; extend to use another version.                                                                                                                                       |

### Credentials

`oras` reads the credentials that `docker login` or `oras login` store in `$DOCKER_CONFIG/config.json` or
`~/.docker/config.json`, and runs the credential helpers configured there. `PATH`, `HOME` and `DOCKER_CONFIG`
are always passed to `oras cp`, so logging in once is enough:

```bash
oras login ghcr.io          # or: docker login ghcr.io
pants publish src/app:app-artifact
```

In CI, pass the token to a login step before `pants publish`, or point `[oras].config` at an auth file. Avoid
`--to-username`/`--to-password` in `[oras].args`: `pants.toml` is committed.

### Publishing into a directory

`oras cp` can write to an OCI image layout directory instead of a registry, which is handy for inspecting what
would be pushed. With `--to-oci-layout`, the registry reference becomes a directory path in the sandbox, so
combine it with `--keep-sandboxes=always` to look at the result:

```bash
pants --oras-args="['--to-oci-layout']" --keep-sandboxes=always publish src/app:app-artifact
```

The layout `pants package` writes to `dist/` is the same artifact; `oras discover`, `oras manifest fetch` and
`oras pull` all accept it with `--oci-layout`:

```bash
oras manifest fetch --oci-layout dist/src.app/app-artifact:1.2.0 | jq
```

### Using a different `oras` version

Add the release's checksums and sizes to `known_versions` and set `version`:

```toml
[oras]
version = "1.3.5"
known_versions.add = [
  "1.3.5|linux_arm64 |<sha256>|<size>",
  "1.3.5|linux_x86_64|<sha256>|<size>",
  "1.3.5|macos_arm64 |<sha256>|<size>",
  "1.3.5|macos_x86_64|<sha256>|<size>",
]
```

Compute each line with `curl -L $URL | tee >(wc -c) >(shasum -a 256) >/dev/null`, where `$URL` is the
`oras_<version>_<os>_<arch>.tar.gz` asset of the
[GitHub release](https://github.com/oras-project/oras/releases) (`linux`/`darwin`, `amd64`/`arm64`). The
release's `oras_<version>_checksums.txt` lists the same checksums.

## Supported versions

| Plugin | Pants  | Python | `oras`     |
| :----- | :----- | :----- | :--------- |
| `0.x`  | `2.33` | `3.14` | `>=1.3,<2` |

Pants loads plugins into its own interpreter, so the plugin requires the Python version Pants runs on. The
packages being wrapped may target any Python version.

## Releases

Wheels are published to [PyPI](https://pypi.org/project/oras-pants-plugin/). Release notes live in the
[changelog](https://github.com/zach-overflow/pants-plugin-depot/blob/main/provides/oras-pants-plugin/CHANGELOG.md)
and on the [GitHub releases page](https://github.com/zach-overflow/pants-plugin-depot/releases).

## Contributing, Bug Reports and Feature Requests

This plugin lives in the [pants-plugin-depot](https://github.com/zach-overflow/pants-plugin-depot) repo.
See its [CONTRIBUTING.md](https://github.com/zach-overflow/pants-plugin-depot/blob/main/CONTRIBUTING.md), or
open an [issue](https://github.com/zach-overflow/pants-plugin-depot/issues/new/choose).
