Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
35bb48a
docs(test): spec for boot & OTA integration test harness
peterus Jul 11, 2026
c823a84
docs(test): fold pipeline rework into boot/OTA test spec
peterus Jul 11, 2026
c737094
docs(test): design Target interface CM4-ready; add bench mechanics
peterus Jul 11, 2026
6202803
docs(test): finalize CM4 flash-backend cost tiers + eMMC/Lite fidelity
peterus Jul 11, 2026
e463069
docs(test): implementation plan for boot & OTA integration test
peterus Jul 11, 2026
54a37eb
test(ota-it): scaffold + dummy OTA server + image/seed helpers (unit-…
peterus Jul 11, 2026
956faee
test(ota-it): Target interface, L0a UUID lint, compute-version (unit-…
peterus Jul 11, 2026
5f66c18
test(ota-it): L0b build assertion + T1/T2 qemu tests (+T3 skipped fol…
peterus Jul 11, 2026
a03136f
ci: L0a on PRs, boot-critical PR test, reusable gate, one-button release
peterus Jul 11, 2026
aeacdb9
test(ota-it): install grub2-common in CI + tolerate missing grub-editenv
peterus Jul 11, 2026
1de0a98
ci: shellcheck only the new scripts, not the whole scripts/ dir
peterus Jul 12, 2026
c33a6fb
fix(ci): L0a scans only git-tracked files, not kas-fetched layers
peterus Jul 12, 2026
6ea2f66
ci: run T2 (cross-build OTA) on boot-critical PRs too
peterus Jul 12, 2026
fcf7579
ci: gate boot-ota-pr on non-draft PRs
peterus Jul 12, 2026
ed616b5
Address Copilot review (round 1)
peterus Jul 12, 2026
4599cce
Address Copilot review (round 2)
peterus Jul 12, 2026
bd4c544
Address Copilot review (round 3)
peterus Jul 12, 2026
b2a0f2c
Address Copilot review (round 4)
peterus Jul 12, 2026
01f81b7
Address Copilot review (round 5)
peterus Jul 12, 2026
c4ae9c4
Address Copilot review (round 6)
peterus Jul 12, 2026
60eff50
fix(ota-it): banner regex captures only the tag charset, not trailing…
peterus Jul 12, 2026
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
25 changes: 25 additions & 0 deletions .github/actions/compute-version/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Compute release version
description: >
Compute the next OE5XRX linux-image release version (YYYY.MM.DD-HH[a-z], UTC
hour, letter suffix on collision) from existing git tags. Mirrors the FW
one-button-release pattern.

outputs:
version:
description: The computed release version.
value: ${{ steps.compute.outputs.version }}

runs:
using: composite
steps:
- name: Fetch tags
shell: bash
run: git fetch --tags --quiet

- name: Compute
id: compute
shell: bash
run: |
version="$(scripts/compute-release-version.sh)"
echo "Computed release version: ${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"
71 changes: 71 additions & 0 deletions .github/workflows/boot-ota-pr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Boot/OTA (boot-critical PRs)

# Shift-left: on PRs that touch boot/OTA-critical paths, build the x86 image and
# run BOTH the boot smoke (T1) and the cross-build OTA cycle (T2) — slot A = the
# latest published release, slot B = this PR's build. The release gate runs the
# same test again as the publish barrier. If no release exists yet (bootstrap),
# T2 skips gracefully and only T1 runs.

# Draft PRs do NOT trigger the (expensive) Hetzner build + QEMU test — so
# Copilot-loop / WIP pushes on a draft don't re-run everything. Mark the PR
# ready for review to run it (that also fires `ready_for_review`).
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'meta-oe5xrx-remotestation/wic/**'
- 'meta-oe5xrx-remotestation/recipes-bsp/**'
- 'meta-oe5xrx-remotestation/recipes-core/ab-layout/**'
- 'meta-oe5xrx-remotestation/recipes-core/images/**'
- 'meta-oe5xrx-remotestation/recipes-core/station-agent/**'
- 'tests/ota-integration/**'
- '.github/workflows/boot-ota-test.yml'
- '.github/workflows/boot-ota-pr.yml'

permissions:
contents: read
actions: read # download-artifact from the build job in the same run

concurrency:
group: boot-ota-pr-${{ github.ref }}
cancel-in-progress: true

jobs:
resolve-slot-a:
name: Resolve slot A (last release)
if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.last.outputs.tag }}
steps:
- name: Latest published release
id: last
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
tag=$(gh release list -R "$REPO" -L 1 --exclude-drafts --exclude-pre-releases --json tagName -q '.[0].tagName' || true)
echo "Slot A (last release): ${tag:-<none — T2 will skip>}"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
Comment thread
peterus marked this conversation as resolved.

build-x64:
name: Build qemux86-64 (dev)
if: ${{ !github.event.pull_request.draft }}
uses: ./.github/workflows/build.yml
with:
machine: qemux86-64
release_tag: ""
secrets: inherit

test:
name: Boot + cross-build OTA (T1+T2)
if: ${{ !github.event.pull_request.draft }}
needs: [build-x64, resolve-slot-a]
uses: ./.github/workflows/boot-ota-test.yml
with:
machine: qemux86-64
new_artifact: yocto-image-qemux86-64
expected_tag: dev
last_release_tag: ${{ needs.resolve-slot-a.outputs.tag }}
secrets: inherit
89 changes: 89 additions & 0 deletions .github/workflows/boot-ota-spike.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Boot/OTA Feasibility Spike

# One-off decision aid: does a GH-hosted ubuntu-latest runner boot the image
# under TCG (no KVM) in acceptable wall-clock and disk? Boots the latest (or a
# given) release via the T1 harness and reports timing + RAM + disk headroom.
# If this is too slow / disk-tight, flip boot-ota-test.yml's runner_label to a
# self-hosted Hetzner label.

on:
workflow_dispatch:
inputs:
release_tag:
description: Release to boot (empty → latest).
type: string
default: ""

permissions:
contents: read # gh release download

jobs:
spike:
name: TCG boot timing
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v6

- name: Host capacity (before)
run: |
echo "== nproc =="; nproc
echo "== free -m =="; free -m
echo "== df -h / =="; df -h /
echo "== /dev/kvm present? =="; ls -l /dev/kvm || echo "no KVM (expected → TCG)"

- name: Install QEMU + OVMF + deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends qemu-system-x86 ovmf grub-common grub2-common python3-pip
sudo python3 -m pip install --break-system-packages -r tests/ota-integration/requirements.txt

- name: Resolve release tag
id: tag
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
REQ_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
tag="$REQ_TAG"
if [ -z "$tag" ]; then
tag=$(gh release list -R "$REPO" -L 1 --exclude-drafts --exclude-pre-releases --json tagName -q '.[0].tagName')
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"

- name: Download release wic
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
gh release download "$TAG" -R "$REPO" \
--pattern "*qemux86-64*.wic.bz2" \
--pattern "*qemux86-64*.wic.xz" \
--pattern "*qemux86-64*.wic.gz" \
--pattern "*qemux86-64*.wic" \
-D spike

- name: Timed TCG boot (T1)
env:
OTA_IT_WIC: spike
OTA_IT_EXPECTED_TAG: ${{ steps.tag.outputs.tag }}
run: |
set -euo pipefail
shopt -s nullglob
cands=(spike/*.wic.bz2 spike/*.wic.xz spike/*.wic.gz spike/*.wic)
OTA_IT_WIC="${cands[0]:-}"
[ -n "$OTA_IT_WIC" ] || { echo "no wic asset downloaded" >&2; exit 1; }
export OTA_IT_WIC
start=$(date +%s)
sudo -E env "PATH=$PATH" \
python3 -m pytest tests/ota-integration/test_boot.py -m qemu -v
end=$(date +%s)
echo "T1 boot wall-clock: $((end - start))s"

- name: Host capacity (after)
run: |
echo "== free -m =="; free -m
echo "== df -h / =="; df -h /
98 changes: 98 additions & 0 deletions .github/workflows/boot-ota-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: Boot & OTA Test

# Reusable boot + cross-build OTA integration test. Boots the built image in
# QEMU (TCG), drives the real station-agent against a dummy OTA server, and
# asserts a serial banner+login AND an agent check-in/commit. Called by the
# boot-critical PR workflow (T1) and by the release gate (T1+T2).
#
# Runner is a parameter: default GH-hosted ubuntu-latest (TCG). If a feasibility
# spike shows GH-hosted is insufficient, pass a self-hosted Hetzner label.

on:
workflow_call:
inputs:
machine:
type: string
default: qemux86-64
runner_label:
type: string
default: ubuntu-latest
new_artifact:
description: Artifact name of the build-under-test wic (same run).
type: string
required: true
expected_tag:
description: Version stamped into the build-under-test (banner + os-release).
type: string
required: true
last_release_tag:
description: Previous release tag = slot A for T2 (empty → T1 only).
type: string
default: ""

jobs:
test:
name: Boot & OTA (${{ inputs.machine }})
runs-on: ${{ inputs.runner_label }}
timeout-minutes: 60
steps:
- uses: actions/checkout@v6

- name: Install QEMU + OVMF + deps
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends qemu-system-x86 ovmf grub-common grub2-common python3-pip
sudo python3 -m pip install --break-system-packages -r tests/ota-integration/requirements.txt

- name: Download build-under-test artifact
uses: actions/download-artifact@v8
with:
name: ${{ inputs.new_artifact }}
path: it-artifacts

- name: Locate build-under-test wic
id: wic
run: |
set -euo pipefail
shopt -s nullglob
# Multiple patterns via an array so a non-matching glob can't fail the
# step under `set -e` when another pattern does match.
cands=(it-artifacts/*.wic.bz2 it-artifacts/*.wic.xz it-artifacts/*.wic.gz it-artifacts/*.rootfs.wic it-artifacts/*.wic)
wic="${cands[0]:-}"
[ -n "$wic" ] || { echo "no wic artifact found" >&2; exit 1; }
echo "wic=$wic" >> "$GITHUB_OUTPUT"
Comment thread
peterus marked this conversation as resolved.

- name: Fetch last release (slot A)
id: slota
if: inputs.last_release_tag != ''
env:
GH_TOKEN: ${{ github.token }}
LAST_RELEASE_TAG: ${{ inputs.last_release_tag }}
MACHINE: ${{ inputs.machine }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# Accept the same compression set release.yml may publish, in the same
# extension order; sidecars (.sha256/.bundle) don't match these globs.
gh release download "$LAST_RELEASE_TAG" -R "$REPO" \
--pattern "*${MACHINE}*.wic.bz2" \
--pattern "*${MACHINE}*.wic.xz" \
--pattern "*${MACHINE}*.wic.gz" \
--pattern "*${MACHINE}*.wic" \
-D slota
shopt -s nullglob
cands=(slota/*.wic.bz2 slota/*.wic.xz slota/*.wic.gz slota/*.wic)
wic="${cands[0]:-}"
# Fail fast: last_release_tag was set, so a missing slot-A asset must
# NOT silently empty the output and make T2 skip (a false-green gate).
[ -n "$wic" ] || { echo "last_release_tag='$LAST_RELEASE_TAG' set but no slot-A wic asset was downloaded" >&2; exit 1; }
echo "wic=$wic" >> "$GITHUB_OUTPUT"

- name: Run boot + OTA integration tests
env:
OTA_IT_WIC: ${{ steps.wic.outputs.wic }}
OTA_IT_LAST_RELEASE_WIC: ${{ steps.slota.outputs.wic }}
OTA_IT_EXPECTED_TAG: ${{ inputs.expected_tag }}
run: |
sudo -E env "PATH=$PATH" \
python3 -m pytest tests/ota-integration -m qemu -v
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ jobs:
run: |
find meta-oe5xrx-remotestation -name '*.sh' -print0 \
| xargs -0 -r shellcheck -e SC1091 -e SC2039
# New scripts added by the boot/OTA test work (lint explicitly rather
# than the whole scripts/ dir, which carries pre-existing intentional
# warnings, e.g. run-qemu.sh's word-splitting ${CPU_FLAGS}).
shellcheck -e SC1091 -e SC2039 \
scripts/l0a-fstab-uuid-lint.sh \
scripts/compute-release-version.sh \
scripts/release.sh

- name: Lint YAML
run: |
Expand All @@ -53,6 +60,16 @@ jobs:
# udev is installed in the "Install kas + linters" step above.
run: udevadm verify meta-oe5xrx-remotestation/recipes-core/oe5xrx-slot-udev/files/90-oe5xrx-slots.rules

- name: L0a — static fstab UUID-mount guard
# Fast counterpart to the L0b ROOTFS_POSTPROCESS assertion: flag any
# wks --use-uuid mount without --no-fstab-update (the #37 bug class).
run: scripts/l0a-fstab-uuid-lint.sh .

- name: OTA-integration unit tests
run: |
pip install --quiet -r tests/ota-integration/requirements.txt
python -m pytest tests/ota-integration -m unit -q

sim-harness:
runs-on: ubuntu-latest
steps:
Expand Down
Loading