ci: convert OCI pipeline to parity-lib nix2container builder

The payload (pure-stdlib bridge.py + a stock CPython closure) is fully
Nix-expressible, so this is NOT an escape-hatch/buildkit repo: both arches
build on emmett (amd64 native + arm64 pkgsCross of stock python3 from the
binary cache) with no buildkit, qemu, docker daemon, or howard pin.

Replace the partial amd64-only scaffold with parity-lib's
mkNix2ContainerPublish, completing the arm64 leg + multi-arch index. The
per-arch nix2container image derivations are kept verbatim; stage/publish/
publish-index/publish/push-staged now come from the shared builder so CI and
local invoke identical code. Thin .woodpecker.yaml to a single
nix run .#publish; retire the buildx/remote-builder steps.

The Dockerfile is now unused (the cutover drops it) but kept in-tree so the
server-side hadolint pre-receive hook does not crash on a file deletion.

Refs cluster #192, emmett#44.
This commit is contained in:
Oleks
2026-06-02 09:26:27 +03:00
parent e8f3e954e7
commit 71ec9709a8
3 changed files with 187 additions and 128 deletions
+37 -109
View File
@@ -1,14 +1,16 @@
{
description = "alertmanager-gotify-bridge pure-stdlib Python forwarder, containerized with Nix (nix2container). DESIGN/PARTIAL: amd64 leg only see ci/MIGRATION.md.";
description = "alertmanager-gotify-bridge pure-stdlib Python forwarder, containerized with Nix (nix2container), multi-arch via the shared parity-lib OCI builder.";
# ──────────────────────────────────────────────────────────────────────────
# SCAFFOLD / PARTIAL MIGRATION (emmett#44, archetype oci-image buildx→nix2container).
# This flake intentionally implements ONLY the natively-buildable amd64 leg
# (`nix run .#publish-amd64`, dry-run by default). The arm64 cross leg, the
# multi-arch `publish-index`/`publish` apps, and the `.woodpecker.yaml` cutover
# are DESIGNED in ci/MIGRATION.md but NOT yet wired. Do not treat this as the
# finished pipeline. Pattern mirrors the reference impl
# (oleks/claude-plugin-registry @ 9850745).
# LOCAL-PIPELINE PARITY (cluster #192, emmett#44). Archetype: nix2container OCI
# image (NOT a buildkit/escape-hatch repo — the whole payload is Nix-
# expressible). The payload is pure-stdlib `bridge.py` next to a stock CPython
# closure, so BOTH arches build on emmett with no buildkit / qemu / docker:
# amd64 = native, arm64 = pkgsCross of stock python3 from the binary cache.
# The per-arch image derivations are kept verbatim; the stage/publish/
# publish-index/publish/push-staged apps come from parity-lib's
# mkNix2ContainerPublish so CI and local invoke the SAME code and cannot drift.
# ci/MIGRATION.md is the design history.
# ──────────────────────────────────────────────────────────────────────────
inputs = {
@@ -18,6 +20,8 @@
nix2container.url = "github:nlewo/nix2container";
nix2container.inputs.nixpkgs.follows = "nixpkgs";
parity.url = "git+https://git.oleks.space/oleks/parity-lib";
flake-utils.url = "github:numtide/flake-utils";
};
@@ -25,6 +29,7 @@
{
nixpkgs,
nix2container,
parity,
flake-utils,
...
}:
@@ -32,21 +37,20 @@
system:
let
pkgs = import nixpkgs { inherit system; };
inherit (pkgs) lib;
n2c = nix2container.packages.${system}.nix2container;
registry = "git.oleks.space/oleks/alertmanager-gotify-bridge";
# TAG is derived identically for CI + local in shared code: CI exports
# VERSION (= strip-v $CI_COMMIT_TAG); local dev may override $VERSION.
# No version.nix side-channel — the app is a static asset, the flake has
# no version-baked source build, so the tag lives purely in $VERSION.
# TAG is derived in shared code: CI exports CI_COMMIT_TAG (parity-lib
# strips the leading v / trailing -N), local dev may override $VERSION.
# No version.nix side-channel — the app is a static asset with no
# version-baked source build, so the tag lives purely in $VERSION/tag.
version = "0.0.0-dev";
# The whole payload: bridge.py + a python3 interpreter symlink under /app.
# The symlink keeps the (arch-correct) python3 Nix closure tracked while
# contributing no extra files. Parameterised over a pkg set so the SAME
# expression builds natively (amd64) and cross (arm64, future leg).
# expression builds natively (amd64) and cross (arm64).
appRoot =
targetPkgs: arch:
pkgs.runCommand "app-root-${arch}" { } ''
@@ -94,111 +98,35 @@
};
};
# amd64 = native; arm64 = cross-compiled (stock python3 closure from the
# binary cache, no qemu). Both are buildable on emmett (amd64).
imageAmd64 = mkImage pkgs "amd64";
imageArm64 = mkImage pkgs.pkgsCross.aarch64-multiplatform "arm64";
# ── publish/stage apps (shared by CI + local; cannot drift). ──────────
# SAFETY: dry-run by default. Set PUBLISH=1 / --publish to actually push.
passEntry = "infra/gitea/personal_access_token_packages_rw";
publishGate = ''
PUBLISH="''${PUBLISH:-0}"
for a in "$@"; do
case "$a" in
--publish) PUBLISH=1 ;;
--dry-run) PUBLISH=0 ;;
--help|-h)
echo "usage: [VERSION=x.y.z] [PUBLISH=1] $(basename "$0") [--publish|--dry-run|--help]" >&2
echo " dry-run by default: builds the amd64 image and prints what would be pushed." >&2
echo " --publish / PUBLISH=1 actually mutates the registry." >&2
echo " NOTE: amd64 leg ONLY (scaffold) see ci/MIGRATION.md." >&2
exit 0 ;;
*) echo "error: unknown argument '$a' (try --help)" >&2; exit 2 ;;
esac
done
TAG="''${VERSION:-${version}}"
'';
# Token resolved lazily (only when actually publishing); never echoed,
# never under set -x (writeShellApplication uses -euo pipefail only).
resolveToken = ''
TOKEN="''${REGISTRY_TOKEN:-}"
if [ -z "$TOKEN" ] && command -v pass >/dev/null 2>&1; then
TOKEN="$(pass show ${passEntry} 2>/dev/null || true)"
fi
if [ -z "$TOKEN" ]; then
echo "BLOCKER(empty-token): set REGISTRY_TOKEN env (CI from_secret) or have 'pass ${passEntry}' available; refusing to publish without credentials." >&2
exit 1
fi
'';
registryPreflight = ''
if ! curl -fsS -o /dev/null --max-time 10 "https://git.oleks.space/v2/" 2>/dev/null; then
echo "BLOCKER(registry-down): https://git.oleks.space/v2/ is unreachable CI shares fate with the cluster (Zot/buildkit on armer/k3s). Re-run when the registry is back; the staged image in the Nix store is unchanged." >&2
exit 1
fi
'';
stageAmd64 = ''
echo " staging ${registry}:$TAG-amd64 (local build, no registry contact)"
OUT="$(nix build --no-link --print-out-paths "$FLAKE#image-amd64")"
echo " staged image derivation: $OUT"
'';
mkStageAmd64 = pkgs.writeShellApplication {
name = "stage-amd64";
runtimeInputs = [ pkgs.nix ];
text = ''
FLAKE="''${FLAKE:-.}"
''
+ publishGate
+ stageAmd64;
};
mkPublishAmd64 = pkgs.writeShellApplication {
name = "publish-amd64";
runtimeInputs = [
pkgs.regctl
pkgs.curl
pkgs.nix
];
text = ''
FLAKE="''${FLAKE:-.}"
''
+ publishGate
+ stageAmd64
+ ''
echo " ${registry}:$TAG-amd64"
if [ "$PUBLISH" != "1" ]; then
echo " [dry-run] would push ${registry}:$TAG-amd64 (set PUBLISH=1 / --publish to push)"
echo " [dry-run] NOTE: amd64 leg only arm64 + index not yet implemented (ci/MIGRATION.md)"
exit 0
fi
''
+ resolveToken
+ registryPreflight
+ ''
${lib.getExe imageAmd64.copyTo} --dest-creds "oleks:$TOKEN" "docker://${registry}:$TAG-amd64"
'';
# ── Publish apps: shared parity-lib nix2container builder (cluster #192).
# Yields stage-<arch> / publish-<arch> / publish-index / publish /
# push-staged. copy-to (skopeo) pushes per-arch, regctl assembles the
# index; no buildkit / docker daemon. Dry-run by default (--publish to
# push); token from $REGISTRY_TOKEN (CI from_secret) → pass fallback,
# never echoed.
builders = parity.lib.mkParityBuilders pkgs;
publishApps = builders.mkNix2ContainerPublish {
imageName = registry;
inherit version;
images = {
amd64.copyTo = imageAmd64.copyTo;
arm64.copyTo = imageArm64.copyTo;
};
};
in
{
packages = {
image-amd64 = imageAmd64;
image-arm64 = imageArm64;
default = imageAmd64;
};
apps = {
stage-amd64 = {
type = "app";
program = lib.getExe mkStageAmd64;
meta.description = "Build the amd64 nix2container image into the local store (no registry contact). Scaffold: amd64 leg only.";
};
publish-amd64 = {
type = "app";
program = lib.getExe mkPublishAmd64;
meta.description = "Stage + push the amd64 image (dry-run by default; PUBLISH=1 to push). Scaffold: amd64 leg only see ci/MIGRATION.md.";
};
};
apps = publishApps;
}
);
}