Initial commit: image registry structure and node-agent
Lint / lint (push) Failing after 34s
Build Images / ${{ fromJSON(needs.select.outputs.images) }} (push) Canceled after 7m37s
Build Images / Select images (push) Successful in 17s

Set up this repo as a base image registry. Each image is self-contained in
its own directory under images/ (Dockerfile + README.md + optional test.sh);
CI discovers them by glob, so adding an image needs no workflow changes.
template/ is the skeleton to copy.

The Makefile is the single entry point for both local work and CI, so a green
`make all` locally means a green pipeline.

Workflows:
  * lint.yaml  — layout check, hadolint, shellcheck
  * build.yaml — diffs against the base commit to build only the images that
    changed, smoke-tests each one before anything is published, then pushes.
    Releases are per-image tags (<image>/vX.Y.Z); main publishes :edge.

First image, node-agent: node:22-alpine plus a GNU userland (Alpine ships
BusyBox, whose applets take narrower flags than scripts and models expect),
helm, kubectl, jq, yq, bind-tools, curl, git, ripgrep, fd and friends.
Nothing is version-pinned — rebuilding is how upstream updates land, and the
published tag is what pins things for consumers.

Its smoke test asserts the deployment contract as well as tool presence: the
image must work non-root, with a read-only root filesystem and all
capabilities dropped, which is how ToolHive runs it.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
2026-08-09 11:40:29 -04:00
co-authored by Claude Opus 5
commit 1878df96ac
14 changed files with 762 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Work out how one image should be tagged, and whether it should be published.
# Prints GITHUB_OUTPUT lines:
#
# tags=<multiline> every ref to build, one per line
# primary=<ref> the ref the smoke test runs against
# version=<string> org.opencontainers.image.version
# created=<rfc3339> org.opencontainers.image.created
# push=true|false whether these refs get published
#
# Tag scheme:
# tag <image>/vX.Y.Z -> :vX.Y.Z :vX.Y :vX :latest published release
# push to main -> :edge :main-<sha> tip of main
# pull request -> :pr-<n> built, never published
# anything else -> :dev-<sha> built, never published
set -euo pipefail
image=${1:?usage: docker-tags.sh <image>}
repo="${REGISTRY:?REGISTRY is not set}/${NAMESPACE:?NAMESPACE is not set}/${image}"
sha=$(git rev-parse --short=7 HEAD)
created=$(date -u +%Y-%m-%dT%H:%M:%SZ)
push=false
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
version=${GITHUB_REF_NAME##*/}
[[ $version =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
echo "tag '${GITHUB_REF_NAME}' is not of the form <image>/vX.Y.Z" >&2
exit 1
}
n=${version#v}
tags=("$repo:$version" "$repo:v${n%.*}" "$repo:v${n%%.*}" "$repo:latest")
push=true
elif [ "${GITHUB_EVENT_NAME:-}" = pull_request ]; then
version="pr-${PR_NUMBER:-0}"
tags=("$repo:$version")
elif [ "${GITHUB_REF_NAME:-}" = main ]; then
version="edge-$sha"
tags=("$repo:edge" "$repo:main-$sha")
push=true
else
version="dev-$sha"
tags=("$repo:$version")
fi
printf 'version %s, push %s, tags: %s\n' "$version" "$push" "${tags[*]}" >&2
echo "tags<<__TAGS__"
printf '%s\n' "${tags[@]}"
echo "__TAGS__"
echo "primary=${tags[0]}"
echo "version=$version"
echo "created=$created"
echo "push=$push"
+96
View File
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Decide which images CI should build. Prints GITHUB_OUTPUT lines:
#
# images=["node-agent",...] JSON array consumed by the build job's matrix
# any=true|false whether there is anything to build at all
#
# Rules, first match wins:
# 1. workflow_dispatch naming one image -> that image
# 2. tag push (<image>/vX.Y.Z) -> the image named in the tag
# 3. shared build plumbing changed -> every image
# 4. anything else -> images with changed files
#
# When the diff base is unknown (first push, force-push, shallow clone) this builds
# everything. Rebuilding too much is the safe direction to fail.
set -euo pipefail
cd "$(dirname "$0")/.."
# A change to any of these can affect how every image is built.
SHARED_PATHS='^(hack/|Makefile|\.hadolint\.yaml|\.gitea/workflows/)'
all_images() {
local dockerfile
for dockerfile in images/*/Dockerfile; do
# Guards against the glob staying literal when there are no images.
[ -f "$dockerfile" ] || continue
basename "$(dirname "$dockerfile")"
done
}
# Names on stdin -> ["a","b"]. Built by hand so the runner needs no jq.
as_json() {
local out='' name
while IFS= read -r name; do
[ -n "$name" ] || continue
out="${out:+$out,}\"$name\""
done
printf '[%s]' "$out"
}
emit() {
local names=$1 reason=$2 any=false
[ -n "$names" ] && any=true
echo "selected (${reason}): ${names:-<none>}" >&2
printf 'images=%s\n' "$(printf '%s\n' "$names" | as_json)"
printf 'any=%s\n' "$any"
exit 0
}
require_image() {
[ -f "images/$1/Dockerfile" ] || {
echo "no such image: images/$1/Dockerfile does not exist" >&2
exit 1
}
}
# 1. Explicit request via workflow_dispatch.
case "${DISPATCH_IMAGE:-}" in
'') ;;
all) emit "$(all_images)" 'workflow_dispatch: all' ;;
*)
require_image "$DISPATCH_IMAGE"
emit "$DISPATCH_IMAGE" "workflow_dispatch: $DISPATCH_IMAGE"
;;
esac
# 2. Release tag: <image>/vX.Y.Z.
if [ "${GITHUB_REF_TYPE:-}" = tag ]; then
image=${GITHUB_REF_NAME%/*}
require_image "$image"
emit "$image" "tag ${GITHUB_REF_NAME}"
fi
# 3 and 4 both need a usable diff base.
base=${BASE_SHA:-}
if [ -z "$base" ] || [[ $base =~ ^0+$ ]] || ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
emit "$(all_images)" 'diff base unavailable, building everything'
fi
changed=$(git diff --name-only "$base" HEAD)
if printf '%s\n' "$changed" | grep -qE "$SHARED_PATHS"; then
emit "$(all_images)" 'shared build plumbing changed'
fi
# Map changed paths back to image names, dropping any that no longer exist so a
# deleted image directory does not fail the build.
selected=$(
printf '%s\n' "$changed" |
sed -n 's#^images/\([^/]*\)/.*#\1#p' |
sort -u |
while IFS= read -r i; do
[ -f "images/$i/Dockerfile" ] && echo "$i"
done
)
emit "$selected" 'changed paths'