name: Windows render verification permissions: contents: read # Suppress hyperframes CLI telemetry from HeyGen's own CI runs. # External users' CI continues to emit telemetry unless they set this themselves. env: HYPERFRAMES_NO_TELEMETRY: "1" # Manually triggered smoke test that renders a HyperFrames composition on a # real Windows runner. Proves the PR #336 `where ffmpeg` fix actually works # end-to-end: FFmpeg is discovered natively on Windows, Chrome is installed # and launched, frames are captured, and an MP4 is produced — without Docker # or WSL. on: pull_request: # `edited` is required so the workflow re-fires when a PR's base ref is # set back to `main` after a Graphite stack restack momentarily flips # the base off of `main`. Without it, `pull_request` triggers are not # re-evaluated on `base_ref_changed`, leaving required checks skipped # for that head SHA forever. types: [opened, synchronize, reopened, edited] branches: [main] push: branches: [main] workflow_dispatch: inputs: ref: description: "Git ref to render (branch / tag / SHA)." required: false default: "main" concurrency: group: windows-render-${{ github.ref }} cancel-in-progress: true jobs: changes: name: Detect changes runs-on: ubuntu-latest timeout-minutes: 2 outputs: code: ${{ steps.filter.outputs.code }} steps: # Force git-based change detection instead of the pull_request REST API. # The API path can fail the workflow on transient listFiles timeouts # before the Windows render jobs even start. - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 id: filter with: token: "" filters: | code: - "packages/**" - "scripts/**" - "package.json" - "bun.lock" - ".github/workflows/windows-render.yml" preflight: name: Preflight (lint + format) needs: changes if: needs.changes.outputs.code == 'true' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event.inputs.ref }} - uses: ./.github/actions/preflight render-windows: name: Render on windows-latest needs: [changes, preflight] if: needs.changes.outputs.code == 'true' || github.event_name == 'workflow_dispatch' runs-on: windows-latest timeout-minutes: 30 steps: - name: Disable Windows Defender real-time monitoring shell: pwsh continue-on-error: true run: Set-MpPreference -DisableRealtimeMonitoring $true - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event.inputs.ref }} lfs: true - name: Show platform info shell: pwsh run: | Write-Host "OS: $([System.Environment]::OSVersion.VersionString)" Write-Host "PowerShell: $($PSVersionTable.PSVersion)" Write-Host "Runner: windows-latest" # ----------------------------------------------------------------- # Install FFmpeg via the shared composite action so the install logic # stays identical between this job and `test-windows` below. See # .github/actions/install-ffmpeg-windows for why we bypass Chocolatey. # ----------------------------------------------------------------- - name: Install FFmpeg uses: ./.github/actions/install-ffmpeg-windows - name: Set FFMPEG_BIN for bun install shell: pwsh run: | $path = (Get-Command ffmpeg.exe).Source "FFMPEG_BIN=$path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append # ----------------------------------------------------------------- # Verify FFmpeg feature inventory. # # The engine shells out to a fixed set of encoders (libx264 for MP4, # libx265 for HEVC, libvpx-vp9 for WebM, prores_ks for transparent # MOV, aac for audio), muxers (mp4 / mov / webm), and demuxers # (image2pipe for streaming RGBA frames, rawvideo for HDR PQ frames, # mov,mp4 for video frame extraction). Some of these are GPL-only, # so a future build swap could silently drop one and break a code # path the canary render doesn't exercise. Fail fast here instead. # ----------------------------------------------------------------- - name: Verify FFmpeg feature inventory shell: pwsh run: | $ErrorActionPreference = 'Stop' function Assert-FfmpegFeature { param( [Parameter(Mandatory)] [string] $Listing, [Parameter(Mandatory)] [string] $Name, [Parameter(Mandatory)] [string] $Kind ) # `ffmpeg -encoders` etc. emit one feature per line as # ` `, so a whitespace boundary on # each side is enough to disambiguate (e.g. `mov` vs `movflags`). $pattern = "(^|\s)$([regex]::Escape($Name))(\s|$)" if ($Listing -notmatch $pattern) { throw "Required FFmpeg $Kind '$Name' not present in this build" } Write-Host " ok: $Kind $Name" } Write-Host "--- encoders ---" $encoders = (& ffmpeg -hide_banner -encoders 2>&1) -join "`n" foreach ($enc in @('libx264', 'libx265', 'libvpx-vp9', 'prores_ks', 'aac')) { Assert-FfmpegFeature -Listing $encoders -Name $enc -Kind 'encoder' } Write-Host "--- muxers ---" $muxers = (& ffmpeg -hide_banner -muxers 2>&1) -join "`n" foreach ($mux in @('mp4', 'mov', 'webm')) { Assert-FfmpegFeature -Listing $muxers -Name $mux -Kind 'muxer' } Write-Host "--- demuxers ---" $demuxers = (& ffmpeg -hide_banner -demuxers 2>&1) -join "`n" foreach ($dem in @('image2pipe', 'rawvideo', 'mov,mp4,m4a,3gp,3g2,mj2')) { Assert-FfmpegFeature -Listing $demuxers -Name $dem -Kind 'demuxer' } - name: Install Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - name: Install dependencies shell: pwsh # Force the pre-1.3 "hoisted" linker on Windows. Bun 1.3.x's default # "isolated" linker creates nested workspace junctions that don't # materialize reliably on Windows GHA runners — manifests as # `Cannot find package 'esbuild'` walking up from # `packages/producer/build.mjs` and `EPERM stat` on # `packages/producer/node_modules/@fontsource/*` from generators. # See oven-sh/bun#23615, #18354, #10146. Linux CI keeps isolated; # the lockfile is linker-agnostic so this flag is safe with # --frozen-lockfile. run: bun install --frozen-lockfile --linker=hoisted - name: Build all packages shell: pwsh run: bun run build # ----------------------------------------------------------------- # Prove the PR #336 fix: hyperframes doctor exercises findFFmpeg() # and whichBinary() — both must pass on Windows without workarounds. # ----------------------------------------------------------------- - name: hyperframes doctor (verifies `where ffmpeg` fix) shell: pwsh run: node packages/cli/dist/cli.js doctor - name: Scaffold canary composition shell: pwsh # init's --skip-skills flag is neutered (see init.ts); opt out of the # GitHub skills check via this env so the canary scaffold stays offline. env: HYPERFRAMES_SKIP_SKILLS: "1" run: | New-Item -ItemType Directory -Force -Path "$env:RUNNER_TEMP\windows-canary" | Out-Null cd "$env:RUNNER_TEMP\windows-canary" node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" init canary --example blank --non-interactive --skip-skills $fixtures = "$env:GITHUB_WORKSPACE\.github\workflows\fixtures" Copy-Item "$fixtures\windows-canary.html" "canary\index.html" -Force - name: Render canary composition shell: pwsh run: | cd "$env:RUNNER_TEMP\windows-canary\canary" node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" render ` --fps 30 ` --quality draft ` --workers 2 ` --output renders\canary.mp4 - name: Verify rendered MP4 shell: pwsh run: | $mp4 = "$env:RUNNER_TEMP\windows-canary\canary\renders\canary.mp4" if (-not (Test-Path $mp4)) { throw "canary.mp4 not produced" } $probe = ffprobe -v error -select_streams v:0 ` -show_entries stream=width,height,r_frame_rate -show_entries format=duration ` -of default=noprint_wrappers=1 $mp4 Write-Host $probe # Parse probe output $width = ($probe | Select-String '^width=(.+)$').Matches.Groups[1].Value $height = ($probe | Select-String '^height=(.+)$').Matches.Groups[1].Value $fps = ($probe | Select-String '^r_frame_rate=(.+)$').Matches.Groups[1].Value $duration = [double]($probe | Select-String '^duration=(.+)$').Matches.Groups[1].Value if ([int]$width -ne 1920) { throw "expected 1920 width, got $width" } if ([int]$height -ne 1080) { throw "expected 1080 height, got $height" } if ($fps -ne "30/1") { throw "expected 30fps, got $fps" } if ($duration -lt 7.5 -or $duration -gt 8.5) { throw "expected ~8s duration, got $duration" } Write-Host "canary.mp4 ok: ${width}x${height} @ $fps, ${duration}s" - name: Scaffold issue #574 reused-video regression shell: pwsh run: | $ErrorActionPreference = 'Stop' $project = "$env:RUNNER_TEMP\issue-574-reused-video" New-Item -ItemType Directory -Force -Path $project | Out-Null cd $project ffmpeg -y ` -f lavfi -i "testsrc2=size=1920x1080:rate=30:duration=12" ` -f lavfi -i "sine=frequency=880:sample_rate=48000:duration=12" ` -c:v libx264 ` -pix_fmt yuv420p ` -r 30 ` -g 250 ` -keyint_min 250 ` -c:a aac ` -shortest ` 1.mp4 @' Issue 574 reused video regression
'@ | Set-Content -Path index.html -Encoding utf8 - name: Render issue #574 reused-video regression shell: pwsh env: PRODUCER_PLAYER_READY_TIMEOUT_MS: "15000" run: | cd "$env:RUNNER_TEMP\issue-574-reused-video" node "$env:GITHUB_WORKSPACE\packages\cli\dist\cli.js" render ` --fps 30 ` --quality standard ` --workers 1 ` --output renders\issue-574.mp4 - name: Verify issue #574 rendered MP4 shell: pwsh run: | $mp4 = "$env:RUNNER_TEMP\issue-574-reused-video\renders\issue-574.mp4" if (-not (Test-Path $mp4)) { throw "issue-574.mp4 not produced" } $probe = ffprobe -v error -select_streams v:0 ` -show_entries stream=width,height,r_frame_rate -show_entries format=duration ` -of default=noprint_wrappers=1 $mp4 Write-Host $probe $width = ($probe | Select-String '^width=(.+)$').Matches.Groups[1].Value $height = ($probe | Select-String '^height=(.+)$').Matches.Groups[1].Value $fps = ($probe | Select-String '^r_frame_rate=(.+)$').Matches.Groups[1].Value $duration = [double]($probe | Select-String '^duration=(.+)$').Matches.Groups[1].Value if ([int]$width -ne 1920) { throw "expected 1920 width, got $width" } if ([int]$height -ne 1080) { throw "expected 1080 height, got $height" } if ($fps -ne "30/1") { throw "expected 30fps, got $fps" } if ($duration -lt 11.5 -or $duration -gt 12.5) { throw "expected ~12s duration, got $duration" } Write-Host "issue-574.mp4 ok: ${width}x${height} @ $fps, ${duration}s" - name: Upload rendered MP4 artifact if: always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: windows-render-${{ github.run_id }} path: | ${{ runner.temp }}/windows-canary/canary/renders/canary.mp4 ${{ runner.temp }}/issue-574-reused-video/renders/issue-574.mp4 if-no-files-found: error retention-days: 7 # ------------------------------------------------------------------- # Unit-test suites on Windows. Mirrors the Linux `test` job in ci.yml # so we catch Windows-specific regressions (path separators, shell # invocations, CRLF, file URLs, etc.) in existing vitest suites. # The producer package is skipped because its tests require Docker / # Linux-only tooling (Dockerfile.test, LFS golden MP4 baselines). # The aws-lambda package is skipped because it targets the AWS Lambda # Linux runtime exclusively (`@sparticuz/chromium` is Linux-only; the # ZIP layout is for `/var/task` on AL2023). Its handler.test.ts also # trips a bun-on-Windows workspace-symlink quirk reading the # producer's transitive `hono` dep. # ------------------------------------------------------------------- test-windows: name: Tests on windows-latest needs: [changes, preflight] if: needs.changes.outputs.code == 'true' || github.event_name == 'workflow_dispatch' runs-on: windows-latest timeout-minutes: 20 steps: - name: Disable Windows Defender real-time monitoring shell: pwsh continue-on-error: true run: Set-MpPreference -DisableRealtimeMonitoring $true - name: Checkout uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: ref: ${{ github.event.inputs.ref }} lfs: true # ----------------------------------------------------------------- # Install FFmpeg so vitest suites that gate on `HAS_FFMPEG` # (e.g. packages/engine videoFrameExtractor.test.ts) actually run on # Windows. Without it those suites `describe.skipIf(!HAS_FFMPEG)` # themselves silently and any Windows-specific regression in the # FFmpeg-driven code paths would not be caught here. # ----------------------------------------------------------------- - name: Install FFmpeg uses: ./.github/actions/install-ffmpeg-windows - name: Set FFMPEG_BIN for bun install shell: pwsh run: | $path = (Get-Command ffmpeg.exe).Source "FFMPEG_BIN=$path" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - name: Install Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 22 - name: Install dependencies shell: pwsh # Force the pre-1.3 "hoisted" linker on Windows. Bun 1.3.x's default # "isolated" linker creates nested workspace junctions that don't # materialize reliably on Windows GHA runners — manifests as # `Cannot find package 'esbuild'` walking up from # `packages/producer/build.mjs` and `EPERM stat` on # `packages/producer/node_modules/@fontsource/*` from generators. # See oven-sh/bun#23615, #18354, #10146. Linux CI keeps isolated; # the lockfile is linker-agnostic so this flag is safe with # --frozen-lockfile. run: bun install --frozen-lockfile --linker=hoisted - name: Build shell: pwsh run: bun run build - name: Run tests (all packages except producer + aws-lambda) shell: pwsh # Enumerate the packages we want to test instead of negating — # `bun run --filter "!a" --filter "!b"` composes as a union (any # package matching either negation runs), not an intersection. # That meant `@hyperframes/producer` was effectively still being # tested on Windows, which is what we explicitly want skipped # (Docker / LFS-baseline tooling). run: bun run --filter @hyperframes/core --filter @hyperframes/engine --filter @hyperframes/player --filter @hyperframes/cli --filter @hyperframes/studio --filter @hyperframes/shader-transitions test - name: Run runtime contract test shell: pwsh run: bun run --filter "@hyperframes/core" test:hyperframe-runtime-ci