Last active
September 3, 2026 21:36
-
-
Save rikonor/7c380900451477a8f0c13681c9c36b94 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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