Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
82 changes: 14 additions & 68 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,10 @@

on:
pull_request:
branches: [main]
push:
branches:
- main

concurrency:
group: regression-${{ github.ref }}
cancel-in-progress: true

# Least-privilege token: only reading code. Jobs that need more (e.g. GHA
# cache reads/writes from docker/build-push-action with `type=gha`) elevate
# their own permissions inline.
permissions:
contents: read

jobs:
changes:
name: Detect changes
Expand All @@ -36,132 +25,89 @@
- "packages/engine/**"
- "Dockerfile*"

# Build the regression Docker image once, export it as a tarball, and upload
# as an artifact. Each matrix shard then downloads + `docker load`s it instead
# of rebuilding from cache. Measured on PR #419: the Docker build step takes
# ~4 min per shard even with GHA cache, so 11 shards = ~44 min of redundant
# build time per run. This job replaces that with a single ~4 min build plus
# ~15s of artifact download per shard.
build-image:
name: Build regression test image
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
actions: write # docker/build-push-action `type=gha` cache reads + writes
steps:
- name: Checkout
uses: actions/checkout@v4
# No LFS needed here — Dockerfile.test only copies source + package manifests,
# not the golden baselines under packages/producer/tests/**/output.

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build test image to tarball
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.test
tags: hyperframes-producer:test
cache-from: type=gha,scope=regression-test-image
cache-to: type=gha,mode=max,scope=regression-test-image
outputs: type=docker,dest=/tmp/regression-test-image.tar

- name: Report image size
run: ls -lh /tmp/regression-test-image.tar

- name: Upload image artifact
uses: actions/upload-artifact@v4
with:
name: regression-test-image
path: /tmp/regression-test-image.tar
retention-days: 1
compression-level: 1

regression-shards:
needs: [changes, build-image]
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 40
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- shard: fast
args: "--sequential --exclude-tags slow,render-compat,hdr"
- shard: render-compat
args: "--sequential gsap-letters-render-compat css-spinner-render-compat raf-ball-render-compat iframe-render-compat"
- shard: hdr
args: "--sequential hdr-pq hdr-image-only"
args: "--sequential hdr-regression hdr-hlg-regression"
- shard: styles-a
args: "style-1-prod style-2-prod style-3-prod"
- shard: styles-b
args: "style-4-prod style-5-prod style-6-prod"
- shard: styles-c
args: "style-7-prod style-8-prod style-9-prod"
- shard: styles-d
args: "style-10-prod style-11-prod style-12-prod"
- shard: styles-e
args: "style-13-prod style-15-prod style-16-prod"
- shard: styles-f
args: "style-17-prod style-18-prod"
- shard: styles-g
args: "overlay-montage-prod"
steps:
- name: Checkout (with LFS)
uses: actions/checkout@v4
with:
lfs: true

- name: Validate LFS files
run: |
echo "Checking golden baseline MP4s are real files (not LFS pointers)..."
for mp4 in packages/producer/tests/*/output/output.mp4; do
if [ -f "$mp4" ]; then
size=$(stat --format=%s "$mp4")
if [ "$size" -lt 1000 ]; then
echo "ERROR: $mp4 appears to be an LFS pointer ($size bytes)"
exit 1
fi
echo "OK: $mp4 ($size bytes)"
fi
done

- name: Download test image artifact
uses: actions/download-artifact@v4
with:
name: regression-test-image
path: /tmp
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Load test image
run: |
docker load -i /tmp/regression-test-image.tar
docker image ls hyperframes-producer:test
- name: Build test Docker image (cached)
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile.test
load: true
tags: hyperframes-producer:test
cache-from: type=gha,scope=regression-test-image
cache-to: type=gha,mode=min,scope=regression-test-image

- name: "Run regression shard: ${{ matrix.shard }}"
run: |
echo "Shard: ${{ matrix.shard }}"
echo "Args: ${{ matrix.args }}"
docker run --rm \
--security-opt seccomp=unconfined \
--shm-size=4g \
-v ${{ github.workspace }}/packages/producer/tests:/app/packages/producer/tests \
hyperframes-producer:test \
${{ matrix.args }}

- name: Upload failure artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: regression-failures-${{ matrix.shard }}
path: packages/producer/tests/*/failures/
if-no-files-found: ignore

