release: restore ZUPT and harden source-only 5.2.2
This commit is contained in:
parent
74e393ba3e
commit
ff99770bd0
205 changed files with 19627 additions and 13215 deletions
777
.github/workflows/ci.yml
vendored
777
.github/workflows/ci.yml
vendored
|
|
@ -1,254 +1,613 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
#
|
||||
# Zupt CI matrix.
|
||||
#
|
||||
# Mirrors the project's local-verification protocol from PROMPT.md §6:
|
||||
# 1. Plain GCC build
|
||||
# 2. Plain Clang build
|
||||
# 3. Strict GCC (full warning set)
|
||||
# 4. Strict Clang (full warning set)
|
||||
# 5. ASAN + UBSAN
|
||||
# 6. Full regression suite (12 suites: audit, dedup, path-traversal,
|
||||
# argument-order, block-swap, F-08, F-09 byte sweep, F-10, F-11,
|
||||
# F-12, packaging syntax, dist reproducibility)
|
||||
# 7. License header audit
|
||||
# 8. `make dist` reproducibility (two runs, sha256 must match)
|
||||
# 9. aarch64 cross-test via QEMU emulation
|
||||
# 10. Automatic release on git tag push
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
tags: ['v*']
|
||||
branches:
|
||||
- master
|
||||
- 'codex/**'
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' && !startsWith(github.ref, 'refs/tags/') }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ─── Plain build + test, exactly as a user would do it ───
|
||||
source-policy:
|
||||
name: Source-only, license, shell and secret policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Check out all refs without LFS or submodules
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
|
||||
- name: Install audit tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
dpkg-dev file git-lfs libarchive-tools libxml2-utils make python3 ruby \
|
||||
shellcheck unzip
|
||||
|
||||
- name: Audit tracked files, worktree and HEAD archive
|
||||
run: bash scripts/check-source-only.sh
|
||||
|
||||
- name: Exercise positive and negative scanner fixtures
|
||||
run: bash tests/test_source_only.sh
|
||||
|
||||
- name: Audit license headers
|
||||
run: make WITH_SDK=0 WITH_PQBOX=0 audit-licenses
|
||||
|
||||
- name: Validate release packaging metadata
|
||||
run: bash tests/test_packaging_syntax.sh
|
||||
|
||||
- name: ShellCheck release and source-policy scripts
|
||||
run: |
|
||||
shellcheck \
|
||||
packaging/build-deb.sh \
|
||||
packaging/build-rpm.sh \
|
||||
packaging/build-appimage.sh \
|
||||
packaging/build-dmg.sh \
|
||||
packaging/build-gui-appimage.sh \
|
||||
packaging/build-gui-deb.sh \
|
||||
packaging/build-gui-rpm.sh \
|
||||
packaging/opensuse/source-audit.sh \
|
||||
scripts/check-source-only.sh \
|
||||
scripts/export-opensuse-package.sh \
|
||||
scripts/test-installed-zupt.sh \
|
||||
tests/test_atomic_archive_output.sh \
|
||||
tests/test_authenticated_dedup_reorder.sh \
|
||||
tests/test_benchmark_temp_safety.sh \
|
||||
tests/test_block_type_confusion.sh \
|
||||
tests/test_disk_device_capacity.sh \
|
||||
tests/test_f09_preface.sh \
|
||||
tests/test_legacy_disk_5_2_1.sh \
|
||||
tests/test_path_traversal.sh \
|
||||
tests/test_pqbox.sh \
|
||||
tests/test_sdk.sh \
|
||||
tests/test_source_only.sh
|
||||
|
||||
- name: Credential material audit (paths only)
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
findings=$(git grep -Il -E -- \
|
||||
"-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----|https?://[^/@[:space:]]+:[A-Za-z0-9_+=.-]{20,}@|gh[pousr]_[A-Za-z0-9]{30,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|AKIA[A-Z0-9]{16}|xox[baprs]-[A-Za-z0-9-]{20,}|(FORGEJO_TOKEN|SECURITYOPS_TOKEN|GITHUB_TOKEN|CODEBERG_TOKEN)[[:space:]]*[:=][[:space:]]*['\\\"]?[A-Za-z0-9_+=./-]{20,}" \
|
||||
-- . || true)
|
||||
if [[ -n $findings ]]; then
|
||||
printf '%s\n' "$findings" >&2
|
||||
echo 'credential-like material found in tracked files' >&2
|
||||
exit 1
|
||||
fi
|
||||
echo 'No private-key block, named token assignment, or credential-bearing URL found.'
|
||||
|
||||
build-and-test:
|
||||
name: Build and full tests (${{ matrix.cc }})
|
||||
needs: source-policy
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
cc: [gcc, clang]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install build tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential clang dpkg-dev python3
|
||||
- name: Build (${{ matrix.cc }})
|
||||
run: make CC=${{ matrix.cc }} -j$(nproc)
|
||||
- name: zupt version
|
||||
run: ./zupt version
|
||||
- name: Full regression suite
|
||||
run: make test
|
||||
- name: License header audit
|
||||
run: make audit-licenses
|
||||
sudo apt-get install -y build-essential clang file libarchive-tools python3 unzip
|
||||
- name: Clean source-only build
|
||||
run: |
|
||||
make clean
|
||||
make -j"$(nproc)" CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0
|
||||
- name: Distribution checks
|
||||
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 check
|
||||
- name: Extended upstream tests
|
||||
run: make CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 test-all
|
||||
- name: Functional test of the built CLI
|
||||
run: bash scripts/test-installed-zupt.sh "$PWD/zupt"
|
||||
|
||||
# ─── Strict warning matrix — what the project's §6 protocol uses ───
|
||||
strict-warnings:
|
||||
name: Strict warnings (${{ matrix.cc }})
|
||||
needs: source-policy
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- cc: gcc
|
||||
cflags: "-Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -Wformat-security -Wlogical-op -Wjump-misses-init -Wdouble-promotion -O2 -std=c11 -Werror"
|
||||
flags: >-
|
||||
-O2 -g -std=c11 -Wall -Wextra -Wpedantic -Wshadow
|
||||
-Wcast-align -Wstrict-prototypes -Wmissing-prototypes
|
||||
-Wnull-dereference -Wformat=2 -Werror
|
||||
- cc: clang
|
||||
cflags: "-Wall -Wextra -Wpedantic -Wshadow -Wcast-align -Wstrict-prototypes -Wmissing-prototypes -Wnull-dereference -O2 -std=c11 -Werror"
|
||||
flags: >-
|
||||
-O2 -g -std=c11 -Wall -Wextra -Wpedantic -Wshadow
|
||||
-Wcast-align -Wstrict-prototypes -Wmissing-prototypes
|
||||
-Wnull-dereference -Wformat=2 -Werror
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential clang
|
||||
- name: Strict ${{ matrix.cc }} build (warnings → errors)
|
||||
run: make CC=${{ matrix.cc }} CFLAGS="${{ matrix.cflags }}" -j$(nproc)
|
||||
|
||||
# ─── ASAN + UBSAN — catches memory bugs the warning matrix can't ───
|
||||
sanitizers:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
- name: Build with ASAN + UBSAN
|
||||
run: make test-asan
|
||||
- name: Native --pq byte-exact roundtrip under ASAN
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
run: |
|
||||
# Native hybrid ML-KEM-768 + X25519 (the source-only default; --pq-sdk
|
||||
# needs a WITH_SDK=1 build and is unavailable here).
|
||||
./zupt_asan keygen -o /tmp/k.priv
|
||||
./zupt_asan keygen --pub -o /tmp/k.pub -k /tmp/k.priv
|
||||
./zupt_asan compress --pq /tmp/k.pub /tmp/a.zupt include/
|
||||
mkdir -p /tmp/extracted
|
||||
./zupt_asan extract --pq /tmp/k.priv -o /tmp/extracted /tmp/a.zupt
|
||||
diff -qr include /tmp/extracted/include
|
||||
|
||||
# ─── PIE hardening build — verifies no runtime breakage from -fPIE ───
|
||||
pie-hardening:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential
|
||||
- name: Build with PIE + hardening
|
||||
run: |
|
||||
make CFLAGS="-O2 -std=c11 -fPIE -fstack-protector-strong -D_FORTIFY_SOURCE=2 -Wformat -Wformat-security" \
|
||||
LDFLAGS="-pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack" \
|
||||
-j$(nproc)
|
||||
- name: Verify binary is PIE
|
||||
run: |
|
||||
file ./zupt | grep -E "ELF .*executable.*pie|ELF .*shared object" || \
|
||||
{ file ./zupt; echo "binary is not PIE"; exit 1; }
|
||||
- name: Smoke test
|
||||
run: |
|
||||
echo "test" > /tmp/in.txt
|
||||
./zupt c -p secret /tmp/a.zupt /tmp/in.txt
|
||||
mkdir /tmp/out
|
||||
(cd /tmp/out && ./../../home/runner/work/zupt/zupt/zupt x -p secret /tmp/a.zupt) || \
|
||||
{ cd /tmp/out && "$GITHUB_WORKSPACE/zupt" x -p secret /tmp/a.zupt; }
|
||||
diff -q /tmp/in.txt /tmp/out/in.txt
|
||||
|
||||
# ─── aarch64 cross-build via QEMU emulation ───
|
||||
cross-aarch64:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
platforms: arm64
|
||||
- name: Build + test inside aarch64 container
|
||||
run: |
|
||||
docker run --rm --platform linux/arm64 \
|
||||
-v "$PWD":/src -w /src \
|
||||
ubuntu:24.04 \
|
||||
bash -c '
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq build-essential python3
|
||||
make -j$(nproc)
|
||||
./zupt version
|
||||
make test
|
||||
'
|
||||
|
||||
# ─── make dist reproducibility ───
|
||||
dist-reproducibility:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
- name: First dist build
|
||||
run: make dist
|
||||
- name: Capture sha256 (run 1)
|
||||
id: sha1
|
||||
run: |
|
||||
VER=$(grep '^#define ZUPT_VERSION_STRING' include/zupt.h | awk -F'"' '{print $2}')
|
||||
SHA=$(sha256sum /tmp/vaptvupt-$VER.tar.gz | awk '{print $1}')
|
||||
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "ver=$VER" >> "$GITHUB_OUTPUT"
|
||||
echo "Run 1: $SHA"
|
||||
- name: Second dist build (must produce identical sha256)
|
||||
run: make dist
|
||||
- name: Verify reproducibility
|
||||
run: |
|
||||
VER="${{ steps.sha1.outputs.ver }}"
|
||||
SHA2=$(sha256sum /tmp/vaptvupt-$VER.tar.gz | awk '{print $1}')
|
||||
if [ "$SHA2" != "${{ steps.sha1.outputs.sha }}" ]; then
|
||||
echo "::error::make dist is NOT reproducible"
|
||||
echo " run 1: ${{ steps.sha1.outputs.sha }}"
|
||||
echo " run 2: $SHA2"
|
||||
exit 1
|
||||
fi
|
||||
echo "Reproducible ✓ ($SHA2)"
|
||||
- name: Upload reproducible source tarball
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: zupt-source-tarball
|
||||
path: /tmp/vaptvupt-*.tar.gz
|
||||
|
||||
# ─── Packaging-recipe syntax (cross-distro) ───
|
||||
packaging-syntax:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install validators
|
||||
persist-credentials: false
|
||||
- name: Install compilers
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential dpkg-dev ruby rpm
|
||||
- name: Build (for include/zupt.h to exist; not strictly needed for syntax test)
|
||||
run: make -j$(nproc)
|
||||
- name: Run packaging syntax test
|
||||
run: bash tests/test_packaging_syntax.sh
|
||||
sudo apt-get install -y build-essential clang
|
||||
- name: Compile with warnings as errors
|
||||
run: |
|
||||
make clean
|
||||
make -j"$(nproc)" CC=${{ matrix.cc }} V=1 WITH_SDK=0 WITH_PQBOX=0 \
|
||||
CFLAGS="${{ matrix.flags }}"
|
||||
|
||||
# ─── Automatic GitHub release on git tag push ───
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [build-and-test, strict-warnings, sanitizers, dist-reproducibility, packaging-syntax]
|
||||
sanitizers:
|
||||
name: ASan, LSan and UBSan
|
||||
needs: source-policy
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install build deps
|
||||
run: sudo apt-get update && sudo apt-get install -y build-essential python3
|
||||
- name: Build reproducible source tarball
|
||||
run: make dist
|
||||
- name: Get version
|
||||
id: ver
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install compiler and test tools
|
||||
run: |
|
||||
VER=$(grep '^#define ZUPT_VERSION_STRING' include/zupt.h | awk -F'"' '{print $2}')
|
||||
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
||||
- name: Verify tag matches version
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential file python3
|
||||
- name: Instrumented functional tests
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=1:abort_on_error=1
|
||||
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
|
||||
run: make V=1 WITH_SDK=0 WITH_PQBOX=0 test-asan-run
|
||||
- name: Mutation smoke under sanitizers
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=1:abort_on_error=1
|
||||
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
|
||||
run: make V=1 WITH_SDK=0 WITH_PQBOX=0 fuzz-format-run
|
||||
|
||||
static-analysis:
|
||||
name: GCC static analyzer
|
||||
needs: source-policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install GCC
|
||||
run: |
|
||||
TAG="${GITHUB_REF#refs/tags/}"
|
||||
EXPECTED="v${{ steps.ver.outputs.version }}"
|
||||
if [ "$TAG" != "$EXPECTED" ]; then
|
||||
echo "::error::tag $TAG doesn't match include/zupt.h $EXPECTED"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential
|
||||
- name: Analyze every source translation unit
|
||||
run: |
|
||||
make clean
|
||||
make -j"$(nproc)" CC=gcc V=1 WITH_SDK=0 WITH_PQBOX=0 \
|
||||
CFLAGS="-O1 -g -std=c11 -Wall -Wextra -Werror -fanalyzer"
|
||||
|
||||
source-archive:
|
||||
name: Reproducible audited source archive
|
||||
needs: source-policy
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install archive audit tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y file libarchive-tools python3 unzip
|
||||
- name: Build the source archive twice
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
mkdir -p "$RUNNER_TEMP/dist-one" "$RUNNER_TEMP/dist-two" \
|
||||
"$RUNNER_TEMP/release-source"
|
||||
make DIST_TARBALL="$RUNNER_TEMP/dist-one/zupt-$version.tar.gz" dist
|
||||
make DIST_TARBALL="$RUNNER_TEMP/dist-two/zupt-$version.tar.gz" dist
|
||||
cmp "$RUNNER_TEMP/dist-one/zupt-$version.tar.gz" \
|
||||
"$RUNNER_TEMP/dist-two/zupt-$version.tar.gz"
|
||||
cp "$RUNNER_TEMP/dist-one/zupt-$version.tar.gz" \
|
||||
"$RUNNER_TEMP/release-source/"
|
||||
(cd "$RUNNER_TEMP/release-source" && sha256sum "zupt-$version.tar.gz" > \
|
||||
"zupt-$version.tar.gz.sha256")
|
||||
bash scripts/check-source-only.sh --archive \
|
||||
"$RUNNER_TEMP/release-source/zupt-$version.tar.gz"
|
||||
- name: Match downstream recipe checksums to the tagged source archive
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
source_tar="$RUNNER_TEMP/release-source/zupt-$version.tar.gz"
|
||||
actual_sha=$(sha256sum "$source_tar" | awk '{print $1}')
|
||||
aur_sha=$(awk -F"'" '/^sha256sums=/ { print $2; exit }' packaging/aur/PKGBUILD)
|
||||
homebrew_sha=$(awk -F'"' '/^[[:space:]]*sha256 / { print $2; exit }' packaging/homebrew/zupt.rb)
|
||||
guix_base32=$(sed -n 's/^[[:space:]]*(base32 "\([^"]*\)").*/\1/p' \
|
||||
packaging/guix/zupt.scm | head -n 1)
|
||||
actual_base32=$(python3 - "$source_tar" <<'PY'
|
||||
import hashlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
alphabet = "0123456789abcdfghijklmnpqrsvwxyz"
|
||||
digest = hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).digest()
|
||||
value = int.from_bytes(digest, "little")
|
||||
length = (len(digest) * 8 + 4) // 5
|
||||
print("".join(alphabet[(value >> (5 * index)) & 31]
|
||||
for index in range(length - 1, -1, -1)))
|
||||
PY
|
||||
)
|
||||
[[ $aur_sha == "$actual_sha" && $homebrew_sha == "$actual_sha" ]] || {
|
||||
echo 'AUR or Homebrew checksum does not match the source archive' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $guix_base32 == "$actual_base32" ]] || {
|
||||
echo 'Guix checksum does not match the source archive' >&2
|
||||
exit 1
|
||||
}
|
||||
- name: Upload source and checksum
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-source
|
||||
path: ${{ runner.temp }}/release-source/*
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
debian-package:
|
||||
name: Debian/Ubuntu source-built package
|
||||
needs: [source-policy, build-and-test]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- name: Install Debian package tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential binutils dpkg-dev file git libarchive-tools python3 python3-pyqt6 unzip
|
||||
- name: Build and extract-test the DEB
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/release-deb"
|
||||
DIST_DIR="$RUNNER_TEMP/release-deb" RUN_CHECKS=1 bash packaging/build-deb.sh
|
||||
- name: Build and content-test the GUI DEB
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/release-gui-deb"
|
||||
DIST_DIR="$RUNNER_TEMP/release-gui-deb" bash packaging/build-gui-deb.sh
|
||||
gui_deb="$RUNNER_TEMP/release-gui-deb/zupt-gui_5.2.2_all.deb"
|
||||
test -s "$gui_deb"
|
||||
test "$(dpkg-deb -f "$gui_deb" Package)" = zupt-gui
|
||||
test "$(dpkg-deb -f "$gui_deb" Version)" = 5.2.2
|
||||
test "$(dpkg-deb -f "$gui_deb" Architecture)" = all
|
||||
- name: Install, functionally test and uninstall the DEBs
|
||||
run: |
|
||||
deb=$(find "$RUNNER_TEMP/release-deb" -maxdepth 1 -type f -name '*.deb' -print -quit)
|
||||
gui_deb="$RUNNER_TEMP/release-gui-deb/zupt-gui_5.2.2_all.deb"
|
||||
test -n "$deb" && test -s "$gui_deb"
|
||||
sudo apt-get install -y "$deb" "$gui_deb"
|
||||
bash scripts/test-installed-zupt.sh /usr/bin/zupt
|
||||
QT_QPA_PLATFORM=offscreen zupt-gui --version | grep -Fx "zupt-gui 5.2.2"
|
||||
test ! -e /usr/bin/vaptvupt
|
||||
sudo apt-get purge -y zupt-gui zupt
|
||||
test ! -e /usr/bin/zupt-gui
|
||||
test ! -e /usr/bin/zupt
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-deb
|
||||
path: ${{ runner.temp }}/release-deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-gui-deb
|
||||
path: ${{ runner.temp }}/release-gui-deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
tumbleweed-rpm:
|
||||
name: openSUSE Tumbleweed x86_64 RPM gate
|
||||
needs: [source-policy, build-and-test]
|
||||
runs-on: ubuntu-24.04
|
||||
container: opensuse/tumbleweed:latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Bootstrap Git before checkout
|
||||
run: |
|
||||
zypper --non-interactive --gpg-auto-import-keys refresh
|
||||
zypper --non-interactive install --no-recommends \
|
||||
bash ca-certificates git-core
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install native openSUSE tooling
|
||||
run: |
|
||||
zypper --non-interactive install --no-recommends \
|
||||
bash binutils cpio coreutils diffutils file findutils gawk gcc git-core grep gzip \
|
||||
libxml2-tools make osc obs-service-obs_scm obs-service-recompress \
|
||||
obs-service-tar python3-base rpm-build rpmlint sed \
|
||||
shadow spec-cleaner tar unzip util-linux
|
||||
- name: Confirm the Factory architecture gate
|
||||
run: test "$(uname -m)" = x86_64
|
||||
- name: Validate OBS service and spec syntax
|
||||
run: |
|
||||
xmllint --noout packaging/opensuse/_service
|
||||
test -x /usr/lib/obs/service/obs_scm
|
||||
test -x /usr/lib/obs/service/tar
|
||||
test -x /usr/lib/obs/service/recompress
|
||||
rpmspec -P packaging/opensuse/zupt.spec >/dev/null
|
||||
spec-cleaner --no-copyright packaging/opensuse/zupt.spec \
|
||||
> "$RUNNER_TEMP/zupt.spec.cleaned"
|
||||
diff -u packaging/opensuse/zupt.spec \
|
||||
"$RUNNER_TEMP/zupt.spec.cleaned"
|
||||
- name: Exercise pinned OBS source service chain on release tags
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
service_dir=$RUNNER_TEMP/obs-service
|
||||
mkdir -p "$service_dir"
|
||||
cp packaging/opensuse/_service "$service_dir/"
|
||||
# `osc service runall` additionally requires OBS working-copy metadata.
|
||||
# Use osc's installed service executor to validate this standalone,
|
||||
# repository-owned _service file with the exact same local services.
|
||||
python3 - "$service_dir" <<'PY'
|
||||
import sys
|
||||
from xml.etree import ElementTree
|
||||
from osc.obs_scm.serviceinfo import Serviceinfo
|
||||
|
||||
service_dir = sys.argv[1]
|
||||
service_info = Serviceinfo()
|
||||
service_info.read(ElementTree.parse(f"{service_dir}/_service").getroot())
|
||||
raise SystemExit(service_info.execute(service_dir, "all", verbose=True))
|
||||
PY
|
||||
mapfile -t service_archives < <(find "$service_dir" -maxdepth 1 \
|
||||
-type f -name 'zupt-*.tar.gz' -print)
|
||||
test "${#service_archives[@]}" -eq 1
|
||||
bash scripts/check-source-only.sh --archive "${service_archives[0]}"
|
||||
- name: Build source and binary RPMs with real checks
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/release-rpm"
|
||||
DIST_DIR="$RUNNER_TEMP/release-rpm" bash packaging/build-rpm.sh
|
||||
- name: Run rpmlint without suppressions
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
rpmlint "$RUNNER_TEMP"/release-rpm/*.rpm 2>&1 \
|
||||
| tee "$RUNNER_TEMP/rpmlint.log"
|
||||
if grep -Eq ': E:' "$RUNNER_TEMP/rpmlint.log"; then
|
||||
echo 'rpmlint reported one or more errors' >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Compute sha256
|
||||
id: sha
|
||||
- name: Install, functionally test and uninstall the RPM
|
||||
run: |
|
||||
VER="${{ steps.ver.outputs.version }}"
|
||||
SHA=$(sha256sum /tmp/vaptvupt-$VER.tar.gz | awk '{print $1}')
|
||||
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||
echo "$SHA vaptvupt-$VER.tar.gz" > /tmp/vaptvupt-$VER.tar.gz.sha256
|
||||
- name: Create GitHub release
|
||||
uses: softprops/action-gh-release@v2
|
||||
rpm_file=$(find "$RUNNER_TEMP/release-rpm" -maxdepth 1 -type f \
|
||||
-name '*.rpm' ! -name '*.src.rpm' -print -quit)
|
||||
test -n "$rpm_file"
|
||||
zypper --non-interactive install --allow-unsigned-rpm "$rpm_file"
|
||||
test_home=/tmp/zupt-ci-user
|
||||
useradd --create-home --home-dir "$test_home" --shell /bin/bash zupt-ci
|
||||
runuser -u zupt-ci -- env HOME="$test_home" TMPDIR="$test_home" \
|
||||
bash "$GITHUB_WORKSPACE/scripts/test-installed-zupt.sh" \
|
||||
/usr/bin/zupt
|
||||
test ! -e /usr/bin/vaptvupt
|
||||
zypper --non-interactive remove zupt
|
||||
test ! -e /usr/bin/zupt
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
files: |
|
||||
/tmp/vaptvupt-${{ steps.ver.outputs.version }}.tar.gz
|
||||
/tmp/vaptvupt-${{ steps.ver.outputs.version }}.tar.gz.sha256
|
||||
body: |
|
||||
## Zupt v${{ steps.ver.outputs.version }}
|
||||
name: release-rpm
|
||||
path: ${{ runner.temp }}/release-rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
Reproducible source tarball.
|
||||
gui-rpm-package:
|
||||
name: Fedora noarch GUI RPM and SRPM gate
|
||||
needs: [source-policy, build-and-test]
|
||||
runs-on: ubuntu-24.04
|
||||
container: fedora:latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
steps:
|
||||
- name: Bootstrap checkout dependencies
|
||||
run: dnf install -y ca-certificates git
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install native build, package, audit and GUI runtime tools
|
||||
run: |
|
||||
dnf install -y \
|
||||
binutils cpio file findutils gcc git-core gzip libarchive make \
|
||||
python3 python3-pyside6 rpm-build rpmdevtools tar unzip
|
||||
- name: Build and content-test the GUI RPM and source RPM
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/release-gui-rpm"
|
||||
DIST_DIR="$RUNNER_TEMP/release-gui-rpm" bash packaging/build-gui-rpm.sh
|
||||
test -s "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm"
|
||||
test -s "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.src.rpm"
|
||||
test "$(rpm -qp --qf '%{NAME}' "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm")" = zupt-gui
|
||||
test "$(rpm -qp --qf '%{VERSION}-%{RELEASE}' "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm")" = 5.2.2-1
|
||||
test "$(rpm -qp --qf '%{ARCH}' "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm")" = noarch
|
||||
rpm -qp --requires "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm" | \
|
||||
grep -Fx 'zupt >= 5.2.2'
|
||||
test "$(rpm -qp --qf '%{NAME}' "$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.src.rpm")" = zupt-gui
|
||||
- name: Build the matching Fedora CLI RPM
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
core_top="$RUNNER_TEMP/core-rpmbuild"
|
||||
mkdir -p "$core_top"/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
make DIST_TARBALL="$core_top/SOURCES/zupt-$version.tar.gz" dist
|
||||
rpmbuild --define "_topdir $core_top" -ba packaging/rpm/zupt.spec
|
||||
- name: Install and functionally test the GUI with the packaged CLI
|
||||
run: |
|
||||
core_rpm=$(find "$RUNNER_TEMP/core-rpmbuild/RPMS" -type f \
|
||||
-name 'zupt-5.2.2-1.*.rpm' ! -name '*-debuginfo-*' \
|
||||
! -name '*-debugsource-*' -print -quit)
|
||||
gui_rpm="$RUNNER_TEMP/release-gui-rpm/zupt-gui-5.2.2-1.noarch.rpm"
|
||||
test -n "$core_rpm" && test -s "$gui_rpm"
|
||||
dnf install -y "$core_rpm" "$gui_rpm"
|
||||
bash scripts/test-installed-zupt.sh /usr/bin/zupt
|
||||
QT_QPA_PLATFORM=offscreen zupt-gui --version | grep -Fx 'zupt-gui 5.2.2'
|
||||
test ! -e /usr/bin/vaptvupt
|
||||
dnf remove -y zupt-gui zupt
|
||||
test ! -e /usr/bin/zupt-gui
|
||||
test ! -e /usr/bin/zupt
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-gui-rpm
|
||||
path: ${{ runner.temp }}/release-gui-rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
```
|
||||
sha256: ${{ steps.sha.outputs.sha }}
|
||||
```
|
||||
linux-portable:
|
||||
name: Linux x86_64 notice-bearing CLI tar.xz gate
|
||||
needs: [source-policy, build-and-test]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install build and archive tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential binutils file python3 xz-utils
|
||||
- name: Build and audit the native executable
|
||||
run: |
|
||||
test "$(uname -m)" = x86_64
|
||||
make clean
|
||||
make -j"$(nproc)" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
|
||||
make V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
|
||||
bash scripts/test-installed-zupt.sh "$PWD/zupt"
|
||||
if readelf -d zupt | grep -Eq '(RPATH|RUNPATH)'; then
|
||||
echo 'Linux portable binary contains RPATH/RUNPATH' >&2
|
||||
exit 1
|
||||
fi
|
||||
mapfile -t needed < <(readelf -d zupt | sed -n 's/.*Shared library: \[\([^]]*\)\].*/\1/p')
|
||||
((${#needed[@]} > 0))
|
||||
for library in "${needed[@]}"; do
|
||||
case $library in
|
||||
libc.so.6|libm.so.6|libpthread.so.0) ;;
|
||||
*) echo "unexpected Linux runtime dependency: $library" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
if ldd zupt | grep -Fq 'not found'; then
|
||||
echo 'Linux portable binary has an unresolved runtime dependency' >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Assemble and extracted-package-test the tar.xz
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
epoch=$(<.source-date-epoch)
|
||||
root="$RUNNER_TEMP/linux-work/zupt-$version-linux-x86_64"
|
||||
output="$RUNNER_TEMP/release-linux-x86_64/zupt-$version-linux-x86_64.tar.xz"
|
||||
mkdir -p "$root" "$(dirname "$output")"
|
||||
install -m 0755 zupt "$root/zupt"
|
||||
install -m 0644 README.md CHANGELOG.md SECURITY.md THREAT_MODEL.md \
|
||||
LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 LICENSE-BSD-2-Clause \
|
||||
LICENSE-BSD-3-Clause LICENSE-CC0-1.0 NOTICE \
|
||||
THIRD-PARTY-NOTICES.md "$root/"
|
||||
tar --sort=name --mtime="@$epoch" --owner=0 --group=0 --numeric-owner \
|
||||
-C "$(dirname "$root")" -cJf "$output" "$(basename "$root")"
|
||||
extract=$(mktemp -d)
|
||||
tar -xJf "$output" -C "$extract"
|
||||
bash scripts/test-installed-zupt.sh \
|
||||
"$extract/$(basename "$root")/zupt"
|
||||
test "$(find "$extract/$(basename "$root")" -maxdepth 1 -type f | wc -l)" -eq 13
|
||||
sha256sum "$output"
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-linux-x86_64
|
||||
path: ${{ runner.temp }}/release-linux-x86_64/*.tar.xz
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
See CHANGELOG.md for release notes.
|
||||
gui-portable:
|
||||
name: Source-only GUI portable ZIP gate
|
||||
needs: [source-policy, build-and-test]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
- name: Install GUI smoke-test and archive tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential file python3 python3-pyqt6 unzip zip
|
||||
- name: Assemble, audit and execute the portable GUI source bundle
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
make -j"$(nproc)" V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
|
||||
root="$RUNNER_TEMP/gui-work/zupt-gui-$version-portable"
|
||||
output="$RUNNER_TEMP/release-gui-portable/zupt-gui-$version-portable.zip"
|
||||
mkdir -p "$root/assets" "$(dirname "$output")"
|
||||
install -m 0644 gui/src/zupt_gui.py "$root/zupt_gui.py"
|
||||
install -m 0755 packaging/portable/zupt-gui.sh \
|
||||
packaging/portable/zupt-gui.command "$root/"
|
||||
install -m 0644 packaging/portable/zupt-gui.bat "$root/"
|
||||
install -m 0644 packaging/portable/README.txt "$root/README.txt"
|
||||
install -m 0644 gui/assets/zupt-icon.png gui/assets/zupt.ico "$root/assets/"
|
||||
install -m 0644 LICENSE-AGPL-3.0 gui/LICENSE-GUI CHANGELOG.md "$root/"
|
||||
install -m 0644 gui/assets/README.md "$root/ASSET-PROVENANCE.md"
|
||||
bash scripts/check-source-only.sh --tree "$root"
|
||||
QT_QPA_PLATFORM=offscreen PATH="$PWD:$PATH" \
|
||||
"$root/zupt-gui.sh" --version | grep -Fx "zupt-gui $version"
|
||||
epoch=$(<.source-date-epoch)
|
||||
find "$root" -exec touch -d "@$epoch" {} +
|
||||
(cd "$(dirname "$root")" && zip -X -9 -r "$output" "$(basename "$root")")
|
||||
extract=$(mktemp -d)
|
||||
unzip -q "$output" -d "$extract"
|
||||
bash scripts/check-source-only.sh --tree "$extract/$(basename "$root")"
|
||||
QT_QPA_PLATFORM=offscreen PATH="$PWD:$PATH" \
|
||||
"$extract/$(basename "$root")/zupt-gui.sh" --version | \
|
||||
grep -Fx "zupt-gui $version"
|
||||
sha256sum "$output"
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-gui-portable
|
||||
path: ${{ runner.temp }}/release-gui-portable/*.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
### Verifying the tarball
|
||||
|
||||
```sh
|
||||
sha256sum -c zupt-${{ steps.ver.outputs.version }}.tar.gz.sha256
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
```sh
|
||||
tar xzf zupt-${{ steps.ver.outputs.version }}.tar.gz
|
||||
cd zupt-${{ steps.ver.outputs.version }}
|
||||
make
|
||||
make test
|
||||
sudo make install
|
||||
```
|
||||
target-packages:
|
||||
name: Windows and macOS release gates
|
||||
if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch'
|
||||
needs:
|
||||
- source-policy
|
||||
- build-and-test
|
||||
- strict-warnings
|
||||
- sanitizers
|
||||
- static-analysis
|
||||
- source-archive
|
||||
- debian-package
|
||||
- tumbleweed-rpm
|
||||
- gui-rpm-package
|
||||
- linux-portable
|
||||
- gui-portable
|
||||
uses: ./.github/workflows/cross-platform.yml
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
379
.github/workflows/cross-platform.yml
vendored
379
.github/workflows/cross-platform.yml
vendored
|
|
@ -1,181 +1,252 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
#
|
||||
# Cross-platform GUI + CLI binaries, built on REAL Windows and macOS runners.
|
||||
#
|
||||
# Why a dedicated workflow: the GUI is a PySide6/PyQt6 app and the CLI is
|
||||
# portable C11, but self-contained native installers (Windows .exe/.msi,
|
||||
# macOS .app/.dmg) can only be produced on the target OS. This workflow builds
|
||||
# them on GitHub's windows-latest and macos-latest runners and attaches them to
|
||||
# the GitHub release on a `v*` tag. Run it manually with "Run workflow"
|
||||
# (workflow_dispatch) to smoke-test the build before tagging.
|
||||
#
|
||||
# Artifacts produced:
|
||||
# Windows: vaptvupt.exe (CLI, mingw), vaptvupt-gui.exe (PyInstaller onefile),
|
||||
# VaptVupt-Setup-<ver>.exe (Inno Setup installer)
|
||||
# macOS: vaptvupt (CLI, universal where possible), VaptVupt-<ver>.dmg
|
||||
# All: vaptvupt-gui-<ver>-portable.zip (Python GUI + launchers)
|
||||
|
||||
name: cross-platform
|
||||
name: target release packages
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# ─────────────────────────── Windows ───────────────────────────
|
||||
windows:
|
||||
windows-x86_64:
|
||||
name: Windows x86_64 package and smoke test
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: 'msys2 {0}'
|
||||
shell: msys2 {0}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up MSYS2 (mingw gcc + make)
|
||||
uses: msys2/setup-msys2@v2
|
||||
- name: Check out the audited source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
|
||||
- name: Install the Windows C toolchain
|
||||
uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0
|
||||
with:
|
||||
msystem: UCRT64
|
||||
update: true
|
||||
install: >-
|
||||
mingw-w64-ucrt-x86_64-binutils
|
||||
mingw-w64-ucrt-x86_64-gcc
|
||||
make
|
||||
bsdtar
|
||||
coreutils
|
||||
- name: Build CLI (vaptvupt.exe, source-only, C fallback crypto)
|
||||
run: |
|
||||
make CC=gcc WITH_SDK=0 -j2
|
||||
./vaptvupt.exe version || ./vaptvupt version
|
||||
cp "$(ls vaptvupt.exe vaptvupt 2>/dev/null | head -1)" vaptvupt.exe 2>/dev/null || true
|
||||
- name: Set up Python
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Use the runner's native Python (not MSYS) for PyInstaller so the
|
||||
# produced .exe targets the standard Windows Python ABI.
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install PySide6 pyinstaller
|
||||
- name: Get version
|
||||
id: ver
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ver = (Select-String -Path include/zupt.h -Pattern '^#define ZUPT_VERSION_STRING "([^"]+)"').Matches.Groups[1].Value
|
||||
"version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
|
||||
- name: Bundle GUI with PyInstaller (vaptvupt-gui.exe)
|
||||
shell: pwsh
|
||||
run: |
|
||||
# onefile GUI that carries the CLI beside it via --add-binary.
|
||||
pyinstaller --noconfirm --onefile --windowed `
|
||||
--name vaptvupt-gui `
|
||||
--icon gui/assets/zupt-icon.png `
|
||||
--add-binary "vaptvupt.exe;." `
|
||||
--add-data "gui/assets/zupt-icon.png;assets" `
|
||||
gui/src/zupt_gui.py
|
||||
- name: Build Inno Setup installer
|
||||
shell: pwsh
|
||||
run: |
|
||||
choco install innosetup --no-progress -y
|
||||
& "$env:ChocolateyInstall\bin\ISCC.exe" `
|
||||
"/DAppVersion=${{ steps.ver.outputs.version }}" `
|
||||
packaging/windows/vaptvupt-gui.iss
|
||||
- name: Collect artifacts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$v = "${{ steps.ver.outputs.version }}"
|
||||
New-Item -ItemType Directory -Force out | Out-Null
|
||||
Copy-Item vaptvupt.exe "out/vaptvupt-$v-windows-x86_64.exe"
|
||||
Copy-Item dist/vaptvupt-gui.exe "out/vaptvupt-gui-$v-windows-x86_64.exe"
|
||||
if (Test-Path "packaging/windows/Output") {
|
||||
Copy-Item packaging/windows/Output/*.exe "out/" -ErrorAction SilentlyContinue
|
||||
}
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows
|
||||
path: out/*
|
||||
- name: Attach to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
diffutils
|
||||
file
|
||||
findutils
|
||||
git
|
||||
gzip
|
||||
make
|
||||
python
|
||||
tar
|
||||
unzip
|
||||
zip
|
||||
|
||||
# ─────────────────────────── macOS ───────────────────────────
|
||||
macos:
|
||||
- name: Audit source before building
|
||||
run: bash scripts/check-source-only.sh
|
||||
|
||||
- name: Build from source
|
||||
run: |
|
||||
test "$(uname -m)" = x86_64
|
||||
make clean
|
||||
make -j2 CC=gcc V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0
|
||||
|
||||
- name: Run the source-only distribution checks on Windows
|
||||
run: make CC=gcc V=1 WITH_SDK=0 WITH_PQBOX=0 INSTALL_LEGACY_ALIAS=0 check
|
||||
|
||||
- name: Native CLI smoke and round-trip
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
if [[ -x ./zupt.exe ]]; then
|
||||
exe=$PWD/zupt.exe
|
||||
elif [[ -x ./zupt ]]; then
|
||||
exe=$PWD/zupt
|
||||
else
|
||||
echo 'ZUPT executable was not produced' >&2
|
||||
exit 1
|
||||
fi
|
||||
version_output=$("$exe" --version)
|
||||
version_line=${version_output%%$'\n'*}
|
||||
read -r product reported_version _ <<< "$version_line"
|
||||
if [[ $product != zupt || $reported_version != "$version" ]]; then
|
||||
echo 'native Windows executable version does not match include/zupt.h' >&2
|
||||
exit 1
|
||||
fi
|
||||
"$exe" --help >/dev/null
|
||||
if "$exe" --definitely-invalid-option >/dev/null 2>&1; then
|
||||
echo 'invalid option returned success' >&2
|
||||
exit 1
|
||||
fi
|
||||
test_root=$(mktemp -d)
|
||||
trap 'chmod -R u+rwX "$test_root" 2>/dev/null || true; rm -rf -- "$test_root"' EXIT
|
||||
output_dir="$test_root/saída-安全"
|
||||
archive="$test_root/cópia-安全.zupt"
|
||||
mkdir -p "$test_root/input/subdir" "$output_dir"
|
||||
printf 'Windows release smoke test\n' > "$test_root/input/café.txt"
|
||||
printf 'UTF-8: café-安全\n' > "$test_root/input/subdir/ação-安全.txt"
|
||||
printf 'emoji filename\n' > "$test_root/input/subdir/emoji-😀.bin"
|
||||
: > "$test_root/input/empty"
|
||||
dd if=/dev/urandom of="$test_root/input/subdir/random.bin" bs=4096 count=4 2>/dev/null
|
||||
(cd "$test_root" && "$exe" compress "$archive" input)
|
||||
"$exe" test "$archive"
|
||||
"$exe" list "$archive" > "$test_root/list.txt" 2>&1
|
||||
grep -F 'café.txt' "$test_root/list.txt"
|
||||
grep -F 'ação-安全.txt' "$test_root/list.txt"
|
||||
grep -F 'emoji-😀.bin' "$test_root/list.txt"
|
||||
"$exe" extract -o "$output_dir" "$archive"
|
||||
diff -r "$test_root/input" "$output_dir/input"
|
||||
objdump -p "$exe" > "$test_root/imports.txt"
|
||||
if grep -Eqi '(vendor[/\\]|libvuptsdk|libpqvaptvupt|libgcc_s|libstdc\+\+|libwinpthread|msys-2[.]0|cygwin1)[^[:space:]]*[.]dll' \
|
||||
"$test_root/imports.txt"; then
|
||||
echo 'Windows binary imports a non-system or vendored runtime' >&2
|
||||
exit 1
|
||||
fi
|
||||
version_output=$(env PATH='/c/Windows/System32:/c/Windows' "$exe" --version)
|
||||
version_line=${version_output%%$'\n'*}
|
||||
read -r product reported_version _ <<< "$version_line"
|
||||
if [[ $product != zupt || $reported_version != "$version" ]]; then
|
||||
echo 'restricted-PATH Windows executable version does not match include/zupt.h' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Assemble Windows release files
|
||||
run: |
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
bundle="out/work/zupt-$version-windows-x86_64"
|
||||
mkdir -p out "$bundle"
|
||||
if [[ -x ./zupt.exe ]]; then source_exe=./zupt.exe; else source_exe=./zupt; fi
|
||||
install -m 0755 "$source_exe" "$bundle/zupt.exe"
|
||||
install -m 0644 README.md CHANGELOG.md LICENSE LICENSE-AGPL-3.0 LICENSE-GPL-3.0 \
|
||||
LICENSE-BSD-2-Clause LICENSE-BSD-3-Clause LICENSE-CC0-1.0 \
|
||||
NOTICE THIRD-PARTY-NOTICES.md "$bundle/"
|
||||
toolchain_prefix=${MINGW_PREFIX:-/ucrt64}
|
||||
install -m 0644 \
|
||||
"$toolchain_prefix/share/licenses/crt/COPYING" \
|
||||
"$bundle/MINGW-CRT-COPYING.txt"
|
||||
install -m 0644 \
|
||||
"$toolchain_prefix/share/licenses/crt/COPYING.MinGW-w64-runtime.txt" \
|
||||
"$bundle/COPYING.MinGW-w64-runtime.txt"
|
||||
install -m 0644 \
|
||||
"$toolchain_prefix/share/licenses/crt/COPYING.MinGW-w64.txt" \
|
||||
"$bundle/COPYING.MinGW-w64.txt"
|
||||
install -m 0644 \
|
||||
"$toolchain_prefix/share/licenses/gcc-libs/COPYING3" \
|
||||
"$bundle/GCC-COPYING3.txt"
|
||||
install -m 0644 \
|
||||
"$toolchain_prefix/share/licenses/gcc-libs/COPYING.RUNTIME" \
|
||||
"$bundle/GCC-RUNTIME-LIBRARY-EXCEPTION.txt"
|
||||
zip_path=$PWD/out/zupt-$version-windows-x86_64.zip
|
||||
(cd out/work && zip -9 -r "$zip_path" \
|
||||
"zupt-$version-windows-x86_64")
|
||||
|
||||
- name: Extract and functionally test the Windows ZIP
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' include/zupt.h)
|
||||
zip_path=$PWD/out/zupt-$version-windows-x86_64.zip
|
||||
unzip -t "$zip_path"
|
||||
extract_root=$(mktemp -d)
|
||||
cleanup() {
|
||||
chmod -R u+rwX "$extract_root" 2>/dev/null || true
|
||||
rm -rf -- "$extract_root"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
unzip -q "$zip_path" -d "$extract_root"
|
||||
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
|
||||
test -s "$extract_root/zupt-$version-windows-x86_64/$notice"
|
||||
done
|
||||
packaged_exe=$extract_root/zupt-$version-windows-x86_64/zupt.exe
|
||||
test -x "$packaged_exe"
|
||||
version_output=$(env PATH='/c/Windows/System32:/c/Windows' \
|
||||
"$packaged_exe" --version)
|
||||
version_line=${version_output%%$'\n'*}
|
||||
read -r product reported_version _ <<< "$version_line"
|
||||
if [[ $product != zupt || $reported_version != "$version" ]]; then
|
||||
echo 'Windows ZIP executable version does not match include/zupt.h' >&2
|
||||
exit 1
|
||||
fi
|
||||
env PATH='/c/Windows/System32:/c/Windows' "$packaged_exe" --help >/dev/null
|
||||
mkdir -p "$extract_root/smoke/input" "$extract_root/smoke/saída-安全"
|
||||
printf 'Windows ZIP package test\n' > "$extract_root/smoke/input/payload-ação-😀.txt"
|
||||
(
|
||||
cd "$extract_root/smoke"
|
||||
env PATH='/c/Windows/System32:/c/Windows' \
|
||||
"$packaged_exe" compress cópia-安全.zupt input
|
||||
env PATH='/c/Windows/System32:/c/Windows' \
|
||||
"$packaged_exe" test cópia-安全.zupt
|
||||
env PATH='/c/Windows/System32:/c/Windows' \
|
||||
"$packaged_exe" extract -o saída-安全 cópia-安全.zupt
|
||||
)
|
||||
cmp "$extract_root/smoke/input/payload-ação-😀.txt" \
|
||||
"$extract_root/smoke/saída-安全/input/payload-ação-😀.txt"
|
||||
|
||||
- name: Upload tested Windows files
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-windows-x86_64
|
||||
path: out/*.zip
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
macos-native:
|
||||
name: macOS native DMG and installed-image test
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build CLI (vaptvupt, clang)
|
||||
run: |
|
||||
make CC=clang WITH_SDK=0 -j3
|
||||
./vaptvupt version
|
||||
- uses: actions/setup-python@v5
|
||||
with: { python-version: '3.12' }
|
||||
- name: Install GUI build deps
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install PySide6 pyinstaller
|
||||
brew install create-dmg || true
|
||||
- name: Get version
|
||||
id: ver
|
||||
run: echo "version=$(awk -F'\"' '/^#define ZUPT_VERSION_STRING/{print $2}' include/zupt.h)" >> "$GITHUB_OUTPUT"
|
||||
- name: Bundle GUI (.app) with PyInstaller
|
||||
run: |
|
||||
pyinstaller --noconfirm --windowed \
|
||||
--name "VaptVupt" \
|
||||
--add-binary "vaptvupt:." \
|
||||
--add-data "gui/assets/zupt-icon.png:assets" \
|
||||
gui/src/zupt_gui.py
|
||||
- name: Build .dmg
|
||||
run: |
|
||||
V="${{ steps.ver.outputs.version }}"
|
||||
create-dmg --volname "VaptVupt $V" --window-size 500 300 \
|
||||
--app-drop-link 350 120 --icon "VaptVupt.app" 150 120 \
|
||||
"VaptVupt-$V.dmg" "dist/VaptVupt.app" || \
|
||||
{ mkdir -p dmgroot && cp -R dist/VaptVupt.app dmgroot/ && \
|
||||
hdiutil create -volname "VaptVupt $V" -srcfolder dmgroot -ov -format UDZO "VaptVupt-$V.dmg"; }
|
||||
- name: Collect artifacts
|
||||
run: |
|
||||
V="${{ steps.ver.outputs.version }}"
|
||||
mkdir -p out
|
||||
cp vaptvupt "out/vaptvupt-$V-macos"
|
||||
cp "VaptVupt-$V.dmg" out/
|
||||
- uses: actions/upload-artifact@v4
|
||||
- name: Check out the audited source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
name: macos
|
||||
path: out/*
|
||||
- name: Attach to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: out/*
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
|
||||
# ─────────────── Portable GUI (works on every OS) ───────────────
|
||||
portable:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Get version
|
||||
id: ver
|
||||
run: echo "version=$(awk -F'\"' '/^#define ZUPT_VERSION_STRING/{print $2}' include/zupt.h)" >> "$GITHUB_OUTPUT"
|
||||
- name: Assemble portable package
|
||||
- name: Audit source before building
|
||||
run: bash scripts/check-source-only.sh
|
||||
|
||||
- name: Build and validate the native DMG
|
||||
run: |
|
||||
V="${{ steps.ver.outputs.version }}"
|
||||
D="vaptvupt-gui-$V-portable"
|
||||
mkdir -p "$D/assets"
|
||||
cp gui/src/zupt_gui.py "$D/"
|
||||
cp gui/assets/zupt-icon.png "$D/assets/"
|
||||
cp packaging/portable/vaptvupt-gui.bat "$D/"
|
||||
cp packaging/portable/vaptvupt-gui.command "$D/"
|
||||
cp packaging/portable/vaptvupt-gui.sh "$D/"
|
||||
cp packaging/portable/README.txt "$D/"
|
||||
chmod +x "$D/vaptvupt-gui.command" "$D/vaptvupt-gui.sh"
|
||||
zip -r "$D.zip" "$D"
|
||||
- uses: actions/upload-artifact@v4
|
||||
mkdir -p out
|
||||
DIST_DIR="$PWD/out" RUN_CHECKS=1 bash packaging/build-dmg.sh
|
||||
|
||||
- name: Mount and functionally test the packaged binary
|
||||
run: |
|
||||
dmg=$(find out -maxdepth 1 -type f -name '*.dmg' -print -quit)
|
||||
test -n "$dmg"
|
||||
mount_point=$(mktemp -d)
|
||||
cleanup() {
|
||||
hdiutil detach "$mount_point" >/dev/null 2>&1 || true
|
||||
chmod -R u+rwX "$mount_point" 2>/dev/null || true
|
||||
rm -rf -- "$mount_point"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
hdiutil attach -nobrowse -readonly -mountpoint "$mount_point" "$dmg" >/dev/null
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' \
|
||||
include/zupt.h)
|
||||
packaged_binary=$mount_point/ZUPT.app/Contents/MacOS/zupt
|
||||
version_output=$("$packaged_binary" --version)
|
||||
version_line=${version_output%%$'\n'*}
|
||||
read -r product reported_version _ <<< "$version_line"
|
||||
if [[ $product != zupt || $reported_version != "$version" ]]; then
|
||||
echo 'mounted macOS executable version does not match include/zupt.h' >&2
|
||||
exit 1
|
||||
fi
|
||||
bash packaging/build-dmg.sh --test-binary \
|
||||
"$packaged_binary"
|
||||
hdiutil detach "$mount_point"
|
||||
trap - EXIT HUP INT TERM
|
||||
rmdir "$mount_point"
|
||||
|
||||
- name: Upload tested macOS DMG
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: portable
|
||||
path: vaptvupt-gui-*-portable.zip
|
||||
- name: Attach to release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: vaptvupt-gui-*-portable.zip
|
||||
name: release-macos-native
|
||||
path: out/*.dmg
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
|
|
|||
644
.github/workflows/promote-release.yml
vendored
Normal file
644
.github/workflows/promote-release.yml
vendored
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# Copyright (c) 2025-2026 Cristian Cezar Moisés
|
||||
|
||||
name: Promote a tested release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
source_run_id:
|
||||
description: Successful manually dispatched CI run that produced the assets
|
||||
required: true
|
||||
type: number
|
||||
tag:
|
||||
description: Existing annotated release tag, for example v5.2.2
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: promote-release-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
promote:
|
||||
name: Promote tested assets to the canonical GitHub release
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
steps:
|
||||
- name: Validate the tag and source CI run through the GitHub API
|
||||
id: provenance
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
SOURCE_RUN_ID: ${{ inputs.source_run_id }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
[[ $SOURCE_RUN_ID =~ ^[1-9][0-9]*$ ]] || {
|
||||
echo 'source_run_id must be a positive integer' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $RELEASE_TAG =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || {
|
||||
echo 'tag must have the form vX.Y.Z' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
tag_ref_api="repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG"
|
||||
tag_object_type=$(gh api "$tag_ref_api" --jq '.object.type')
|
||||
tag_object_sha=$(gh api "$tag_ref_api" --jq '.object.sha')
|
||||
[[ $tag_object_type == tag && $tag_object_sha =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo 'GitHub release ref is not an annotated tag' >&2
|
||||
exit 1
|
||||
}
|
||||
tag_object_api="repos/$GITHUB_REPOSITORY/git/tags/$tag_object_sha"
|
||||
target_type=$(gh api "$tag_object_api" --jq '.object.type')
|
||||
peeled_sha=$(gh api "$tag_object_api" --jq '.object.sha')
|
||||
[[ $target_type == commit && $peeled_sha =~ ^[0-9a-f]{40}$ ]] || {
|
||||
echo 'annotated tag does not point directly to a commit' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_api="repos/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID"
|
||||
run_status=$(gh api "$run_api" --jq '.status')
|
||||
run_conclusion=$(gh api "$run_api" --jq '.conclusion')
|
||||
run_event=$(gh api "$run_api" --jq '.event')
|
||||
run_head_branch=$(gh api "$run_api" --jq '.head_branch // ""')
|
||||
run_workflow_id=$(gh api "$run_api" --jq '.workflow_id')
|
||||
run_sha=$(gh api "$run_api" --jq '.head_sha')
|
||||
run_repository=$(gh api "$run_api" --jq '.head_repository.full_name // ""')
|
||||
workflow_path=$(gh api \
|
||||
"repos/$GITHUB_REPOSITORY/actions/workflows/$run_workflow_id" \
|
||||
--jq '.path')
|
||||
[[ $run_status == completed && $run_conclusion == success ]] || {
|
||||
echo 'source CI run is not completed successfully' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $run_event == workflow_dispatch ]] || {
|
||||
echo 'source CI run must have been started with workflow_dispatch' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $run_head_branch == "$RELEASE_TAG" ]] || {
|
||||
echo 'source CI run must have been dispatched from the release tag' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $workflow_path == .github/workflows/ci.yml ]] || {
|
||||
echo 'source run did not execute .github/workflows/ci.yml' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $run_repository == "$GITHUB_REPOSITORY" ]] || {
|
||||
echo 'source CI run belongs to a different head repository' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $run_sha =~ ^[0-9a-f]{40}$ && $run_sha == "$peeled_sha" ]] || {
|
||||
echo 'source CI head SHA does not match the peeled release tag' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
artifact_json=$RUNNER_TEMP/source-run-artifacts.json
|
||||
gh api "$run_api/artifacts?per_page=100" > "$artifact_json"
|
||||
python3 - "$artifact_json" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
expected = {
|
||||
"release-source",
|
||||
"release-deb",
|
||||
"release-rpm",
|
||||
"release-gui-deb",
|
||||
"release-gui-rpm",
|
||||
"release-linux-x86_64",
|
||||
"release-gui-portable",
|
||||
"release-windows-x86_64",
|
||||
"release-macos-native",
|
||||
}
|
||||
artifacts = payload.get("artifacts", [])
|
||||
names = [artifact.get("name", "") for artifact in artifacts]
|
||||
if payload.get("total_count") != len(expected):
|
||||
raise SystemExit("source CI run artifact count mismatch")
|
||||
if set(names) != expected or len(names) != len(set(names)):
|
||||
raise SystemExit("source CI run artifact-name allowlist mismatch")
|
||||
if any(artifact.get("expired") for artifact in artifacts):
|
||||
raise SystemExit("one or more source CI artifacts have expired")
|
||||
PY
|
||||
{
|
||||
printf 'head_sha=%s\n' "$peeled_sha"
|
||||
printf 'tag_object_sha=%s\n' "$tag_object_sha"
|
||||
printf 'tag=%s\n' "$RELEASE_TAG"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Check out the exact tested commit without persisted credentials
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ steps.provenance.outputs.head_sha }}
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
lfs: false
|
||||
submodules: false
|
||||
|
||||
- name: Confirm the local annotated tag and source version
|
||||
id: release
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.provenance.outputs.tag }}
|
||||
EXPECTED_SHA: ${{ steps.provenance.outputs.head_sha }}
|
||||
EXPECTED_TAG_OBJECT: ${{ steps.provenance.outputs.tag_object_sha }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ $(git rev-parse HEAD) == "$EXPECTED_SHA" ]] || {
|
||||
echo 'checked-out commit differs from the validated source run' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $(git cat-file -t "refs/tags/$RELEASE_TAG") == tag ]] || {
|
||||
echo 'checked-out release ref is not an annotated tag' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $(git rev-parse "refs/tags/$RELEASE_TAG") == "$EXPECTED_TAG_OBJECT" ]] || {
|
||||
echo 'local annotated tag object differs from the validated GitHub tag' >&2
|
||||
exit 1
|
||||
}
|
||||
[[ $(git rev-parse "$RELEASE_TAG^{commit}") == "$EXPECTED_SHA" ]] || {
|
||||
echo 'local peeled tag does not match the tested commit' >&2
|
||||
exit 1
|
||||
}
|
||||
version=$(sed -n 's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p' \
|
||||
include/zupt.h)
|
||||
[[ -n $version && $RELEASE_TAG == "v$version" ]] || {
|
||||
echo 'tag does not match include/zupt.h' >&2
|
||||
exit 1
|
||||
}
|
||||
printf 'version=%s\n' "$version" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Install validation tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y file libarchive-tools python3 python3-pyqt6 rpm unzip xz-utils
|
||||
|
||||
- name: Download the exact source artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-source
|
||||
path: ${{ runner.temp }}/incoming/release-source
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact DEB artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-deb
|
||||
path: ${{ runner.temp }}/incoming/release-deb
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact RPM artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-rpm
|
||||
path: ${{ runner.temp }}/incoming/release-rpm
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact GUI DEB artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-gui-deb
|
||||
path: ${{ runner.temp }}/incoming/release-gui-deb
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact GUI RPM artifacts from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-gui-rpm
|
||||
path: ${{ runner.temp }}/incoming/release-gui-rpm
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact Linux tar.xz artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-linux-x86_64
|
||||
path: ${{ runner.temp }}/incoming/release-linux-x86_64
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact portable GUI source bundle
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-gui-portable
|
||||
path: ${{ runner.temp }}/incoming/release-gui-portable
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact Windows artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-windows-x86_64
|
||||
path: ${{ runner.temp }}/incoming/release-windows-x86_64
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Download the exact macOS artifact from the validated run
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-macos-native
|
||||
path: ${{ runner.temp }}/incoming/release-macos-native
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ inputs.source_run_id }}
|
||||
github-token: ${{ github.token }}
|
||||
|
||||
- name: Enforce the allowlist and validate every release format
|
||||
env:
|
||||
RELEASE_TAG: ${{ steps.provenance.outputs.tag }}
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
export LC_ALL=C
|
||||
incoming=$RUNNER_TEMP/incoming
|
||||
asset_dir=$RUNNER_TEMP/release-assets
|
||||
mkdir -p "$asset_dir"
|
||||
|
||||
artifact_names=(
|
||||
release-source
|
||||
release-deb
|
||||
release-rpm
|
||||
release-gui-deb
|
||||
release-gui-rpm
|
||||
release-linux-x86_64
|
||||
release-gui-portable
|
||||
release-windows-x86_64
|
||||
release-macos-native
|
||||
)
|
||||
expected_dirs=$RUNNER_TEMP/artifact-dirs.expected
|
||||
actual_dirs=$RUNNER_TEMP/artifact-dirs.actual
|
||||
printf '%s\0' "${artifact_names[@]}" | LC_ALL=C sort -z > "$expected_dirs"
|
||||
find "$incoming" -mindepth 1 -maxdepth 1 -type d -printf '%f\0' | \
|
||||
LC_ALL=C sort -z > "$actual_dirs"
|
||||
cmp "$expected_dirs" "$actual_dirs" || {
|
||||
echo 'downloaded artifact directory allowlist mismatch' >&2
|
||||
exit 1
|
||||
}
|
||||
if find "$incoming" -mindepth 1 -maxdepth 1 ! -type d -print -quit | \
|
||||
grep -q .; then
|
||||
echo 'unexpected non-directory entry in artifact download root' >&2
|
||||
exit 1
|
||||
fi
|
||||
if find "$incoming" -mindepth 2 ! -type f -print -quit | grep -q .; then
|
||||
echo 'artifact contains a directory, symlink, or special file' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source_name="zupt-$VERSION.tar.gz"
|
||||
source_sidecar="$source_name.sha256"
|
||||
deb_name="zupt_${VERSION}_amd64.deb"
|
||||
rpm_name="zupt-$VERSION-0.x86_64.rpm"
|
||||
srpm_name="zupt-$VERSION-0.src.rpm"
|
||||
gui_deb_name="zupt-gui_${VERSION}_all.deb"
|
||||
gui_rpm_name="zupt-gui-$VERSION-1.noarch.rpm"
|
||||
gui_srpm_name="zupt-gui-$VERSION-1.src.rpm"
|
||||
linux_tar_name="zupt-$VERSION-linux-x86_64.tar.xz"
|
||||
gui_portable_name="zupt-gui-$VERSION-portable.zip"
|
||||
windows_zip_name="zupt-$VERSION-windows-x86_64.zip"
|
||||
dmg_relative=()
|
||||
for arch in x86_64 arm64; do
|
||||
candidate="release-macos-native/ZUPT-$VERSION-macOS-$arch.dmg"
|
||||
[[ ! -f $incoming/$candidate || -L $incoming/$candidate ]] || \
|
||||
dmg_relative+=("$candidate")
|
||||
done
|
||||
((${#dmg_relative[@]} == 1)) || {
|
||||
echo 'expected exactly one native macOS DMG' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
expected_relative=(
|
||||
"release-source/$source_name"
|
||||
"release-source/$source_sidecar"
|
||||
"release-deb/$deb_name"
|
||||
"release-rpm/$rpm_name"
|
||||
"release-rpm/$srpm_name"
|
||||
"release-gui-deb/$gui_deb_name"
|
||||
"release-gui-rpm/$gui_rpm_name"
|
||||
"release-gui-rpm/$gui_srpm_name"
|
||||
"release-linux-x86_64/$linux_tar_name"
|
||||
"release-gui-portable/$gui_portable_name"
|
||||
"release-windows-x86_64/$windows_zip_name"
|
||||
"${dmg_relative[0]}"
|
||||
)
|
||||
expected_relative_list=$RUNNER_TEMP/artifact-files.expected
|
||||
actual_relative_list=$RUNNER_TEMP/artifact-files.actual
|
||||
printf '%s\0' "${expected_relative[@]}" | LC_ALL=C sort -z \
|
||||
> "$expected_relative_list"
|
||||
find "$incoming" -mindepth 2 -type f -printf '%P\0' | LC_ALL=C sort -z \
|
||||
> "$actual_relative_list"
|
||||
cmp "$expected_relative_list" "$actual_relative_list" || {
|
||||
echo 'downloaded file allowlist mismatch' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
expected_assets=()
|
||||
for relative in "${expected_relative[@]}"; do
|
||||
name=${relative#*/}
|
||||
cp -- "$incoming/$relative" "$asset_dir/$name"
|
||||
expected_assets+=("$name")
|
||||
done
|
||||
expected_list=$RUNNER_TEMP/release-assets.expected
|
||||
printf '%s\0' "${expected_assets[@]}" | LC_ALL=C sort -z > "$expected_list"
|
||||
|
||||
source_tar=$asset_dir/$source_name
|
||||
sidecar=$asset_dir/$source_sidecar
|
||||
actual_source_sha=$(sha256sum "$source_tar" | awk '{print $1}')
|
||||
[[ $(<"$sidecar") == "$actual_source_sha $source_name" ]] || {
|
||||
echo 'source archive sidecar is not the exact expected SHA-256 record' >&2
|
||||
exit 1
|
||||
}
|
||||
(cd "$asset_dir" && sha256sum -c -- "$source_sidecar")
|
||||
file "$source_tar" | grep -Eqi 'gzip compressed data'
|
||||
tar -tzf "$source_tar" >/dev/null
|
||||
bash scripts/check-source-only.sh --archive "$source_tar"
|
||||
archive_version=$(tar -xOf "$source_tar" \
|
||||
"zupt-$VERSION/include/zupt.h" | sed -n \
|
||||
's/^#define ZUPT_VERSION_STRING "\([^"]*\)".*/\1/p')
|
||||
[[ $archive_version == "$VERSION" && $RELEASE_TAG == "v$archive_version" ]] || {
|
||||
echo 'source archive version does not match the release tag' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
deb=$asset_dir/$deb_name
|
||||
dpkg-deb --info "$deb" >/dev/null
|
||||
[[ $(dpkg-deb -f "$deb" Package) == zupt ]]
|
||||
[[ $(dpkg-deb -f "$deb" Version) == "$VERSION" ]]
|
||||
[[ $(dpkg-deb -f "$deb" Architecture) == amd64 ]]
|
||||
|
||||
rpm_file=$asset_dir/$rpm_name
|
||||
[[ $(rpm -qp --qf '%{NAME}' "$rpm_file") == zupt ]]
|
||||
[[ $(rpm -qp --qf '%{VERSION}' "$rpm_file") == "$VERSION" ]]
|
||||
[[ $(rpm -qp --qf '%{RELEASE}' "$rpm_file") == 0 ]]
|
||||
[[ $(rpm -qp --qf '%{ARCH}' "$rpm_file") == x86_64 ]]
|
||||
srpm=$asset_dir/$srpm_name
|
||||
[[ $(rpm -qp --qf '%{NAME}' "$srpm") == zupt ]]
|
||||
[[ $(rpm -qp --qf '%{VERSION}' "$srpm") == "$VERSION" ]]
|
||||
[[ $(rpm -qp --qf '%{RELEASE}' "$srpm") == 0 ]]
|
||||
[[ $(rpm -qp --qf '%{ARCH}' "$srpm") == src ]]
|
||||
|
||||
gui_deb=$asset_dir/$gui_deb_name
|
||||
dpkg-deb --info "$gui_deb" >/dev/null
|
||||
[[ $(dpkg-deb -f "$gui_deb" Package) == zupt-gui ]]
|
||||
[[ $(dpkg-deb -f "$gui_deb" Version) == "$VERSION" ]]
|
||||
[[ $(dpkg-deb -f "$gui_deb" Architecture) == all ]]
|
||||
|
||||
gui_rpm=$asset_dir/$gui_rpm_name
|
||||
[[ $(rpm -qp --qf '%{NAME}' "$gui_rpm") == zupt-gui ]]
|
||||
[[ $(rpm -qp --qf '%{VERSION}' "$gui_rpm") == "$VERSION" ]]
|
||||
[[ $(rpm -qp --qf '%{RELEASE}' "$gui_rpm") == 1 ]]
|
||||
[[ $(rpm -qp --qf '%{ARCH}' "$gui_rpm") == noarch ]]
|
||||
rpm -qp --requires "$gui_rpm" | grep -Fx "zupt >= $VERSION"
|
||||
gui_srpm=$asset_dir/$gui_srpm_name
|
||||
[[ $(rpm -qp --qf '%{NAME}' "$gui_srpm") == zupt-gui ]]
|
||||
[[ $(rpm -qp --qf '%{VERSION}' "$gui_srpm") == "$VERSION" ]]
|
||||
[[ $(rpm -qp --qf '%{RELEASE}' "$gui_srpm") == 1 ]]
|
||||
[[ $(rpm -qp --qf '%{ARCH}' "$gui_srpm") == src ]]
|
||||
|
||||
linux_tar=$asset_dir/$linux_tar_name
|
||||
python3 - "$linux_tar" "zupt-$VERSION-linux-x86_64" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
archive = pathlib.Path(sys.argv[1])
|
||||
root = sys.argv[2]
|
||||
expected_files = {
|
||||
"zupt", "README.md", "CHANGELOG.md", "SECURITY.md",
|
||||
"THREAT_MODEL.md", "LICENSE", "LICENSE-AGPL-3.0",
|
||||
"LICENSE-GPL-3.0", "LICENSE-BSD-2-Clause",
|
||||
"LICENSE-BSD-3-Clause", "LICENSE-CC0-1.0", "NOTICE",
|
||||
"THIRD-PARTY-NOTICES.md",
|
||||
}
|
||||
with tarfile.open(archive, "r:xz") as package:
|
||||
members = package.getmembers()
|
||||
names = [member.name for member in members]
|
||||
if len(names) != len(set(names)):
|
||||
raise SystemExit("duplicate Linux tar member")
|
||||
actual_files = set()
|
||||
for member in members:
|
||||
path = pathlib.PurePosixPath(member.name)
|
||||
if (path.is_absolute() or ".." in path.parts or not path.parts or
|
||||
path.parts[0] != root or member.issym() or member.islnk() or
|
||||
not (member.isdir() or member.isfile())):
|
||||
raise SystemExit("unsafe Linux tar member")
|
||||
if member.isfile():
|
||||
actual_files.add("/".join(path.parts[1:]))
|
||||
if actual_files != expected_files:
|
||||
raise SystemExit("Linux tar member allowlist mismatch")
|
||||
PY
|
||||
linux_extract=$RUNNER_TEMP/linux-package
|
||||
mkdir -p "$linux_extract"
|
||||
tar -xJf "$linux_tar" -C "$linux_extract"
|
||||
linux_binary="$linux_extract/zupt-$VERSION-linux-x86_64/zupt"
|
||||
file "$linux_binary" | grep -Eqi 'ELF.*executable'
|
||||
bash scripts/test-installed-zupt.sh "$linux_binary"
|
||||
|
||||
gui_portable=$asset_dir/$gui_portable_name
|
||||
python3 - "$gui_portable" "zupt-gui-$VERSION-portable" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
archive = pathlib.Path(sys.argv[1])
|
||||
root = sys.argv[2]
|
||||
expected = {
|
||||
f"{root}/", f"{root}/assets/", f"{root}/zupt_gui.py",
|
||||
f"{root}/zupt-gui.sh", f"{root}/zupt-gui.command",
|
||||
f"{root}/zupt-gui.bat", f"{root}/README.txt",
|
||||
f"{root}/assets/zupt-icon.png", f"{root}/assets/zupt.ico",
|
||||
f"{root}/LICENSE-AGPL-3.0", f"{root}/LICENSE-GUI",
|
||||
f"{root}/ASSET-PROVENANCE.md", f"{root}/CHANGELOG.md",
|
||||
}
|
||||
with zipfile.ZipFile(archive) as package:
|
||||
names = package.namelist()
|
||||
if len(names) != len(set(names)) or set(names) != expected:
|
||||
raise SystemExit("portable GUI ZIP member allowlist mismatch")
|
||||
for name in names:
|
||||
path = pathlib.PurePosixPath(name)
|
||||
if path.is_absolute() or ".." in path.parts or path.parts[0] != root:
|
||||
raise SystemExit("unsafe portable GUI ZIP member")
|
||||
PY
|
||||
bash scripts/check-source-only.sh --archive "$gui_portable"
|
||||
gui_extract=$RUNNER_TEMP/gui-portable
|
||||
mkdir -p "$gui_extract"
|
||||
unzip -q "$gui_portable" -d "$gui_extract"
|
||||
QT_QPA_PLATFORM=offscreen ZUPT_BIN="$linux_binary" \
|
||||
"$gui_extract/zupt-gui-$VERSION-portable/zupt-gui.sh" --version | \
|
||||
grep -Fx "zupt-gui $VERSION"
|
||||
|
||||
windows_zip=$asset_dir/$windows_zip_name
|
||||
unzip -t "$windows_zip" >/dev/null
|
||||
python3 - "$windows_zip" "zupt-$VERSION-windows-x86_64" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
archive = pathlib.Path(sys.argv[1])
|
||||
root = sys.argv[2]
|
||||
with zipfile.ZipFile(archive) as package:
|
||||
names = package.namelist()
|
||||
if len(names) != len(set(names)):
|
||||
raise SystemExit("duplicate Windows ZIP member")
|
||||
expected = {
|
||||
f"{root}/",
|
||||
f"{root}/zupt.exe",
|
||||
f"{root}/README.md",
|
||||
f"{root}/CHANGELOG.md",
|
||||
f"{root}/LICENSE",
|
||||
f"{root}/LICENSE-AGPL-3.0",
|
||||
f"{root}/LICENSE-GPL-3.0",
|
||||
f"{root}/LICENSE-BSD-2-Clause",
|
||||
f"{root}/LICENSE-BSD-3-Clause",
|
||||
f"{root}/LICENSE-CC0-1.0",
|
||||
f"{root}/NOTICE",
|
||||
f"{root}/THIRD-PARTY-NOTICES.md",
|
||||
f"{root}/MINGW-CRT-COPYING.txt",
|
||||
f"{root}/COPYING.MinGW-w64-runtime.txt",
|
||||
f"{root}/COPYING.MinGW-w64.txt",
|
||||
f"{root}/GCC-COPYING3.txt",
|
||||
f"{root}/GCC-RUNTIME-LIBRARY-EXCEPTION.txt",
|
||||
}
|
||||
if set(names) != expected:
|
||||
raise SystemExit("Windows ZIP member allowlist mismatch")
|
||||
for name in names:
|
||||
path = pathlib.PurePosixPath(name)
|
||||
if (path.is_absolute() or "\\" in name or ".." in path.parts or
|
||||
not path.parts or path.parts[0] != root):
|
||||
raise SystemExit("unsafe or unexpected Windows ZIP member")
|
||||
executable = f"{root}/zupt.exe"
|
||||
if names.count(executable) != 1:
|
||||
raise SystemExit("Windows ZIP executable is missing or duplicated")
|
||||
for notice in (
|
||||
f"{root}/MINGW-CRT-COPYING.txt",
|
||||
f"{root}/COPYING.MinGW-w64-runtime.txt",
|
||||
f"{root}/COPYING.MinGW-w64.txt",
|
||||
f"{root}/GCC-COPYING3.txt",
|
||||
f"{root}/GCC-RUNTIME-LIBRARY-EXCEPTION.txt",
|
||||
):
|
||||
if not package.read(notice):
|
||||
raise SystemExit("Windows toolchain notice is empty")
|
||||
PY
|
||||
unzip -p "$windows_zip" \
|
||||
"zupt-$VERSION-windows-x86_64/zupt.exe" \
|
||||
> "$RUNNER_TEMP/windows-zip-zupt.exe"
|
||||
python3 - "$RUNNER_TEMP/windows-zip-zupt.exe" <<'PY'
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
|
||||
executable = pathlib.Path(sys.argv[1])
|
||||
with executable.open("rb") as stream:
|
||||
header = stream.read(64)
|
||||
if len(header) != 64 or header[:2] != b"MZ":
|
||||
raise SystemExit("Windows ZIP executable lacks MZ magic")
|
||||
pe_offset = struct.unpack_from("<I", header, 0x3C)[0]
|
||||
stream.seek(pe_offset)
|
||||
if stream.read(4) != b"PE\0\0":
|
||||
raise SystemExit("Windows ZIP executable lacks PE signature")
|
||||
PY
|
||||
|
||||
dmg_name=${dmg_relative[0]#*/}
|
||||
dmg=$asset_dir/$dmg_name
|
||||
python3 - "$dmg" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
image = pathlib.Path(sys.argv[1])
|
||||
with image.open("rb") as stream:
|
||||
stream.seek(-512, 2)
|
||||
if stream.read(4) != b"koly":
|
||||
raise SystemExit("DMG lacks the UDIF trailer magic")
|
||||
PY
|
||||
|
||||
checksum_tmp=$RUNNER_TEMP/SHA256SUMS
|
||||
(cd "$asset_dir" && xargs -0 sha256sum < "$expected_list") \
|
||||
> "$checksum_tmp"
|
||||
mv "$checksum_tmp" "$asset_dir/SHA256SUMS"
|
||||
(cd "$asset_dir" && sha256sum -c SHA256SUMS)
|
||||
cp "$expected_list" "$RUNNER_TEMP/release-assets.list"
|
||||
printf 'SHA256SUMS\0' >> "$RUNNER_TEMP/release-assets.list"
|
||||
echo 'All downloaded release assets match the exact allowlist and formats.'
|
||||
|
||||
- name: Refuse to mutate an existing GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ steps.provenance.outputs.tag }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
existing_tags=$RUNNER_TEMP/github-release-tags
|
||||
gh api --paginate "repos/$GITHUB_REPOSITORY/releases" \
|
||||
--jq '.[].tag_name' > "$existing_tags"
|
||||
if grep -Fxq -- "$RELEASE_TAG" "$existing_tags"; then
|
||||
echo 'GitHub release already exists; refusing to replace or add assets' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Publish the already-tested byte-identical asset set
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RELEASE_TAG: ${{ steps.provenance.outputs.tag }}
|
||||
RELEASE_COMMIT: ${{ steps.provenance.outputs.head_sha }}
|
||||
VERSION: ${{ steps.release.outputs.version }}
|
||||
SOURCE_RUN_ID: ${{ inputs.source_run_id }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
set +x
|
||||
umask 077
|
||||
asset_dir=$RUNNER_TEMP/release-assets
|
||||
(cd "$asset_dir" && sha256sum -c SHA256SUMS)
|
||||
mapfile -d '' -t asset_names < "$RUNNER_TEMP/release-assets.list"
|
||||
release_assets=()
|
||||
for name in "${asset_names[@]}"; do
|
||||
path=$asset_dir/$name
|
||||
[[ -f $path && ! -L $path ]] || {
|
||||
printf 'validated release asset disappeared or changed type: %q\n' \
|
||||
"$name" >&2
|
||||
exit 1
|
||||
}
|
||||
release_assets+=("$path")
|
||||
done
|
||||
cat > "$RUNNER_TEMP/release-notes.md" <<EOF
|
||||
ZUPT $VERSION was built and tested by manually dispatched CI run
|
||||
https://github.com/$GITHUB_REPOSITORY/actions/runs/$SOURCE_RUN_ID
|
||||
for annotated tag $RELEASE_TAG at commit $RELEASE_COMMIT. The job
|
||||
definitions and logs in that run record the runner images,
|
||||
architectures, toolchains, results, and explicit skips.
|
||||
|
||||
The attached source archive, CLI DEB/RPM/source RPM, GUI
|
||||
DEB/RPM/source RPM, notice-bearing Linux x86_64 tar.xz, source-only
|
||||
portable GUI ZIP, Windows CLI ZIP, and native macOS CLI DMG are the
|
||||
exact artifacts validated by that run. SHA256SUMS records every
|
||||
attached payload asset. GitHub is the canonical upstream release.
|
||||
|
||||
Binary packages are release-page assets only. The Git tree and source
|
||||
archive remain source-only, built with WITH_SDK=0 and WITH_PQBOX=0.
|
||||
AppImage and bare executables are intentionally excluded: the former
|
||||
lacks an audited runtime source/relink handoff, while the latter does
|
||||
not carry the required license and notice payload beside the program.
|
||||
EOF
|
||||
gh release create "$RELEASE_TAG" "${release_assets[@]}" \
|
||||
--repo "$GITHUB_REPOSITORY" --draft --verify-tag \
|
||||
--title "ZUPT $VERSION" \
|
||||
--notes-file "$RUNNER_TEMP/release-notes.md"
|
||||
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false
|
||||
Loading…
Reference in a new issue