feat(release): publish versionless padctl_<arch>.deb assets, point docs at /latest/download/ #349
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | |
| ' |