Skip to content

ci: keep fork PRs off the self-hosted runners #273

ci: keep fork PRs off the self-hosted runners

ci: keep fork PRs off the self-hosted runners #273

Workflow file for this run

# SPDX-License-Identifier: Apache-2.0
name: CI
on:
push:
pull_request:
# The CI image package is public, so no packages scope is needed to pull it.
permissions:
contents: read
jobs:
build-test:
# Fork PRs must never reach the persistent host: approving a fork PR is a
# gate, not an isolation boundary, and approved code would run here as the
# runner user. Forks go to a throwaway GitHub-hosted VM instead.
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || fromJSON('["self-hosted", "libmisbklv"]') }}
container:
# Pinned to an immutable commit tag, never :latest -- a queued run must
# get the image it was queued against, and the publisher workflow runs
# independently of this one. Bump this line when ci/Dockerfile changes,
# after CI image has published the new tag. The package is public, so
# no registry credentials are needed here.
image: ghcr.io/nitsuga/libmisbklv-ci:9eeeeae9701449e06eee671c40b00fe4892888a7
# On the host the workspace is bind-mounted and shared with native jobs,
# so a root container leaves root-owned files the next checkout cannot
# clean. A hosted VM is throwaway and expects the default root.
options: ${{ github.event.pull_request.head.repo.fork && ' ' || '--user 1000:1000' }}
# Compiler cache shared by every job on this host. ccache is safe under
# concurrent access, so parallel jobs warm one cache instead of three.
# A fork PR gets a throwaway path inside its own VM.
volumes:
- ${{ github.event.pull_request.head.repo.fork && '/tmp/ccache:/ccache' || '/home/eric/ci-cache/ccache:/ccache' }}
# A hung test otherwise runs to GitHub's 6-hour cap: gst_video_insert did
# exactly that, nine times over, before finish() grew a drain timeout.
timeout-minutes: 20
steps:
- uses: actions/checkout@v5
- name: Configure
run: cmake --preset release
- name: Build
run: cmake --build --preset release -j3
- name: ccache stats
run: ccache -s
- name: Test
# --timeout names the test that hung; the job timeout above is the backstop.
run: ctest --test-dir build/release --output-on-failure --timeout 600
- name: Install
run: cmake --install build/release --prefix "$PWD/_install"
- name: Consumer smoke test (find_package, core + gst component)
# Builds a throwaway out-of-tree project against the install — catches
# export/packaging gaps that a bare `cmake --install` cannot.
run: |
mkdir -p /tmp/consumer
cat > /tmp/consumer/CMakeLists.txt <<'EOF'
cmake_minimum_required(VERSION 3.20)
project(consumer CXX)
set(CMAKE_CXX_STANDARD 20)
find_package(misbklv REQUIRED COMPONENTS gst)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE misbklv::gst) # transitively: core
EOF
cat > /tmp/consumer/main.cpp <<'EOF'
// Construct both facade types with the in-memory backend so this
// remains a link/ABI check without opening a real source or sink.
#include <memory>
#include "misbklv/gst_backend.hpp"
#include "misbklv/mock_backend.hpp"
#include "misbklv/stream.hpp"
int main() {
auto gst_backend = misbklv::make_gst_backend();
misbklv::KlvStream stream(std::make_unique<misbklv::MockBackend>(), "unused");
misbklv::KlvSink sink(std::make_unique<misbklv::MockBackend>(), misbklv::InsertConfig{});
(void)stream.error();
(void)sink.error();
(void)sink.poll();
(void)sink.close();
return gst_backend ? 0 : 1;
}
EOF
cmake -S /tmp/consumer -B /tmp/consumer/build -DCMAKE_PREFIX_PATH="$PWD/_install"
cmake --build /tmp/consumer/build -j3
sanitizers:
timeout-minutes: 20
# Real-world hardening: the core parse/codec paths must be free of UB / OOB
# on adversarial input. gstreamer is off so ASan isn't noised by gst internals.
# Fork PRs must never reach the persistent host: approving a fork PR is a
# gate, not an isolation boundary, and approved code would run here as the
# runner user. Forks go to a throwaway GitHub-hosted VM instead.
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || fromJSON('["self-hosted", "libmisbklv"]') }}
container:
# Pinned to an immutable commit tag, never :latest -- a queued run must
# get the image it was queued against, and the publisher workflow runs
# independently of this one. Bump this line when ci/Dockerfile changes,
# after CI image has published the new tag. The package is public, so
# no registry credentials are needed here.
image: ghcr.io/nitsuga/libmisbklv-ci:9eeeeae9701449e06eee671c40b00fe4892888a7
# On the host the workspace is bind-mounted and shared with native jobs,
# so a root container leaves root-owned files the next checkout cannot
# clean. A hosted VM is throwaway and expects the default root.
options: ${{ github.event.pull_request.head.repo.fork && ' ' || '--user 1000:1000' }}
# Compiler cache shared by every job on this host. ccache is safe under
# concurrent access, so parallel jobs warm one cache instead of three.
# A fork PR gets a throwaway path inside its own VM.
volumes:
- ${{ github.event.pull_request.head.repo.fork && '/tmp/ccache:/ccache' || '/home/eric/ci-cache/ccache:/ccache' }}
steps:
- uses: actions/checkout@v5
- name: Configure (ASan + UBSan, no gstreamer)
run: cmake --preset sanitize
- name: Build
run: cmake --build --preset sanitize -j3
- name: Test under sanitizers
run: ctest --test-dir build/sanitize --output-on-failure --timeout 600
env:
UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1
ASAN_OPTIONS: abort_on_error=1
generated-drift:
timeout-minutes: 10
# ADRs 0012/0028: committed generated outputs must match fresh regeneration.
# Fork PRs must never reach the persistent host: approving a fork PR is a
# gate, not an isolation boundary, and approved code would run here as the
# runner user. Forks go to a throwaway GitHub-hosted VM instead.
runs-on: ${{ github.event.pull_request.head.repo.fork && 'ubuntu-latest' || fromJSON('["self-hosted", "libmisbklv"]') }}
# No container: this job needs only python3 and git, both present on the
# host and on ubuntu-latest. It also runs PR-supplied generator scripts
# directly, which is exactly why it must not land on the persistent host
# for a fork.
steps:
- uses: actions/checkout@v5
- name: Regenerate committed outputs
run: |
for r in uas0601 vmti0903 vtarget0903; do
python3 tools/gen_registry.py \
"registry/$r.toml" "include/misbklv/registry/${r}_tables.generated.hpp"
done
python3 test/fixtures/generate_synthetic_fixtures.py \
registry/uas0601.toml test/fixtures
- name: Fail on drift
run: git diff --exit-code -- include/misbklv/registry test/fixtures