ci: keep fork PRs off the self-hosted runners #273
Workflow file for this run
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
| # 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 |