Skip to content

release: cut v1.2.1 #25

release: cut v1.2.1

release: cut v1.2.1 #25

Workflow file for this run

name: release
# Triggers:
# * tag push (vX.Y.Z) — full release: builds packages for every distro
# in the matrix, then attaches them to a GitHub Release.
# * manual dispatch — same matrix, but artifacts go to the workflow run
# only (no Release object created). Useful for testing the pipeline
# between proper releases.
# * push to main — regression gate: full matrix, build + validate +
# packaged e2e, artifacts on the run only. Catches packaging bugs at
# merge time instead of at tag time, and yields current-main packages
# for every distro. Path-filtered so doc-only commits skip it (paths
# are not evaluated for tag pushes, so releases are unaffected).
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- 'src/**'
- 'proto/**'
- 'pkg/**'
- 'tests/e2e/**'
- 'CMakeLists.txt'
- 'configure.ac'
- 'Makefile.am'
- 'config_m4/**'
- 'autogen.sh'
- 'VERSION'
- 'grpc-collector.pc.in'
- '.dockerignore'
- '.github/workflows/release.yml'
workflow_dispatch:
# Superseded main-push runs are cancelled; tag runs never are.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: ${{ github.ref_type != 'tag' }}
permissions:
contents: write # creates the GitHub Release on tag push.
packages: write # for the optional ghcr.io image push.
jobs:
packages:
name: ${{ matrix.distro.name }}
runs-on: ubuntu-latest
container:
image: ${{ matrix.distro.image }}
strategy:
fail-fast: false
matrix:
distro:
- { name: "debian-12 (bookworm)", image: "debian:12" }
- { name: "debian-13 (trixie)", image: "debian:13" }
- { name: "ubuntu-24.04 (noble)", image: "ubuntu:24.04" }
- { name: "fedora", image: "fedora:latest" }
steps:
# actions/checkout needs `git` inside the container; install it
# first on the rpm distros where the base image is minimal.
- name: bootstrap git (rpm distros)
if: contains(matrix.distro.image, 'rocky') || contains(matrix.distro.image, 'fedora')
run: |
# --allowerasing lets dnf swap curl-minimal for curl on rocky:9
dnf install -y -q --allowerasing git tar gzip curl >/dev/null
- name: bootstrap git (deb distros)
if: contains(matrix.distro.image, 'debian') || contains(matrix.distro.image, 'ubuntu')
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y --no-install-recommends git ca-certificates curl tar >/dev/null
- uses: actions/checkout@v5
- name: install build deps
run: bash pkg/build/install_deps.sh
- name: install nfpm
run: |
NFPM_VERSION=v2.41.3
NFPM_ARCH=x86_64
curl -fsSL "https://github.com/goreleaser/nfpm/releases/download/${NFPM_VERSION}/nfpm_${NFPM_VERSION#v}_Linux_${NFPM_ARCH}.tar.gz" \
| tar -xz -C /usr/local/bin nfpm
nfpm --version
- name: derive version
id: version
# Deb container images default this step to dash, which lacks
# ${VAR::7} — same trap the validate steps already pin bash for.
shell: bash
run: |
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
VERSION="${GITHUB_REF_NAME#v}"
else
# No '-' or '+': rpm forbids them in Version. Dots only.
VERSION="$(sed 's/^v//' VERSION 2>/dev/null || echo 0.0.0).dev.${GITHUB_SHA::7}"
fi
echo "VERSION=${VERSION}" >> "${GITHUB_ENV}"
echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
- name: build collector + library
run: bash pkg/build/build_collector.sh
- name: nfpm pack (standalone + library)
run: bash pkg/build/make_packages.sh
- name: smoke-test the package
run: |
# Install the freshly built standalone .deb/.rpm into the same
# container; verify the binary loads and prints --help/-V.
set -e
if [ "${PKG_FORMAT}" = "deb" ]; then
apt-get install -y -qq ./dist/mdt-dialout-collector_${VERSION}_${DISTRO_TAG}_amd64.deb
else
dnf install -y -q ./dist/mdt-dialout-collector_${VERSION}_${DISTRO_TAG}_amd64.rpm
fi
/opt/mdt-dialout-collector/bin/mdt_dialout_collector -V || true
ls -l /lib/systemd/system/mdt-dialout-collector.service
ls -l /etc/opt/mdt-dialout-collector/
- name: upload artifacts
uses: actions/upload-artifact@v6
with:
name: packages-${{ env.DISTRO_TAG }}
path: dist/*
validate:
# Install the freshly built packages in a CLEAN container of the
# target distro (no build deps). Catches missing/wrong runtime
# depends and unresolved transitive deps on system gRPC — neither
# of which the build-container smoke step can see.
name: validate ${{ matrix.distro.name }}
needs: packages
runs-on: ubuntu-latest
container:
image: ${{ matrix.distro.image }}
strategy:
fail-fast: false
matrix:
distro:
- { name: "debian-12 (bookworm)", image: "debian:12", tag: "bookworm" }
- { name: "debian-13 (trixie)", image: "debian:13", tag: "trixie" }
- { name: "ubuntu-24.04 (noble)", image: "ubuntu:24.04", tag: "noble" }
- { name: "fedora", image: "fedora:latest", tag: "fc" }
steps:
- name: bootstrap (rpm distros)
if: contains(matrix.distro.image, 'rocky') || contains(matrix.distro.image, 'fedora')
run: dnf install -y -q git tar gzip iproute >/dev/null
- name: bootstrap (deb distros)
if: contains(matrix.distro.image, 'debian') || contains(matrix.distro.image, 'ubuntu')
run: |
export DEBIAN_FRONTEND=noninteractive
# Ubuntu's minimal image excludes man pages via dpkg-cfg path-exclude;
# un-exclude them so the validate file-layout check can find them.
rm -f /etc/dpkg/dpkg.cfg.d/excludes
apt-get update -qq
apt-get install -y --no-install-recommends \
git ca-certificates iproute2 >/dev/null
- uses: actions/checkout@v5
- uses: actions/download-artifact@v7
with:
name: packages-${{ matrix.distro.tag }}
path: dist
- name: locate packages
id: pkgs
# Force bash explicitly: container default may be sh, which lacks shopt.
shell: bash
run: |
# Diagnostic ALWAYS prints, even if locate later fails or skips.
echo "::warning title=shell::$(readlink -f "$0" 2>/dev/null || echo unknown) BASH_VERSION=${BASH_VERSION:-NONE}"
echo "::group::dist tree"
ls -laR dist/ 2>&1 || echo "(dist/ missing)"
echo "::endgroup::"
# Surface the layout as an annotation so future debug rounds don't
# require another commit cycle.
tree_summary=$(ls -mR dist/ 2>&1 | tr '\n' '|')
echo "::warning title=dist layout::${tree_summary}"
# Recursive glob — handles both dist/foo.deb and dist/<name>/foo.deb.
shopt -s nullglob globstar
all=( dist/**/mdt-dialout-collector_*.deb dist/**/mdt-dialout-collector_*.rpm \
dist/**/mdt-dialout-collector-lib_*.deb dist/**/mdt-dialout-collector-lib_*.rpm )
shopt -u nullglob globstar
STANDALONE= ; LIBRARY=
for f in "${all[@]}"; do
case "$(basename "$f")" in
mdt-dialout-collector-lib_*) LIBRARY="$f" ;;
mdt-dialout-collector_*) STANDALONE="$f" ;;
esac
done
echo "STANDALONE=${STANDALONE}"
echo "LIBRARY=${LIBRARY}"
test -n "${STANDALONE}" || { echo "::error::standalone .deb/.rpm not found"; exit 1; }
test -n "${LIBRARY}" || { echo "::error::library .deb/.rpm not found"; exit 1; }
echo "standalone=${STANDALONE}" >> "${GITHUB_OUTPUT}"
echo "library=${LIBRARY}" >> "${GITHUB_OUTPUT}"
# Split the standalone validation into individually-named steps so
# which one fails is visible from the workflow API/UI even without
# access to the per-step log body.
- name: validate — install standalone package
run: |
PKG="${{ steps.pkgs.outputs.standalone }}"
case "${PKG}" in /*|./*) ;; *) PKG="./${PKG}" ;; esac
. /etc/os-release
case "${ID}" in
debian|ubuntu)
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq "${PKG}"
;;
fedora|rocky|rhel|centos|almalinux)
if [ "${ID}" != "fedora" ]; then
dnf install -y -q epel-release >/dev/null
fi
dnf install -y -q "${PKG}"
;;
esac
- name: validate — file layout
shell: bash
run: |
# Enumerate so the failure annotation names the missing file.
ok=1
for path in \
/usr/share/man/man1/mdt_dialout_collector.1.gz \
/opt/mdt-dialout-collector/bin/mdt_dialout_collector \
/usr/bin/mdt_dialout_collector \
/lib/systemd/system/mdt-dialout-collector.service \
/etc/opt/mdt-dialout-collector/mdt_dialout_collector.conf
do
# -e follows symlinks; -L succeeds only on symlinks.
if [ -e "${path}" ] || [ -L "${path}" ]; then
echo " OK ${path}"
else
echo "::error title=missing file::${path}"
ok=0
fi
done
[ "${ok}" = 1 ] || exit 1
- name: validate — launch daemon
shell: bash
run: |
cp pkg/validate/minimal.conf /tmp/mdt-validate.conf
# Plain `&` + disown — `$!` reliably captures the daemon PID. (setsid
# forks-then-execs when the shell is a process-group leader, which
# makes `$!` the dead parent's PID — see network-analytics/mdt-dialout-collector#issue.)
/opt/mdt-dialout-collector/bin/mdt_dialout_collector \
-f /tmp/mdt-validate.conf > /tmp/mdt-validate.log 2>&1 &
echo $! > /tmp/mdt-validate.pid
disown
sleep 3
PID="$(cat /tmp/mdt-validate.pid)"
if ! kill -0 "${PID}" 2>/dev/null; then
echo "::error::daemon exited prematurely (pid was ${PID})"
cat /tmp/mdt-validate.log
exit 1
fi
echo "daemon running, pid=${PID}"
- name: validate — vendor sockets listening
shell: bash
run: |
set -e
for port in 20007 20008 20009 20010; do
if ! ss -lnt | grep -q ":${port}\b"; then
echo "::error::port ${port} not listening"
ss -lntp || true
echo "--- daemon log ---"
cat /tmp/mdt-validate.log || true
exit 1
fi
done
- name: validate — graceful shutdown
shell: bash
run: |
PID="$(cat /tmp/mdt-validate.pid)"
echo "PID from file: '${PID}'"
# Treat zombies as exited: the daemon's parent (the launching step's
# bash) is gone, and the container's PID 1 doesn't always reap orphans
# — so a cleanly-exited daemon may linger as <defunct>. kill -0
# succeeds on zombies, so we must inspect /proc/PID/status.
is_alive() {
local p="$1"
kill -0 "${p}" 2>/dev/null || return 1
local st=$(awk '/^State:/{print $2}' /proc/${p}/status 2>/dev/null)
[ "${st}" != "Z" ]
}
if is_alive "${PID}"; then
kill -TERM "${PID}" 2>/dev/null || {
echo "::error::kill -TERM failed for live pid ${PID}"
exit 1
}
# 4 vendor Srvs drain up to 5 s each — 30 s wait covers worst case.
for i in $(seq 1 120); do
is_alive "${PID}" || break
sleep 0.25
done
else
echo "::warning::pid ${PID} already not-alive (state $(awk '/^State:/{print $2}' /proc/${PID}/status 2>/dev/null || echo missing))"
fi
if is_alive "${PID}"; then
tail_log=$(tail -20 /tmp/mdt-validate.log 2>/dev/null | tr '\n' '|')
echo "::error title=shutdown stuck::pid=${PID} state=$(grep ^State /proc/${PID}/status 2>/dev/null) | log tail: ${tail_log}"
cat /tmp/mdt-validate.log
kill -KILL "${PID}" 2>/dev/null || true
exit 1
fi
echo "--- daemon log ---"
cat /tmp/mdt-validate.log
tail_log=$(tail -10 /tmp/mdt-validate.log 2>/dev/null | tr '\n' '|')
echo "::warning title=shutdown ok::log tail: ${tail_log}"
- name: validate library package
run: bash pkg/validate/library.sh "${{ steps.pkgs.outputs.library }}"
e2e-package:
# Real-traffic e2e against the freshly-built package: podman spins up
# redpanda + per-vendor synthetic gRPC clients + a clean-distro container
# that apt/dnf-installs the .deb/.rpm. Catches regressions the static
# validate-package job can't see (postinst behavior + actual data path
# under load).
name: e2e ${{ matrix.distro.name }}
needs: packages
runs-on: ubuntu-latest # host runner: needs podman (pre-installed on ubuntu-latest)
strategy:
fail-fast: false
matrix:
distro:
- { name: "debian-12 (bookworm)", tag: "bookworm" }
- { name: "debian-13 (trixie)", tag: "trixie" }
- { name: "ubuntu-24.04 (noble)", tag: "noble" }
- { name: "fedora", tag: "fc" }
steps:
- uses: actions/checkout@v5
# GitHub runners share NAT egress IPs whose anonymous Docker Hub pull
# quota is chronically exhausted — every e2e image (debian bases +
# redpanda) comes from docker.io, so pull authenticated when the
# ci.yaml mirror credentials are present; degrade to anonymous if not.
- name: podman login docker.io (avoid anonymous pull rate limit)
env:
DH_USER: ${{ vars.DOCKERHUB_USERNAME }}
DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
run: |
if [ -z "${DH_USER}" ] || [ -z "${DH_TOKEN}" ]; then
echo "::warning::vars.DOCKERHUB_USERNAME or secrets.DOCKERHUB_TOKEN unset — anonymous pulls (rate-limit risk)"
exit 0
fi
echo "${DH_TOKEN}" | podman login docker.io -u "${DH_USER}" --password-stdin
- uses: actions/download-artifact@v7
with:
name: packages-${{ matrix.distro.tag }}
path: dist
- name: locate standalone package
id: pkg
shell: bash
run: |
shopt -s nullglob globstar
files=( dist/**/mdt-dialout-collector_*.deb dist/**/mdt-dialout-collector_*.rpm )
shopt -u nullglob globstar
STANDALONE=
for f in "${files[@]}"; do
case "$(basename "$f")" in
mdt-dialout-collector-lib_*) ;;
mdt-dialout-collector_*) STANDALONE="$f" ;;
esac
done
test -n "${STANDALONE}" \
|| { echo "::error::no standalone .deb/.rpm in dist/"; ls -laR dist/; exit 1; }
echo "path=${STANDALONE}" >> "${GITHUB_OUTPUT}"
- name: run e2e against installed package
run: bash tests/e2e/run.sh --package "${{ steps.pkg.outputs.path }}"
release:
name: GitHub Release
needs: [packages, validate, e2e-package]
runs-on: ubuntu-latest
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v7
with:
path: dist
merge-multiple: true
- name: list artifacts
run: ls -lh dist/
- name: create release
uses: softprops/action-gh-release@v3
with:
files: dist/*
generate_release_notes: true
fail_on_unmatched_files: true