Skip to content

Instantly share code, notes, and snippets.

@rikonor
Last active September 3, 2026 21:36
Show Gist options
  • Select an option

  • Save rikonor/7c380900451477a8f0c13681c9c36b94 to your computer and use it in GitHub Desktop.

Select an option

Save rikonor/7c380900451477a8f0c13681c9c36b94 to your computer and use it in GitHub Desktop.
# syntax=docker/dockerfile:1.7
FROM debian:bookworm-slim
# Install base prerequisites
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
git \
bash \
&& rm -rf /var/lib/apt/lists/*
# Install a pinned, checksum-verified mise release for either common image architecture.
ARG MISE_VERSION=2026.8.14
ARG TARGETARCH
RUN case "$TARGETARCH" in \
amd64) mise_arch=x64; mise_sha256=64d5f34aeb7a4e0e327dc1c9be66cd8162e14899a47b11901154a100285a3d61 ;; \
arm64) mise_arch=arm64; mise_sha256=940639580227bd838e3b3ea5b2084ea397399b0db162c2e4dd90b5730850e48e ;; \
*) echo "Unsupported architecture: $TARGETARCH" >&2; exit 1 ;; \
esac && \
archive="mise-v${MISE_VERSION}-linux-${mise_arch}.tar.gz" && \
curl -fsSLo "/tmp/$archive" \
"https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/$archive" && \
echo "$mise_sha256 /tmp/$archive" | sha256sum -c - && \
tar -xzf "/tmp/$archive" --strip-components=2 -C /usr/local/bin mise/bin/mise && \
rm "/tmp/$archive"
WORKDIR /cli
# 1. Declare toolchains and global mise settings
RUN cat <<'EOF' > /cli/mise.toml
[tools]
"aqua:nushell/nushell" = "0.115.1"
[settings]
experimental = true
EOF
# 2. Trust this image-owned config and pre-install runtimes for offline execution
RUN mise trust /cli/mise.toml && mise install
# 3. Create task directory hierarchy for namespace routing
RUN mkdir -p /cli/.mise/tasks/db \
/cli/.mise/tasks/api \
/cli/.mise/tasks/release
# -----------------------------------------------------------------------------
# TASK: status (Root health inspection in Nushell)
# -----------------------------------------------------------------------------
RUN cat <<'EOF' > /cli/.mise/tasks/status
#!/usr/bin/env nu
#MISE description="Inspect platform service health and runtime metrics"
#USAGE flag "-f --format <fmt>" help="Output format" default="table" {
#USAGE choices "table" "json" "yaml"
#USAGE }
#USAGE flag "-w --warn-only" help="Filter output to non-healthy services only"
let services = [
{ service: "auth-api", status: "healthy", latency_ms: 24, uptime: "99.98%" },
{ service: "postgres-primary", status: "degraded", latency_ms: 185, uptime: "98.40%" },
{ service: "redis-cache", status: "healthy", latency_ms: 3, uptime: "100.0%" },
{ service: "worker-pool", status: "healthy", latency_ms: 45, uptime: "99.90%" }
]
let warn_only = (($env.usage_warn_only? | default "false") == "true")
let filtered = if $warn_only {
$services | where status != "healthy"
} else {
$services
}
match $env.usage_format {
"json" => ($filtered | to json),
"yaml" => ($filtered | to yaml),
_ => ($filtered | table --expand)
}
EOF
# -----------------------------------------------------------------------------
# TASK: db:backup (Database snapshot manager in Nushell)
# -----------------------------------------------------------------------------
RUN cat <<'EOF' > /cli/.mise/tasks/db/backup
#!/usr/bin/env nu
#MISE description="Create a compressed database snapshot"
#USAGE arg "<database>" help="Target database name"
#USAGE flag "-c --compression <type>" help="Compression algorithm" default="zstd" {
#USAGE choices "gzip" "zstd" "none"
#USAGE }
#USAGE flag "-o --output-dir <dir>" help="Output directory path" default="/tmp/backups"
#USAGE flag "-d --dry-run" help="Simulate backup without writing data"
let db = $env.usage_database
let comp = $env.usage_compression
let out_dir = $env.usage_output_dir
let is_dry = (($env.usage_dry_run? | default "false") == "true")
let timestamp = (date now | format date "%Y%m%d_%H%M%S")
let ext = match $comp { "gzip" => ".sql.gz", "zstd" => ".sql.zst", _ => ".sql" }
let filename = $"($out_dir)/($db)_($timestamp)($ext)"
if $is_dry {
print $"[DRY-RUN] Target: ($db)"
print $"[DRY-RUN] Destination: ($filename)"
print $"[DRY-RUN] Algorithm: ($comp)"
} else {
mkdir $out_dir
print $"Initiating backup for ($db)..."
print $"Snapshot created successfully: ($filename)"
}
EOF
# -----------------------------------------------------------------------------
# TASK: api:query (JSON pipeline querying in Nushell)
# -----------------------------------------------------------------------------
RUN cat <<'EOF' > /cli/.mise/tasks/api/query
#!/usr/bin/env nu
#MISE description="Filter and transform live mock API payload"
#USAGE flag "-l --limit <num>" help="Max records to return" default="10"
#USAGE flag "-m --min-score <score>" help="Filter items with score >=" default="50"
#USAGE flag "-s --sort-desc" help="Sort descending by score"
let limit = ($env.usage_limit | into int)
let min_score = ($env.usage_min_score | into int)
let sort_desc = (($env.usage_sort_desc? | default "false") == "true")
let raw_payload = [
{ id: "usr_1", role: "admin", score: 85, region: "us-east" },
{ id: "usr_2", role: "member", score: 40, region: "eu-central" },
{ id: "usr_3", role: "guest", score: 15, region: "us-west" },
{ id: "usr_4", role: "member", score: 92, region: "us-east" },
{ id: "usr_5", role: "admin", score: 67, region: "ap-southeast" }
]
let processed = (
$raw_payload
| where score >= $min_score
| sort-by score --reverse=$sort_desc
| first $limit
)
$processed | table --expand
EOF
# -----------------------------------------------------------------------------
# TASK: release:deploy (Polyglot example: Bash task with rigid validation)
# -----------------------------------------------------------------------------
RUN cat <<'EOF' > /cli/.mise/tasks/release/deploy
#!/usr/bin/env bash
#MISE description="Deploy service artifact to a targeted cluster"
#USAGE arg "<service>" help="Service identifier"
#USAGE flag "-e --env <env>" help="Target infrastructure environment" default="dev" {
#USAGE choices "dev" "staging" "prod"
#USAGE }
#USAGE flag "-t --tag <version>" help="Image tag or semantic version" default="latest"
#USAGE flag "-f --force" help="Bypass deployment locks"
set -euo pipefail
echo "=========================================="
echo "DEPLOYMENT TARGET: ${usage_env^^}"
echo "SERVICE: ${usage_service}"
echo "TAG: ${usage_tag}"
echo "FORCE OVERRIDE: ${usage_force:-false}"
echo "=========================================="
if [ "$usage_env" = "prod" ] && [ "${usage_force:-false}" != "true" ]; then
expected="deploy $usage_service to prod"
printf 'Type exactly "%s" to continue: ' "$expected"
if ! IFS= read -r confirmation || [ "$confirmation" != "$expected" ]; then
echo "Production deployment cancelled. Use --force to bypass this prompt." >&2
exit 1
fi
fi
echo "Deploying artifact..."
echo "Deployment successful."
EOF
# 4. Set execution permissions for all embedded tasks
RUN chmod -R +x /cli/.mise/tasks
# 5. Present the image as a CLI while keeping the no-argument task listing useful.
RUN cat <<'EOF' > /usr/local/bin/one-punch-man
#!/usr/bin/env bash
set -euo pipefail
if [ "$#" -eq 0 ] || { [ "$#" -eq 1 ] && [ "$1" = "--tasks" ]; }; then
exec mise tasks ls
fi
exec mise run --raw "$@"
EOF
RUN chmod +x /usr/local/bin/one-punch-man
ENTRYPOINT ["/usr/local/bin/one-punch-man"]
# Build image directly from standard input
docker build -t cloudctl - < Dockerfile
# 1. View all auto-discovered subcommands
docker run --rm cloudctl
# 2. Inspect generated documentation for a nested subcommand
docker run --rm cloudctl db:backup --help
# Output:
# Create a compressed database snapshot
#
# Usage: db:backup [FLAGS] <DATABASE>
#
# Arguments:
# <DATABASE> Target database name
#
# Flags:
# -c, --compression <type> Compression algorithm [default: zstd] [choices: gzip, zstd, none]
# -o, --output-dir <dir> Output directory path [default: /tmp/backups]
# -d, --dry-run Simulate backup without writing data
# 3. Execute structured Nushell task with filtering and alternative format
docker run --rm cloudctl status --warn-only --format yaml
# Output:
# - service: postgres-primary
# status: degraded
# latency_ms: 185
# uptime: 98.40%
# 4. Run Nushell data pipeline task
docker run --rm cloudctl api:query --min-score 60 --sort-desc
# 5. Enforce choice validation on Bash task (fails automatically if invalid)
docker run --rm cloudctl release:deploy auth-service --env qa
# Output:
# Error: invalid value 'qa' for '--env <env>'. Must be one of: dev, staging, prod
# 6. Execute successful deployment
docker run --rm cloudctl release:deploy auth-service --env prod --force
# syntax=docker/dockerfile:1.7
# -----------------------------------------------------------------------------
# A genuinely self-contained CLI appliance.
#
# Build:
# docker build -t one-punch-man - < Dockerfile
#
# Everything — mise config, tasks, CLI wrapper — lives in this one file.
# -----------------------------------------------------------------------------
ARG DEBIAN_IMAGE="debian:bookworm-20260824-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171"
# =============================================================================
# BUILD STAGE
# =============================================================================
FROM ${DEBIAN_IMAGE} AS build
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gzip \
tar \
&& rm -rf /var/lib/apt/lists/*
# -----------------------------------------------------------------------------
# Install mise itself from an immutable, checksum-verified release.
# -----------------------------------------------------------------------------
ARG MISE_VERSION=2026.8.14
ARG TARGETARCH
RUN set -eu; \
case "$TARGETARCH" in \
amd64) \
mise_arch=x64; \
mise_sha256=64d5f34aeb7a4e0e327dc1c9be66cd8162e14899a47b11901154a100285a3d61 \
;; \
arm64) \
mise_arch=arm64; \
mise_sha256=940639580227bd838e3b3ea5b2084ea397399b0db162c2e4dd90b5730850e48e \
;; \
*) \
echo "Unsupported architecture: $TARGETARCH" >&2; \
exit 1 \
;; \
esac; \
archive="mise-v${MISE_VERSION}-linux-${mise_arch}.tar.gz"; \
curl \
--fail \
--silent \
--show-error \
--location \
--retry 5 \
--retry-all-errors \
--output "/tmp/${archive}" \
"https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/${archive}"; \
echo "${mise_sha256} /tmp/${archive}" | sha256sum -c -; \
tar \
-xzf "/tmp/${archive}" \
--strip-components=2 \
-C /usr/local/bin \
mise/bin/mise; \
rm "/tmp/${archive}"
WORKDIR /cli
ENV MISE_TRUSTED_CONFIG_PATHS=/cli
# -----------------------------------------------------------------------------
# Materialize the entire application from this Dockerfile.
# -----------------------------------------------------------------------------
RUN <<'BUILD_APP'
set -eu
mkdir -p \
/cli/.mise/tasks/http \
/cli/.mise/tasks/release
# =============================================================================
# mise configuration
# =============================================================================
cat > /cli/mise.toml <<'TOML'
min_version = "2026.8.14"
[tools]
"aqua:nushell/nushell" = "0.115.1"
TOML
# =============================================================================
# TASK: http:check
#
# A small, actually useful primitive:
#
# one-punch-man http:check https://api.example.com/healthz
# =============================================================================
cat > /cli/.mise/tasks/http/check <<'NU'
#!/usr/bin/env nu
#MISE description="Check an HTTP endpoint and fail on an unexpected status"
#USAGE arg "<url>" help="URL to request"
#USAGE flag "-s --status <code>" help="Expected HTTP status" default="200"
#USAGE flag "-t --timeout <duration>" help="Request timeout" default="5sec"
#USAGE flag "-f --format <fmt>" help="Output format" default="table" {
#USAGE choices "table" "json"
#USAGE }
let url = $env.usage_url
let expected = ($env.usage_status | into int)
let timeout = ($env.usage_timeout | into duration)
let output_format = $env.usage_format
let token = ($env.ONE_PUNCH_TOKEN? | default "")
let headers = if ($token | is-empty) {
{
Accept: "application/json"
}
} else {
{
Accept: "application/json"
Authorization: $"Bearer ($token)"
}
}
let attempt = try {
{
transport_ok: true
response: (
http get
--full
--allow-errors
--max-time $timeout
--headers $headers
$url
)
}
} catch {|err|
{
transport_ok: false
error: ($err | get -o msg | default "HTTP request failed")
}
}
if $attempt.transport_ok == false {
let result = {
url: $url
ok: false
expected: $expected
status: null
detail: $attempt.error
}
match $output_format {
"json" => (print ($result | to json -r))
_ => (print ([$result] | table --expand))
}
exit 1
}
let actual = $attempt.response.status
let healthy = ($actual == $expected)
let result = {
url: $url
ok: $healthy
expected: $expected
status: $actual
detail: (if $healthy {
"ok"
} else {
$"expected HTTP ($expected), received ($actual)"
})
}
match $output_format {
"json" => (print ($result | to json -r))
_ => (print ([$result] | table --expand))
}
if $healthy == false {
exit 1
}
NU
# =============================================================================
# TASK: release:gate
#
# This is the more interesting real-world task.
#
# Given a JSON manifest, make several HTTP checks concurrently and optionally
# assert values inside JSON response bodies.
#
# The command exits non-zero if ANY assertion fails.
#
# Manifest can be a file:
#
# one-punch-man release:gate /checks.json
#
# ...or stdin:
#
# cat checks.json | one-punch-man release:gate -
# =============================================================================
cat > /cli/.mise/tasks/release/gate <<'NU'
#!/usr/bin/env nu
#MISE description="Gate a release on HTTP status and JSON assertions"
#USAGE arg "<manifest>" help="JSON manifest path, or '-' to read stdin"
#USAGE flag "-t --timeout <duration>" help="Timeout per request" default="5sec"
#USAGE flag "-j --jobs <num>" help="Maximum concurrent checks" default="4"
#USAGE flag "-f --format <fmt>" help="Output format" default="table" {
#USAGE choices "table" "json"
#USAGE }
def render-value [value: any] {
try {
$value | to json -r
} catch {
$value | into string
}
}
def probe [
check: record
timeout: duration
headers: record
] {
let url = ($check | get url)
let name = (
$check
| get -o name
| default $url
)
let expected_status = (
$check
| get -o status
| default 200
| into int
)
let attempt = try {
{
transport_ok: true
response: (
http get
--full
--allow-errors
--max-time $timeout
--headers $headers
$url
)
}
} catch {|err|
{
transport_ok: false
error: ($err | get -o msg | default "HTTP request failed")
}
}
if $attempt.transport_ok == false {
return {
name: $name
ok: false
status: null
detail: $attempt.error
}
}
let response = $attempt.response
if $response.status != $expected_status {
return {
name: $name
ok: false
status: $response.status
detail: $"expected HTTP ($expected_status), received ($response.status)"
}
}
let json_path = (
$check
| get -o json_path
| default ""
)
# No JSON assertion requested: HTTP status was enough.
if ($json_path | is-empty) {
return {
name: $name
ok: true
status: $response.status
detail: "ok"
}
}
let has_expected_value = (
$check
| columns
| any {|column| $column == "equals" }
)
if $has_expected_value == false {
return {
name: $name
ok: false
status: $response.status
detail: $"'json_path' ($json_path) requires an 'equals' value"
}
}
# Turn e.g. "dependencies.postgres.status"
# into a real Nu cell-path.
let cell_path = (
$json_path
| split row "."
| into cell-path
)
let lookup = try {
{
found: true
value: ($response.body | get $cell_path)
}
} catch {
{
found: false
value: null
}
}
if $lookup.found == false {
return {
name: $name
ok: false
status: $response.status
detail: $"JSON path not found: ($json_path)"
}
}
let expected = ($check | get equals)
if $lookup.value != $expected {
let expected_text = (render-value $expected)
let actual_text = (render-value $lookup.value)
return {
name: $name
ok: false
status: $response.status
detail: $"($json_path): expected ($expected_text), received ($actual_text)"
}
}
{
name: $name
ok: true
status: $response.status
detail: $"($json_path) matched"
}
}
let manifest_path = $env.usage_manifest
let timeout = ($env.usage_timeout | into duration)
let jobs = ($env.usage_jobs | into int)
let output_format = $env.usage_format
let manifest = if $manifest_path == "-" {
open --raw /dev/stdin | from json
} else {
open $manifest_path
}
let checks = (
$manifest
| get -o checks
| default []
)
if ($checks | is-empty) {
error make {
msg: $"No checks found in manifest: ($manifest_path)"
}
}
let token = ($env.ONE_PUNCH_TOKEN? | default "")
let headers = if ($token | is-empty) {
{
Accept: "application/json"
}
} else {
{
Accept: "application/json"
Authorization: $"Bearer ($token)"
}
}
let results = (
$checks
| par-each
--threads $jobs
--keep-order
{|check| probe $check $timeout $headers}
)
match $output_format {
"json" => {
print ($results | to json -r)
}
_ => {
print ($results | table --expand)
}
}
let failures = (
$results
| where ok == false
| length
)
if $failures > 0 {
exit 1
}
NU
# =============================================================================
# CLI entrypoint
# =============================================================================
cat > /usr/local/bin/one-punch-man <<'SH'
#!/bin/sh
set -eu
# Force discovery to happen from our image-owned application root even if
# somebody overrides Docker's working directory.
cd /cli
case "${1:-}" in
"")
exec mise tasks ls
;;
--tasks)
exec mise tasks ls
;;
-V|--version)
printf 'one-punch-man %s\n' "${ONE_PUNCH_MAN_VERSION:-dev}"
;;
-h|--help)
cat <<'HELP'
one-punch-man - self-contained operational CLI
Usage:
one-punch-man
one-punch-man --tasks
one-punch-man --version
one-punch-man <task> [arguments...]
Examples:
one-punch-man http:check https://api.example.com/healthz
one-punch-man release:gate /checks.json
one-punch-man release:gate - < checks.json
Available tasks:
HELP
echo
exec mise tasks ls
;;
esac
exec mise run --raw "$@"
SH
chmod 0555 \
/cli/.mise/tasks/http/check \
/cli/.mise/tasks/release/gate \
/usr/local/bin/one-punch-man
BUILD_APP
# -----------------------------------------------------------------------------
# Install Nu into mise's system-wide shared location.
#
# This is deliberately NOT tied to /root.
# -----------------------------------------------------------------------------
RUN mise install --system
# -----------------------------------------------------------------------------
# Build-time validation.
#
# Don't merely hope the generated filesystem is valid:
# * verify the exact Nu version
# * parse/typecheck the Nu source without executing it
# * verify mise can discover the tasks and their #USAGE metadata
# -----------------------------------------------------------------------------
RUN set -eu; \
test "$(mise exec -- nu --version)" = "0.115.1"; \
mise exec -- nu -c \
'if not (nu-check --debug /cli/.mise/tasks/http/check) { exit 1 }; if not (nu-check --debug /cli/.mise/tasks/release/gate) { exit 1 }'; \
mise tasks ls >/dev/null; \
mise run --raw http:check --help >/dev/null; \
mise run --raw release:gate --help >/dev/null
# =============================================================================
# RUNTIME STAGE
# =============================================================================
FROM ${DEBIAN_IMAGE} AS runtime
ARG ONE_PUNCH_MAN_VERSION=0.1.0
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# mise itself
COPY --from=build /usr/local/bin/mise /usr/local/bin/mise
# our CLI wrapper
COPY --from=build /usr/local/bin/one-punch-man /usr/local/bin/one-punch-man
# system-wide mise-installed toolchains
COPY --from=build /usr/local/share/mise /usr/local/share/mise
# image-owned config + tasks
COPY --from=build /cli /cli
WORKDIR /cli
# -----------------------------------------------------------------------------
# Runtime policy:
#
# The image is immutable.
#
# mise may resolve and execute the tools baked into the image, but:
# * no auto-install
# * no command-not-found installation
# * no fallback to random system binaries
# * no HTTP traffic from mise itself
#
# MISE_OFFLINE affects mise, NOT `nu`'s `http get`.
# -----------------------------------------------------------------------------
ENV ONE_PUNCH_MAN_VERSION="${ONE_PUNCH_MAN_VERSION}" \
HOME="/tmp" \
MISE_DATA_DIR="/tmp/mise-data" \
MISE_CACHE_DIR="/tmp/mise-cache" \
MISE_STATE_DIR="/tmp/mise-state" \
MISE_TRUSTED_CONFIG_PATHS="/cli" \
MISE_AUTO_INSTALL="false" \
MISE_TASK_RUN_AUTO_INSTALL="false" \
MISE_NOT_FOUND_AUTO_INSTALL="false" \
MISE_NOT_FOUND_SYSTEM_FALLBACK="false" \
MISE_AUTO_UPDATE="false" \
MISE_OFFLINE="true"
# Nothing in the runtime requires root.
#
# Using a numeric identity also means we don't need to mutate /etc/passwd
# merely to create a cosmetic username.
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/one-punch-man"]
# syntax=docker/dockerfile:1.7
# Self-contained CLI appliance.
# Build: docker build -t one-punch-man - < Dockerfile
#
# Cache model: toolchain inputs and installation are isolated from frequently
# edited task/wrapper source, so app edits do not rerun `mise install --system`.
ARG DEBIAN_IMAGE="debian:bookworm-20260824-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171"
ARG MISE_VERSION=2026.8.14
ARG NU_VERSION=0.115.1
# CONFIG STAGE
# Toolchain config is isolated so app-script edits do not invalidate installs.
FROM ${DEBIAN_IMAGE} AS config
ARG MISE_VERSION
ARG NU_VERSION
WORKDIR /cli
RUN <<BUILD_CONFIG
set -eu
cat > /cli/mise.toml <<TOML
min_version = "${MISE_VERSION}"
[tools]
"aqua:nushell/nushell" = "${NU_VERSION}"
TOML
BUILD_CONFIG
# MISE STAGE
#
# Install mise itself from an immutable, checksum-verified release.
FROM ${DEBIAN_IMAGE} AS mise
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gzip \
tar \
&& rm -rf /var/lib/apt/lists/*
# Changing MISE_VERSION also requires updating the per-architecture checksums.
ARG MISE_VERSION
ARG TARGETARCH
RUN set -eu; \
case "$TARGETARCH" in \
amd64) \
mise_arch=x64; \
mise_sha256=64d5f34aeb7a4e0e327dc1c9be66cd8162e14899a47b11901154a100285a3d61 \
;; \
arm64) \
mise_arch=arm64; \
mise_sha256=940639580227bd838e3b3ea5b2084ea397399b0db162c2e4dd90b5730850e48e \
;; \
*) \
echo "Unsupported architecture: $TARGETARCH" >&2; \
exit 1 \
;; \
esac; \
archive="mise-v${MISE_VERSION}-linux-${mise_arch}.tar.gz"; \
curl \
--fail \
--silent \
--show-error \
--location \
--retry 5 \
--retry-all-errors \
--output "/tmp/${archive}" \
"https://github.com/jdx/mise/releases/download/v${MISE_VERSION}/${archive}"; \
echo "${mise_sha256} /tmp/${archive}" | sha256sum -c -; \
tar \
-xzf "/tmp/${archive}" \
--strip-components=2 \
-C /usr/local/bin \
mise/bin/mise; \
rm "/tmp/${archive}"
# TOOLCHAIN STAGE
# Expensive cache boundary: depends only on mise + mise.toml, never app scripts.
FROM mise AS toolchain
WORKDIR /cli
ENV MISE_TRUSTED_CONFIG_PATHS=/cli
COPY --from=config /cli/mise.toml /cli/mise.toml
# Install Nu into mise's system-wide shared location.
# This is deliberately NOT tied to /root.
RUN mise install --system
# APP STAGE
# Frequently edited task/wrapper source stays cheap to rebuild.
FROM ${DEBIAN_IMAGE} AS app
WORKDIR /cli
COPY --from=config /cli/mise.toml /cli/mise.toml
RUN <<'BUILD_APP'
set -eu
mkdir -p \
/cli/.mise/tasks/http \
/cli/.mise/tasks/release
# TASK: http:check
#
# A small, actually useful primitive:
#
# one-punch-man http:check https://api.example.com/healthz
cat > /cli/.mise/tasks/http/check <<'NU'
#!/usr/bin/env nu
#MISE description="Check an HTTP endpoint and fail on an unexpected status"
#USAGE arg "<url>" help="URL to request"
#USAGE flag "-s --status <code>" help="Expected HTTP status" default="200"
#USAGE flag "-t --timeout <duration>" help="Request timeout" default="5sec"
#USAGE flag "-f --format <fmt>" help="Output format" default="table" {
#USAGE choices "table" "json"
#USAGE }
let url = $env.usage_url
let expected = ($env.usage_status | into int)
let timeout = ($env.usage_timeout | into duration)
let output_format = $env.usage_format
let token = ($env.ONE_PUNCH_TOKEN? | default "")
let headers = if ($token | is-empty) {
{
Accept: "application/json"
}
} else {
{
Accept: "application/json"
Authorization: $"Bearer ($token)"
}
}
let attempt = try {
{
transport_ok: true
response: (
http get
--full
--allow-errors
--max-time $timeout
--headers $headers
$url
)
}
} catch {|err|
{
transport_ok: false
error: ($err | get -o msg | default "HTTP request failed")
}
}
if $attempt.transport_ok == false {
let result = {
url: $url
ok: false
expected: $expected
status: null
detail: $attempt.error
}
match $output_format {
"json" => (print ($result | to json -r))
_ => (print ([$result] | table --expand))
}
exit 1
}
let actual = $attempt.response.status
let healthy = ($actual == $expected)
let result = {
url: $url
ok: $healthy
expected: $expected
status: $actual
detail: (if $healthy {
"ok"
} else {
$"expected HTTP ($expected), received ($actual)"
})
}
match $output_format {
"json" => (print ($result | to json -r))
_ => (print ([$result] | table --expand))
}
if $healthy == false {
exit 1
}
NU
# TASK: release:gate
#
# This is the more interesting real-world task.
#
# Given a JSON manifest, make several HTTP checks concurrently and optionally
# assert values inside JSON response bodies.
#
# The command exits non-zero if ANY assertion fails.
#
# Manifest can be a file:
#
# one-punch-man release:gate /checks.json
#
# ...or stdin:
#
# cat checks.json | one-punch-man release:gate -
cat > /cli/.mise/tasks/release/gate <<'NU'
#!/usr/bin/env nu
#MISE description="Gate a release on HTTP status and JSON assertions"
#USAGE arg "<manifest>" help="JSON manifest path, or '-' to read stdin"
#USAGE flag "-t --timeout <duration>" help="Timeout per request" default="5sec"
#USAGE flag "-j --jobs <num>" help="Maximum concurrent checks" default="4"
#USAGE flag "-f --format <fmt>" help="Output format" default="table" {
#USAGE choices "table" "json"
#USAGE }
def render-value [value: any] {
try {
$value | to json -r
} catch {
$value | into string
}
}
def probe [
check: record
timeout: duration
headers: record
] {
let url = ($check | get url)
let name = (
$check
| get -o name
| default $url
)
let expected_status = (
$check
| get -o status
| default 200
| into int
)
let attempt = try {
{
transport_ok: true
response: (
http get
--full
--allow-errors
--max-time $timeout
--headers $headers
$url
)
}
} catch {|err|
{
transport_ok: false
error: ($err | get -o msg | default "HTTP request failed")
}
}
if $attempt.transport_ok == false {
return {
name: $name
ok: false
status: null
detail: $attempt.error
}
}
let response = $attempt.response
if $response.status != $expected_status {
return {
name: $name
ok: false
status: $response.status
detail: $"expected HTTP ($expected_status), received ($response.status)"
}
}
let json_path = (
$check
| get -o json_path
| default ""
)
# No JSON assertion requested: HTTP status was enough.
if ($json_path | is-empty) {
return {
name: $name
ok: true
status: $response.status
detail: "ok"
}
}
let has_expected_value = (
$check
| columns
| any {|column| $column == "equals" }
)
if $has_expected_value == false {
return {
name: $name
ok: false
status: $response.status
detail: $"'json_path' ($json_path) requires an 'equals' value"
}
}
# Turn e.g. "dependencies.postgres.status"
# into a real Nu cell-path.
let cell_path = (
$json_path
| split row "."
| into cell-path
)
let lookup = try {
{
found: true
value: ($response.body | get $cell_path)
}
} catch {
{
found: false
value: null
}
}
if $lookup.found == false {
return {
name: $name
ok: false
status: $response.status
detail: $"JSON path not found: ($json_path)"
}
}
let expected = ($check | get equals)
if $lookup.value != $expected {
let expected_text = (render-value $expected)
let actual_text = (render-value $lookup.value)
return {
name: $name
ok: false
status: $response.status
detail: $"($json_path): expected ($expected_text), received ($actual_text)"
}
}
{
name: $name
ok: true
status: $response.status
detail: $"($json_path) matched"
}
}
let manifest_path = $env.usage_manifest
let timeout = ($env.usage_timeout | into duration)
let jobs = ($env.usage_jobs | into int)
let output_format = $env.usage_format
if $jobs < 1 {
error make {
msg: "--jobs must be at least 1"
}
}
let manifest = if $manifest_path == "-" {
open --raw /dev/stdin | from json
} else {
open --raw $manifest_path | from json
}
let checks = (
$manifest
| get -o checks
| default []
)
if ($checks | is-empty) {
error make {
msg: $"No checks found in manifest: ($manifest_path)"
}
}
let token = ($env.ONE_PUNCH_TOKEN? | default "")
let headers = if ($token | is-empty) {
{
Accept: "application/json"
}
} else {
{
Accept: "application/json"
Authorization: $"Bearer ($token)"
}
}
let results = (
$checks
| par-each
--threads $jobs
--keep-order
{|check| probe $check $timeout $headers}
)
match $output_format {
"json" => {
print ($results | to json -r)
}
_ => {
print ($results | table --expand)
}
}
let failures = (
$results
| where ok == false
| length
)
if $failures > 0 {
exit 1
}
NU
# CLI entrypoint
cat > /usr/local/bin/one-punch-man <<'SH'
#!/bin/sh
set -eu
# Force discovery to happen from our image-owned application root even if
# somebody overrides Docker's working directory.
cd /cli
case "${1:-}" in
"")
exec mise tasks ls
;;
--tasks)
exec mise tasks ls
;;
-V|--version)
printf 'one-punch-man %s\n' "${ONE_PUNCH_MAN_VERSION:-dev}"
;;
-h|--help)
cat <<'HELP'
one-punch-man - self-contained operational CLI
Usage:
one-punch-man
one-punch-man --tasks
one-punch-man --version
one-punch-man <task> [arguments...]
Examples:
one-punch-man http:check https://api.example.com/healthz
one-punch-man release:gate /checks.json
one-punch-man release:gate - < checks.json
Available tasks:
HELP
echo
exec mise tasks ls
;;
esac
exec mise run --raw "$@"
SH
chmod 0555 \
/cli/.mise/tasks/http/check \
/cli/.mise/tasks/release/gate \
/usr/local/bin/one-punch-man
BUILD_APP
# RUNTIME BASE STAGE
# Independent branch so BuildKit can prepare the minimal runtime in parallel.
FROM ${DEBIAN_IMAGE} AS runtime-base
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# RUNTIME STAGE
# Copy stable toolchain artifacts before frequently edited app artifacts.
FROM runtime-base AS runtime
ARG ONE_PUNCH_MAN_VERSION=0.1.0
ARG NU_VERSION
# Stable build artifacts first.
COPY --from=toolchain /usr/local/bin/mise /usr/local/bin/mise
COPY --from=toolchain /usr/local/share/mise /usr/local/share/mise
# Frequently edited application artifacts last.
COPY --from=app /usr/local/bin/one-punch-man /usr/local/bin/one-punch-man
COPY --from=app /cli /cli
WORKDIR /cli
# Runtime policy. mise can resolve the baked toolchain, but it cannot install,
# update, fall back to arbitrary system binaries, or make its own HTTP requests.
# MISE_OFFLINE affects mise, not Nu's `http get`.
ENV ONE_PUNCH_MAN_VERSION="${ONE_PUNCH_MAN_VERSION}" \
HOME="/tmp" \
MISE_DATA_DIR="/tmp/mise-data" \
MISE_CACHE_DIR="/tmp/mise-cache" \
MISE_STATE_DIR="/tmp/mise-state" \
MISE_TRUSTED_CONFIG_PATHS="/cli" \
MISE_AUTO_INSTALL="false" \
MISE_TASK_RUN_AUTO_INSTALL="false" \
MISE_NOT_FOUND_AUTO_INSTALL="false" \
MISE_NOT_FOUND_SYSTEM_FALLBACK="false" \
MISE_AUTO_UPDATE="false" \
MISE_OFFLINE="true"
# The runtime needs no root privileges. A numeric identity avoids mutating
# /etc/passwd merely to create a cosmetic username.
USER 65532:65532
# Validate under the exact environment and identity used at runtime.
RUN set -eu; \
test ! -w /cli; \
test ! -w /usr/local/share/mise; \
test "$(mise exec -- nu --version)" = "$NU_VERSION"; \
mise exec -- nu -c \
'if not (nu-check --debug /cli/.mise/tasks/http/check) { exit 1 }; if not (nu-check --debug /cli/.mise/tasks/release/gate) { exit 1 }'; \
mise tasks ls >/dev/null; \
mise run --raw http:check --help >/dev/null; \
mise run --raw release:gate --help >/dev/null; \
/usr/local/bin/one-punch-man --version >/dev/null
ENTRYPOINT ["/usr/local/bin/one-punch-man"]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment