NAME

App::Actions::Check - find out whether this computer can run workflows with Docker

SYNOPSIS

Check the computer and print a report (this is action-run check):

use App::Actions::Check;

my $status = App::Actions::Check->new->run;    # 0 = ready, 2 = not ready
exit $status;

Also check that Docker can see your project directory:

my $check = App::Actions::Check->new(directory => '/home/me/src/project');
exit $check->run;

Print a JSON report for a CI job (action-run check --json):

my $check = App::Actions::Check->new(json => 1);
$check->run;    # prints one JSON object

Print nothing unless something is wrong:

my $check = App::Actions::Check->new(quiet => 1);
die "Docker is not ready\n" if $check->run;

Get the report as data, without printing it:

open my $sink, '>', \my $ignored;
my $check = App::Actions::Check->new(output => $sink);
my $status = $check->run;
my $report = $check->summary($status);
print "Docker $report->{docker}{daemon}\n";

DESCRIPTION

Most failures to run a workflow locally have one of a few causes: Docker is not installed, the Docker service is not running, it runs Windows containers instead of Linux ones, or it cannot see the folders on this computer. This module checks each of these, in order, and stops at the first that fails:

1. The host. Which system and Perl this is. This never fails.
2. The Docker program. Can docker version run?
3. The Docker service (daemon). Does it answer? Does it run Linux containers?
4. Folder sharing (bind mounts). It writes a file in a new temporary folder, starts one small container (alpine:3.20 by default) with that folder mounted, and reads the file back. If you give a directory, it also mounts that folder read-only and compares the number of entries seen inside the container with the number on the computer. This finds a drive that Docker Desktop does not share.

Every problem is followed by one or more remedy lines that say how to fix it, in the words of the system the check runs on (for example Docker Desktop settings on Windows, systemctl on Linux).

A text report looks like this:

action-run check
  host      Linux (linux), perl 5.40.0, x86_64-linux
  docker    docker
  client    29.0.1
  daemon    29.0.1, linux containers
  image     alpine:3.20
  mount     /tmp/action-run-check-AbC123 -> /action-run-check: OK
  schema    version 2 (action-run schema check)
Result: ready

METHODS

new

my $check = App::Actions::Check->new(%options);

Makes a check. Nothing is checked yet.

ARGUMENTS

All are optional.

  • docker_bin - the Docker program. Default as in "new" in App::Actions::Docker.

  • image - the small image used for the folder test. Default alpine:3.20.

  • platform - passed to Docker as --platform, for example linux/amd64 on an ARM computer.

  • directory - a folder to test as well, for example your project.

  • output - where to print. A file handle. Default STDOUT.

  • title - the first line. Default action-run check. An empty string leaves it out.

  • summary - default true. False leaves out the last Result: line, for a caller that prints its own.

  • quiet - true to print the text report only if there is a problem.

  • json - true to print one JSON object instead of the text report. See "JSON OUTPUT".

  • schema_line - default true. False leaves out the schema line, for a caller whose report is not this document.

  • os - the system whose remedies are printed, as in $^O (linux, darwin, MSWin32, ...). Default: this computer's.

RETURNS

The new object.

SIDE EFFECTS

None.

EXAMPLE

my $check = App::Actions::Check->new(platform => 'linux/amd64', title => '');

API SPECIFICATION

INPUT

{
    docker_bin  => { type => 'string', optional => 1 },
    image       => { type => 'string', optional => 1, default => 'alpine:3.20' },
    platform    => { type => 'string', optional => 1 },
    directory   => { type => 'string', optional => 1 },
    output      => { type => 'object', optional => 1 },
    title       => { type => 'string', optional => 1, default => 'action-run check' },
    summary     => { type => 'boolean', optional => 1, default => 1 },
    quiet       => { type => 'boolean', optional => 1 },
    json        => { type => 'boolean', optional => 1 },
    schema_line => { type => 'boolean', optional => 1, default => 1 },
    os          => { type => 'string', optional => 1 },
}

