7 Commits
Author SHA1 Message Date
rmcguire 6ceb78b00c rm kubeconform
Build Images / Select images (push) Successful in 11s
Lint / Lint (push) Successful in 37s
Build Images / Build changed images (push) Successful in 2m1s
2026-08-10 17:15:13 -04:00
rmcguire f79a175021 rm kubeconform 2026-08-10 17:12:47 -04:00
rmcguire 552d3dd289 add kubeconform 2026-08-10 17:06:34 -04:00
rmcguireandClaude Opus 5 e1fda786c1 Always rebuild images from scratch
Build Images / Build changed images (push) Successful in 5m45s
Build Images / Select images (push) Successful in 11s
Lint / Lint (push) Successful in 7s
Drop the layer-caching work: no CACHE_MODE, no registry buildcache tag, no buildx
container builder, no scheduled cache-busting build, no no_cache input.

Instead build with --no-cache --pull every time. Caching was working against the
point of this repo: nothing is version-pinned, so a cached `apk add` layer keeps
shipping whatever packages existed when it was first built, and a rebuild quietly
stops meaning "current". --no-cache alone isn't enough either — it would still
build on a stale local copy of the FROM image.

Verified the only CACHED entries left are the `# syntax=` frontend image and
--pull re-resolving the base manifest (unchanged digest). The apk and yq layers
re-run every build.

Costs ~40s per image, which also makes the four-node cache-sharing question moot.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
node-agent/v0.1.0
2026-08-09 12:29:14 -04:00
rmcguireandClaude Opus 5 b888f09eb1 Readable job names, latest tracks main, and an AGENTS.md
Job names in the Gitea UI rendered as the raw matrix expression
("${{ fromJSON(needs.select.outputs.images) }}"). Gitea resolves a job's matrix
when it PARSES the workflow, before `needs` outputs exist, so matrix.image was
interpolated against an unresolved matrix. There is no way to win with a dynamic
matrix: jobparser.nameWithMatrix interpolates a name containing "${{ }}", and
appends "(<values>)" to one that doesn't — either way the raw expression shows.
See go-gitea/gitea#28207.

So drop the matrix. build.yaml now runs one job, "Build changed images", that
loops over the selected images and emits ::group:: markers, giving a collapsible
section per image. Also: every step has an explicit static name, because Gitea
does not interpolate expressions in names either ("Log in to ${{ env.REGISTRY }}"
rendered literally).

Tag scheme, per review: `latest` now tracks main rather than the newest release,
and `edge` is gone — it's an Alpine/Traefik convention, not a broad standard, and
`main-<sha>` already covers "a specific commit". `latest` has exactly one owner
so a release tag and a main build can't race to define it. Release tags remain
immutable `:vX.Y.Z`/`:vX.Y`/`:vX` for pinning. This also means the ToolHive
manifest's `:latest` resolves as soon as this lands on main, with no release tag
needed first.

hack/docker-tags.sh folded into hack/build-images.sh, which is now the whole
pipeline — tag, build, smoke-test, push — shared by `make build` and CI. It uses
plain `docker build` instead of buildx, dropping setup-buildx-action: it loads
into the local store so the test runs pre-publish, and emits a plain manifest
with no attestations for Gitea's registry.

Verified in a simulated runner (repo in a docker volume, socket mounted, real
runner image): select + build + group markers, all four tag modes, and the
multi-image loop with a scratch second image.

AGENTS.md records the conventions and, importantly, the three Gitea gotchas that
all look fine locally: no bind-mounting the workspace into a sibling container,
no dynamic matrix, no expressions in names.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-09 12:05:23 -04:00
rmcguireandClaude Opus 5 7a81afec65 Lint by piping files to the linters instead of bind-mounting
Build Images / ${{ fromJSON(needs.select.outputs.images) }} (push) Successful in 3m33s
Build Images / Select images (push) Successful in 41s
Lint / lint (push) Successful in 44s
`make lint` passed locally but failed in Gitea Actions with every Dockerfile
reported as missing:

    hadolint: images/node-agent/Dockerfile: withBinaryFile: does not exist

The job itself runs in a container, and the checkout lives in a docker volume
rather than on the host filesystem. Starting hadolint as a sibling container
through the host's docker socket meant `-v "$(CURDIR)":/repo` was resolved
against the HOST, where /workspace/... does not exist — so Docker created an
empty directory and mounted that.

Stream each file in on stdin instead. No shared filesystem is involved, so the
same command works locally and in CI. The linters then only ever see "-" as the
filename, so hack/lint.sh prints the real path itself, and only on failure.

Reproduced the original error and verified the fix against a faithful local
simulation: the repo in a docker volume at a path absent from the host, with
the socket mounted, running the real runner image.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-09 11:48:50 -04:00
rmcguireandClaude Opus 5 1878df96ac 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]>
2026-08-09 11:40:29 -04:00