Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b80361b788 | |||
| 44d6ba9c8d | |||
| 38bb59e3d0 | |||
| bf36991833 | |||
| d1f8297c46 | |||
| 63f17f167d | |||
| 984a239311 |
@@ -17,4 +17,4 @@ steps:
|
|||||||
from_secret: ci_drift_token
|
from_secret: ci_drift_token
|
||||||
commands:
|
commands:
|
||||||
- echo "▸ arch=$(uname -m)"
|
- echo "▸ arch=$(uname -m)"
|
||||||
- nix shell nixpkgs#python3 nixpkgs#python3Packages.pyyaml -c python3 drift_check.py
|
- "nix-shell -p 'python3.withPackages (ps: [ ps.pyyaml ])' --run 'python3 drift_check.py'"
|
||||||
|
|||||||
+5
-1
@@ -67,7 +67,11 @@ build)
|
|||||||
push_flag="--push"
|
push_flag="--push"
|
||||||
fi
|
fi
|
||||||
echo "▸ building ${IMAGE}:${tag} for linux/${arch}"
|
echo "▸ building ${IMAGE}:${tag} for linux/${arch}"
|
||||||
docker buildx build --platform "linux/${arch}" "${push_flag}" -t "${IMAGE}:${tag}" .
|
# --provenance=false --sbom=false: see entrypoint.sh buildx_build_flow —
|
||||||
|
# without these, BuildKit pushes even a single-platform build as a
|
||||||
|
# manifest list, which `docker manifest create` then refuses to
|
||||||
|
# reference as a member (oleks/ci-scripts#15 real bug, found in CI).
|
||||||
|
docker buildx build --platform "linux/${arch}" --provenance=false --sbom=false "${push_flag}" -t "${IMAGE}:${tag}" .
|
||||||
;;
|
;;
|
||||||
manifest)
|
manifest)
|
||||||
registry_login
|
registry_login
|
||||||
|
|||||||
+45
-3
@@ -29,6 +29,13 @@
|
|||||||
# manifest step) using the SAME plugin image. Split into `buildx-build` /
|
# manifest step) using the SAME plugin image. Split into `buildx-build` /
|
||||||
# `buildx-manifest` instead; wiki updated to match.
|
# `buildx-manifest` instead; wiki updated to match.
|
||||||
#
|
#
|
||||||
|
# Version resolution for buildx-build/buildx-manifest (oleks/ci-scripts#21,
|
||||||
|
# see resolve_version() below): defaults to the raw CI_COMMIT_TAG (or an
|
||||||
|
# explicit `version:` setting, or "latest") — unchanged from before. A repo
|
||||||
|
# whose own build script normalizes tags (strips a leading "v" and a
|
||||||
|
# trailing "-<build-number>") can opt in with `tag_strip_v: "true"` to
|
||||||
|
# reproduce that exactly; default is off.
|
||||||
|
#
|
||||||
# See wiki Spec/Modern-CI-Target and Spec/Fleet Publish Plugin System.
|
# See wiki Spec/Modern-CI-Target and Spec/Fleet Publish Plugin System.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -67,6 +74,33 @@ render_tag() {
|
|||||||
echo "$scheme"
|
echo "$scheme"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolve_version() {
|
||||||
|
# resolve_version — the string substituted for {version} in tag_scheme,
|
||||||
|
# used by both buildx_build_flow and buildx_manifest_flow so the two
|
||||||
|
# stay in lockstep (oleks/ci-scripts#21).
|
||||||
|
#
|
||||||
|
# Precedence: an explicit `version:` setting (PLUGIN_VERSION) wins over
|
||||||
|
# the tag the pipeline triggered on (CI_COMMIT_TAG), else "latest".
|
||||||
|
# Nothing currently sets PLUGIN_VERSION (audited across every repo using
|
||||||
|
# this plugin), so this is a no-op precedence change for every existing
|
||||||
|
# consumer today.
|
||||||
|
#
|
||||||
|
# `tag_strip_v: "true"` (PLUGIN_TAG_STRIP_V) additionally strips a
|
||||||
|
# leading "v" and a trailing "-<build-number>" suffix, e.g. "v0.22.7-4"
|
||||||
|
# -> "0.22.7" — the normalization xonsh-image's (and csi-s3's) ci/local.sh
|
||||||
|
# already does before tagging. Default is "false": preserves the raw
|
||||||
|
# tag verbatim, which is what ci-scripts' own self-build (ci/local.sh,
|
||||||
|
# this plugin's own v1.0.x tags) and every other current consumer of
|
||||||
|
# this plugin already expect. Opt in per-repo via `tag_strip_v: "true"`
|
||||||
|
# in `settings:` — do not flip the default.
|
||||||
|
local version="${PLUGIN_VERSION:-${CI_COMMIT_TAG:-latest}}"
|
||||||
|
if [ "${PLUGIN_TAG_STRIP_V:-false}" = "true" ]; then
|
||||||
|
version="${version#v}"
|
||||||
|
version="${version%-[0-9]*}"
|
||||||
|
fi
|
||||||
|
printf '%s' "$version"
|
||||||
|
}
|
||||||
|
|
||||||
# --- attic watch-store lifecycle: explicit non-masking shutdown, never `|| true` ---
|
# --- attic watch-store lifecycle: explicit non-masking shutdown, never `|| true` ---
|
||||||
ATTIC_PID=""
|
ATTIC_PID=""
|
||||||
|
|
||||||
@@ -130,12 +164,19 @@ buildx_build_flow() {
|
|||||||
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
||||||
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
||||||
fi
|
fi
|
||||||
local version="${CI_COMMIT_TAG:-${PLUGIN_VERSION:-latest}}"
|
local version
|
||||||
|
version=$(resolve_version)
|
||||||
local tag
|
local tag
|
||||||
tag=$(render_tag "$tag_scheme" "$version" "$arch")
|
tag=$(render_tag "$tag_scheme" "$version" "$arch")
|
||||||
local context="${PLUGIN_CONTEXT:-.}"
|
local context="${PLUGIN_CONTEXT:-.}"
|
||||||
echo "▸ building ${PLUGIN_IMAGE_NAME}:${tag} for linux/${arch} (context ${context})"
|
echo "▸ building ${PLUGIN_IMAGE_NAME}:${tag} for linux/${arch} (context ${context})"
|
||||||
docker buildx build --push --platform "linux/${arch}" -t "${PLUGIN_IMAGE_NAME}:${tag}" "${context}"
|
# --provenance=false --sbom=false: BuildKit >=0.11 attaches attestations
|
||||||
|
# by default, which forces even a single-platform build to push as an
|
||||||
|
# OCI manifest LIST (index) rather than a plain image manifest. That
|
||||||
|
# breaks `docker manifest create` downstream (buildx-manifest flow),
|
||||||
|
# which refuses to add a manifest-list as a member. Disable both so the
|
||||||
|
# per-arch tag is a plain image manifest.
|
||||||
|
docker buildx build --push --provenance=false --sbom=false --platform "linux/${arch}" -t "${PLUGIN_IMAGE_NAME}:${tag}" "${context}"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildx_manifest_flow() {
|
buildx_manifest_flow() {
|
||||||
@@ -145,7 +186,8 @@ buildx_manifest_flow() {
|
|||||||
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
||||||
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
||||||
fi
|
fi
|
||||||
local version="${CI_COMMIT_TAG:-${PLUGIN_VERSION:-latest}}"
|
local version
|
||||||
|
version=$(resolve_version)
|
||||||
local dest_tags="${PLUGIN_DEST_TAGS:-${version},latest}"
|
local dest_tags="${PLUGIN_DEST_TAGS:-${version},latest}"
|
||||||
|
|
||||||
IFS=',' read -ra platforms <<<"$PLUGIN_PLATFORMS"
|
IFS=',' read -ra platforms <<<"$PLUGIN_PLATFORMS"
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# GENERATED by oleks/ci-scripts — do not hand-edit.
|
||||||
|
# Source: oleks/ci-scripts manifests/ondemand-release-amd64.yaml + generator/render_ondemand.py (oleks/ci-scripts#25).
|
||||||
|
# Re-render with: python3 generator/render_ondemand.py --out <repo>
|
||||||
|
|
||||||
|
labels:
|
||||||
|
arch: amd64
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1) ESTIMATE + ORDER + PROVIDE (oleks/ci-scripts#25): size the build, order
|
||||||
|
# an on-demand EphemeralBuilder of class ci, wait for the node to join.
|
||||||
|
- name: provision
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
|
commands:
|
||||||
|
- echo "▸ arch=$(uname -m)"
|
||||||
|
- ATTR=".#release"
|
||||||
|
- SYS="x86_64-linux"
|
||||||
|
- echo "▸ estimating sizing for $ATTR ($SYS)"
|
||||||
|
- nix-estimate "$ATTR" --system "$SYS" --json > estimate.json
|
||||||
|
- jq -e ".sizing.version == 1" estimate.json > /dev/null
|
||||||
|
- SIZING=$(jq -c ".sizing" estimate.json)
|
||||||
|
- echo "▸ sizing: $SIZING"
|
||||||
|
- echo "▸ ordering on-demand builder(s) (class ci)"
|
||||||
|
- nix-estimate "$ATTR" --system "$SYS" --provision ci | kubectl apply -f - -o name | tee ordered-builders.txt
|
||||||
|
- echo "▸ waiting for ordered builder(s) to become Ready"
|
||||||
|
- while read -r cr; do echo "▸ waiting $cr"; kubectl wait --for=condition=Ready "$cr" --timeout=1200s; done < ordered-builders.txt
|
||||||
|
|
||||||
|
# 2) BUILD on the freshly-joined ephemeral node. Pinned via nodeSelector; the
|
||||||
|
# existing k8s-backend Woodpecker agent schedules this step-pod there.
|
||||||
|
- name: build
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
|
depends_on:
|
||||||
|
- provision
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: amd64
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: 8Gi
|
||||||
|
limits:
|
||||||
|
memory: 32Gi
|
||||||
|
commands:
|
||||||
|
- echo "▸ arch=$(uname -m)"
|
||||||
|
- nixos-ci-entrypoint true
|
||||||
|
- PUBLISH=1 nix run .#publish
|
||||||
|
|
||||||
|
# 3) REAP: delete the ordered CR(s) to tear the node down at end of window.
|
||||||
|
# runs_on success+failure so a failed build never strands a paid node.
|
||||||
|
- name: reap
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
runs_on:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
commands:
|
||||||
|
- echo "▸ arch=$(uname -m)"
|
||||||
|
- if [ -f ordered-builders.txt ]; then while read -r cr; do echo "▸ reaping $cr"; kubectl delete "$cr" --ignore-not-found; done < ordered-builders.txt; else echo "▸ no ordered-builders.txt; nothing to reap"; fi
|
||||||
+33
-1
@@ -19,6 +19,38 @@ HEADER = """# GENERATED by oleks/ci-scripts — do not hand-edit.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def is_no_build(repo: dict) -> bool:
|
||||||
|
"""s390x = no-build: s390x hardware is currently unavailable, so those
|
||||||
|
wheels are not built/published (the repo code is kept as a cross-compile
|
||||||
|
reference only). Any manifest entry may opt in/out with an explicit
|
||||||
|
`no_build:` flag; otherwise entries under `s390x/` default to no-build.
|
||||||
|
See the ci-scripts wiki Spec/* pages and memory feedback-no-s390x-builds."""
|
||||||
|
return repo.get("no_build", str(repo.get("dir", "")).startswith("s390x/"))
|
||||||
|
|
||||||
|
|
||||||
|
def render_no_build(repo: dict) -> str:
|
||||||
|
"""Render a neutralized pipeline that does NOT build. Uses `event: manual`
|
||||||
|
so tag pushes never auto-trigger a build; a manual run just prints why."""
|
||||||
|
arch = repo.get("arch", "amd64")
|
||||||
|
return f"""{HEADER}# s390x = NO-BUILD: s390x hardware is currently unavailable, so this wheel is
|
||||||
|
# not built or published — the repo is kept as a cross-compile reference only.
|
||||||
|
# Re-enable by setting `no_build: false` on this repo in manifest.yaml.
|
||||||
|
labels:
|
||||||
|
arch: {arch}
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: manual
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-disabled
|
||||||
|
image: alpine:3
|
||||||
|
commands:
|
||||||
|
- echo "s390x build disabled — hardware currently unavailable."
|
||||||
|
- echo "Repo kept as a cross-compile reference only; not built/published."
|
||||||
|
- echo "See oleks/ci-scripts wiki Spec/* and memory feedback-no-s390x-builds."
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def render(repo: dict) -> str:
|
def render(repo: dict) -> str:
|
||||||
arch = repo["arch"]
|
arch = repo["arch"]
|
||||||
mem_req = repo["memory_request"]
|
mem_req = repo["memory_request"]
|
||||||
@@ -246,7 +278,7 @@ def main():
|
|||||||
]
|
]
|
||||||
for repo, render_fn in jobs:
|
for repo, render_fn in jobs:
|
||||||
target = BUILDING / repo["dir"] / ".woodpecker.yaml"
|
target = BUILDING / repo["dir"] / ".woodpecker.yaml"
|
||||||
rendered = render_fn(repo)
|
rendered = render_no_build(repo) if is_no_build(repo) else render_fn(repo)
|
||||||
if check_only:
|
if check_only:
|
||||||
current = target.read_text() if target.exists() else ""
|
current = target.read_text() if target.exists() else ""
|
||||||
if current != rendered:
|
if current != rendered:
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render the estimator-driven on-demand CI pipeline (oleks/ci-scripts#25).
|
||||||
|
|
||||||
|
FROZEN CONTRACT — estimate -> order -> provide -> build -> reap. The generated
|
||||||
|
pipeline's pre-step drives the nix-estimator + builder-arbitrage on-demand
|
||||||
|
window (estimate the sizing, order an EphemeralBuilder of class `ci`, wait for
|
||||||
|
the node to join, build pinned to it, then reap the CR) so a heavy release
|
||||||
|
build gets a fresh, right-sized cloud node that is online only for the window.
|
||||||
|
|
||||||
|
SHAPE (`kind: ondemand` in manifests/*.yaml):
|
||||||
|
1. provision pre-step:
|
||||||
|
- `nix-estimate <attr> --system <sys> --json` -> parse the `sizing`
|
||||||
|
block (version-pinned; the pre-step asserts sizing.version == 1);
|
||||||
|
- `nix-estimate <attr> --system <sys> --provision ci | kubectl apply -f -`
|
||||||
|
emits N EphemeralBuilder docs (class: ci) and applies them, capturing
|
||||||
|
the applied CR names into a workspace file;
|
||||||
|
- `kubectl wait --for=condition=Ready ephemeralbuilder/<name>` on each
|
||||||
|
ordered CR until the backing node has joined and is serving.
|
||||||
|
2. build step: the real build, pinned to the freshly-joined node via
|
||||||
|
`backend_options.kubernetes.nodeSelector` (Option B / plain-worker MVP —
|
||||||
|
the ephemeral node joins armer k3s as a plain `kubernetes.io/arch=<arch>`
|
||||||
|
worker and the EXISTING k8s-backend Woodpecker agent schedules this
|
||||||
|
amd64 step-pod onto it; no per-node Woodpecker agent is baked in).
|
||||||
|
3. reap step: `kubectl delete` every ordered CR (idle-park would also reap on
|
||||||
|
`parking.policy: cost`, but an explicit delete tears the node down at the
|
||||||
|
end of the window). Runs on `success` AND `failure` so a failed build
|
||||||
|
never strands a paid node.
|
||||||
|
|
||||||
|
Convention notes (match generate.py / render_thin_*):
|
||||||
|
- GENERATED header is emitted verbatim; edit the manifest + re-render.
|
||||||
|
- Every step's first command is `echo "▸ arch=$(uname -m)"` (Woodpecker
|
||||||
|
arch banner) — kept, never dropped.
|
||||||
|
- No `|| true`: the reap uses `kubectl delete --ignore-not-found` (a CR the
|
||||||
|
idle-park already reaped is not an error) rather than masking failures.
|
||||||
|
|
||||||
|
gVisor / PodGater caveat (out of scope for #25, noted for the follow-up): the
|
||||||
|
build step-pod is spawned by the shared Woodpecker k8s-backend agent, so it is
|
||||||
|
NOT stamped by the builder-arbitrage operator's PodGater. A Kyverno backstop on
|
||||||
|
the ci node is the follow-up that enforces the gVisor runtime class there.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 generator/render_ondemand.py --out /path/to/scratch
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).parent.parent
|
||||||
|
|
||||||
|
GENERATED_HEADER = (
|
||||||
|
"# GENERATED by oleks/ci-scripts — do not hand-edit.\n"
|
||||||
|
"# Source: oleks/ci-scripts manifests/{repo}.yaml + "
|
||||||
|
"generator/render_ondemand.py (oleks/ci-scripts#25).\n"
|
||||||
|
"# Re-render with: python3 generator/render_ondemand.py --out <repo>\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default images. The orchestrator step needs `nix-estimate` + `kubectl` + `jq`
|
||||||
|
# on PATH; the build step is the repo's own build image. Both are manifest
|
||||||
|
# overridable (orchestrator_image / build_image).
|
||||||
|
DEFAULT_ORCHESTRATOR_IMAGE = "git.oleks.space/oleks/nix-ci:latest"
|
||||||
|
DEFAULT_BUILD_IMAGE = "git.oleks.space/oleks/nix-ci:latest"
|
||||||
|
|
||||||
|
# nix-estimate is invoked verbatim per the contract; the binary name is
|
||||||
|
# manifest-overridable (e.g. a `nix run <flake>#nix-estimate --` prefix) but
|
||||||
|
# defaults to the bare CLI assumed present in the orchestrator image.
|
||||||
|
DEFAULT_ESTIMATE_CMD = "nix-estimate"
|
||||||
|
DEFAULT_CLASS = "ci"
|
||||||
|
DEFAULT_WAIT_TIMEOUT = "1200s"
|
||||||
|
# Workspace-relative file the provision step writes the applied CR names into;
|
||||||
|
# read back by the reap step (Woodpecker mounts the workspace across steps,
|
||||||
|
# so this is how the ordered set crosses the step boundary).
|
||||||
|
ORDERED_FILE = "ordered-builders.txt"
|
||||||
|
|
||||||
|
|
||||||
|
def _when_block(manifest: dict) -> str:
|
||||||
|
"""The `when:` trigger. Defaults to the release-tag shape every other
|
||||||
|
renderer here uses; a manifest may instead pin `push` branches."""
|
||||||
|
branches = manifest.get("branches")
|
||||||
|
if branches:
|
||||||
|
joined = "[" + ", ".join(branches) + "]"
|
||||||
|
return f"when:\n - event: push\n branch: {joined}\n"
|
||||||
|
ref = manifest.get("tag_ref", "refs/tags/v*")
|
||||||
|
return f'when:\n - event: tag\n ref: "{ref}"\n'
|
||||||
|
|
||||||
|
|
||||||
|
def _provision_commands(manifest: dict) -> str:
|
||||||
|
estimate_cmd = manifest.get("estimate_cmd", DEFAULT_ESTIMATE_CMD)
|
||||||
|
attr = manifest["target"]
|
||||||
|
system = manifest["system"]
|
||||||
|
cls = manifest.get("provision_class", DEFAULT_CLASS)
|
||||||
|
wait_timeout = manifest.get("wait_timeout", DEFAULT_WAIT_TIMEOUT)
|
||||||
|
|
||||||
|
# Each list item is one command; Woodpecker concatenates a step's commands
|
||||||
|
# into a single `set -e` shell script, so vars set here persist within the
|
||||||
|
# step. Kept shellcheck-clean (quoted expansions, `read -r`).
|
||||||
|
lines = [
|
||||||
|
'echo "▸ arch=$(uname -m)"',
|
||||||
|
f'ATTR="{attr}"',
|
||||||
|
f'SYS="{system}"',
|
||||||
|
'echo "▸ estimating sizing for $ATTR ($SYS)"',
|
||||||
|
f'{estimate_cmd} "$ATTR" --system "$SYS" --json > estimate.json',
|
||||||
|
# Assert the versioned sizing block the contract froze (schema v1);
|
||||||
|
# -e makes jq exit non-zero (failing the step) on a false/absent match.
|
||||||
|
'jq -e ".sizing.version == 1" estimate.json > /dev/null',
|
||||||
|
'SIZING=$(jq -c ".sizing" estimate.json)',
|
||||||
|
'echo "▸ sizing: $SIZING"',
|
||||||
|
'echo "▸ ordering on-demand builder(s) (class ' + cls + ')"',
|
||||||
|
# The provision path emits N EphemeralBuilder docs; -o name echoes the
|
||||||
|
# applied CR identities (ephemeralbuilder.arbitrage.oleks.space/<name>)
|
||||||
|
# which we persist for the wait + reap.
|
||||||
|
f'{estimate_cmd} "$ATTR" --system "$SYS" --provision {cls} '
|
||||||
|
f"| kubectl apply -f - -o name | tee {ORDERED_FILE}",
|
||||||
|
'echo "▸ waiting for ordered builder(s) to become Ready"',
|
||||||
|
f'while read -r cr; do echo "▸ waiting $cr"; '
|
||||||
|
f'kubectl wait --for=condition=Ready "$cr" --timeout={wait_timeout}; '
|
||||||
|
f"done < {ORDERED_FILE}",
|
||||||
|
]
|
||||||
|
return "\n".join(f" - {line}" for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _reap_commands(manifest: dict) -> str:
|
||||||
|
lines = [
|
||||||
|
'echo "▸ arch=$(uname -m)"',
|
||||||
|
# --ignore-not-found: a CR the idle-park (parking.policy: cost) already
|
||||||
|
# reaped is not an error — this is the no-`|| true` equivalent.
|
||||||
|
f"if [ -f {ORDERED_FILE} ]; then "
|
||||||
|
f'while read -r cr; do echo "▸ reaping $cr"; '
|
||||||
|
f'kubectl delete "$cr" --ignore-not-found; '
|
||||||
|
f"done < {ORDERED_FILE}; "
|
||||||
|
f'else echo "▸ no {ORDERED_FILE}; nothing to reap"; fi',
|
||||||
|
]
|
||||||
|
return "\n".join(f" - {line}" for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_commands(manifest: dict) -> str:
|
||||||
|
run = manifest["build_run"]
|
||||||
|
lines = ['echo "▸ arch=$(uname -m)"']
|
||||||
|
lines += [line for line in run.rstrip("\n").splitlines()]
|
||||||
|
return "\n".join(f" - {line}" for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _kube_env_block(manifest: dict) -> str:
|
||||||
|
"""Optional scoped-kubeconfig secret (emmett consumes the fleet via a
|
||||||
|
scoped SA kubeconfig). Absent -> rely on the in-cluster ServiceAccount."""
|
||||||
|
secret = manifest.get("kubeconfig_secret")
|
||||||
|
if not secret:
|
||||||
|
return ""
|
||||||
|
return (
|
||||||
|
f" environment:\n KUBECONFIG_CONTENT:\n from_secret: {secret}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _memory_block(manifest: dict, indent: str) -> str:
|
||||||
|
mem_req = manifest.get("memory_request")
|
||||||
|
mem_lim = manifest.get("memory_limit")
|
||||||
|
if not (mem_req or mem_lim):
|
||||||
|
return ""
|
||||||
|
req = mem_req or mem_lim
|
||||||
|
lim = mem_lim or mem_req
|
||||||
|
return (
|
||||||
|
f"{indent}resources:\n"
|
||||||
|
f"{indent} requests:\n"
|
||||||
|
f"{indent} memory: {req}\n"
|
||||||
|
f"{indent} limits:\n"
|
||||||
|
f"{indent} memory: {lim}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render(manifest: dict) -> str:
|
||||||
|
repo = manifest["repo"]
|
||||||
|
arch = manifest["arch"]
|
||||||
|
orchestrator_image = manifest.get("orchestrator_image", DEFAULT_ORCHESTRATOR_IMAGE)
|
||||||
|
build_image = manifest.get("build_image", DEFAULT_BUILD_IMAGE)
|
||||||
|
|
||||||
|
header = GENERATED_HEADER.format(repo=repo)
|
||||||
|
when_block = _when_block(manifest)
|
||||||
|
provision_cmds = _provision_commands(manifest)
|
||||||
|
build_cmds = _build_commands(manifest)
|
||||||
|
reap_cmds = _reap_commands(manifest)
|
||||||
|
kube_env = _kube_env_block(manifest)
|
||||||
|
# The build step pins to the freshly-joined node via nodeSelector (Option B
|
||||||
|
# plain-worker: kubernetes.io/arch=<arch>). Any extra manifest node_selector
|
||||||
|
# keys are merged in (e.g. a per-order node label once the operator stamps
|
||||||
|
# one — reserved, not required for the MVP).
|
||||||
|
node_selector = {"kubernetes.io/arch": arch}
|
||||||
|
node_selector.update(manifest.get("node_selector", {}))
|
||||||
|
node_selector_lines = "\n".join(
|
||||||
|
f" {k}: {v}" for k, v in node_selector.items()
|
||||||
|
)
|
||||||
|
build_mem = _memory_block(manifest, " ")
|
||||||
|
|
||||||
|
# est_makespan_min echo helps derive the on-demand window; parking/maxDuration
|
||||||
|
# themselves are set by the estimator's --provision path, not here.
|
||||||
|
return f"""{header}labels:
|
||||||
|
arch: {arch}
|
||||||
|
|
||||||
|
{when_block}
|
||||||
|
steps:
|
||||||
|
# 1) ESTIMATE + ORDER + PROVIDE (oleks/ci-scripts#25): size the build, order
|
||||||
|
# an on-demand EphemeralBuilder of class ci, wait for the node to join.
|
||||||
|
- name: provision
|
||||||
|
image: {orchestrator_image}
|
||||||
|
{kube_env} commands:
|
||||||
|
{provision_cmds}
|
||||||
|
|
||||||
|
# 2) BUILD on the freshly-joined ephemeral node. Pinned via nodeSelector; the
|
||||||
|
# existing k8s-backend Woodpecker agent schedules this step-pod there.
|
||||||
|
- name: build
|
||||||
|
image: {build_image}
|
||||||
|
depends_on:
|
||||||
|
- provision
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
{node_selector_lines}
|
||||||
|
{build_mem} commands:
|
||||||
|
{build_cmds}
|
||||||
|
|
||||||
|
# 3) REAP: delete the ordered CR(s) to tear the node down at end of window.
|
||||||
|
# runs_on success+failure so a failed build never strands a paid node.
|
||||||
|
- name: reap
|
||||||
|
image: {orchestrator_image}
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
runs_on:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
{kube_env} commands:
|
||||||
|
{reap_cmds}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_makespan_hint(est_makespan_min: float) -> int:
|
||||||
|
"""maxDuration = ceil(makespan * 1.5) minutes — mirrors the estimator's
|
||||||
|
--provision derivation so the two stay legible together. Provided as a
|
||||||
|
pure helper for tests/docs; the generated pipeline does not set it (the
|
||||||
|
estimator's --provision path stamps requirements.maxDuration itself)."""
|
||||||
|
return math.ceil(est_makespan_min * 1.5)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--out", type=pathlib.Path, required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
manifests_dir = HERE / "manifests"
|
||||||
|
wrote_any = False
|
||||||
|
for path in sorted(manifests_dir.glob("*.yaml")):
|
||||||
|
manifest = yaml.safe_load(path.read_text())
|
||||||
|
if manifest.get("kind") != "ondemand":
|
||||||
|
continue
|
||||||
|
repo = manifest["repo"]
|
||||||
|
out_dir = args.out / repo
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
target = out_dir / ".woodpecker.yaml"
|
||||||
|
target.write_text(render(manifest))
|
||||||
|
print(f"wrote {target}")
|
||||||
|
wrote_any = True
|
||||||
|
if not wrote_any:
|
||||||
|
print("no ondemand manifests found (kind: ondemand)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render thin ci-fleet-publish-based pipelines for the fleet family
|
||||||
|
(oleks/ci-scripts#16): the 10 identical-fleet repos in manifest.yaml's
|
||||||
|
`repos:` list and the 5 thin-parity repos in `thin_repos:`.
|
||||||
|
|
||||||
|
This is a PREP/PLANNING script (per team-lead direction on #16/#17): it
|
||||||
|
renders into a local --out dir for diffing against the current generated
|
||||||
|
`.woodpecker.yaml` files. It does NOT write into ~/projects/building/ and is
|
||||||
|
not wired into generate.py's main() — that wiring (and the actual repo
|
||||||
|
migration) happens once the diffs are reviewed and the tag-scheme question
|
||||||
|
is settled.
|
||||||
|
|
||||||
|
Behavior-preservation approach: every piece of shell logic that generate.py's
|
||||||
|
render()/render_thin() used to inline (resolv.conf, nix.conf, attic
|
||||||
|
lifecycle, redis/sccache env, token remap, doctor gate) either maps to a
|
||||||
|
PLUGIN_* setting the plugin already implements (nameserver, cores, max_jobs,
|
||||||
|
cache), or gets folded into the `run:` command as an explicit shell
|
||||||
|
one-liner/block — the plugin's `publish` flow just execs `bash -lc
|
||||||
|
"$PLUGIN_RUN"`, so anything that was a `commands:` line before can still be
|
||||||
|
a line in `run:`.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 generator/render_thin_fleet.py --out /path/to/scratch
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).parent.parent
|
||||||
|
PLUGIN_IMAGE = "git.oleks.space/oleks/ci-fleet-publish:v1"
|
||||||
|
|
||||||
|
GENERATED_HEADER = (
|
||||||
|
"# GENERATED by oleks/ci-scripts — do not hand-edit.\n"
|
||||||
|
"# Source: oleks/ci-scripts manifest.yaml + generator/render_thin_fleet.py"
|
||||||
|
" (oleks/ci-scripts#16).\n"
|
||||||
|
"# Re-render with: python3 generator/render_thin_fleet.py --out <repo>\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
NAMESERVER = "169.254.20.10"
|
||||||
|
|
||||||
|
|
||||||
|
def render_identical(repo: dict) -> str:
|
||||||
|
"""The 10 `repos:` entries (oleks/ci-scripts#1 family)."""
|
||||||
|
attic = repo.get("attic", True)
|
||||||
|
redis_cache = repo.get("redis_cache", False)
|
||||||
|
cores = repo.get("cores")
|
||||||
|
max_jobs = repo.get("max_jobs", False)
|
||||||
|
|
||||||
|
run_lines = []
|
||||||
|
if redis_cache:
|
||||||
|
run_lines += [
|
||||||
|
"export SCCACHE_REDIS_ENDPOINT=tcp://redis.default.svc.cluster.local:6379",
|
||||||
|
"export SCCACHE_REDIS_PASSWORD=$REDIS_PASSWORD",
|
||||||
|
"export CCACHE_REMOTE_STORAGE=redis://redis.default.svc.cluster.local:6379",
|
||||||
|
"export CCACHE_REMOTE_ONLY=1",
|
||||||
|
]
|
||||||
|
run_lines.append("PUBLISH=1 nix run .#publish")
|
||||||
|
run_block = "\n".join(f" {line}" for line in run_lines)
|
||||||
|
|
||||||
|
settings_lines = [
|
||||||
|
f' run: |\n{run_block}',
|
||||||
|
f' cache: "{str(attic).lower()}"',
|
||||||
|
f' nameserver: "{NAMESERVER}"',
|
||||||
|
]
|
||||||
|
if max_jobs:
|
||||||
|
settings_lines.append(' max_jobs: "true"')
|
||||||
|
if cores:
|
||||||
|
settings_lines.append(f' cores: "{cores}"')
|
||||||
|
settings_block = "\n".join(settings_lines)
|
||||||
|
|
||||||
|
env_lines = [" GITEA_CLONE_TOKEN:", " from_secret: gitea_clone_token"]
|
||||||
|
if attic:
|
||||||
|
env_lines += [" ATTIC_TOKEN:", " from_secret: attic_token"]
|
||||||
|
if redis_cache:
|
||||||
|
env_lines += [" REDIS_PASSWORD:", " from_secret: redis_password"]
|
||||||
|
env_block = "\n".join(env_lines)
|
||||||
|
|
||||||
|
return f"""{GENERATED_HEADER}labels:
|
||||||
|
arch: {repo["arch"]}
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: publish
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
{env_block}
|
||||||
|
settings:
|
||||||
|
{settings_block}
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: {repo["memory_request"]}
|
||||||
|
limits:
|
||||||
|
memory: {repo["memory_limit"]}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_thin_parity(repo: dict) -> str:
|
||||||
|
"""The 5 `thin_repos:` entries (oleks/ci-scripts#10 family)."""
|
||||||
|
doctor = repo.get("doctor", False)
|
||||||
|
token_direct = repo.get("token_direct", True)
|
||||||
|
publish_style = repo.get("publish_style", "publish1")
|
||||||
|
tags_full = repo.get("tags_full", False)
|
||||||
|
gitea_clone_token = repo.get("gitea_clone_token", True)
|
||||||
|
|
||||||
|
run_lines = []
|
||||||
|
if doctor:
|
||||||
|
run_lines.append(
|
||||||
|
"nix run git+https://git.oleks.space/oleks/parity-lib#pipeline-doctor -- . --strict"
|
||||||
|
)
|
||||||
|
if not token_direct:
|
||||||
|
run_lines.append('export REGISTRY_TOKEN="$CI_REGISTRY_TOKEN"')
|
||||||
|
if publish_style == "publish1":
|
||||||
|
run_lines.append("PUBLISH=1 nix run .#publish")
|
||||||
|
else:
|
||||||
|
run_lines.append("nix run .#publish-s390x -- --publish")
|
||||||
|
run_block = "\n".join(f" {line}" for line in run_lines)
|
||||||
|
|
||||||
|
settings_lines = [
|
||||||
|
f' run: |\n{run_block}',
|
||||||
|
' cache: "false"', # oleks/ci-scripts#10: this family never used
|
||||||
|
# the inline attic accelerator (dead ATTIC_TOKEN in some repos) —
|
||||||
|
# cache: false preserves that, doesn't silently turn caching on.
|
||||||
|
f' nameserver: "{NAMESERVER}"',
|
||||||
|
]
|
||||||
|
settings_block = "\n".join(settings_lines)
|
||||||
|
|
||||||
|
env_lines = []
|
||||||
|
if gitea_clone_token:
|
||||||
|
env_lines += [" GITEA_CLONE_TOKEN:", " from_secret: gitea_clone_token"]
|
||||||
|
if token_direct:
|
||||||
|
env_lines += [" REGISTRY_TOKEN:", " from_secret: registry_token"]
|
||||||
|
else:
|
||||||
|
env_lines += [" CI_REGISTRY_TOKEN:", " from_secret: registry_token"]
|
||||||
|
env_block = "\n".join(env_lines) if env_lines else " {}"
|
||||||
|
|
||||||
|
# oleks/ci-scripts#10 dead_secrets (ATTIC_TOKEN/REDIS_PASSWORD declared
|
||||||
|
# but never referenced) are deliberately DROPPED here, not carried
|
||||||
|
# forward — they were unused in the underlying command either way.
|
||||||
|
|
||||||
|
clone_note = ""
|
||||||
|
if tags_full:
|
||||||
|
clone_note = (
|
||||||
|
"\n# NOTE: original PLUGIN_TAGS=true/PLUGIN_DEPTH=0 (full clone incl. tags)\n"
|
||||||
|
"# is a `clone:` block override the thin contract doesn't have a settings\n"
|
||||||
|
"# equivalent for yet — needs resolving before this repo migrates (see\n"
|
||||||
|
"# migration report).\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""{GENERATED_HEADER}labels:
|
||||||
|
arch: {repo["arch"]}
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
{clone_note}
|
||||||
|
steps:
|
||||||
|
- name: publish
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
{env_block}
|
||||||
|
settings:
|
||||||
|
{settings_block}
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: {repo["memory_request"]}
|
||||||
|
limits:
|
||||||
|
memory: {repo["memory_limit"]}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--out", type=pathlib.Path, required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
args.out.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
manifest = yaml.safe_load((HERE / "manifest.yaml").read_text())
|
||||||
|
for repo in manifest["repos"]:
|
||||||
|
name = pathlib.Path(repo["dir"]).name
|
||||||
|
target = args.out / f"{name}.woodpecker.yaml"
|
||||||
|
target.write_text(render_identical(repo))
|
||||||
|
print(f"wrote {target}")
|
||||||
|
for repo in manifest.get("thin_repos", []):
|
||||||
|
name = pathlib.Path(repo["dir"]).name
|
||||||
|
target = args.out / f"{name}.woodpecker.yaml"
|
||||||
|
target.write_text(render_thin_parity(repo))
|
||||||
|
print(f"wrote {target}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Render thin ci-fleet-publish-based pipelines for the quartet family
|
||||||
|
(oleks/ci-scripts#17, #19) — PREP/PLANNING script, not wired into anything yet.
|
||||||
|
|
||||||
|
SHAPES (from manifests/*.yaml, `kind:`):
|
||||||
|
- xonsh-image, csi-s3: kind: matrix — real buildx per-arch-build +
|
||||||
|
manifest-consolidate quartets. Maps onto the plugin's
|
||||||
|
buildx-build/buildx-manifest flow. (oleks/ci-scripts#17, done first.)
|
||||||
|
- the-eye, emdash.kotkanagrilli.fi: kind: single — one native-arch
|
||||||
|
`docker buildx` build via `ci/local.sh`, no manifest step (the-eye also
|
||||||
|
has a second `grpc` component, same shape, second step in the same
|
||||||
|
file). Maps onto the plugin's `flow: publish` — same single-step shape
|
||||||
|
render_thin_fleet.py uses for the fleet family.
|
||||||
|
- flake-hub, woodpecker-peek: kind: split — separate per-arch files
|
||||||
|
(amd64.yaml/arm64.yaml), `nix run .#publish[-<arch>]` against parity-lib's
|
||||||
|
attic-closure archetype, no docker buildx at all. Also maps onto
|
||||||
|
`flow: publish`, one thin step per arch file — per-arch file layout is
|
||||||
|
kept (NOT collapsed into one matrix file): the two arches build against
|
||||||
|
different node-bound PVCs and were already split for that reason (see
|
||||||
|
manifests/flake-hub.yaml header), a constraint the thin migration doesn't
|
||||||
|
change.
|
||||||
|
|
||||||
|
(oleks/ci-scripts#19 implements the single/split renderers below; the matrix
|
||||||
|
renderer was already done for #17.)
|
||||||
|
|
||||||
|
For xonsh-image/csi-s3, migrating to the plugin's buildx-build/buildx-manifest
|
||||||
|
flow means the plugin's generic `docker buildx build --push <context>`
|
||||||
|
(building the repo's own Dockerfile directly) REPLACES what `ci/local.sh
|
||||||
|
--arch/--manifest` used to do internally. This is a bigger behavioral swap
|
||||||
|
than the fleet family's migration (which just moves inline shell into
|
||||||
|
`run:`) — needs explicit confirmation that ci/local.sh's Dockerfile build is
|
||||||
|
the ONLY thing it does before this ships (see report).
|
||||||
|
|
||||||
|
`kind: matrix` manifests must set `image_name:` explicitly, verified against
|
||||||
|
the live Gitea package registry (oleks/ci-scripts#22). It is NEVER inferred
|
||||||
|
from the manifest's `repo:`/dir field — `xonsh-image`'s repo field and its
|
||||||
|
actually-published image (`oleks/xonsh`, confirmed live) genuinely differ;
|
||||||
|
inferring one from the other silently pushes a migrated pipeline to a
|
||||||
|
different, wrong (and possibly empty) image path.
|
||||||
|
|
||||||
|
For the-eye/emdash (single) and flake-hub/woodpecker-peek (split), the
|
||||||
|
existing pipelines already invoke a repo-owned script (`ci/local.sh`,
|
||||||
|
`nix run .#publish-*`) as their entire build logic — same shape as the fleet
|
||||||
|
family's `run:` migration, not the bigger buildx-quartet swap above.
|
||||||
|
|
||||||
|
Behavior-preservation notes (single/split):
|
||||||
|
- The manual `echo "▸ arch=$(uname -m)"` command line is dropped from
|
||||||
|
`run:` — entrypoint.sh already prints that banner unconditionally before
|
||||||
|
any flow runs (line ~35), so keeping it in `run:` too would just double
|
||||||
|
it. Same convention render_thin_fleet.py already uses.
|
||||||
|
- `clone:` step overrides are dropped, matching the precedent already set
|
||||||
|
for the fleet family (render_thin_fleet.py) and the matrix renderer
|
||||||
|
below: Woodpecker's implicit clone is judged equivalent for the
|
||||||
|
PLUGIN_TAGS=false/PLUGIN_DEPTH=1 shape all four of these repos use. None
|
||||||
|
of the 4 use the `tags_full` (full-history) shape render_thin_fleet.py
|
||||||
|
flagged as an unresolved gap — this repo set doesn't hit that gap.
|
||||||
|
- `backend_options.kubernetes.nodeSelector`/`resources`/`labels` (the
|
||||||
|
per-pod tracing labels: commit-branch/commit-sha/commit-tag/
|
||||||
|
pipeline-number) are NOT manifest-parametrized inputs today — render.py's
|
||||||
|
single/split templates hardcode them per component/leg. They are
|
||||||
|
preserved here as the same fixed shape, still populated from the
|
||||||
|
manifest's `arch`/leg key.
|
||||||
|
- ATTIC_TOKEN handling: split legs already have `attic_token` wired as a
|
||||||
|
plain `environment:` secret (consumed by nixos-ci-entrypoint's own
|
||||||
|
ATTIC_TOKEN handling today). The plugin's `attic_start`/`attic_stop`
|
||||||
|
(entrypoint.sh) already replaces that lifecycle when `cache: "true"` —
|
||||||
|
same mapping render_thin_fleet.py uses for the identical-repos family.
|
||||||
|
the-eye/emdash never reference ATTIC_TOKEN today, so they get
|
||||||
|
`cache: "false"`, not silently turned on.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 generator/render_thin_quartet.py --out /path/to/scratch
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
HERE = pathlib.Path(__file__).parent.parent
|
||||||
|
PLUGIN_IMAGE = "git.oleks.space/oleks/ci-fleet-publish:v1"
|
||||||
|
|
||||||
|
GENERATED_HEADER = (
|
||||||
|
"# GENERATED by oleks/ci-scripts — do not hand-edit.\n"
|
||||||
|
"# Source: oleks/ci-scripts manifests/{repo}.yaml + "
|
||||||
|
"generator/render_thin_quartet.py (oleks/ci-scripts#17, #19).\n"
|
||||||
|
"# Re-render with: python3 generator/render_thin_quartet.py --out <repo>/.woodpecker\n"
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_build(manifest: dict, arch: str, image_name: str) -> str:
|
||||||
|
node_selector = manifest.get("node_selector")
|
||||||
|
backend_options_block = ""
|
||||||
|
if node_selector:
|
||||||
|
lines = "\n".join(f" {k}: {v}" for k, v in node_selector.items())
|
||||||
|
backend_options_block = (
|
||||||
|
" backend_options:\n kubernetes:\n nodeSelector:\n"
|
||||||
|
+ lines
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
# oleks/ci-scripts#21: opt-in only — reproduces a repo's own ci/local.sh
|
||||||
|
# tag normalization (strip leading "v" + trailing "-N"). Default off
|
||||||
|
# (entrypoint.sh's resolve_version() default matches today's raw-tag
|
||||||
|
# behavior), set per-manifest via `tag_strip_v: true`.
|
||||||
|
tag_strip_v_line = (
|
||||||
|
' tag_strip_v: "true"\n' if manifest.get("tag_strip_v") else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return f"""{GENERATED_HEADER}labels:
|
||||||
|
arch: {arch}
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: build-{arch}
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
settings:
|
||||||
|
flow: buildx-build
|
||||||
|
image_name: "{image_name}"
|
||||||
|
tag_scheme: "{{version}}-{{arch}}"
|
||||||
|
{tag_strip_v_line}{backend_options_block}"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_manifest(manifest: dict, arches: list, image_name: str) -> str:
|
||||||
|
platforms = ",".join(arches)
|
||||||
|
tag_strip_v_line = (
|
||||||
|
' tag_strip_v: "true"\n' if manifest.get("tag_strip_v") else ""
|
||||||
|
)
|
||||||
|
return f"""{GENERATED_HEADER}when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
{chr(10).join(f" - build-{a}" for a in arches)}
|
||||||
|
|
||||||
|
# oleks/ci-scripts#9: run even if a per-arch build failed — the plugin's
|
||||||
|
# buildx-manifest flow itself probes the registry for which per-arch tags
|
||||||
|
# actually landed and builds the manifest from whatever exists.
|
||||||
|
runs_on:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: manifest
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
settings:
|
||||||
|
flow: buildx-manifest
|
||||||
|
image_name: "{image_name}"
|
||||||
|
platforms: "{platforms}"
|
||||||
|
tag_scheme: "{{version}}-{{arch}}"
|
||||||
|
{tag_strip_v_line}"""
|
||||||
|
|
||||||
|
|
||||||
|
def _comment_block(text: str, indent: str) -> str:
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
lines = text.rstrip("\n").splitlines()
|
||||||
|
return "".join(f"{indent}# {line}\n" if line else f"{indent}#\n" for line in lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_single_step(component: dict, arch: str) -> str:
|
||||||
|
"""One thin `flow: publish` step for a `kind: single` component
|
||||||
|
(the-eye's build-and-push / build-and-push-grpc, emdash's build-and-push).
|
||||||
|
Mirrors templates/single/COMPONENT_STEP.tmpl's fixed env/backend_options
|
||||||
|
shape, swapping the `commands:` block for a `settings: {{run, flow, cache}}`.
|
||||||
|
"""
|
||||||
|
command = f"./ci/local.sh --arch {arch}"
|
||||||
|
if component.get("component"):
|
||||||
|
command += f" --component {component['component']}"
|
||||||
|
step_comment = _comment_block(component.get("step_comment", ""), " ")
|
||||||
|
buildkit_comment = _comment_block(component.get("buildkit_comment", ""), " ")
|
||||||
|
version_comment = _comment_block(component.get("version_comment", ""), " ")
|
||||||
|
memory = component.get("memory", "4Gi")
|
||||||
|
buildkit_addr = f"tcp://buildkit-{arch}.infra.svc.cluster.local:1234"
|
||||||
|
|
||||||
|
return f"""{step_comment} - name: {component["name"]}
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
{buildkit_comment} BUILDKIT_ADDR: {buildkit_addr}
|
||||||
|
PUBLISH: "1"
|
||||||
|
BRANCH: "${{CI_COMMIT_BRANCH}}"
|
||||||
|
{version_comment} VERSION: "0.1.${{CI_PIPELINE_NUMBER}}"
|
||||||
|
settings:
|
||||||
|
run: |
|
||||||
|
{command}
|
||||||
|
flow: publish
|
||||||
|
cache: "false"
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: {arch}
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
memory: {memory}
|
||||||
|
limits:
|
||||||
|
memory: {memory}
|
||||||
|
labels:
|
||||||
|
commit-branch: "${{CI_COMMIT_BRANCH}}"
|
||||||
|
commit-sha: "${{CI_COMMIT_SHA}}"
|
||||||
|
pipeline-number: "${{CI_PIPELINE_NUMBER}}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_single(manifest: dict) -> dict:
|
||||||
|
"""`kind: single` (the-eye, emdash.kotkanagrilli.fi) → one file,
|
||||||
|
one-or-more thin `flow: publish` steps (the-eye has a second `grpc`
|
||||||
|
component alongside the web build)."""
|
||||||
|
arch = manifest["arch"]
|
||||||
|
branches = "[" + ", ".join(manifest["branches"]) + "]"
|
||||||
|
header = manifest["header"].rstrip("\n") + "\n"
|
||||||
|
label_lines = (
|
||||||
|
_comment_block(manifest.get("label_comment", ""), " ") + f" arch: {arch}"
|
||||||
|
)
|
||||||
|
|
||||||
|
steps = "\n".join(
|
||||||
|
render_single_step(component, arch) for component in manifest["components"]
|
||||||
|
)
|
||||||
|
|
||||||
|
body = f"""{header}
|
||||||
|
labels:
|
||||||
|
{label_lines}
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: {branches}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
{steps}"""
|
||||||
|
return {"container.yaml": GENERATED_HEADER.format(repo=manifest["repo"]) + body}
|
||||||
|
|
||||||
|
|
||||||
|
def render_split_leg(manifest: dict, arch: str, leg: dict) -> str:
|
||||||
|
"""One thin `flow: publish` file for a `kind: split` leg (flake-hub's
|
||||||
|
amd64.yaml/arm64.yaml, woodpecker-peek's amd64.yaml/arm64.yaml)."""
|
||||||
|
setup_script = manifest.get("setup_script", "ci/setup.sh")
|
||||||
|
header = leg["header"].rstrip("\n") + "\n"
|
||||||
|
entrypoint_comment = _comment_block(leg.get("entrypoint_comment", ""), " ")
|
||||||
|
tag_event_block = (
|
||||||
|
"\n - event: tag\n" if manifest.get("when_tag_event", True) else "\n"
|
||||||
|
)
|
||||||
|
publish_flag = " -- --publish" if leg.get("publish_flag", True) else ""
|
||||||
|
|
||||||
|
run_lines = f" sh {setup_script}\n"
|
||||||
|
if entrypoint_comment:
|
||||||
|
run_lines += entrypoint_comment
|
||||||
|
run_lines += f" PUBLISH=1 nix run .#{leg['app']}{publish_flag}\n"
|
||||||
|
|
||||||
|
return f"""{GENERATED_HEADER.format(repo=manifest["repo"])}{header}
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main{tag_event_block}
|
||||||
|
steps:
|
||||||
|
- name: build-{arch}
|
||||||
|
image: {PLUGIN_IMAGE}
|
||||||
|
environment:
|
||||||
|
ATTIC_TOKEN:
|
||||||
|
from_secret: attic_token
|
||||||
|
GITEA_CLONE_TOKEN:
|
||||||
|
from_secret: gitea_clone_token
|
||||||
|
settings:
|
||||||
|
run: |
|
||||||
|
{run_lines} flow: publish
|
||||||
|
cache: "true"
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: {arch}
|
||||||
|
labels:
|
||||||
|
commit-tag: "${{CI_COMMIT_TAG}}"
|
||||||
|
commit-branch: "${{CI_COMMIT_BRANCH}}"
|
||||||
|
pipeline-number: "${{CI_PIPELINE_NUMBER}}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def render_split(manifest: dict) -> dict:
|
||||||
|
"""`kind: split` (flake-hub, woodpecker-peek) → one file per arch leg,
|
||||||
|
kept separate (never collapsed into a single matrix file — the two arches
|
||||||
|
build against different node-bound PVCs, see manifests/flake-hub.yaml)."""
|
||||||
|
return {
|
||||||
|
f"{arch}.yaml": render_split_leg(manifest, arch, leg)
|
||||||
|
for arch, leg in manifest["legs"].items()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--out", type=pathlib.Path, required=True)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
manifests_dir = HERE / "manifests"
|
||||||
|
for path in sorted(manifests_dir.glob("*.yaml")):
|
||||||
|
manifest = yaml.safe_load(path.read_text())
|
||||||
|
kind = manifest.get("kind")
|
||||||
|
repo = manifest["repo"]
|
||||||
|
out_dir = args.out / repo
|
||||||
|
|
||||||
|
if kind == "matrix":
|
||||||
|
# oleks/ci-scripts#22: image_name must be explicit in the
|
||||||
|
# manifest, never inferred from the repo/dir name — the two
|
||||||
|
# silently diverge for xonsh-image (repo field "xonsh-image",
|
||||||
|
# actually-published image "oleks/xonsh") and inference would
|
||||||
|
# have pushed a migrated pipeline to an empty, wrong image path.
|
||||||
|
image_name = manifest.get("image_name")
|
||||||
|
if not image_name:
|
||||||
|
raise SystemExit(
|
||||||
|
f"manifest for {repo!r} (kind=matrix) is missing required "
|
||||||
|
"'image_name' field (oleks/ci-scripts#22: must be explicit, "
|
||||||
|
"verified against the live package registry — do not infer "
|
||||||
|
"it from the repo/dir name)"
|
||||||
|
)
|
||||||
|
arches = manifest["arches"]
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for arch in arches:
|
||||||
|
target = out_dir / f"build-{arch}.yaml"
|
||||||
|
target.write_text(render_build(manifest, arch, image_name))
|
||||||
|
print(f"wrote {target}")
|
||||||
|
target = out_dir / "manifest.yaml"
|
||||||
|
target.write_text(render_manifest(manifest, arches, image_name))
|
||||||
|
print(f"wrote {target}")
|
||||||
|
elif kind == "single":
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name, body in render_single(manifest).items():
|
||||||
|
target = out_dir / name
|
||||||
|
target.write_text(body)
|
||||||
|
print(f"wrote {target}")
|
||||||
|
elif kind == "split":
|
||||||
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
for name, body in render_split(manifest).items():
|
||||||
|
target = out_dir / name
|
||||||
|
target.write_text(body)
|
||||||
|
print(f"wrote {target}")
|
||||||
|
else:
|
||||||
|
print(f"skip {repo} (kind={kind}, unknown)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -22,9 +22,18 @@
|
|||||||
# cores - Nix `cores` setting written into /etc/nix/nix.conf
|
# cores - Nix `cores` setting written into /etc/nix/nix.conf
|
||||||
# max_jobs - whether to also cap `max-jobs = 1` (small/serial builds)
|
# max_jobs - whether to also cap `max-jobs = 1` (small/serial builds)
|
||||||
# attic - whether to run the inline `attic watch-store` accelerator
|
# attic - whether to run the inline `attic watch-store` accelerator
|
||||||
|
# no_build - if true, render a NEUTRALIZED pipeline (event: manual, no
|
||||||
|
# build/publish). s390x = no-build: s390x hardware is
|
||||||
|
# currently UNAVAILABLE, so those wheels are not built —
|
||||||
|
# the repo code is kept as a cross-compile reference only.
|
||||||
|
# Entries under s390x/ default to no_build even without the
|
||||||
|
# flag (generate.py is_no_build). Re-enable by setting
|
||||||
|
# no_build: false. arm64 builds are unaffected (arm64 hw is
|
||||||
|
# available). See wiki Spec/* + memory feedback-no-s390x-builds.
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- dir: s390x/lxml-s390x
|
- dir: s390x/lxml-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 2Gi
|
memory_request: 2Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -32,6 +41,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/pillow-s390x
|
- dir: s390x/pillow-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 6Gi
|
memory_request: 6Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -39,6 +49,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/psycopg-s390x
|
- dir: s390x/psycopg-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 6Gi
|
memory_request: 6Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -46,6 +57,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/psycopg2-binary-s390x
|
- dir: s390x/psycopg2-binary-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 6Gi
|
memory_request: 6Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -53,6 +65,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/tiktoken-s390x
|
- dir: s390x/tiktoken-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 6Gi
|
memory_request: 6Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -61,6 +74,7 @@ repos:
|
|||||||
attic: true
|
attic: true
|
||||||
redis_cache: true
|
redis_cache: true
|
||||||
- dir: s390x/duckdb-s390x
|
- dir: s390x/duckdb-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -68,6 +82,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/faiss-cpu-s390x
|
- dir: s390x/faiss-cpu-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -75,6 +90,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/numpy-s390x
|
- dir: s390x/numpy-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -82,6 +98,7 @@ repos:
|
|||||||
max_jobs: true
|
max_jobs: true
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/pymupdf-s390x
|
- dir: s390x/pymupdf-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 50Gi
|
memory_limit: 50Gi
|
||||||
@@ -89,6 +106,7 @@ repos:
|
|||||||
max_jobs: false
|
max_jobs: false
|
||||||
attic: true
|
attic: true
|
||||||
- dir: s390x/markupsafe-s390x
|
- dir: s390x/markupsafe-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 2Gi
|
memory_request: 2Gi
|
||||||
memory_limit: 6Gi
|
memory_limit: 6Gi
|
||||||
@@ -126,6 +144,7 @@ repos:
|
|||||||
# bespoke hand-maintained file; flagged alongside fastuuid.
|
# bespoke hand-maintained file; flagged alongside fastuuid.
|
||||||
thin_repos:
|
thin_repos:
|
||||||
- dir: s390x/asyncpg-s390x
|
- dir: s390x/asyncpg-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -136,6 +155,7 @@ thin_repos:
|
|||||||
dead_secrets: []
|
dead_secrets: []
|
||||||
gitea_clone_token: false
|
gitea_clone_token: false
|
||||||
- dir: s390x/cryptography-s390x
|
- dir: s390x/cryptography-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -147,6 +167,7 @@ thin_repos:
|
|||||||
tags_full: false
|
tags_full: false
|
||||||
dead_secrets: [attic_token, redis_password]
|
dead_secrets: [attic_token, redis_password]
|
||||||
- dir: s390x/lightningcss-s390x
|
- dir: s390x/lightningcss-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
@@ -156,6 +177,7 @@ thin_repos:
|
|||||||
tags_full: true
|
tags_full: true
|
||||||
dead_secrets: [attic_token, redis_password]
|
dead_secrets: [attic_token, redis_password]
|
||||||
- dir: s390x/nextjs-swc-s390x
|
- dir: s390x/nextjs-swc-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 50Gi
|
memory_limit: 50Gi
|
||||||
@@ -165,6 +187,7 @@ thin_repos:
|
|||||||
tags_full: true
|
tags_full: true
|
||||||
dead_secrets: [attic_token, redis_password]
|
dead_secrets: [attic_token, redis_password]
|
||||||
- dir: s390x/rollup-s390x
|
- dir: s390x/rollup-s390x
|
||||||
|
no_build: true
|
||||||
arch: amd64
|
arch: amd64
|
||||||
memory_request: 8Gi
|
memory_request: 8Gi
|
||||||
memory_limit: 32Gi
|
memory_limit: 32Gi
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ kind: matrix
|
|||||||
explicit_clone: true
|
explicit_clone: true
|
||||||
image: git.oleks.space/oleks/nix-ci:latest
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
script: ci/local.sh
|
script: ci/local.sh
|
||||||
|
# oleks/ci-scripts#22: matches ci/local.sh's IMAGE_REPO default
|
||||||
|
# ("csi-s3-driver"), NOT the `repo:` field above. Unlike xonsh-image there is
|
||||||
|
# no live package under this name yet to cross-check (csi-s3's --publish path
|
||||||
|
# has never run in CI: no pipeline history, no registry package) — this is
|
||||||
|
# the intended target per the script, not a confirmed-live value. csi-s3
|
||||||
|
# remains EXCLUDED from the #17 migration regardless (oleks/ci-scripts#20:
|
||||||
|
# too bespoke — build-args, dual per-arch tags, skip-if-published, dropped
|
||||||
|
# clone/timeout overrides); this field is added for manifest correctness only.
|
||||||
|
image_name: git.oleks.space/oleks/csi-s3-driver
|
||||||
var_name: TARGET_ARCH
|
var_name: TARGET_ARCH
|
||||||
arches: [amd64, arm64]
|
arches: [amd64, arm64]
|
||||||
commented_arches:
|
commented_arches:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Example estimator-driven on-demand CI target (oleks/ci-scripts#25).
|
||||||
|
#
|
||||||
|
# Renders with generator/render_ondemand.py (NOT the quartet renderer — the
|
||||||
|
# `kind: ondemand` dispatch is handled by its own script; render_thin_quartet
|
||||||
|
# skips this manifest as an unknown kind by design).
|
||||||
|
#
|
||||||
|
# SHAPE: a heavy amd64 release build that must NOT serialise through the single
|
||||||
|
# emmett-amd64 Woodpecker agent. The pre-step estimates the sizing, orders a
|
||||||
|
# fresh on-demand amd64 cloud node (EphemeralBuilder class: ci), waits for it to
|
||||||
|
# join armer k3s as a plain kubernetes.io/arch=amd64 worker, builds there, then
|
||||||
|
# reaps the node. See the reference order CR:
|
||||||
|
# builder-arbitrage config/samples/arbitrage_v1alpha1_ephemeralbuilder_ci-amd64-release-slot.yaml
|
||||||
|
#
|
||||||
|
# Fields:
|
||||||
|
# repo - logical name (used in the GENERATED header + out path)
|
||||||
|
# kind - `ondemand` (dispatch key for render_ondemand.py)
|
||||||
|
# target - nix flake attr to build/estimate (e.g. .#release)
|
||||||
|
# system - nix system double (x86_64-linux) fed to nix-estimate
|
||||||
|
# --system; the CR arch is derived by the estimator's
|
||||||
|
# provision.arch_for_system(system).
|
||||||
|
# arch - k8s/Go arch double (amd64) for the node label + the
|
||||||
|
# build step's nodeSelector pin. MUST be the k8s double,
|
||||||
|
# not the nix double (kubernetes.io/arch enum is amd64).
|
||||||
|
# provision_class - EphemeralBuilder class (ci) passed to --provision.
|
||||||
|
# build_run - the actual build command(s) run on the ordered node.
|
||||||
|
# wait_timeout - kubectl wait --for=condition=Ready timeout.
|
||||||
|
# kubeconfig_secret - (optional) Woodpecker secret holding a scoped SA
|
||||||
|
# kubeconfig; omit to use the step-pod's in-cluster SA.
|
||||||
|
# node_selector - (optional) extra nodeSelector keys merged onto the
|
||||||
|
# arch pin (reserved; the MVP needs only the arch pin).
|
||||||
|
# orchestrator_image - (optional) image with nix-estimate + kubectl + jq.
|
||||||
|
# build_image - (optional) the repo's build image.
|
||||||
|
# memory_request /
|
||||||
|
# memory_limit - (optional) build step k8s memory sizing.
|
||||||
|
repo: ondemand-release-amd64
|
||||||
|
kind: ondemand
|
||||||
|
target: .#release
|
||||||
|
system: x86_64-linux
|
||||||
|
arch: amd64
|
||||||
|
provision_class: ci
|
||||||
|
wait_timeout: 1200s
|
||||||
|
build_run: |
|
||||||
|
nixos-ci-entrypoint true
|
||||||
|
PUBLISH=1 nix run .#publish
|
||||||
|
memory_request: 8Gi
|
||||||
|
memory_limit: 32Gi
|
||||||
@@ -4,3 +4,14 @@ image: git.oleks.space/oleks/nix-ci
|
|||||||
script: ci/local.sh
|
script: ci/local.sh
|
||||||
arches: [amd64, arm64]
|
arches: [amd64, arm64]
|
||||||
buildkit_prefix: buildkit
|
buildkit_prefix: buildkit
|
||||||
|
# oleks/ci-scripts#22: the PUBLISHED image is "oleks/xonsh", not
|
||||||
|
# "oleks/xonsh-image" — ci/local.sh's IMAGE_REPO default is "xonsh", and the
|
||||||
|
# live Gitea package registry confirms it (tags 0.22.7, 0.22.7-amd64,
|
||||||
|
# 0.22.7-arm64, latest all under package "xonsh"). Do not derive this from
|
||||||
|
# the `repo:` field above.
|
||||||
|
image_name: git.oleks.space/oleks/xonsh
|
||||||
|
# oleks/ci-scripts#21: ci/local.sh strips a leading "v" (and a trailing
|
||||||
|
# "-N" build suffix) from CI_COMMIT_TAG before tagging — live published tags
|
||||||
|
# are "0.22.7"/"0.22.7-<arch>", not "v0.22.7-<arch>". Reproduces that exactly
|
||||||
|
# via entrypoint.sh's opt-in resolve_version() normalization.
|
||||||
|
tag_strip_v: true
|
||||||
|
|||||||
Reference in New Issue
Block a user