Skip to content

feat(release): publish versionless padctl_<arch>.deb assets, point docs at /latest/download/ #349

feat(release): publish versionless padctl_<arch>.deb assets, point docs at /latest/download/

feat(release): publish versionless padctl_<arch>.deb assets, point docs at /latest/download/ #349

Workflow file for this run

name: install-flow
on: [push, pull_request]
concurrency:
group: install-flow-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
install-sandbox:
name: install / rootless-docker sandbox
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- name: Build padctl (no libusb — install flow only needs the static binary)
run: zig build -Dlibusb=false
- name: Run install-flow sandbox
run: |
set -euo pipefail
# Stage assets into a directory the container can read
STAGE="$PWD/_ci_stage"
mkdir -p "$STAGE/bin"
cp zig-out/bin/padctl "$STAGE/bin/padctl"
cp -r devices "$STAGE/devices"
cp -r mappings "$STAGE/mappings" 2>/dev/null || true
docker run --rm \
--privileged \
-v "$STAGE:/opt/padctl:ro" \
-v "$PWD:/src:ro" \
debian:12 \
/bin/bash -euo pipefail -c '
# ---- bootstrap ----
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
sudo adduser coreutils procps 2>/dev/null
# Create test user with passwordless sudo
useradd -m -s /bin/bash testuser
echo "testuser ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/testuser
cp /opt/padctl/bin/padctl /usr/local/bin/padctl
chmod +x /usr/local/bin/padctl
# ---- install ----
# Run as testuser so SUDO_USER is set when they call sudo padctl install.
# udevadm/systemctl calls inside padctl will fail silently (no daemon) —
# that is expected; file installation and ensureUserXdgDirs still execute.
su - testuser -c "sudo SUDO_USER=testuser SUDO_UID=$(id -u testuser) \
HOME=/home/testuser \
/usr/local/bin/padctl install --prefix /usr/local 2>&1"
# ---- invariant checks ----
FAIL=0
check() {
local desc="$1"; shift
if eval "$@"; then
echo " PASS: $desc"
else
echo " FAIL: $desc"
FAIL=1
fi
}
echo "=== install invariants ==="
check "/etc/systemd/user/padctl.service exists" \
"[ -f /etc/systemd/user/padctl.service ]"
check "/etc/systemd/user/padctl.service is a regular file (not symlink)" \
"[ ! -L /etc/systemd/user/padctl.service ]"
check "/home/testuser/.local/state/padctl exists" \
"[ -e /home/testuser/.local/state/padctl ]"
check "/home/testuser/.local/state/padctl is NOT a symlink (issue #139)" \
"[ ! -L /home/testuser/.local/state/padctl ]"
check "/home/testuser/.local/state/padctl is a directory (issue #139 v2)" \
"[ -d /home/testuser/.local/state/padctl ]"
check "/home/testuser/.local/state/padctl owned by testuser" \
"[ \"\$(stat -c %U /home/testuser/.local/state/padctl)\" = testuser ]"
check "/usr/local/bin/padctl installed" \
"[ -f /usr/local/bin/padctl ]"
if [ $FAIL -ne 0 ]; then
echo "=== INSTALL INVARIANTS FAILED ==="
echo "--- /home/testuser tree ---"
find /home/testuser -maxdepth 4 2>/dev/null || true
echo "--- /etc/systemd/user ---"
ls -la /etc/systemd/user/ 2>/dev/null || true
exit 1
fi
# ---- uninstall ----
su - testuser -c "sudo SUDO_USER=testuser SUDO_UID=$(id -u testuser) \
HOME=/home/testuser \
/usr/local/bin/padctl uninstall --prefix /usr/local 2>&1"
echo "=== uninstall invariants ==="
FAIL=0
check "/usr/local/bin/padctl removed after uninstall" \
"[ ! -f /usr/local/bin/padctl ]"
check "/etc/systemd/user/padctl.service removed after uninstall" \
"[ ! -f /etc/systemd/user/padctl.service ]"
if [ $FAIL -ne 0 ]; then
echo "=== UNINSTALL INVARIANTS FAILED ==="
exit 1
fi
echo "=== all invariants passed ==="
'
pkg-deb-smoke:
name: pkg / deb structure invariants
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
- name: Build static padctl (musl)
run: zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-linux-musl -Dlibusb=false
- name: Stage release tarball (mirror release.yml build job)
run: |
set -euo pipefail
STAGING=padctl-ci-x86_64-linux-musl
mkdir -p "${STAGING}/bin" "${STAGING}/install"
for bin in padctl padctl-capture padctl-debug; do
[ -f "zig-out/bin/${bin}" ] && cp "zig-out/bin/${bin}" "${STAGING}/bin/"
done
sudo ./zig-out/bin/padctl install --destdir "$PWD/_install_stage" --prefix /usr --no-enable --no-start
cp _install_stage/usr/lib/systemd/user/*.service "${STAGING}/install/"
cp _install_stage/usr/lib/udev/rules.d/*.rules "${STAGING}/install/"
[ -f _install_stage/usr/bin/padctl-reconnect ] && cp _install_stage/usr/bin/padctl-reconnect "${STAGING}/install/"
cp -r devices "${STAGING}/"
cp LICENSE README.md "${STAGING}/"
tar czf "${STAGING}.tar.gz" "${STAGING}"
- name: Build .deb (mirror release.yml build-deb job)
run: |
set -euo pipefail
VERSION=0.0.0-ci
DEB_ARCH=amd64
TARGET=x86_64-linux-musl
TARBALL="padctl-ci-${TARGET}.tar.gz"
PKG="padctl_${VERSION}_${DEB_ARCH}"
DEST="${PWD}/${PKG}"
tar -xzf "${TARBALL}"
SRC="padctl-ci-${TARGET}"
mkdir -p "${DEST}/DEBIAN"
for f in control postinst prerm postrm; do
cp "contrib/deb/DEBIAN-BIN/$f" "${DEST}/DEBIAN/$f"
done
chmod 0644 "${DEST}/DEBIAN/control"
chmod 0755 "${DEST}/DEBIAN/postinst" "${DEST}/DEBIAN/prerm" "${DEST}/DEBIAN/postrm"
sed -i "s/^Version:.*/Version: ${VERSION}/" "${DEST}/DEBIAN/control"
sed -i "s/^Architecture:.*/Architecture: ${DEB_ARCH}/" "${DEST}/DEBIAN/control"
mkdir -p "${DEST}/usr/bin" "${DEST}/usr/lib/systemd/user" \
"${DEST}/usr/lib/udev/rules.d" "${DEST}/usr/share/padctl" \
"${DEST}/usr/share/doc/padctl"
for bin in padctl padctl-capture padctl-debug padctl-reconnect; do
[ -f "${SRC}/bin/${bin}" ] && install -Dm755 "${SRC}/bin/${bin}" "${DEST}/usr/bin/${bin}"
done
[ -f "${SRC}/install/padctl-reconnect" ] && install -Dm755 "${SRC}/install/padctl-reconnect" "${DEST}/usr/bin/padctl-reconnect"
cp "${SRC}/install/"*.service "${DEST}/usr/lib/systemd/user/"
cp "${SRC}/install/"*.rules "${DEST}/usr/lib/udev/rules.d/"
cp -r "${SRC}/devices" "${DEST}/usr/share/padctl/"
install -Dm644 "${SRC}/LICENSE" "${DEST}/usr/share/doc/padctl/copyright"
dpkg-deb --build --root-owner-group "${DEST}" "${PKG}.deb"
- name: Verify .deb file layout
run: |
set -euo pipefail
OUT=$(dpkg-deb -c padctl_0.0.0-ci_amd64.deb)
echo "${OUT}" | grep -q ' \./usr/bin/padctl$' \
|| { echo "FAIL: /usr/bin/padctl missing"; echo "${OUT}"; exit 1; }
echo "${OUT}" | grep -q ' \./usr/lib/systemd/user/padctl\.service$' \
|| { echo "FAIL: padctl.service missing"; echo "${OUT}"; exit 1; }
echo "${OUT}" | grep -q ' \./usr/lib/udev/rules\.d/60-padctl\.rules$' \
|| { echo "FAIL: 60-padctl.rules missing"; echo "${OUT}"; exit 1; }
echo "${OUT}" | grep -q ' \./usr/share/padctl/devices/flydigi/vader5\.toml$' \
|| { echo "FAIL: vader5.toml not under /usr/share/padctl/devices/"; echo "${OUT}"; exit 1; }
DEVICES=$(echo "${OUT}" | grep -c '^.* \./usr/share/padctl/devices/.\+/.\+\.toml$' || true)
[ "${DEVICES}" -ge 5 ] \
|| { echo "FAIL: expected >=5 .toml under devices/, got ${DEVICES}"; echo "${OUT}"; exit 1; }
BROKEN=$(echo "${OUT}" | grep -c '^.* \./usr/share/padctl/[^/]\+/[^/]\+\.toml$' || true)
[ "${BROKEN}" -eq 0 ] \
|| { echo "FAIL: ${BROKEN} TOMLs at broken layout /usr/share/padctl/<vendor>/ (pre-#217 regression)"; echo "${OUT}"; exit 1; }
echo "PASS: .deb structure invariants (${DEVICES} device TOMLs under devices/, 0 at broken layout)"
- name: Install .deb in Debian 12 + runtime invariants
run: |
ZON_VER=$(grep -E '^\s*\.version\s*=' build.zig.zon | sed -E 's/.*"([^"]+)".*/\1/')
docker run --rm -v "$PWD:/work:ro" -e ZON_VER="${ZON_VER}" debian:12 /bin/bash -euo pipefail -c '
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends systemd file 2>/dev/null
echo "::group::install"
dpkg -i /work/padctl_0.0.0-ci_amd64.deb || (apt-get install -fy && dpkg -i /work/padctl_0.0.0-ci_amd64.deb)
echo "::endgroup::"
echo "::group::reinstall idempotent"
dpkg -i /work/padctl_0.0.0-ci_amd64.deb
echo "::endgroup::"
echo "::group::file layout"
test -d /usr/share/padctl/devices || { echo FAIL devices dir missing; exit 1; }
test -f /usr/share/padctl/devices/flydigi/vader5.toml || { echo FAIL vader5.toml missing; exit 1; }
test -x /usr/bin/padctl || { echo FAIL padctl bin not executable; exit 1; }
test -f /usr/lib/systemd/user/padctl.service || { echo FAIL service unit missing; exit 1; }
test -f /usr/lib/udev/rules.d/60-padctl.rules || { echo FAIL udev rules missing; exit 1; }
echo "::endgroup::"
echo "::group::version sync (zon=${ZON_VER})"
BIN_VER=$(/usr/bin/padctl --version | awk "{print \$2}")
if [ "${BIN_VER}" != "${ZON_VER}" ]; then
echo "FAIL: --version reported ${BIN_VER}, expected ${ZON_VER} from build.zig.zon (SSOT path build_options.version is broken)"
exit 1
fi
echo "::endgroup::"
echo "::group::shipped device TOMLs all parse"
FAIL=0; TOTAL=0
for toml in $(find /usr/share/padctl/devices -name "*.toml"); do
TOTAL=$((TOTAL+1))
/usr/bin/padctl --validate "$toml" 2>&1 | grep -q ": OK$" || { echo "FAIL validate: $toml"; FAIL=$((FAIL+1)); }
done
[ "$FAIL" -eq 0 ] || { echo "FAIL: ${FAIL}/${TOTAL} TOMLs failed --validate"; exit 1; }
echo "PASS validated ${TOTAL}/${TOTAL}"
echo "::endgroup::"
echo "::group::udev rules content (Vader 5 Pro / 8BitDo / input-group fallback)"
grep -q "37d7.*2401" /usr/lib/udev/rules.d/60-padctl.rules || { echo FAIL vader5 USB VID/PID rule missing; exit 1; }
grep -q "GROUP=\"input\"" /usr/lib/udev/rules.d/60-padctl.rules || { echo FAIL input-group fallback missing; exit 1; }
grep -q "TAG+=\"uaccess\"" /usr/lib/udev/rules.d/60-padctl.rules || { echo FAIL uaccess tag missing; exit 1; }
echo "::endgroup::"
echo "::group::systemd unit critical fields"
grep -qE "^ExecStart=/usr/bin/padctl" /usr/lib/systemd/user/padctl.service || { echo FAIL ExecStart wrong; exit 1; }
grep -qE "^StateDirectory=padctl$" /usr/lib/systemd/user/padctl.service || { echo FAIL StateDirectory wrong; exit 1; }
grep -qE "^NoNewPrivileges=true$" /usr/lib/systemd/user/padctl.service || { echo FAIL NoNewPrivileges missing; exit 1; }
echo "::endgroup::"
echo "::group::dump subcommand recognized (issue #216 symptom 3)"
/usr/bin/padctl dump 2>&1 | grep -qiE "unknown argument" && { echo FAIL dump still unknown; exit 1; } || echo "PASS dump recognized"
echo "::endgroup::"
echo "::group::scan + list-mappings smoke"
/usr/bin/padctl scan >/dev/null 2>&1 || { echo FAIL scan crashed; exit 1; }
/usr/bin/padctl list-mappings >/dev/null 2>&1 || { echo FAIL list-mappings crashed; exit 1; }
echo "::endgroup::"
echo "::group::static linkage (no glibc dep)"
file /usr/bin/padctl | grep -q "statically linked" || { echo FAIL not statically linked; file /usr/bin/padctl; ldd /usr/bin/padctl 2>&1 || true; exit 1; }
echo "::endgroup::"
echo "::group::uninstall removes everything"
apt-get remove -y padctl
test ! -f /usr/bin/padctl || { echo FAIL bin still present; exit 1; }
test ! -f /usr/lib/systemd/user/padctl.service || { echo FAIL service still present; exit 1; }
test ! -f /usr/lib/udev/rules.d/60-padctl.rules || { echo FAIL udev still present; exit 1; }
echo "::endgroup::"
echo "::group::purge cleans residual share dir"
apt-get purge -y padctl
test ! -d /usr/share/padctl || { echo FAIL share dir not cleaned: $(ls /usr/share/padctl); exit 1; }
echo "::endgroup::"
echo "ALL INVARIANTS PASS"
'