#!/usr/bin/env bash # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (c) 2025-2026 Cristian Cezar Moisés set -Eeuo pipefail export LC_ALL=C root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P) cd -- "$root" pass_count=0 fail_count=0 skip_count=0 pass() { pass_count=$((pass_count + 1)); printf 'PASS: %s\n' "$*"; } fail() { fail_count=$((fail_count + 1)); printf 'FAIL: %s\n' "$*" >&2; } skip() { skip_count=$((skip_count + 1)); printf 'SKIP: %s\n' "$*"; } has_exact_line_crlf_safe() { local expected=$1 path=$2 line while IFS= read -r line || [[ -n $line ]]; do line=${line%$'\r'} if [[ $line == "$expected" ]]; then return 0 fi done < "$path" return 1 } version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h) [[ -n $version ]] || { printf 'FAIL: cannot determine upstream version\n' >&2; exit 1; } for script in packaging/build-deb.sh packaging/build-rpm.sh \ packaging/build-appimage.sh packaging/build-gui-appimage.sh \ packaging/build-gui-deb.sh packaging/build-gui-rpm.sh \ gui/packaging/appimage/build-appimage.sh packaging/build-dmg.sh \ scripts/check-source-only.sh scripts/export-opensuse-package.sh \ scripts/test-installed-zupt.sh packaging/opensuse/source-audit.sh; do if bash -n "$script"; then pass "$script shell syntax"; else fail "$script shell syntax"; fi done if ! command -v make >/dev/null 2>&1; then skip 'make unavailable for Debian rules syntax' elif make -n -f packaging/debian/rules override_dh_auto_build >/dev/null; then pass 'Debian rules make syntax' else fail 'Debian rules make syntax' fi check_recipe_version() { local recipe=$1 recipe_version=$2 if [[ $recipe_version == "$version" ]]; then pass "$recipe version is $version" else fail "$recipe version is '$recipe_version' (expected $version)" fi } check_recipe_version AUR \ "$(sed -n 's/^pkgver=//p' packaging/aur/PKGBUILD)" check_recipe_version Debian \ "$(sed -n '1s/^zupt (\([^-]*\)-.*/\1/p' packaging/debian/changelog)" check_recipe_version Fedora \ "$(awk '/^Version:/{print $2; exit}' packaging/rpm/zupt.spec)" check_recipe_version Homebrew \ "$(sed -n 's/^[[:space:]]*version "\([^"]*\)".*/\1/p' packaging/homebrew/zupt.rb)" check_recipe_version Nix \ "$(sed -n 's/^[[:space:]]*version = "\([^"]*\)";.*/\1/p' packaging/nix/flake.nix | head -1)" check_recipe_version Guix \ "$(sed -n 's/^(define %zupt-version "\([^"]*\)")/\1/p' packaging/guix/zupt.scm)" check_recipe_version openSUSE \ "$(awk '/^Version:/{print $2; exit}' packaging/opensuse/zupt.spec)" check_recipe_version GUI-Deb-Control \ "$(awk '/^Version:/{print $2; exit}' gui/packaging/deb/control)" if grep -Fqx "VERSION=\${VERSION:-$version}" install.sh && \ has_exact_line_crlf_safe \ "if not defined VERSION set \"VERSION=$version\"" \ gui/packaging/windows/build-windows.bat && \ grep -Fqx "Depends: python3 (>= 3.9), python3-pyqt6 | python3-pyside6.qtwidgets, zupt (= $version)" \ gui/packaging/deb/control; then pass 'installer and static GUI package defaults match the upstream version' else fail 'installer or static GUI package defaults do not match the upstream version' fi if grep -En 'REPLACE_AFTER|REPLACE_WITH|sha256sums=\(.SKIP.|base32 .REPLACE' \ packaging/aur/PKGBUILD packaging/homebrew/zupt.rb packaging/guix/zupt.scm; then if git tag --points-at HEAD 2>/dev/null | grep -Fxq "v$version"; then fail 'tagged release recipes contain an unpinned source checksum' else pass 'release recipe checksums are explicitly pending final archive generation' fi else pass 'release recipe source checksums are pinned' fi if [[ -x packaging/debian/rules ]]; then pass 'Debian rules is executable' else fail 'Debian rules is not executable' fi if command -v dpkg-parsechangelog >/dev/null 2>&1; then if dpkg-parsechangelog -l packaging/debian/changelog >/dev/null; then pass 'Debian changelog parses' else fail 'Debian changelog does not parse' fi else skip 'dpkg-parsechangelog unavailable' fi if command -v ruby >/dev/null 2>&1; then if ruby -c packaging/homebrew/zupt.rb >/dev/null; then pass 'Homebrew formula Ruby syntax' else fail 'Homebrew formula Ruby syntax' fi else skip 'Ruby unavailable for Homebrew syntax' fi if command -v nix-instantiate >/dev/null 2>&1; then if nix-instantiate --parse packaging/nix/flake.nix >/dev/null; then pass 'Nix flake syntax' else fail 'Nix flake syntax' fi else skip 'nix-instantiate unavailable for flake syntax' fi if command -v guile >/dev/null 2>&1; then if guile -c '(use-modules (guix gexp)) (call-with-input-file "packaging/guix/zupt.scm" (lambda (p) (let loop ((x (read p))) (unless (eof-object? x) (loop (read p))))))'; then pass 'Guix recipe reader syntax' else fail 'Guix recipe reader syntax' fi else skip 'Guile unavailable for Guix syntax' fi if command -v xmllint >/dev/null 2>&1; then if xmllint --noout packaging/opensuse/_service; then pass 'openSUSE service XML' else fail 'openSUSE service XML' fi elif python3 -c 'import xml.etree.ElementTree as E; E.parse("packaging/opensuse/_service")' 2>/dev/null; then pass 'openSUSE service XML (Python parser)' else fail 'openSUSE service XML' fi service=packaging/opensuse/_service spec=packaging/opensuse/zupt.spec if grep -Fq 'https://github.com/cristiancmoises/zupt.git' "$service" && \ grep -Fq "refs/tags/v$version" "$service" && \ grep -Fq 'disable' "$service" && \ grep -Fq 'disable' "$service" && \ grep -Fq '/dev/null 2>&1; then' \ <<<"$tumbleweed_job" && \ grep -Fq 'zypper --non-interactive remove busybox-gawk' \ <<<"$tumbleweed_job" && \ grep -Fq 'git config --global --add safe.directory "$GITHUB_WORKSPACE"' \ <<<"$fedora_gui_job"; then pass 'RPM container jobs trust the exact workspace and replace busybox-gawk' else fail 'RPM container workspace trust or busybox-gawk replacement is incomplete' fi # These are literal shell expressions required inside the promotion workflow. # shellcheck disable=SC2016 if grep -Fq 'zupt-gui_${VERSION}_all.deb' .github/workflows/promote-release.yml && \ grep -Fq 'zupt-gui-$VERSION-1.noarch.rpm' .github/workflows/promote-release.yml && \ grep -Fq 'zupt-gui-$VERSION-1.src.rpm' .github/workflows/promote-release.yml && \ grep -Fq 'zupt-$VERSION-linux-x86_64.tar.xz' .github/workflows/promote-release.yml && \ grep -Fq 'zupt-gui-$VERSION-portable.zip' .github/workflows/promote-release.yml && \ grep -Fq 'zupt >= $VERSION' .github/workflows/promote-release.yml; then pass 'release promotion allowlists gated CLI and GUI package formats' else fail 'release promotion is missing a gated package format or dependency check' fi if ! grep -Eqi 'git[.]securityops[.]co|forgejo|canonical server|GitHub mirror' \ .github/workflows/promote-release.yml && \ grep -Fq 'GitHub is the canonical upstream release' \ .github/workflows/promote-release.yml; then pass 'GitHub is the sole canonical release target' else fail 'release promotion still depends on a non-GitHub canonical forge' fi if grep -Fq 'path: out/*.zip' .github/workflows/cross-platform.yml && \ ! grep -Fq 'out/*.exe' .github/workflows/cross-platform.yml && \ grep -Fq 'windows_zip_name=' .github/workflows/promote-release.yml && \ ! grep -Fq 'windows_exe_name=' .github/workflows/promote-release.yml; then pass 'Windows release policy promotes the notice-bearing ZIP only' else fail 'Windows workflow still permits a bare EXE release asset' fi windows_notices_ok=1 for notice in MINGW-CRT-COPYING.txt COPYING.MinGW-w64-runtime.txt \ COPYING.MinGW-w64.txt GCC-COPYING3.txt \ GCC-RUNTIME-LIBRARY-EXCEPTION.txt; do grep -Fq "$notice" .github/workflows/cross-platform.yml || \ windows_notices_ok=0 grep -Fq "$notice" .github/workflows/promote-release.yml || \ windows_notices_ok=0 done if ((windows_notices_ok)); then pass 'static Windows bundle preserves MinGW and GCC runtime notices' else fail 'static Windows bundle omits MinGW or GCC runtime notices' fi if grep -Eiq 'AppImage.*(not|excluded|outside)' SECURITY.md && \ grep -Eiq 'AppImage.*(not|excluded|outside)' THREAT_MODEL.md && \ grep -Eiq 'AppImage.*(not|excluded|outside)' AUDIT.md && \ grep -Eiq 'AppImage.*(not|excluded|outside)' doc/zupt.1; then pass 'current security and user documentation records release exclusions' else fail 'current documentation still permits an AppImage or bare EXE claim' fi handoff_legal_ok=1 for legal_file in LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-COMMERCIAL \ LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 \ NOTICE THIRD-PARTY-NOTICES.md; do grep -Fq "$legal_file" scripts/export-opensuse-package.sh || \ handoff_legal_ok=0 done if ((handoff_legal_ok)) && \ grep -Fq 'handoff legal file is missing or empty' scripts/export-opensuse-package.sh; then pass 'openSUSE handoff includes and checks complete public legal payload' else fail 'openSUSE handoff omits or does not validate a public legal file' fi if grep -Fq 'PYTHON-NOTICE.txt' gui/packaging/windows/build-windows.bat && \ grep -Fq 'PYINSTALLER-NOTICE.txt' gui/packaging/windows/build-windows.bat && \ grep -Fq 'QT-NOTICE.txt' gui/packaging/windows/build-windows.bat && \ grep -Fq 'PYSIDE6-NOTICE.txt' gui/packaging/windows/build-windows.bat && \ grep -Fq 'PYQT6-NOTICE.txt' gui/packaging/windows/build-windows.bat; then pass 'downstream Windows GUI helper requires every runtime notice class' else fail 'downstream Windows GUI helper permits incomplete runtime notices' fi if grep -Eq '^Name:[[:space:]]+zupt$' "$spec" && \ grep -Eq '^Source0:[[:space:]]+%\{name\}-%\{version\}\.tar\.gz$' "$spec" && \ grep -Eq '^License:[[:space:]]+AGPL-3\.0-or-later AND GPL-3\.0-or-later AND BSD-2-Clause AND BSD-3-Clause AND CC0-1\.0$' "$spec" && \ grep -Eq '^Provides:[[:space:]]+bundled\(vaptvupt-codec\) = 2\.65\.3$' "$spec" && \ grep -Eq '^Provides:[[:space:]]+vaptvupt = %\{version\}-%\{release\}$' "$spec" && \ grep -Eq '^Obsoletes:[[:space:]]+vaptvupt < %\{version\}$' "$spec" && \ grep -Fq 'WITH_SDK=0 WITH_PQBOX=0' "$spec" && \ grep -Fq 'INSTALL_LEGACY_ALIAS=0' "$spec" && \ grep -Fq 'INSTALL_LICENSES=0' "$spec" && \ grep -Fq '%{_bindir}/zupt' "$spec" && \ ! grep -Fq '%{_bindir}/vaptvupt' "$spec"; then pass 'openSUSE spec source, license, features and alias policy' else fail 'openSUSE spec source, license, features or alias policy' fi if grep -Fq 'LICENSE-BSD-3-Clause' packaging/build-deb.sh && \ grep -Fq 'LICENSE-CC0-1.0' packaging/build-deb.sh && \ [[ $(grep -Fc 'LICENSE-BSD-3-Clause' packaging/build-dmg.sh) -ge 2 ]] && \ [[ $(grep -Fc 'LICENSE-CC0-1.0' packaging/build-dmg.sh) -ge 2 ]] && \ grep -Fq 'LICENSE-BSD-3-Clause' packaging/build-appimage.sh && \ grep -Fq 'LICENSE-CC0-1.0' packaging/build-appimage.sh && \ grep -Fq 'LICENSE-BSD-3-Clause' packaging/build-gui-appimage.sh && \ grep -Fq 'LICENSE-CC0-1.0' packaging/build-gui-appimage.sh && \ grep -Fq 'LICENSE-BSD-3-Clause' .github/workflows/cross-platform.yml && \ grep -Fq 'LICENSE-CC0-1.0' .github/workflows/cross-platform.yml && \ grep -Fq 'LICENSE-BSD-3-Clause' .github/workflows/promote-release.yml && \ grep -Fq 'LICENSE-CC0-1.0' .github/workflows/promote-release.yml && \ grep -Fq 'LICENSE-BSD-3-Clause' packaging/debian/zupt.docs && \ grep -Fq 'LICENSE-CC0-1.0' packaging/debian/zupt.docs; then pass 'binary bundle paths preserve BSD-3-Clause and CC0-1.0 texts' else fail 'a binary bundle path omits BSD-3-Clause or CC0-1.0 text' fi if grep -Fq 'LICENSE-BSD-3-Clause' gui/packaging/flatpak/dev.zupt.gui.yml && \ grep -Fq 'LICENSE-CC0-1.0' gui/packaging/flatpak/dev.zupt.gui.yml && \ grep -Fq 'ZUPT_WINDOWS_RUNTIME_NOTICES_DIR' gui/packaging/windows/build-windows.bat && \ grep -Fq 'MANIFEST.txt' gui/packaging/windows/build-windows.bat && \ grep -Fq 'LICENSE-BSD-3-Clause' packaging/windows/zupt-gui.iss && \ grep -Fq 'LICENSE-CC0-1.0' packaging/windows/zupt-gui.iss && \ grep -Fq 'RuntimeNoticesDir' packaging/windows/zupt-gui.iss && \ grep -Fq 'LICENSE-BSD-3-Clause' sdk/Makefile.sdk && \ grep -Fq 'LICENSE-CC0-1.0' sdk/Makefile.sdk && \ grep -Fq 'LICENSE-BSD-3-Clause' packaging/homebrew/zupt.rb && \ grep -Fq 'LICENSE-CC0-1.0' packaging/nix/flake.nix && \ grep -Fq 'LICENSEDIR' Makefile && \ grep -Fq 'LICENSE-BSD-3-Clause' Makefile && \ grep -Fq 'LICENSE-GUI' gui/install.sh; then pass 'auxiliary bundles install project and external-runtime notices' else fail 'an auxiliary bundle omits project or external-runtime notices' fi if command -v rpmspec >/dev/null 2>&1; then if rpmspec -P "$spec" >/dev/null; then pass 'openSUSE spec parses with rpmspec' else fail 'openSUSE spec rpmspec parse' fi else skip 'rpmspec unavailable' fi if command -v shellcheck >/dev/null 2>&1; then shell_files=( packaging/build-deb.sh packaging/build-rpm.sh packaging/build-appimage.sh packaging/build-gui-appimage.sh packaging/build-gui-deb.sh packaging/build-gui-rpm.sh gui/packaging/appimage/build-appimage.sh packaging/build-dmg.sh packaging/opensuse/source-audit.sh scripts/check-source-only.sh scripts/export-opensuse-package.sh scripts/test-installed-zupt.sh tests/test_source_only.sh tests/test_packaging_syntax.sh ) if shellcheck -x "${shell_files[@]}"; then pass 'ShellCheck tracked shell scripts' else fail 'ShellCheck tracked shell scripts' fi else skip 'ShellCheck unavailable' fi for workflow in .github/workflows/*.yml; do if grep -Eq 'pull_request_target:' "$workflow"; then fail "$workflow uses pull_request_target" else pass "$workflow avoids pull_request_target" fi if grep -Eqi 'git[[:space:]]+push|credential\.helper[[:space:]]+store' "$workflow"; then fail "$workflow contains unsafe publishing command" else pass "$workflow avoids direct Git credential mutation" fi done printf 'SUMMARY: PASS=%d FAIL=%d SKIP=%d\n' "$pass_count" "$fail_count" "$skip_count" ((fail_count == 0))