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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:35 +08:00
commit 85453da49f
4031 changed files with 710987 additions and 0 deletions
+81
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+36
View File
@@ -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
+31
View File
@@ -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"
+64
View File
@@ -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"
+19
View File
@@ -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)
+43
View File
@@ -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:
- "*"
+42
View File
@@ -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//"]
}
]
}
+96
View File
@@ -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
+682
View File
@@ -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
+55
View File
@@ -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 }}"
+52
View File
@@ -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>
+167
View File
@@ -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"
+163
View File
@@ -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"
+191
View File
@@ -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
+159
View File
@@ -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[@]}"
+450
View File
@@ -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