Repository navigation
chore: re-baseline the duplicate-window census for spec 0089 #433
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # | |
| # Serial Studio | |
| # https://serial-studio.com/ | |
| # | |
| # Copyright (C) 2020–2025 Alex Spataru | |
| # | |
| # This file is dual-licensed: | |
| # | |
| # - Under the GNU GPLv3 (or later) for builds that exclude Pro modules. | |
| # - Under the Serial Studio Commercial License for builds that include | |
| # any Pro functionality. | |
| # | |
| # You must comply with the terms of one of these licenses, depending | |
| # on your use case. | |
| # | |
| # For GPL terms, see <https://www.gnu.org/licenses/gpl-3.0.html> | |
| # For commercial terms, see LICENSES/LicenseRef-SerialStudio-Commercial.txt. | |
| # | |
| # SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-SerialStudio-Commercial | |
| # | |
| #--------------------------------------------------------------------------------------------------- | |
| # Workflow configuration | |
| #--------------------------------------------------------------------------------------------------- | |
| name: CI | |
| on: | |
| push: | |
| paths-ignore: | |
| - '**.md' | |
| pull_request: | |
| paths-ignore: | |
| - '**.md' | |
| #--------------------------------------------------------------------------------------------------- | |
| # Workflow environment | |
| #--------------------------------------------------------------------------------------------------- | |
| env: | |
| # | |
| # Application information | |
| # | |
| VERSION: "4.1.0" | |
| PUBLISHER: "Alex Spataru" | |
| UNIXNAME: "serial-studio-pro" | |
| EXECUTABLE: "Serial-Studio-Pro" | |
| APPLICATION: "Serial Studio Pro" | |
| DESCRIPTION: "Multi-purpose serial data visualization & processing program" | |
| # | |
| # Microsoft Store (MSIX) package identity | |
| # | |
| MSIX_IDENTITY_NAME: "AlexSpataru.SerialStudioPro" | |
| MSIX_PUBLISHER: "CN=05B4181B-4085-4067-8FAF-BD59DCB9A376" | |
| MSIX_PUBLISHER_DISPLAY: "Alex Spataru" | |
| # | |
| # QML location (for windeployqt/macdeployqt) | |
| # | |
| QML_DIR: "../../app/qml" | |
| # | |
| # Qt version selection | |
| # | |
| QT_VERSION_LINUX: 6.11.2 | |
| QT_VERSION_MACOS: 6.11.2 | |
| QT_VERSION_WINDOWS: 6.11.2 | |
| QTFRAMEWORK_BYPASS_LICENSE_CHECK: "true" | |
| # | |
| # gRPC selection | |
| # | |
| GRPC_VERSION: 1.78.1 | |
| GRPC_REPO: alex-spataru/gRPC-Builds | |
| # | |
| # Unity build batch size (sources per unity TU). Single source of truth for | |
| # every configure site: both PGO stages must see the identical value or the | |
| # unity TU layout diverges and profile data is silently dropped. | |
| # | |
| # Sized against the largest unity target and the narrowest runner: core/Ui has | |
| # 213 sources, and ninja runs at nproc + 2, so 5 jobs in flight on the 3-vCPU | |
| # macOS arm64 box. At 32 that target is 7 TUs, two waves with the second one | |
| # mostly idle; at 48 it is 5, a single wave, and no smaller target loses a | |
| # wave. Going past 48 only fattens the TUs without dropping a wave, and peak | |
| # compiler RSS times 5 has to stay inside the runner's 7 GB. | |
| # | |
| # Note: setting batch size to 0 results in a single TU with all sources | |
| # | |
| SS_UNITY_BATCH: 48 | |
| # | |
| # Excluded AppImage libraries | |
| # | |
| LINUXDEPLOY_EXCLUDED_LIBRARIES: "libmysqlclient.so*;libqsqlmimer.so;libqsqlmysql.so;libqsqlodbc.so;libqsqlpsql.so;libqsqloci.so;libqsqldb2.so;libqsqlibase.so" | |
| #--------------------------------------------------------------------------------------------------- | |
| # Workflow jobs | |
| #--------------------------------------------------------------------------------------------------- | |
| jobs: | |
| #--------------------------------------------------------------------------------------------------- | |
| # GNU/Linux build (x86_64) | |
| #--------------------------------------------------------------------------------------------------- | |
| build-linux: | |
| runs-on: ubuntu-24.04 | |
| name: '🐧 Linux Build (x86_64)' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # This repo has no submodules; 'submodules: recursive' is a no-op kept for future deps. | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| submodules: 'recursive' | |
| # | |
| # apt payload for a Qt 6 desktop build: the xcb/xkb platform stack, GStreamer for | |
| # QtMultimedia, and the packaging tools (rpm, fakeroot, python3-venv) linuxdeploy shells out to | |
| # | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-desktop-deps | |
| # | |
| # gRPC arrives as a prebuilt tarball from GRPC_REPO; building it from source would cost more | |
| # than the rest of the job put together. The retry policy lives in the action, which the | |
| # macOS legs share | |
| # | |
| - name: '⚙️ Download prebuilt gRPC' | |
| uses: ./.github/actions/download-grpc | |
| with: | |
| version: ${{env.GRPC_VERSION}} | |
| repo: ${{env.GRPC_REPO}} | |
| asset: grpc-${{env.GRPC_VERSION}}-linux-x86_64.tar.gz | |
| prefix-dir: ${{github.workspace}}/grpc-prefix | |
| workspace: ${{github.workspace}} | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| # | |
| # Cache restore, install-on-miss, cache save and the PATH / CMAKE_PREFIX_PATH export. This | |
| # job owns the save half of the entry the restore-only legs read | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: gcc_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-x64 | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| # | |
| # get-cmake supplies a current CMake independently of whatever the runner image ships | |
| # | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # Unit tests live inside the build job so the Qt cache has one consumer per arch; | |
| # a parallel unit job racing this restore could get denied or evict the entry | |
| # | |
| - name: '🚧 Configure unit-test tier (Pro)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -G Ninja -B build/unit-ci \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DSS_BUILD_TESTS=ON \ | |
| -DBUILD_GPL3=OFF \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON | |
| # | |
| # Lean Debug tier: no gRPC, no WebEngine, no mimalloc, system zlib and expat; the application | |
| # target never builds here. Pro, because this job is where the shipped Pro binary comes | |
| # from: a GPL3 unit tier never registers the suites gated on the commercial sources, and | |
| # never compiles the #ifdef BUILD_COMMERCIAL branches of the TUs a suite links, so both | |
| # were proved by the sanitizer legs alone, the slowest jobs in the run and the last to | |
| # report. BUILD_COMMERCIAL validates the license at configure time, hence the credentials | |
| # above. | |
| # | |
| - name: '🚧 Build unit-test tier' | |
| run: cmake --build build/unit-ci --target ss_unit_tests | |
| # | |
| # Runs on both arches: DSPSimd.h picks its SIMD lane from the target architecture | |
| # | |
| - name: '🧪 Run ctest' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --test-dir build/unit-ci --output-on-failure | |
| # | |
| # Findings G12 / M7: the repo had no QML tier, so unqualified property access and binding | |
| # loops were caught only by somebody noticing a runtime warning. The gate lives here rather | |
| # than in `lint` because qmllint needs Qt and a configured tree, and this job has both. | |
| # Findings are normalized without line numbers (they churn on every edit above the warning) | |
| # and compared against app/qml/qmllint-baseline.json | |
| # | |
| - name: '🔬 QML lint' | |
| run: | | |
| set -uo pipefail | |
| cmake --build build/unit-ci --target ss_qmllint > qmllint.txt 2>&1 || true | |
| python3 - <<'PY' | |
| import json, os, re, sys | |
| BASELINE = "app/qml/qmllint-baseline.json" | |
| text = open("qmllint.txt", encoding="utf-8", errors="replace").read() | |
| if "No rule to make target" in text or "unknown target" in text: | |
| sys.exit("::error::the ss_qmllint target does not exist; is Qt's qmllint installed?") | |
| pattern = re.compile(r"^(?:Warning|Error|Info):\s+(\S+?):\d+:\d+:\s+(.*)$") | |
| found = set() | |
| for line in text.split("\n"): | |
| match = pattern.match(line.strip()) | |
| if not match: | |
| continue | |
| # qmllint prints absolute paths; key on the repo-relative part so the baseline | |
| # does not depend on where the workspace happened to be checked out. | |
| path = match.group(1).replace(os.sep, "/") | |
| marker = path.find("app/qml/") | |
| path = path[marker:] if marker >= 0 else os.path.relpath(path, os.getcwd()) | |
| found.add(f"{path} :: {match.group(2).strip()}") | |
| baseline = json.load(open(BASELINE, encoding="utf-8")) | |
| accepted = set(baseline.get("accepted", [])) | |
| new = sorted(found - accepted) | |
| print(f"{len(found)} qmllint finding(s), {len(accepted)} accepted, {len(new)} new") | |
| for entry in new: | |
| print(f" {entry}") | |
| if not baseline.get("seeded"): | |
| print("::warning::qmllint baseline is unseeded; paste the list above into " | |
| f"{BASELINE} and set seeded=true to arm the gate") | |
| sys.exit(0) | |
| if new: | |
| sys.exit(f"::error::{len(new)} qmllint finding(s) not in {BASELINE}") | |
| gone = sorted(accepted - found) | |
| if gone: | |
| print("::warning::baseline entries no longer reported; prune them: " | |
| + ", ".join(gone)) | |
| PY | |
| - name: '📤 Upload artifact: qmllint output' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: qmllint-Linux-x64 | |
| path: qmllint.txt | |
| if-no-files-found: ignore | |
| # | |
| # Stage 1 of the two-stage PGO flow: an instrumented Release build whose only job is to emit | |
| # profiles for the training runs below | |
| # | |
| - name: '🚧 Configure with CMake (PGO generate)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=GENERATE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # Finding L1: CMake composes the command line as <DEFINES> <INCLUDES> <FLAGS>, so a FORTIFY | |
| # level passed as a compile DEFINITION lands ahead of every -U_FORTIFY_SOURCE in FLAGS and is | |
| # cancelled by it -- which is how hardened Linux packages shipped with no FORTIFY at all. | |
| # Reading the real compile line is the only way to know which token wins, so this step derives | |
| # the level Hardening.cmake should have chosen and asserts the compile line agrees | |
| # | |
| - name: '🛡 Assert FORTIFY survives the compile line' | |
| run: | | |
| set -euo pipefail | |
| GCC_MAJOR=$(cc -dumpversion | cut -d. -f1) | |
| GLIBC=$(getconf GNU_LIBC_VERSION | awk '{print $2}') | |
| GLIBC_MAJOR=${GLIBC%%.*} | |
| GLIBC_MINOR=${GLIBC#*.} | |
| GLIBC_MINOR=${GLIBC_MINOR%%.*} | |
| EXPECT=2 | |
| if [ "$GCC_MAJOR" -ge 12 ] && [ "$GLIBC_MAJOR" -ge 2 ] && [ "$GLIBC_MINOR" -ge 34 ]; then | |
| EXPECT=3 | |
| fi | |
| echo "cc major $GCC_MAJOR, glibc $GLIBC -> expecting -D_FORTIFY_SOURCE=$EXPECT" | |
| LAST=$(python3 - <<'PY' | |
| import json, sys | |
| db = json.load(open("build/compile_commands.json")) | |
| rows = [e for e in db | |
| if "app/CMakeFiles" in (e.get("output") or "") + " " + (e.get("file") or "")] | |
| if not rows: | |
| sys.exit("no compile_commands.json entry for the application target") | |
| seen = set() | |
| for e in rows: | |
| cmd = e.get("command") or " ".join(e.get("arguments", [])) | |
| tokens = [t for t in cmd.split() if "_FORTIFY_SOURCE" in t] | |
| if not tokens: | |
| sys.exit("no FORTIFY token on an application compile line") | |
| seen.add(tokens[-1]) | |
| if len(seen) != 1: | |
| sys.exit("application TUs disagree on the FORTIFY level: %s" % sorted(seen)) | |
| print(seen.pop()) | |
| PY | |
| ) | |
| echo "last FORTIFY token on the application compile line: $LAST" | |
| if [ "$LAST" != "-D_FORTIFY_SOURCE=$EXPECT" ]; then | |
| echo "::error::FORTIFY cancelled or wrong level: got '$LAST', expected -D_FORTIFY_SOURCE=$EXPECT" | |
| exit 1 | |
| fi | |
| # | |
| # Builds the instrumented binary the two training runs profile | |
| # | |
| - name: '🚧 Build application (instrumented)' | |
| run: cmake --build build --config Release | |
| # | |
| # Pro widgets are license-gated, so an unactivated training run profiles the GPL subset | |
| # and the shipped binary is optimized for code no Pro user runs. A failed activation is | |
| # therefore a failed build, not a warning. The two training loads follow it: --min-fps 1 | |
| # turns the benchmark into a profile generator (the real gate runs later, against the | |
| # optimized binary), and the big_db_test load exercises the project and dashboard paths | |
| # the hotpath benchmark never reaches | |
| # | |
| - name: '🏋️ PGO training runs' | |
| uses: ./.github/actions/pgo-train-linux | |
| with: | |
| app: ./build/app/${{env.UNIXNAME}} | |
| qt-lib-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/lib | |
| license-key: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| # | |
| # Stage 2: identical flags with PGO_STAGE=USE so the compiler consumes the profiles just | |
| # written | |
| # | |
| - name: '🚧 Reconfigure with CMake (PGO use)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=USE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # The binary that ships, and the one every gate below measures | |
| # | |
| - name: '🚧 Build application (PGO optimized)' | |
| run: cmake --build build --config Release | |
| # | |
| # Findings G12 / M7: instantiates every compiled .qml against stubbed Cpp_* globals and | |
| # fails on a ReferenceError -- the GPL-build-hits-a-Pro-global class of bug. This is the | |
| # commercial leg; the sanitize job runs the same suite on a GPL build | |
| # | |
| - name: '🧪 QML instantiation self-test' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/lib | |
| run: ./build/app/${{env.UNIXNAME}} --headless --selftest-suite qml | |
| # | |
| # The CI throughput gate: nine tiers scaled off --min-fps. A miss fails the build | |
| # | |
| - name: '🚦 Hotpath throughput gate (256 kHz)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/lib | |
| run: | | |
| ./build/app/${{env.UNIXNAME}} --headless --benchmark-hotpath --min-fps 256000 \ | |
| --benchmark-output benchmark.txt | |
| - name: '📤 Upload artifact: benchmark report (Linux x64)' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: benchmark-Linux-x64 | |
| path: | | |
| benchmark.txt | |
| pgo-train.txt | |
| # | |
| # Spec 0084: the line-table symbols objcopy split out of the stripped binary, so a profile or | |
| # crash stack from this exact build can be symbolicated later | |
| # | |
| - name: '📤 Upload artifact: symbols (Linux x64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: symbols-Linux-x64 | |
| path: build/app/${{env.UNIXNAME}}.debug | |
| if-no-files-found: ignore | |
| # | |
| # Replays the big_db_test load against the optimized binary; catches the teardown crashes the | |
| # throughput benchmark never sees | |
| # | |
| - name: '🩺 Big project verification (optimized)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/lib | |
| run: | | |
| bash tests/benchmarks/big_db_test/run_load.sh "./build/app/${{env.UNIXNAME}}" 15 | |
| # | |
| # always(): activations are seat-limited, so a failed job still has to release the seat | |
| # | |
| - name: '🔑 Deactivate Serial Studio license' | |
| if: always() | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/lib | |
| run: | | |
| ./build/app/${{env.UNIXNAME}} --deactivate || true | |
| # | |
| # The Linux packaging chain, shared with the other arch: signing identity, AppDir, the | |
| # glibc-bundled AppImage (spec 0065, the ONLY Linux packaging path), the deb/rpm pair cut | |
| # from that same converted tree, the glibc 2.28 smoke gate that proves the set installs on | |
| # Debian 10 / RHEL 8, and the signatures. Only arch strings and checksums differ | |
| # | |
| - name: '📦 Package for Linux' | |
| uses: ./.github/actions/package-linux | |
| with: | |
| workspace: ${{github.workspace}} | |
| arch-label: x64 | |
| machine-arch: x86_64 | |
| deb-arch: amd64 | |
| config-arch: x86_64 | |
| system-lib-dir: /usr/lib/x86_64-linux-gnu | |
| qt-plugins-path: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64/${{env.QT_VERSION_LINUX}}/gcc_64/plugins | |
| qml-dir: ${{env.QML_DIR}} | |
| sharun-sha256: f35d4f59f2e0b1a5ec12ef126d78197fd4fd14c6f99b4b16dee6a5ddad6baa93 | |
| appimagetool-sha256: ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 | |
| executable: ${{env.EXECUTABLE}} | |
| version: ${{env.VERSION}} | |
| unixname: ${{env.UNIXNAME}} | |
| description: ${{env.DESCRIPTION}} | |
| gpg-private-key: ${{secrets.GPG_PRIVATE_KEY}} | |
| gpg-passphrase: ${{secrets.GPG_PASSPHRASE}} | |
| gpg-key-id: ${{secrets.GPG_KEY_ID}} | |
| # | |
| # Release artifact: the Linux AppImage users download — the glibc-bundled build (spec | |
| # 0065) runs on everything from Debian 10 / RHEL 8 up, so it is the only one shipped | |
| # | |
| - name: '📤 Upload artifact: AppImage' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.AppImage | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.AppImage | |
| # | |
| # Release artifact: Debian package | |
| # | |
| - name: '📤 Upload artifact: .deb (x64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.deb | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.deb | |
| # | |
| # Release artifact: RPM package | |
| # | |
| - name: '📤 Upload artifact: .rpm (x64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.rpm | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.rpm | |
| # | |
| # Detached signatures plus the raw binary and the public key, so a user can verify without | |
| # trusting the release page; ignored when signing was skipped | |
| # | |
| - name: '📤 Upload artifact: signatures + signed binary (x64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64-signed | |
| if-no-files-found: ignore | |
| path: | | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.AppImage.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.deb.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.rpm.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64 | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-x64.asc | |
| signing-public-key.asc | |
| #--------------------------------------------------------------------------------------------------- | |
| # GNU/Linux build (aarch64) | |
| #--------------------------------------------------------------------------------------------------- | |
| build-linux-arm64: | |
| runs-on: ubuntu-24.04-arm | |
| name: '🐧 Linux Build (arm64)' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # This repo has no submodules; 'submodules: recursive' is a no-op kept for future deps. | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| submodules: 'recursive' | |
| # | |
| # apt payload for a Qt 6 desktop build: the xcb/xkb platform stack, GStreamer for | |
| # QtMultimedia, and the packaging tools (rpm, fakeroot, python3-venv) linuxdeploy shells out to | |
| # | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-desktop-deps | |
| # | |
| # gRPC arrives as a prebuilt tarball from GRPC_REPO; building it from source would cost more | |
| # than the rest of the job put together. The retry policy lives in the action, which the | |
| # macOS legs share | |
| # | |
| - name: '⚙️ Download prebuilt gRPC' | |
| uses: ./.github/actions/download-grpc | |
| with: | |
| version: ${{env.GRPC_VERSION}} | |
| repo: ${{env.GRPC_REPO}} | |
| asset: grpc-${{env.GRPC_VERSION}}-linux-aarch64.tar.gz | |
| prefix-dir: ${{github.workspace}}/grpc-prefix | |
| workspace: ${{github.workspace}} | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| # | |
| # Cache restore, install-on-miss, cache save and the PATH / CMAKE_PREFIX_PATH export. This | |
| # job owns the save half of the entry the restore-only legs read | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-arm64 | |
| qt-arch: arm64 | |
| qt-host: gcc_arm64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-arm64 | |
| cache-extra-path: '' | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| # | |
| # get-cmake supplies a current CMake independently of whatever the runner image ships | |
| # | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # Unit tests live inside the build job so the Qt cache has one consumer per arch; | |
| # a parallel unit job racing this restore could get denied or evict the entry | |
| # | |
| - name: '🚧 Configure unit-test tier (Pro)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -G Ninja -B build/unit-ci \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DSS_BUILD_TESTS=ON \ | |
| -DBUILD_GPL3=OFF \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON | |
| # | |
| # Lean Debug tier: no gRPC, no WebEngine, no mimalloc, system zlib and expat; the application | |
| # target never builds here. Pro, because this job is where the shipped Pro binary comes | |
| # from: a GPL3 unit tier never registers the suites gated on the commercial sources, and | |
| # never compiles the #ifdef BUILD_COMMERCIAL branches of the TUs a suite links, so both | |
| # were proved by the sanitizer legs alone, the slowest jobs in the run and the last to | |
| # report. BUILD_COMMERCIAL validates the license at configure time, hence the credentials | |
| # above. | |
| # | |
| - name: '🚧 Build unit-test tier' | |
| run: cmake --build build/unit-ci --target ss_unit_tests | |
| # | |
| # Runs on both arches: DSPSimd.h picks its SIMD lane from the target architecture | |
| # | |
| - name: '🧪 Run ctest' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| run: ctest --test-dir build/unit-ci --output-on-failure | |
| # | |
| # Stage 1 of the two-stage PGO flow: an instrumented Release build whose only job is to emit | |
| # profiles for the training runs below | |
| # | |
| - name: '🚧 Configure with CMake (PGO generate)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=GENERATE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # Builds the instrumented binary the two training runs profile | |
| # | |
| - name: '🚧 Build application (instrumented)' | |
| run: cmake --build build --config Release | |
| # | |
| # Pro widgets are license-gated, so an unactivated training run profiles the GPL subset | |
| # and the shipped binary is optimized for code no Pro user runs. A failed activation is | |
| # therefore a failed build, not a warning. The two training loads follow it: --min-fps 1 | |
| # turns the benchmark into a profile generator (the real gate runs later, against the | |
| # optimized binary), and the big_db_test load exercises the project and dashboard paths | |
| # the hotpath benchmark never reaches | |
| # | |
| - name: '🏋️ PGO training runs' | |
| uses: ./.github/actions/pgo-train-linux | |
| with: | |
| app: ./build/app/${{env.UNIXNAME}} | |
| qt-lib-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-arm64/${{env.QT_VERSION_LINUX}}/gcc_arm64/lib | |
| license-key: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| # | |
| # Stage 2: identical flags with PGO_STAGE=USE so the compiler consumes the profiles just | |
| # written | |
| # | |
| - name: '🚧 Reconfigure with CMake (PGO use)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=USE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # The binary that ships, and the one every gate below measures | |
| # | |
| - name: '🚧 Build application (PGO optimized)' | |
| run: cmake --build build --config Release | |
| # | |
| # The CI throughput gate: nine tiers scaled off --min-fps. A miss fails the build | |
| # | |
| - name: '🚦 Hotpath throughput gate (256 kHz)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-arm64/${{env.QT_VERSION_LINUX}}/gcc_arm64/lib | |
| run: | | |
| ./build/app/${{env.UNIXNAME}} --headless --benchmark-hotpath --min-fps 256000 \ | |
| --benchmark-output benchmark.txt | |
| - name: '📤 Upload artifact: benchmark report (Linux arm64)' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: benchmark-Linux-arm64 | |
| path: | | |
| benchmark.txt | |
| pgo-train.txt | |
| - name: '📤 Upload artifact: symbols (Linux arm64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: symbols-Linux-arm64 | |
| path: build/app/${{env.UNIXNAME}}.debug | |
| if-no-files-found: ignore | |
| # | |
| # always(): activations are seat-limited, so a failed job still has to release the seat | |
| # | |
| - name: '🔑 Deactivate Serial Studio license' | |
| if: always() | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| LD_LIBRARY_PATH: >- | |
| ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-arm64/${{env.QT_VERSION_LINUX}}/gcc_arm64/lib | |
| run: | | |
| ./build/app/${{env.UNIXNAME}} --deactivate || true | |
| # | |
| # The Linux packaging chain, shared with the other arch: signing identity, AppDir, the | |
| # glibc-bundled AppImage (spec 0065, the ONLY Linux packaging path), the deb/rpm pair cut | |
| # from that same converted tree, the glibc 2.28 smoke gate that proves the set installs on | |
| # Debian 10 / RHEL 8, and the signatures. Only arch strings and checksums differ | |
| # | |
| - name: '📦 Package for Linux' | |
| uses: ./.github/actions/package-linux | |
| with: | |
| workspace: ${{github.workspace}} | |
| arch-label: arm64 | |
| machine-arch: aarch64 | |
| deb-arch: arm64 | |
| config-arch: arm64 | |
| system-lib-dir: /usr/lib/aarch64-linux-gnu | |
| qt-plugins-path: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-arm64/${{env.QT_VERSION_LINUX}}/gcc_arm64/plugins | |
| qml-dir: ${{env.QML_DIR}} | |
| sharun-sha256: bf3b8cc04e3025ef9dcd2718ee4728ccc68c941dadeeac7fdf778fc2c99d8b31 | |
| appimagetool-sha256: f0837e7448a0c1e4e650a93bb3e85802546e60654ef287576f46c71c126a9158 | |
| executable: ${{env.EXECUTABLE}} | |
| version: ${{env.VERSION}} | |
| unixname: ${{env.UNIXNAME}} | |
| description: ${{env.DESCRIPTION}} | |
| gpg-private-key: ${{secrets.GPG_PRIVATE_KEY}} | |
| gpg-passphrase: ${{secrets.GPG_PASSPHRASE}} | |
| gpg-key-id: ${{secrets.GPG_KEY_ID}} | |
| # | |
| # Release artifact: the Linux AppImage users download — the glibc-bundled build (spec | |
| # 0065) runs on everything from Debian 10 / RHEL 8 up, so it is the only one shipped | |
| # | |
| - name: '📤 Upload artifact: AppImage' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.AppImage | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.AppImage | |
| # | |
| # Release artifact: Debian package | |
| # | |
| - name: '📤 Upload artifact: .deb (arm64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.deb | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.deb | |
| # | |
| # Release artifact: RPM package | |
| # | |
| - name: '📤 Upload artifact: .rpm (arm64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.rpm | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.rpm | |
| # | |
| # Detached signatures plus the raw binary and the public key, so a user can verify without | |
| # trusting the release page; ignored when signing was skipped | |
| # | |
| - name: '📤 Upload artifact: signatures + signed binary (arm64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64-signed | |
| if-no-files-found: ignore | |
| path: | | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.AppImage.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.deb.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.rpm.asc | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64 | |
| ${{env.EXECUTABLE}}-${{env.VERSION}}-Linux-arm64.asc | |
| signing-public-key.asc | |
| #--------------------------------------------------------------------------------------------------- | |
| # macOS arm64 slice | |
| #--------------------------------------------------------------------------------------------------- | |
| build-macos-arm64: | |
| runs-on: macos-latest | |
| name: '🍎 macOS Build (arm64)' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # This repo has no submodules; 'submodules: recursive' is a no-op kept for future deps. | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| submodules: 'recursive' | |
| # | |
| # One universal tarball feeds both macOS slices, so the arm64 and x86_64 jobs stay in sync. | |
| # Retried for the same reason as the Linux legs; see the x86_64 job. | |
| # | |
| - name: '⚙️ Download prebuilt gRPC (universal)' | |
| uses: ./.github/actions/download-grpc | |
| with: | |
| version: ${{env.GRPC_VERSION}} | |
| repo: ${{env.GRPC_REPO}} | |
| asset: grpc-${{env.GRPC_VERSION}}-macos-universal.tar.gz | |
| prefix-dir: ${{github.workspace}}/grpc-prefix | |
| workspace: ${{github.workspace}} | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| # | |
| # Restore-only half of the cache pair; the save step below is gated on a miss so a hit never | |
| # rewrites the entry | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_MACOS}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: macos | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_MACOS}}-arm64 | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| # | |
| # get-cmake supplies a current CMake independently of whatever the runner image ships | |
| # | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # Stage 1 of the two-stage PGO flow, pinned to arm64: this job builds only the native slice | |
| # | |
| - name: '🚧 Configure with CMake (PGO generate, arm64)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=GENERATE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \ | |
| -DCMAKE_OSX_ARCHITECTURES="arm64" \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # Builds the instrumented arm64 binary the training runs profile | |
| # | |
| - name: '🚧 Build application (instrumented, arm64)' | |
| run: cmake --build build --config Release | |
| # | |
| # Pro widgets are license-gated, so an unactivated training run profiles the GPL subset and | |
| # the shipped binary is optimized for code no Pro user runs. A failed activation is therefore | |
| # a failed build, not a warning | |
| # | |
| - name: '🔑 Activate Serial Studio license (Pro hotpath training)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| ./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}} --activate "$SERIAL_STUDIO_LICENSE_KEY" | |
| # | |
| # --min-fps 1 turns the benchmark into a profile generator: the real gate runs later, against | |
| # the optimized binary | |
| # | |
| - name: '🏋️ PGO training run (hotpath)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| ./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}} --headless --benchmark-hotpath --min-fps 1 \ | |
| --benchmark-output pgo-train.txt | |
| # | |
| # Second profile: the big_db_test load exercises the project and dashboard paths that the | |
| # hotpath benchmark never reaches | |
| # | |
| - name: '🏋️ PGO training run (big project)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| bash tests/benchmarks/big_db_test/run_load.sh "./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}}" 25 | |
| # | |
| # Stage 2: LLVM_PROFDATA comes from xcrun so the merge tool matches the toolchain that wrote | |
| # the raw profiles | |
| # | |
| - name: '🚧 Reconfigure with CMake (PGO use, arm64)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DENABLE_PGO=ON \ | |
| -DPGO_STAGE=USE \ | |
| -DSS_ALLOC_STATS=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DLLVM_PROFDATA=$(xcrun -f llvm-profdata) \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \ | |
| -DCMAKE_OSX_ARCHITECTURES="arm64" \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # The arm64 half of the universal binary the merge job assembles | |
| # | |
| - name: '🚧 Build application (PGO optimized, arm64)' | |
| run: cmake --build build --config Release | |
| # | |
| # The CI throughput gate: nine tiers scaled off --min-fps. A miss fails the build | |
| # | |
| - name: '🚦 Hotpath throughput gate (256 kHz)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| ./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}} \ | |
| --headless --benchmark-hotpath --min-fps 256000 --benchmark-output benchmark.txt | |
| - name: '📤 Upload artifact: benchmark report (macOS arm64)' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: benchmark-macOS-arm64 | |
| path: | | |
| benchmark.txt | |
| pgo-train.txt | |
| - name: '📤 Upload artifact: symbols (macOS arm64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: symbols-macOS-arm64 | |
| path: build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}}.dSYM | |
| if-no-files-found: ignore | |
| # | |
| # Replays the big_db_test load against the optimized binary; catches the teardown crashes the | |
| # throughput benchmark never sees | |
| # | |
| - name: '🩺 Big project verification (optimized)' | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| bash tests/benchmarks/big_db_test/run_load.sh "./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}}" 15 | |
| # | |
| # always(): activations are seat-limited, so a failed job still has to release the seat | |
| # | |
| - name: '🔑 Deactivate Serial Studio license' | |
| if: always() | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| DYLD_FRAMEWORK_PATH: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/lib | |
| run: | | |
| ./build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}} --deactivate || true | |
| # | |
| # cpack builds the DMG from the deployed bundle | |
| # | |
| - name: '📦 Package application' | |
| run: | | |
| cd build | |
| cpack --verbose | |
| # | |
| # The merge job needs a bundle, not a disk image, so the .app is pulled back out of the DMG | |
| # | |
| - name: '💿 Mount DMG and copy application' | |
| run: | | |
| VOLUME=$(yes | hdiutil attach ./build/*.dmg -nobrowse | grep "Volumes" | awk '{print $3}') | |
| cp -a "$VOLUME/${{env.EXECUTABLE}}.app" "${{env.APPLICATION}}.app" | |
| hdiutil detach "$VOLUME" | |
| # | |
| # The deploy tools bundle only the native platform plugin; the integration tests run with | |
| # QT_QPA_PLATFORM=offscreen | |
| # | |
| - name: '🧩 Bundle offscreen platform plugin (headless CI tests)' | |
| run: | | |
| QT_PLUGINS_PATH="${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64/${{env.QT_VERSION_MACOS}}/macos/plugins" | |
| DEST="${{env.APPLICATION}}.app/Contents/PlugIns/platforms" | |
| mkdir -p "$DEST" | |
| if [ -f "$QT_PLUGINS_PATH/platforms/libqoffscreen.dylib" ]; then | |
| cp "$QT_PLUGINS_PATH/platforms/libqoffscreen.dylib" "$DEST/" | |
| else | |
| echo "Warning: offscreen platform plugin not found at $QT_PLUGINS_PATH/platforms" | |
| fi | |
| # | |
| # COPYFILE_DISABLE=1 keeps AppleDouble ._ files out of the tarball, so the bundle survives the | |
| # round trip through the artifact store intact | |
| # | |
| - name: '🗜 Archive deployed .app' | |
| run: | | |
| COPYFILE_DISABLE=1 tar -czf macos-arm64-app.tar.gz "${{env.APPLICATION}}.app" | |
| # | |
| # retention-days: 1 marks this as an intermediate for the universal merge job, not a release | |
| # artifact | |
| # | |
| - name: '📤 Upload artifact: arm64 deployed .app' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: macos-arm64-app | |
| path: macos-arm64-app.tar.gz | |
| retention-days: 1 | |
| #--------------------------------------------------------------------------------------------------- | |
| # macOS x86_64 | |
| #--------------------------------------------------------------------------------------------------- | |
| build-macos-intel: | |
| runs-on: macos-latest | |
| name: '🍎 macOS Build (x86_64)' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # This repo has no submodules; 'submodules: recursive' is a no-op kept for future deps. | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| submodules: 'recursive' | |
| # | |
| # One universal tarball feeds both macOS slices, so the arm64 and x86_64 jobs stay in sync. | |
| # Retried for the same reason as the Linux legs; see the x86_64 job. | |
| # | |
| - name: '⚙️ Download prebuilt gRPC (universal)' | |
| uses: ./.github/actions/download-grpc | |
| with: | |
| version: ${{env.GRPC_VERSION}} | |
| repo: ${{env.GRPC_REPO}} | |
| asset: grpc-${{env.GRPC_VERSION}}-macos-universal.tar.gz | |
| prefix-dir: ${{github.workspace}}/grpc-prefix | |
| workspace: ${{github.workspace}} | |
| github-token: ${{secrets.GITHUB_TOKEN}} | |
| # | |
| # Restore-only half of the cache pair; the save step below is gated on a miss so a hit never | |
| # rewrites the entry | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_MACOS}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_MACOS}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: macos | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_MACOS}}-x86_64 | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| # | |
| # get-cmake supplies a current CMake independently of whatever the runner image ships | |
| # | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # No PGO here: this job only supplies the x86_64 half of the universal binary, and the arm64 | |
| # runner can only train a profile through emulation, which would describe the emulator | |
| # | |
| - name: '🚧 Configure with CMake (production, x86_64)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja \ | |
| -DSS_BUILD_COMMIT=${{ github.sha }} \ | |
| -DPRODUCTION_OPTIMIZATION=ON \ | |
| -DENABLE_HARDENING=ON \ | |
| -DENABLE_GRPC=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 \ | |
| -DCMAKE_OSX_ARCHITECTURES="x86_64" \ | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix;${CMAKE_PREFIX_PATH}" \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DBUILD_GPL3=OFF | |
| # | |
| # Builds the x86_64 slice | |
| # | |
| - name: '🚧 Build application (x86_64)' | |
| run: cmake --build build --config Release | |
| # | |
| # Lifts the slice out of the bundle; the upload itself is the next step | |
| # | |
| - name: '📤 Upload artifact: x86_64 executable' | |
| run: cp "build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}}" "${{env.EXECUTABLE}}-x86_64" | |
| # | |
| # retention-days: 1, an intermediate consumed by the universal merge job | |
| # | |
| - name: '📤 Upload artifact: x86_64 slice' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: macos-intel-exe | |
| path: ${{env.EXECUTABLE}}-x86_64 | |
| retention-days: 1 | |
| - name: '📤 Upload artifact: symbols (macOS x86_64)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: symbols-macOS-x86_64 | |
| path: build/app/${{env.EXECUTABLE}}.app/Contents/MacOS/${{env.EXECUTABLE}}.dSYM | |
| #--------------------------------------------------------------------------------------------------- | |
| # macOS universal merge + sign + notarize. | |
| #--------------------------------------------------------------------------------------------------- | |
| build-macos: | |
| runs-on: macos-latest | |
| name: '🍎 macOS Build (universal)' | |
| needs: | |
| - build-macos-arm64 | |
| - build-macos-intel | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # No submodules: this job merges, signs and notarizes prebuilt artifacts and never compiles | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| # | |
| # create-dmg is an npm package | |
| # | |
| - name: '⚙️ Install Node' | |
| uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 | |
| with: | |
| node-version: 22 | |
| # | |
| # The deployed arm64 bundle produced by build-macos-arm64 | |
| # | |
| - name: '📥 Download arm64 deployed .app' | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | |
| with: | |
| name: macos-arm64-app | |
| path: . | |
| # | |
| # The bare x86_64 executable produced by build-macos-intel | |
| # | |
| - name: '📥 Download x86_64 slice' | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | |
| with: | |
| name: macos-intel-exe | |
| path: ./intel-slice | |
| # | |
| # Unpacks the bundle the lipo step edits in place | |
| # | |
| - name: '🗜 Unpack deployed .app' | |
| run: tar -xzf macos-arm64-app.tar.gz | |
| # | |
| # lipo fuses the two slices into the bundle's existing executable; the greps assert both | |
| # architectures survived the merge | |
| # | |
| - name: '🔗 Lipo arm64 + x86_64 into a universal binary' | |
| run: | | |
| APP_EXE="${{env.APPLICATION}}.app/Contents/MacOS/${{env.EXECUTABLE}}" | |
| X64_EXE="intel-slice/${{env.EXECUTABLE}}-x86_64" | |
| lipo -create "$APP_EXE" "$X64_EXE" -output "$APP_EXE.universal" | |
| mv "$APP_EXE.universal" "$APP_EXE" | |
| chmod +x "$APP_EXE" | |
| lipo -info "$APP_EXE" | |
| lipo -info "$APP_EXE" | grep -q "arm64" && lipo -info "$APP_EXE" | grep -q "x86_64" | |
| # | |
| # Every dynamically-linked dependency inside the bundle needs the same treatment as the | |
| # executable: without both slices the DMG would advertise itself as universal while that | |
| # library failed to load on Intel. dlopen failing at runtime is the only symptom: the build, | |
| # the signature and the notarisation all pass. Qt's own bundled dylibs (the FFmpeg set behind | |
| # Qt Multimedia) ship fat from the online installer, so a library already carrying both | |
| # slices only needs the assertion; the merge branch exists for a future arm64-only dependency | |
| # and expects the intel job to publish a matching slice under intel-frameworks/. | |
| # | |
| - name: '🔗 Lipo bundled dynamic libraries' | |
| run: | | |
| FW="${{env.APPLICATION}}.app/Contents/Frameworks" | |
| if [ ! -d "$FW" ]; then | |
| echo "No Frameworks directory; nothing to merge." | |
| exit 0 | |
| fi | |
| shopt -s nullglob | |
| for LIB in "$FW"/*.dylib; do | |
| BASE="$(basename "$LIB")" | |
| if lipo -info "$LIB" | grep -q "x86_64" && lipo -info "$LIB" | grep -q "arm64"; then | |
| echo "$BASE is already universal; nothing to merge." | |
| continue | |
| fi | |
| SLICE="intel-frameworks/$BASE" | |
| if [ ! -f "$SLICE" ]; then | |
| echo "::error::$BASE has no x86_64 slice; the universal bundle would ship it arm64-only." | |
| exit 1 | |
| fi | |
| lipo -create "$LIB" "$SLICE" -output "$LIB.universal" | |
| mv "$LIB.universal" "$LIB" | |
| lipo -info "$LIB" | |
| lipo -info "$LIB" | grep -q "arm64" | |
| lipo -info "$LIB" | grep -q "x86_64" | |
| done | |
| # | |
| # The stamp goes in before codesign: any edit to the bundle afterwards invalidates the | |
| # signature | |
| # | |
| - name: '📝 Stamp packaging metadata (before codesign)' | |
| run: | | |
| printf '{"packageType":"dmg","arch":"universal"}\n' \ | |
| > "${{env.APPLICATION}}.app/Contents/Resources/ss-config.json" | |
| # | |
| # Loads the Developer ID identity from the p12 secret into the runner keychain | |
| # | |
| - name: '🪪 Import Certificates' | |
| uses: apple-actions/import-codesign-certs@fe74d46e82474f87e1ba79832ad28a4013d0e33a # v6.1.0 | |
| with: | |
| p12-file-base64: ${{secrets.APPLE_CERTIFICATES_P12}} | |
| p12-password: ${{secrets.APPLE_CERTIFICATES_P12_PASSWORD}} | |
| # | |
| # --options runtime is what notarization requires; the entitlements are the ones shipped in | |
| # app/deploy/macOS | |
| # | |
| - name: '✍🏻 Sign Application with Entitlements' | |
| env: | |
| APPLE_APPID_TEAM_ID: ${{secrets.APPLE_APPID_TEAM_ID}} | |
| run: | | |
| codesign --force --deep --options runtime \ | |
| --entitlements "${GITHUB_WORKSPACE}/app/deploy/macOS/Serial-Studio.entitlements" \ | |
| --sign "$APPLE_APPID_TEAM_ID" \ | |
| "${{env.APPLICATION}}.app" | |
| # | |
| # create-dmg builds the release image; LICENSE.md is removed first so it is not folded into | |
| # the DMG layout | |
| # | |
| - name: '💽 Create nice DMG' | |
| run: | | |
| npm install --global create-dmg | |
| rm LICENSE.md | |
| create-dmg "${{env.APPLICATION}}.app" --dmg-title="${{env.APPLICATION}}" | |
| mv "${{env.APPLICATION}} ${{env.VERSION}}.dmg" "${{env.EXECUTABLE}}-${{env.VERSION}}-macOS.dmg" | |
| # | |
| # --wait blocks on Apple's verdict, so a rejected build fails the job here | |
| # | |
| - name: '📋 Notarize' | |
| shell: bash | |
| env: | |
| PRODUCT_PATH: ${{env.EXECUTABLE}}-${{env.VERSION}}-macOS.dmg | |
| APPLE_ID: ${{secrets.APPLE_NOTARIZATION_USERNAME}} | |
| APP_PASSWORD: ${{secrets.APPLE_NOTARIZATION_PASSWORD}} | |
| TEAM_ID: ${{secrets.APPLE_NOTARIZATION_TEAMID}} | |
| run: | | |
| echo "Submitting $PRODUCT_PATH for notarization..." | |
| xcrun notarytool submit "$PRODUCT_PATH" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APP_PASSWORD" \ | |
| --team-id "$TEAM_ID" \ | |
| --wait \ | |
| --output-format json | |
| # | |
| # Stapling the ticket lets Gatekeeper clear the DMG on a machine that is offline | |
| # | |
| - name: '📌 Staple' | |
| shell: bash | |
| run: | | |
| PRODUCT="${{env.EXECUTABLE}}-${{env.VERSION}}-macOS.dmg" | |
| echo "Stapling $PRODUCT..." | |
| xcrun stapler staple "$PRODUCT" | |
| # | |
| # Release artifact: the signed, notarized, stapled universal DMG | |
| # | |
| - name: '📤 Upload artifact: DMG' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-macOS.dmg | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-macOS.dmg | |
| #--------------------------------------------------------------------------------------------------- | |
| # Windows build (MSVC 2022 x86_64) | |
| #--------------------------------------------------------------------------------------------------- | |
| build-windows: | |
| runs-on: windows-latest | |
| name: '🧊 Windows Build (x86_64)' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # core.autocrlf=input keeps the checkout LF-only, which is what code-verify and clang-format | |
| # assume | |
| # | |
| - run: git config --global core.autocrlf input | |
| # | |
| # This repo has no submodules; 'submodules: recursive' is a no-op kept for future deps. | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| with: | |
| submodules: 'recursive' | |
| # | |
| # Restore-only half of the cache pair; the save step below is gated on a miss so a hit never | |
| # rewrites the entry | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_WINDOWS}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_WINDOWS}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: msvc2022_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_WINDOWS}}-x64-msvc2022_64-v2 | |
| installer-shell: pwsh | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| # | |
| # get-cmake supplies a current CMake; both of its caches are off on this image | |
| # | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: false | |
| useCloudCache: false | |
| # | |
| # Sets the x64 MSVC environment that the Ninja generator and clang-cl inherit | |
| # | |
| - name: '🛠 Setup MSVC Development Environment' | |
| uses: TheMrMilchmann/setup-msvc-dev@368ef7d1ee4d1171b31d4a7f67f4d954f903f5a9 # v4.1.0 | |
| with: | |
| arch: x64 | |
| # | |
| # The VS-bundled clang-cl is used in preference to any LLVM on PATH, and the step throws | |
| # rather than silently falling back | |
| # | |
| - name: '🛠 Use Microsoft (VS-bundled) clang-cl' | |
| run: | | |
| $clangDir = Join-Path $env:VCINSTALLDIR 'Tools\Llvm\x64\bin' | |
| if (-not (Test-Path (Join-Path $clangDir 'clang-cl.exe'))) { throw "VS clang-cl not found at $clangDir" } | |
| Add-Content -Path $env:GITHUB_PATH -Value $clangDir | |
| & (Join-Path $clangDir 'clang-cl.exe') --version | |
| # | |
| # gRPC arrives as a prebuilt tarball from GRPC_REPO; building it from source would cost more | |
| # than the rest of the job put together. | |
| # | |
| # Retried: the release-asset endpoint answers HTTP 500 often enough to redden a run on its own | |
| # (2026-09-03, Linux arm64), and a single failed GET here costs the whole job. --clobber so a | |
| # partial download from the previous attempt does not make the retry fail on an existing file. | |
| # | |
| - name: '⚙️ Download prebuilt gRPC' | |
| run: | | |
| for ($attempt = 1; $attempt -le 5; $attempt++) { | |
| gh release download v${{env.GRPC_VERSION}} ` | |
| -R ${{env.GRPC_REPO}} ` | |
| -p "grpc-${{env.GRPC_VERSION}}-windows-x86_64.zip" ` | |
| --clobber | |
| if ($LASTEXITCODE -eq 0) { break } | |
| if ($attempt -eq 5) { throw "gh release download failed after $attempt attempts" } | |
| Start-Sleep -Seconds ($attempt * 15) | |
| } | |
| mkdir "${{github.workspace}}\grpc-prefix" | |
| 7z x grpc-${{env.GRPC_VERSION}}-windows-x86_64.zip -o"${{github.workspace}}\grpc-prefix" | |
| env: | |
| GH_TOKEN: ${{secrets.GITHUB_TOKEN}} | |
| # | |
| # Stage 1 of the two-stage PGO flow: an instrumented Release build whose only job is to emit | |
| # profiles for the training runs below | |
| # | |
| - name: '🚧 Configure with CMake (PGO generate)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja ` | |
| -DSS_BUILD_COMMIT=${{ github.sha }} ` | |
| -DCMAKE_CXX_COMPILER=clang-cl ` | |
| -DCMAKE_C_COMPILER=clang-cl ` | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix" ` | |
| -DPRODUCTION_OPTIMIZATION=ON ` | |
| -DENABLE_HARDENING=ON ` | |
| -DENABLE_GRPC=ON ` | |
| -DENABLE_PGO=ON ` | |
| -DPGO_STAGE=GENERATE ` | |
| -DSS_ALLOC_STATS=ON ` | |
| -DSS_UNITY_BUILD=ON ` | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} ` | |
| -DCMAKE_BUILD_TYPE=Release ` | |
| -DBUILD_COMMERCIAL=ON ` | |
| -DBUILD_GPL3=OFF | |
| # | |
| # Builds the instrumented binary the two training runs profile | |
| # | |
| - name: '🚧 Build application (instrumented)' | |
| run: cmake --build build --config Release | |
| # | |
| # Pro widgets are license-gated, so an unactivated training run profiles the GPL subset and | |
| # the shipped binary is optimized for code no Pro user runs. A failed activation is therefore | |
| # a failed build, not a warning | |
| # | |
| - name: '🔑 Activate Serial Studio license (Pro hotpath training)' | |
| env: | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| run: | | |
| $p = Start-Process -FilePath "build/app/${{env.EXECUTABLE}}.exe" ` | |
| -ArgumentList '--activate',"$env:SERIAL_STUDIO_LICENSE_KEY" ` | |
| -WorkingDirectory $PWD -PassThru -NoNewWindow | |
| if (-not $p.WaitForExit(120000)) { $p.Kill(); throw 'license activation timed out' } | |
| if ($p.ExitCode -ne 0) { throw "license activation failed (exit $($p.ExitCode))" } | |
| # | |
| # --min-fps 1 turns the benchmark into a profile generator: the real gate runs later, against | |
| # the optimized binary | |
| # | |
| - name: '🏋️ PGO training run (hotpath)' | |
| run: | | |
| $p = Start-Process -FilePath "build/app/${{env.EXECUTABLE}}.exe" ` | |
| -ArgumentList '--headless','--benchmark-hotpath','--min-fps','1','--benchmark-output','pgo-train.txt' ` | |
| -WorkingDirectory $PWD -PassThru -NoNewWindow | |
| if (-not $p.WaitForExit(300000)) { $p.Kill(); throw 'benchmark timed out (no PGO profile)' } | |
| if (-not (Select-String -Path pgo-train.txt -Pattern 'HOTPATH_PASS=1' -Quiet)) { throw 'hotpath allocation gate failed (see pgo-train.txt)' } | |
| # | |
| # continue-on-error: the second profile is a bonus, and this load is the flakiest step in the | |
| # job on Windows | |
| # | |
| - name: '🏋️ PGO training run (big project)' | |
| continue-on-error: true | |
| run: | | |
| ./tests/benchmarks/big_db_test/run_load.ps1 -App "build/app/${{env.EXECUTABLE}}.exe" -Seconds 25 | |
| # | |
| # Stage 2: identical flags with PGO_STAGE=USE so the compiler consumes the profiles just | |
| # written | |
| # | |
| - name: '🚧 Reconfigure with CMake (PGO use)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -B build -G Ninja ` | |
| -DSS_BUILD_COMMIT=${{ github.sha }} ` | |
| -DCMAKE_CXX_COMPILER=clang-cl ` | |
| -DCMAKE_C_COMPILER=clang-cl ` | |
| -DCMAKE_PREFIX_PATH="${{github.workspace}}/grpc-prefix" ` | |
| -DPRODUCTION_OPTIMIZATION=ON ` | |
| -DENABLE_HARDENING=ON ` | |
| -DENABLE_GRPC=ON ` | |
| -DENABLE_PGO=ON ` | |
| -DPGO_STAGE=USE ` | |
| -DSS_ALLOC_STATS=ON ` | |
| -DSS_UNITY_BUILD=ON ` | |
| -DSS_UNITY_BATCH_SIZE=${{ env.SS_UNITY_BATCH }} ` | |
| -DSS_PACKAGE_TYPE=msi ` | |
| -DCMAKE_BUILD_TYPE=Release ` | |
| -DBUILD_COMMERCIAL=ON ` | |
| -DBUILD_GPL3=OFF | |
| # | |
| # The binary that ships, and the one every gate below measures | |
| # | |
| - name: '🚧 Build application (PGO optimized)' | |
| run: cmake --build build --config Release | |
| # | |
| # The throughput gate reads HOTPATH_PASS out of benchmark.txt: Start-Process loses the child's | |
| # exit code, so the verdict travels through the file. A miss fails the build | |
| # | |
| - name: '🚦 Hotpath throughput gate (256 kHz)' | |
| run: | | |
| $p = Start-Process -FilePath "build/app/${{env.EXECUTABLE}}.exe" ` | |
| -ArgumentList '--headless','--benchmark-hotpath','--min-fps','256000','--benchmark-output','benchmark.txt' ` | |
| -WorkingDirectory $PWD -PassThru -NoNewWindow | |
| if (-not $p.WaitForExit(300000)) { $p.Kill(); throw 'benchmark timed out' } | |
| if (Select-String -Path benchmark.txt -Pattern 'HOTPATH_PASS=1' -Quiet) { exit 0 } else { exit 1 } | |
| - name: '📤 Upload artifact: benchmark report (Windows)' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: benchmark-Windows | |
| path: | | |
| benchmark.txt | |
| pgo-train.txt | |
| - name: '📤 Upload artifact: symbols (Windows)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: symbols-Windows | |
| path: build/app/${{env.EXECUTABLE}}.pdb | |
| if-no-files-found: ignore | |
| # | |
| # always(): activations are seat-limited, so a failed job still has to release the seat | |
| # | |
| - name: '🔑 Deactivate Serial Studio license' | |
| if: always() | |
| run: | | |
| try { | |
| $p = Start-Process -FilePath "build/app/${{env.EXECUTABLE}}.exe" ` | |
| -ArgumentList '--deactivate' ` | |
| -WorkingDirectory $PWD -PassThru -NoNewWindow | |
| if (-not $p.WaitForExit(120000)) { $p.Kill() } | |
| } catch { | |
| Write-Host "License deactivation error: $_" | |
| } | |
| exit 0 | |
| # | |
| # msbuild is what the WiX toolchain shells out to | |
| # | |
| - name: '⚙️ Add msbuild to PATH' | |
| uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0 | |
| # | |
| # WiX is the CPack backend for the MSI | |
| # | |
| - name: '⚙️ Install WiX' | |
| run: dotnet tool install --global wix | |
| # | |
| # cpack drives WiX; the installer is renamed to the release naming scheme | |
| # | |
| - name: '📦 Create MSI installer' | |
| run: | | |
| cd build | |
| cpack --verbose | |
| mv *.msi ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msi | |
| mv ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msi ../ | |
| # | |
| # The same build packaged as a plain archive, the base of the portable ZIP | |
| # | |
| - name: '📦 Create portable TGZ' | |
| run: | | |
| cd build | |
| cpack -G TGZ | |
| mv *.tar.gz ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.tgz | |
| # | |
| # Unpacks the TGZ so the portable ZIP can be assembled with a launcher and a license beside it | |
| # | |
| - name: '📦 Extract TGZ contents' | |
| shell: bash | |
| run: | | |
| cd build | |
| 7z x ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.tgz | |
| 7z x ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.tar | |
| DIRNAME=$(tar -tf ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.tar 2>/dev/null | head -1 | cut -f1 -d"/" || true) | |
| echo $DIRNAME > dirname.txt | |
| cp ../app/deploy/windows/license.rtf ./License.rtf | |
| # | |
| # The deploy tools bundle only the native platform plugin; the integration tests run with | |
| # QT_QPA_PLATFORM=offscreen | |
| # | |
| - name: '🧩 Bundle offscreen platform plugin (headless CI tests)' | |
| shell: bash | |
| run: | | |
| cd build | |
| DIRNAME=$(cat dirname.txt) | |
| QT_PLUGINS_PATH="${{github.workspace}}/Qt-${{env.QT_VERSION_WINDOWS}}-${{runner.os}}-x64/${{env.QT_VERSION_WINDOWS}}/msvc2022_64/plugins" | |
| DEST="$DIRNAME/plugins/platforms" | |
| mkdir -p "$DEST" | |
| if [ -f "$QT_PLUGINS_PATH/platforms/qoffscreen.dll" ]; then | |
| cp "$QT_PLUGINS_PATH/platforms/qoffscreen.dll" "$DEST/" | |
| else | |
| echo "Warning: offscreen platform plugin not found at $QT_PLUGINS_PATH/platforms" | |
| fi | |
| # | |
| # Portable entry point: a .bat next to the extracted tree, so users do not have to dig into | |
| # bin\ for the executable | |
| # | |
| - name: '📝 Create Windows batch launcher' | |
| shell: bash | |
| run: | | |
| cd build | |
| DIRNAME=$(cat dirname.txt) | |
| echo "@echo off" > "${{env.APPLICATION}}.bat" | |
| echo "start \"\" \"%~dp0${DIRNAME}\\bin\\${{env.EXECUTABLE}}.exe\"" >> "${{env.APPLICATION}}.bat" | |
| # | |
| # Stamps ss-config.json as portable and reads it straight back out of the archive as a check | |
| # | |
| - name: '📦 Add files to ZIP' | |
| shell: bash | |
| run: | | |
| cd build | |
| DIRNAME=$(cat dirname.txt) | |
| printf '{"packageType":"portable","arch":"x86_64"}\n' > "$DIRNAME/bin/ss-config.json" | |
| 7z a ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows-Portable.zip "${{env.APPLICATION}}.bat" License.rtf $DIRNAME/* | |
| 7z e -so ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows-Portable.zip "$DIRNAME/bin/ss-config.json" | grep -q '"packageType":"portable"' | |
| mv ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows-Portable.zip .. | |
| # | |
| # Store package: the logo variants are rendered from PackageIcon.png onto transparent square | |
| # canvases, so no plate bleeds behind the icon. The taskbar and the Start "all apps" list read | |
| # the target-size variants, and they are shipped in their unplated form so Windows draws the | |
| # bare icon instead of stamping it on the accent-colour plate. resources.pri is what makes | |
| # those qualified variants resolve at runtime; without the index Windows only sees the neutral | |
| # logo and plates it. The manifest is generated inline, and makepri/makeappx are located in the | |
| # installed Windows SDK rather than assumed on PATH | |
| # | |
| - name: '📦 Create MSIX package (Microsoft Store)' | |
| run: | | |
| $dirname = (Get-Content build/dirname.txt -TotalCount 1).Trim() | |
| $staging = "build/msix" | |
| New-Item -ItemType Directory -Force "$staging/Assets" | Out-Null | |
| Copy-Item -Recurse -Force "build/$dirname/*" $staging | |
| Set-Content -Path "$staging/bin/ss-config.json" ` | |
| -Value '{"packageType":"msix","arch":"x86_64"}' -Encoding utf8 -NoNewline | |
| Add-Type -AssemblyName System.Drawing | |
| $icon = [System.Drawing.Image]::FromFile("$PWD/app/deploy/PackageIcon.png") | |
| function Save-Logo($size, $name) { | |
| $bmp = New-Object System.Drawing.Bitmap($size, $size) | |
| $gfx = [System.Drawing.Graphics]::FromImage($bmp) | |
| $gfx.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic | |
| $gfx.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality | |
| $gfx.DrawImage($icon, 0, 0, $size, $size) | |
| $bmp.Save("$PWD/$staging/Assets/$name", [System.Drawing.Imaging.ImageFormat]::Png) | |
| $gfx.Dispose() | |
| $bmp.Dispose() | |
| } | |
| Save-Logo 50 'StoreLogo.png' | |
| Save-Logo 150 'Square150x150Logo.png' | |
| Save-Logo 44 'Square44x44Logo.png' | |
| foreach ($s in 16, 24, 32, 48, 256) { | |
| Save-Logo $s "Square44x44Logo.targetsize-$s.png" | |
| Save-Logo $s "Square44x44Logo.targetsize-${s}_altform-unplated.png" | |
| } | |
| $icon.Dispose() | |
| $description = $env:DESCRIPTION -replace '&', '&' | |
| $manifest = @" | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <Package | |
| xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" | |
| xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" | |
| xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"> | |
| <Identity Name="$($env:MSIX_IDENTITY_NAME)" | |
| Publisher="$($env:MSIX_PUBLISHER)" | |
| Version="$($env:VERSION).0" | |
| ProcessorArchitecture="x64" /> | |
| <Properties> | |
| <DisplayName>$($env:APPLICATION)</DisplayName> | |
| <PublisherDisplayName>$($env:MSIX_PUBLISHER_DISPLAY)</PublisherDisplayName> | |
| <Logo>Assets\StoreLogo.png</Logo> | |
| </Properties> | |
| <Dependencies> | |
| <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26100.0" /> | |
| </Dependencies> | |
| <Resources> | |
| <Resource Language="en-US" /> | |
| </Resources> | |
| <Applications> | |
| <Application Id="SerialStudioPro" | |
| Executable="bin\$($env:EXECUTABLE).exe" | |
| EntryPoint="Windows.FullTrustApplication"> | |
| <uap:VisualElements DisplayName="$($env:APPLICATION)" | |
| Description="$description" | |
| BackgroundColor="transparent" | |
| Square150x150Logo="Assets\Square150x150Logo.png" | |
| Square44x44Logo="Assets\Square44x44Logo.png" /> | |
| </Application> | |
| </Applications> | |
| <Capabilities> | |
| <rescap:Capability Name="runFullTrust" /> | |
| </Capabilities> | |
| </Package> | |
| "@ | |
| Set-Content -Path "$staging/AppxManifest.xml" -Value $manifest -Encoding utf8 | |
| $makepri = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\x64\makepri.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object FullName -Descending | Select-Object -First 1 | |
| if (-not $makepri) { throw 'makepri.exe not found in any installed Windows SDK' } | |
| & $makepri.FullName createconfig /cf "build/priconfig.xml" /dq en-US /o | |
| if ($LASTEXITCODE -ne 0) { throw "makepri createconfig failed with exit code $LASTEXITCODE" } | |
| & $makepri.FullName new /pr "$staging" /mn "$staging/AppxManifest.xml" /cf "build/priconfig.xml" /of "$staging/resources.pri" /o | |
| if ($LASTEXITCODE -ne 0) { throw "makepri new failed with exit code $LASTEXITCODE" } | |
| Remove-Item "build/priconfig.xml" -Force | |
| $makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\x64\makeappx.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object FullName -Descending | Select-Object -First 1 | |
| if (-not $makeappx) { throw 'makeappx.exe not found in any installed Windows SDK' } | |
| & $makeappx.FullName pack /d $staging /p "${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msix" /o | |
| if ($LASTEXITCODE -ne 0) { throw "makeappx failed with exit code $LASTEXITCODE" } | |
| $stamp = & 7z e -so "${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msix" "bin/ss-config.json" | |
| if ($stamp -notmatch '"packageType":"msix"') { throw 'MSIX ss-config.json stamp mismatch' } | |
| # | |
| # Release artifact: the MSI installer | |
| # | |
| - name: '📤 Upload artifact: MSI installer' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msi | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msi | |
| # | |
| # Release artifact: the portable ZIP | |
| # | |
| - name: '📤 Upload artifact: Portable ZIP' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows-Portable | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows-Portable.zip | |
| # | |
| # Release artifact: the MSIX package uploaded to the Microsoft Store | |
| # | |
| - name: '📤 Upload artifact: MSIX package (Store upload)' | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msix | |
| path: ${{env.EXECUTABLE}}-${{env.VERSION}}-Windows.msix | |
| #--------------------------------------------------------------------------------------------------- | |
| # Create release | |
| #--------------------------------------------------------------------------------------------------- | |
| upload: | |
| name: '🗂 Publish (continuous)' | |
| # | |
| # Publication waits on the builds and the linters only: the release goes out as soon as the | |
| # last package exists, and the pytest suite reports on it in parallel without holding it. | |
| # Every dependency must succeed (the default needs semantics), so a red build or a missed | |
| # throughput gate still blocks the publish. | |
| # | |
| needs: | |
| - lint | |
| - build-linux | |
| - build-linux-arm64 | |
| - build-macos | |
| - build-windows | |
| # | |
| # Publication is a side effect on a shared, mutable tag: the step below deletes the | |
| # 'continuous' release and recreates it. Without the branch guard every push on every | |
| # branch (and every same-repo PR) republished it, and two runs racing each other left the | |
| # tag pointing at one build with another's assets. | |
| # | |
| if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/') | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| runs-on: ubuntu-latest | |
| env: | |
| RELEASE_TAG: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && github.ref_name || 'continuous' }} | |
| steps: | |
| # | |
| # No submodules: this job only needs the workflow checkout to run gh | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| # | |
| # merge-multiple flattens every build job's artifact into one ./artifacts directory | |
| # | |
| - name: '📥 Download artifacts' | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | |
| with: | |
| path: ./artifacts | |
| merge-multiple: true | |
| # | |
| # The continuous release is recreated from scratch on every run; deleting the tag with it | |
| # keeps the release page from accumulating stale assets | |
| # | |
| - name: '🗑️ Delete previous release and tag if exists' | |
| run: | | |
| if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" &>/dev/null; then | |
| echo "Release '$RELEASE_TAG' exists. Deleting..." | |
| gh release delete "$RELEASE_TAG" --cleanup-tag --yes --repo "$GITHUB_REPOSITORY" || echo "Failed to delete release. Ignoring." | |
| else | |
| echo "No release named '$RELEASE_TAG' found. Skipping delete." | |
| fi | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # | |
| # prerelease + allowUpdates: 'continuous' is a moving target, while a pushed tag publishes | |
| # under its own name | |
| # | |
| - name: '🚀 Create Release' | |
| uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 | |
| with: | |
| tag: ${{ env.RELEASE_TAG }} | |
| commit: ${{ github.sha }} | |
| prerelease: true | |
| allowUpdates: true | |
| replacesArtifacts: true | |
| name: 'Continuous Build' | |
| artifacts: | | |
| artifacts/*.AppImage | |
| artifacts/*.deb | |
| artifacts/*.rpm | |
| artifacts/*.dmg | |
| artifacts/*.msi | |
| artifacts/*.zip | |
| #--------------------------------------------------------------------------------------------------- | |
| # Static analysis (fail-fast, no build) | |
| #--------------------------------------------------------------------------------------------------- | |
| lint: | |
| runs-on: ubuntu-24.04 | |
| name: '🔬 Lint' | |
| permissions: | |
| contents: read | |
| steps: | |
| # | |
| # No submodules: the linters only read first-party sources and scripts | |
| # | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| # | |
| # 3.11 with pip caching: every gate in this job is a Python script | |
| # | |
| - name: '🐍 Set up Python' | |
| uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 | |
| with: | |
| python-version: '3.11' | |
| cache: 'pip' | |
| # | |
| # requirements.lock is the hash-pinned resolution of tests/requirements.txt; --require-hashes | |
| # is what makes a CI run reproducible instead of a function of the day it ran | |
| # | |
| - name: '🐍 Install Python dependencies' | |
| run: pip install --require-hashes -r tests/requirements.lock | |
| # | |
| # The repo is REUSE-compliant (REUSE.toml + LICENSES/); a file without an SPDX header fails | |
| # here | |
| # | |
| - name: '⚖️ Verify REUSE licensing compliance' | |
| run: reuse lint | |
| # | |
| # scripts/code-verify.py is the style contract: errors block the job, advisories do not | |
| # | |
| - name: '🔬 Verify code conventions' | |
| run: python3 scripts/code-verify.py --check | |
| # | |
| # clang-format is pinned in the lock installed above, so this compares the tree against one | |
| # exact LLVM release rather than against whatever the runner ships. Without this gate a | |
| # contributor running a different clang-format lands a tree-wide restyle as a side effect of | |
| # sanitize-commit.py, which is how 54 unrelated files rode in on one commit (2026-09-10) | |
| # | |
| - name: '🎨 Verify clang-format' | |
| run: python3 scripts/sanitize-commit.py --check-format | |
| # | |
| # Spec 0076: the core/ static libraries only stay layered if it is mechanical. This resolves | |
| # every quoted include, rejects an upward one, and catches a core/ source no target owns | |
| # | |
| - name: '🧱 Verify core layering' | |
| run: python3 scripts/layer-verify.py | |
| # | |
| # Growth ratchets. Each has a checked-in baseline and fails only on an increase, so the | |
| # existing debt does not block CI while new debt does. Until 2026-08-25 the singleton census | |
| # ran only inside sanitize-commit.py, which made it a gate somebody had to remember to run | |
| # | |
| - name: '🔒 Ratchet global state (spec 0039)' | |
| run: python3 scripts/code-verify.py --singleton-census --check | |
| - name: '🔒 Ratchet translation-unit size' | |
| run: python3 scripts/code-verify.py --tu-census --check | |
| # | |
| # Clone families are invisible to a per-file linter: every file passes on its own. This | |
| # ratchets the summed count of shared 10-line windows across first-party file pairs | |
| # | |
| - name: '🔒 Ratchet duplicated code' | |
| run: python3 scripts/code-verify.py --dup-census --check | |
| # | |
| # Spec 0077: every Core::Bus topic has a publisher and a subscriber, no token names a topic | |
| # Messages.h does not declare, and no bus token sits in a hotpath translation unit | |
| # | |
| - name: '🔒 Verify bus topics' | |
| run: python3 scripts/code-verify.py --bus-census --check | |
| # | |
| # The tooling's own tests: 8000+ lines of source-rewriting Python and a 3600-line workflow | |
| # had zero coverage until spec 0075, so a rule regression rewrote files silently and CI | |
| # drift was invisible | |
| # | |
| - name: '🔬 Test the repository tooling' | |
| run: python3 -m pytest scripts/tests/ -q | |
| # | |
| # The AI-facing docs are the tier that rots silently; this checks their paths, symbols and | |
| # pinned constants against the tree | |
| # | |
| - name: '🔬 Verify AI documentation claims' | |
| run: python3 scripts/claim-verify.py --quiet | |
| # | |
| # Markdown AI-narration scan over the user-facing docs | |
| # | |
| - name: '🔬 Verify documentation conventions' | |
| run: python3 scripts/documentation-verify.py --quiet | |
| # | |
| # The .code-report cleanup checklist, uploaded even on success so the baseline debt stays | |
| # visible | |
| # | |
| - name: '📤 Upload code-verify report' | |
| if: always() | |
| uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | |
| with: | |
| name: code-verify-report | |
| path: | | |
| .code-report | |
| .doc-report | |
| .claim-report | |
| if-no-files-found: ignore | |
| # | |
| # Spec 0037: every drift gate the repo owns has a caller. The snapshot projection is --strict | |
| # here (locally it warns, because only a build can refresh api-schema.json); its failure | |
| # message names the ordered fix | |
| # | |
| - name: '🧬 Verify generated API surfaces' | |
| run: | | |
| python3 scripts/registry-verify.py | |
| python3 scripts/generate-command-strings.py --check | |
| python3 scripts/generate-property-registry.py --check | |
| python3 scripts/generate-property-registry.py --check-snapshot --strict | |
| python3 scripts/generate-sdk.py --check | |
| #--------------------------------------------------------------------------------------------------- | |
| # Sanitizers, fuzz corpus and the GPLv3 build gate | |
| #--------------------------------------------------------------------------------------------------- | |
| # | |
| # cmake/Sanitizers.cmake names TSan the only tool that proves the lock-free SPSC hotpath | |
| # invariants, and until spec 0075 nothing ran it. These jobs are that proof plus the ASan/UBSan | |
| # leg, and they are where the fuzz corpora replay under instrumentation. They are deliberately | |
| # NOT in upload's needs, and both carry continue-on-error at the job level: a sanitizer finding | |
| # must neither hold the release hostage nor redden a run whose product is fine. GitHub renders | |
| # such a job green, so each leg ends with an annotation step that turns any failed step into a | |
| # warning on the run summary -- that annotation, not the job icon, is the signal to read. | |
| # | |
| # Both legs were disabled from 2026-09-04 to 2026-09-11. What the last enabled run reported, and | |
| # what fixed it: UBSan findings inside vendored code (open62541's comparator dispatch, LuaJIT's | |
| # tagged pointers and shifts) now opt those two archives out of the one check each trips; | |
| # libplctag's Clang Debug branch hardwired ASan into its sources, a hard error next to | |
| # -fsanitize=thread, and lib/libplctag/CMakeLists.txt strips it; two enums without a fixed | |
| # underlying type made the out-of-range fallbacks the suites exercise undefined; and the CSV | |
| # fuzz harness fed the scanners the empty input libFuzzer always runs first. | |
| # | |
| # Three jobs, not one. ASan and TSan cannot coexist in a build, so a single job compiled the tree | |
| # twice back to back -- 60+ minutes of wall clock on the critical path, and on 2026-09-03 the | |
| # runner was killed mid-link twice before ctest ever started. Split, they run in parallel, and | |
| # each one links with lld against -gline-tables-only objects through a 2-slot link pool, which is | |
| # what brought the peak link footprint back under the runner's memory. | |
| # | |
| # Both sanitizer legs build Pro (BUILD_GPL3=OFF): the Pro modules -- Modbus, CAN, OPC UA, S7comm, | |
| # EtherNet/IP, IEC 60870-5-104, Sparkplug B, MDF4 -- are the ones customers pay for, and they were | |
| # the only part of the tree no sanitizer ever looked at. The global BUILD_COMMERCIAL also reaches | |
| # the unit tier, so every #ifdef BUILD_COMMERCIAL branch in a shared TU is instrumented too. No | |
| # license is activated here: the tier inspects code, not entitlement, and the five build jobs | |
| # already hold the concurrent activations this account allows. | |
| # | |
| # build-gpl3 is what keeps the GPLv3 half honest. With both sanitizer legs on Pro, nothing else in | |
| # the workflow compiles the GPL application, and a stray Pro symbol outside its #ifdef would ship | |
| # a source tarball that does not build. | |
| # | |
| #--------------------------------------------------------------------------------------------------- | |
| sanitize: | |
| continue-on-error: true | |
| runs-on: ubuntu-24.04 | |
| name: '🧫 Sanitizers (ASan + UBSan, Pro)' | |
| permissions: | |
| contents: read | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| # | |
| # detect_leaks=0: Qt's plugin and QML machinery leaks by design at exit, so leak reports | |
| # here would be pure noise. The memory-error half is what this job is for. | |
| # | |
| ASAN_OPTIONS: detect_leaks=0:abort_on_error=1 | |
| UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1 | |
| steps: | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-toolchain-deps | |
| # | |
| # Restore-only: build-linux owns the save half of this entry, so two jobs never race to | |
| # write it | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: gcc_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-x64 | |
| save-cache: 'false' | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # Clang because ENABLE_FUZZERS needs libFuzzer; mimalloc off because an allocator override | |
| # and ASan cannot both own malloc. SS_INAPP_TESTS is what exposes --benchmark-hotpath. | |
| # The credentials travel through env: -- CMake reads them with $ENV{} -- and a Pro configure | |
| # validates them against Lemon Squeezy before it generates the license guards | |
| # | |
| - name: '🚧 Configure ASan + UBSan tier (Pro)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -G Ninja -B build/asan \ | |
| -DCMAKE_C_COMPILER=clang \ | |
| -DCMAKE_CXX_COMPILER=clang++ \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DDEBUG_SANITIZER=ON \ | |
| -DENABLE_FUZZERS=ON \ | |
| -DSS_BUILD_TESTS=ON \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DBUILD_GPL3=OFF \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_JOB_POOLS=link=2 \ | |
| -DCMAKE_JOB_POOL_LINK=link \ | |
| -DSS_SANITIZER_DEBUG_LEVEL=-gline-tables-only | |
| - name: '🚧 Build ASan + UBSan tier' | |
| run: cmake --build build/asan | |
| # | |
| # Step-level continue-on-error on the four run steps: one failing suite must not skip the | |
| # corpus replay, the pipeline pass and the QML pass, or a single finding hides three others. | |
| # The annotation step at the end reads their outcomes | |
| # | |
| - name: '🧪 Run ctest (ASan + UBSan)' | |
| id: asan_ctest | |
| continue-on-error: true | |
| run: ctest --test-dir build/asan --output-on-failure | |
| # | |
| # --no-tests=ignore: the fuzz set is empty until the defect packages land their targets, and | |
| # an empty selection is not a failure | |
| # | |
| - name: '🧬 Replay fuzz corpora' | |
| id: asan_fuzz | |
| continue-on-error: true | |
| run: ctest --test-dir build/asan -R '^fuzz_' --output-on-failure --no-tests=ignore | |
| # | |
| # --min-fps 1: instrumented throughput is meaningless as a number, but the run exercises the | |
| # whole parse pipeline under ASan/UBSan, which is what this leg is for | |
| # | |
| - name: '🏋️ Hotpath pipeline under ASan + UBSan' | |
| id: asan_hotpath | |
| continue-on-error: true | |
| run: ./build/asan/app/${{env.UNIXNAME}} --headless --benchmark-hotpath --min-fps 1 | |
| # | |
| # The Pro file set is a superset of the GPL one, so this instantiates every .qml the product | |
| # ships -- Plot3D, Waterfall, ImageView, the output widgets and the Pro driver panes included | |
| # | |
| - name: '🧪 QML instantiation self-test (Pro build)' | |
| id: asan_qml | |
| continue-on-error: true | |
| run: ./build/asan/app/${{env.UNIXNAME}} --headless --selftest-suite qml | |
| # | |
| # The job stays green whatever happened above, so this is where a finding becomes visible: | |
| # one warning per failed step on the run summary, and a build or configure failure (which | |
| # skips the run steps) reports through the job status the same way | |
| # | |
| - name: '⚠️ Annotate sanitizer findings' | |
| if: always() | |
| run: | | |
| failed=0 | |
| for step in \ | |
| 'ctest:${{steps.asan_ctest.outcome}}' \ | |
| 'fuzz corpus replay:${{steps.asan_fuzz.outcome}}' \ | |
| 'hotpath pipeline:${{steps.asan_hotpath.outcome}}' \ | |
| 'QML self-test:${{steps.asan_qml.outcome}}'; do | |
| name="${step%%:*}" | |
| outcome="${step##*:}" | |
| if [ "$outcome" = "failure" ]; then | |
| echo "::warning title=ASan + UBSan (${name})::the step reported findings; see the job log" | |
| failed=1 | |
| fi | |
| done | |
| if [ "${{job.status}}" = "failure" ] && [ "$failed" = "0" ]; then | |
| echo "::warning title=ASan + UBSan (build)::the tier did not build; see the job log" | |
| fi | |
| #--------------------------------------------------------------------------------------------------- | |
| # ThreadSanitizer | |
| #--------------------------------------------------------------------------------------------------- | |
| # | |
| # TSan is its own build: it cannot coexist with ASan. The unit tier alone, because the | |
| # SPSC/DirectConnection invariants it proves live in the suites, not the GUI. | |
| # | |
| #--------------------------------------------------------------------------------------------------- | |
| sanitize-tsan: | |
| continue-on-error: true | |
| runs-on: ubuntu-24.04 | |
| name: '🧫 Sanitizers (TSan, Pro)' | |
| permissions: | |
| contents: read | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| TSAN_OPTIONS: halt_on_error=1:suppressions=${{github.workspace}}/app/tests/tsan.supp | |
| steps: | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-toolchain-deps | |
| # | |
| # Restore-only: build-linux owns the save half of this entry, so two jobs never race to | |
| # write it | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: gcc_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-x64 | |
| save-cache: 'false' | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # link=1, not the ASan leg's 2: the whole unit tier links one instrumented executable per | |
| # suite, and serializing those links costs minutes, not the job. It was introduced for the | |
| # 2026-09-04 VM shutdown "mid-link" (test executable 1457 of 1482), but ninja prints an edge | |
| # when it FINISHES, and the runs of 2026-09-11/12 died the same way with every link done: the | |
| # edge still in flight each time was the TSan compile of lib/open62541/open62541.c, which | |
| # lib/open62541/CMakeLists.txt now leaves uninstrumented under ENABLE_TSAN | |
| # | |
| - name: '🚧 Configure TSan tier (Pro)' | |
| env: | |
| ARCGIS_API_KEY: ${{secrets.ARCGIS_API_KEY}} | |
| SERIAL_STUDIO_LICENSE_KEY: ${{secrets.SERIAL_STUDIO_LICENSE_KEY}} | |
| SERIAL_STUDIO_INSTANCE_ID: ${{secrets.SERIAL_STUDIO_INSTANCE_ID}} | |
| run: | | |
| cmake -G Ninja -B build/tsan \ | |
| -DCMAKE_C_COMPILER=clang \ | |
| -DCMAKE_CXX_COMPILER=clang++ \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DENABLE_TSAN=ON \ | |
| -DSS_BUILD_TESTS=ON \ | |
| -DBUILD_GPL3=OFF \ | |
| -DBUILD_COMMERCIAL=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_SHARED_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_MODULE_LINKER_FLAGS=-fuse-ld=lld \ | |
| -DCMAKE_JOB_POOLS=link=1 \ | |
| -DCMAKE_JOB_POOL_LINK=link \ | |
| -DSS_SANITIZER_DEBUG_LEVEL=-gline-tables-only | |
| - name: '🚧 Build TSan tier' | |
| run: cmake --build build/tsan --target ss_unit_tests | |
| - name: '🧪 Run ctest (TSan)' | |
| id: tsan_ctest | |
| continue-on-error: true | |
| run: ctest --test-dir build/tsan --output-on-failure | |
| # | |
| # Same contract as the ASan leg: the job is green by construction, the annotation is the | |
| # signal | |
| # | |
| - name: '⚠️ Annotate sanitizer findings' | |
| if: always() | |
| run: | | |
| if [ "${{steps.tsan_ctest.outcome}}" = "failure" ]; then | |
| echo "::warning title=TSan (ctest)::the suites reported findings; see the job log" | |
| elif [ "${{job.status}}" = "failure" ]; then | |
| echo "::warning title=TSan (build)::the tier did not build; see the job log" | |
| fi | |
| #--------------------------------------------------------------------------------------------------- | |
| # GPLv3 build gate | |
| #--------------------------------------------------------------------------------------------------- | |
| # | |
| # The only job that compiles the GPL application. Every other build in this workflow is Pro, so | |
| # without it a Pro symbol referenced outside its #ifdef BUILD_COMMERCIAL guard -- or a QML file | |
| # added to the shared set but registered only in the commercial resource list -- would reach the | |
| # published source tarball as a build failure nobody saw. | |
| # | |
| # No sanitizers: this is a compile, link and start-up gate, and the instrumented legs above | |
| # already cover the runtime. Release with the unity build for the same reason the PGO stages use | |
| # it -- it is the fastest way through ~800 first-party translation units on a 4-core runner. The | |
| # stock linker on purpose, too: the lld the sanitizer legs need is a memory workaround, and a gate | |
| # that exists to prove the GPL tarball builds should not build it a way nobody else does. | |
| # | |
| #--------------------------------------------------------------------------------------------------- | |
| build-gpl3: | |
| runs-on: ubuntu-24.04 | |
| name: '🐧 GPLv3 Build Gate' | |
| permissions: | |
| contents: read | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| steps: | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-toolchain-deps | |
| # | |
| # Restore-only: build-linux owns the save half of this entry, so two jobs never race to | |
| # write it | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: gcc_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-x64 | |
| save-cache: 'false' | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| - name: '🚧 Configure GPLv3 build' | |
| run: | | |
| cmake -G Ninja -B build/gpl3 \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DSS_INAPP_TESTS=ON \ | |
| -DBUILD_GPL3=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON \ | |
| -DSS_UNITY_BUILD=ON \ | |
| -DSS_UNITY_BATCH_SIZE=${{env.SS_UNITY_BATCH}} | |
| - name: '🚧 Build GPLv3 application' | |
| run: cmake --build build/gpl3 | |
| # | |
| # Instantiating every compiled .qml is the cheapest proof that the GPL binary starts and its | |
| # QML tree resolves against the globals this build actually registers | |
| # | |
| - name: '🧪 QML instantiation self-test (GPL build)' | |
| run: ./build/gpl3/app/serial-studio-gpl3 --headless --selftest-suite qml | |
| #--------------------------------------------------------------------------------------------------- | |
| # Per-library build (spec 0077) | |
| #--------------------------------------------------------------------------------------------------- | |
| # | |
| # Not in upload's needs: this job proves the layering, it does not produce a package. It mirrors | |
| # build-gpl3's toolchain and Qt-cache steps (restore-only; build-linux owns the save half). | |
| # | |
| build-core-libraries: | |
| runs-on: ubuntu-24.04 | |
| name: '🧱 Core Libraries Build Alone' | |
| permissions: | |
| contents: read | |
| env: | |
| QT_QPA_PLATFORM: offscreen | |
| steps: | |
| - name: '🧰 Checkout' | |
| uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 | |
| - name: '⚙️ Install dependencies' | |
| uses: ./.github/actions/linux-toolchain-deps | |
| # | |
| # Restore-only: build-linux owns the save half of this entry, so two jobs never race to | |
| # write it | |
| # | |
| - name: '⚙️ Set up Qt' | |
| uses: ./.github/actions/setup-qt | |
| with: | |
| qt-version: ${{env.QT_VERSION_LINUX}} | |
| qt-dir: ${{github.workspace}}/Qt-${{env.QT_VERSION_LINUX}}-${{runner.os}}-x64 | |
| qt-arch: x64 | |
| qt-host: gcc_64 | |
| cache-key: qt-${{runner.os}}-${{env.QT_VERSION_LINUX}}-x64 | |
| save-cache: 'false' | |
| qt-username: ${{secrets.QT_USERNAME}} | |
| qt-password: ${{secrets.QT_PASSWORD}} | |
| - name: '⚙️ Install CMake' | |
| uses: lukka/get-cmake@fffaaafeea488556c2c12dad60690008bc1caacb # v4.4.2 | |
| with: | |
| useLocalCache: true | |
| # | |
| # Spec 0077 AC2: the seven static libraries under core/ configure and build with no application | |
| # and no test target in the graph. SerialStudioUi pulls the other six in dependency order, so a | |
| # library that reaches a layer above it, or app/src, fails here before the executable links it | |
| # | |
| - name: '🚧 Configure the library tier' | |
| run: | | |
| cmake -G Ninja -B build/core-libs \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DBUILD_GPL3=ON \ | |
| -DENABLE_GRPC=OFF \ | |
| -DWITH_WEBENGINE=OFF \ | |
| -DSS_USE_MIMALLOC=OFF \ | |
| -DUSE_SYSTEM_ZLIB=ON \ | |
| -DUSE_SYSTEM_EXPAT=ON | |
| - name: '🧱 Build the seven core libraries' | |
| run: cmake --build build/core-libs --target SerialStudioUi | |
| #--------------------------------------------------------------------------------------------------- | |
| # Integration tests | |
| #--------------------------------------------------------------------------------------------------- | |
| # | |
| # One caller per leg rather than one matrixed job: a matrix carries a single `needs` list, so every | |
| # leg used to wait for the slowest build. The suite consumes the build jobs' artifacts directly, | |
| # never the Release: downloading from the Release (the pre-0075 shape) let one run's job pick up | |
| # another run's binary. It runs alongside 'upload' and reports on the published build without | |
| # gating it. | |
| # | |
| # secrets: inherit forwards SERIAL_STUDIO_LICENSE_KEY, which the activation step needs. | |
| # | |
| test-linux: | |
| name: '🧪 Test' | |
| needs: build-linux | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/test-suite.yml | |
| secrets: inherit | |
| with: | |
| label: Linux | |
| os: linux | |
| runner: ubuntu-24.04 | |
| asset: Linux-x64.AppImage | |
| broker: true | |
| opcua_sim: true | |
| marker_expr: "not destructive and not dos and not audio" | |
| timeout_method: signal | |
| test-linux-arm64: | |
| name: '🧪 Test' | |
| needs: build-linux-arm64 | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/test-suite.yml | |
| secrets: inherit | |
| with: | |
| label: Linux-arm64 | |
| os: linux | |
| runner: ubuntu-24.04-arm | |
| asset: Linux-arm64.AppImage | |
| broker: true | |
| opcua_sim: true | |
| marker_expr: "not destructive and not dos and not audio" | |
| timeout_method: signal | |
| test-macos: | |
| name: '🧪 Test' | |
| needs: build-macos | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/test-suite.yml | |
| secrets: inherit | |
| with: | |
| label: macOS | |
| os: macos | |
| runner: macos-latest | |
| asset: macOS.dmg | |
| broker: true | |
| opcua_sim: true | |
| marker_expr: "not destructive and not dos and not audio" | |
| timeout_method: signal | |
| test-windows: | |
| name: '🧪 Test' | |
| needs: build-windows | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/test-suite.yml | |
| secrets: inherit | |
| with: | |
| label: Windows | |
| os: windows | |
| runner: windows-latest | |
| asset: Windows-Portable | |
| broker: false | |
| opcua_sim: false | |
| marker_expr: "not destructive and not dos and not audio and not requires_broker and not requires_opcua_sim" | |
| timeout_method: thread |