OUTPUT

{ type => 'object', isa => 'App::Actions::Check' }

MESSAGES

None.

run

my $status = $check->run;

Runs the checks and prints the report.

ARGUMENTS

None.

RETURNS

0 if everything is ready; 2 if there is a problem. These are the exit statuses action-run check uses.

SIDE EFFECTS

Runs Docker commands; may download the probe image; starts and removes one container with no network; creates and removes a temporary folder; prints the report to output. Calling run again starts a fresh report.

EXAMPLE

if ($check->run != 0) {
    print STDERR "Fix the problems above, then try again.\n";
}

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'integer', memberof => [ 0, 2 ] }

MESSAGES

Problems are printed, not thrown. Each problem line has its own remedy lines:

+--------------------------------------------------+-----------------------------------------+
| Problem                                          | Usual cause                             |
+--------------------------------------------------+-----------------------------------------+
| the Docker command 'BIN' could not be run        | Docker not installed, or not on PATH    |
| the daemon is not reachable; is Docker running   | Docker not started, or no permission    |
| and can this user access it?                     | (add the user to the docker group)      |
| the daemon runs 'windows' containers; action-run | Docker Desktop in Windows mode          |
| needs Linux containers                           |                                         |
| cannot write 'FILE': ERR                         | temporary folder not writable           |
| 'DIR' is not a directory                         | wrong "directory" option                |
| the probe container failed (exit N)              | image cannot be pulled, or wrong        |
|                                                  | platform                                |
| the container could not read the mounted         | drive not shared, or a remote Docker    |
| temporary directory                              | (DOCKER_HOST) that cannot see this disk |
| could not count the entries in 'DIR'             | the probe printed something unexpected  |
| 'DIR' -> /check-project sees N entries, the host | drive not shared; Docker sees an empty  |
| has M                                            | or different folder                     |
+--------------------------------------------------+-----------------------------------------+

Thrown:

+----------------------------------------------+------------------------------------+------------------+
| Exception                                    | Meaning                            | What to do       |
+----------------------------------------------+------------------------------------+------------------+
| App::Actions::Schema::Invalid: the           | (json only) a bug: the document    | report it; exit  |
| action-run check --json document does not    | does not match its schema; nothing | status 70        |
| match App::Actions::Check->json_schema: ...  | is printed                         |                  |
| Unable to buffer the check report: ERR (die) | out of memory                      |                  |
+----------------------------------------------+------------------------------------+------------------+

summary

my $report = $check->summary($status);

Returns the last report as data instead of text. Call it after "run".

ARGUMENTS

  • $status - what "run" returned.

RETURNS

A hash reference with the keys described in "JSON OUTPUT". Here ready is 1 or 0, not a JSON boolean; convert it before you encode it as JSON.

SIDE EFFECTS

None.

EXAMPLE

my $report = $check->summary($check->run);
print "$_->{problem}\n" for @{ $report->{issues} };

API SPECIFICATION

INPUT

{ exit => { type => 'integer', memberof => [ 0, 2 ] } }

OUTPUT

{ type => 'hashref' }

MESSAGES

None.

host_info

my $host = App::Actions::Check->host_info;
my $host = $check->host_info;
my $host = App::Actions::Check->host_info('MSWin32');

Describes this computer, without running any check.

ARGUMENTS

  • $os - optional. The system name to report. Default: the object's os, or $^O.

RETURNS

A hash reference: os (as in $^O), name (for people, such as macOS), perl (the version, such as 5.40.0) and archname.

SIDE EFFECTS

None.

API SPECIFICATION

INPUT

{ os => { type => 'string', optional => 1 } }

OUTPUT

{ type => 'hashref' }

MESSAGES

None.

image

my $image = $check->image;

Returns the image used for the folder test.

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'string', min => 1 }

MESSAGES

None.

json_schema

my $schema = App::Actions::Check->json_schema;

Returns the JSON schema of the check --json document, as a hash reference. action-run schema check prints the same schema.

ARGUMENTS

None.

