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
+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