Check Image is a Go-based CLI tool designed for validating container images. It ensures that images meet specific standards such as size, age, ports, and security configurations. This project follows Go conventions for command-line tools and is structured into cmd and internal directories.
- Installation
- Docker
- GitHub Action
- Usage
- Commands
- Configuration Files
- Development
- Testing
- CI/CD and Release Process
- Contributing
- License
Download the latest release for your platform from the releases page:
# Set the version you want to install (or use 'latest' tag from releases page)
VERSION=1.0.0 # x-release-please-version
# macOS (Apple Silicon)
curl -sL "https://github.com/jarfernandez/check-image/releases/download/v${VERSION}/check-image_${VERSION}_darwin_arm64.tar.gz" | tar xz
sudo mv check-image /usr/local/bin/
# macOS (Intel)
curl -sL "https://github.com/jarfernandez/check-image/releases/download/v${VERSION}/check-image_${VERSION}_darwin_amd64.tar.gz" | tar xz
sudo mv check-image /usr/local/bin/
# Linux (amd64)
curl -sL "https://github.com/jarfernandez/check-image/releases/download/v${VERSION}/check-image_${VERSION}_linux_amd64.tar.gz" | tar xz
sudo mv check-image /usr/local/bin/
# Linux (arm64)
curl -sL "https://github.com/jarfernandez/check-image/releases/download/v${VERSION}/check-image_${VERSION}_linux_arm64.tar.gz" | tar xz
sudo mv check-image /usr/local/bin/
# Windows (amd64)
# Download https://github.com/jarfernandez/check-image/releases/download/v${VERSION}/check-image_${VERSION}_windows_amd64.zip
# and extract to a directory in your PATHPre-built binaries include the correct version number (e.g., check-image version --short returns v1.0.0).
Requirements: macOS or Linux with Homebrew installed.
brew tap jarfernandez/tap
brew install check-imageTo upgrade to a new version:
brew upgrade check-imageRequirements: Go 1.26 or newer
# Install the latest version
go install github.com/jarfernandez/check-image/cmd/check-image@latest
# Or install a specific version
go install github.com/jarfernandez/check-image/cmd/[email protected] # x-release-please-versionThis will install the check-image binary to your GOBIN directory.
Note: Binaries installed with go install will show version as dev when running check-image version. This is expected behavior as go install compiles from source without version injection. For production use with correct version numbers, use pre-built binaries from releases.
If you've cloned the repository, you can install it locally:
go install ./cmd/check-imageThis is useful for development. The version will show as dev.
Check Image is available as a multi-arch Docker image (linux/amd64, linux/arm64) from GitHub Container Registry:
docker pull ghcr.io/jarfernandez/check-image:latestBasic usage (validates remote registry images):
# Check image age
docker run --rm ghcr.io/jarfernandez/check-image age nginx:latest --max-age 30
# Check image size
docker run --rm ghcr.io/jarfernandez/check-image size nginx:latest --max-size 100
# Check user security requirements
docker run --rm ghcr.io/jarfernandez/check-image user nginx:latest
# Run all checks with JSON output
docker run --rm ghcr.io/jarfernandez/check-image all nginx:latest -o jsonUsing policy files via volume mounts:
# Mount a local config directory
docker run --rm \
-v "$(pwd)/config:/config:ro" \
ghcr.io/jarfernandez/check-image registry nginx:latest \
--registry-policy /config/registry-policy.json
# Run all checks with a config file
docker run --rm \
-v "$(pwd)/config:/config:ro" \
ghcr.io/jarfernandez/check-image all nginx:latest \
--config /config/config.yamlUsing with Docker socket (advanced):
Warning: Mounting the Docker socket grants the container full access to the Docker daemon, which is equivalent to root access on the host. Only use this in trusted environments.
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
ghcr.io/jarfernandez/check-image age my-local-image:latestWithout the Docker socket mounted (the default), check-image automatically uses the remote registry to fetch image metadata. This is the recommended approach for CI/CD pipelines.
Using a specific version:
docker pull ghcr.io/jarfernandez/check-image:1.0.0 # x-release-please-versionCheck Image is available as a GitHub Action for validating container images directly in your CI/CD workflows.
- uses: jarfernandez/[email protected] # x-release-please-version
with:
image: nginx:latestThis runs all 10 checks with default settings. Checks that require additional configuration (registry, labels, platform) will report an error unless their configuration is provided.
- uses: jarfernandez/[email protected] # x-release-please-version
with:
image: myorg/myapp:${{ github.sha }}
config: .check-image/config.yamlThe config file determines which checks to run and their parameters. See All Checks Configuration Files for the format.
- uses: jarfernandez/[email protected] # x-release-please-version
with:
image: nginx:latest
checks: age,size,user
max-age: '30'
max-size: '200'- uses: jarfernandez/[email protected] # x-release-please-version
with:
image: ghcr.io/myorg/app:latest
registry-policy: policies/registry-policy.yaml
labels-policy: policies/labels-policy.json
skip: healthcheckUse continue-on-error to prevent the action from failing the workflow:
- uses: jarfernandez/[email protected] # x-release-please-version
id: check
continue-on-error: true
with:
image: nginx:latest
config: .check-image/config.yaml
- name: Handle results
if: steps.check.outputs.result == 'failed'
run: echo "Image validation failed but continuing"The action captures full JSON output for programmatic use in subsequent steps:
- uses: jarfernandez/[email protected] # x-release-please-version
id: check
continue-on-error: true
with:
image: nginx:latest
checks: age,size
- name: Process results
if: always()
run: echo '${{ steps.check.outputs.json }}' | jq '.summary'| Input | Required | Default | Description |
|---|---|---|---|
image |
Yes | - | Container image to validate |
config |
No | - | Path to config file for the all command |
checks |
No | - | Comma-separated list of checks to run (mutually exclusive with skip) |
skip |
No | - | Comma-separated list of checks to skip (mutually exclusive with checks) |
fail-fast |
No | false |
Stop on first check failure |
max-age |
No | - | Maximum image age in days |
max-size |
No | - | Maximum image size in MB |
max-layers |
No | - | Maximum number of layers |
allowed-ports |
No | - | Comma-separated allowed ports or @file path |
allowed-platforms |
No | - | Comma-separated allowed platforms or @file path |
registry-policy |
No | - | Path to registry policy file |
labels-policy |
No | - | Path to labels policy file |
secrets-policy |
No | - | Path to secrets policy file |
skip-env-vars |
No | false |
Skip environment variable checks |
skip-files |
No | false |
Skip file system checks |
allow-shell-form |
No | false |
Allow shell form for entrypoint or cmd |
log-level |
No | info |
Log level |
version |
No | 1.0.0 |
check-image version to use |
| Output | Description |
|---|---|
result |
Validation result: passed, failed, or error |
json |
Full JSON output from check-image |
The action also generates a Step Summary visible in the GitHub Actions UI with a results table, details of failed checks, and full JSON output.
The action downloads the check-image binary from GitHub Releases and verifies its SHA-256 checksum against the checksums.txt published by GoReleaser before execution. No additional dependencies are needed for validating remote registry images. To validate local Docker images (e.g., after docker build), Docker must be available on the runner — this is satisfied by default on ubuntu-latest runners.
After installation, you can run the CLI tool:
check-image --helpCheck Image supports multiple image sources using a transport-based syntax (compatible with Skopeo):
OCI Layout Directory (with tag):
check-image age oci:/path/to/layout:latest
check-image size oci:./nginx-layout:v1.23 --max-size 100OCI Layout Directory (with digest):
check-image age oci:/path/to/layout@sha256:abc123...OCI Archive (tarball):
check-image age oci-archive:/path/to/image.tar:latest
check-image size oci-archive:./exported-image.tar:1.0 --max-size 100Docker Archive (saved with docker save):
check-image age docker-archive:/path/to/saved.tar:nginx:latest
check-image size docker-archive:./backup.tar:myapp:2.0Default Behavior (Docker daemon or remote registry):
check-image age nginx:latest
check-image size docker.io/nginx:latest --max-size 100
# Tag defaults to 'latest' if not specified
check-image age nginx
check-image registry ghcr.io/kubernetes-sigs/kind/node --registry-policy config/registry-policy.jsonCreating Archive Files:
To create OCI archives:
# Using skopeo
skopeo copy docker://nginx:latest oci-archive:nginx.tar:latest
# Using podman
podman save --format oci-archive -o nginx.tar nginx:latestTo create Docker archives:
# Using docker
docker save -o nginx.tar nginx:latest
# Using podman (Docker-compatible format)
podman save --format docker-archive -o nginx.tar nginx:latestImportant Notes:
- When using explicit transport prefixes (
oci:,oci-archive:,docker-archive:), only that source is attempted (no fallback) - Without a transport prefix, Check Image tries the local Docker daemon first, then falls back to remote registry
- The
registrycommand validation is skipped for non-registry transports (e.g.,oci:) - OCI archives are extracted to a temporary directory during processing (automatically cleaned up)
- Archive extraction includes security checks: path traversal protection and 5GB decompression limit
The CLI supports various commands for validating container images. Each command is defined in the cmd/check-image/commands directory.
Validates that the image size and layer count are within acceptable limits.
check-image size <image> --max-size <MB> --max-layers <count>Options:
--max-size: Maximum image size in MB (default: 500)--max-layers: Maximum number of layers (default: 20)
Validates that the image is not older than a specified number of days.
check-image age <image> --max-age <days>Options:
--max-age: Maximum image age in days (default: 90)
Validates that the image registry is trusted based on a policy file.
check-image registry <image> --registry-policy <file>Options:
--registry-policy: Path to registry policy file (JSON or YAML, required)
Policy file supports either:
trusted-registries: Allowlist of trusted registriesexcluded-registries: Blocklist of excluded registries
Validates that the image does not expose unauthorized ports.
check-image ports <image> --allowed-ports <ports>Options:
--allowed-ports: Comma-separated list of allowed ports or@<file>with JSON/YAML array
Validates that the image has a healthcheck defined.
check-image healthcheck <image>The command checks that:
- A healthcheck section exists in the image configuration
- The healthcheck test command is not empty
- The healthcheck is not explicitly disabled (
NONE)
Validates that the image does not contain sensitive data (passwords, tokens, keys).
check-image secrets <image> [--secrets-policy <file>] [--skip-env-vars] [--skip-files]Options:
--secrets-policy: Path to secrets policy file (JSON or YAML, optional)--skip-env-vars: Skip environment variable checks--skip-files: Skip file system checks
The command scans:
- Environment variables for sensitive patterns (password, secret, token, key, etc.)
- Files across all image layers for common secret files (SSH keys, cloud credentials, password files, etc.)
Validates that the image has a startup command defined (ENTRYPOINT or CMD) and uses exec form.
check-image entrypoint <image> [--allow-shell-form]Options:
--allow-shell-form: Allow shell form without failing (default: exec form required)
The command checks that:
- At least one of ENTRYPOINT or CMD is defined in the image configuration
- Neither uses shell form (
/bin/sh -c ...or/bin/bash -c ...) unless--allow-shell-formis set
Exec form (["nginx", "-g", "daemon off;"]) is preferred over shell form because it avoids an intermediate shell process, ensures signals (e.g. SIGTERM) reach the real process directly, and eliminates unintended shell interpretation.
When --allow-shell-form is set and shell form is detected, the check passes and the result details include "shell-form-allowed": true for transparency.
Validates that the image has required labels (OCI annotations) with correct values.
check-image labels <image> --labels-policy <file>Options:
--labels-policy: Path to labels policy file (JSON or YAML, required)
The command validates three types of requirements:
- Existence check: Label must be present with any value (only
namespecified) - Exact value match: Label value must exactly match the specified string (
nameandvalue) - Pattern match: Label value must match the regular expression (
nameandpattern)
Validates that the image platform (OS and architecture) is in the allowed list.
check-image platform <image> --allowed-platforms <platforms>Options:
--allowed-platforms: Comma-separated list of allowed platforms or@<file>with JSON/YAML array (required)
The platform string is constructed as OS/Architecture (e.g., linux/amd64) or OS/Architecture/Variant for architectures with variants (e.g., linux/arm/v7). The check validates the resolved image's platform — the platform of the concrete image that would actually be executed, not a manifest index listing.
Validates that the image user meets security requirements.
check-image user <image> [--user-policy <file>] [--min-uid <n>] [--max-uid <n>] [--blocked-users <list>] [--require-numeric]Options:
--user-policy: Path to user policy file (JSON or YAML, optional). Supports-for stdin--min-uid: Minimum allowed UID (optional)--max-uid: Maximum allowed UID (optional)--blocked-users: Comma-separated list of blocked usernames (optional)--require-numeric: Require user to be a numeric UID (optional)
Without any flags or policy file, performs a basic non-root check (rejects empty user, "root", and UID 0). With flags or a policy file, enforces UID ranges, blocked usernames, and numeric UID requirements.
Precedence: CLI flags override policy file values. When both are provided, the policy file is loaded first, then CLI flags are overlaid on top.
Limitation: Without the image's /etc/passwd, username-to-UID resolution is not possible. The command validates the raw User field string only. UID range checks (--min-uid, --max-uid) only apply when the user is a numeric value.
Runs all validation checks on a container image at once.
check-image all <image> [flags]Options:
--config,-c: Path to configuration file (JSON or YAML)--include: Comma-separated list of checks to run (age, size, ports, registry, healthcheck, secrets, labels, entrypoint, platform, user)--skip: Comma-separated list of checks to skip (age, size, ports, registry, healthcheck, secrets, labels, entrypoint, platform, user)--max-age,-a: Maximum age in days (default: 90)--max-size,-m: Maximum size in MB (default: 500)--max-layers,-y: Maximum number of layers (default: 20)--allowed-ports,-p: Comma-separated list of allowed ports or@<file>--allowed-platforms: Comma-separated list of allowed platforms or@<file>--registry-policy,-r: Registry policy file (JSON or YAML)--labels-policy: Labels policy file (JSON or YAML)--secrets-policy,-s: Secrets policy file (JSON or YAML)--skip-env-vars: Skip environment variable checks in secrets detection--skip-files: Skip file system checks in secrets detection--allow-shell-form: Allow shell form for entrypoint or cmd--user-policy: User policy file (JSON or YAML)--min-uid: Minimum allowed UID--max-uid: Maximum allowed UID--blocked-users: Comma-separated list of blocked usernames--require-numeric: Require user to be a numeric UID--fail-fast: Stop on first check failure (default: false)
Note: --include and --skip are mutually exclusive.
Precedence rules:
- Without
--config: all 10 checks run with defaults, except those in--skip - With
--config: only checks present in the config file run, except those in--skip --includeoverrides config file check selection (runs only specified checks)- CLI flags override config file values
--includeand--skipalways take precedence over the config file
Shows the check-image version with full build information.
check-image versioncheck-image version v0.12.1
commit: a1b2c3d
built at: 2026-02-18T12:34:56Z
go version: go1.26.0
platform: linux/amd64
Options:
--short: Print only the version number
check-image version --shortv0.12.1
The version and build metadata are injected at build time using ldflags:
go build \
-ldflags "-X github.com/jarfernandez/check-image/internal/version.Version=v0.1.0 \
-X github.com/jarfernandez/check-image/internal/version.Commit=abc1234 \
-X github.com/jarfernandez/check-image/internal/version.BuildDate=2026-02-18T12:34:56Z" \
./cmd/check-imageThe Go version and platform are read from the Go runtime and do not require ldflags injection.
All commands support:
--output,-o: Output format:text(default),json--color: Color output mode:auto(default),always,never— only applies to--output=text. Inautomode, colors are enabled when stdout is a terminal and disabled in pipes, redirections, and CI. Respects theNO_COLORenvironment variable andCLICOLOR_FORCE--log-level: Set log level (trace, debug, info, warn, error, fatal, panic)--username: Registry username for authentication (env:CHECK_IMAGE_USERNAME)--password: Registry password or token (env:CHECK_IMAGE_PASSWORD). Caution: visible in process list — prefer--password-stdinor the env var.--password-stdin: Read the registry password from stdin. Cannot be combined with other flags that also read from stdin (--config -,--allowed-ports @-, etc.)
Check Image supports three ways to provide credentials for private registries, applied with the following precedence:
- CLI flags (
--username/--password/--password-stdin) — highest priority - Environment variables (
CHECK_IMAGE_USERNAME/CHECK_IMAGE_PASSWORD) ~/.docker/config.jsonand credential helpers (authn.DefaultKeychain) — already works without any changes
Explicit credentials are scoped to the target registry extracted from the image reference. Requests to other registries (e.g., base-layer pulls from a different host) use the default keychain (~/.docker/config.json and credential helpers) instead, preventing unintended credential forwarding. For per-registry credentials across multiple invocations, configure Docker's credential helpers in ~/.docker/config.json as usual.
Using flags:
check-image size my-registry.example.com/private-image:latest \
--username myuser \
--password mypasswordUsing --password-stdin (recommended — avoids password in process list):
echo "mytoken" | check-image age my-registry.example.com/private-image:latest \
--username myuser \
--password-stdin
# Or read from a file
check-image user my-registry.example.com/private-image:latest \
--username myuser \
--password-stdin < ~/.secrets/registry-tokenUsing environment variables (recommended for CI/CD):
export CHECK_IMAGE_USERNAME=myuser
export CHECK_IMAGE_PASSWORD=mypassword
check-image healthcheck my-registry.example.com/private-image:latestSecurity note: Environment variables are readable from
/proc/<pid>/environby processes running as the same OS user. In CI/CD systems, always setCHECK_IMAGE_PASSWORDas a masked secret so the value is redacted from logs:# GitHub Actions env: CHECK_IMAGE_USERNAME: ${{ secrets.REGISTRY_USERNAME }} CHECK_IMAGE_PASSWORD: ${{ secrets.REGISTRY_TOKEN }}On shared machines, prefer
--password-stdinor pass the variable inline to limit its lifetime to a single command:CHECK_IMAGE_USERNAME="myuser" CHECK_IMAGE_PASSWORD="mytoken" \ check-image healthcheck my-registry.example.com/private-image:latest
Using ~/.docker/config.json (already works — no flags needed):
docker login my-registry.example.com
check-image secrets my-registry.example.com/private-image:latestImportant notes:
--passwordand--password-stdinare mutually exclusive- Specifying
--usernamewithout a password (or vice versa) is an error --password-stdinreads the entire stdin input and strips trailing\r\n— it cannot be combined with other flags that also consume stdin (--config -,--allowed-ports @-, etc.)- For GHCR (GitHub Container Registry) with a Personal Access Token, use
--username <github-username> --password-stdinand pipe the PAT
All commands support JSON output with --output json (or -o json). This is useful for scripting and CI/CD pipelines.
Individual command:
check-image age nginx:latest -o json{
"check": "age",
"image": "nginx:latest",
"passed": true,
"message": "Image is less than 90 days old",
"details": {
"created-at": "2025-12-01T00:00:00Z",
"age-days": 75,
"max-age": 90
}
}All command:
check-image all nginx:latest --skip registry,labels -o json{
"image": "nginx:latest",
"passed": false,
"checks": [
{
"check": "age",
"image": "nginx:latest",
"passed": true,
"message": "Image is less than 90 days old",
"details": {
"created-at": "2026-02-04T23:53:09Z",
"age-days": 15.975077092155601,
"max-age": 90
}
}
],
"summary": {
"total": 6,
"passed": 3,
"failed": 3,
"errored": 0,
"skipped": [
"registry",
"labels"
]
}
}Version command (full):
check-image version -o json{
"version": "v0.12.1",
"commit": "a1b2c3d",
"built-at": "2026-02-18T12:34:56Z",
"go-version": "go1.26.0",
"platform": "linux/amd64"
}Version command (short):
check-image version --short -o json{
"version": "v0.12.1"
}| Exit Code | Meaning | Example |
|---|---|---|
| 0 | Validation succeeded or no checks ran | Image passes all checks |
| 1 | Validation failed | Image is too old, runs as root, exposes unauthorized ports |
| 2 | Execution error | Invalid config file, image not found, invalid arguments |
In the all command, if some checks fail validation and others have execution errors, exit code 2 (execution error) takes precedence over exit code 1 (validation failure).
Usage in scripts:
check-image age nginx:latest --max-age 30
case $? in
0) echo "Image passed validation" ;;
1) echo "Image failed validation" ;;
2) echo "Tool encountered an error" ;;
esacThe config/ directory contains sample configuration files that can be used as templates:
config/allowed-ports.json- Sample allowed ports configuration in JSON formatconfig/allowed-ports.yaml- Sample allowed ports configuration in YAML format
Example usage:
check-image ports nginx:latest --allowed-ports @config/allowed-ports.jsonconfig/allowed-platforms.json- Sample allowed platforms configuration in JSON formatconfig/allowed-platforms.yaml- Sample allowed platforms configuration in YAML format
Example usage:
check-image platform nginx:latest --allowed-platforms @config/allowed-platforms.yamlconfig/registry-policy.json- Sample registry trust policy in JSON formatconfig/registry-policy.yaml- Sample registry trust policy in YAML format
Example usage:
check-image registry nginx:latest --registry-policy config/registry-policy.jsonconfig/secrets-policy.json- Sample secrets detection policy in JSON formatconfig/secrets-policy.yaml- Sample secrets detection policy in YAML format
Example usage:
check-image secrets nginx:latest --secrets-policy config/secrets-policy.jsonconfig/user-policy.json- Sample user validation policy in JSON formatconfig/user-policy.yaml- Sample user validation policy in YAML format
Example usage:
check-image user nginx:latest --user-policy config/user-policy.yamlconfig/config.json- Sample configuration for theallcommand in JSON formatconfig/config.yaml- Sample configuration for theallcommand in YAML format
These files define which checks to run and their parameters. Only checks present in the file are executed.
Example usage:
check-image all nginx:latest -c config/config.yamlAll policy and configuration files support reading from standard input using the - syntax. This enables dynamic configuration from pipelines and scripts.
Format Auto-detection:
- When reading from stdin, the format (JSON or YAML) is automatically detected based on content
- JSON content starts with
{or[ - Everything else is treated as YAML
- Maximum size limit: 10MB
Supported commands:
Registry policy from stdin:
echo '{"trusted-registries": ["docker.io", "ghcr.io"]}' | \
check-image registry nginx:latest --registry-policy -
# Or with YAML
echo 'trusted-registries:
- docker.io
- ghcr.io' | \
check-image registry nginx:latest --registry-policy -Secrets policy from stdin:
cat secrets-policy.yaml | \
check-image secrets nginx:latest --secrets-policy -User policy from stdin:
echo '{"min-uid": 1000, "max-uid": 65534}' | \
check-image user nginx:latest --user-policy -Allowed ports from stdin:
echo '{"allowed-ports": [80, 443]}' | \
check-image ports nginx:latest --allowed-ports @-All command config from stdin:
cat config/config.json | \
check-image all nginx:latest --config -Pipeline examples:
# Generate config dynamically and validate
jq -n '{"trusted-registries": ["docker.io"]}' | \
check-image registry nginx:latest --registry-policy -
# Use environment-based configuration
envsubst < config-template.yaml | \
check-image all nginx:latest --config -The all command configuration files support inline policy embedding, allowing you to define registry-policy, secrets-policy, labels-policy, and user-policy as objects directly in the config file instead of referencing separate files. This simplifies deployment by consolidating all configuration into a single file.
Example files:
config/config-inline.json- Complete configuration with inline policies (JSON)config/config-inline.yaml- Complete configuration with inline policies (YAML)
Inline vs File Reference:
File-based approach (separate policy files):
{
"checks": {
"registry": {
"registry-policy": "config/registry-policy.json"
},
"secrets": {
"secrets-policy": "config/secrets-policy.json"
}
}
}Inline approach (embedded policies):
{
"checks": {
"registry": {
"registry-policy": {
"trusted-registries": ["docker.io", "ghcr.io", "gcr.io"]
}
},
"secrets": {
"secrets-policy": {
"check-env-vars": true,
"check-files": true,
"excluded-env-vars": ["PUBLIC_KEY"],
"excluded-paths": ["/usr/share/**"]
}
}
}
}Benefits:
- Simpler deployment: Single file contains all configuration
- Version control: Easier to track policy changes alongside check parameters
- Portability: No need to manage multiple policy files
- Flexibility: Mix inline and file references in the same config
Usage:
# Use inline configuration
check-image all nginx:latest --config config/config-inline.yaml
# Inline config with CLI overrides
check-image all nginx:latest --config config/config-inline.json --max-age 30
# Mix with stdin
cat config/config-inline.yaml | check-image all nginx:latest --config -Note: Both file paths (strings) and inline objects are supported. You can mix both approaches in the same configuration file based on your needs.
To build the project from source:
git clone https://github.com/jarfernandez/check-image.git
cd check-image
go build -o check-image ./cmd/check-imageThe binary will be created in the project root directory. You can then move it to a location in your PATH or run it directly with ./check-image.
Alternatively, to install directly to your GOBIN directory:
go install ./cmd/check-imageThis project uses pre-commit hooks to enforce code quality and formatting standards before each commit. The hooks automatically run gofmt, go fix, go vet, golangci-lint, go mod tidy, execute tests with go-test-mod, and validate commit messages follow Conventional Commits format.
-
Install pre-commit framework:
# macOS brew install pre-commit # Or with pip pip install pre-commit
-
Install golangci-lint:
# macOS brew install golangci-lint # Or with go go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
-
(Optional) Install gosec for security scanning:
go install github.com/securego/gosec/v2/cmd/gosec@latest
-
Install the pre-commit hooks:
pre-commit install pre-commit install --hook-type commit-msg
The hooks run automatically on git commit. You can also:
-
Run manually on all files:
pre-commit run --all-files
-
Run only tests:
pre-commit run go-test-mod
-
Skip hooks in emergencies (not recommended):
git commit --no-verify
Mandatory checks (will block commits):
- File quality: trailing whitespace, end-of-file newlines, line endings
- Config validation: YAML and JSON syntax
- Go formatting:
gofmt - Go tidying:
go mod tidy - Go modernization:
go fix(applies API migration rewrites, e.g.,//go:fix inline) - Go analysis:
go vet - Go linting:
golangci-lint(see.golangci.ymlfor configuration) - Go tests:
go testviago-test-mod - Commit message: Conventional Commits format validation
Warning checks (informational only):
- Security:
gosecscans for security issues but doesn't block commits
cmd/check-image/main.go: The entry point of the application that initializes the CLI and executes commands.cmd/check-image/commands/: Contains individual command implementations using thecobralibrary.internal/fileutil/: Provides file reading utilities with support for JSON/YAML parsing and stdin input.internal/imageutil/: Provides utilities for interacting with container images, such as fetching images from local or remote sources and retrieving image configurations.internal/labels/: Handles label policy loading and validation for required OCI annotations.internal/logutil/: Provides log sanitization utilities that strip control characters from image-controlled strings before they reach log output.internal/output/: Defines output format types, result structs, and JSON rendering helpers.internal/registry/: Manages registry policies, including trusted and excluded registries.internal/secrets/: Handles secrets detection, including policy loading and scanning for sensitive data in environment variables and files.internal/user/: Handles user policy loading and validation for UID ranges, blocked usernames, and numeric UID requirements.internal/version/: Manages the application version string, injected at build time via ldflags.config/: Contains sample configuration files for registry policies, allowed ports, labels, secrets detection, and all-checks configuration.go.mod: Defines the module and its dependencies.go.sum: Contains the checksums for module dependencies.
github.com/spf13/cobra: For CLI command structure.github.com/google/go-containerregistry: For interacting with container registries.github.com/sirupsen/logrus: For logging.github.com/mattn/go-isatty: For terminal detection (controls log color output).github.com/stretchr/testify: For test assertions.gopkg.in/yaml.v3: For parsing YAML configuration files.
The project has comprehensive unit tests with 94.6% overall coverage. All tests are deterministic, fast, and run without requiring Docker daemon, registry access, or network connectivity.
# Run all tests
go test ./...
# Run with coverage
go test ./... -cover
# Run specific package tests
go test ./internal/imageutil -v
go test ./internal/secrets -v
# Generate coverage report
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out- internal/version: 100.0% coverage
- internal/output: 100.0% coverage
- internal/labels: 100.0% coverage
- internal/logutil: 100.0% coverage
- internal/registry: 100.0% coverage
- internal/user: 100.0% coverage
- internal/secrets: 97.4% coverage
- internal/fileutil: 90.0% coverage
- internal/imageutil: 88.7% coverage
- cmd/check-image/commands: 91.6% coverage
- cmd/check-image: 73.9% coverage
All tests are deterministic, fast, and run without requiring Docker daemon, registry access, or network connectivity. Tests use in-memory images, temporary directories, and OCI layout structures for validation.
This project uses GitHub Actions for continuous integration and automated releases.
Every pull request and push to main automatically runs:
- Tests: Full test suite on Linux, macOS, and Windows
- Linting:
golangci-lint,gofmt,go vet, andgo mod tidyverification - Build Verification: Cross-compilation for 5 platforms (Linux/macOS/Windows on amd64/arm64)
- PR Title Validation: Enforces Conventional Commits format (PRs only)
- CodeQL Analysis: Static security analysis for Go code (also runs on a weekly schedule)
All checks must pass before merging to main.
The GitHub Action is tested in a dedicated workflow (test-action.yml) that triggers on pull requests that modify action.yml or entrypoint.sh.
Releases are fully automated using release-please:
-
Development: Make changes and commit using Conventional Commits:
git commit -m "feat: add new validation check" git commit -m "fix: resolve race condition in image loading"
-
Merge to Main: After PR approval and merge, release-please automatically:
- Analyzes commits since the last release
- Calculates the next version based on commit types:
feat:→ minor version bump (0.1.0 → 0.2.0)fix:→ patch version bump (0.1.0 → 0.1.1)BREAKING CHANGE:→ major version bump (0.1.0 → 1.0.0)
- Creates/updates a "Release PR" with updated CHANGELOG.md, README.md version references, and action.yml default version
-
Release: When the Release PR is merged, three jobs run in sequence:
- release-please job: Creates the git tag and GitHub Release with the changelog
- goreleaser job: Builds binaries for all platforms and uploads them to the release; version is injected via ldflags
- docker job:
- Lints
Dockerfilewith hadolint - Builds a single-arch image (
linux/amd64) for Trivy security scanning (CRITICAL/HIGH vulnerabilities) - Validates the image with check-image itself (dogfooding: size, user, ports, secrets)
- Builds and pushes a multi-arch image (
linux/amd64,linux/arm64) to GHCR with semver tags (major.minor.patch,major.minor,major,latest)
- Lints
Releases include pre-built binaries for:
- Linux: amd64, arm64
- macOS: amd64, arm64
- Windows: amd64
This project requires Conventional Commits format:
<type>: <description>
[optional body]
[optional footer]
Allowed types:
feat: New featurefix: Bug fixdocs: Documentation changesrefactor: Code refactoringtest: Test updateschore: Maintenance tasksperf: Performance improvementsci: CI/CD changesbuild: Build system changesrevert: Revert previous commit
Examples:
git commit -m "feat: add support for OCI archives"
git commit -m "fix: handle missing environment variables"
git commit -m "docs: update installation instructions"Contributions are welcome! Please ensure that:
- All new commands and utilities are covered by tests.
- Code follows Go's idiomatic error handling practices.
- Variable names are meaningful and descriptive.
- Functions are small and focused on a single responsibility.
- Table-driven tests are used for functions with multiple input scenarios.
- Commit messages follow Conventional Commits format.
- Pre-commit hooks pass before committing.
This project is licensed under the MIT License. See the LICENSE file for more details.