This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
# Centralized registry/host constants (oleks/ci-scripts#4)
|
||||
|
||||
Woodpecker has no cross-repo variable store and no `include:` mechanism, so
|
||||
these constants can't be defined once and referenced by name inside raw YAML
|
||||
pipelines. Two real mechanisms exist instead — use whichever fits the value:
|
||||
|
||||
1. **Woodpecker secret or org secret** — for anything that's actually a
|
||||
credential, or that we want to be able to rotate without touching every
|
||||
repo's pipeline file. Prefer this whenever a value isn't public.
|
||||
2. **Manifest field, interpolated by the oleks/ci-scripts generator**
|
||||
(`generator/render.py`, oleks/ci-scripts#2) — for values that are public
|
||||
infrastructure addresses baked into rendered YAML. Put the value in the
|
||||
per-repo manifest once; the generator stamps it into every rendered file.
|
||||
This is the practical stand-in for a "constants file" Woodpecker can't
|
||||
natively `include:`.
|
||||
|
||||
There is no third option (a shared "constants.yaml" `include:`d by every
|
||||
pipeline) — Woodpecker pipelines are standalone YAML with no include
|
||||
directive, so the generator is the only place central substitution happens.
|
||||
|
||||
## Inventory
|
||||
|
||||
| Constant | Value(s) | Where it's inlined today | Mechanism |
|
||||
|---|---|---|---|
|
||||
| Git server / netrc machine | `git.oleks.space` | every `clone:` block | manifest field `git_host` (rarely changes; secret would be overkill) |
|
||||
| Gitea clone token | — | every `clone:` block, `GITEA_CLONE_TOKEN` env | **secret** `gitea_clone_token` (org secret, already is one) |
|
||||
| Registry push token | — | matrix/manifest build steps | **secret** `registry_token` (org secret) |
|
||||
| Attic cache token | — | split (attic-closure) build steps | **secret** `attic_token` (org secret) |
|
||||
| Build image | `git.oleks.space/oleks/nix-ci:latest` / `:latest-arm64` | every build step's `image:` | manifest field `image` + arch suffix rule (generator appends `-arm64` for arm64 legs) |
|
||||
| BuildKit address | `tcp://buildkit-${ARCH}.infra.svc.cluster.local:1234`, `tcp://buildkit-rootless-${ARCH}.infra.svc.cluster.local:1234` | matrix-shape build/manifest steps | manifest field `buildkit_prefix` (`buildkit` or `buildkit-rootless`), arch interpolated by the generator |
|
||||
| Resolver nameserver | `169.254.20.10` | s390x cross-build pipelines (building/s390x, out of this worker's scope) | manifest field `nameserver` where the shape needs it; coordinate with fleet-worker (owns building/s390x + building/arm64) |
|
||||
| Kubernetes node selector overrides | e.g. `kubernetes.io/hostname: howard2404` (csi-s3, pinned to the only s390x/arm64-capable node) | matrix-shape `backend_options.kubernetes.nodeSelector` | manifest field `node_selector` (map, optional) |
|
||||
|
||||
## Convention going forward
|
||||
|
||||
- If a value is secret-shaped (token, password, key) → it MUST be a
|
||||
Woodpecker secret, never a manifest field or literal in generated YAML.
|
||||
- If a value is public infrastructure (hostname, image ref, in-cluster
|
||||
service DNS name) that's the same across most repos → put it in the
|
||||
per-repo manifest consumed by `generator/render.py` (oleks/ci-scripts#2),
|
||||
not hand-typed into `.woodpecker/*.yaml`.
|
||||
- If a value is genuinely repo-specific (a one-off node pin, a nonstandard
|
||||
arch list) → it's still a manifest field, just not shared across repos.
|
||||
The manifest is the single place per-repo variance lives; the rendered
|
||||
YAML is never hand-edited (see the "GENERATED" header the generator
|
||||
stamps on every output file).
|
||||
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render Woodpecker buildx-quartet pipelines from a per-repo manifest.
|
||||
|
||||
oleks/ci-scripts#2 — one generator, three supported shapes (`kind:` in the
|
||||
manifest), because the repos in scope aren't actually one shape:
|
||||
|
||||
matrix - single build.yaml with a `matrix: {ARCH: [...]}` + manifest.yaml
|
||||
combining the per-arch tags (xonsh-image, csi-s3, ...).
|
||||
split - separate per-arch files (amd64.yaml, arm64.yaml, ...), no
|
||||
manifest step — used for attic-closure "nix run .#publish-*"
|
||||
publishes rather than docker buildx (woodpecker-peek, ...).
|
||||
single - one container.yaml, one native-arch build (+ optional extra
|
||||
components like a grpc image), no matrix, no manifest
|
||||
(the-eye, emdash.kotkanagrilli.fi).
|
||||
|
||||
Usage:
|
||||
python3 generator/render.py manifests/<repo>.yaml --out /path/to/repo/.woodpecker
|
||||
|
||||
Every rendered file gets a "GENERATED by oleks/ci-scripts — do not hand-edit"
|
||||
header. Re-running is idempotent: same manifest in, byte-identical files out.
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
HERE = pathlib.Path(__file__).parent
|
||||
TEMPLATES = HERE / "templates"
|
||||
|
||||
GENERATED_HEADER = (
|
||||
"# GENERATED by oleks/ci-scripts — do not hand-edit.\n"
|
||||
"# Source manifest: {manifest_path}\n"
|
||||
"# Regenerate: python3 generator/render.py {manifest_path} --out <repo>/.woodpecker\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
|
||||
def load_template(*parts):
|
||||
return (TEMPLATES / pathlib.Path(*parts)).read_text()
|
||||
|
||||
|
||||
def render_matrix(manifest: dict) -> dict:
|
||||
arches = manifest["arches"]
|
||||
image = manifest["image"]
|
||||
script = manifest.get("script", "ci/local.sh")
|
||||
buildkit_prefix = manifest.get("buildkit_prefix", "buildkit")
|
||||
node_selector = manifest.get("node_selector")
|
||||
timeout = manifest.get("timeout")
|
||||
manifest_arches_env = manifest.get("manifest_arches_env")
|
||||
|
||||
arch_list = "\n".join(f" - {a}" for a in arches)
|
||||
timeout_block = f"\ntimeout: {timeout}\n" if timeout else ""
|
||||
if node_selector:
|
||||
lines = "\n".join(f" {k}: {v}" for k, v in node_selector.items())
|
||||
node_selector_block = f" nodeSelector:\n{lines}\n"
|
||||
node_selector_top_block = (
|
||||
" backend_options:\n kubernetes:\n nodeSelector:\n" + lines + "\n"
|
||||
)
|
||||
else:
|
||||
node_selector_block = ""
|
||||
node_selector_top_block = ""
|
||||
|
||||
manifest_extra_env = ""
|
||||
if manifest_arches_env:
|
||||
manifest_extra_env = f' MANIFEST_ARCHES: "{manifest_arches_env}"\n'
|
||||
if node_selector:
|
||||
manifest_extra_env = (
|
||||
f' BUILDKIT_ADDR: "tcp://{buildkit_prefix}-{arches[0]}.infra.svc.cluster.local:1234"\n'
|
||||
+ manifest_extra_env
|
||||
)
|
||||
|
||||
build = load_template("matrix", "build.yaml.tmpl").format(
|
||||
arch_list=arch_list,
|
||||
timeout_block=timeout_block,
|
||||
script=script,
|
||||
image=image,
|
||||
buildkit_prefix=buildkit_prefix,
|
||||
node_selector_block=node_selector_block,
|
||||
)
|
||||
mnf = load_template("matrix", "manifest.yaml.tmpl").format(
|
||||
script=script,
|
||||
image=image,
|
||||
manifest_extra_env=manifest_extra_env,
|
||||
node_selector_top_block=node_selector_top_block,
|
||||
)
|
||||
return {"build.yaml": build, "manifest.yaml": mnf}
|
||||
|
||||
|
||||
def render_split(manifest: dict) -> dict:
|
||||
repo = manifest["repo"]
|
||||
git_host = manifest.get("git_host", "git.oleks.space")
|
||||
setup_script = manifest.get("setup_script", "ci/setup.sh")
|
||||
out = {}
|
||||
tmpl = load_template("split", "PERARCH.yaml.tmpl")
|
||||
for arch, leg in manifest["legs"].items():
|
||||
header = leg["header"].rstrip("\n") + "\n"
|
||||
entrypoint_comment = "".join(
|
||||
f" # {l}\n" if l else " #\n"
|
||||
for l in leg["entrypoint_comment"].rstrip("\n").splitlines()
|
||||
)
|
||||
out[f"{arch}.yaml"] = tmpl.format(
|
||||
repo=repo,
|
||||
nix_system=leg["nix_system"],
|
||||
arch=arch,
|
||||
header=header,
|
||||
entrypoint_comment=entrypoint_comment,
|
||||
git_host=git_host,
|
||||
image=leg["image"],
|
||||
setup_script=setup_script,
|
||||
app=leg["app"],
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def render_single(manifest: dict) -> dict:
|
||||
repo = manifest["repo"]
|
||||
arch = manifest["arch"]
|
||||
git_host = manifest.get("git_host", "git.oleks.space")
|
||||
branches = "[" + ", ".join(manifest["branches"]) + "]"
|
||||
buildkit_addr = f"tcp://buildkit-{arch}.infra.svc.cluster.local:1234"
|
||||
|
||||
step_tmpl = load_template("single", "COMPONENT_STEP.tmpl")
|
||||
steps = []
|
||||
for c in manifest["components"]:
|
||||
command = f"./ci/local.sh --arch {arch}"
|
||||
if c.get("component"):
|
||||
command += f" --component {c['component']}"
|
||||
steps.append(
|
||||
step_tmpl.format(
|
||||
step_name=c["name"],
|
||||
image=manifest["image"],
|
||||
buildkit_addr=buildkit_addr,
|
||||
command=command,
|
||||
arch=arch,
|
||||
memory=c.get("memory", "4Gi"),
|
||||
)
|
||||
)
|
||||
container = load_template("single", "container.yaml.tmpl").format(
|
||||
repo=repo,
|
||||
arch=arch,
|
||||
branches=branches,
|
||||
git_host=git_host,
|
||||
component_steps="\n".join(steps),
|
||||
)
|
||||
return {"container.yaml": container}
|
||||
|
||||
|
||||
RENDERERS = {"matrix": render_matrix, "split": render_split, "single": render_single}
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("manifest", type=pathlib.Path)
|
||||
ap.add_argument("--out", type=pathlib.Path, required=True, help="target .woodpecker/ dir")
|
||||
ap.add_argument("--check", action="store_true", help="don't write, just diff against --out")
|
||||
args = ap.parse_args()
|
||||
|
||||
manifest = yaml.safe_load(args.manifest.read_text())
|
||||
kind = manifest["kind"]
|
||||
if kind not in RENDERERS:
|
||||
sys.exit(f"unknown kind: {kind}")
|
||||
|
||||
files = RENDERERS[kind](manifest)
|
||||
header = GENERATED_HEADER.format(manifest_path=args.manifest)
|
||||
|
||||
args.out.mkdir(parents=True, exist_ok=True)
|
||||
changed = False
|
||||
for name, body in files.items():
|
||||
target = args.out / name
|
||||
content = header + body
|
||||
if args.check:
|
||||
existing = target.read_text() if target.exists() else ""
|
||||
if existing != content:
|
||||
changed = True
|
||||
print(f"DIFF: {target}")
|
||||
else:
|
||||
target.write_text(content)
|
||||
print(f"wrote {target}")
|
||||
|
||||
if args.check and changed:
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,33 @@
|
||||
matrix:
|
||||
ARCH:
|
||||
{arch_list}
|
||||
|
||||
when:
|
||||
- event: tag
|
||||
ref: "refs/tags/v*"
|
||||
{timeout_block}
|
||||
labels:
|
||||
arch: ${{ARCH}}
|
||||
|
||||
# Thin wrapper (cluster #196, emmett#44): CI runs the SAME {script} a
|
||||
# developer runs, so CI and local can't drift. CI-specific bits are env
|
||||
# overrides only: BUILDKIT_ADDR -> the in-cluster NATIVE per-arch buildkit
|
||||
# (no QEMU), PUBLISH=1 -> actually push. Output is unchanged: a per-arch
|
||||
# :<version>-<arch> image, later merged into :<version>+:latest by manifest.yaml.
|
||||
steps:
|
||||
- name: build-and-push
|
||||
image: {image}:latest
|
||||
environment:
|
||||
REGISTRY_TOKEN:
|
||||
from_secret: registry_token
|
||||
BUILDKIT_ADDR: "tcp://{buildkit_prefix}-${{ARCH}}.infra.svc.cluster.local:1234"
|
||||
PUBLISH: "1"
|
||||
commands:
|
||||
- echo "▸ arch=$(uname -m)"
|
||||
- {script} --arch "${{ARCH}}"
|
||||
backend_options:
|
||||
kubernetes:
|
||||
{node_selector_block} labels:
|
||||
commit-tag: "${{CI_COMMIT_TAG}}"
|
||||
pipeline-number: "${{CI_PIPELINE_NUMBER}}"
|
||||
build-arch: "${{ARCH}}"
|
||||
@@ -0,0 +1,21 @@
|
||||
when:
|
||||
- event: tag
|
||||
ref: "refs/tags/v*"
|
||||
|
||||
depends_on:
|
||||
- build
|
||||
|
||||
# Thin wrapper (cluster #196, emmett#44): the SAME {script} composes the
|
||||
# per-arch tags into the multi-arch :<version> + :latest manifest list.
|
||||
# PUBLISH=1 is the only CI-specific override (local default is dry-run).
|
||||
steps:
|
||||
- name: manifest
|
||||
image: {image}:latest
|
||||
environment:
|
||||
REGISTRY_TOKEN:
|
||||
from_secret: registry_token
|
||||
{manifest_extra_env} PUBLISH: "1"
|
||||
commands:
|
||||
- echo "▸ arch=$(uname -m)"
|
||||
- {script} --manifest
|
||||
{node_selector_top_block}
|
||||
@@ -0,0 +1,26 @@
|
||||
- name: {step_name}
|
||||
image: {image}
|
||||
environment:
|
||||
REGISTRY_TOKEN:
|
||||
from_secret: registry_token
|
||||
BUILDKIT_ADDR: {buildkit_addr}
|
||||
PUBLISH: "1"
|
||||
BRANCH: "${{CI_COMMIT_BRANCH}}"
|
||||
VERSION: "0.1.${{CI_PIPELINE_NUMBER}}"
|
||||
commands:
|
||||
- echo "▸ arch=$(uname -m)"
|
||||
- {command}
|
||||
|
||||
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}}"
|
||||
@@ -0,0 +1,25 @@
|
||||
# Build + push the {repo} container (cluster #196, emmett#44).
|
||||
#
|
||||
# Keep-Dockerfile OCI image build; native single-arch buildkit, no matrix,
|
||||
# no manifest step. See {repo}/ci/local.sh for the actual build logic.
|
||||
|
||||
labels:
|
||||
arch: {arch}
|
||||
|
||||
when:
|
||||
- event: push
|
||||
branch: {branches}
|
||||
|
||||
clone:
|
||||
- name: clone
|
||||
image: woodpeckerci/plugin-git
|
||||
environment:
|
||||
CI_NETRC_MACHINE: {git_host}
|
||||
CI_NETRC_USERNAME: oleks
|
||||
CI_NETRC_PASSWORD:
|
||||
from_secret: gitea_clone_token
|
||||
PLUGIN_TAGS: "false"
|
||||
PLUGIN_DEPTH: "1"
|
||||
|
||||
steps:
|
||||
{component_steps}
|
||||
@@ -0,0 +1,39 @@
|
||||
{header}
|
||||
when:
|
||||
- event: push
|
||||
branch: main
|
||||
- event: tag
|
||||
|
||||
clone:
|
||||
- name: clone
|
||||
image: woodpeckerci/plugin-git
|
||||
environment:
|
||||
CI_NETRC_MACHINE: {git_host}
|
||||
CI_NETRC_USERNAME: oleks
|
||||
CI_NETRC_PASSWORD:
|
||||
from_secret: gitea_clone_token
|
||||
PLUGIN_TAGS: "false"
|
||||
PLUGIN_DEPTH: "1"
|
||||
|
||||
steps:
|
||||
- name: build-{arch}
|
||||
image: {image}
|
||||
environment:
|
||||
ATTIC_TOKEN:
|
||||
from_secret: attic_token
|
||||
GITEA_CLONE_TOKEN:
|
||||
from_secret: gitea_clone_token
|
||||
backend_options:
|
||||
kubernetes:
|
||||
nodeSelector:
|
||||
kubernetes.io/arch: {arch}
|
||||
labels:
|
||||
commit-tag: "${{CI_COMMIT_TAG}}"
|
||||
commit-branch: "${{CI_COMMIT_BRANCH}}"
|
||||
pipeline-number: "${{CI_PIPELINE_NUMBER}}"
|
||||
commands:
|
||||
- echo "▸ arch=$(uname -m)"
|
||||
# nix.conf substituters + {git_host} netrc (to fetch the parity-lib
|
||||
# flake input). Same bootstrap as flake-hub/ci/setup.sh.
|
||||
- sh {setup_script}
|
||||
{entrypoint_comment} - PUBLISH=1 nix run .#{app} -- --publish
|
||||
@@ -0,0 +1,10 @@
|
||||
repo: csi-s3
|
||||
kind: matrix
|
||||
image: git.oleks.space/oleks/nix-ci
|
||||
script: ci/local.sh
|
||||
arches: [amd64, arm64]
|
||||
buildkit_prefix: buildkit-rootless
|
||||
timeout: 240
|
||||
node_selector:
|
||||
kubernetes.io/hostname: howard2404
|
||||
manifest_arches_env: "amd64 arm64"
|
||||
@@ -0,0 +1,9 @@
|
||||
repo: the-eye
|
||||
kind: single
|
||||
image: git.oleks.space/oleks/nix-ci:latest-arm64
|
||||
arch: arm64
|
||||
branches: [develop, staging, production]
|
||||
components:
|
||||
- name: build-and-push
|
||||
- name: build-and-push-grpc
|
||||
component: grpc
|
||||
@@ -0,0 +1,31 @@
|
||||
repo: woodpecker-peek
|
||||
kind: split
|
||||
git_host: git.oleks.space
|
||||
setup_script: ci/setup.sh
|
||||
legs:
|
||||
amd64:
|
||||
nix_system: x86_64-linux
|
||||
image: git.oleks.space/oleks/nix-ci:latest
|
||||
app: publish-x86_64-linux
|
||||
header: |
|
||||
# Build woodpecker-peek for x86_64-linux and push the closure to the
|
||||
# attic binary cache. Mirrors ~/projects/flake-hub/.woodpecker/amd64.yaml.
|
||||
entrypoint_comment: |
|
||||
Same entrypoint as a local `nix run .#publish-x86_64-linux -- --publish`.
|
||||
All build/login/push logic lives in parity-lib's mkAtticClosurePublish;
|
||||
PUBLISH=1 makes the shared app actually push (local runs dry-run).
|
||||
arm64:
|
||||
nix_system: aarch64-linux
|
||||
image: git.oleks.space/oleks/nix-ci:latest-arm64
|
||||
app: publish-aarch64-linux
|
||||
header: |
|
||||
# Build woodpecker-peek for aarch64-linux and push the closure to attic.
|
||||
#
|
||||
# NODE-BOUND LEG (emmett#44, attic-closure): this binary is cgo/fyne +
|
||||
# OpenGL + X11 with no cross path, so aarch64-linux CANNOT be built on
|
||||
# emmett (linux/amd64) and is intentionally NOT a local-parity flake app.
|
||||
# It must run on an aarch64 node — hence the arch nodeSelector below.
|
||||
# Mirrors ~/projects/flake-hub/.woodpecker/arm64.yaml.
|
||||
entrypoint_comment: |
|
||||
Identical shared parity-lib entrypoint as amd64, only the arch differs.
|
||||
All build/login/push logic lives in parity-lib's mkAtticClosurePublish.
|
||||
@@ -0,0 +1,6 @@
|
||||
repo: xonsh-image
|
||||
kind: matrix
|
||||
image: git.oleks.space/oleks/nix-ci
|
||||
script: ci/local.sh
|
||||
arches: [amd64, arm64]
|
||||
buildkit_prefix: buildkit
|
||||
@@ -0,0 +1,25 @@
|
||||
# Canonical Woodpecker clone step (oleks/ci-scripts#3).
|
||||
#
|
||||
# Copy this `clone:` block verbatim into any pipeline that needs an explicit
|
||||
# clone override (private repo over HTTPS via a Gitea PAT, shallow clone, or
|
||||
# tags disabled). Most simple pipelines don't need this at all — Woodpecker's
|
||||
# built-in implicit clone already authenticates the same way when the repo is
|
||||
# private and the server has forge credentials wired up. Reach for this block
|
||||
# when you need to tune PLUGIN_TAGS/PLUGIN_DEPTH or you're on a runner that
|
||||
# doesn't have implicit forge auth (e.g. a Kubernetes backend crossing
|
||||
# namespaces).
|
||||
#
|
||||
# Required secret: `gitea_clone_token` — a Gitea PAT with repo read scope,
|
||||
# set as an org secret so every repo can reference it without redefining it
|
||||
# (see docs/constants.md, oleks/ci-scripts#4).
|
||||
|
||||
clone:
|
||||
- name: clone
|
||||
image: woodpeckerci/plugin-git
|
||||
environment:
|
||||
CI_NETRC_MACHINE: git.oleks.space
|
||||
CI_NETRC_USERNAME: oleks
|
||||
CI_NETRC_PASSWORD:
|
||||
from_secret: gitea_clone_token
|
||||
PLUGIN_TAGS: "false" # set "true" if the build needs tag history (e.g. nix-shell git describe)
|
||||
PLUGIN_DEPTH: "1" # shallow clone; drop this key entirely for full history
|
||||
Reference in New Issue
Block a user