Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 81 additions & 6 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,41 @@ name: Desktop binaries

on:
push:
tags: ['**']
tags: ['v*']
workflow_dispatch:
inputs:
packages:
description: Packages to build (manual runs do not publish a release)
type: choice
default: all
options: [all, adapter, gcs, qgroundcontrol, mission-planner]
gcs-target:
description: QGroundControl platform (manual builds only)
type: choice
default: all
options: [all, linux-amd64, windows-amd64, macos-universal]

permissions:
contents: read

concurrency:
group: desktop-${{ github.ref }}
group: desktop-${{ github.ref }}-${{ inputs.packages || 'all' }}-${{ inputs.gcs-target || 'all' }}
cancel-in-progress: true

jobs:
gcs:
name: GCS packages
if: inputs.packages != 'adapter'
uses: ./.github/workflows/gcs.yml
with:
component: ${{ (inputs.packages == 'qgroundcontrol' || inputs.packages == 'mission-planner') && inputs.packages || 'all' }}
target: ${{ inputs.gcs-target || 'all' }}
permissions:
contents: read
actions: read

build:
if: inputs.packages == '' || inputs.packages == 'all' || inputs.packages == 'adapter'
name: ${{ matrix.target }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -57,6 +80,8 @@ jobs:
run: bash build/ci/install-linux-deps.sh
- name: Install pinned Wails CLI
run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.34
- name: Validate version metadata
run: node scripts/check-version.mjs
- name: Backend tests and vet
run: |
go test -race ./internal/...
Expand All @@ -80,24 +105,74 @@ jobs:
run: |
wails3 build GOARCH=amd64
tar -czf bin/utm-adapter-linux-amd64.tar.gz -C bin utm-adapter
- name: Package Linux AppImage
if: runner.os == 'Linux'
run: |
wails3 task linux:create:appimage
image=$(find bin -maxdepth 1 -name '*.AppImage' -print -quit)
test -n "$image"
mv "$image" bin/utm-adapter-linux-amd64.AppImage
- name: Package Windows installer with WebView2 bootstrapper
if: runner.os == 'Windows'
run: |
choco install nsis --yes --no-progress
export PATH="/c/Program Files (x86)/NSIS:$PATH"
wails3 task windows:create:nsis:installer INSTALL_SCOPE=user
installer=$(find bin -maxdepth 1 -name '*-installer.exe' -print -quit)
test -n "$installer"
mv "$installer" bin/utm-adapter-windows-amd64-installer.exe
- name: Check Svelte and TypeScript
run: npm --prefix frontend run check
- name: Generate download checksum
env:
TARGET: ${{ matrix.target }}
run: |
node --input-type=module <<'JS'
import { readFileSync, writeFileSync } from 'node:fs';
import { readFileSync, writeFileSync, readdirSync } from 'node:fs';
import { createHash } from 'node:crypto';
const name = `utm-adapter-${process.env.TARGET}.${process.env.TARGET.startsWith('linux') ? 'tar.gz' : 'zip'}`;
writeFileSync(`bin/${name}.sha256`, `${createHash('sha256').update(readFileSync(`bin/${name}`)).digest('hex')} ${name}\n`);
for (const name of readdirSync('bin').filter(n => n.startsWith('utm-adapter-') && /\.(zip|tar\.gz|exe|AppImage)$/.test(n))) {
writeFileSync(`bin/${name}.sha256`, `${createHash('sha256').update(readFileSync(`bin/${name}`)).digest('hex')} ${name}\n`);
}
JS
- uses: actions/upload-artifact@v4
with:
name: utm-adapter-${{ matrix.target }}
path: |
bin/utm-adapter-${{ matrix.target }}.zip
bin/utm-adapter-${{ matrix.target }}.tar.gz
bin/utm-adapter-${{ matrix.target }}.*.sha256
bin/utm-adapter-${{ matrix.target }}.AppImage
bin/utm-adapter-${{ matrix.target }}-installer.exe
bin/utm-adapter-${{ matrix.target }}*.sha256
if-no-files-found: error
retention-days: 30

release:
name: Verify release downloads
if: (inputs.packages == '' || inputs.packages == 'all') && (inputs.gcs-target == '' || inputs.gcs-target == 'all')
needs: [build, gcs]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
pattern: utm-adapter-*
merge-multiple: true
path: release
- name: Verify checksums and assemble integration sources
run: |
cd release
sha256sum --check ./*.sha256
cp ../COMPATIBILITY.md release-notes.md
tar -czf utm-adapter-gcs-integrations.tar.gz -C .. integrations scripts/prepare-qgc.py build/gcs-versions.json
sha256sum utm-adapter-gcs-integrations.tar.gz > utm-adapter-gcs-integrations.tar.gz.sha256
- name: Publish verified downloads
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
cd release
gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --verify-tag --draft --title "UTM Adapter $RELEASE_TAG" --notes-file release-notes.md ./*.zip ./*.tar.gz ./*.exe ./*.AppImage ./*.dmg ./*.build.json ./*.sha256
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false
235 changes: 235 additions & 0 deletions .github/workflows/gcs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
name: GCS builds

on:
workflow_call:
inputs:
component:
type: string
default: all
target:
type: string
default: all
workflow_dispatch:
inputs:
component:
description: GCS packages to build
type: choice
default: all
options: [all, qgroundcontrol, mission-planner]
target:
description: QGroundControl platform
type: choice
default: all
options: [all, linux-amd64, windows-amd64, macos-universal]
pull_request:
paths:
- '.github/workflows/gcs.yml'
- '.github/workflows/desktop.yml'
- 'build/gcs-versions.json'
- 'cmd/mission-planner-plugin/**'
- 'go.mod'
- 'go.sum'
- 'frontend/package.json'
- 'integrations/**'
- 'internal/adapter/**'
- 'scripts/*gcs*'
- 'scripts/*qgc*'
- 'scripts/*mission-planner*'
- 'scripts/*mission_planner*'

permissions:
contents: read
actions: read

jobs:
pins:
if: inputs.component != 'mission-planner'
runs-on: ubuntu-24.04
outputs:
qgc-ref: ${{ steps.pins.outputs.qgc-ref }}
qgc-repository: ${{ steps.pins.outputs.qgc-repository }}
qgc-matrix: ${{ steps.pins.outputs.qgc-matrix }}
steps:
- uses: actions/checkout@v4
- id: pins
env:
QGC_TARGET: ${{ inputs.target || 'all' }}
run: |
echo "qgc-ref=$(jq -r .qgroundcontrol.commit build/gcs-versions.json)" >> "$GITHUB_OUTPUT"
echo "qgc-repository=$(jq -r .qgroundcontrol.repository build/gcs-versions.json)" >> "$GITHUB_OUTPUT"
matrix=$(jq -cn --arg target "$QGC_TARGET" '[
{runner:"ubuntu-24.04",host:"linux",arch:"linux_gcc_64",target:"linux-amd64"},
{runner:"windows-2022",host:"windows",arch:"win64_msvc2022_64",target:"windows-amd64"},
{runner:"macos-15",host:"mac",arch:"clang_64",target:"macos-universal"}
] | map(select($target == "all" or .target == $target)) | {include: .}')
test "$(echo "$matrix" | jq '.include | length')" -gt 0
echo "qgc-matrix=$matrix" >> "$GITHUB_OUTPUT"

qgroundcontrol:
name: QGroundControl (${{ matrix.target }})
needs: pins
if: inputs.component != 'mission-planner'
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.pins.outputs.qgc-matrix) }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 180
defaults:
run:
shell: bash
steps:
# Upstream composite actions expect QGC at GITHUB_WORKSPACE, including nested actions.
- uses: actions/checkout@v4
with:
repository: ${{ needs.pins.outputs.qgc-repository }}
ref: ${{ needs.pins.outputs.qgc-ref }}
submodules: recursive
fetch-depth: 0
persist-credentials: false
- uses: actions/checkout@v4
with:
path: adapter
persist-credentials: false
- name: Verify pinned upstream and prepare minimal overlay
env:
QGC_REF: ${{ needs.pins.outputs.qgc-ref }}
run: |
test "$(git rev-parse HEAD)" = "$QGC_REF"
python3 adapter/scripts/prepare-qgc.py .
python3 -m unittest discover -s adapter/scripts -p 'test_*.py'
- name: Set up upstream Qt, compiler and build tools
uses: ./.github/actions/build-setup
with:
qt-host: ${{ matrix.host }}
qt-arch: ${{ matrix.arch }}
build-type: Release
cache-variant: openutm
aqt-source: git+https://github.com/miurahr/aqtinstall.git@8d961e61720b3c06583517fbc68d57ec0a4e9f95
- uses: ./.github/actions/install-dependencies
with:
nsis: ${{ runner.os == 'Windows' && 'true' || 'false' }}
# Dependency setup may create a venv containing only CI tools. CMake
# reuses it, so install generator dependencies explicitly before configure.
- name: Install upstream source generator dependencies
run: |
python3 tools/setup/install_python.py scripts,ci
if [[ "$RUNNER_OS" == "Windows" ]]; then
venv_python=.venv/Scripts/python.exe
else
venv_python=.venv/bin/python
fi
"$venv_python" -c 'import defusedxml, httpx, jinja2, ninja'
"$venv_python" -m pip --version
ninja --version
- name: Test native restriction map
env:
QSG_RHI_BACKEND: software
run: |
qmllint adapter/integrations/qgroundcontrol/AdapterRestrictions.qml
qmltestrunner -platform offscreen -input adapter/integrations/qgroundcontrol/tests
- uses: ./.github/actions/cmake-configure
# The pinned upstream's automatic check ignores Python version markers,
# then syncs only scripts and removes Ninja. We install and check both
# dependency groups above, so keep that environment intact in CMake.
with:
build-dir: ${{ runner.temp }}/build
stable: 'true'
extra-args: >-
-DQGC_CUSTOM_DIR=custom-openutm-adapter
-DQGC_BUILD_TESTING=OFF
-DQGC_MACOS_SIGN_WITH_IDENTITY=OFF
-DQGC_AUTO_PYTHON_VENV=OFF
- uses: ./.github/actions/cmake-build
with:
build-dir: ${{ runner.temp }}/build
parallel-jobs: '3'
- uses: ./.github/actions/cmake-install
with:
build-dir: ${{ runner.temp }}/build
- name: Install Windows package for smoke test
if: runner.os == 'Windows'
shell: pwsh
run: |
$installers = @(Get-ChildItem "$env:RUNNER_TEMP/build" -Filter '*installer*.exe')
if ($installers.Count -ne 1) { throw 'Expected exactly one QGC installer' }
# NSIS /D requires a native path; forward slashes can be discarded.
$destination = Join-Path $env:RUNNER_TEMP 'qgc-install-test'
$process = Start-Process $installers[0].FullName -ArgumentList '/S', "/D=$destination" -Wait -PassThru
if ($process.ExitCode -ne 0) { throw "Installer failed: $($process.ExitCode)" }
if (!(Test-Path (Join-Path $destination 'bin\QGroundControl.exe'))) { throw "Installed QGC executable missing from $destination" }
- name: Mount macOS package for smoke test
if: runner.os == 'macOS'
run: hdiutil attach "$RUNNER_TEMP/build/QGroundControl.dmg" -mountpoint "$RUNNER_TEMP/qgc-dmg" -nobrowse
- name: Boot packaged QGC and verify custom restriction feed
env:
QGC_BINARY: ${{ runner.os == 'macOS' && format('{0}/qgc-dmg/QGroundControl.app/Contents/MacOS/QGroundControl', runner.temp) || runner.os == 'Windows' && format('{0}/qgc-install-test/bin/QGroundControl.exe', runner.temp) || format('{0}/build/QGroundControl-x86_64.AppImage', runner.temp) }}
QGC_ARCHS: ${{ runner.os == 'macOS' && 'x86_64h arm64' || '' }}
run: python3 adapter/scripts/test_qgc_package.py --qgc-source . --binary "$QGC_BINARY" --expect-archs "$QGC_ARCHS"
- name: Stage packages, provenance and checksums
env:
TARGET: ${{ matrix.target }}
ADAPTER_COMMIT: ${{ github.sha }}
run: |
case "$RUNNER_OS" in
macOS) package="$RUNNER_TEMP/build/QGroundControl.dmg" ;;
Linux) package="$RUNNER_TEMP/build/QGroundControl-x86_64.AppImage" ;;
Windows) package=$(find "$RUNNER_TEMP/build" -maxdepth 1 -iname '*installer*.exe' -print -quit) ;;
esac
python3 adapter/scripts/package-gcs.py --component qgroundcontrol --target "$TARGET" --input "$package" --output adapter/bin/gcs --adapter-commit "$ADAPTER_COMMIT"
- name: Unmount macOS package
if: always() && runner.os == 'macOS'
run: hdiutil detach "$RUNNER_TEMP/qgc-dmg" || true
- name: Verify portable download checksums
run: cd adapter/bin/gcs && sha256sum --check ./*.sha256
- uses: actions/upload-artifact@v4
with:
name: utm-adapter-gcs-qgroundcontrol-${{ matrix.target }}
path: adapter/bin/gcs/*
if-no-files-found: error
retention-days: 30

mission-planner:
name: Mission Planner plugin (Windows x64)
if: inputs.component != 'qgroundcontrol'
runs-on: windows-2025
timeout-minutes: 30
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Test package generation
run: python -m unittest discover -s scripts -p 'test_*.py'
- name: Download and verify pinned Mission Planner release
run: |
$pin = (Get-Content build/gcs-versions.json -Raw | ConvertFrom-Json).missionPlanner
$archive = "$env:RUNNER_TEMP/MissionPlanner.zip"
Invoke-WebRequest $pin.url -OutFile $archive
if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLower() -ne $pin.sha256) { throw 'Mission Planner checksum mismatch' }
Expand-Archive $archive -DestinationPath "$env:RUNNER_TEMP/MissionPlanner"
- name: Build release plugin using production generator
run: ./scripts/build-mission-planner.ps1 -MissionPlannerDirectory "$env:RUNNER_TEMP/MissionPlanner" -OutputDirectory bin/mission-planner
- name: Package plugin and source with provenance
env:
ADAPTER_COMMIT: ${{ github.sha }}
run: |
python scripts/package-gcs.py --component missionPlanner --target windows-amd64 --input bin/mission-planner --output bin/gcs --adapter-commit "$env:ADAPTER_COMMIT"
if ($LASTEXITCODE -ne 0) { throw 'Plugin packaging failed' }
- name: Load and initialize the packaged DLL against Mission Planner
run: |
$archive = @(Get-ChildItem bin/gcs -Filter '*.zip')
if ($archive.Count -ne 1) { throw 'Expected exactly one plugin archive' }
powershell.exe -NoProfile -File scripts/test_mission_planner_package.ps1 -Archive $archive[0].FullName -MissionPlannerDirectory "$env:RUNNER_TEMP/MissionPlanner"
if ($LASTEXITCODE -ne 0) { throw 'Packaged plugin integration test failed' }
- name: Verify portable download checksums
shell: bash
run: cd bin/gcs && sha256sum --check ./*.sha256
- uses: actions/upload-artifact@v4
with:
name: utm-adapter-gcs-mission-planner-windows-amd64
path: bin/gcs/*
if-no-files-found: error
retention-days: 30
Loading
Loading