From a25301263cc4c2a772ce2c7839866281dcd59cc4 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 07:40:22 -0800 Subject: [PATCH 1/9] Add Nix container build for nettest Create containers/nettest/default.nix using dockerTools.buildLayeredImage with the same tools as the Dockerfile (curl, jq, dnsutils, cacert, bash). Update container-list and container-tag-and-release to handle containers that have both a Dockerfile and default.nix, requiring --nix or --dockerfile flag when both exist. Co-Authored-By: Claude Opus 4.6 --- containers/nettest/default.nix | 38 ++++++++++++ .../feature-nettest-nix-container.feature.md | 1 + mise-tasks/container-list | 26 +++++--- mise-tasks/container-tag-and-release | 59 +++++++++++++++---- 4 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 containers/nettest/default.nix create mode 100644 docs/changelog.d/feature-nettest-nix-container.feature.md diff --git a/containers/nettest/default.nix b/containers/nettest/default.nix new file mode 100644 index 0000000..1739c41 --- /dev/null +++ b/containers/nettest/default.nix @@ -0,0 +1,38 @@ +# Nix-built nettest container +# Equivalent to the Dockerfile: curl, jq, bind (nslookup), ca-certs, bash +# Built with dockerTools.buildLayeredImage for efficient layer caching +{ pkgs ? import { system = "aarch64-linux"; } }: + +let + testScript = ./test-connectivity.sh; + + tools = pkgs.buildEnv { + name = "nettest-tools"; + paths = [ + pkgs.curl + pkgs.jq + pkgs.dnsutils # provides nslookup, dig + pkgs.cacert + pkgs.coreutils + pkgs.bashInteractive + ]; + }; +in +pkgs.dockerTools.buildLayeredImage { + name = "blumeops/nettest"; + tag = "latest"; + + contents = [ tools ]; + + extraCommands = '' + cp ${testScript} test-connectivity.sh + chmod +x test-connectivity.sh + ''; + + config = { + Entrypoint = [ "/bin/bash" "/test-connectivity.sh" ]; + Env = [ + "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" + ]; + }; +} diff --git a/docs/changelog.d/feature-nettest-nix-container.feature.md b/docs/changelog.d/feature-nettest-nix-container.feature.md new file mode 100644 index 0000000..6bf24fb --- /dev/null +++ b/docs/changelog.d/feature-nettest-nix-container.feature.md @@ -0,0 +1 @@ +Added Nix container build for nettest, validating the full nix-container-builder pipeline on ringtail. Updated container-list and container-tag-and-release to support containers with both Dockerfile and default.nix. diff --git a/mise-tasks/container-list b/mise-tasks/container-list index 0122e77..b0d449b 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -14,20 +14,26 @@ echo "" for dir in "$CONTAINER_DIR"/*/; do [[ -d "$dir" ]] || continue - # Determine build type - if [[ -f "$dir/default.nix" ]]; then - build_type="nix" - elif [[ -f "$dir/Dockerfile" ]]; then - build_type="dockerfile" - else - continue - fi + # Determine available build types + has_dockerfile=false + has_nix=false + [[ -f "$dir/Dockerfile" ]] && has_dockerfile=true + [[ -f "$dir/default.nix" ]] && has_nix=true + + # Skip directories with no build files + $has_dockerfile || $has_nix || continue + + # Build type label + types=() + $has_dockerfile && types+=("dockerfile") + $has_nix && types+=("nix") + label=$(IFS=+; echo "${types[*]}") # Extract container name from directory container=$(basename "$dir") image="blumeops/$container" - echo "[$build_type] $container" + echo "[$label] $container" echo " Image: $REGISTRY/$image" echo " Path: $dir" @@ -48,6 +54,8 @@ done echo "---" echo "To release a new version:" echo " mise run container-tag-and-release " +echo " mise run container-tag-and-release --nix # nix build" +echo " mise run container-tag-and-release --dockerfile # dockerfile build" echo "" echo "Example:" echo " mise run container-tag-and-release nettest v1.0.0" diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index 493f00f..bd916e8 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -5,9 +5,13 @@ set -euo pipefail CONTAINER="${1:-}" VERSION="${2:-}" +BUILD_TYPE_FLAG="${3:-}" if [[ -z "$CONTAINER" || -z "$VERSION" ]]; then - echo "Usage: mise run container-tag-and-release " + echo "Usage: mise run container-tag-and-release [--nix|--dockerfile]" + echo "" + echo "When a container has both a Dockerfile and default.nix, you must specify" + echo "the build type with --nix or --dockerfile." echo "" echo "Run 'mise run container-list' to see available containers and recent tags." exit 1 @@ -21,28 +25,57 @@ fi # Determine build type: Nix or Dockerfile CONTAINER_DIR="containers/${CONTAINER}" -if [[ -f "$CONTAINER_DIR/default.nix" ]]; then - BUILD_TYPE="nix" - TAG="${CONTAINER}-nix-${VERSION}" -elif [[ -f "$CONTAINER_DIR/Dockerfile" ]]; then - BUILD_TYPE="dockerfile" - TAG="${CONTAINER}-${VERSION}" -else +HAS_NIX=false +HAS_DOCKERFILE=false + +[[ -f "$CONTAINER_DIR/default.nix" ]] && HAS_NIX=true +[[ -f "$CONTAINER_DIR/Dockerfile" ]] && HAS_DOCKERFILE=true + +if ! $HAS_NIX && ! $HAS_DOCKERFILE; then echo "Error: No Dockerfile or default.nix found in '$CONTAINER_DIR'" echo "" echo "Available containers:" for dir in containers/*/; do [[ -d "$dir" ]] || continue name=$(basename "$dir") - if [[ -f "$dir/default.nix" ]]; then - echo " - $name (nix)" - elif [[ -f "$dir/Dockerfile" ]]; then - echo " - $name (dockerfile)" - fi + types=() + [[ -f "$dir/Dockerfile" ]] && types+=("dockerfile") + [[ -f "$dir/default.nix" ]] && types+=("nix") + [[ ${#types[@]} -gt 0 ]] && echo " - $name ($(IFS=, ; echo "${types[*]}"))" done exit 1 fi +if $HAS_NIX && $HAS_DOCKERFILE; then + # Both exist — require explicit flag + case "$BUILD_TYPE_FLAG" in + --nix) + BUILD_TYPE="nix" + ;; + --dockerfile) + BUILD_TYPE="dockerfile" + ;; + *) + echo "Error: '$CONTAINER' has both a Dockerfile and default.nix." + echo "" + echo "Specify the build type:" + echo " mise run container-tag-and-release $CONTAINER $VERSION --nix" + echo " mise run container-tag-and-release $CONTAINER $VERSION --dockerfile" + exit 1 + ;; + esac +elif $HAS_NIX; then + BUILD_TYPE="nix" +elif $HAS_DOCKERFILE; then + BUILD_TYPE="dockerfile" +fi + +if [[ "$BUILD_TYPE" == "nix" ]]; then + TAG="${CONTAINER}-nix-${VERSION}" +else + TAG="${CONTAINER}-${VERSION}" +fi + echo "Creating release tag: $TAG" echo "Build type: $BUILD_TYPE" echo "" -- 2.50.1 (Apple Git-155) From 29faa5d207d7924064cb70e2f8c7e4c55591ed52 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 07:54:49 -0800 Subject: [PATCH 2/9] Rewrite container-tag-and-release as typer CLI with dry-run support Port from bash to uv run --script with typer. Default behavior now builds both variants (dockerfile + nix) when both exist. Add --nix and --dockerfile flags to release only one variant, and --dry-run to preview without creating tags. Co-Authored-By: Claude Opus 4.6 --- mise-tasks/container-list | 6 +- mise-tasks/container-tag-and-release | 231 ++++++++++++++++----------- 2 files changed, 140 insertions(+), 97 deletions(-) diff --git a/mise-tasks/container-list b/mise-tasks/container-list index b0d449b..a2a585c 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -52,10 +52,10 @@ for dir in "$CONTAINER_DIR"/*/; do done echo "---" -echo "To release a new version:" +echo "To release a new version (builds all available types by default):" echo " mise run container-tag-and-release " -echo " mise run container-tag-and-release --nix # nix build" -echo " mise run container-tag-and-release --dockerfile # dockerfile build" +echo " mise run container-tag-and-release --nix # nix only" +echo " mise run container-tag-and-release --dockerfile # dockerfile only" echo "" echo "Example:" echo " mise run container-tag-and-release nettest v1.0.0" diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index bd916e8..e86484d 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -1,110 +1,153 @@ -#!/usr/bin/env bash +#!/usr/bin/env -S uv run --script +# /// script +# requires-python = ">=3.12" +# dependencies = ["typer>=0.15.0"] +# /// #MISE description="Release a container image by creating a git tag" +#USAGE arg "" help="Container name (directory under containers/)" +#USAGE arg "" help="Version in vX.Y.Z format" +#USAGE flag "--nix" help="Release only the nix variant" +#USAGE flag "--dockerfile" help="Release only the dockerfile variant" +#USAGE flag "--dry-run" help="Show what would be done without creating tags" +"""Release a container image by creating git tag(s) that trigger CI builds. -set -euo pipefail +When a container has both a Dockerfile and default.nix, both tags are created +by default. Use --nix or --dockerfile to release only one variant. -CONTAINER="${1:-}" -VERSION="${2:-}" -BUILD_TYPE_FLAG="${3:-}" +Tag conventions: + -v -> triggers build-container.yaml (Dockerfile) + -nix-v -> triggers build-container-nix.yaml (Nix) +""" -if [[ -z "$CONTAINER" || -z "$VERSION" ]]; then - echo "Usage: mise run container-tag-and-release [--nix|--dockerfile]" - echo "" - echo "When a container has both a Dockerfile and default.nix, you must specify" - echo "the build type with --nix or --dockerfile." - echo "" - echo "Run 'mise run container-list' to see available containers and recent tags." - exit 1 -fi +import re +import subprocess +import sys +from pathlib import Path -# Validate version format -if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Error: Version must be in format vX.Y.Z (e.g. v1.0.0)" - exit 1 -fi +import typer -# Determine build type: Nix or Dockerfile -CONTAINER_DIR="containers/${CONTAINER}" -HAS_NIX=false -HAS_DOCKERFILE=false +REGISTRY = "registry.ops.eblu.me" +FORGE_ACTIONS = "https://forge.ops.eblu.me/eblume/blumeops/actions" -[[ -f "$CONTAINER_DIR/default.nix" ]] && HAS_NIX=true -[[ -f "$CONTAINER_DIR/Dockerfile" ]] && HAS_DOCKERFILE=true +app = typer.Typer(add_completion=False) -if ! $HAS_NIX && ! $HAS_DOCKERFILE; then - echo "Error: No Dockerfile or default.nix found in '$CONTAINER_DIR'" - echo "" - echo "Available containers:" - for dir in containers/*/; do - [[ -d "$dir" ]] || continue - name=$(basename "$dir") - types=() - [[ -f "$dir/Dockerfile" ]] && types+=("dockerfile") - [[ -f "$dir/default.nix" ]] && types+=("nix") - [[ ${#types[@]} -gt 0 ]] && echo " - $name ($(IFS=, ; echo "${types[*]}"))" - done - exit 1 -fi -if $HAS_NIX && $HAS_DOCKERFILE; then - # Both exist — require explicit flag - case "$BUILD_TYPE_FLAG" in - --nix) - BUILD_TYPE="nix" - ;; - --dockerfile) - BUILD_TYPE="dockerfile" - ;; - *) - echo "Error: '$CONTAINER' has both a Dockerfile and default.nix." - echo "" - echo "Specify the build type:" - echo " mise run container-tag-and-release $CONTAINER $VERSION --nix" - echo " mise run container-tag-and-release $CONTAINER $VERSION --dockerfile" - exit 1 - ;; - esac -elif $HAS_NIX; then - BUILD_TYPE="nix" -elif $HAS_DOCKERFILE; then - BUILD_TYPE="dockerfile" -fi +def git(*args: str) -> str: + result = subprocess.run( + ["git", *args], capture_output=True, text=True, check=True + ) + return result.stdout.strip() -if [[ "$BUILD_TYPE" == "nix" ]]; then - TAG="${CONTAINER}-nix-${VERSION}" -else - TAG="${CONTAINER}-${VERSION}" -fi -echo "Creating release tag: $TAG" -echo "Build type: $BUILD_TYPE" -echo "" +def git_tag_exists(tag: str) -> bool: + result = subprocess.run( + ["git", "rev-parse", tag], capture_output=True, text=True + ) + return result.returncode == 0 -# Check if tag already exists -if git rev-parse "$TAG" >/dev/null 2>&1; then - echo "Error: Tag '$TAG' already exists" - echo "Existing tags for $CONTAINER:" - git tag -l "${CONTAINER}-*v*" | sort -V | tail -5 - exit 1 -fi -# Image name follows convention: blumeops/ -IMAGE="blumeops/${CONTAINER}" +def list_containers() -> None: + typer.echo("Available containers:") + for d in sorted(Path("containers").iterdir()): + if not d.is_dir(): + continue + types = [] + if (d / "Dockerfile").exists(): + types.append("dockerfile") + if (d / "default.nix").exists(): + types.append("nix") + if types: + typer.echo(f" - {d.name} ({', '.join(types)})") -echo "Container: $CONTAINER" -echo "Directory: $CONTAINER_DIR" -echo "Image: registry.ops.eblu.me/$IMAGE:$VERSION" -echo "" -# Create and push tag -git tag "$TAG" -git push origin "$TAG" +def create_and_push_tag(tag: str, image: str, version: str, dry_run: bool) -> bool: + """Create a git tag and push it. Returns True on success.""" + if git_tag_exists(tag): + typer.echo(f" Skip: Tag '{tag}' already exists") + return False + if dry_run: + typer.echo(f" [dry-run] Would create and push: {tag} -> {REGISTRY}/{image}:{version}") + else: + git("tag", tag) + git("push", "origin", tag) + typer.echo(f" {tag} -> {REGISTRY}/{image}:{version}") + return True -echo "" -echo "Tag '$TAG' created and pushed" -echo "" -echo "The workflow will now build and push:" -echo " registry.ops.eblu.me/$IMAGE:$VERSION" -echo "" -echo "Monitor the build at:" -echo " https://forge.ops.eblu.me/eblume/blumeops/actions" + +@app.command() +def main( + container: str = typer.Argument(help="Container name (directory under containers/)"), + version: str = typer.Argument(help="Version in vX.Y.Z format"), + nix: bool = typer.Option(False, "--nix", help="Release only the nix variant"), + dockerfile: bool = typer.Option(False, "--dockerfile", help="Release only the dockerfile variant"), + dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without creating tags"), +) -> None: + """Release a container image by creating git tag(s) that trigger CI builds.""" + if nix and dockerfile: + typer.echo("Error: --nix and --dockerfile are mutually exclusive") + raise typer.Exit(1) + + if not re.match(r"^v\d+\.\d+\.\d+$", version): + typer.echo("Error: Version must be in format vX.Y.Z (e.g. v1.0.0)") + raise typer.Exit(1) + + container_dir = Path("containers") / container + has_dockerfile = (container_dir / "Dockerfile").exists() + has_nix = (container_dir / "default.nix").exists() + + if not has_dockerfile and not has_nix: + typer.echo(f"Error: No Dockerfile or default.nix found in '{container_dir}'") + typer.echo() + list_containers() + raise typer.Exit(1) + + if nix and not has_nix: + typer.echo(f"Error: --nix specified but no default.nix in '{container_dir}'") + raise typer.Exit(1) + if dockerfile and not has_dockerfile: + typer.echo(f"Error: --dockerfile specified but no Dockerfile in '{container_dir}'") + raise typer.Exit(1) + + # Decide which builds to release + builds: list[str] = [] + if nix: + builds = ["nix"] + elif dockerfile: + builds = ["dockerfile"] + else: + if has_dockerfile: + builds.append("dockerfile") + if has_nix: + builds.append("nix") + + image = f"blumeops/{container}" + + if dry_run: + typer.echo("[dry-run mode]") + typer.echo(f"Container: {container}") + typer.echo(f"Image: {REGISTRY}/{image}") + typer.echo(f"Version: {version}") + typer.echo(f"Builds: {', '.join(builds)}") + typer.echo() + + # Create and push tags + tags_created = 0 + for build in builds: + if build == "nix": + tag = f"{container}-nix-{version}" + else: + tag = f"{container}-{version}" + if create_and_push_tag(tag, image, version, dry_run): + tags_created += 1 + + if tags_created == 0: + typer.echo() + typer.echo("No tags created (all already existed)") + raise typer.Exit(1) + + typer.echo() + typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") + + +if __name__ == "__main__": + app() -- 2.50.1 (Apple Git-155) From 2623c1c6fe0ff628271843cc632d007fa2a8b7ec Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 07:59:05 -0800 Subject: [PATCH 3/9] Use separate registry tags for nix vs dockerfile builds Nix builds push to :v-nix so both variants coexist in the registry instead of overwriting each other. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/build-container-nix.yaml | 6 +++--- mise-tasks/container-tag-and-release | 14 ++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml index f66691f..b3d1d57 100644 --- a/.forgejo/workflows/build-container-nix.yaml +++ b/.forgejo/workflows/build-container-nix.yaml @@ -4,8 +4,8 @@ # Pushes to Zot registry via skopeo # # Examples: -# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix -# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix +# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix +# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix, pushes :v2.1.0-nix name: Build Container (Nix) on: @@ -80,7 +80,7 @@ jobs: run: | CONTAINER="${{ steps.parse.outputs.container }}" VERSION="${{ steps.parse.outputs.version }}" - IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION" + IMAGE="registry.ops.eblu.me/blumeops/$CONTAINER:$VERSION-nix" echo "Pushing to $IMAGE" skopeo copy \ diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index e86484d..e47be5c 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -15,8 +15,8 @@ When a container has both a Dockerfile and default.nix, both tags are created by default. Use --nix or --dockerfile to release only one variant. Tag conventions: - -v -> triggers build-container.yaml (Dockerfile) - -nix-v -> triggers build-container-nix.yaml (Nix) + -v -> build-container.yaml -> :v + -nix-v -> build-container-nix.yaml -> :v-nix """ import re @@ -60,17 +60,17 @@ def list_containers() -> None: typer.echo(f" - {d.name} ({', '.join(types)})") -def create_and_push_tag(tag: str, image: str, version: str, dry_run: bool) -> bool: +def create_and_push_tag(tag: str, image: str, image_tag: str, dry_run: bool) -> bool: """Create a git tag and push it. Returns True on success.""" if git_tag_exists(tag): typer.echo(f" Skip: Tag '{tag}' already exists") return False if dry_run: - typer.echo(f" [dry-run] Would create and push: {tag} -> {REGISTRY}/{image}:{version}") + typer.echo(f" [dry-run] Would create and push: {tag} -> {REGISTRY}/{image}:{image_tag}") else: git("tag", tag) git("push", "origin", tag) - typer.echo(f" {tag} -> {REGISTRY}/{image}:{version}") + typer.echo(f" {tag} -> {REGISTRY}/{image}:{image_tag}") return True @@ -135,9 +135,11 @@ def main( for build in builds: if build == "nix": tag = f"{container}-nix-{version}" + image_tag = f"{version}-nix" else: tag = f"{container}-{version}" - if create_and_push_tag(tag, image, version, dry_run): + image_tag = version + if create_and_push_tag(tag, image, image_tag, dry_run): tags_created += 1 if tags_created == 0: -- 2.50.1 (Apple Git-155) From 514a797067f2fb86575f84f71a9147efee8c3c88 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:04:42 -0800 Subject: [PATCH 4/9] Fix nix container build: resolve nixpkgs from flake registry The runner service doesn't have NIX_PATH set, so fails. Add a step to resolve nixpkgs from the flake registry and set NIX_PATH. Also switch to nix-build (legacy CLI) and stop hardcoding aarch64-linux in default.nix. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/build-container-nix.yaml | 14 +++++++++++++- containers/nettest/default.nix | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml index b3d1d57..82bafff 100644 --- a/.forgejo/workflows/build-container-nix.yaml +++ b/.forgejo/workflows/build-container-nix.yaml @@ -66,13 +66,25 @@ jobs: echo "" echo "Skipping build." + - name: Resolve nixpkgs + if: steps.check.outputs.exists == 'true' + id: nixpkgs + run: | + # Resolve nixpkgs from the flake registry for lookup + NIXPKGS_PATH=$(nix flake metadata nixpkgs --json | jq -r '.path') + echo "Resolved nixpkgs: $NIXPKGS_PATH" + echo "path=$NIXPKGS_PATH" >> "$GITHUB_OUTPUT" + - name: Build with nix if: steps.check.outputs.exists == 'true' id: build + env: + NIX_PATH: "nixpkgs=${{ steps.nixpkgs.outputs.path }}" run: | CONTAINER="${{ steps.parse.outputs.container }}" echo "Building containers/$CONTAINER/default.nix" - nix build -f "containers/$CONTAINER/default.nix" -o result + echo "NIX_PATH=$NIX_PATH" + nix-build "containers/$CONTAINER/default.nix" -o result echo "Build complete: $(readlink result)" - name: Push to registry diff --git a/containers/nettest/default.nix b/containers/nettest/default.nix index 1739c41..d92520b 100644 --- a/containers/nettest/default.nix +++ b/containers/nettest/default.nix @@ -1,7 +1,7 @@ # Nix-built nettest container # Equivalent to the Dockerfile: curl, jq, bind (nslookup), ca-certs, bash # Built with dockerTools.buildLayeredImage for efficient layer caching -{ pkgs ? import { system = "aarch64-linux"; } }: +{ pkgs ? import { } }: let testScript = ./test-connectivity.sh; -- 2.50.1 (Apple Git-155) From dfe0f22108db19d04d0a3ef54fa91a104430af1b Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:09:14 -0800 Subject: [PATCH 5/9] Add jq to nix-container-builder runner hostPackages The nix container build workflow uses jq to parse flake metadata when resolving nixpkgs. Co-Authored-By: Claude Opus 4.6 --- nixos/ringtail/configuration.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index fd93c39..4dfb92f 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -456,7 +456,7 @@ in tokenFile = "/etc/forgejo-runner/token.env"; labels = [ "nix-container-builder:host" ]; hostPackages = with pkgs; [ - bash coreutils curl gawk gitMinimal gnused nodejs wget + bash coreutils curl gawk gitMinimal gnused jq nodejs wget nix skopeo ]; settings = { -- 2.50.1 (Apple Git-155) From a008ee4702a79529596184893c72905c917b6222 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:22:00 -0800 Subject: [PATCH 6/9] Add container policy.json and registries.conf for ringtail Minimal container config: policy.json for skopeo image pushes, and registries.conf with unqualified search matching indri's minikube setup (registry.ops.eblu.me, docker.io, ghcr.io, quay.io). Co-Authored-By: Claude Opus 4.6 --- nixos/ringtail/configuration.nix | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nixos/ringtail/configuration.nix b/nixos/ringtail/configuration.nix index 4dfb92f..6cb0581 100644 --- a/nixos/ringtail/configuration.nix +++ b/nixos/ringtail/configuration.nix @@ -446,6 +446,15 @@ in "d /mnt/storage2 0755 eblume users -" ]; + # Container config for skopeo (used by the forgejo runner to push images) + # and for unqualified image pulls via Zot pull-through cache + environment.etc."containers/policy.json".text = builtins.toJSON { + default = [{ type = "insecureAcceptAnything"; }]; + }; + environment.etc."containers/registries.conf".text = '' + unqualified-search-registries = ["registry.ops.eblu.me", "docker.io", "ghcr.io", "quay.io"] + ''; + # Forgejo Actions runner (nix container builder) services.gitea-actions-runner = { package = pkgs.forgejo-runner; -- 2.50.1 (Apple Git-155) From 58e13fab7dc886c085b7892344d9c1fff7775e4a Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:27:19 -0800 Subject: [PATCH 7/9] Add hostname package to nettest nix container Alpine provides hostname via busybox; nix needs it explicitly. Co-Authored-By: Claude Opus 4.6 --- containers/nettest/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/containers/nettest/default.nix b/containers/nettest/default.nix index d92520b..4520804 100644 --- a/containers/nettest/default.nix +++ b/containers/nettest/default.nix @@ -14,6 +14,7 @@ let pkgs.dnsutils # provides nslookup, dig pkgs.cacert pkgs.coreutils + pkgs.hostname pkgs.bashInteractive ]; }; -- 2.50.1 (Apple Git-155) From e7f6a71e9bf55a6fc78bf7224b57d79f35b384ae Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:33:35 -0800 Subject: [PATCH 8/9] Simplify container tagging: one tag triggers all workflows Both the Dockerfile and Nix workflows now trigger on the same tag pattern (*-v[0-9]*). Each workflow checks for its build file and skips if not present. This eliminates the need for separate -nix- tags and --nix/--dockerfile flags in the release script. Co-Authored-By: Claude Opus 4.6 --- .forgejo/workflows/build-container-nix.yaml | 38 +++------ .forgejo/workflows/build-container.yaml | 1 - mise-tasks/container-list | 6 +- mise-tasks/container-tag-and-release | 93 ++++++--------------- 4 files changed, 40 insertions(+), 98 deletions(-) diff --git a/.forgejo/workflows/build-container-nix.yaml b/.forgejo/workflows/build-container-nix.yaml index 82bafff..3c041a6 100644 --- a/.forgejo/workflows/build-container-nix.yaml +++ b/.forgejo/workflows/build-container-nix.yaml @@ -1,17 +1,17 @@ # Nix container build workflow -# Triggers on tags matching: -nix-v -# Builds from containers//default.nix using nix build -# Pushes to Zot registry via skopeo +# Triggers on tags matching: -v +# Builds from containers//default.nix if it exists, skips otherwise +# Pushes to Zot registry via skopeo with -nix image tag suffix # # Examples: -# nettest-nix-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix -# myapp-nix-v2.1.0 -> builds containers/myapp/default.nix, pushes :v2.1.0-nix +# nettest-v1.0.0 -> builds containers/nettest/default.nix, pushes :v1.0.0-nix +# devpi-v2.1.0 -> skips (no default.nix) name: Build Container (Nix) on: push: tags: - - '*-nix-v[0-9]*' + - '*-v[0-9]*' jobs: build: @@ -23,10 +23,10 @@ jobs: TAG="${GITHUB_REF_NAME}" echo "Tag: $TAG" - # Extract container name (everything before -nix-v) - # e.g., "nettest-nix-v1.0.0" -> "nettest" - CONTAINER="${TAG%-nix-v[0-9]*}" - VERSION="${TAG#"${CONTAINER}"-nix-}" + # Extract container name (everything before -v) + # e.g., "nettest-v1.0.0" -> "nettest", "my-app-v2.0.0" -> "my-app" + CONTAINER="${TAG%-v[0-9]*}" + VERSION="${TAG#"${CONTAINER}"-}" echo "container=$CONTAINER" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" @@ -46,26 +46,10 @@ jobs: echo "Found $CONTEXT/default.nix" echo "exists=true" >> "$GITHUB_OUTPUT" else - echo "No default.nix found at $CONTEXT/default.nix" + echo "No default.nix found at $CONTEXT/default.nix — skipping" echo "exists=false" >> "$GITHUB_OUTPUT" fi - - name: Skip if container not found - if: steps.check.outputs.exists != 'true' - run: | - echo "========================================" - echo "Nix container not found: ${{ steps.parse.outputs.container }}" - echo "========================================" - echo "" - echo "Tag '${{ github.ref_name }}' does not match any nix container in containers/" - echo "" - echo "Available nix containers:" - for nix in containers/*/default.nix; do - [ -f "$nix" ] && echo " - $(basename "$(dirname "$nix")")" - done - echo "" - echo "Skipping build." - - name: Resolve nixpkgs if: steps.check.outputs.exists == 'true' id: nixpkgs diff --git a/.forgejo/workflows/build-container.yaml b/.forgejo/workflows/build-container.yaml index b76978f..98231cf 100644 --- a/.forgejo/workflows/build-container.yaml +++ b/.forgejo/workflows/build-container.yaml @@ -17,7 +17,6 @@ on: jobs: build: - if: "!contains(github.ref_name, '-nix-v')" runs-on: k8s steps: - name: Parse tag diff --git a/mise-tasks/container-list b/mise-tasks/container-list index a2a585c..b785599 100755 --- a/mise-tasks/container-list +++ b/mise-tasks/container-list @@ -52,10 +52,10 @@ for dir in "$CONTAINER_DIR"/*/; do done echo "---" -echo "To release a new version (builds all available types by default):" +echo "To release a new version:" echo " mise run container-tag-and-release " -echo " mise run container-tag-and-release --nix # nix only" -echo " mise run container-tag-and-release --dockerfile # dockerfile only" +echo "" +echo "One tag triggers all applicable workflows (dockerfile and/or nix)." echo "" echo "Example:" echo " mise run container-tag-and-release nettest v1.0.0" diff --git a/mise-tasks/container-tag-and-release b/mise-tasks/container-tag-and-release index e47be5c..d3795cc 100755 --- a/mise-tasks/container-tag-and-release +++ b/mise-tasks/container-tag-and-release @@ -6,17 +6,12 @@ #MISE description="Release a container image by creating a git tag" #USAGE arg "" help="Container name (directory under containers/)" #USAGE arg "" help="Version in vX.Y.Z format" -#USAGE flag "--nix" help="Release only the nix variant" -#USAGE flag "--dockerfile" help="Release only the dockerfile variant" #USAGE flag "--dry-run" help="Show what would be done without creating tags" -"""Release a container image by creating git tag(s) that trigger CI builds. +"""Release a container image by creating a git tag that triggers CI builds. -When a container has both a Dockerfile and default.nix, both tags are created -by default. Use --nix or --dockerfile to release only one variant. - -Tag conventions: - -v -> build-container.yaml -> :v - -nix-v -> build-container-nix.yaml -> :v-nix +One tag triggers all applicable workflows: + - Dockerfile present -> Build Container workflow -> :v + - default.nix present -> Build Container (Nix) workflow -> :v-nix """ import re @@ -60,33 +55,13 @@ def list_containers() -> None: typer.echo(f" - {d.name} ({', '.join(types)})") -def create_and_push_tag(tag: str, image: str, image_tag: str, dry_run: bool) -> bool: - """Create a git tag and push it. Returns True on success.""" - if git_tag_exists(tag): - typer.echo(f" Skip: Tag '{tag}' already exists") - return False - if dry_run: - typer.echo(f" [dry-run] Would create and push: {tag} -> {REGISTRY}/{image}:{image_tag}") - else: - git("tag", tag) - git("push", "origin", tag) - typer.echo(f" {tag} -> {REGISTRY}/{image}:{image_tag}") - return True - - @app.command() def main( container: str = typer.Argument(help="Container name (directory under containers/)"), version: str = typer.Argument(help="Version in vX.Y.Z format"), - nix: bool = typer.Option(False, "--nix", help="Release only the nix variant"), - dockerfile: bool = typer.Option(False, "--dockerfile", help="Release only the dockerfile variant"), dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without creating tags"), ) -> None: - """Release a container image by creating git tag(s) that trigger CI builds.""" - if nix and dockerfile: - typer.echo("Error: --nix and --dockerfile are mutually exclusive") - raise typer.Exit(1) - + """Release a container image by creating a git tag that triggers CI builds.""" if not re.match(r"^v\d+\.\d+\.\d+$", version): typer.echo("Error: Version must be in format vX.Y.Z (e.g. v1.0.0)") raise typer.Exit(1) @@ -101,52 +76,36 @@ def main( list_containers() raise typer.Exit(1) - if nix and not has_nix: - typer.echo(f"Error: --nix specified but no default.nix in '{container_dir}'") - raise typer.Exit(1) - if dockerfile and not has_dockerfile: - typer.echo(f"Error: --dockerfile specified but no Dockerfile in '{container_dir}'") - raise typer.Exit(1) - - # Decide which builds to release - builds: list[str] = [] - if nix: - builds = ["nix"] - elif dockerfile: - builds = ["dockerfile"] - else: - if has_dockerfile: - builds.append("dockerfile") - if has_nix: - builds.append("nix") - image = f"blumeops/{container}" + tag = f"{container}-{version}" + + # Show what workflows will trigger + builds = [] + if has_dockerfile: + builds.append(f" dockerfile -> {REGISTRY}/{image}:{version}") + if has_nix: + builds.append(f" nix -> {REGISTRY}/{image}:{version}-nix") if dry_run: typer.echo("[dry-run mode]") typer.echo(f"Container: {container}") - typer.echo(f"Image: {REGISTRY}/{image}") - typer.echo(f"Version: {version}") - typer.echo(f"Builds: {', '.join(builds)}") + typer.echo(f"Tag: {tag}") + typer.echo(f"Builds:") + for b in builds: + typer.echo(b) typer.echo() - # Create and push tags - tags_created = 0 - for build in builds: - if build == "nix": - tag = f"{container}-nix-{version}" - image_tag = f"{version}-nix" - else: - tag = f"{container}-{version}" - image_tag = version - if create_and_push_tag(tag, image, image_tag, dry_run): - tags_created += 1 - - if tags_created == 0: - typer.echo() - typer.echo("No tags created (all already existed)") + if git_tag_exists(tag): + typer.echo(f"Error: Tag '{tag}' already exists") raise typer.Exit(1) + if dry_run: + typer.echo(f"[dry-run] Would create and push tag: {tag}") + else: + git("tag", tag) + git("push", "origin", tag) + typer.echo(f"Tag '{tag}' created and pushed") + typer.echo() typer.echo(f"Monitor builds at: {FORGE_ACTIONS}") -- 2.50.1 (Apple Git-155) From 3fbbc2d412e98abfbb977d73d3c14ee85bb04863 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Thu, 19 Feb 2026 08:41:03 -0800 Subject: [PATCH 9/9] Update docs for unified container build pipeline - build-container-image how-to: document nix builds, --dry-run, unified tag triggering both workflows, add nettest nix pattern - ringtail reference: expand runner section with host packages, nixpkgs resolution, and container config files - forgejo reference: document both runners and both workflows - changelog fragment: reflect final scope Co-Authored-By: Claude Opus 4.6 --- .../feature-nettest-nix-container.feature.md | 2 +- .../deployment/build-container-image.md | 35 ++++++++++++++----- docs/reference/infrastructure/ringtail.md | 7 ++-- docs/reference/services/forgejo.md | 17 +++++---- 4 files changed, 43 insertions(+), 18 deletions(-) diff --git a/docs/changelog.d/feature-nettest-nix-container.feature.md b/docs/changelog.d/feature-nettest-nix-container.feature.md index 6bf24fb..b529de2 100644 --- a/docs/changelog.d/feature-nettest-nix-container.feature.md +++ b/docs/changelog.d/feature-nettest-nix-container.feature.md @@ -1 +1 @@ -Added Nix container build for nettest, validating the full nix-container-builder pipeline on ringtail. Updated container-list and container-tag-and-release to support containers with both Dockerfile and default.nix. +Added Nix container build for nettest, validating the full nix-container-builder pipeline on ringtail. One git tag now triggers both Dockerfile and Nix workflows — each skips if its build file is absent. Rewrote container-tag-and-release as a typer CLI with --dry-run support. Added container policy.json and registries.conf to ringtail for skopeo. diff --git a/docs/how-to/deployment/build-container-image.md b/docs/how-to/deployment/build-container-image.md index 3086b23..18233eb 100644 --- a/docs/how-to/deployment/build-container-image.md +++ b/docs/how-to/deployment/build-container-image.md @@ -1,6 +1,6 @@ --- title: Build Container Image -modified: 2026-02-15 +modified: 2026-02-19 last-reviewed: 2026-02-15 tags: - how-to @@ -14,30 +14,35 @@ How to create a custom container image in BlumeOps, build it locally, and releas ## Prerequisites -- [Dagger CLI](https://docs.dagger.io/install) installed locally -- A Dockerfile for the service you want to build +- [Dagger CLI](https://docs.dagger.io/install) installed locally (for Dockerfile builds) +- A `Dockerfile` and/or `default.nix` for the service ## 1. Create the container directory -Add a `Dockerfile` (and any supporting files) under `containers//`: +Add build files under `containers//`: ``` containers// -├── Dockerfile +├── Dockerfile (built by Dagger on the k8s runner) +├── default.nix (built by nix-build on the ringtail runner) └── (optional scripts, configs) ``` -The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. +A container can have one or both build files. The directory name becomes the image name: `registry.ops.eblu.me/blumeops/`. ## 2. Build locally -Test your image with Dagger: +**Dockerfile** — test with Dagger: ```bash dagger call build --src=. --container-name= ``` -This builds `containers//Dockerfile` using the Dagger `docker_build()` function. Fix any build errors before proceeding. +**Nix** — test with nix-build (requires nix, e.g. on [[ringtail]]): + +```bash +nix-build containers//default.nix -o result +``` ## 3. Release @@ -47,7 +52,14 @@ Once the image builds cleanly, create a tagged release: mise run container-tag-and-release v1.0.0 ``` -This creates a git tag `-v1.0.0` and pushes it. The `build-container` Forgejo workflow triggers on the tag, builds the image via Dagger, and publishes it to the registry as `registry.ops.eblu.me/blumeops/:v1.0.0`. +Use `--dry-run` to preview without creating tags. + +This creates a single git tag `-v1.0.0` and pushes it. Both Forgejo workflows trigger on the tag — each checks for its build file and skips if not present: + +| Build file | Workflow | Runner | Registry tag | +|------------|----------|--------|--------------| +| `Dockerfile` | `build-container.yaml` | `k8s` (indri) | `:v1.0.0` | +| `default.nix` | `build-container-nix.yaml` | `nix-container-builder` ([[ringtail]]) | `:v1.0.0-nix` | Check available images and tags with: @@ -76,6 +88,7 @@ Existing containers demonstrate several build approaches: | Multi-stage with Node + Go | [[#navidrome]] | Separate UI and backend build stages | | Multi-stage Elixir | [[#teslamate]] | Elixir release with Node assets | | Runtime tarball download | [[#kiwix-serve]] | Download pre-built binary with arch detection | +| Nix `dockerTools` | [[#nettest-nix]] | `buildLayeredImage` with nixpkgs tools | ### transmission @@ -97,6 +110,10 @@ Existing containers demonstrate several build approaches: `containers/kiwix-serve/Dockerfile` — Downloads a pre-built binary from upstream, with architecture detection for cross-platform support. +### nettest (nix) + +`containers/nettest/default.nix` — Uses `dockerTools.buildLayeredImage` with `buildEnv` to merge nixpkgs tools (curl, jq, dnsutils, bash). Runs alongside the existing Dockerfile; the nix variant is tagged `:version-nix` in the registry. + ## Related - [[deploy-k8s-service]] — Deploying the service that uses the image diff --git a/docs/reference/infrastructure/ringtail.md b/docs/reference/infrastructure/ringtail.md index 45bc757..f6e0cc3 100644 --- a/docs/reference/infrastructure/ringtail.md +++ b/docs/reference/infrastructure/ringtail.md @@ -1,6 +1,6 @@ --- title: Ringtail -modified: 2026-02-18 +modified: 2026-02-19 tags: - infrastructure - host @@ -81,7 +81,7 @@ argocd cluster add default --name k3s-ringtail ### Forgejo Actions Runner -A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix build` and pushes them to Zot via `skopeo`. +A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd service via the NixOS `services.gitea-actions-runner` module. It builds containers using `nix-build` and pushes them to Zot via `skopeo`. | Property | Value | |----------|-------| @@ -89,6 +89,9 @@ A native Forgejo Actions runner (`ringtail-nix-builder`) runs as a systemd servi | **Execution** | Host (no containers) | | **Token** | `/etc/forgejo-runner/token.env` (provisioned by Ansible) | | **Service unit** | `gitea-runner-nix_container_builder.service` | +| **Host packages** | bash, coreutils, curl, gawk, git, gnused, jq, nodejs, wget, nix, skopeo | + +The runner resolves `` from the flake registry at build time. Container trust policy (`/etc/containers/policy.json`) and registry search order (`/etc/containers/registries.conf`) are configured minimally in `configuration.nix` for skopeo — no full `virtualisation.containers` module needed. ## Maintenance Notes diff --git a/docs/reference/services/forgejo.md b/docs/reference/services/forgejo.md index db84339..e7c0b10 100644 --- a/docs/reference/services/forgejo.md +++ b/docs/reference/services/forgejo.md @@ -1,6 +1,6 @@ --- title: Forgejo -modified: 2026-02-08 +modified: 2026-02-19 tags: - service - git @@ -31,15 +31,20 @@ Git forge and CI/CD platform. **Primary source of truth for blumeops** (mirrored ## CI/CD (Forgejo Actions) -**Runner:** Kubernetes pod with Docker-in-Docker sidecar -- Namespace: `forgejo-runner` -- Labels: `k8s` -- ArgoCD app: `forgejo-runner` +**Runners:** + +| Runner | Host | Labels | Purpose | +|--------|------|--------|---------| +| k8s DinD pod | [[indri]] (minikube) | `k8s` | Dockerfile builds via Dagger | +| ringtail-nix-builder | [[ringtail]] (native) | `nix-container-builder` | Nix builds via `nix-build` + `skopeo` | **Workflows:** `.forgejo/workflows/` -- `build-container.yaml` - Container image builds on tag +- `build-container.yaml` - Dockerfile builds on tag (runs on `k8s`) +- `build-container-nix.yaml` - Nix builds on tag (runs on `nix-container-builder`) - `build-blumeops.yaml` - Documentation builds and releases +Both container workflows trigger on the same tag pattern (`*-v[0-9]*`). Each checks for its build file (`Dockerfile` or `default.nix`) and skips if not present. See [[build-container-image]]. + ## Secrets (Forgejo Config) Server configuration secrets managed via 1Password → Ansible: -- 2.50.1 (Apple Git-155)