diff --git a/.woodpecker.yaml b/.woodpecker.yaml index aae3c30..dbba665 100644 --- a/.woodpecker.yaml +++ b/.woodpecker.yaml @@ -45,7 +45,8 @@ steps: EOF - if [ -n "$GITEA_CLONE_TOKEN" ]; then echo "machine git.oleks.space login oleks password $GITEA_CLONE_TOKEN" >~/.netrc && chmod 600 ~/.netrc; fi - # Thin front door: CI and `nix run .#publish-arm64` on emmett run the - # exact same shared app (emmett#44, archetype oci-image-skopeo). The app - # is dry-run by default; PUBLISH=1 makes it actually push. - - PUBLISH=1 nix run .#publish-arm64 + # Thin front door: CI and `nix run .#publish` on emmett run the exact + # same shared parity-lib nix2container app (emmett#44, cluster #201). + # `.#publish` pushes :-arm64, assembles the index :, and mirrors + # :latest. Dry-run by default; PUBLISH=1 makes it actually push. + - PUBLISH=1 nix run .#publish diff --git a/flake.lock b/flake.lock index 4fe7997..3d6e425 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,106 @@ { "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "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_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": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1775487831, + "narHash": "sha256-2lguQpLPQaxpQCJjXhmEEAfabwsAhkP29Z7fgLzHARA=", + "owner": "nlewo", + "repo": "nix2container", + "rev": "76be9608a7f4d6c985d28b0e7be903ae2547df3e", + "type": "github" + }, + "original": { + "owner": "nlewo", + "repo": "nix2container", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1777954456, @@ -16,9 +117,66 @@ "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", + "fleet": "fleet", + "nixpkgs": [ + "parity", + "fleet", + "nixpkgs-ci" + ] + }, + "locked": { + "lastModified": 1780597793, + "narHash": "sha256-TGFRv37oEyJY96Ax+oHK05cttGUuf9tifrRh/AQiiXs=", + "ref": "refs/heads/main", + "rev": "413f78c365d2d433f3f74cfc59c146de87097b1f", + "revCount": 12, + "type": "git", + "url": "https://git.oleks.space/oleks/parity-lib" + }, + "original": { + "type": "git", + "url": "https://git.oleks.space/oleks/parity-lib" + } + }, "root": { "inputs": { - "nixpkgs": "nixpkgs" + "nix2container": "nix2container", + "nixpkgs": "nixpkgs", + "parity": "parity" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" } } }, diff --git a/flake.nix b/flake.nix index db5adeb..4b39029 100644 --- a/flake.nix +++ b/flake.nix @@ -1,13 +1,24 @@ { description = "Angie web server (aarch64) — OCI image for Gitea registry"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + nix2container.url = "github:nlewo/nix2container"; + nix2container.inputs.nixpkgs.follows = "nixpkgs"; + parity.url = "git+https://git.oleks.space/oleks/parity-lib"; + }; outputs = - { self, nixpkgs }: + { + nixpkgs, + nix2container, + parity, + ... + }: let system = "aarch64-linux"; pkgs = import nixpkgs { inherit system; }; + n2c = nix2container.packages.${system}.nix2container; # nixpkgs ships angie compiled against `--prefix=/etc/angie` and # `--http-log-path=/var/log/angie/access.log`; the package's `bin/angie` @@ -41,29 +52,43 @@ } ''; - image = pkgs.dockerTools.streamLayeredImage { - name = "angie"; + # The image root extra: /etc/angie (the bundled conf-dir + our main config) + # plus the runtime dirs angie expects to exist. angie runs as root and the + # container gets a writable upper layer, so the dirs only need to EXIST — + # no sticky/1777 perms needed (this replaces streamLayeredImage's + # extraCommands, which did the same mkdir + cp of the angie conf-dir). + rootExtra = pkgs.runCommand "angie-root" { } '' + mkdir -p $out/var/log/nginx $out/var/cache/angie $out/var/lib/angie \ + $out/run $out/tmp $out/etc/angie/http.d + cp ${angie}/conf/* $out/etc/angie/ + chmod -R u+w $out/etc/angie + cp -f ${mainConf} $out/etc/angie/angie.conf + ''; + + # OCI image via nix2container (cluster #201): yields `.copyTo` (skopeo) so + # it consumes the shared parity-lib mkNix2ContainerPublish — no buildkit, + # no inline skopeo/token/guard. Replaces dockerTools.streamLayeredImage. + image = n2c.buildImage { + name = "git.oleks.space/oleks/angie"; tag = angie.version; - contents = with pkgs; [ - angie - cacert - dockerTools.fakeNss - coreutils - bash + arch = "arm64"; + layers = [ + (n2c.buildLayer { + copyToRoot = [ + rootExtra + angie + pkgs.cacert + pkgs.dockerTools.fakeNss + pkgs.coreutils + pkgs.bash + ]; + maxLayers = 25; + # reproducible = false materializes the layer tar so the image streams + # verbatim from any host (remote-builder + binary-cache safe); parity + # is asserted at the OCI manifest DIGEST (nix run .#verify-digest, #195). + reproducible = false; + }) ]; - # Writable runtime dirs. /var/log/nginx is the compiled-in path for - # angie (matches nixpkgs' nginx build flags); the chart's main config - # also writes pid to /run. - extraCommands = '' - mkdir -p var/log/nginx var/cache/angie var/lib/angie run tmp etc/angie/http.d - chmod 1777 tmp run - # Ship the conf-dir bundled with angie (mime.types, fastcgi_params, - # scgi_params, uwsgi_params, koi-utf, koi-win, etc.) — chart configs - # `include fastcgi_params;` and similar resolve relative to /etc/angie. - cp ${angie}/conf/* etc/angie/ - chmod -R u+w etc/angie - cp -f ${mainConf} etc/angie/angie.conf - ''; config = { Entrypoint = [ "${angie}/bin/angie" ]; Cmd = [ @@ -78,112 +103,19 @@ WorkingDir = "/etc/angie"; }; }; - # Shared build+publish logic for the arm64 OCI leg. This script IS the - # parity code (emmett#44, archetype: oci-image-skopeo): both - # `.woodpecker.yaml` and a local `nix run .#publish-arm64` invoke the - # exact same entrypoint, so CI and local cannot drift. - # - # Safety: DRY-RUN by default. It builds the image stream and prints the - # refs it WOULD push, but performs no registry contact unless PUBLISH=1 - # is set. The token is never printed and this script never runs under - # `set -x` (which would leak the auth header). - publish-arm64 = pkgs.writeShellApplication { - name = "publish-arm64"; - runtimeInputs = with pkgs; [ - skopeo - coreutils - gnused - gitMinimal - ]; - # `pass` is optional (only the local fallback path needs it) and may - # live outside this closure, so it is resolved from PATH at runtime. - text = '' - set -euo pipefail - # --- usage ------------------------------------------------------- - if [ "''${1:-}" = "--help" ] || [ "''${1:-}" = "-h" ]; then - printf '%s\n' \ - "publish-arm64 — build the angie arm64 OCI image and (optionally) push it." \ - "" \ - "Builds the arm64 docker-archive via Nix and uses skopeo to copy it to" \ - "the Gitea OCI registry as :-arm64, then mirrors that digest to" \ - ":latest-arm64." \ - "" \ - "DRY-RUN by default: prints the refs it would push and exits without" \ - "contacting the registry. Set PUBLISH=1 to actually push." \ - "" \ - "Env:" \ - " PUBLISH=1 actually push (default: dry-run)" \ - " VERSION= override the tag (default: angie.version)" \ - " REGISTRY_TOKEN= registry RW token; if empty, falls back to" \ - " pass infra/gitea/personal_access_token_packages_rw" \ - "" \ - "Flags:" \ - " --help, -h this help" \ - " --dry-run force dry-run even if PUBLISH=1" - exit 0 - fi - - DRY_RUN=0 - if [ "''${PUBLISH:-0}" != "1" ]; then DRY_RUN=1; fi - if [ "''${1:-}" = "--dry-run" ]; then DRY_RUN=1; fi - - REGISTRY="git.oleks.space" - IMAGE="oleks/angie" - - # --- version / tag (identical for CI + local) -------------------- - VERSION="''${VERSION:-$(nix eval --raw .#angieVersion)}" - echo "angie version: $VERSION" - echo "target refs: docker://$REGISTRY/$IMAGE:$VERSION-arm64" - echo " docker://$REGISTRY/$IMAGE:latest-arm64" - - # --- build (cluster-independent) --------------------------------- - echo "building arm64 image stream..." - STREAM="$(nix build .#default --print-out-paths --no-link)" - - if [ "$DRY_RUN" = "1" ]; then - echo "DRY-RUN: built $STREAM" - echo "DRY-RUN: would skopeo-copy docker-archive -> docker://$REGISTRY/$IMAGE:$VERSION-arm64" - echo "DRY-RUN: would mirror :$VERSION-arm64 -> :latest-arm64" - echo "DRY-RUN: set PUBLISH=1 to actually push." - exit 0 - fi - - # --- token resolution (never printed) ---------------------------- - TOKEN="''${REGISTRY_TOKEN:-}" - if [ -z "$TOKEN" ]; then - if command -v pass >/dev/null 2>&1; then - TOKEN="$(pass infra/gitea/personal_access_token_packages_rw)" - fi - fi - if [ -z "$TOKEN" ]; then - echo "ERROR: no registry token. Set REGISTRY_TOKEN or store it at" >&2 - echo " pass infra/gitea/personal_access_token_packages_rw" >&2 - exit 1 - fi - - # skopeo's containers/image library stages under /var/tmp (not TMPDIR). - mkdir -p /var/tmp && chmod 1777 /var/tmp || true - - AUTHFILE="$(mktemp -d)/auth.json" - # shellcheck disable=SC2064 - trap "rm -rf '$(dirname "$AUTHFILE")'" EXIT - printf '{"auths":{"%s":{"auth":"%s"}}}\n' \ - "$REGISTRY" "$(printf 'oleks:%s' "$TOKEN" | base64 -w0)" \ - > "$AUTHFILE" - - echo "pushing :$VERSION-arm64 ..." - skopeo copy --insecure-policy --authfile "$AUTHFILE" \ - docker-archive:<("$STREAM") \ - "docker://$REGISTRY/$IMAGE:$VERSION-arm64" - - echo "mirroring :$VERSION-arm64 -> :latest-arm64 ..." - skopeo copy --insecure-policy --authfile "$AUTHFILE" \ - "docker://$REGISTRY/$IMAGE:$VERSION-arm64" \ - "docker://$REGISTRY/$IMAGE:latest-arm64" - - echo "done." - ''; + # Publish apps: shared parity-lib nix2container builder (cluster #201). + # Single-arch (arm64): yields stage-arm64 / publish-arm64 / publish-index / + # publish / push-staged / verify-digest. copy-to (skopeo) pushes the arch + # tag, regctl assembles the index; dry-run by default (--publish to push); + # token $REGISTRY_TOKEN -> pass fallback, never echoed. + builders = parity.lib.mkParityBuilders pkgs; + publishApps = builders.mkNix2ContainerPublish { + imageName = "git.oleks.space/oleks/angie"; + inherit (angie) version; + images = { + arm64.copyTo = image.copyTo; + }; }; in { @@ -191,13 +123,9 @@ default = image; }; - apps.${system} = { - publish-arm64 = { - type = "app"; - program = "${publish-arm64}/bin/publish-arm64"; - meta.description = "Build and publish the angie arm64 OCI image to git.oleks.space (dry-run by default; PUBLISH=1 to push)."; - }; - }; + # stage-arm64 / publish-arm64 / publish-index / publish / push-staged / + # verify-digest come from parity-lib. + apps.${system} = publishApps; # Plain string — read by CI via `nix eval --raw .#angieVersion`. angieVersion = angie.version;