From 71ec9709a860454ceef46f888d7841bf7b845cb7 Mon Sep 17 00:00:00 2001 From: Oleks Date: Tue, 2 Jun 2026 09:26:27 +0300 Subject: [PATCH] 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. --- .woodpecker.yaml | 30 ++++------ flake.lock | 139 +++++++++++++++++++++++++++++++++++++++++++- flake.nix | 146 ++++++++++++----------------------------------- 3 files changed, 187 insertions(+), 128 deletions(-) diff --git a/.woodpecker.yaml b/.woodpecker.yaml index b837d2a..c471c79 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -5,33 +5,27 @@ when: - event: tag ref: "refs/tags/v*" -skip_clone: true - +# Local-pipeline parity (cluster #192, emmett#44). The flake apps ARE the shared +# code: this same `nix run .#publish` is what a developer runs on emmett. The +# pure-stdlib bridge.py + a stock CPython closure are fully Nix-expressible, so +# both arches build from this single amd64 runner (amd64 native + arm64 +# pkgsCross) — no Dockerfile, no buildkit, no remote builder, no howard pin. +# nix2container copy-to (skopeo) pushes each arch; regctl assembles the index. +# The app DRY-RUNS by default; CI opts in with PUBLISH=1. :latest is a digest +# copy of :TAG made last and guarded off for dev tags. steps: - - name: clone - image: alpine/git - environment: - CLONE_TOKEN: - from_secret: registry_token - commands: - - git clone --depth 1 --branch $CI_COMMIT_TAG https://oleks:$CLONE_TOKEN@git.oleks.space/oleks/alertmanager-gotify-bridge.git . - - - name: build-and-push + - name: publish image: git.oleks.space/oleks/nix-ci:latest environment: + # One token env var everywhere; the app reads it (never interpolated). REGISTRY_TOKEN: from_secret: registry_token commands: - - echo "$REGISTRY_TOKEN" | docker login git.oleks.space -u oleks --password-stdin - - docker buildx create --name arm64 --driver remote "tcp://buildkit-rootless-arm64.infra.svc.cluster.local:1234" - - TAG=$(echo "$CI_COMMIT_TAG" | sed 's/^v//') - - IMAGE="git.oleks.space/oleks/alertmanager-gotify-bridge" - - echo "Building $IMAGE:$TAG" - - docker buildx build --builder arm64 --platform linux/amd64,linux/arm64 --tag "$IMAGE:$TAG" --tag "$IMAGE:latest" --push . + - nixos-ci-entrypoint bash -c "set -e; PUBLISH=1 nix run .#publish" backend_options: kubernetes: nodeSelector: - kubernetes.io/hostname: howard2404 + kubernetes.io/arch: amd64 labels: commit-tag: "${CI_COMMIT_TAG}" commit-branch: "${CI_COMMIT_BRANCH}" diff --git a/flake.lock b/flake.lock index b941ca8..dffc7fe 100644 --- a/flake.lock +++ b/flake.lock @@ -18,6 +18,24 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "fleet": { "inputs": { "nixpkgs": "nixpkgs", @@ -72,6 +90,69 @@ "url": "https://git.oleks.space/oleks/fleet-pins" } }, + "fleet_2": { + "inputs": { + "nixpkgs": "nixpkgs_2", + "nixpkgs-armer": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-bim": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-ci": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-emmett": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-howard": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-mermaid": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-mermaid-gpu": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-micron": [ + "parity", + "fleet", + "nixpkgs" + ], + "nixpkgs-projects": [ + "parity", + "fleet", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1779533061, + "narHash": "sha256-orWNYXtYURhEj3X4+xGMAhaEcKRvwXqTtJ8x2jV/M+Q=", + "ref": "refs/heads/main", + "rev": "b818e345ec4470e4b3e335bd2f864183c512116d", + "revCount": 13, + "type": "git", + "url": "https://git.oleks.space/oleks/fleet-pins" + }, + "original": { + "type": "git", + "url": "https://git.oleks.space/oleks/fleet-pins" + } + }, "nix2container": { "inputs": { "nixpkgs": [ @@ -108,6 +189,46 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "type": "github" + } + }, + "parity": { + "inputs": { + "flake-utils": "flake-utils_2", + "fleet": "fleet_2", + "nixpkgs": [ + "parity", + "fleet", + "nixpkgs-ci" + ] + }, + "locked": { + "lastModified": 1780379798, + "narHash": "sha256-vm88bZ2O/KHb14dUPlpuSMPzQlwDRwiTfZrZUMpo1Pw=", + "ref": "refs/heads/main", + "rev": "af64a8ea4c537f6b5e9f2bee0fbddc59648f7d32", + "revCount": 7, + "type": "git", + "url": "https://git.oleks.space/oleks/parity-lib" + }, + "original": { + "type": "git", + "url": "https://git.oleks.space/oleks/parity-lib" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", @@ -116,7 +237,8 @@ "nixpkgs": [ "fleet", "nixpkgs-projects" - ] + ], + "parity": "parity" } }, "systems": { @@ -133,6 +255,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 7bdf9d3..559aa84 100644 --- a/flake.nix +++ b/flake.nix @@ -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- / publish- / 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; } ); }