feat: shared per-archetype parity publish-app builders (v0.1.0)
Implements the shared parity flake-module library so the ~51 parity repos
consume one source of truth instead of hand-inlined publish shells.
- lib.mk{PyPiWheel,S390xNpm,GenericBinary,Nix2Container,GoBinary,Helm}Publish
builders returning stage-<arch>/publish-<arch>/publish-index/publish/
push-staged apps per the corrected emmett#44 standard (build-parity stages to
./.parity-stage with no registry contact; publish dry-runs by default;
publish-index is build-free + fail-closed; :latest is the last digest copy).
- Shared ci/parity-lib.sh: token resolution ($REGISTRY_TOKEN + pass fallback,
never printed), dev-tag guard, version derivation, dry-run gate, preflight.
- pipeline-doctor package/app asserting the parity contract (cluster #193).
Refs cluster #192, #193, #194, emmett#44.
This commit is contained in:
@@ -0,0 +1,742 @@
|
||||
# Per-archetype publish-app BUILDERS (cluster #192/#194, emmett#44).
|
||||
#
|
||||
# `mkParityBuilders { pkgs }` returns the six mk*Publish builders plus the
|
||||
# shared building blocks. Each mk*Publish takes a small attrset and returns an
|
||||
# attrset of flake apps following the corrected parity standard:
|
||||
#
|
||||
# stage-<arch> BUILD-parity, cluster-independent: writes the artifact to the
|
||||
# on-disk stage (./.parity-stage/<arch>), NO registry contact.
|
||||
# publish-<arch> stage + push that arch. DRY-RUN by default (--publish to push).
|
||||
# publish-index build-free multi-arch assembly from digest-pinned refs
|
||||
# (regctl), fail-closed if a required arch was not pushed.
|
||||
# publish all locally-buildable arches + publish-index; ':latest' is a
|
||||
# digest copy of ':TAG' done LAST as the idempotent mutation.
|
||||
# push-staged replay artifacts from ./.parity-stage to the registry.
|
||||
#
|
||||
# Not every archetype yields every app: a single-arch wheel/binary/npm addon
|
||||
# has no multi-arch index, so it exposes stage/publish/push-staged only. The OCI
|
||||
# (nix2container) builder is the one that yields the full index/latest set.
|
||||
{ pkgs }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
|
||||
shellLib = ../ci/parity-lib.sh;
|
||||
|
||||
# Common preamble: source the shared shell helpers from the store and apply
|
||||
# any per-app coordinate overrides BEFORE the helpers are used.
|
||||
preamble =
|
||||
{
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
''
|
||||
set -euo pipefail
|
||||
# shellcheck source=/dev/null
|
||||
. ${shellLib}
|
||||
# Coordinate overrides are read by the sourced lib; export so shellcheck
|
||||
# treats them as used (SC2034) and child processes inherit them.
|
||||
export PARITY_REGISTRY_HOST=${lib.escapeShellArg registryHost}
|
||||
export PARITY_REGISTRY_OWNER=${lib.escapeShellArg registryOwner}
|
||||
export PARITY_PASS_ENTRY=${lib.escapeShellArg passEntry}
|
||||
export PARITY_STAGE_DIR="''${PARITY_STAGE_DIR:-''${PWD}/.parity-stage}"
|
||||
'';
|
||||
|
||||
mkApp = drv: desc: {
|
||||
type = "app";
|
||||
program = lib.getExe drv;
|
||||
meta.description = desc;
|
||||
};
|
||||
|
||||
baseInputs = with pkgs; [
|
||||
coreutils
|
||||
curl
|
||||
findutils
|
||||
gnugrep
|
||||
gnused
|
||||
pass
|
||||
];
|
||||
|
||||
# =========================================================================
|
||||
# PyPI wheel (single-arch). Stage a built wheel into .parity-stage, then
|
||||
# POST it to the Gitea PyPI registry (idempotent: 409 = already present).
|
||||
# Args: { pname, version|versionFn, wheel (drv|path producing *.whl),
|
||||
# arch ? "s390x", registryHost?, registryOwner?, passEntry? }
|
||||
# =========================================================================
|
||||
mkPyPiWheelPublish =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
wheel,
|
||||
arch ? "s390x",
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
extractCmd ? null,
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
# The wheel input may be a directory (Nix store output) we glob for *.whl,
|
||||
# or an explicit single .whl path.
|
||||
wheelRef = "${wheel}";
|
||||
stageText = ''
|
||||
${head}
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
echo "→ staging ${pname} $VERSION wheel (${arch}, no registry contact)"
|
||||
d="$(parity_stage_reset ${lib.escapeShellArg arch})"
|
||||
src=${lib.escapeShellArg wheelRef}
|
||||
if [ -d "$src" ]; then
|
||||
# shellcheck disable=SC2044
|
||||
for w in $(find "$src" -name '*.whl'); do cp "$w" "$d/"; done
|
||||
else
|
||||
cp "$src" "$d/"
|
||||
fi
|
||||
whl="$(find "$d" -name '*.whl' | head -1)"
|
||||
if [ -z "$whl" ]; then echo "BLOCKER(no-wheel): no *.whl under $src" >&2; exit 1; fi
|
||||
printf 'pypi %s %s\n' "${pname}" "$VERSION" >"$d/.parity-meta"
|
||||
sha256sum "$whl"
|
||||
echo " staged: $whl"
|
||||
'';
|
||||
stage = pkgs.writeShellApplication {
|
||||
name = "stage-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nix ];
|
||||
text = stageText;
|
||||
};
|
||||
publish = pkgs.writeShellApplication {
|
||||
name = "publish-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nix ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Build + publish the ${pname} ${arch} wheel to the Gitea PyPI registry" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
${lib.getExe stage}
|
||||
d="$(parity_stage_path ${lib.escapeShellArg arch})"
|
||||
whl="$(find "$d" -name '*.whl' | head -1)"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would POST $(basename "$whl") -> https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/pypi (name=${pname} version=$VERSION)"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to upload."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
sha="$(sha256sum "$whl" | cut -d' ' -f1)"
|
||||
http="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token $tok" \
|
||||
-F ':action=file_upload' -F 'protocol_version=1' \
|
||||
-F "sha256_digest=$sha" -F "name=${pname}" -F "version=$VERSION" \
|
||||
-F 'filetype=bdist_wheel' -F "content=@$whl" \
|
||||
"https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/pypi")"
|
||||
case "$http" in
|
||||
200 | 201 | 409) echo "Published ${pname} $VERSION (HTTP $http)" ;;
|
||||
*) echo "BLOCKER(upload-failed): POST returned HTTP $http" >&2; exit 1 ;;
|
||||
esac
|
||||
'';
|
||||
};
|
||||
pushStaged = mkPushStagedGeneric {
|
||||
inherit
|
||||
pname
|
||||
arch
|
||||
registryHost
|
||||
registryOwner
|
||||
passEntry
|
||||
;
|
||||
kind = "pypi";
|
||||
};
|
||||
in
|
||||
{
|
||||
"stage-${arch}" = mkApp stage "Stage the ${pname} ${arch} wheel into .parity-stage (no registry contact).";
|
||||
"publish-${arch}" = mkApp publish "Stage + publish the ${pname} ${arch} wheel (dry-run by default; --publish to push).";
|
||||
"publish" = mkApp publish "Publish all locally-buildable arches (${arch} only for this PyPI repo).";
|
||||
"push-staged" = pushStaged;
|
||||
};
|
||||
|
||||
# =========================================================================
|
||||
# s390x npm native addon (single-arch). Stage a ready-to-publish package dir
|
||||
# (the .node + a generated package.json) then `npm publish`.
|
||||
# Args: { pname (npm full name, e.g. @rollup/rollup-linux-s390x-gnu),
|
||||
# version, nodeFile (drv|path to the *.node), nodeFileName,
|
||||
# packageJson (string, with $version interpolated by the app),
|
||||
# arch ? "s390x", registry* }
|
||||
# =========================================================================
|
||||
mkS390xNpmPublish =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
nodeFile,
|
||||
nodeFileName,
|
||||
packageJson,
|
||||
arch ? "s390x",
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
stage = pkgs.writeShellApplication {
|
||||
name = "stage-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nodejs ];
|
||||
text = ''
|
||||
${head}
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
echo "→ staging ${pname}@$VERSION (${arch}, no registry contact)"
|
||||
d="$(parity_stage_reset ${lib.escapeShellArg arch})"
|
||||
install -m 0644 ${lib.escapeShellArg "${nodeFile}"} "$d/${nodeFileName}"
|
||||
cat >"$d/package.json" <<EOF
|
||||
${packageJson}
|
||||
EOF
|
||||
printf 'npm %s %s\n' "${pname}" "$VERSION" >"$d/.parity-meta"
|
||||
echo " staged into $d:"
|
||||
ls -lh "$d"
|
||||
'';
|
||||
};
|
||||
publish = pkgs.writeShellApplication {
|
||||
name = "publish-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nodejs ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Build + publish the ${pname} ${arch} npm addon to the Gitea npm registry" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
${lib.getExe stage}
|
||||
d="$(parity_stage_path ${lib.escapeShellArg arch})"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would 'npm publish' $d -> https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to push."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
reg="https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/"
|
||||
( cd "$d"
|
||||
set +e
|
||||
out="$(npm publish --registry="$reg" \
|
||||
"--//$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/:_authToken=$tok" 2>&1)"
|
||||
rc=$?
|
||||
set -e
|
||||
printf '%s\n' "$out" | sed "s/$tok/***REDACTED***/g"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if printf '%s' "$out" | grep -qiE 'already exists|409|conflict'; then
|
||||
echo "Version already published — idempotent success."; exit 0
|
||||
fi
|
||||
exit "$rc"
|
||||
fi )
|
||||
echo "Published ${pname}@$VERSION"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
"stage-${arch}" = mkApp stage "Stage the ${pname} ${arch} npm addon into .parity-stage (no registry contact).";
|
||||
"publish-${arch}" = mkApp publish "Stage + publish the ${pname} ${arch} npm addon (dry-run by default; --publish to push).";
|
||||
"publish" = mkApp publish "Publish all locally-buildable arches (${arch} only for this npm repo).";
|
||||
"push-staged" = mkPushStagedGeneric {
|
||||
inherit
|
||||
pname
|
||||
arch
|
||||
registryHost
|
||||
registryOwner
|
||||
passEntry
|
||||
;
|
||||
kind = "npm";
|
||||
};
|
||||
};
|
||||
|
||||
# =========================================================================
|
||||
# Generic binary (single-arch). Stage a built binary, PUT it to the Gitea
|
||||
# generic registry (idempotent: 201 created / 409 exists).
|
||||
# Args: { pname, version, binary (drv|path), assetName, arch ? "s390x",
|
||||
# registry* }
|
||||
# =========================================================================
|
||||
mkGenericBinaryPublish =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
binary,
|
||||
assetName,
|
||||
arch ? "s390x",
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
stage = pkgs.writeShellApplication {
|
||||
name = "stage-${arch}";
|
||||
runtimeInputs = baseInputs ++ [
|
||||
pkgs.nix
|
||||
pkgs.file
|
||||
];
|
||||
text = ''
|
||||
${head}
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
echo "→ staging ${pname} $VERSION (${arch}, no registry contact)"
|
||||
d="$(parity_stage_reset ${lib.escapeShellArg arch})"
|
||||
cp ${lib.escapeShellArg "${binary}"} "$d/${assetName}"
|
||||
printf 'generic %s %s %s\n' "${pname}" "$VERSION" "${assetName}" >"$d/.parity-meta"
|
||||
file "$d/${assetName}" 2>/dev/null || true
|
||||
echo " staged: $d/${assetName}"
|
||||
'';
|
||||
};
|
||||
publish = pkgs.writeShellApplication {
|
||||
name = "publish-${arch}";
|
||||
runtimeInputs = baseInputs ++ [
|
||||
pkgs.nix
|
||||
pkgs.file
|
||||
];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Build + publish the ${pname} ${arch} binary to the Gitea generic registry" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
${lib.getExe stage}
|
||||
d="$(parity_stage_path ${lib.escapeShellArg arch})"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would PUT $d/${assetName} -> https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/generic/${pname}/$VERSION/${assetName}"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to upload."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
http="$(curl -s -o /dev/null -w '%{http_code}' -X PUT \
|
||||
-H "Authorization: token $tok" --upload-file "$d/${assetName}" \
|
||||
"https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/generic/${pname}/$VERSION/${assetName}")"
|
||||
case "$http" in
|
||||
201 | 409) echo "Published ${assetName} $VERSION (HTTP $http)" ;;
|
||||
*) echo "BLOCKER(upload-failed): PUT returned HTTP $http" >&2; exit 1 ;;
|
||||
esac
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
"stage-${arch}" = mkApp stage "Stage the ${pname} ${arch} binary into .parity-stage (no registry contact).";
|
||||
"publish-${arch}" = mkApp publish "Stage + publish the ${pname} ${arch} binary (dry-run by default; --publish to push).";
|
||||
"publish" = mkApp publish "Publish all locally-buildable arches (${arch} only for this generic-binary repo).";
|
||||
"push-staged" = mkPushStagedGeneric {
|
||||
inherit
|
||||
pname
|
||||
arch
|
||||
assetName
|
||||
registryHost
|
||||
registryOwner
|
||||
passEntry
|
||||
;
|
||||
kind = "generic";
|
||||
inherit version;
|
||||
};
|
||||
};
|
||||
|
||||
# =========================================================================
|
||||
# Go binary (single-arch generic). Thin alias over the generic-binary builder
|
||||
# — a cross-compiled Go binary is published to the generic registry exactly
|
||||
# like any other binary. Kept as a distinct named builder so the archetype is
|
||||
# explicit in consumer flakes and pipeline-doctor.
|
||||
# =========================================================================
|
||||
mkGoBinaryPublish = mkGenericBinaryPublish;
|
||||
|
||||
# =========================================================================
|
||||
# Helm chart (single artifact). Stage `helm package` output (.tgz) then
|
||||
# `helm push` to the OCI registry (idempotent: a duplicate version errors,
|
||||
# which we treat as success).
|
||||
# Args: { pname (chart name), version, chartSrc (drv|path to chart dir),
|
||||
# ociRepo ? "oci://git.oleks.space/oleks", registry* }
|
||||
# =========================================================================
|
||||
mkHelmPublish =
|
||||
{
|
||||
pname,
|
||||
version,
|
||||
chartSrc,
|
||||
ociRepo ? "oci://git.oleks.space/oleks",
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
stage = pkgs.writeShellApplication {
|
||||
name = "stage-chart";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.kubernetes-helm ];
|
||||
text = ''
|
||||
${head}
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
echo "→ staging helm chart ${pname} $VERSION (no registry contact)"
|
||||
d="$(parity_stage_reset chart)"
|
||||
helm package ${lib.escapeShellArg "${chartSrc}"} --version "$VERSION" --app-version "$VERSION" --destination "$d"
|
||||
printf 'helm %s %s\n' "${pname}" "$VERSION" >"$d/.parity-meta"
|
||||
echo " staged:"; ls -lh "$d"/*.tgz
|
||||
'';
|
||||
};
|
||||
publish = pkgs.writeShellApplication {
|
||||
name = "publish-chart";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.kubernetes-helm ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Package + publish the ${pname} helm chart to ${ociRepo}" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
${lib.getExe stage}
|
||||
d="$(parity_stage_path chart)"
|
||||
tgz="$(find "$d" -name '*.tgz' | head -1)"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would 'helm push $(basename "$tgz")' -> ${ociRepo}"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to push."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
echo "$tok" | helm registry login "$PARITY_REGISTRY_HOST" -u "$PARITY_REGISTRY_OWNER" --password-stdin >/dev/null 2>&1
|
||||
set +e
|
||||
out="$(helm push "$tgz" ${lib.escapeShellArg ociRepo} 2>&1)"
|
||||
rc=$?
|
||||
set -e
|
||||
printf '%s\n' "$out" | sed "s/$tok/***REDACTED***/g"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if printf '%s' "$out" | grep -qiE 'already exists|409|conflict'; then
|
||||
echo "Chart version already published — idempotent success."; exit 0
|
||||
fi
|
||||
exit "$rc"
|
||||
fi
|
||||
echo "Published chart ${pname} $VERSION"
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
"stage-chart" = mkApp stage "Package the ${pname} helm chart into .parity-stage (no registry contact).";
|
||||
"publish-chart" = mkApp publish "Package + publish the ${pname} helm chart (dry-run by default; --publish to push).";
|
||||
"publish" = mkApp publish "Publish the ${pname} helm chart.";
|
||||
"push-staged" = mkPushStagedHelm {
|
||||
inherit
|
||||
pname
|
||||
ociRepo
|
||||
registryHost
|
||||
registryOwner
|
||||
passEntry
|
||||
;
|
||||
};
|
||||
};
|
||||
|
||||
# =========================================================================
|
||||
# nix2container OCI image (MULTI-ARCH). This is the archetype that yields the
|
||||
# full stage/publish-<arch>/publish-index/publish/push-staged set.
|
||||
#
|
||||
# Args: {
|
||||
# imageName, # e.g. git.oleks.space/oleks/nix-ci
|
||||
# version|tagFn, # the immutable :TAG
|
||||
# images, # { <arch> = { copyTo = <nix2container copyTo drv>; }; }
|
||||
# # only arches buildable on THIS host should appear
|
||||
# arches ? attrNames images, # the full set the index must cover (fail-closed)
|
||||
# registry* }
|
||||
#
|
||||
# Per-arch publish copies the arch image to <imageName>:<TAG>-<arch>.
|
||||
# publish-index assembles <imageName>:<TAG> from the per-arch digest-pinned
|
||||
# tags via `regctl index create`, failing closed if any required arch tag is
|
||||
# missing. publish runs all local arches, then the index, then copies
|
||||
# :<TAG> -> :latest as the LAST idempotent mutation.
|
||||
# =========================================================================
|
||||
mkNix2ContainerPublish =
|
||||
{
|
||||
imageName,
|
||||
version,
|
||||
images,
|
||||
arches ? lib.attrNames images,
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
localArches = lib.attrNames images;
|
||||
|
||||
# per-arch stage: realise the image's copy-to closure locally (no push).
|
||||
mkArchStage =
|
||||
arch:
|
||||
pkgs.writeShellApplication {
|
||||
name = "stage-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nix ];
|
||||
text = ''
|
||||
${head}
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
echo "→ staging ${imageName}:$VERSION-${arch} (build closure only, no push)"
|
||||
d="$(parity_stage_reset ${lib.escapeShellArg arch})"
|
||||
# Realise the nix2container copyTo wrapper (its closure is the image).
|
||||
out=${lib.escapeShellArg "${images.${arch}.copyTo}"}
|
||||
printf 'oci %s %s %s\n' ${lib.escapeShellArg imageName} "$VERSION" "${arch}" >"$d/.parity-meta"
|
||||
printf '%s\n' "$out" >"$d/copy-to-path"
|
||||
echo " staged copyTo: $out"
|
||||
'';
|
||||
};
|
||||
|
||||
mkArchPublish =
|
||||
arch:
|
||||
pkgs.writeShellApplication {
|
||||
name = "publish-${arch}";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nix ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Build + push ${imageName}:<TAG>-${arch}" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
${lib.getExe (mkArchStage arch)}
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would copy ${imageName}:$VERSION-${arch} (digest-pinned per-arch tag)"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to push."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
creds="$PARITY_REGISTRY_OWNER:$tok"
|
||||
${lib.escapeShellArg "${images.${arch}.copyTo}"}/bin/copy-to \
|
||||
--dest-creds "$creds" \
|
||||
"docker://${imageName}:$VERSION-${arch}"
|
||||
echo "Pushed ${imageName}:$VERSION-${arch}"
|
||||
'';
|
||||
};
|
||||
|
||||
archStageApps = lib.listToAttrs (
|
||||
map (a: {
|
||||
name = "stage-${a}";
|
||||
value = mkApp (mkArchStage a) "Stage the ${imageName} ${a} image closure into .parity-stage (no push).";
|
||||
}) localArches
|
||||
);
|
||||
archPublishApps = lib.listToAttrs (
|
||||
map (a: {
|
||||
name = "publish-${a}";
|
||||
value = mkApp (mkArchPublish a) "Build + push ${imageName}:<TAG>-${a} (dry-run by default; --publish to push).";
|
||||
}) localArches
|
||||
);
|
||||
|
||||
# build-free multi-arch index from the per-arch digest-pinned tags.
|
||||
indexApp = pkgs.writeShellApplication {
|
||||
name = "publish-index";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.regclient ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Assemble the multi-arch index ${imageName}:<TAG> from pushed per-arch tags" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
req=(${lib.concatStringsSep " " (map lib.escapeShellArg arches)})
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would 'regctl index create ${imageName}:$VERSION' from:"
|
||||
for a in "''${req[@]}"; do echo " ${imageName}:$VERSION-$a"; done
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to assemble."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
regctl registry login "$PARITY_REGISTRY_HOST" -u "$PARITY_REGISTRY_OWNER" --pass-stdin <<<"$tok" >/dev/null 2>&1
|
||||
# Fail-closed: every required arch tag must exist before assembly.
|
||||
refs=()
|
||||
for a in "''${req[@]}"; do
|
||||
ref="${imageName}:$VERSION-$a"
|
||||
if ! regctl manifest head "$ref" >/dev/null 2>&1; then
|
||||
echo "BLOCKER(missing-arch): required arch tag $ref was not pushed this run; refusing to assemble a partial index." >&2
|
||||
exit 1
|
||||
fi
|
||||
refs+=("$ref")
|
||||
done
|
||||
regctl index create "${imageName}:$VERSION" "''${refs[@]/#/--ref=}"
|
||||
echo "Assembled index ${imageName}:$VERSION"
|
||||
'';
|
||||
};
|
||||
|
||||
# publish: all locally-buildable arches -> index -> :latest (last).
|
||||
publishAll = pkgs.writeShellApplication {
|
||||
name = "publish";
|
||||
runtimeInputs = baseInputs ++ [
|
||||
pkgs.nix
|
||||
pkgs.regclient
|
||||
];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Publish all local arches of ${imageName} + the multi-arch index; :latest = digest copy of :TAG" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
fwd=()
|
||||
[ "$PARITY_PUBLISH" = "1" ] && fwd=(--publish)
|
||||
${lib.concatMapStringsSep "\n" (a: " ${lib.getExe (mkArchPublish a)} \"\${fwd[@]}\"") localArches}
|
||||
${lib.getExe indexApp} "''${fwd[@]}"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would copy ${imageName}:$VERSION -> ${imageName}:latest (digest copy, LAST mutation)"
|
||||
exit 0
|
||||
fi
|
||||
tok="$(parity_resolve_token)"
|
||||
regctl registry login "$PARITY_REGISTRY_HOST" -u "$PARITY_REGISTRY_OWNER" --pass-stdin <<<"$tok" >/dev/null 2>&1
|
||||
# ':latest' is a pure digest copy of the just-assembled ':TAG', done
|
||||
# LAST so it is the single idempotent mutation that flips the channel.
|
||||
regctl image copy "${imageName}:$VERSION" "${imageName}:latest"
|
||||
echo "Published ${imageName}:$VERSION and flipped :latest"
|
||||
'';
|
||||
};
|
||||
|
||||
pushStaged = pkgs.writeShellApplication {
|
||||
name = "push-staged";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.nix ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Replay staged ${imageName} arch closures from .parity-stage to the registry" "$@"
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would replay staged arch images under $PARITY_STAGE_DIR to ${imageName}:$VERSION-<arch>"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to push."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
creds="$PARITY_REGISTRY_OWNER:$tok"
|
||||
shopt -s nullglob
|
||||
for d in "$PARITY_STAGE_DIR"/*/; do
|
||||
[ -f "$d/copy-to-path" ] || continue
|
||||
arch="$(basename "$d")"
|
||||
copyto="$(cat "$d/copy-to-path")"
|
||||
"$copyto/bin/copy-to" --dest-creds "$creds" "docker://${imageName}:$VERSION-$arch"
|
||||
echo "Replayed ${imageName}:$VERSION-$arch"
|
||||
done
|
||||
'';
|
||||
};
|
||||
in
|
||||
archStageApps
|
||||
// archPublishApps
|
||||
// {
|
||||
"publish-index" = mkApp indexApp "Assemble the multi-arch index ${imageName}:<TAG> from pushed per-arch tags (fail-closed).";
|
||||
"publish" = mkApp publishAll "Publish all local arches + index; :latest = digest copy of :TAG (last mutation).";
|
||||
"push-staged" = mkApp pushStaged "Replay staged ${imageName} arch closures from .parity-stage to the registry.";
|
||||
};
|
||||
|
||||
# =========================================================================
|
||||
# Shared push-staged for single-artifact archetypes (pypi/npm/generic): replay
|
||||
# the staged file from .parity-stage to the registry without rebuilding.
|
||||
# =========================================================================
|
||||
mkPushStagedGeneric =
|
||||
{
|
||||
pname,
|
||||
arch,
|
||||
kind,
|
||||
version ? "",
|
||||
assetName ? "",
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
# The kind is known at Nix-eval time, so the kind-specific replay body is
|
||||
# selected here (not via a runtime `case` on a constant, which trips
|
||||
# shellcheck SC2194).
|
||||
kindBody = {
|
||||
pypi = ''
|
||||
whl="$(find "$d" -name '*.whl' | head -1)"
|
||||
sha="$(sha256sum "$whl" | cut -d' ' -f1)"
|
||||
http="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
||||
-H "Authorization: token $tok" -F ':action=file_upload' \
|
||||
-F 'protocol_version=1' -F "sha256_digest=$sha" \
|
||||
-F "name=${pname}" -F "version=$VERSION" -F 'filetype=bdist_wheel' \
|
||||
-F "content=@$whl" \
|
||||
"https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/pypi")"
|
||||
case "$http" in
|
||||
200 | 201 | 409) echo "Replayed ${pname} $VERSION (HTTP $http)" ;;
|
||||
*) echo "BLOCKER(upload-failed): HTTP $http" >&2; exit 1 ;;
|
||||
esac
|
||||
'';
|
||||
generic = ''
|
||||
http="$(curl -s -o /dev/null -w '%{http_code}' -X PUT \
|
||||
-H "Authorization: token $tok" --upload-file "$d/${assetName}" \
|
||||
"https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/generic/${pname}/$VERSION/${assetName}")"
|
||||
case "$http" in
|
||||
201 | 409) echo "Replayed ${assetName} $VERSION (HTTP $http)" ;;
|
||||
*) echo "BLOCKER(upload-failed): HTTP $http" >&2; exit 1 ;;
|
||||
esac
|
||||
'';
|
||||
npm = ''
|
||||
reg="https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/"
|
||||
(
|
||||
cd "$d"
|
||||
set +e
|
||||
out="$(npm publish --registry="$reg" "--//$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/:_authToken=$tok" 2>&1)"
|
||||
rc=$?
|
||||
set -e
|
||||
printf '%s\n' "$out" | sed "s/$tok/***REDACTED***/g"
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
if printf '%s' "$out" | grep -qiE 'already exists|409|conflict'; then
|
||||
echo "Already published — idempotent."
|
||||
exit 0
|
||||
fi
|
||||
exit "$rc"
|
||||
fi
|
||||
)
|
||||
'';
|
||||
};
|
||||
app = pkgs.writeShellApplication {
|
||||
name = "push-staged";
|
||||
runtimeInputs = baseInputs ++ lib.optional (kind == "npm") pkgs.nodejs;
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Replay the staged ${pname} ${arch} artifact (${kind}) from .parity-stage to the registry" "$@"
|
||||
d="$(parity_stage_path ${lib.escapeShellArg arch})"
|
||||
if [ ! -d "$d" ]; then
|
||||
echo "BLOCKER(no-stage): nothing staged at $d — run 'nix run .#stage-${arch}' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION="$(parity_derive_version ${lib.escapeShellArg version})"
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would replay $d (${kind}) -> https://$PARITY_REGISTRY_HOST (${pname} $VERSION)"
|
||||
echo "[dry-run] re-run with --publish / PUBLISH=1 to push."
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
${kindBody.${kind}}
|
||||
'';
|
||||
};
|
||||
in
|
||||
mkApp app "Replay the staged ${pname} ${arch} artifact (${kind}) from .parity-stage (dry-run by default; --publish to push).";
|
||||
|
||||
mkPushStagedHelm =
|
||||
{
|
||||
pname,
|
||||
ociRepo,
|
||||
registryHost ? "git.oleks.space",
|
||||
registryOwner ? "oleks",
|
||||
passEntry ? "infra/gitea/personal_access_token_packages_rw",
|
||||
}:
|
||||
let
|
||||
head = preamble { inherit registryHost registryOwner passEntry; };
|
||||
app = pkgs.writeShellApplication {
|
||||
name = "push-staged";
|
||||
runtimeInputs = baseInputs ++ [ pkgs.kubernetes-helm ];
|
||||
text = ''
|
||||
${head}
|
||||
parity_parse_args "Replay the staged ${pname} helm chart from .parity-stage to ${ociRepo}" "$@"
|
||||
d="$(parity_stage_path chart)"
|
||||
tgz="$(find "$d" -name '*.tgz' 2>/dev/null | head -1 || true)"
|
||||
if [ -z "$tgz" ]; then
|
||||
echo "BLOCKER(no-stage): no staged chart under $d — run 'nix run .#stage-chart' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$PARITY_PUBLISH" != "1" ]; then
|
||||
echo "[dry-run] would 'helm push $(basename "$tgz")' -> ${ociRepo}"
|
||||
exit 0
|
||||
fi
|
||||
parity_devtag_guard
|
||||
tok="$(parity_resolve_token)"
|
||||
parity_registry_preflight
|
||||
echo "$tok" | helm registry login "$PARITY_REGISTRY_HOST" -u "$PARITY_REGISTRY_OWNER" --password-stdin >/dev/null 2>&1
|
||||
helm push "$tgz" ${lib.escapeShellArg ociRepo}
|
||||
echo "Replayed chart $(basename "$tgz")"
|
||||
'';
|
||||
};
|
||||
in
|
||||
mkApp app "Replay the staged ${pname} helm chart from .parity-stage (dry-run by default; --publish to push).";
|
||||
|
||||
in
|
||||
{
|
||||
inherit
|
||||
mkPyPiWheelPublish
|
||||
mkS390xNpmPublish
|
||||
mkGenericBinaryPublish
|
||||
mkNix2ContainerPublish
|
||||
mkGoBinaryPublish
|
||||
mkHelmPublish
|
||||
;
|
||||
# Shared building blocks, exposed for advanced/bespoke consumers.
|
||||
inherit mkApp shellLib;
|
||||
}
|
||||
Reference in New Issue
Block a user