RETURNS

A new hash reference each time, so you may change it.

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'hashref' }

MESSAGES

None.

json_schema_version

my $version = App::Actions::Check->json_schema_version;

Returns the version of the JSON document's shape. The document carries the same number as schema_version. It goes up whenever the shape changes.

API SPECIFICATION

INPUT

{}

OUTPUT

{ type => 'integer', min => 1 }

MESSAGES

None.

JSON OUTPUT

With json, the report is one JSON object. Every key is always there. A value that could not be found out is null:

{
  "schema_version": 2,
  "ready": true,
  "problems": 0,
  "host": { "os": "linux", "name": "Linux", "perl": "5.40.0", "archname": "x86_64-linux" },
  "docker": { "bin": "docker", "client": "29.0.1", "daemon": "29.0.1", "containers": "linux" },
  "image": "alpine:3.20",
  "mounts": [
    { "host": "/tmp/action-run-check-XXXX", "container": "/action-run-check", "ok": true },
    { "host": "/src/project", "container": "/check-project", "entries": 17, "expected": 17, "ok": true }
  ],
  "issues": [ { "problem": "...", "remedies": [ "..." ] } ]
}

schema_version is the number "json_schema_version" returns. The schema itself is what action-run schema check prints. The examples in t/fixtures/check.json, check-unready.json, check-unreachable.json and check-uncounted.json show a ready computer, a folder mismatch, a Docker that could not be run, and a folder whose entries could not be counted.

Before the document is printed, it is checked against its schema. If it does not match (a bug), nothing is printed and an App::Actions::Schema::Invalid is thrown; action-run then exits with 70. The return value of "run" is the same with or without json.

COMMON PITFALLS

  • The check stops at the first failure. If Docker is not running, the folder test is not tried. Run the check again after each fix.

  • It downloads an image. The first run pulls alpine:3.20 (a few megabytes). Without internet access, pass an image you already have.

  • The image needs sh, cat, ls and wc. A "distroless" or "scratch" image will fail the probe.

  • A remote Docker cannot see your files. With DOCKER_HOST pointing at another computer, the folder test fails. That is a real problem: workflows will not see your project either.

  • summary is only meaningful after run. Before it, the report is empty.

  • In summary, ready is 0 or 1, not a JSON boolean.

  • quiet does not silence json. With json, the document is always printed.

ENCODING

Folder names may contain any characters your system allows, including non-ASCII and emoji; they are passed to Docker as bytes and reported as bytes. The text report itself is ASCII except for those names and for what Docker prints. The JSON report is UTF-8 encoded only if the input strings are; decode folder names with your system's encoding if you need characters.

AUTHOR

Nigel Horne, njh@nigelhorne.com

FORMAL SPECIFICATION

This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above.

How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. ΔCheck means the routine can change the object. The lines below the rule are always true when the routine returns.

[ISSUE, MOUNT]
STAGE ::= host | program | daemon | linux | mounts | project
passes : STAGE → 𝔹                   what the computer would answer

┌─ Check ───────────────────────────────────────────
│ image    : seq₁ CHAR
│ problems : ℕ
│ issues   : seq ISSUE
│ mounts   : seq MOUNT
├───────────────────────────────────────────────────
│ problems = #issues
└───────────────────────────────────────────────────

new

┌─ New ─────────────────────────────────────────────
│ Check'
│ image? : optional seq CHAR
├───────────────────────────────────────────────────
│ image' = if image? = ⊥ ∨ image? = ⟨⟩ then "alpine:3.20" else image?
│ problems' = 0 ∧ issues' = ⟨⟩ ∧ mounts' = ⟨⟩
└───────────────────────────────────────────────────

run

┌─ Run ─────────────────────────────────────────────
│ ΔCheck
│ r! : {0, 2}
├───────────────────────────────────────────────────
│ order = ⟨program, daemon, linux, mounts, project⟩
│ failed = first s in order with ¬passes(s), or ⊥
│ failed = ⊥ ⇒ r! = 0 ∧ problems' = 0
│ failed ≠ ⊥ ⇒ r! = 2 ∧ problems' = 1
│ ∀ s after failed in order • s is not tried
│ project is tried only when a directory was given
└───────────────────────────────────────────────────

