chore: import upstream snapshot with attribution
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
Docs / Validate docs (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Sync skills to ClawHub / Publish changed skills (push) Waiting to run
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
name: Bug Report
|
||||
description: Report a bug to help us improve
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug! Before filing, please:
|
||||
- Search [existing issues](https://github.com/heygen-com/hyperframes/issues) to avoid duplicates
|
||||
- Make sure the issue reproduces without custom extensions or plugins
|
||||
- Test on the latest version (`npx hyperframes upgrade`)
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Describe the bug
|
||||
description: A clear description of what the bug is.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: reproduction-link
|
||||
attributes:
|
||||
label: Link to reproduction
|
||||
description: |
|
||||
A link to a **public** GitHub repository with a minimal reproduction of the issue.
|
||||
|
||||
Start from a clean project with `npx hyperframes init repro --non-interactive --example blank`, make only the changes needed to demonstrate the bug, push to a public repo, and paste the link here.
|
||||
|
||||
**Issues without a minimal reproduction may be closed.** A reproduction helps us diagnose and fix your issue faster — often in hours instead of days.
|
||||
placeholder: "https://github.com/username/my-hyperframes-repro"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: Step-by-step instructions using the linked reproduction.
|
||||
placeholder: |
|
||||
1. Clone the repo above
|
||||
2. Run `npx hyperframes render`
|
||||
3. See error...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: What you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: What actually happened. Include error messages or screenshots if applicable.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx hyperframes doctor` and paste the output here.
|
||||
render: shell
|
||||
placeholder: |
|
||||
✓ Version 0.x.x (latest)
|
||||
✓ Node.js v22.x.x (darwin arm64)
|
||||
✓ FFmpeg ffmpeg version 7.x ...
|
||||
✓ Chrome bundled: /path/to/chrome
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context, logs, or screenshots.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for Hyperframes
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for suggesting an improvement! Please search [existing issues](https://github.com/heygen-com/hyperframes/issues) first to avoid duplicates.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem
|
||||
description: What problem does this solve? What's the use case?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: How should it work? Include code examples or mockups if helpful.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Any alternative approaches you've thought about.
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Any other context, mockups, or examples.
|
||||
@@ -0,0 +1,60 @@
|
||||
name: Install FFmpeg (Windows)
|
||||
description: >-
|
||||
Download a pinned FFmpeg GPL build for Windows from BtbN/FFmpeg-Builds and put
|
||||
ffmpeg.exe / ffprobe.exe on PATH. We bypass `choco install ffmpeg` because
|
||||
the Chocolatey community feed regularly returns 503 / 504 / NuGet resolver
|
||||
errors with no retry, which makes it unsuitable as a CI dependency. From the
|
||||
consumer's perspective this is equivalent — `where ffmpeg` (the PR #336
|
||||
validation) and `findFFmpeg()` both pass.
|
||||
|
||||
inputs:
|
||||
release-url:
|
||||
description: >-
|
||||
URL of a BtbN/FFmpeg-Builds zip to install. Pinned to a specific
|
||||
autobuild tag so a new upstream nightly can't silently change encoder
|
||||
behavior under us. The asset filename embeds the git hash, so both
|
||||
the tag and the filename must be bumped together when upgrading.
|
||||
required: false
|
||||
default: https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-04-30-13-44/ffmpeg-N-124278-gcc3ca17127-win64-gpl.zip
|
||||
max-attempts:
|
||||
description: Max download attempts before failing.
|
||||
required: false
|
||||
default: "8"
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install FFmpeg from BtbN/FFmpeg-Builds
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$url = '${{ inputs.release-url }}'
|
||||
$maxAttempts = [int]'${{ inputs.max-attempts }}'
|
||||
|
||||
$zip = Join-Path $env:RUNNER_TEMP 'ffmpeg.zip'
|
||||
$dir = Join-Path $env:RUNNER_TEMP 'ffmpeg'
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
|
||||
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
||||
Write-Host "--- Downloading ffmpeg from BtbN/FFmpeg-Builds (attempt $attempt/$maxAttempts) ---"
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing
|
||||
break
|
||||
} catch {
|
||||
Write-Warning "Download failed: $($_.Exception.Message)"
|
||||
if ($attempt -eq $maxAttempts) { throw }
|
||||
Start-Sleep -Seconds (30 * $attempt)
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "--- Extracting ffmpeg ---"
|
||||
Expand-Archive -Path $zip -DestinationPath $dir -Force
|
||||
|
||||
$bin = Get-ChildItem -Path $dir -Recurse -Filter 'ffmpeg.exe' | Select-Object -First 1
|
||||
if (-not $bin) { throw "ffmpeg.exe not found after extracting $url" }
|
||||
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $bin.Directory.FullName
|
||||
|
||||
Write-Host "--- ffmpeg sanity check ---"
|
||||
& $bin.FullName -version | Select-Object -First 1
|
||||
@@ -0,0 +1,31 @@
|
||||
name: Preflight
|
||||
description: |
|
||||
Cheap lint + format gate that runs before expensive CI jobs (regression
|
||||
shards, perf shards, parity renders, Windows renders, catalog previews).
|
||||
Single source of truth for the gate's bun/node/cache/lint/format steps —
|
||||
tweak here and every preflight job picks it up.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- uses: actions/cache@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4
|
||||
with:
|
||||
path: ~/.bun/install/cache
|
||||
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
|
||||
restore-keys: |
|
||||
bun-${{ runner.os }}-
|
||||
|
||||
- shell: bash
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
- shell: bash
|
||||
run: bun run lint
|
||||
|
||||
- shell: bash
|
||||
run: bun run format:check
|
||||
@@ -0,0 +1,28 @@
|
||||
name: Prepare FFMPEG_BIN for bun install
|
||||
description: >-
|
||||
Copies the system ffmpeg binary to a writable temp location and sets
|
||||
FFMPEG_BIN in the environment. ffmpeg-static's postinstall script checks
|
||||
whether a file already exists at FFMPEG_BIN; if it does and process.exit(0)
|
||||
is respected by the runtime, the CDN download is skipped entirely. If the
|
||||
runtime intercepts process.exit(0) and the download proceeds anyway, using
|
||||
a writable target prevents the EACCES failure that occurs when the
|
||||
destination is a system path like /usr/bin/ffmpeg.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Copy ffmpeg to writable path
|
||||
shell: bash
|
||||
run: |
|
||||
FFMPEG=$(which ffmpeg 2>/dev/null || echo "")
|
||||
if [ -n "$FFMPEG" ]; then
|
||||
cp "$FFMPEG" "$RUNNER_TEMP/hf-ffmpeg"
|
||||
else
|
||||
# ffmpeg not pre-installed; write a stub so ffmpeg-static's postinstall
|
||||
# finds a file at FFMPEG_BIN and exits early (statSync check) without
|
||||
# attempting the CDN download. Jobs that need a real ffmpeg binary
|
||||
# install it via apt before calling this action.
|
||||
printf '#!/bin/sh\nexec ffmpeg "$@"\n' > "$RUNNER_TEMP/hf-ffmpeg"
|
||||
fi
|
||||
chmod +x "$RUNNER_TEMP/hf-ffmpeg"
|
||||
echo "FFMPEG_BIN=$RUNNER_TEMP/hf-ffmpeg" >> "$GITHUB_ENV"
|
||||
@@ -0,0 +1,64 @@
|
||||
name: "Hyperframes CodeQL config"
|
||||
|
||||
# Use GitHub's default security-extended suite — it's a strict superset of the
|
||||
# default suite (more queries, slightly higher false-positive rate). Pair it with
|
||||
# the query-filters below so the extra queries don't drown the dashboard.
|
||||
queries:
|
||||
- uses: security-extended
|
||||
|
||||
# Per-rule path filters. The intent is to silence rules that have known false
|
||||
# positives on specific file shapes (generated test artifacts, CDN-script test
|
||||
# fixtures, functional-cleanup regex) WITHOUT excluding those paths from all
|
||||
# analysis — a malicious contributor adding e.g. a command-injection sink into
|
||||
# a "test fixture" would still get caught.
|
||||
#
|
||||
# To audit what changed: look at PR diffs touching this file. Reviewers should
|
||||
# treat it like CODEOWNERS — adding a new path exclusion is a policy change.
|
||||
query-filters:
|
||||
# Generated test artifacts (golden baselines written by the producer test
|
||||
# harness). Every compiled.html re-rasterizes the regex-stripped composition;
|
||||
# the same alerts fire on every fixture and on every re-render.
|
||||
- exclude:
|
||||
id: js/incomplete-sanitization
|
||||
paths:
|
||||
- "packages/producer/tests/**/output/compiled.html"
|
||||
- "packages/producer/tests/**/failures/*.html"
|
||||
|
||||
# Test fixtures and skill test corpora intentionally load CDN scripts without
|
||||
# SRI — pinning hashes there would fight the test's purpose (we want the test
|
||||
# to use whatever the registry hands back, the same way a composition would).
|
||||
- exclude:
|
||||
id: js/functionality-from-untrusted-source
|
||||
paths:
|
||||
- "packages/producer/tests/**"
|
||||
- "skills/**/test-corpus/**"
|
||||
- "skills/**/assets/test-corpus/**"
|
||||
|
||||
# The hand-rolled HTML cleanup regex in our build-time tooling looks like a
|
||||
# sanitizer to CodeQL but isn't one — it strips framework bootstraps from
|
||||
# captured pages before they're fed back into our own renderer (Puppeteer,
|
||||
# not a user-facing DOM). Same for the text normalizer in the whisper path
|
||||
# (caption text → SRT/VTT, no DOM emission). Scope these exclusions to the
|
||||
# exact files that contain functional regex, not to whole directories, so
|
||||
# any new code in cli/, core/, or producer/ that LOOKS like a sanitizer
|
||||
# still trips the rules.
|
||||
- exclude:
|
||||
id: js/bad-tag-filter
|
||||
paths:
|
||||
- "packages/cli/src/capture/index.ts"
|
||||
- "packages/cli/src/whisper/normalize.ts"
|
||||
- "packages/core/src/lint/utils.ts"
|
||||
- "packages/producer/src/services/htmlCompiler.ts"
|
||||
- exclude:
|
||||
id: js/incomplete-multi-character-sanitization
|
||||
paths:
|
||||
- "packages/cli/src/capture/index.ts"
|
||||
- "packages/cli/src/whisper/normalize.ts"
|
||||
|
||||
# Golden baselines inline vendor libraries (Three.js, GSAP) whose bundled
|
||||
# Math.random() calls trip insecure-randomness. These are test artifacts
|
||||
# from compiled compositions, not production code paths.
|
||||
- exclude:
|
||||
id: js/insecure-randomness
|
||||
paths:
|
||||
- "packages/producer/tests/**/output/compiled.html"
|
||||
@@ -0,0 +1,19 @@
|
||||
## What
|
||||
|
||||
Brief description of the change.
|
||||
|
||||
## Why
|
||||
|
||||
Why is this change needed?
|
||||
|
||||
## How
|
||||
|
||||
How was this implemented? Any notable design decisions?
|
||||
|
||||
## Test plan
|
||||
|
||||
How was this tested?
|
||||
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] Manual testing performed
|
||||
- [ ] Documentation updated (if applicable)
|
||||
@@ -0,0 +1,43 @@
|
||||
changelog:
|
||||
exclude:
|
||||
labels:
|
||||
- skip-changelog
|
||||
- dependencies
|
||||
authors:
|
||||
- dependabot
|
||||
- dependabot[bot]
|
||||
categories:
|
||||
- title: Breaking Changes
|
||||
labels:
|
||||
- breaking-change
|
||||
- breaking
|
||||
- title: Features
|
||||
labels:
|
||||
- enhancement
|
||||
- feature
|
||||
- title: Fixes
|
||||
labels:
|
||||
- bug
|
||||
- fix
|
||||
- title: Docs & Examples
|
||||
labels:
|
||||
- documentation
|
||||
- docs
|
||||
- examples
|
||||
- title: Catalog
|
||||
labels:
|
||||
- catalog
|
||||
- registry
|
||||
- title: Performance
|
||||
labels:
|
||||
- performance
|
||||
- perf
|
||||
- title: Internal
|
||||
labels:
|
||||
- internal
|
||||
- refactor
|
||||
- tests
|
||||
- ci
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", ":semanticCommits", ":dependencyDashboard"],
|
||||
"schedule": ["before 6am on monday"],
|
||||
"timezone": "America/Los_Angeles",
|
||||
"prConcurrentLimit": 5,
|
||||
"prHourlyLimit": 2,
|
||||
"rangeStrategy": "bump",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": ["before 6am on monday"]
|
||||
},
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": true,
|
||||
"labels": ["security"]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"groupName": "dev dependencies (non-major)"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"matchDepTypes": ["dependencies"],
|
||||
"groupName": "prod patch updates"
|
||||
},
|
||||
{
|
||||
"matchUpdateTypes": ["major"],
|
||||
"dependencyDashboardApproval": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"groupName": "github-actions",
|
||||
"pinDigests": true
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"matchPackageNames": ["/^@hyperframes//"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Catalog Previews
|
||||
|
||||
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"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "registry/blocks/**"
|
||||
- "registry/components/**"
|
||||
- "scripts/generate-catalog-previews.ts"
|
||||
- ".github/workflows/catalog-previews.yml"
|
||||
|
||||
concurrency:
|
||||
group: catalog-previews-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
preflight:
|
||||
name: Preflight (lint + format)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: ./.github/actions/preflight
|
||||
|
||||
render-previews:
|
||||
name: Render catalog previews
|
||||
needs: preflight
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- run: bun run build
|
||||
|
||||
# Chrome headless shell for rendering
|
||||
- uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
- name: Render changed block/component previews
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
run: |
|
||||
# Find which blocks/components changed in this PR
|
||||
CHANGED_ITEMS=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD -- registry/blocks/ registry/components/ \
|
||||
| grep -E '^registry/(blocks|components)/' \
|
||||
| sed 's|^registry/[^/]*/\([^/]*\)/.*|\1|' \
|
||||
| sort -u)
|
||||
|
||||
if [ -z "$CHANGED_ITEMS" ]; then
|
||||
echo "No block/component changes detected."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Changed items: $CHANGED_ITEMS"
|
||||
FAILED=0
|
||||
|
||||
for item in $CHANGED_ITEMS; do
|
||||
echo "Rendering preview for: $item"
|
||||
if ! timeout 120 npx tsx scripts/generate-catalog-previews.ts --only "$item" --skip-video; then
|
||||
echo "::warning::Failed to render preview for $item"
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
echo "::warning::$FAILED item(s) failed to render"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload preview artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: catalog-previews
|
||||
path: docs/images/catalog/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,682 @@
|
||||
name: CI
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: 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"
|
||||
|
||||
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]
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
cli: ${{ steps.filter.outputs.cli }}
|
||||
skills: ${{ steps.filter.outputs.skills }}
|
||||
steps:
|
||||
# Force git-based change detection instead of the pull_request REST API.
|
||||
# The API path can fail the whole workflow on transient listFiles
|
||||
# timeouts before any real CI work starts.
|
||||
- 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"
|
||||
- "tsconfig*.json"
|
||||
- "Dockerfile*"
|
||||
- ".github/workflows/**"
|
||||
cli:
|
||||
- "packages/cli/**"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- ".github/workflows/ci.yml"
|
||||
skills:
|
||||
- "skills/**"
|
||||
- "skills-manifest.json"
|
||||
- "package.json"
|
||||
- ".github/workflows/ci.yml"
|
||||
|
||||
build:
|
||||
name: Build
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: corepack enable
|
||||
- run: corepack prepare pnpm@10.17.1 --activate
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run build
|
||||
- run: bun run verify:packed-manifests
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run lint
|
||||
|
||||
# `fallow audit` runs dead-code + complexity + duplication analysis scoped to
|
||||
# the changed files. The default `--gate new-only` means existing legacy
|
||||
# findings don't fail the build — only NEW issues introduced by the PR do.
|
||||
# This stops bleeding while letting incremental cleanup land separately.
|
||||
#
|
||||
# On findings, the job posts (or updates) a sticky comment on the PR so
|
||||
# reviewers see the full list inline instead of digging through CI logs.
|
||||
fallow:
|
||||
name: Fallow audit
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true' && github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
# Scope write access to this single job — the rest of `ci.yml` keeps the
|
||||
# workflow-level `pull-requests: read` default so build / lint / test
|
||||
# tokens can't post or modify PR comments. Job-level permissions override
|
||||
# the workflow block.
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
# Full history so `--base origin/main` can diff against the merge
|
||||
# base on stacked PRs, not just the shallow tip.
|
||||
fetch-depth: 0
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- name: Run fallow audit
|
||||
id: audit
|
||||
# `bun install` above made `bunx fallow` resolve from node_modules, so
|
||||
# we don't re-download fallow each run. The script disables `errexit`
|
||||
# so the audit's non-zero exit (on findings) doesn't abort before we
|
||||
# write the exit code to the step output. The size check guards
|
||||
# against fallow crashing before producing markdown (e.g. transient
|
||||
# parse failure) — without it we'd post a blank sticky comment.
|
||||
run: |
|
||||
set +e
|
||||
bunx fallow audit --base origin/main --fail-on-issues \
|
||||
--format pr-comment-github \
|
||||
> /tmp/fallow-comment.md
|
||||
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
|
||||
if [ ! -s /tmp/fallow-comment.md ]; then
|
||||
echo "fallow produced no output — see the job logs above." > /tmp/fallow-comment.md
|
||||
fi
|
||||
- name: Post sticky comment (findings)
|
||||
if: steps.audit.outputs.exit_code != '0'
|
||||
# Fork PRs run with a read-only GITHUB_TOKEN regardless of the
|
||||
# workflow's `permissions:` block, so the comment post will fail on
|
||||
# forks. Don't fail the whole job — the audit gate below still fires.
|
||||
continue-on-error: true
|
||||
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||
with:
|
||||
# `header` matches fallow's built-in `<!-- fallow-id: fallow-results -->`
|
||||
# sentinel so subsequent runs update the same comment.
|
||||
header: fallow-results
|
||||
path: /tmp/fallow-comment.md
|
||||
- name: Remove stale sticky comment (clean run)
|
||||
if: steps.audit.outputs.exit_code == '0'
|
||||
continue-on-error: true
|
||||
uses: marocchino/sticky-pull-request-comment@52423e01640425a022ef5fd42c6fb5f633a02728 # v2.9.1
|
||||
with:
|
||||
header: fallow-results
|
||||
delete: true
|
||||
- name: Fail if audit found issues
|
||||
if: steps.audit.outputs.exit_code != '0'
|
||||
run: |
|
||||
echo "::error::Fallow audit found new issues — see the PR comment above for details."
|
||||
exit 1
|
||||
|
||||
format:
|
||||
name: Format
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run format:check
|
||||
|
||||
typecheck:
|
||||
name: Typecheck
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run build
|
||||
- run: bun run --filter '*' typecheck
|
||||
|
||||
test:
|
||||
name: Test
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run test:scripts
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||
- run: bun run --filter '!@hyperframes/producer' test
|
||||
|
||||
producer-source-tests:
|
||||
name: "Producer: ${{ matrix.lane }} tests"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
lane: [unit, integration]
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install FFmpeg for integration tests
|
||||
if: matrix.lane == 'integration'
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends ffmpeg
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --filter @hyperframes/engine build
|
||||
- run: bun run producer:test:${{ matrix.lane }}
|
||||
|
||||
# Tests under skills/**/*.test.mjs are bare `node --test` files with only
|
||||
# `node:` built-in imports. They aren't part of any workspace package, and
|
||||
# the main `Test` job's `code` path filter excludes `skills/**`, so without
|
||||
# this dedicated job they'd never run in CI. Examples:
|
||||
# * skills/media-use/scripts/resolve.test.mjs
|
||||
# * skills/media-use/scripts/lib/manifest.test.mjs
|
||||
# Several of these are regression guards (e.g. shell-injection cases), so
|
||||
# the whole point is that they fire on PRs that touch skills/.
|
||||
test-skills:
|
||||
name: "Test: skills"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.skills == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Discover and run skills tests
|
||||
# We expand the test list via bash so the job fails loudly when the
|
||||
# matcher comes back empty, rather than silently no-op'ing (which
|
||||
# would defeat the whole point of this job).
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t SKILLS_TESTS < <(find skills -type f -name "*.test.mjs" | sort)
|
||||
if [ "${#SKILLS_TESTS[@]}" -eq 0 ]; then
|
||||
echo "::error::No skills/**/*.test.mjs files found. Did the layout change?"
|
||||
exit 1
|
||||
fi
|
||||
printf 'Running %d skills test file(s):\n' "${#SKILLS_TESTS[@]}"
|
||||
printf ' * %s\n' "${SKILLS_TESTS[@]}"
|
||||
node --test "${SKILLS_TESTS[@]}"
|
||||
|
||||
# Guards that skills-manifest.json (the published freshness fingerprint read
|
||||
# by `hyperframes skills check`) was regenerated when a skill changed. Runs
|
||||
# `gen:skills-manifest --check`, which compares per-skill content hashes; the
|
||||
# manifest carries no version/timestamp, so it only fails on real content
|
||||
# drift. bun runs the TS script directly, no install needed.
|
||||
skills-manifest:
|
||||
name: "Skills: manifest in sync"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.skills == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 3
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- name: Verify skills-manifest.json matches skills/
|
||||
run: bun packages/cli/scripts/gen-skills-manifest.ts --check
|
||||
|
||||
cli-npx-shim:
|
||||
name: "CLI: npx shim (${{ matrix.os }})"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.cli == 'true'
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 10
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
if: runner.os != 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
- name: Install dependencies
|
||||
if: runner.os == 'Windows'
|
||||
run: bun install --frozen-lockfile --ignore-scripts --linker=hoisted
|
||||
- run: bun run --cwd packages/cli test src/utils/npxCommand.test.ts src/commands/skills.test.ts
|
||||
|
||||
sdk-tests:
|
||||
name: "SDK: unit + contract + smoke"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- run: bun install --frozen-lockfile
|
||||
# Build workspace deps so the sdk's @hyperframes/parsers + core subpath
|
||||
# imports resolve via the "node" export condition (dist) under vitest.
|
||||
- run: bun run --filter '@hyperframes/parsers' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --filter @hyperframes/sdk test
|
||||
|
||||
test-runtime-contract:
|
||||
name: "Test: runtime contract"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
# Runtime coverage now imports core modules that consume workspace
|
||||
# subpaths. Build their dist exports before Vitest resolves them.
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --filter @hyperframes/core test:hyperframe-runtime-ci
|
||||
|
||||
studio-load-smoke:
|
||||
name: "Studio: load smoke"
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
# Build workspace deps so the studio vite.config.ts (loaded by Node) can
|
||||
# resolve @hyperframes/core and @hyperframes/studio-server via the "node"
|
||||
# export condition (dist).
|
||||
- run: bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
- run: bun run --cwd packages/core build
|
||||
- run: bun run --cwd packages/core build:hyperframes-runtime
|
||||
- name: Start studio and check for runtime errors
|
||||
run: |
|
||||
# Start the studio Vite dev server (fast — no bundle step)
|
||||
bun run --filter '@hyperframes/studio' dev -- --port 5199 &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for the server to be ready (up to 20s)
|
||||
for i in $(seq 1 40); do
|
||||
if curl -sf http://localhost:5199/ >/dev/null 2>&1; then break; fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if ! curl -sf http://localhost:5199/ >/dev/null 2>&1; then
|
||||
echo "FAIL: studio dev server did not start"
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load the studio in headless Chrome with API mocking to trigger
|
||||
# the full splash→main transition (catches hooks-after-early-return bugs)
|
||||
node scripts/studio-runtime-smoke.mjs http://localhost:5199/#project=smoke-test
|
||||
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
|
||||
smoke-global-install:
|
||||
name: "Smoke: global install"
|
||||
needs: [changes, build]
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run build
|
||||
|
||||
# Pack the CLI as a tarball (simulates what `npm publish` produces)
|
||||
- name: Pack CLI tarball
|
||||
run: cd packages/cli && npm pack
|
||||
|
||||
# Install globally using --prefix to avoid sudo
|
||||
- name: Install globally via npm
|
||||
run: npm install -g --prefix /tmp/hf-smoke ./packages/cli/hyperframes-cli-*.tgz
|
||||
|
||||
# Scaffold a blank project
|
||||
- name: Init blank project
|
||||
run: |
|
||||
export PATH="/tmp/hf-smoke/bin:$PATH"
|
||||
mkdir /tmp/hf-project && cd /tmp/hf-project
|
||||
hyperframes init test-project --example blank
|
||||
|
||||
# Start preview, probe the runtime endpoint, assert no esbuild errors
|
||||
- name: Smoke-test preview server
|
||||
run: |
|
||||
export PATH="/tmp/hf-smoke/bin:$PATH"
|
||||
cd /tmp/hf-project/test-project
|
||||
|
||||
# Start the preview server in the background; capture stderr
|
||||
CI=true hyperframes preview --port 3099 2>/tmp/hf-stderr.log &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Wait for the server to be ready (up to 15 s)
|
||||
for i in $(seq 1 30); do
|
||||
if curl -sf http://localhost:3099/ >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Probe the runtime JS endpoint
|
||||
BODY=$(curl -sf http://localhost:3099/api/runtime.js | head -c 200 || true)
|
||||
if [ -z "$BODY" ]; then
|
||||
echo "FAIL: /api/runtime.js returned empty response"
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
cat /tmp/hf-stderr.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kill $SERVER_PID 2>/dev/null || true
|
||||
wait $SERVER_PID 2>/dev/null || true
|
||||
|
||||
# Assert stderr does not contain esbuild / runtime load errors
|
||||
if grep -qE '✘ \[ERROR\]|Failed to load runtime' /tmp/hf-stderr.log; then
|
||||
echo "FAIL: preview emitted runtime errors:"
|
||||
cat /tmp/hf-stderr.log
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PASS: global install smoke test succeeded"
|
||||
|
||||
cli-smoke-required:
|
||||
name: "CLI smoke (required)"
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
lfs: true
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install FFmpeg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg
|
||||
- uses: ./.github/actions/prepare-ffmpeg-bin
|
||||
- name: Install dependencies
|
||||
run: bun install --frozen-lockfile
|
||||
- name: Build monorepo
|
||||
run: bun run build
|
||||
|
||||
- name: Create smoke input video
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ffmpeg -hide_banner -loglevel error \
|
||||
-f lavfi -i testsrc2=size=640x360:rate=30 \
|
||||
-f lavfi -i sine=frequency=880:sample_rate=48000 \
|
||||
-t 3 \
|
||||
-c:v libx264 \
|
||||
-pix_fmt yuv420p \
|
||||
-c:a aac \
|
||||
-shortest \
|
||||
-y /tmp/hf-cli-input.mp4
|
||||
test -s /tmp/hf-cli-input.mp4
|
||||
|
||||
- name: Smoke-test CLI from monorepo source
|
||||
# init's --skip-skills flag is neutered (see init.ts); opt out of the
|
||||
# GitHub skills check via this env so the smoke test stays offline/fast.
|
||||
env:
|
||||
HYPERFRAMES_SKIP_SKILLS: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf /tmp/hf-cli-inside
|
||||
|
||||
bun run --filter @hyperframes/cli dev -- init /tmp/hf-cli-inside \
|
||||
--example warm-grain \
|
||||
--video /tmp/hf-cli-input.mp4 \
|
||||
--skip-transcribe \
|
||||
--non-interactive \
|
||||
--skip-skills
|
||||
|
||||
bun run --filter @hyperframes/cli dev -- lint /tmp/hf-cli-inside
|
||||
bun run --filter @hyperframes/cli dev -- validate /tmp/hf-cli-inside --timeout 3000
|
||||
bun run --filter @hyperframes/cli dev -- render /tmp/hf-cli-inside \
|
||||
--quality standard \
|
||||
--workers auto \
|
||||
--strict \
|
||||
--output /tmp/hf-cli-inside/renders/inside.mp4 2>&1 | tee /tmp/hf-cli-render.log
|
||||
|
||||
test -s /tmp/hf-cli-inside/renders/inside.mp4
|
||||
|
||||
- name: Assert page.goto completes under 5s budget
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Extract the longest page.goto time from render logs.
|
||||
# Format: [initSession:MODE] page.goto complete (NNNms)
|
||||
MAX_MS=$(grep -oP 'page\.goto complete \(\K[0-9]+' /tmp/hf-cli-render.log | sort -n | tail -1)
|
||||
if [ -z "$MAX_MS" ]; then
|
||||
echo "::error::No page.goto timing found in render logs — log format may have changed. Update the grep pattern."
|
||||
exit 1
|
||||
fi
|
||||
echo "Slowest page.goto: ${MAX_MS}ms (budget: 5000ms)"
|
||||
if [ "$MAX_MS" -gt 5000 ]; then
|
||||
echo "::error::page.goto took ${MAX_MS}ms — exceeds 5s budget. Possible Proxy/stub regression."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Pack CLI tarball
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/hf-cli-pack
|
||||
cd packages/cli
|
||||
PACKED_TARBALL="$(npm pack --pack-destination /tmp/hf-cli-pack | tail -n 1)"
|
||||
test -n "$PACKED_TARBALL"
|
||||
test -f "/tmp/hf-cli-pack/$PACKED_TARBALL"
|
||||
echo "HF_CLI_TARBALL=/tmp/hf-cli-pack/$PACKED_TARBALL" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Install packed CLI outside monorepo
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install -g --prefix /tmp/hf-cli-global "$HF_CLI_TARBALL"
|
||||
|
||||
- name: Smoke-test packed CLI outside monorepo
|
||||
# init's --skip-skills flag is neutered (see init.ts); opt out of the
|
||||
# GitHub skills check via this env so the smoke test stays offline/fast.
|
||||
env:
|
||||
HYPERFRAMES_SKIP_SKILLS: "1"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export PATH="/tmp/hf-cli-global/bin:$PATH"
|
||||
rm -rf /tmp/hf-cli-outside
|
||||
|
||||
hyperframes init /tmp/hf-cli-outside \
|
||||
--example warm-grain \
|
||||
--video /tmp/hf-cli-input.mp4 \
|
||||
--skip-transcribe \
|
||||
--non-interactive \
|
||||
--skip-skills
|
||||
|
||||
hyperframes lint /tmp/hf-cli-outside
|
||||
hyperframes validate /tmp/hf-cli-outside --timeout 3000
|
||||
hyperframes render /tmp/hf-cli-outside \
|
||||
--quality standard \
|
||||
--workers auto \
|
||||
--strict \
|
||||
--output /tmp/hf-cli-outside/renders/outside.mp4
|
||||
|
||||
test -s /tmp/hf-cli-outside/renders/outside.mp4
|
||||
|
||||
filesize:
|
||||
name: File size check
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check file sizes (max 600 lines)
|
||||
# Scoped to files THIS PR changed under packages/studio. Walking the
|
||||
# whole tree blamed every unrelated PR for pre-existing offenders.
|
||||
# Falls back to a full scan on push events (no base ref available)
|
||||
# so the rule still guards main.
|
||||
run: |
|
||||
set -e
|
||||
if [ -n "${{ github.base_ref }}" ]; then
|
||||
mapfile -t files < <(
|
||||
git diff --name-only --diff-filter=ACMR \
|
||||
"origin/${{ github.base_ref }}...HEAD" -- \
|
||||
'packages/studio/**/*.ts' 'packages/studio/**/*.tsx' \
|
||||
| grep -vE '\.(test|spec)\.(ts|tsx)$|\.generated\.' || true
|
||||
)
|
||||
else
|
||||
mapfile -t files < <(
|
||||
find packages/studio -path '*/node_modules' -prune -o \
|
||||
\( -name '*.ts' -o -name '*.tsx' \) -print \
|
||||
| grep -vE '\.(test|spec)\.(ts|tsx)$|\.generated\.'
|
||||
)
|
||||
fi
|
||||
EXIT=0
|
||||
for f in "${files[@]}"; do
|
||||
[ -z "$f" ] && continue
|
||||
[ -f "$f" ] || continue # skip files deleted in this PR
|
||||
lines=$(wc -l < "$f")
|
||||
if [ "$lines" -gt 600 ]; then
|
||||
echo "::error file=$f::$f has $lines lines (max 600)"
|
||||
EXIT=1
|
||||
fi
|
||||
done
|
||||
exit $EXIT
|
||||
|
||||
semantic-pr-title:
|
||||
name: Semantic PR title
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@e32d7e603df1aa1ba07e981f2a23455dee596825 # v5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
types: |
|
||||
feat
|
||||
fix
|
||||
docs
|
||||
style
|
||||
refactor
|
||||
perf
|
||||
test
|
||||
build
|
||||
ci
|
||||
chore
|
||||
revert
|
||||
@@ -0,0 +1,55 @@
|
||||
# CodeQL advanced setup. Replaces GitHub's default code-scanning setup; the
|
||||
# repo must have default setup disabled in Security → Code scanning → "Set up"
|
||||
# before this workflow can run.
|
||||
#
|
||||
# Languages were taken from the existing default-setup config (JS/TS, Python,
|
||||
# Actions). Triggers mirror what default setup ran: push to main, every PR
|
||||
# against main, and a weekly schedule.
|
||||
#
|
||||
# The rules and path filters live in .github/codeql/codeql-config.yml so policy
|
||||
# changes show up as a normal PR diff.
|
||||
name: CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
# Mondays at 14:39 UTC — matches the cadence default setup was running on.
|
||||
- cron: "39 14 * * 1"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
actions: read
|
||||
contents: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
- language: python
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
config-file: ./.github/codeql/codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Docs
|
||||
|
||||
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"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "DOCS_GUIDELINES.md"
|
||||
- "packages/core/schemas/**"
|
||||
- "scripts/sync-schemas.ts"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "docs/**"
|
||||
- "DOCS_GUIDELINES.md"
|
||||
- "packages/core/schemas/**"
|
||||
- "scripts/sync-schemas.ts"
|
||||
|
||||
concurrency:
|
||||
group: docs-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
name: Validate docs
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Check schema mirror (core → docs)
|
||||
run: npx tsx scripts/sync-schemas.ts --check
|
||||
|
||||
- name: Validate build
|
||||
working-directory: docs
|
||||
run: npx mint validate
|
||||
|
||||
- name: Check broken links
|
||||
working-directory: docs
|
||||
run: npx mint broken-links
|
||||
@@ -0,0 +1,55 @@
|
||||
# Validates the experimental fast-capture (drawElementImage) VIDEO path on a
|
||||
# native amd64 Linux runner — where chrome-headless-shell's per-frame BeginFrame
|
||||
# drives a real paint each frame, so drawElementImage's snapshot is fresh and
|
||||
# video captures correctly. This is the one part of the feature that could not be
|
||||
# validated locally (macOS has no BeginFrame; Docker-on-rosetta hung).
|
||||
# See docs/fast-capture-limitations.md (Limitation 2).
|
||||
#
|
||||
# Manual trigger: Actions → "Fast-capture video validation" → Run workflow.
|
||||
name: Fast-capture video validation
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
# Manual only. NOTE: currently fails by design — fast capture cannot capture
|
||||
# video on any platform yet (see docs/fast-capture-limitations.md, Limitation 2).
|
||||
# This is the regression gate for if/when fast video is implemented.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
composition:
|
||||
description: Test composition to render (must contain <video>)
|
||||
default: sub-composition-video
|
||||
min_psnr:
|
||||
description: Min fast-vs-baseline PSNR (dB) to pass
|
||||
default: "25"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
- name: Build test Docker image (cached)
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # 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=max,scope=regression-test-image
|
||||
|
||||
- name: Validate fast-capture video (drawElement + BeginFrame)
|
||||
run: |
|
||||
docker run --rm \
|
||||
--security-opt seccomp=unconfined \
|
||||
--shm-size=4g \
|
||||
-e PRODUCER_VALIDATE_COMP='${{ inputs.composition }}' \
|
||||
-e PRODUCER_VALIDATE_MIN_PSNR='${{ inputs.min_psnr }}' \
|
||||
--workdir /app/packages/producer \
|
||||
--entrypoint bunx \
|
||||
hyperframes-producer:test tsx scripts/validate-fast-video.ts
|
||||
@@ -0,0 +1,81 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #0a0a0a;
|
||||
color: #f4eee4;
|
||||
font-family:
|
||||
system-ui,
|
||||
-apple-system,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
gap: 40px;
|
||||
}
|
||||
#headline {
|
||||
font-size: 180px;
|
||||
font-weight: 900;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 0.9;
|
||||
color: #c7c4f7;
|
||||
text-align: center;
|
||||
}
|
||||
#sub {
|
||||
font-size: 36px;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
color: #f4eee4;
|
||||
opacity: 0.7;
|
||||
}
|
||||
#bar {
|
||||
width: 560px;
|
||||
height: 4px;
|
||||
background: #9b95f0;
|
||||
transform-origin: left center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="main"
|
||||
data-start="0"
|
||||
data-duration="8"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<div id="headline">HYPERFRAMES</div>
|
||||
<div id="sub">RENDERED ON WINDOWS</div>
|
||||
<div id="bar"></div>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
tl.from("#headline", { y: 80, opacity: 0, duration: 0.9, ease: "power3.out" }, 0.2);
|
||||
tl.from("#sub", { y: 40, opacity: 0, duration: 0.7, ease: "power2.out" }, 0.6);
|
||||
tl.fromTo("#bar", { scaleX: 0 }, { scaleX: 1, duration: 1.2, ease: "power4.inOut" }, 1.0);
|
||||
tl.to("#headline", { letterSpacing: "-0.01em", duration: 4.0, ease: "sine.inOut" }, 2.5);
|
||||
tl.to(["#headline", "#sub", "#bar"], { opacity: 0, duration: 0.8, ease: "power2.in" }, 7.0);
|
||||
window.__timelines["main"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,167 @@
|
||||
name: Player perf
|
||||
|
||||
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"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
concurrency:
|
||||
group: player-perf-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
perf: ${{ steps.filter.outputs.perf }}
|
||||
steps:
|
||||
# Force git-based change detection instead of the pull_request REST API.
|
||||
# The API path can fail the perf workflow on transient listFiles timeouts.
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
filters: |
|
||||
perf:
|
||||
- "packages/player/**"
|
||||
- "packages/core/**"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- ".github/workflows/player-perf.yml"
|
||||
|
||||
preflight:
|
||||
name: Preflight (lint + format)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.perf == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: ./.github/actions/preflight
|
||||
|
||||
perf-shards:
|
||||
name: "Perf: ${{ matrix.shard }}"
|
||||
needs: [changes, preflight]
|
||||
if: needs.changes.outputs.perf == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- shard: load
|
||||
scenarios: load
|
||||
runs: "5"
|
||||
- shard: fps
|
||||
scenarios: fps
|
||||
runs: "3"
|
||||
- shard: scrub
|
||||
scenarios: scrub
|
||||
runs: "3"
|
||||
- shard: drift
|
||||
scenarios: drift
|
||||
runs: "3"
|
||||
- shard: parity
|
||||
scenarios: parity
|
||||
runs: "3"
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
# Player perf loads packages/player/dist/hyperframes-player.global.js
|
||||
# and packages/core/dist/hyperframe.runtime.iife.js, so a full build is required.
|
||||
- run: bun run build
|
||||
|
||||
- name: Set up Chrome (headless shell)
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
# The parity scenario shells out to `ffmpeg -lavfi ssim` to score the
|
||||
# live-playback frame against the sync-seek reference frame. ffmpeg is
|
||||
# not on the default ubuntu-latest runner image, and a missing binary
|
||||
# surfaces as ENOENT inside computeSsim() — informative, but cheaper
|
||||
# to just install it here so the shard never trips on infra.
|
||||
- name: Install ffmpeg (parity shard only)
|
||||
if: matrix.shard == 'parity'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends ffmpeg
|
||||
ffmpeg -version | head -n 1
|
||||
|
||||
- name: Run player perf — ${{ matrix.shard }} (measure mode)
|
||||
working-directory: packages/player
|
||||
env:
|
||||
PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
run: |
|
||||
bun run perf \
|
||||
--mode=measure \
|
||||
--scenarios=${{ matrix.scenarios }} \
|
||||
--runs=${{ matrix.runs }}
|
||||
|
||||
- name: Upload perf results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: player-perf-${{ matrix.shard }}
|
||||
path: packages/player/tests/perf/results/
|
||||
if-no-files-found: warn
|
||||
retention-days: 30
|
||||
|
||||
# Summary job — matches the required check name in branch protection.
|
||||
# Logs an explicit "skipped" / "passed" / "failed" line both to stdout and to
|
||||
# $GITHUB_STEP_SUMMARY so a false skip is obvious in the Checks UI without
|
||||
# having to dig into the changes-job logs.
|
||||
player-perf:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, perf-shards]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check results
|
||||
env:
|
||||
PERF_FILTER_RESULT: ${{ needs.changes.outputs.perf }}
|
||||
PERF_SHARDS_RESULT: ${{ needs.perf-shards.result }}
|
||||
run: |
|
||||
{
|
||||
echo "## Player perf gate"
|
||||
echo ""
|
||||
echo "- paths-filter \`perf\` matched: \`${PERF_FILTER_RESULT}\`"
|
||||
echo "- perf-shards result: \`${PERF_SHARDS_RESULT}\`"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "${PERF_FILTER_RESULT}" != "true" ]; then
|
||||
echo "::notice title=Player perf::SKIPPED — no changes under packages/player/**, packages/core/**, package.json, bun.lock, or .github/workflows/player-perf.yml. Auto-pass."
|
||||
echo "**Status:** SKIPPED (no player/core changes — auto-pass)" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${PERF_SHARDS_RESULT}" != "success" ]; then
|
||||
echo "::error title=Player perf::FAILED — perf-shards result was '${PERF_SHARDS_RESULT}'. See the per-shard logs above."
|
||||
echo "**Status:** FAILED (perf-shards result: \`${PERF_SHARDS_RESULT}\`)" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "::notice title=Player perf::PASSED — all perf shards completed successfully."
|
||||
echo "**Status:** PASSED" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,163 @@
|
||||
name: preview-regression
|
||||
|
||||
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"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: preview-regression-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
name: Detect changes
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
outputs:
|
||||
preview: ${{ steps.filter.outputs.preview }}
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
filters: |
|
||||
preview:
|
||||
- "packages/core/**"
|
||||
- "packages/parsers/**"
|
||||
- "packages/studio-server/**"
|
||||
- "packages/lint/**"
|
||||
- "packages/player/**"
|
||||
- "packages/studio/**"
|
||||
- "packages/cli/**"
|
||||
- "packages/producer/src/parity-harness.ts"
|
||||
- "packages/producer/src/parity-fixtures.ts"
|
||||
- "packages/producer/tests/parity/**"
|
||||
- "package.json"
|
||||
- "bun.lock"
|
||||
- ".github/workflows/preview-regression.yml"
|
||||
|
||||
preflight:
|
||||
name: Preflight (lint + format)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.preview == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: ./.github/actions/preflight
|
||||
|
||||
preview-parity:
|
||||
name: Preview parity
|
||||
needs: [changes, preflight]
|
||||
if: needs.changes.outputs.preview == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- run: bun install --frozen-lockfile
|
||||
|
||||
- name: Build workspace packages (required for vite config loading)
|
||||
run: |
|
||||
bun run --filter '@hyperframes/{parsers,lint,studio-server}' build
|
||||
bun run --cwd packages/core build
|
||||
|
||||
- name: Run Studio preview routing regression
|
||||
run: |
|
||||
bun run --cwd packages/studio test -- vite.thumbnail.test.ts src/utils/projectRouting.test.ts src/utils/frameCapture.test.ts
|
||||
bun run --cwd packages/studio-server test -- src/routes/thumbnail.test.ts
|
||||
|
||||
- name: Build preview runtime
|
||||
run: bun run --cwd packages/core build:hyperframes-runtime
|
||||
|
||||
- name: Prepare parity fixtures
|
||||
run: bun run --cwd packages/producer parity:fixtures:ci
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg
|
||||
|
||||
- name: Set up Chrome
|
||||
id: setup-chrome
|
||||
uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1
|
||||
with:
|
||||
chrome-version: stable
|
||||
|
||||
- name: Start parity fixture server
|
||||
run: |
|
||||
cd packages/producer/tests/parity/fixtures
|
||||
python3 -m http.server 4173 --bind 127.0.0.1 > /tmp/preview-parity-http.log 2>&1 &
|
||||
echo "$!" > /tmp/preview-parity-http.pid
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS http://127.0.0.1:4173/minimal-wysiwyg.html >/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
cat /tmp/preview-parity-http.log
|
||||
exit 1
|
||||
|
||||
- name: Run preview parity check
|
||||
working-directory: packages/producer
|
||||
env:
|
||||
PUPPETEER_EXECUTABLE_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
|
||||
run: bun run parity:check:ci
|
||||
|
||||
- name: Upload parity artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: preview-parity-artifacts
|
||||
path: packages/producer/.debug/parity-harness-ci/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
|
||||
preview-regression:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, preview-parity]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check results
|
||||
env:
|
||||
PREVIEW_FILTER_RESULT: ${{ needs.changes.outputs.preview }}
|
||||
PREVIEW_PARITY_RESULT: ${{ needs.preview-parity.result }}
|
||||
run: |
|
||||
{
|
||||
echo "## Preview regression gate"
|
||||
echo ""
|
||||
echo "- paths-filter \`preview\` matched: \`${PREVIEW_FILTER_RESULT}\`"
|
||||
echo "- preview-parity result: \`${PREVIEW_PARITY_RESULT}\`"
|
||||
echo ""
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
if [ "${PREVIEW_FILTER_RESULT}" != "true" ]; then
|
||||
echo "::notice title=Preview regression::SKIPPED — no preview/runtime changes. Auto-pass."
|
||||
echo "**Status:** SKIPPED (no preview/runtime changes — auto-pass)" >> "$GITHUB_STEP_SUMMARY"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${PREVIEW_PARITY_RESULT}" != "success" ]; then
|
||||
echo "**Status:** FAILED" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "Preview parity check failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "**Status:** PASSED" >> "$GITHUB_STEP_SUMMARY"
|
||||
@@ -0,0 +1,191 @@
|
||||
name: Publish to npm
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Version to publish (e.g. 0.4.11). Tag v<version> must already exist."
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
environment: npm-publish
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
# Run on tag push, manual dispatch, OR when a release/* PR is merged
|
||||
if: >-
|
||||
github.event_name == 'push' ||
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.pull_request.merged == true &&
|
||||
startsWith(github.event.pull_request.head.ref, 'release/v'))
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# On manual dispatch, check out the existing tag so we publish the
|
||||
# exact commit that was tagged — not whatever is currently on main.
|
||||
ref: >-
|
||||
${{ github.event_name == 'workflow_dispatch'
|
||||
&& format('refs/tags/v{0}', inputs.version)
|
||||
|| github.ref }}
|
||||
|
||||
- name: Resolve version
|
||||
id: version
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then
|
||||
VERSION="${INPUT_VERSION}"
|
||||
VERSION="${VERSION#v}"
|
||||
else
|
||||
BRANCH="${PR_HEAD_REF}"
|
||||
VERSION="${BRANCH#release/v}"
|
||||
fi
|
||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Detect pre-release tag (e.g. 0.1.16-alpha.1 → alpha, 0.1.16-beta.2 → beta)
|
||||
if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then
|
||||
DIST_TAG="${BASH_REMATCH[1]}"
|
||||
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
DIST_TAG="latest"
|
||||
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved version=${VERSION} dist_tag=${DIST_TAG}"
|
||||
|
||||
- name: Validate release channel
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
DIST_TAG: ${{ steps.version.outputs.dist_tag }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: node scripts/validate-release-channel.mjs
|
||||
|
||||
- name: Create release tag
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
git tag "v$VERSION"
|
||||
git push origin "v$VERSION"
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- run: corepack enable
|
||||
- run: corepack prepare pnpm@10.17.1 --activate
|
||||
- run: bun install --frozen-lockfile
|
||||
- run: bun run build
|
||||
- run: bun run verify:packed-manifests
|
||||
|
||||
- name: Publish packages
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
DIST_TAG: ${{ steps.version.outputs.dist_tag }}
|
||||
run: |
|
||||
FAILED=0
|
||||
|
||||
publish_pkg() {
|
||||
local filter="$1"
|
||||
local name="$2"
|
||||
|
||||
# Check if this version is already published
|
||||
if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then
|
||||
echo "⏭️ ${name}@${VERSION} already published — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "📦 Publishing ${name}@${VERSION}..."
|
||||
if pnpm --filter "$filter" publish --access public --no-git-checks --tag "$DIST_TAG"; then
|
||||
echo "✅ ${name}@${VERSION} published"
|
||||
else
|
||||
echo "❌ ${name}@${VERSION} failed to publish"
|
||||
FAILED=1
|
||||
fi
|
||||
}
|
||||
|
||||
publish_pkg "@hyperframes/parsers" "@hyperframes/parsers"
|
||||
publish_pkg "@hyperframes/lint" "@hyperframes/lint"
|
||||
publish_pkg "@hyperframes/studio-server" "@hyperframes/studio-server"
|
||||
publish_pkg "@hyperframes/core" "@hyperframes/core"
|
||||
publish_pkg "@hyperframes/sdk" "@hyperframes/sdk"
|
||||
publish_pkg "@hyperframes/engine" "@hyperframes/engine"
|
||||
publish_pkg "@hyperframes/player" "@hyperframes/player"
|
||||
publish_pkg "@hyperframes/producer" "@hyperframes/producer"
|
||||
publish_pkg "@hyperframes/shader-transitions" "@hyperframes/shader-transitions"
|
||||
publish_pkg "@hyperframes/studio" "@hyperframes/studio"
|
||||
publish_pkg "@hyperframes/aws-lambda" "@hyperframes/aws-lambda"
|
||||
publish_pkg "@hyperframes/gcp-cloud-run" "@hyperframes/gcp-cloud-run"
|
||||
|
||||
# CLI is @hyperframes/cli in the monorepo but published as unscoped "hyperframes" on npm.
|
||||
# Rewrite the name in package.json before publishing, then use npm publish directly
|
||||
# since pnpm --filter won't match the rewritten name.
|
||||
if npm view "hyperframes@${VERSION}" version >/dev/null 2>&1; then
|
||||
echo "⏭️ hyperframes@${VERSION} already published — skipping"
|
||||
else
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const p = 'packages/cli/package.json';
|
||||
const pkg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
||||
pkg.name = 'hyperframes';
|
||||
fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
echo "📦 Publishing hyperframes@${VERSION}..."
|
||||
if (cd packages/cli && npm publish --access public --tag "$DIST_TAG"); then
|
||||
echo "✅ hyperframes@${VERSION} published"
|
||||
else
|
||||
echo "❌ hyperframes@${VERSION} failed to publish"
|
||||
FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
echo "::error::One or more packages failed to publish"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
PRERELEASE: ${{ steps.version.outputs.prerelease }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
NOTES_FILE="releases/v${VERSION}.md"
|
||||
|
||||
# Skip if release already exists (idempotent re-runs)
|
||||
if gh release view "v${VERSION}" --repo "$REPO" >/dev/null 2>&1; then
|
||||
echo "Release v${VERSION} already exists — skipping"
|
||||
else
|
||||
FLAGS=(--repo "$REPO" --title "v${VERSION}")
|
||||
if [ -f "$NOTES_FILE" ]; then
|
||||
FLAGS+=(--notes-file "$NOTES_FILE")
|
||||
else
|
||||
echo "No reviewed release notes found at $NOTES_FILE — using GitHub generated notes"
|
||||
FLAGS+=(--generate-notes)
|
||||
fi
|
||||
if [ "$PRERELEASE" = "true" ]; then
|
||||
FLAGS+=(--prerelease)
|
||||
fi
|
||||
gh release create "v${VERSION}" "${FLAGS[@]}"
|
||||
fi
|
||||
@@ -0,0 +1,159 @@
|
||||
name: regression
|
||||
|
||||
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"
|
||||
|
||||
# Graphite can update a branch and its PR base separately for the same head
|
||||
# SHA. Keep only the newest expensive regression run for each PR/ref.
|
||||
concurrency:
|
||||
group: regression-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
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 whole workflow on transient listFiles
|
||||
# timeouts before any regression shard even starts.
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
token: ""
|
||||
filters: |
|
||||
code:
|
||||
- "packages/core/**"
|
||||
- "packages/producer/**"
|
||||
- "packages/engine/**"
|
||||
- "Dockerfile*"
|
||||
|
||||
preflight:
|
||||
name: Preflight (lint + format)
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
- uses: ./.github/actions/preflight
|
||||
|
||||
regression-shards:
|
||||
needs: [changes, preflight]
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
# Shards are bin-packed by measured per-test duration (LPT heuristic on
|
||||
# CI run 25893372795) so each row carries ~15-16 min of work. When a
|
||||
# new fixture lands, drop it into the currently-lightest shard or
|
||||
# re-balance against fresh `test_suite_summary` timings. Worst-shard
|
||||
# work time was 19.3 min under the old tag-based split (styles-e);
|
||||
# the rebalance brings every shard within ~40s of the others.
|
||||
include:
|
||||
- shard: shard-1
|
||||
args: "hdr-regression style-5-prod style-3-prod mov-prores"
|
||||
- shard: shard-2
|
||||
args: "style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets"
|
||||
- shard: shard-3
|
||||
args: "style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9"
|
||||
- shard: shard-4
|
||||
args: "style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr"
|
||||
- shard: shard-5
|
||||
args: "style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression"
|
||||
- shard: shard-6
|
||||
args: "overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed"
|
||||
- shard: shard-7
|
||||
args: "sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector"
|
||||
- shard: shard-8
|
||||
args: "style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity"
|
||||
steps:
|
||||
- name: Checkout (with LFS)
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # 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: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
||||
- name: Build test Docker image (cached)
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.test
|
||||
load: true
|
||||
tags: hyperframes-producer:test
|
||||
cache-from: type=gha,scope=regression-test-image
|
||||
# PR matrices can fan out across many stacked branches at once. Let
|
||||
# them consume the shared cache, but keep a single writer on main so
|
||||
# concurrent exports cannot exhaust the Actions cache service.
|
||||
cache-to: ${{ github.event_name == 'push' && 'type=gha,mode=max,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@ea165f8d65b6e75b540449e92b4886f43607fa02 # 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:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changes, regression-shards]
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check results
|
||||
run: |
|
||||
if [ "${{ needs.changes.outputs.code }}" != "true" ]; then
|
||||
echo "No code changes — skipping regression (auto-pass)"
|
||||
exit 0
|
||||
fi
|
||||
if [ "${{ needs.regression-shards.result }}" != "success" ]; then
|
||||
echo "One or more regression shards failed"
|
||||
exit 1
|
||||
fi
|
||||
@@ -0,0 +1,66 @@
|
||||
name: Sync skills to ClawHub
|
||||
|
||||
# Publishes changed skills under skills/ to ClawHub (https://clawhub.ai/heygen-com)
|
||||
# whenever they land on main. `clawhub sync` only publishes skills whose content
|
||||
# changed vs. the registry, auto-bumping the patch version — unchanged skills are
|
||||
# a no-op, so this is safe to run on every push.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "skills/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: "Preview what would publish without publishing"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Serialize runs so two pushes don't race on the same skill version.
|
||||
concurrency:
|
||||
group: clawhub-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Publish changed skills
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
CLAWHUB_DISABLE_TELEMETRY: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install ClawHub CLI
|
||||
run: npm i -g clawhub@0.23.1
|
||||
|
||||
- name: Authenticate
|
||||
env:
|
||||
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
|
||||
run: clawhub login --token "$CLAWHUB_TOKEN" --no-input --label "hyperframes CI"
|
||||
|
||||
- name: Sync skills
|
||||
env:
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
run: |
|
||||
FLAGS=(
|
||||
--all
|
||||
--owner heygen-com
|
||||
--bump patch
|
||||
--changelog "Synced from ${GITHUB_SHA:0:7} (${GITHUB_REF_NAME})"
|
||||
--source-repo "$GITHUB_REPOSITORY"
|
||||
--source-commit "$GITHUB_SHA"
|
||||
--source-ref "$GITHUB_REF"
|
||||
)
|
||||
if [ "$DRY_RUN" = "true" ]; then
|
||||
FLAGS+=(--dry-run)
|
||||
fi
|
||||
clawhub sync "${FLAGS[@]}"
|
||||
@@ -0,0 +1,450 @@
|
||||
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
|
||||
# `<flags> <name> <description>`, 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
|
||||
|
||||
@'
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Issue 574 reused video regression</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
#root {
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="root"
|
||||
data-start="0"
|
||||
data-duration="12"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
>
|
||||
<video id="video1" src="1.mp4" data-start="0" muted data-duration="4" data-track-index="0" data-media-start="0"></video>
|
||||
<video id="video2" src="1.mp4" data-start="4" muted data-duration="4" data-track-index="0" data-media-start="4"></video>
|
||||
<video id="video3" src="1.mp4" data-start="8" muted data-duration="4" data-track-index="0" data-media-start="8"></video>
|
||||
</div>
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'@ | 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
|
||||
Reference in New Issue
Block a user