[SCM] PostGIS Buildbots; Jenkins jobs and instructions for setting up winnie and debbie bots. branch freebsd14-runner-iac created. 6d857954da22783f548a45970594e6f974596eac
git at osgeo.org
git at osgeo.org
Sun Jul 26 08:37:37 PDT 2026
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "PostGIS Buildbots; Jenkins jobs and instructions for setting up winnie and debbie bots.".
The branch, freebsd14-runner-iac has been created
at 6d857954da22783f548a45970594e6f974596eac (commit)
- Log -----------------------------------------------------------------
commit 6d857954da22783f548a45970594e6f974596eac
Author: Darafei Praliaskouski <me at komzpa.net>
Date: Sun Jul 26 19:37:27 2026 +0400
Add FreeBSD 14 runner recipe
diff --git a/freebsd/freebsd14_runner/.gitignore b/freebsd/freebsd14_runner/.gitignore
new file mode 100644
index 0000000..1ba4781
--- /dev/null
+++ b/freebsd/freebsd14_runner/.gitignore
@@ -0,0 +1,8 @@
+.tmp/
+downloads/
+images/
+logs/
+artifacts/
+config/runner.local.env
+secrets/freebsd_runner_ed25519
+secrets/freebsd_runner_ed25519.pub
diff --git a/freebsd/freebsd14_runner/Makefile b/freebsd/freebsd14_runner/Makefile
new file mode 100644
index 0000000..a60caf6
--- /dev/null
+++ b/freebsd/freebsd14_runner/Makefile
@@ -0,0 +1,58 @@
+SHELL := /usr/bin/env bash
+.DEFAULT_GOAL := help
+
+export TMPDIR := $(CURDIR)/.tmp
+CONFIG := config/runner.env
+include $(CONFIG)
+-include config/runner.local.env
+
+.PHONY: help all fetch seed deps verify runtime-files clean deps-clean start stop status ssh package-lock
+
+help:
+ @printf '%s\n' 'Targets:' \
+ ' make all fetch, seed, install deps, verify' \
+ ' make fetch download and checksum FreeBSD image' \
+ ' make seed expand, resize, inject SSH/rc config' \
+ ' make deps boot seeded image and install packages' \
+ ' make verify boot deps image and run liblwgeom CUnit' \
+ ' make start|stop run or stop deps image under QEMU' \
+ ' make runtime-files render systemd/guest runtime examples' \
+ ' make deps-clean remove dependency layer only'
+
+all: fetch seed deps verify runtime-files
+
+fetch:
+ @scripts/fetch-freebsd.sh
+
+seed: fetch
+ @scripts/seed-image.sh
+
+deps: seed
+ @scripts/build-deps.sh
+
+verify: deps
+ @scripts/verify-postgis.sh
+
+runtime-files:
+ @scripts/render-runtime-files.sh
+
+start:
+ @scripts/vm.sh start images/deps.qcow2
+
+stop:
+ @scripts/vm.sh stop
+
+status:
+ @scripts/vm.sh status
+
+ssh:
+ @scripts/ssh.sh
+
+package-lock:
+ @scripts/package-lock.sh
+
+clean:
+ rm -rf images logs artifacts .tmp
+
+deps-clean:
+ rm -f images/deps.qcow2 images/deps.qcow2.ready artifacts/deps-packages.tsv artifacts/package-lock.txt
diff --git a/freebsd/freebsd14_runner/README.md b/freebsd/freebsd14_runner/README.md
new file mode 100644
index 0000000..b22c521
--- /dev/null
+++ b/freebsd/freebsd14_runner/README.md
@@ -0,0 +1,29 @@
+# FreeBSD 14 PostGIS runner
+
+This directory contains the reproducible FreeBSD VM recipe for PostGIS-specific CI.
+It fetches a FreeBSD cloud image, seeds SSH access, installs the package set in
+`config/packages.txt`, records a package lock, and verifies the guest by building
+PostGIS `liblwgeom` and running its CUnit suite.
+
+From a fresh host with QEMU/KVM and project sources available:
+
+```sh
+git clone https://gitea.osgeo.org/postgis/postgis-buildbots.git
+cd postgis-buildbots/freebsd/freebsd14_runner
+cp config/runner.env config/runner.local.env
+$EDITOR config/runner.local.env
+make all
+```
+
+The checked-in `config/runner.env` is intentionally a template. A human supplies
+the host path to a PostGIS source checkout, SSH access material, hardware
+virtualisation, and the Woodpecker server/agent secret. Generated VM images,
+logs, downloads, runtime artifacts, and private keys are ignored.
+
+`make runtime-files` renders the host systemd unit and guest Woodpecker examples
+for the current checkout path. The checked-in examples use `/srv/postgis` as the
+install root; regenerate them after installing elsewhere.
+
+The sibling lane verified this recipe on 2026-07-26: dependency-layer rebuild
+`386.47s`, boot-to-ready `14s`, verify wall `65.14s`, and liblwgeom CUnit
+`358` tests / `5710` assertions with `0` failures.
diff --git a/freebsd/freebsd14_runner/config/package-lock.txt b/freebsd/freebsd14_runner/config/package-lock.txt
new file mode 100644
index 0000000..32ff134
--- /dev/null
+++ b/freebsd/freebsd14_runner/config/package-lock.txt
@@ -0,0 +1,110 @@
+abseil-20250127.1_1
+autoconf-2.73
+autoconf-switch-20220527
+automake-1.18.1
+bash-5.3.15
+binutils-2.44,1
+bison-3.8.2_2,1
+boost-libs-1.89.0_2
+brotli-1.2.0,1
+ca_root_nss-3.125
+cunit-2.1.3_3
+curl-8.21.0
+expat-2.8.2
+firstboot-freebsd-update-1.4
+fontconfig-2.17.1,1
+freetype2-2.14.3
+freexl-2.0.0
+gcc14-14.2.0_6
+geos-3.14.1
+gettext-runtime-1.0_1
+giflib-5.2.2
+git-2.54.0
+gmake-4.4.1
+gmp-6.3.0
+gnupg-2.5.20_1
+gnutls-3.8.13
+gpgme-2.1.2
+gpgmepp-2.1.0
+hdf5-1.12.2_2,1
+icu-76.1,1
+indexinfo-0.3.1_1
+jbigkit-2.1_3
+jpeg-turbo-3.1.4.1
+json-c-0.19
+jsoncpp-1.9.6_1
+lcms2-2.19.1
+lerc-4.1.0
+libaec-1.1.7
+libassuan-3.0.2
+libdeflate-1.25
+libedit-3.1.20260512,1
+libffi-3.6.0
+libgcrypt-1.12.2
+libgeotiff-1.7.4_2
+libgpg-error-1.61
+libiconv-1.18_1
+libidn2-2.3.8
+libkml-1.3.0_31
+libksba-1.8.0
+liblz4-1.10.0_2,1
+libnghttp2-1.69.0
+libpsl-0.22.0
+librttopo-1.1.0
+libssh2-1.11.1_1,3
+libtasn1-4.21.0
+libtextstyle-1.0
+libtool-2.5.4_1
+libunistring-1.4.2
+libxml2-2.15.3
+llvm19-19.1.7_4
+lua53-5.3.6_1
+m4-1.4.21,1
+minizip-1.3.1
+mpc-1.4.1
+mpdecimal-4.0.1
+mpfr-4.2.2,1
+netcdf-4.10.0
+nettle-3.10.2
+npth-1.8
+nspr-4.39
+nss-3.126
+openjpeg-2.5.4_2
+p11-kit-0.26.2
+p5-Authen-SASL-2.1900
+p5-Crypt-URandom-0.54
+p5-Digest-HMAC-1.05
+p5-Error-0.17030
+p5-IO-Socket-SSL-2.099
+p5-MIME-Base32-1.303
+p5-MIME-Base64-3.16
+p5-Mozilla-CA-20250602
+p5-Net-SSLeay-1.94
+p5-URI-5.34
+pcre2-10.47_1
+perl5-5.42.2
+pinentry-1.3.3
+pinentry-curses-1.3.3
+pkg-2.7.5
+pkgconf-2.4.3_1,1
+png-1.6.58
+poppler-26.04.0_1
+poppler-data-0.4.12
+postgresql16-client-16.14
+postgresql16-server-16.14
+proj-9.8.1,1
+protobuf-29.6,1
+protobuf-c-1.5.1_4
+python312-3.12.13_3
+qhull-8.0.2_3,1
+readline-8.3.3
+sfcgal-2.3.0
+spatialite-5.1.0_6
+sqlite3-3.53.3,1
+sudo-1.9.17p2_2
+tiff-4.7.1_1
+uriparser-1.0.2
+webp-1.6.0
+wget-1.25.0
+xerces-c3-3.3.0
+zstd-1.5.7_2
diff --git a/freebsd/freebsd14_runner/config/packages.txt b/freebsd/freebsd14_runner/config/packages.txt
new file mode 100644
index 0000000..c3e2421
--- /dev/null
+++ b/freebsd/freebsd14_runner/config/packages.txt
@@ -0,0 +1,26 @@
+# PostGIS build and liblwgeom CUnit dependencies.
+autoconf
+automake
+bison
+cunit
+gdal
+geos
+gmake
+json-c
+libiconv
+libtool
+libxml2
+pcre2
+perl5
+pkgconf
+postgresql16-client
+postgresql16-server
+proj
+protobuf-c
+sfcgal
+sqlite3
+wget
+ca_root_nss
+git
+bash
+sudo
diff --git a/freebsd/freebsd14_runner/config/runner.env b/freebsd/freebsd14_runner/config/runner.env
new file mode 100644
index 0000000..2e39c7d
--- /dev/null
+++ b/freebsd/freebsd14_runner/config/runner.env
@@ -0,0 +1,27 @@
+# Declared inputs for the reproducible FreeBSD PostGIS CI runner.
+FREEBSD_VERSION="14.4-RELEASE"
+FREEBSD_ARCH="amd64"
+FREEBSD_IMAGE_FLAVOR="BASIC-CLOUDINIT-ufs"
+FREEBSD_IMAGE_FORMAT="qcow2"
+FREEBSD_IMAGE_BASE_URL="https://download.freebsd.org/releases/VM-IMAGES"
+
+DISK_SIZE="80G"
+MEMORY_MB="16384"
+CPUS="16"
+SSH_HOST="127.0.0.1"
+SSH_PORT="2422"
+SSH_USER="root"
+SSH_KEY="secrets/freebsd_runner_ed25519"
+
+QEMU_NETDEV="user,id=net0,hostfwd=tcp:127.0.0.1:2422-:22"
+QEMU_DEVICE="virtio-net-pci,netdev=net0"
+QEMU_MACHINE="q35,accel=kvm"
+QEMU_CPU="host"
+
+POSTGIS_SRC="/srv/postgis/postgis-src"
+GUEST_WORKDIR="/usr/local/src/postgis"
+VERIFY_JOBS="16"
+
+WOODPECKER_AGENT_VERSION="3.9.0"
+WOODPECKER_SERVER="https://woodpecker.example.invalid"
+WOODPECKER_SECRET_FILE="/usr/local/etc/woodpecker/agent-secret"
diff --git a/freebsd/freebsd14_runner/guest/install-woodpecker-agent.sh b/freebsd/freebsd14_runner/guest/install-woodpecker-agent.sh
new file mode 100755
index 0000000..8044c05
--- /dev/null
+++ b/freebsd/freebsd14_runner/guest/install-woodpecker-agent.sh
@@ -0,0 +1,47 @@
+#!/bin/sh
+set -eu
+: "${WOODPECKER_AGENT_VERSION:?set WOODPECKER_AGENT_VERSION}"
+: "${WOODPECKER_SERVER:?set WOODPECKER_SERVER}"
+ARCH=$(uname -m)
+case "$ARCH" in
+ amd64|x86_64) WOODPECKER_ARCH=amd64 ;;
+ arm64|aarch64) WOODPECKER_ARCH=arm64 ;;
+ *) echo "unsupported architecture: $ARCH" >&2; exit 1 ;;
+esac
+URL="https://github.com/woodpecker-ci/woodpecker/releases/download/v${WOODPECKER_AGENT_VERSION}/woodpecker-agent_freebsd_${WOODPECKER_ARCH}.tar.gz"
+install -d -m 0755 /usr/local/bin /usr/local/etc/woodpecker
+fetch -o /tmp/woodpecker-agent.tar.gz "$URL"
+tar -C /usr/local/bin -xzf /tmp/woodpecker-agent.tar.gz woodpecker-agent
+chmod 0755 /usr/local/bin/woodpecker-agent
+cat > /usr/local/etc/woodpecker/agent.env <<ENV
+WOODPECKER_SERVER=${WOODPECKER_SERVER}
+WOODPECKER_AGENT_SECRET_FILE=/usr/local/etc/woodpecker/agent-secret
+WOODPECKER_BACKEND=local
+WOODPECKER_MAX_WORKFLOWS=1
+WOODPECKER_HOSTNAME=freebsd-postgis-runner
+ENV
+if [ ! -f /usr/local/etc/woodpecker/agent-secret ]; then
+ cat > /usr/local/etc/woodpecker/agent-secret.example <<'SECRET'
+replace-with-secret-from-woodpecker-server
+SECRET
+ chmod 0600 /usr/local/etc/woodpecker/agent-secret.example
+fi
+cat > /usr/local/etc/rc.d/woodpecker_agent <<'RC'
+#!/bin/sh
+# PROVIDE: woodpecker_agent
+# REQUIRE: NETWORKING LOGIN
+# KEYWORD: shutdown
+. /etc/rc.subr
+name="woodpecker_agent"
+rcvar="woodpecker_agent_enable"
+command="/usr/local/bin/woodpecker-agent"
+woodpecker_agent_env_file="/usr/local/etc/woodpecker/agent.env"
+load_rc_config "$name"
+: ${woodpecker_agent_enable:="NO"}
+if [ -f "$woodpecker_agent_env_file" ]; then
+ export $(grep -v '^#' "$woodpecker_agent_env_file" | xargs)
+fi
+run_rc_command "$1"
+RC
+chmod 0755 /usr/local/etc/rc.d/woodpecker_agent
+sysrc woodpecker_agent_enable=YES
diff --git a/freebsd/freebsd14_runner/scripts/build-deps.sh b/freebsd/freebsd14_runner/scripts/build-deps.sh
new file mode 100755
index 0000000..f59cfd6
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/build-deps.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+need qemu-img; need ssh; need scp
+if [ -f images/deps.qcow2.ready ]; then
+ log "dependency image already ready"
+ exit 0
+fi
+./scripts/vm.sh stop >/dev/null || true
+rm -f images/deps.qcow2 images/deps.qcow2.ready
+cp images/seeded.qcow2 images/deps.qcow2
+qemu-img info images/deps.qcow2 > artifacts/deps-qemu-img-info.before.txt
+./scripts/vm.sh start images/deps.qcow2
+./scripts/wait-ssh.sh
+if [ -s config/package-lock.txt ]; then
+ pkg_source=config/package-lock.txt
+else
+ pkg_source=config/packages.txt
+fi
+pkg_list=$(awk 'NF && $1 !~ /^#/ {print $1}' "$pkg_source" | awk '!seen[$0]++')
+remote_list=$(printf '%s ' $pkg_list)
+log "installing packages: $remote_list"
+start=$(date +%s)
+ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "set -eu; env ASSUME_ALWAYS_YES=yes pkg bootstrap -f; pkg update -f; pkg install -y $remote_list; pkg clean -ay; service sshd enable || true" | tee logs/pkg-install.log
+end=$(date +%s); printf 'dependency_layer\t%s\n' "$((end-start))" >> artifacts/timings.tsv
+./scripts/package-lock.sh
+ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'pkg info | sort' > artifacts/deps-packages.tsv
+./scripts/vm.sh stop
+qemu-img info images/deps.qcow2 > artifacts/deps-qemu-img-info.after.txt
+touch images/deps.qcow2.ready
+log "dependency image ready"
diff --git a/freebsd/freebsd14_runner/scripts/common.sh b/freebsd/freebsd14_runner/scripts/common.sh
new file mode 100755
index 0000000..34fdd13
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/common.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+set -euo pipefail
+ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+cd "$ROOT"
+export TMPDIR="$ROOT/.tmp"
+mkdir -p "$TMPDIR" downloads images logs artifacts secrets
+source config/runner.env
+if [ -n "${FREEBSD_IMAGE_FLAVOR:-}" ]; then
+ IMAGE_NAME="FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}-${FREEBSD_IMAGE_FLAVOR}.${FREEBSD_IMAGE_FORMAT}"
+else
+ IMAGE_NAME="FreeBSD-${FREEBSD_VERSION}-${FREEBSD_ARCH}.${FREEBSD_IMAGE_FORMAT}"
+fi
+IMAGE_XZ="${IMAGE_NAME}.xz"
+RELEASE_URL="${FREEBSD_IMAGE_BASE_URL}/${FREEBSD_VERSION}/${FREEBSD_ARCH}/Latest/${IMAGE_XZ}"
+CHECKSUM_URL="${FREEBSD_IMAGE_BASE_URL}/${FREEBSD_VERSION}/${FREEBSD_ARCH}/Latest/CHECKSUM.SHA256"
+SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5 -i "$SSH_KEY" -p "$SSH_PORT")
+need() { command -v "$1" >/dev/null || { echo "missing required command: $1" >&2; exit 1; }; }
+log() { printf '[%(%FT%T%z)T] %s\n' -1 "$*"; }
+time_cmd() { local label=$1; shift; local start end; start=$(date +%s); "$@"; end=$(date +%s); printf '%s\t%s\n' "$label" "$((end-start))" >> artifacts/timings.tsv; }
+ensure_kvm() {
+ if [[ "$QEMU_MACHINE" == *accel=kvm* ]] && [ ! -r /dev/kvm ]; then
+ if command -v sudo >/dev/null && sudo -n true 2>/dev/null; then
+ sudo setfacl -m "u:$(id -un):rw" /dev/kvm
+ fi
+ fi
+ if [[ "$QEMU_MACHINE" == *accel=kvm* ]] && [ ! -r /dev/kvm ]; then
+ echo "KVM requested but /dev/kvm is not readable by $(id -un); add the user to kvm or run: sudo setfacl -m u:$(id -un):rw /dev/kvm" >&2
+ exit 1
+ fi
+}
diff --git a/freebsd/freebsd14_runner/scripts/fetch-freebsd.sh b/freebsd/freebsd14_runner/scripts/fetch-freebsd.sh
new file mode 100755
index 0000000..f0d1853
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/fetch-freebsd.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+need curl; need xz; need sha256sum
+log "fetching checksum manifest"
+curl -fsSL -o downloads/CHECKSUM.SHA256 "$CHECKSUM_URL"
+if [ ! -s "downloads/$IMAGE_XZ" ]; then
+ log "downloading $RELEASE_URL"
+ time_cmd fetch_release curl -fL --retry 4 --retry-delay 5 -o "downloads/$IMAGE_XZ" "$RELEASE_URL"
+else
+ log "using existing downloads/$IMAGE_XZ"
+fi
+expected=$(awk -v f="$IMAGE_XZ" '$0 ~ f { gsub(/[()]/,"",$2); print $4 " downloads/" $2 }' downloads/CHECKSUM.SHA256 | head -1)
+[ -n "$expected" ] || { echo "checksum for $IMAGE_XZ not found" >&2; exit 1; }
+printf '%s\n' "$expected" | sha256sum -c -
+log "verified $IMAGE_XZ"
diff --git a/freebsd/freebsd14_runner/scripts/package-lock.sh b/freebsd/freebsd14_runner/scripts/package-lock.sh
new file mode 100755
index 0000000..e382412
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/package-lock.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+out=artifacts/package-lock.txt
+ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "pkg query '%n-%v'" | sort > "$out"
+cp "$out" config/package-lock.txt
+log "wrote $out and config/package-lock.txt"
diff --git a/freebsd/freebsd14_runner/scripts/render-runtime-files.sh b/freebsd/freebsd14_runner/scripts/render-runtime-files.sh
new file mode 100755
index 0000000..eb09c24
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/render-runtime-files.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+cat > systemd/freebsd-postgis-runner.service <<EOF2
+[Unit]
+Description=FreeBSD PostGIS CI runner VM
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=forking
+WorkingDirectory=$ROOT
+Environment=TMPDIR=$ROOT/.tmp
+ExecStart=$ROOT/scripts/vm.sh start $ROOT/images/deps.qcow2
+ExecStartPost=$ROOT/scripts/wait-ssh.sh
+ExecStop=$ROOT/scripts/vm.sh stop
+PIDFile=$ROOT/logs/freebsd-runner.pid
+TimeoutStartSec=600
+TimeoutStopSec=120
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
+EOF2
+cat > systemd/woodpecker-agent.example.env <<EOF2
+WOODPECKER_SERVER=$WOODPECKER_SERVER
+WOODPECKER_AGENT_SECRET_FILE=$WOODPECKER_SECRET_FILE
+WOODPECKER_BACKEND=local
+WOODPECKER_MAX_WORKFLOWS=1
+WOODPECKER_HOSTNAME=freebsd-postgis-runner
+EOF2
+cat > systemd/freebsd-guest-woodpecker-agent.service <<'EOF2'
+[Unit]
+Description=Woodpecker CI agent, local backend
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+EnvironmentFile=/usr/local/etc/woodpecker/agent.env
+ExecStart=/usr/local/bin/woodpecker-agent
+Restart=always
+RestartSec=10
+User=root
+Group=wheel
+
+[Install]
+WantedBy=multi-user.target
+EOF2
+cat > systemd/WOODPECKER.md <<EOF2
+The FreeBSD guest agent binary is installed by guest/install-woodpecker-agent.sh.
+It downloads Woodpecker agent v$WOODPECKER_AGENT_VERSION from the upstream GitHub
+release asset woodpecker-agent_freebsd_amd64.tar.gz, writes
+/usr/local/etc/woodpecker/agent.env, expects the secret at
+$WOODPECKER_SECRET_FILE, and enables an rc.d service using the local backend.
+EOF2
+cat > secrets/woodpecker-agent-secret.example <<'EOF2'
+# Place the Woodpecker agent secret in the real file named by WOODPECKER_AGENT_SECRET_FILE.
+# This example is intentionally not a secret.
+replace-with-secret-from-woodpecker-server
+EOF2
+log "rendered systemd and Woodpecker examples"
diff --git a/freebsd/freebsd14_runner/scripts/seed-image.sh b/freebsd/freebsd14_runner/scripts/seed-image.sh
new file mode 100755
index 0000000..d81509c
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/seed-image.sh
@@ -0,0 +1,58 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+need qemu-img; need xz; need ssh-keygen; need qemu-system-x86_64; need socat
+if [ ! -f "$SSH_KEY" ]; then
+ log "creating local runner SSH key at $SSH_KEY"
+ ssh-keygen -q -t ed25519 -N '' -f "$SSH_KEY" -C "freebsd-runner@$HOSTNAME"
+fi
+if [ -f images/seeded.qcow2.ready ]; then
+ log "seeded image already ready"
+ exit 0
+fi
+./scripts/vm.sh stop >/dev/null || true
+rm -f images/seeded.qcow2.ready images/seed.iso logs/seed-console.*
+if [ ! -f images/release.qcow2 ] || [ "$(cat images/release.qcow2.source 2>/dev/null || true)" != "$IMAGE_XZ" ]; then
+ rm -f images/release.qcow2 images/seeded.qcow2
+ log "expanding release image"
+ time_cmd expand_release xz -dkc "downloads/$IMAGE_XZ" > images/release.qcow2
+ printf '%s\n' "$IMAGE_XZ" > images/release.qcow2.source
+fi
+if [ ! -f images/seeded.qcow2 ]; then
+ cp images/release.qcow2 images/seeded.qcow2
+ qemu-img resize images/seeded.qcow2 "$DISK_SIZE"
+fi
+pub=$(cat "$SSH_KEY.pub")
+cat > .tmp/serial-seed.commands <<EOF2
+mkdir -p /root/.ssh
+printf '%s\n' '$pub' > /root/.ssh/authorized_keys
+chmod 700 /root/.ssh
+chmod 600 /root/.ssh/authorized_keys
+printf '\nPermitRootLogin prohibit-password\nPubkeyAuthentication yes\nPasswordAuthentication no\nUseDNS no\n' >> /etc/ssh/sshd_config
+sysrc sshd_enable=YES
+sysrc growfs_enable=YES
+sysrc ifconfig_DEFAULT=DHCP
+service sshd restart || service sshd start
+touch /root/postgis-runner-seeded
+echo __POSTGIS_SERIAL_DONE__
+EOF2
+log "booting once and seeding through serial console"
+ensure_kvm
+start=$(date +%s)
+qemu-system-x86_64 \
+ -name freebsd-postgis-runner-seed \
+ -machine "$QEMU_MACHINE" -cpu "$QEMU_CPU" \
+ -smp "$CPUS" -m "$MEMORY_MB" \
+ -drive "file=images/seeded.qcow2,if=virtio,cache=writeback,discard=unmap" \
+ -netdev "$QEMU_NETDEV" -device "$QEMU_DEVICE" \
+ -display none -serial unix:logs/seed-console.sock,server,nowait \
+ -monitor unix:logs/seed-console.monitor,server,nowait \
+ -pidfile logs/seed-console.pid -daemonize
+./scripts/serial-run.py logs/seed-console.sock .tmp/serial-seed.commands logs/seed-console.log
+./scripts/wait-ssh.sh
+printf 'system_powerdown\n' | socat - UNIX-CONNECT:logs/seed-console.monitor >/dev/null || true
+for _ in $(seq 1 120); do [ -f logs/seed-console.pid ] && kill -0 "$(cat logs/seed-console.pid)" 2>/dev/null || break; sleep 1; done
+[ -f logs/seed-console.pid ] && kill "$(cat logs/seed-console.pid)" 2>/dev/null || true
+end=$(date +%s); printf 'seed_image_boot\t%s\n' "$((end-start))" >> artifacts/timings.tsv
+touch images/seeded.qcow2.ready
+qemu-img info images/seeded.qcow2 > artifacts/seeded-qemu-img-info.txt
+log "seeded image ready"
diff --git a/freebsd/freebsd14_runner/scripts/serial-run.py b/freebsd/freebsd14_runner/scripts/serial-run.py
new file mode 100755
index 0000000..a02437f
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/serial-run.py
@@ -0,0 +1,41 @@
+#!/usr/bin/env python3
+import os, select, socket, sys, time
+sock_path, command_file, log_path = sys.argv[1:4]
+for _ in range(300):
+ if os.path.exists(sock_path): break
+ time.sleep(0.1)
+else:
+ raise SystemExit(f"serial socket not found: {sock_path}")
+sock = socket.socket(socket.AF_UNIX); sock.connect(sock_path); sock.setblocking(False)
+log = open(log_path, "ab", buffering=0)
+buf = b""
+def read_until(marker, timeout):
+ global buf
+ needle = marker.encode(); end = time.time() + timeout
+ if needle in buf:
+ return
+ while time.time() < end:
+ readable, _, _ = select.select([sock], [], [], 1)
+ if not readable: continue
+ data = sock.recv(8192)
+ if not data: raise SystemExit("serial socket closed")
+ log.write(data); sys.stdout.buffer.write(data); sys.stdout.flush()
+ buf += data
+ if needle in buf: return
+ raise SystemExit(f"timeout waiting for {marker!r}")
+def wait_prompt(timeout=120):
+ global buf
+ read_until("root@", timeout)
+ read_until("#", 30)
+ buf = b""
+read_until("login:", 1800)
+sock.sendall(b"root\r")
+buf = b""
+wait_prompt(180)
+for raw_line in open(command_file, "r", encoding="utf-8"):
+ line = raw_line.rstrip("\n")
+ if not line or line.startswith("#"):
+ continue
+ sock.sendall(line.encode() + b"\r")
+ time.sleep(0.05)
+ wait_prompt(600)
diff --git a/freebsd/freebsd14_runner/scripts/ssh.sh b/freebsd/freebsd14_runner/scripts/ssh.sh
new file mode 100755
index 0000000..cf4e026
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/ssh.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+exec ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "$@"
diff --git a/freebsd/freebsd14_runner/scripts/verify-postgis.sh b/freebsd/freebsd14_runner/scripts/verify-postgis.sh
new file mode 100755
index 0000000..79ea747
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/verify-postgis.sh
@@ -0,0 +1,24 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+need git; need ssh
+[ -d "$POSTGIS_SRC/.git" ] || { echo "POSTGIS_SRC is not a git clone: $POSTGIS_SRC" >&2; exit 1; }
+./scripts/vm.sh stop >/dev/null || true
+./scripts/vm.sh start images/deps.qcow2
+./scripts/wait-ssh.sh
+log "copying PostGIS source into guest"
+git -C "$POSTGIS_SRC" rev-parse HEAD > artifacts/postgis-head.txt
+ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "rm -rf '$GUEST_WORKDIR'; mkdir -p '$GUEST_WORKDIR'"
+git -C "$POSTGIS_SRC" archive --format=tar HEAD | ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "tar -C '$GUEST_WORKDIR' -xf -"
+log "building and running liblwgeom CUnit"
+start=$(date +%s)
+set +e
+ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" "cd '$GUEST_WORKDIR' && ./autogen.sh && ./configure --without-raster --without-topology --without-address-standardizer && gmake -j$VERIFY_JOBS -C liblwgeom check" 2>&1 | tee logs/verify-liblwgeom-cunit.log
+status=${PIPESTATUS[0]}
+set -e
+end=$(date +%s); printf 'verify_liblwgeom_cunit\t%s\n' "$((end-start))" >> artifacts/timings.tsv
+./scripts/vm.sh stop
+if [ "$status" -ne 0 ]; then
+ echo "PostGIS liblwgeom CUnit verification failed" >&2
+ exit "$status"
+fi
+log "verification passed"
diff --git a/freebsd/freebsd14_runner/scripts/vm.sh b/freebsd/freebsd14_runner/scripts/vm.sh
new file mode 100755
index 0000000..93bf24a
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/vm.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+need qemu-system-x86_64; need socat
+cmd=${1:-status}
+image=${2:-images/deps.qcow2}
+seed_iso=${3:-}
+pidfile=logs/freebsd-runner.pid
+monitor=logs/freebsd-runner.monitor
+serial=logs/freebsd-runner.serial.log
+is_running() { [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; }
+case "$cmd" in
+ start)
+ if is_running; then log "VM already running pid $(cat "$pidfile")"; exit 0; fi
+ rm -f "$monitor"
+ log "starting VM from $image"
+ ensure_kvm
+ extra_drive=()
+ if [ -n "$seed_iso" ]; then
+ extra_drive=(-drive "file=$seed_iso,media=cdrom,readonly=on")
+ fi
+ qemu-system-x86_64 \
+ -name freebsd-postgis-runner \
+ -machine "$QEMU_MACHINE" -cpu "$QEMU_CPU" \
+ -smp "$CPUS" -m "$MEMORY_MB" \
+ -drive "file=$image,if=virtio,cache=writeback,discard=unmap" \
+ "${extra_drive[@]}" \
+ -netdev "$QEMU_NETDEV" -device "$QEMU_DEVICE" \
+ -display none -serial "file:$serial" \
+ -monitor "unix:$monitor,server,nowait" \
+ -pidfile "$pidfile" -daemonize
+ ;;
+ stop)
+ if ! is_running; then log "VM not running"; rm -f "$pidfile"; exit 0; fi
+ log "requesting ACPI shutdown"
+ printf 'system_powerdown\n' | socat - UNIX-CONNECT:"$monitor" >/dev/null || true
+ for _ in $(seq 1 90); do is_running || { rm -f "$pidfile"; log "VM stopped"; exit 0; }; sleep 1; done
+ log "ACPI shutdown timed out; killing VM"
+ kill "$(cat "$pidfile")" || true
+ rm -f "$pidfile"
+ ;;
+ status)
+ if is_running; then echo "running pid $(cat "$pidfile")"; else echo stopped; fi
+ ;;
+ *) echo "usage: $0 start [image]|stop|status" >&2; exit 2 ;;
+esac
diff --git a/freebsd/freebsd14_runner/scripts/wait-ssh.sh b/freebsd/freebsd14_runner/scripts/wait-ssh.sh
new file mode 100755
index 0000000..9413256
--- /dev/null
+++ b/freebsd/freebsd14_runner/scripts/wait-ssh.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+source "$(dirname "$0")/common.sh"
+start=$(date +%s)
+for _ in $(seq 1 240); do
+ if ssh "${SSH_OPTS[@]}" "${SSH_USER}@${SSH_HOST}" 'uname -a >/dev/null' 2>/dev/null; then
+ end=$(date +%s); printf 'boot_to_ssh\t%s\n' "$((end-start))" >> artifacts/timings.tsv; exit 0
+ fi
+ sleep 2
+done
+echo "SSH did not become ready" >&2
+exit 1
diff --git a/freebsd/freebsd14_runner/systemd/WOODPECKER.md b/freebsd/freebsd14_runner/systemd/WOODPECKER.md
new file mode 100644
index 0000000..164fe24
--- /dev/null
+++ b/freebsd/freebsd14_runner/systemd/WOODPECKER.md
@@ -0,0 +1,5 @@
+The FreeBSD guest agent binary is installed by guest/install-woodpecker-agent.sh.
+It downloads Woodpecker agent v3.9.0 from the upstream GitHub
+release asset woodpecker-agent_freebsd_amd64.tar.gz, writes
+/usr/local/etc/woodpecker/agent.env, expects the secret at
+/usr/local/etc/woodpecker/agent-secret, and enables an rc.d service using the local backend.
diff --git a/freebsd/freebsd14_runner/systemd/freebsd-guest-woodpecker-agent.service b/freebsd/freebsd14_runner/systemd/freebsd-guest-woodpecker-agent.service
new file mode 100644
index 0000000..a1648fb
--- /dev/null
+++ b/freebsd/freebsd14_runner/systemd/freebsd-guest-woodpecker-agent.service
@@ -0,0 +1,15 @@
+[Unit]
+Description=Woodpecker CI agent, local backend
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+EnvironmentFile=/usr/local/etc/woodpecker/agent.env
+ExecStart=/usr/local/bin/woodpecker-agent
+Restart=always
+RestartSec=10
+User=root
+Group=wheel
+
+[Install]
+WantedBy=multi-user.target
diff --git a/freebsd/freebsd14_runner/systemd/freebsd-postgis-runner.service b/freebsd/freebsd14_runner/systemd/freebsd-postgis-runner.service
new file mode 100644
index 0000000..48c50f7
--- /dev/null
+++ b/freebsd/freebsd14_runner/systemd/freebsd-postgis-runner.service
@@ -0,0 +1,20 @@
+[Unit]
+Description=FreeBSD PostGIS CI runner VM
+After=network-online.target
+Wants=network-online.target
+
+[Service]
+Type=forking
+WorkingDirectory=/srv/postgis/freebsd14_runner
+Environment=TMPDIR=/srv/postgis/freebsd14_runner/.tmp
+ExecStart=/srv/postgis/freebsd14_runner/scripts/vm.sh start /srv/postgis/freebsd14_runner/images/deps.qcow2
+ExecStartPost=/srv/postgis/freebsd14_runner/scripts/wait-ssh.sh
+ExecStop=/srv/postgis/freebsd14_runner/scripts/vm.sh stop
+PIDFile=/srv/postgis/freebsd14_runner/logs/freebsd-runner.pid
+TimeoutStartSec=600
+TimeoutStopSec=120
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=multi-user.target
diff --git a/freebsd/freebsd14_runner/systemd/woodpecker-agent.example.env b/freebsd/freebsd14_runner/systemd/woodpecker-agent.example.env
new file mode 100644
index 0000000..f8e5091
--- /dev/null
+++ b/freebsd/freebsd14_runner/systemd/woodpecker-agent.example.env
@@ -0,0 +1,5 @@
+WOODPECKER_SERVER=https://woodpecker.example.invalid
+WOODPECKER_AGENT_SECRET_FILE=/usr/local/etc/woodpecker/agent-secret
+WOODPECKER_BACKEND=local
+WOODPECKER_MAX_WORKFLOWS=1
+WOODPECKER_HOSTNAME=freebsd-postgis-runner
-----------------------------------------------------------------------
hooks/post-receive
--
PostGIS Buildbots; Jenkins jobs and instructions for setting up winnie and debbie bots.
More information about the postgis-tickets
mailing list