# Summary job — matches the required check name in branch protection
regression:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
runs-on: ubuntu-latest
needs: [changes, regression-shards]
if: always()
Expand Down
4 changes: 2 additions & 2 deletions packages/engine/src/utils/ffprobe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe("extractVideoMetadata", () => {
it("reads HDR PNG cICP metadata when ffprobe color fields are absent", async () => {
const fixturePath = resolve(
__dirname,
"../../../producer/tests/hdr-image-only/src/hdr-photo.png",
"../../../producer/tests/hdr-regression/src/hdr-photo-pq.png",
);

const metadata = await extractVideoMetadata(fixturePath);
Expand Down Expand Up @@ -102,7 +102,7 @@ describe("extractPngMetadataFromBuffer", () => {

it("continues to parse the checked-in HDR PNG fixture", () => {
const fixture = readFileSync(
resolve(__dirname, "../../../producer/tests/hdr-image-only/src/hdr-photo.png"),
resolve(__dirname, "../../../producer/tests/hdr-regression/src/hdr-photo-pq.png"),
);
expect(extractPngMetadataFromBuffer(fixture)?.colorSpace?.colorTransfer).toBe("smpte2084");
});
Expand Down
52 changes: 52 additions & 0 deletions packages/producer/tests/hdr-hlg-regression/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# hdr-hlg-regression

Regression test that locks down end-to-end **HDR HLG (BT.2020 ARIB STD-B67)**
video rendering. Companion to `hdr-regression` (PQ), kept as a separate suite
so the HLG-specific encoder/metadata path stays tested in isolation.

## What it covers

| Window | Time | Shape | Expected |
| ------ | ------------ | ------------------------------------- | -------- |
| A | 0.0 – 2.5 s | Baseline HLG video + DOM overlay | pass |
| B | 2.5 – 5.0 s | Wrapper opacity fade around HLG video | pass |

The test pins the contract that:

- `extractVideoMetadata` reports `bt2020/arib-std-b67/limited` for the HLG
source (i.e. HLG is detected and not silently coerced to PQ).
- `isHdrColorSpace` flips the orchestrator into the layered HDR path on the
HLG signal.
- The HLG source is decoded into `rgb48le` and blitted under the SDR DOM
overlay on every frame.
- Wrapper-opacity composition (window B) does not break HLG pass-through.
- `hdrEncoder` writes HEVC Main10 / `yuv420p10le` / BT.2020 HLG with the
correct color tags (no PQ mastering display metadata for HLG).

The suite is intentionally short (5 s, two windows) — it exists to detect
regressions in the HLG-specific code path, not to enumerate every composition
shape (those live in `hdr-regression`).

## Tolerance

`maxFrameFailures` is **0** here. HLG is a pure pass-through path — no known
failures, no transcoder workarounds — and HEVC encoding against the rendered
`rgb48le` buffer is byte-deterministic on the same fixture. Any drift is a
real regression, not codec noise, so the budget is the strictest possible.

## Fixture

`src/hdr-hlg-clip.mp4` — last 5 seconds of a user-recorded HEVC HLG clip,
remuxed (no re-encode) so the HLG color tags survive verbatim.

## Running

```bash
cd packages/producer
bun run test hdr-hlg-regression

bun run test:update hdr-hlg-regression
```

In CI it runs in the `hdr` shard alongside `hdr-regression`
(see `.github/workflows/regression.yml`).
14 changes: 14 additions & 0 deletions packages/producer/tests/hdr-hlg-regression/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "hdr-hlg-regression",
"description": "Regression test for HDR HLG (BT.2020 ARIB STD-B67) video pass-through. Two windows: (A) baseline HLG video + SDR DOM overlay, (B) wrapper-opacity fade applied to an HLG video. Verifies that an HLG HEVC source drives the layered HDR pipeline end-to-end (extractVideoMetadata reports hlg, ffmpegFrameSource decodes correctly, hdrEncoder writes HEVC Main10 / yuv420p10le / BT.2020 HLG with the appropriate mastering metadata) and that opacity composition does not break HLG signal pass-through.",
"tags": ["regression", "hdr"],
"minPsnr": 28,
"maxFrameFailures": 0,
"minAudioCorrelation": 0,
"maxAudioLagWindows": 1,
"renderConfig": {
"fps": 30,
"workers": 1,
"hdr": true
}
}

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/producer/tests/hdr-hlg-regression/output/output.mp4
Git LFS file not shown
Git LFS file not shown
112 changes: 112 additions & 0 deletions packages/producer/tests/hdr-hlg-regression/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>HDR HLG Regression Suite</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
background: #000;
}

#main {
position: relative;
width: 1920px;
height: 1080px;
overflow: hidden;
background: #000;
}

.hdr-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}

.label {
position: absolute;
padding: 14px 22px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 36px;
font-weight: 700;
color: #ffffff;
background: rgba(0, 0, 0, 0.6);
border-radius: 8px;
letter-spacing: 0.04em;
z-index: 100;
}

.label-tl {
top: 48px;
left: 64px;
}

/* Window B: opacity-tweenable wrapper around the HLG video */
#window-b-wrapper {
position: absolute;
inset: 0;
opacity: 1;
}
</style>
</head>
<body>
<div
id="main"
data-composition-id="hdr-hlg-regression"
data-start="0"
data-duration="5"
data-width="1920"
data-height="1080"
>
<!-- Window A · Static baseline HLG · 0.0–2.5s -->
<video
id="wa-video"
class="clip hdr-video"
data-start="0"
data-duration="2.5"
data-track-index="0"
src="hdr-hlg-clip.mp4"
muted
playsinline
></video>
<div class="label label-tl clip" data-start="0" data-duration="2.5">
A · HLG baseline + DOM overlay
</div>

<!-- Window B · Wrapper opacity fade on HLG · 2.5–5.0s -->
<div id="window-b-wrapper">
<video
id="wb-video"
class="clip hdr-video"
data-start="2.5"
data-duration="2.5"
data-track-index="0"
src="hdr-hlg-clip.mp4"
muted
playsinline
></video>
</div>
<div class="label label-tl clip" data-start="2.5" data-duration="2.5">
B · HLG wrapper opacity fade
</div>
</div>

<script>
window.__timelines = window.__timelines || {};

const tl = gsap.timeline({ paused: true });

// Window B · wrapper opacity 1 → 0.15 → 1 inside the 2.5s window
tl.to("#window-b-wrapper", { opacity: 0.15, duration: 1.0, ease: "power2.inOut" }, 2.75);
tl.to("#window-b-wrapper", { opacity: 1.0, duration: 1.0, ease: "power2.inOut" }, 3.75);

window.__timelines["hdr-hlg-regression"] = tl;
</script>
</body>
</html>
49 changes: 0 additions & 49 deletions packages/producer/tests/hdr-image-only/README.md

This file was deleted.

14 changes: 0 additions & 14 deletions packages/producer/tests/hdr-image-only/meta.json

This file was deleted.

72 changes: 0 additions & 72 deletions packages/producer/tests/hdr-image-only/output/compiled.html

This file was deleted.

3 changes: 0 additions & 3 deletions packages/producer/tests/hdr-image-only/output/output.mp4

This file was deleted.

Loading
Loading