summary

┌─ Summary ─────────────────────────────────────────
│ ΞCheck
│ exit? : {0, 2}
│ r!    : KEY ⇸ VALUE
├───────────────────────────────────────────────────
│ r!.ready = (if exit? = 0 then 1 else 0)
│ r!.problems = problems ∧ r!.issues = issues ∧ r!.mounts = mounts
│ r!.schema_version = json_schema_version
└───────────────────────────────────────────────────

host_info

┌─ HostInfo ────────────────────────────────────────
│ os? : optional seq CHAR
│ r!  : KEY ⇸ VALUE
├───────────────────────────────────────────────────
│ r!.os = first non-empty of ⟨os?, self.os, $^O⟩
│ r!.name = displayName(r!.os) if known, else r!.os
│ dom r! = {os, name, perl, archname}
└───────────────────────────────────────────────────

image

┌─ Image ───────────────────────────────────────────
│ ΞCheck
│ r! : seq₁ CHAR
├───────────────────────────────────────────────────
│ r! = image
└───────────────────────────────────────────────────

json_schema

┌─ JsonSchema ──────────────────────────────────────
│ r! : SCHEMA
├───────────────────────────────────────────────────
│ r! = decode(JSON_SCHEMA) ∧ r! is a new object
└───────────────────────────────────────────────────

json_schema_version

┌─ JsonSchemaVersion ───────────────────────────────
│ r! : ℕ₁
├───────────────────────────────────────────────────
│ r! = JSON_SCHEMA_VERSION
└───────────────────────────────────────────────────

STATE DIAGRAM

"run" moves through these states. Any failed test records one problem with its remedies and jumps to NOT READY:

 new(%options)
      |
      v
+-----------+  run   +------------+       +----------------+  fails  +-----------+
|  CREATED  |------->| HOST       |------>| DOCKER PROGRAM |-------->|           |
+-----------+        | (always ok)|       | docker version |         |           |
      ^              +------------+       +----------------+         |           |
      |                                          | ok                |           |
      |                                          v                   |           |
      |                                   +----------------+  fails  |           |
      |                                   | DAEMON         |-------->|           |
      |                                   | server version |         |           |
      |                                   +----------------+         |           |
      |                                          | ok                |           |
      |                                          v                   |    NOT    |
      |                                   +----------------+  not    |   READY   |
      |                                   | LINUX          |  linux  |  status 2 |
      |                                   | CONTAINERS?    |-------->|           |
      |                                   +----------------+         |           |
      |                                          | yes               |           |
      |                                          v                   |           |
      |                                   +----------------+  fails  |           |
      |                                   | TEMP MOUNT     |-------->|           |
      |                                   | write, run,    |         |           |
      |                                   | read back      |         |           |
      |                                   +----------------+         |           |
      |                                          | ok                |           |
      |                          no directory    v                   |           |
      |                        +---------+---------------+  differ   |           |
      |                        |         | PROJECT MOUNT |---------->|           |
      |                        |         | count entries |           +-----------+
      |                        |         +---------------+                 |
      |                        v                 | same                     |
      |                  +-----------+           |                          |
      |                  |   READY   |<----------+                          |
      |                  |  status 0 |                                      |
      |                  +-----------+                                      |
      |                        |                                            |
      |                        +--------------------+-----------------------+
      |                                             v
      |                                  +---------------------+
      |                                  | REPORT              |  json: check against the
      |                                  | text, or JSON       |  schema, then print (or
      |                                  | (quiet: only if not |  throw Schema::Invalid)
      |                                  |  ready)             |
      |                                  +---------------------+
      |                                             |
      +---------------- run again ------------------+   summary($status) reads the report

LICENSE AND COPYRIGHT

Copyright 2026 Nigel Horne.

This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.