Dist-Zilla-Plugin-Docker-API

 view release on metacpan or  search on metacpan

.claude/agents/dzil-docker-worker.md  view on Meta::CPAN

---

You are the dzil-docker-worker for **Dist::Zilla::Plugin::Docker::API**.

Implement, refactor, debug and test this distribution. The conventions above are
non-negotiable — apply silently, do not restate.

**Where your lane ends.** You own the plugin: how phases hook into Dist::Zilla, how
dist.ini config becomes attributes, how tags are templated, and how `Client.pm` adapts
`API::Docker`. You do not own the Engine API. If the task turns on what the daemon
accepts or answers — an endpoint's parameters, an event stream's shape, registry auth,
version gating — that is `../p5-api-docker`'s, and the answer is a ticket on its board,
not a workaround here. Guessing at daemon behavior from this adapter is how a wrong
assumption gets cemented; `_split_image_ref` exists because exactly that went wrong once.

Coordinate via `karr`: pick tickets from the local board, record drift you find as new
tickets rather than expanding scope mid-change.

## Repo-specific notes — beyond the briefed skills

**Two consumers, both in this workspace.** `@Author::GETTY::Docker` in

.claude/skills/dzil-docker-core/SKILL.md  view on Meta::CPAN

## The client seam

`client_class` (default `…::API::Client`) is loaded with a runtime
`eval "require"` and constructed with two coderefs, `logger` and
`logger_fatal`, that funnel into the plugin's `log`/`log_fatal`. **This seam is
the entire test strategy** — everything below it is fake-able and nothing in
`API.pm` may reach `API::Docker` directly.

Inside `Client.pm`:

- **`_split_image_ref` exists because the engine's tag endpoint takes repo and
  tag separately** (`POST /images/{name}/tag?repo=&tag=`). Passing a full
  reference as `repo` leaves `tag` empty, the engine appends its own default,
  and podman rejects `example/app:1.0:latest` with a 500 — every version tag
  quietly lost. A colon before the last slash is a registry port, not a tag.
- **A failed build's output rides on the exception, not the event loop.**
  API::Docker croaks on the `errorDetail` event before the caller iterates, so
  the preceding stream is drained from `$err->events` — otherwise a failed
  build logs only its last line.
- **Concise mode is the default.** `_is_build_step_header` recognises the
  legacy builder (`Step N/M :`), Podman's classic builder (`STEP N/M:`) and
  BuildKit (`#N [N/M]`, `#N [name N/M]`). A fourth builder format needs a
  fourth pattern here, plus a case in `t/60-build-progress.t`.
- Registry auth is read from `~/.docker/config.json` (or `$DOCKER_CONFIG`),
  handling `identitytoken`, base64 `auth` and plain user/password, with the
  Docker Hub key aliases spelled out.

## Cross-repo: `../p5-api-docker` owns the wire

`API::Docker` is a Getty distribution in the same workspace and `cpanfile` pins
it (`0.003`). A question about what the daemon accepts or answers — endpoint
shape, event stream, auth header, version gating — is **that** repo's, and gets
a ticket on its board rather than a workaround here. `_split_image_ref` living
here rather than there is a deliberate choice: `API::Docker` mirrors the
endpoint and should keep doing so.

The consumer in the other direction is `@Author::GETTY::Docker` in
`../p5-dist-zilla-pluginbundle-author-getty`, which constructs this plugin
programmatically. Any attribute rename or `init_arg` change is a cross-repo
change — check that bundle still builds.

## Tests — the Recorder fake, never a daemon

`prove -lr t/` must pass on a machine with no engine at all, and does.

Changes  view on Meta::CPAN

    both checks.
  - A failed release push is fatal. API::Docker 0.004 croaks on a push
    failure rather than returning an errorDetail event, and the client
    catches it and surfaces it through the same log_fatal, carrying the
    engine's errorDetail message when the error is a stream error and
    the exception itself otherwise.
  - A failed build logs the complete build output, not just the failing
    step: the client drains the exception's event list through the same
    progress filter used for a live stream before re-raising.
  - Every tag after the first now applies. The image reference is split
    into repository and tag before reaching the engine's tag endpoint,
    so a registry host:port is no longer mistaken for the tag separator
    and %V/%v no longer vanish -- only "latest" survived the default tag
    list before. A tag the engine refuses is left out of the result
    instead of being reported as applied.
  - New DZIL_DOCKER_API_SKIP=1 skips the image build for one run -- no
    engine contact, no image -- for dzil build/install/test while the
    image cannot build yet. dzil release refuses to run with it set.
  - Document the container engine requirement in POD and README: any
    engine serving the Docker Engine HTTP API works, no docker binary
    needed, and target reaches it unchanged for multi-stage builds.

dist_zilla_docker_api_plugin_plan.md  view on Meta::CPAN

  auth      => $auth,
);

$client->inspect_image($image_ref);
$client->image_exists_locally($image_ref);
$client->remote_tag_exists($image_ref); # optional registry helper
```

### `...::Progress`

Docker build/push endpoints return streaming progress records. The plugin should parse these and forward:

```text
stream/status/progress -> log_debug or log
errorDetail/error      -> log_fatal
aux.ID                 -> captured image ID
```

Important: do not treat HTTP 200 alone as success. Docker build/push can return a successful HTTP response while the stream contains an error event. The adapter must parse the stream completely.

### `...::Registry`

lib/Dist/Zilla/Plugin/Docker/API/Client.pm  view on Meta::CPAN

    my ($engine) = grep { ($_->{Name} // '') =~ /engine/i }
        @{ $version->{Components} // [] };

    return {
        version     => $version->{Version},
        api_version => $version->{ApiVersion},
        engine      => $engine ? $engine->{Name} : undef,
    };
}

# The engine's tag endpoint takes the repository and the tag as two separate
# parameters (POST /images/{name}/tag?repo=&tag=), so a full image reference
# has to be taken apart before it goes over the wire. Passing the whole thing
# as `repo` leaves `tag` empty and the engine fills in its own default:
# podman then rejects `example/app:1.0:latest` with a 500 and every version
# tag is quietly lost. Splitting happens here rather than in API::Docker,
# which mirrors the endpoint and should keep doing so.
sub _split_image_ref {
    my ( $self, $ref ) = @_;

    my $colon = rindex $ref, ':';
    return ( $ref, undef ) if $colon < 0;
    # A colon before the last slash belongs to a registry host:port, not to a
    # tag: registry.example.com:5000/team/app carries no tag at all.
    return ( $ref, undef ) if rindex($ref, '/') > $colon;

    return ( substr($ref, 0, $colon), substr($ref, $colon + 1) );

t/45-image-ref-tagging.t  view on Meta::CPAN

use strict;
use warnings;
use Test::More;

use Dist::Zilla::Plugin::Docker::API::Client;

# The engine's tag endpoint takes the repository and the tag as two separate
# parameters (POST /images/{name}/tag?repo=&tag=). Handing it a full image
# reference as `repo` and no `tag` makes the engine append its own default:
# podman turns `example/app:1.0` into `example/app:1.0:latest` and answers 500.
# The result was that every version tag silently failed while the build log
# still claimed the image had been tagged.

{
    package FakeImages;
    sub new {
        my ($class, %arg) = @_;



( run in 0.935 second using v1.01-cache-2.11-cpan-16aabcbfbcb )