Dist-Zilla-Plugin-Docker-API

 view release on metacpan or  search on metacpan

.gitignore  view on Meta::CPAN

Build
Build.bat
pm_to_blib
# XS build artifacts
*.o
*.bs
# Testing
cover_db/
pod2htmd.tmp
# Claude Code — commit: skills/, agents/, hooks/, settings.json
# Ignore: local overrides, credentials, session data
.claude/*.local.*
.claude/local/
.claude/.credentials.json
.claude/statsig/
.claude/todos/
.claude/projects/

# karr materialized task view -- never commit
tasks/
config.yml

# skilletor: committe Config + Ignore-Block
!.claude/skilletor.json

CLAUDE.md  view on Meta::CPAN


2. **Use `mcp__firecrawl__firecrawl_scrape`** over `WebFetch` for fetching
   page content.

3. **Use `context7` for library docs** (CPAN, npm, etc.) — *except* this
   distribution itself. For `Dist::Zilla::Plugin::Docker::API` always read
   the local source under `lib/`, never context7.

4. **Untracked files that are not in `.gitignore` belong in the commit.**
   `.gitignore` is the source of truth. Only obvious secrets
   (`.env`, credentials) are excluded — and even then warn, don't silently
   drop them.

5. **Auto-Memory is for personal/user preferences only.** Project
   conventions belong in this `CLAUDE.md` or in a skill, never in
   auto-memory.

6. **Perl edits go to a `dzil-docker-*` agent**, which gets
   `getty-perl-core` and the object-system skills force-loaded via
   `briefing.skills`. If you are editing Perl without that briefing, load
   `getty-perl-core` first. See Delegation below.

Changes  view on Meta::CPAN

    build), and empty directories. Top-level dotfiles such as
    .dockerignore are no longer left out of the context.
  - build_load, release_load and the deprecated load are removed: nothing
    ever read them, as the image always lands in the engine's store when
    built through the API. Setting one in a dist.ini now warns that it is
    no longer supported and is discarded, like phase.
  - A second "platform" value is now a fatal error instead of being
    dropped silently, which built and pushed a single-platform image
    from a multi-platform config. The /build endpoint takes exactly one
    platform; multi-arch images are not what this plugin does.
  - Registry credentials are now resolved from an ordered list of auth
    files instead of only ~/.docker/config.json, so a plain "podman
    login" (or "docker login") is picked up automatically. The order,
    first file with a matching entry winning, is REGISTRY_AUTH_FILE,
    then $DOCKER_CONFIG/config.json, then Podman's
    $XDG_RUNTIME_DIR/containers/auth.json, then
    ~/.config/containers/auth.json, then ~/.docker/config.json. The
    per-registry entry selection is unchanged.

0.104     2026-08-30 03:51:54Z
  - "dockerfile = ..." in a dist.ini now takes effect; the deprecated

dist_zilla_docker_api_plugin_plan.md  view on Meta::CPAN

Support three layers:

### 1. Let Docker daemon use existing auth

Default for v0.001.

```ini
push = 1
```

No explicit credentials. The Docker daemon/client auth config handles registry credentials.

### 2. Dist::Zilla stash

Later:

```ini
registry_auth = %DockerHub
```

or:

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


# A rejected or expired registry credential otherwise surfaces only when the
# push fails -- after Dist::Zilla gathered and wrote the distribution and
# after_build built and tagged the image, i.e. after the damage. This runs
# before any of it.
#
# The phase hook that runs early enough is before_build, and it can tell a
# release from a plain build: Dist::Zilla::Dist::Builder::release sets
# DZIL_RELEASING before it calls build_archive, so the variable is already
# there when this hook runs (measured against Dist::Zilla 6.037). A plain
# dzil build never sets it and so never needs registry credentials.
#
# Only the registry host of `image` decides which credential applies, and
# that part carries no template variables, so no expansion is needed here.
sub _precheck_registry_auth {
    my ($self) = @_;

    my $image_ref = $self->image;

    my $status = eval { $self->client->verify_auth_for_image_ref($image_ref) };
    my $error = $@;

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

    # own text, which does, is carried through verbatim.
    if ($error) {
        $self->log_fatal('the registry credential check for '.$image_ref
            .' failed: '.$self->_flatten_error($error)
            .' (set DZIL_DOCKER_API_SKIP_PRECHECK=1 to skip this check)');
    }

    # No credential found for that registry. An anonymous push is a legal
    # thing to attempt, so this is a note, not a failure.
    unless (defined $status) {
        $self->log('Docker::API: no registry credentials found for '
            .$image_ref.' - the release push will be anonymous');
        return;
    }

    $self->log('Docker::API registry credentials accepted for '.$image_ref);
}

sub after_build {
    my ($self, $arg) = @_;

    return if $ENV{DZIL_DOCKER_API_SKIP};

    $self->log("Docker::API building image");

    my $build_root = $arg->{build_root};

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

Set C<DZIL_DOCKER_API_SKIP_PRECHECK=1> to skip the check and get the previous
behaviour back, where an unreachable engine only surfaces once the build
reaches the image. With several C<Docker::API> plugins in one F<dist.ini>,
each runs its own precheck.

When C<before_build> runs as part of C<dzil release> (Dist::Zilla sets
C<DZIL_RELEASING> before it calls C<build_archive>, which is early enough to
tell a release from a plain build) and both C<release_enabled> and
C<release_push> are true, the same hook also pre-flights the registry
credential the eventual push would use -- resolved as described in
L</"Registry credentials"> below -- and hands it to the engine's
C<POST /auth> (C<< system->auth >>) before anything is built. A plain
C<dzil build> never triggers this and needs no registry credentials at all.
No credential resolved for the registry is not a failure -- an anonymous push
is a legal thing to attempt, so nothing is checked and nothing fails. A
failed check is fatal, before the build starts, and its message says only
that the check failed, not that the credential was rejected: Podman answers
a rejected credential and an unreachable registry with the same C<500>, so
the two cannot be told apart from the status alone, and the engine's own
text is included instead.

C<DZIL_DOCKER_API_SKIP_PRECHECK=1> skips this credential check along with the
engine version probe above.

Set C<DZIL_DOCKER_API_SKIP=1> to skip the image build entirely for one run --
no engine contact, no image, one loud log line per plugin. This is for local
C<dzil build> / C<dzil install> / C<dzil test> while the image cannot build
yet, for example while a dependency pinned in the F<Dockerfile>'s C<cpanm>
run is not released. C<dzil release> refuses to run with the variable set:
a skipped build phase means there is no image to tag and push.

=head2 Registry credentials

The release push, the C<fail_if_tag_exists> lookup and the registry
credential precheck above all resolve a credential for an image reference
the same way, through C<auth_for_image_ref>, which walks an ordered list of
candidate auth files rather than reading a single one. For the registry the
image reference names, the first candidate file with a usable entry for it
wins; a file with none falls through to the next, so a Docker config and a
Podman auth file can each supply credentials for different registries in
the same run. In order: C<REGISTRY_AUTH_FILE>, if set, names the file
directly; then F<config.json> in the directory named by C<DOCKER_CONFIG>,
if set; then F<$XDG_RUNTIME_DIR/containers/auth.json>, if
C<XDG_RUNTIME_DIR> is set; then F<containers/auth.json> under
C<XDG_CONFIG_HOME> (or under F<~/.config> when that is unset); and finally
F<~/.docker/config.json>. F<~/.dockercfg>, Docker's legacy
pre-C<config.json> format, is never read.

All five files share the same C<auths> block, so once a file is chosen the
selection within the matching registry's entry does not depend on which one

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

Podman's classic builder C<STEP N/M: ...> and BuildKit's C<#N [N/M] ...> —
instead of the full per-command output.
Set to true to see every line the daemon streams back. Errors are always
surfaced regardless of this flag.

=item C<fail_if_tag_exists> - Abort the release if any tag already exists on
the remote registry (default: false). The check runs before anything is
tagged or pushed, and only when C<release_push> is also true. It asks the
I<registry>, not the local daemon, through C<API::Docker>'s
C<< distribution->exists >> (C<GET /distribution/{name}/json>), using the
credential resolved for C<image> (see L</"Registry credentials">), or an
anonymous request when none applies.

An engine that has no C</distribution> route -- rootless Podman among them --
cannot answer the question at all, and that is treated as a release-stopping
failure rather than as "the tag is free": the release aborts with the
engine's own error and a reminder that C<fail_if_tag_exists = 0> releases
without the check.

=item C<skip_latest_on_trial> - Skip C<latest> tag for trial releases

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

    }
    return @files;
}

sub auth_for_image_ref {
    my ($self, $image_ref) = @_;
    my $registry = $self->_registry_for_image_ref($image_ref);
    # Walk the parsed auth files in order and run the per-file registry
    # selection against each. The first file with a usable entry for this
    # registry wins; a file that has none falls through to the next, so a
    # docker config.json and a podman auth.json can each supply credentials
    # for different registries.
    for my $config (@{ $self->_auth_configs }) {
        my $auth = $self->_auth_for_registry($registry, $config);
        return $auth if defined $auth;
    }
    return undef;
}

sub _registry_for_image_ref {
    my ($self, $image_ref) = @_;

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


    my $auth = $self->auth_for_image_ref($image_ref);

    return $self->docker->distribution->exists(
        $image_ref,
        defined $auth ? ( auth => $auth ) : (),
    ) ? 1 : 0;
}

# Pre-flight for the release push: hand the engine, through POST /auth, the
# very credentials push_image would use for this reference and let the
# registry judge them. A rejected credential croaks; the caller is meant to
# make that fatal before anything is built.
#
# Returns undef when no credential could be resolved for the reference. That
# is not a failure -- an anonymous push to a public registry is a legal thing
# to do, and system->auth croaks on an empty AuthConfig, so there would be
# nothing to ask about.
sub verify_auth_for_image_ref {
    my ($self, $image_ref) = @_;

misc/agent-context/CLAUDE.md  view on Meta::CPAN


2. **Use `mcp__firecrawl__firecrawl_scrape`** over `WebFetch` for fetching
   page content.

3. **Use `context7` for library docs** (CPAN, npm, etc.) — *except* this
   distribution itself. For `Dist::Zilla::Plugin::Docker::API` always read
   the local source under `lib/`, never context7.

4. **Untracked files that are not in `.gitignore` belong in the commit.**
   `.gitignore` is the source of truth. Only obvious secrets
   (`.env`, credentials) are excluded — and even then warn, don't silently
   drop them.

5. **Auto-Memory is for personal/user preferences only.** Project
   conventions belong in this `CLAUDE.md` or in a skill, never in
   auto-memory.

6. **Perl edits go to a `dzil-docker-*` agent**, which gets
   `getty-perl-core` and the object-system skills force-loaded via
   `briefing.skills`. If you are editing Perl without that briefing, load
   `getty-perl-core` first. See Delegation below.

t/54-release-auth-precheck.t  view on Meta::CPAN

    is($calls->[0]{image_ref}, 'ghcr.io/example/my-app',
        'it was asked about the configured image');

    is(scalar @{ $rec->calls_of('engine_info') }, 1,
        'the engine precheck still ran too');
    is(scalar @{ $rec->calls_of('build_image') }, 1,
        'and the build still happened');
};

# A plain `dzil build` never sets DZIL_RELEASING. No release is happening,
# so no registry credentials should be needed at all.
subtest 'does not fire on a plain dzil build' => sub {
    local $ENV{DZIL_DOCKER_API_SKIP_PRECHECK};
    delete $ENV{DZIL_DOCKER_API_SKIP_PRECHECK};
    local $ENV{DZIL_RELEASING};
    delete $ENV{DZIL_RELEASING};

    my $tzil = build_dist('');
    $tzil->build;

    my $rec = docker_plugin($tzil)->client;
    is(scalar @{ $rec->calls_of('verify_auth_for_image_ref') }, 0,
        'the auth precheck was never asked');
    is(scalar @{ $rec->calls_of('build_image') }, 1,
        'the build still happened');
};

# A release that never pushes needs no registry credentials either.
subtest 'does not fire when release_push = 0' => sub {
    local $ENV{DZIL_DOCKER_API_SKIP_PRECHECK};
    delete $ENV{DZIL_DOCKER_API_SKIP_PRECHECK};
    local $ENV{DZIL_RELEASING} = 1;

    my $tzil = build_dist("release_push = 0");
    $tzil->build;

    my $rec = docker_plugin($tzil)->client;
    is(scalar @{ $rec->calls_of('verify_auth_for_image_ref') }, 0,

t/54-release-auth-precheck.t  view on Meta::CPAN


    my $ok = eval { $tzil->build; 1 };
    ok($ok, 'the build did not die') or diag("build died: $@");

    is(scalar @{ $rec->calls_of('verify_auth_for_image_ref') }, 1,
        'the precheck was still asked');
    is(scalar @{ $rec->calls_of('build_image') }, 1,
        'and the build proceeded');

    my $logged = join "\n", @{ $tzil->log_messages };
    like($logged, qr/no registry credentials found/i,
        'the anonymous case is noted in the log');
};

# DZIL_DOCKER_API_SKIP_PRECHECK=1 skips *both* prechecks. Prove it against a
# credential that would otherwise be fatal, so a no-op skip couldn't pass by
# accident.
subtest 'DZIL_DOCKER_API_SKIP_PRECHECK=1 skips the engine and the auth check' => sub {
    local $ENV{DZIL_DOCKER_API_SKIP_PRECHECK} = 1;
    local $ENV{DZIL_RELEASING} = 1;

t/56-client-registry-internals.t  view on Meta::CPAN

        'no auth key at all -- an explicit auth => undef is not the same request';
};

subtest 'remote_tag_exists passes the resolved auth when one exists' => sub {
    my ($client, $distribution) = client_for(exists_result => 1);

    my $result = $client->remote_tag_exists('ghcr.io/getty/foo:v1');

    is $result, 1, 'truthy engine answer normalized to 1';
    my $call = $distribution->{calls}[0];
    ok exists $call->{auth}, 'auth key present for a registry with credentials';
    is $call->{auth}{username}, 'getty', 'resolved username carried through';
    is $call->{auth}{password}, 'ghpat', 'resolved password carried through';
};

subtest 'verify_auth_for_image_ref never calls system->auth without a credential' => sub {
    my ($client, undef, $system) = client_for();

    my $result = $client->verify_auth_for_image_ref('unknown.example.com/foo:tag');

    is $result, undef, 'no credential -> undef, not a croak';

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

);

# What verify_auth_for_image_ref returns. undef (the default) means no
# credential could be resolved for the reference, which is not an error.
has verify_auth_result => (
    is      => 'rw',
    default => sub { undef },
);

# Set to a message to make verify_auth_for_image_ref croak -- the registry
# rejecting the credentials.
has verify_auth_error => (
    is      => 'rw',
    default => sub { undef },
);

sub _record {
    my ($self, $name, %args) = @_;
    push @{ $self->calls }, { method => $name, %args };
}



( run in 1.441 second using v1.01-cache-2.11-cpan-3f962061493 )