From 4adb3fe9aae9a50f9981949103c56957ba97a2bd Mon Sep 17 00:00:00 2001 From: Ivan Date: Sat, 9 May 2026 19:00:50 -0500 Subject: [PATCH] chore(ci): update build scripts and release workflow --- .github/workflows/build-release.yml | 141 +++++++++++++++--- scripts/build-backend.js | 54 ++++++- scripts/ci/docker-build-entry.sh | 7 +- scripts/ci/github-apt-linux-packaging.sh | 22 ++- .../ci/github-build-linux-release-assets.sh | 35 +++-- 5 files changed, 218 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml index dbba2a6..c994aa0 100644 --- a/.github/workflows/build-release.yml +++ b/.github/workflows/build-release.yml @@ -81,12 +81,10 @@ jobs: run_unit_tests: true linux-release: - name: Linux release assets + name: Linux release assets (x64) needs: frontend runs-on: ubuntu-latest timeout-minutes: 120 - outputs: - hashes: ${{ steps.slsa-hashes.outputs.hashes }} permissions: contents: read actions: write @@ -156,9 +154,116 @@ jobs: - name: Build release-assets run: bash scripts/ci/github-build-linux-release-assets.sh + - name: Upload Linux release artifact (x64) + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-linux-release-x64-${{ github.ref_name }}-${{ github.run_id }} + path: release-assets/ + if-no-files-found: error + retention-days: 30 + + linux-release-arm64: + name: Linux release assets (arm64) + needs: frontend + runs-on: ubuntu-24.04-arm + timeout-minutes: 120 + permissions: + contents: read + actions: write + env: + FRONTEND_ARTIFACT_NAME: ${{ needs.frontend.outputs.artifact_name }} + MESHCHATX_FRONTEND_PREBUILT: "1" + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Poetry (PyPI pin) + env: + POETRY_VERSION: ${{ env.POETRY_VERSION }} + run: bash scripts/ci/github-install-poetry.sh + + - name: Cache Poetry downloads + uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 + with: + path: ~/.cache/pypoetry + key: ${{ runner.os }}-pypoetry-${{ hashFiles('poetry.lock') }} + restore-keys: | + ${{ runner.os }}-pypoetry- + + - name: Enable pnpm (corepack) + run: corepack enable && corepack prepare "pnpm@${PNPM_VERSION}" --activate + + - name: Set up Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f + with: + node-version: ${{ env.NODE_VERSION }} + cache: pnpm + cache-dependency-path: pnpm-lock.yaml + + - name: Linux packaging APT dependencies + run: bash scripts/ci/github-apt-linux-packaging.sh + + - name: Install project dependencies + run: bash scripts/ci/github-install-deps.sh + + - name: Download frontend artifact + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: ${{ env.FRONTEND_ARTIFACT_NAME }} + path: meshchatx/public + + - name: Verify frontend artifact contents + run: | + set -euo pipefail + test -f meshchatx/public/index.html + test -d meshchatx/public/assets + test -d meshchatx/public/reticulum-docs-bundled/current + + - name: Build release-assets + run: bash scripts/ci/github-build-linux-release-assets.sh + + - name: Upload Linux release artifact (arm64) + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: meshchatx-linux-release-arm64-${{ github.ref_name }}-${{ github.run_id }} + path: release-assets/ + if-no-files-found: error + retention-days: 30 + + collect-linux-slsa-subjects: + name: SLSA subjects + cosign (Linux) + needs: [linux-release, linux-release-arm64] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + permissions: + contents: read + actions: write + id-token: write + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Download x64 artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-linux-release-x64-${{ github.ref_name }}-${{ github.run_id }} + path: release-assets + + - name: Download arm64 artifacts + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-linux-release-arm64-${{ github.ref_name }}-${{ github.run_id }} + path: release-assets + - name: SLSA subject hashes - id: slsa-hashes - if: startsWith(github.ref, 'refs/tags/') + id: hash run: bash scripts/ci/github-slsa-hashes-release-assets.sh - name: SLSA attestations (cosign) @@ -187,14 +292,6 @@ jobs: sh scripts/ci/attest-release-assets.sh ./release-assets rm -f /tmp/cosign.key - - name: Upload Linux release artifact - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 - with: - name: meshchatx-linux-release-${{ github.ref_name }}-${{ github.run_id }} - path: release-assets/ - if-no-files-found: error - retention-days: 30 - flatpak: name: Flatpak (electron-forge) needs: frontend @@ -467,7 +564,7 @@ jobs: slsa-provenance-linux: name: SLSA provenance (Linux) - needs: [linux-release] + needs: [collect-linux-slsa-subjects] if: startsWith(github.ref, 'refs/tags/') permissions: id-token: write @@ -475,7 +572,7 @@ jobs: actions: read uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 with: - base64-subjects: ${{ needs.linux-release.outputs.hashes }} + base64-subjects: ${{ needs.collect-linux-slsa-subjects.outputs.hashes }} upload-assets: false provenance-name: meshchatx-linux-${{ github.ref_name }}.intoto.jsonl @@ -541,6 +638,8 @@ jobs: name: Draft GitHub release (all assets + SLSA) needs: - linux-release + - linux-release-arm64 + - collect-linux-slsa-subjects - slsa-provenance-linux - build-release - slsa-provenance-desktop @@ -553,6 +652,8 @@ jobs: !cancelled() && startsWith(github.ref, 'refs/tags/') && needs.linux-release.result == 'success' && + needs.linux-release-arm64.result == 'success' && + needs.collect-linux-slsa-subjects.result == 'success' && needs.slsa-provenance-linux.result == 'success' && needs.build-release.result == 'success' && needs.collect-desktop-slsa-subjects.result == 'success' && @@ -597,10 +698,16 @@ jobs: echo "track=${track}" >> "${GITHUB_OUTPUT}" echo "Resolved tag ${GITHUB_REF_NAME} -> track=${track}" - - name: Download Linux release assets + - name: Download Linux release assets (x64) uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 with: - name: meshchatx-linux-release-${{ github.ref_name }}-${{ github.run_id }} + name: meshchatx-linux-release-x64-${{ github.ref_name }}-${{ github.run_id }} + path: upload + + - name: Download Linux release assets (arm64) + uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 + with: + name: meshchatx-linux-release-arm64-${{ github.ref_name }}-${{ github.run_id }} path: upload - name: Download Windows dist diff --git a/scripts/build-backend.js b/scripts/build-backend.js index 678fccb..0c4ca37 100755 --- a/scripts/build-backend.js +++ b/scripts/build-backend.js @@ -199,12 +199,50 @@ function shouldRefreshLicenseArtifacts(repoRoot) { return newestInput >= oldestOutput; } +function verifyBinaryArchitecture(buildDir, expectedArch, targetName) { + const binaryPath = path.join(buildDir, targetName); + if (!fs.existsSync(binaryPath)) { + console.warn(`Binary not found at ${binaryPath}, skipping architecture verification.`); + return true; + } + + let output = ""; + try { + const result = spawnSync("file", ["--brief", "--no-pad", binaryPath], { + encoding: "utf-8", + shell: false, + }); + if (result.error || result.status !== 0) { + console.warn(`Could not verify binary architecture: ${result.error?.message || `exit ${result.status}`}`); + return true; + } + output = result.stdout.toLowerCase(); + } catch (e) { + console.warn(`Architecture verification failed: ${e.message}`); + return true; + } + + const isArm64 = output.includes("aarch64") || output.includes("arm64"); + const isX64 = output.includes("x86-64") || output.includes("x86_64") || output.includes("amd64"); + + if (expectedArch === "arm64" && !isArm64) { + console.error(`Architecture mismatch: expected arm64 but binary is not arm64 (${output.trim()}).`); + return false; + } + if (expectedArch === "x64" && !isX64) { + console.error(`Architecture mismatch: expected x64 but binary is not x64 (${output.trim()}).`); + return false; + } + return true; +} + try { const platform = process.env.PLATFORM || process.platform; const arch = process.env.ARCH || process.arch; const isWin = platform === "win32" || platform === "win"; const isDarwin = platform === "darwin"; const targetName = isWin ? "ReticulumMeshChatX.exe" : "ReticulumMeshChatX"; + const rosettaX64 = isDarwin && arch === "x64" && process.arch === "arm64"; let platformFolder = "linux"; if (isWin) { @@ -215,6 +253,18 @@ try { const buildDirRelative = `build/exe/${platformFolder}-${arch}`; const buildDir = path.join(__dirname, "..", buildDirRelative); + if (arch !== process.arch && !rosettaX64 && !process.env.PYTHON_CMD) { + console.error( + `Cross-compilation detected (host: ${process.arch}, target: ${arch}).\n` + + `cx_Freeze produces binaries for the architecture of the Python interpreter it runs under.\n` + + `To build the backend for ${arch}, you must either:\n` + + ` - Build natively on ${arch} hardware (or in a VM/container of that architecture).\n` + + ` - Set PYTHON_CMD to a ${arch} Python interpreter (with Poetry dependencies installed).\n` + + ` - Use Docker with QEMU/binfmt support (e.g., docker run --platform ${platformFolder}/${arch}).` + ); + process.exit(1); + } + // Allow overriding the python command const pythonCmd = process.env.PYTHON_CMD || "poetry run python"; @@ -237,7 +287,6 @@ try { let spawnCmd = cmd; let spawnArgs = licensesArgs; - const rosettaX64 = isDarwin && arch === "x64" && process.arch === "arm64"; if (rosettaX64) { spawnCmd = "arch"; spawnArgs = ["-x86_64", cmd, ...licensesArgs]; @@ -278,6 +327,9 @@ try { if (isDarwin) { stripPythonBytecodeArtifacts(buildDir); } + if (!verifyBinaryArchitecture(buildDir, arch, targetName)) { + process.exit(1); + } const manifestPath = path.join(buildDir, "backend-manifest.json"); const skipManifest = process.env.MESHCHATX_SKIP_BACKEND_MANIFEST === "1" || diff --git a/scripts/ci/docker-build-entry.sh b/scripts/ci/docker-build-entry.sh index cbc2d75..320e094 100644 --- a/scripts/ci/docker-build-entry.sh +++ b/scripts/ci/docker-build-entry.sh @@ -26,7 +26,12 @@ if ! command -v node >/dev/null 2>&1; then fi TASK_VER="${TASK_VERSION:-3.46.4}" -curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VER}/task_linux_amd64.tar.gz" \ +_TASK_ARCH="$(uname -m)" +case "$_TASK_ARCH" in + x86_64) _TASK_ARCH="amd64" ;; + aarch64) _TASK_ARCH="arm64" ;; +esac +curl -fsSL "https://github.com/go-task/task/releases/download/v${TASK_VER}/task_linux_${_TASK_ARCH}.tar.gz" \ | tar xz -C /usr/local/bin task corepack enable diff --git a/scripts/ci/github-apt-linux-packaging.sh b/scripts/ci/github-apt-linux-packaging.sh index 317e640..e022aa3 100644 --- a/scripts/ci/github-apt-linux-packaging.sh +++ b/scripts/ci/github-apt-linux-packaging.sh @@ -5,16 +5,14 @@ set -euo pipefail # shellcheck source=scripts/ci/priv.sh . "$(dirname "$0")/priv.sh" -run_priv dpkg --add-architecture i386 || true +_HOST_ARCH="$(uname -m)" +if [ "$_HOST_ARCH" = "x86_64" ]; then + run_priv dpkg --add-architecture i386 || true +fi run_priv apt-get update -y -run_priv apt-get install -y --no-install-recommends \ - patchelf \ - libopusfile0 \ - espeak-ng \ - zip \ - rpm \ - elfutils \ - fakeroot \ - file \ - libc6:i386 \ - libstdc++6:i386 + +_PKGS="patchelf libopusfile0 espeak-ng zip rpm elfutils fakeroot file" +if [ "$_HOST_ARCH" = "x86_64" ]; then + _PKGS="$_PKGS libc6:i386 libstdc++6:i386" +fi +run_priv apt-get install -y --no-install-recommends $_PKGS diff --git a/scripts/ci/github-build-linux-release-assets.sh b/scripts/ci/github-build-linux-release-assets.sh index beb8cc9..33ed490 100644 --- a/scripts/ci/github-build-linux-release-assets.sh +++ b/scripts/ci/github-build-linux-release-assets.sh @@ -12,23 +12,38 @@ cd "$ROOT" mkdir -p release-assets +HOST_ARCH="$(uname -m)" +case "$HOST_ARCH" in + x86_64) NATIVE_ARCH="x64" ;; + aarch64|arm64) NATIVE_ARCH="arm64" ;; + *) NATIVE_ARCH="$HOST_ARCH" ;; +esac + if [ "${SKIP_WHEEL:-0}" != 1 ]; then - echo "Building Python wheel..." - task build:wheel + if [ "$NATIVE_ARCH" = "x64" ]; then + echo "Building Python wheel..." + task build:wheel + else + echo "Skipping wheel on $NATIVE_ARCH runner (pure-Python wheel built on x64)." + fi else echo "Skipping wheel (SKIP_WHEEL=1)." fi if [ "${SKIP_ELECTRON:-0}" != 1 ]; then - echo "Electron linux x64..." - pnpm run dist:linux-x64 + if [ "$NATIVE_ARCH" = "x64" ]; then + echo "Electron linux x64..." + pnpm run dist:linux-x64 + elif [ "$NATIVE_ARCH" = "arm64" ]; then + echo "Electron linux arm64..." + pnpm run dist:linux-arm64 + fi - echo "Electron linux arm64..." - pnpm run dist:linux-arm64 - - echo "RPM (best-effort)..." - if ! task dist:fe:rpm; then - echo "RPM build failed or skipped; continuing." >&2 + if [ "$NATIVE_ARCH" = "x64" ]; then + echo "RPM (best-effort)..." + if ! task dist:fe:rpm; then + echo "RPM build failed or skipped; continuing." >&2 + fi fi else echo "Skipping Electron packages (SKIP_ELECTRON=1)."