fix/cli-bin-executable
123 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
696cbdbbd0 |
chore(skills): package Codex plugin upload (#2668)
* chore(skills): package Codex plugin upload * chore(skills): harden Codex plugin content * fix(skills): satisfy plugin quality gates * fix(skills): address plugin packaging review * fix(plugin): simplify asset validation * fix(skills): correct embedded-captions catalog count to 35 after nightcity removal The nightcity theme removal left SKILL.md claiming 36 identities in four places, including the frontmatter description the router reads. The catalog now has 35 entries (10 classic + 25 themed). --------- Co-authored-by: Miao Yang <miao.yang@heygen.com> |
||
|
|
6b4df032fe | ci: rerun regression checks after PR base edits (#2659) | ||
|
|
e96ebd74de |
feat(skills): add changelog-video skill for repo-native CC + Codex discovery (#2552)
Packages Jake Moran's changelog-video pipeline (v1, validated end-to-end
by Home on the Jun 23-29 range) as a repo-native skill set that Claude
Code (.claude/skills/) and Codex CLI (.agents/skills/) auto-discover the
moment the repo is opened. No install step; run the skill against a
changelog markdown for a given git range and it produces a lint-clean,
seam-gate-green 1080x1080 MP4 (~45-60s, Annie VO, mock-UI visualizations,
caption rail) end-to-end.
Six skills added byte-identical in both mirror dirs:
- changelog-video (pipeline entry point)
- motion-doctrine (carries seam-stamp.mjs + seam-gate.mjs)
- cut-the-curve, captions-overlay, seam-craft, oversized-cursor
Layout:
- .claude/skills/ - Claude Code project-local auto-discover
- .agents/skills/ - Codex CLI project-local auto-discover (verified via
Magi's clean-home Codex 0.144.3 repro; NOT .codex/skills/)
Fonts, animated background (12 MB), house BGM (5 MB), lexicon, and
align-captions ship inside the skill dirs. .gitattributes routes only
.claude/skills/**/*.{mp4,mp3} + .agents/skills/**/*.{mp4,mp3} through
LFS — narrowly scoped so unrelated Player, Studio, registry, and
marketplace media stay put. HeyGen CLI auth is the one credential the
skill needs; Node >= 22, ffmpeg, and headless Chrome are documented
alongside in both READMEs.
.gitignore: rewrites .claude/ and .agents/ blocks to keep agent-installed
skill hygiene while re-including the six repo-native skill dirs plus
README.md.
CI:
- Extends changes.skills filter to match .claude/skills/**,
.agents/skills/**, scripts/lint-skills.ts, and scripts/check-skill-mirror.mjs.
- New 'Skills: project-native lint + mirror' job runs the extended
lint-skills.ts (schema-driven; required { name, description } + optional
{ license, allowed-tools, metadata }, name pattern check, description
length check) plus a new check-skill-mirror.mjs byte-integrity script
(24 mirrored files must match; README.md deliberately per-CLI).
- Wired into 'bun run lint' locally.
Frontmatter validator:
- Rejects unsupported top-level keys (catches category:-style drift).
- Requires name + description.
- Validates name pattern (^[a-z][a-z0-9-]{0,63}$) and description shape
(non-empty, <=1024 chars).
- Missing frontmatter block itself is a first-class error.
Also strips unsupported top-level 'category:' frontmatter from Jake's
motion-doctrine and cut-the-curve SKILL.mds (both mirrors), rewrites the
TTS invocation from ~/.claude/skills/media-use/... to the tracked
skills/hyperframes-media/scripts/heygen-tts.mjs, swaps npx hyperframes@latest
for the repo-local CLI in the gate step, and fixes a lint issue in Jake's
seam-gate.mjs (ternary-for-side-effect -> if/else).
Validated end-to-end by Home on Jun 23-29 (MP4 posted in C0ACCNHLG3U
thread 1784181166.041319). Independently reviewed R1/R2/R3 by Magi.
Co-authored-by: Jake Moran <jake@heygen.com>
|
||
|
|
83db364f81 | test(repo): enforce workspace contracts | ||
|
|
0af07a07c5 | fix(core): enforce strict runtime safety | ||
|
|
9e7b11998c | test(producer): gate source tests by execution lane | ||
|
|
585aa9f6b2 | chore(repo): forbid tracked generated artifacts | ||
|
|
e04f6dda37 |
feat(engine,cli): drawElement fast-capture config + CLI flag (#1916)
## drawElement fast-capture — config + CLI flag (stack 1/6) Foundation layer for the drawElement fast-capture feature: the config surface and CLI/Docker plumbing that the rest of the stack builds on. ### What this adds - **`packages/engine/src/config.ts`** — new config fields for fast capture: `useDrawElement` / `enableDrawElementWorkerEncode` (macOS-GPU `drawElementImage` capture + worker-offloaded JPEG encode), resolved from env in `resolveConfig` (env `HF_DE_WORKER_ENCODE`). Wired alongside main's existing `staticFrameDedup` (unified downstream in 4/6). - **`packages/cli/src/commands/render.ts`** — `--experimental-fast-capture` flag → sets `experimentalFastCapture`; `--debug` passthrough. - **`packages/cli/src/utils/dockerRunArgs.ts`** — pass the fast-capture env through to the container. - **`.github/workflows/fast-video-validation.yml`** — CI job validating fast-capture renders. - `.oxlintrc.json` / `.fallowrc.jsonc` — ignore-pattern housekeeping for the new paths. ### Notes - Config-only + entrypoint; no capture behavior yet (that's 2/6–4/6). - Tests: `config.test.ts`, `dockerRunArgs.test.ts` added. --- **Stack (drawElement fast-capture, rebased onto current `main`, supersedes #1295 + #1444):** 1. **#1916 config + CLI** ← you are here 2. #1917 drawElementImage capture service 3. #1918 3D projection + compositor-effect risk gate 4. #1919 frame-capture core (routing, worker-encode, static-dedup unification) 5. #1920 producer render stages + remote bg-image localizer 6. #1921 lint rule + player media sync ⚠️ Intermediate PRs (1–5) are split by package boundary for review and **do not each compile independently** (cross-file deps); the complete feature is green at the stack tip (#1921) — tsc-clean on engine + producer, 231 tests pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
8f0bfef757 |
ci: auto-publish changed skills to ClawHub on push to main (#1835)
* ci: sync changed skills to ClawHub on push to main Add a GitHub Actions workflow that runs `clawhub sync` whenever skills/** changes on main, publishing only changed skills to ClawHub (https://clawhub.ai/heygen-com) under the heygen-com publisher and auto-bumping the patch version. Unchanged skills are a no-op, so it is safe to run on every push. Requires the CLAWHUB_TOKEN repo secret. * ci: use Node 22 to match the CI fleet's LTS Address review on #1835 (Miga): the rest of the CI fleet (ci.yml, windows-render, player-perf, preview-regression, docs, catalog-previews) pins setup-node to Node 22 LTS. Node 24 is current, not LTS, and could introduce subtle differences. Align this workflow to 22. |
||
|
|
e73076e93c | fix(core): publish runtime inline artifact (#1787) | ||
|
|
bf630bfe1e |
fix(cli): always check GitHub skills on init while skills.sh syncs (#1768)
* fix(cli): always check GitHub skills on init while skills.sh syncs The "don't pass --skip-skills" guidance lives in SKILL.md, which ships through the laggy skills.sh registry and can't be relied on to reach the agent — so an agent that improvises `--skip-skills` silently dodges the GitHub skills freshness pull. Put the guarantee in the CLI instead (the one channel that updates promptly via `npx hyperframes@latest`): - Neuter the `--skip-skills` FLAG so it no longer skips the check; gate skipping on the HYPERFRAMES_SKIP_SKILLS=1 env var instead (the agent/user CLI path never sets it). Print a one-line notice when the ignored flag is passed. - Wire the env escape hatch into the init test helper (one place) and the CI smoke-test / windows-canary steps so they stay offline and fast. - Update the skill docs that previously told agents `--skip-skills` opts out. Temporary measure while skills.sh catches up — revert init.ts's `skipSkills` to `args["skip-skills"] === true` once it does (noted inline). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): build @hyperframes/lint before core in Test and Studio jobs The lint extraction (#1756) made @hyperframes/lint a runtime dependency of core — core's compiled compiler/staticGuard.js imports it via the package's "node" export condition (./dist/index.js). But the Test and Studio-load-smoke jobs pre-build only @hyperframes/{parsers,studio-server} before packages/core, so loading core's dist at test / dev-server time fails with: ERR_MODULE_NOT_FOUND: Cannot find module .../@hyperframes/lint/dist/index.js imported from .../packages/core/dist/compiler/staticGuard.js Build the canonical pre-core set @hyperframes/{parsers,lint,studio-server} (the glob the root build script uses) in both jobs so it can't drift again. The SDK job is left as-is — it builds parsers+core only and passes. Reproduced locally: removing packages/lint/dist reproduces the exact ERR_MODULE_NOT_FOUND; building lint resolves it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): address PR #1768 review — stale comment + harden offline init - Update the stale interactive-path comment that still said "Opt out with --skip-skills"; the flag is neutered, opt-out is HYPERFRAMES_SKIP_SKILLS=1. - Wrap installAllSkills in ensureSkillsCurrent with try/catch. installAllSkills is already non-strict (swallows its own failures), but since --skip-skills no longer escapes this path, every init — including offline ones that fall through to "install anyway" — runs it. The guard guarantees a skills-install failure only warns and proceeds, never breaks init. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7a4853dfe6 |
refactor: extract @hyperframes/studio-server from core (#1757)
* refactor: extract @hyperframes/studio-server package from core Moves all studio-api routes, helpers, and Hono server wiring from packages/core/src/studio-api/ into a new standalone packages/studio-server package (@hyperframes/studio-server). Core keeps thin re-export stubs at @hyperframes/core/studio-api and the subpath helpers (screenshot-clip, draft-markers, etc.) for backward compatibility. Consumer imports (cli studioServer, vite adapter/config, producer htmlCompiler, studio manualEditsTypes) are updated to import from @hyperframes/studio-server directly. Also exports rewriteInlineStyleAssetUrls from @hyperframes/core root (was in compiler/rewriteSubCompPaths.ts but not re-exported), required by @hyperframes/studio-server/helpers/subComposition. Removes postcss-selector-parser from @hyperframes/core dependencies (moved to @hyperframes/studio-server which owns the routes that used it). Depends on @hyperframes/parsers (PR #1755). * fix(ci): add parsers+studio-server to Dockerfile and build before preview tests * fix(ci): build @hyperframes/studio-server before Test and studio load smoke Studio's vite.config.ts imports @hyperframes/studio-server, which resolves via its "node" export condition to built dist. The Test and studio-load-smoke jobs only built parsers + core, so esbuild's config load failed to resolve the package entry. Build studio-server too. * fix(studio): repoint sdkCutoverParity test import to studio-server sourceMutation moved from core's studio-api to @hyperframes/studio-server; the test still imported the deleted core path. This was masked while studio's vite.config failed to load (couldn't resolve studio-server); now that the config loads, the test runs and the stale import surfaced. |
||
|
|
98d0bdd73c |
refactor: extract @hyperframes/lint from core (#1756)
* refactor: extract @hyperframes/lint package from core Moves all lint rules, hyperframeLinter, lintProject, and related types from packages/core/src/lint/ into a new standalone packages/lint package. Core keeps a thin re-export stub at @hyperframes/core/lint for backward compatibility. Consumer imports (cli lint command, producer hyperframeLint) are updated to import from @hyperframes/lint directly. Depends on @hyperframes/parsers (PR #1755). * fix: restore postcss-selector-parser in core (sourceMutation.ts still uses it) * fix(ci): add parsers+lint to Dockerfile and build before preview tests * chore: update bun.lock after restoring postcss-selector-parser dep * test(cli): update lintProject test for string-dir signature from @hyperframes/lint * refactor(core): single-source the lint engine in @hyperframes/lint Delete core's byte-identical copy of the lint rule engine and re-point staticGuard at @hyperframes/lint, so the render-time render-gate and the studio preview share one rule engine instead of two copies that could silently diverge. Back-compat preserved via the @hyperframes/core/lint stub. Addresses review feedback on the dual-copy footgun. |
||
|
|
cdf9c817e1 |
refactor: extract @hyperframes/parsers from core (#1755)
## Summary Extracts the GSAP parser/writer suite, HTML parser, hf-ids, spring-ease, and the shared composition data types out of `@hyperframes/core/src/parsers/` into a new, independently-publishable **`@hyperframes/parsers`** package. This is the foundation of the [#1749](https://github.com/heygen-com/hyperframes/issues/1749) effort: make HyperFrames' parsing/linting/validation usable as plain libraries in a Node app, without shelling out to the CLI. Parsers is the standalone base every other extracted package builds on. **Part 1 of 3** — splits #1754 into independently-reviewable pieces. Parts 2 (lint) and 3 (studio-server) stack on this branch. ## What moves | | | |---|---| | Source moved out of core | **~9,900 LOC** (`src/parsers/` → `packages/parsers/src/`) | | Total lines removed from core (incl. tests + goldens) | ~19,600 | | Files relocated | 39 | | Tests carried over | **660 passing** (5 skipped, 3 todo) | The big movers: `gsapParser` / `gsapParserAcorn` (the recast + acorn dual parsers), `gsapWriterAcorn`, `gsapSerialize`, `gsapUnroll`, `htmlParser`, `hfIds`, `springEase`, `stableIds`, plus the `__goldens__` corpus. ## Bundle footprint of the new package | Artifact | Size | |---|---| | `dist/` (unpacked) | 1.7 MB | | npm tarball (packed) | 409 KB | | `dist/index.js` | 90 KB (**~21 KB gzipped**) | | Heaviest entries | `gsapWriterAcorn.js` 93 KB · `gsapParser.js` 91 KB | Most of the weight is the GSAP AST machinery (recast/babel/acorn). It's tree-shakeable via subpath entries (`@hyperframes/parsers/hf-ids`, `/gsap-constants`, etc.) so a consumer that only needs `hf-ids` (2 KB) doesn't pull the parsers. ## How `@hyperframes/core` changes The interesting part: **core sheds its entire AST toolchain.** | core `dependencies` | before | after | |---|---|---| | count | 9 | 6 | | removed | — | `@babel/parser`, `acorn`, `acorn-walk`, `magic-string`, `recast` | | added | — | `@hyperframes/parsers`, `linkedom` | Before this PR, importing `@hyperframes/core` at all dragged in babel + recast + acorn just to construct types. Now those live behind `@hyperframes/parsers`, and a consumer that only wants core's runtime/compiler types never resolves the parser stack. Core keeps thin `@deprecated` re-export stubs at the old subpaths (`@hyperframes/core/gsap-parser`, `/gsap-constants`, …) so nothing downstream breaks. ## Design notes - **`"bun"` export condition before `"node"`** in every package export. Bun resolves the TypeScript source directly (no pre-built `dist/`), while Node/tsx/Docker contexts fall through to `"node"` → `dist/`. This keeps the dev loop zero-build while published artifacts stay Node-consumable. - `@hyperframes/parsers` is **standalone** — zero `@hyperframes/*` dependencies — so it can be the base of the stack. ## Test plan - [x] `bun run --filter @hyperframes/parsers test` — 660 tests pass - [x] `bun run --filter @hyperframes/sdk test` — 382 tests pass - [x] `bun run build` — full monorepo build succeeds - [x] Fallow audit passes on CI |
||
|
|
d70ee134cc |
feat(cli): add skills version check, update, and freshness manifest (#1738)
* feat(cli): add skills version check, update, and freshness manifest
Give the HyperFrames skill bundle a content fingerprint so agents and
users can tell whether installed skills are the latest version, on any
platform that can run the CLI.
- skills-manifest.json (repo root): per-skill sha256 over the whole skill
directory; minimal {source, skills}, no version/timestamp so it is fully
deterministic. Generated by scripts/gen-skills-manifest.ts.
- `hyperframes skills check` [--json]: compares installed skills to the
manifest; exits non-zero when something is outdated (agent/CI gate).
- `hyperframes skills update`: thin wrapper over `npx skills update`.
- Passive nudge on render/lint/validate when skills are stale (24h cache,
same opt-out as the CLI self-update notice).
- "latest" resolved via `git ls-remote` + SHA-pinned raw URL to dodge
GitHub raw-CDN lag, falling back to the main branch URL.
- CI job + lefthook hook keep skills-manifest.json in sync with skills/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): add execFile to child_process mock in skills test
skills.test.ts mocks node:child_process but only declared execFileSync
and spawn. Loading skills.js transitively loads skillsManifest.ts, which
runs promisify(execFile) at module load, so vitest threw on the missing
execFile named export. Add a bare stub — these tests never invoke it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init installs all skills; skills update pulls the full set
Make `hyperframes init` the single place skills are pulled in full, and
make "update" mean "get everything" rather than "refresh what's there".
- init now always installs/refreshes ALL skills (incl. ones not yet
present) instead of prompting "Install AI coding skills?" — opt out
with `init --skip-skills`. Both the interactive and non-interactive
paths pass `--all --yes` so the complete set is fetched.
- `hyperframes skills update` switches from `npx skills update` (which
only refreshes already-installed skills) to `skills add --all`, so it
installs missing skills too — the same install step init runs.
- SKILL.md documents init-installs-all and the new update semantics.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): skills check treats missing skills as needing an update
The full skill set is now the goal (init and `skills update` both pull
all, including ones not installed), so a partial install is no longer
"a choice" — it's something to fix.
- diffSkills: updateAvailable is now true when anything is outdated OR
missing (local-only still doesn't count). So `skills check` exits
non-zero — and renders "Update:" instead of "up to date" — whenever a
skill is missing, not just when one is stale.
- The passive render/lint/validate nudge follows suit: it now counts
missing alongside outdated ("N skills out of date or missing"),
tracked via a new skillsMissingCount cache field.
- SKILL.md documents the stricter check.
Note: platforms that intentionally vendor only a subset of skills (e.g.
a Codex snapshot) will now see check report non-zero.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): install/update skills straight from the GitHub repo
`skills add owner/repo` can resolve through the skills.sh registry, which
lags behind the repo — so `update` could install a stale version while
`check` (which resolves latest directly from GitHub) keeps reporting
"outdated", an endless loop.
Switch the install source to the full GitHub URL
(https://github.com/heygen-com/hyperframes), which makes `skills add`
git-clone the repo directly at latest main, bypassing the registry. This
covers `hyperframes skills`, `hyperframes skills update`, and `init`'s
skill install — all of which go through SOURCES. Now install/update and
check agree on what "latest" means.
The init "install skills" hint now points at `npx hyperframes skills
update` so the manual path uses the same GitHub-direct fetch.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(cli): init checks skills against GitHub, installs only when stale
`hyperframes init` now runs the skills version check first and only
(re)installs when something is outdated or missing — instead of
unconditionally re-pulling every time. Re-running init on an
already-current project is now a no-op ("skills are already up to date").
- New ensureSkillsCurrent() helper, shared by both the interactive and
non-interactive init paths (no duplicated install logic).
- The check resolves "latest" straight from GitHub (same source the
install uses); best-effort — if it can't reach GitHub it installs anyway.
- SKILL.md updated to describe the check-then-install behavior.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(cli): address skills manifest review feedback
From the PR review (points 1, 2, 4, 5):
1. Remove the `local-only` skill status. checkSkills only ever hashes
manifest-listed skills, so a local-only status could never appear in
the end-to-end output — and making it appear would wrongly flag
unrelated skills (the `.../skills` dir is shared across sources).
diffSkills now reports only on manifest skills; skills on disk that
aren't in the manifest are ignored.
2. Drop the redundant per-directory sort in listFilesSorted — the single
final out.sort() is what guarantees a deterministic hash (verified:
manifest unchanged).
4. resolveLatestManifest local-path detection now uses path.isAbsolute,
so Windows absolute paths (C:\...) are treated as local instead of
falling through to a remote fetch.
5. fetchManifest validates the response shape (asSkillsManifest) instead
of a blind `as` cast, so a CDN error page served as 200 fails with a
clear error rather than a cryptic crash later in diffSkills.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): strict skills update + auto-discover any agent host
Address PR review (Magi blocker + James/Rames robustness):
- Blocker (Magi): `skills update` is the documented recovery path for
`skills check || skills update`, but it delegated to installAllSkills()
which swallowed missing-npx and failed `skills add` as "skipped",
exiting 0 even when nothing changed. Add a strict mode that throws on
failure; update sets a non-zero exit (init stays best-effort). New tests
simulate a non-zero `skills add` (exit 1) and the success path.
- Robustness (James/Rames #2): the upstream `skills` CLI installs into
~72 agent conventions; a hard-coded list (4, or even 11) can't track
that. Replace defaultSkillRoots with discoverSkillRoots — it scans cwd +
$HOME for any `<host>/skills/<manifest-skill>/SKILL.md` (plus the XDG
`.config/<host>/skills`), so detection is structural and future-proof,
no closed list. agentFromDir infers the host from the path.
- Tests (Rames #3): temp-fixture detection tests for every convention ×
{project, global}, scope priority, claude-code preference, the
no-install case, the --dir override, and an unknown/new host (proving
the no-closed-list property).
- Docs (Rames #4/#5): SKILL.md notes init's best-effort GitHub round-trip;
findRepoManifest climbs 16 levels (was 8) for deep monorepos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): resolve CodeQL file-system race + de-flake Windows npx test
Two CI fixes:
- CodeQL (high, js/file-system-race) at gen-skills-manifest.ts: the
existsSync(outPath) precheck followed by writeFileSync(outPath) is a
check-then-write race. Read the committed manifest directly in a
try/catch instead (missing/unreadable ⇒ "no committed manifest"), so
there's no precheck to race against. Behavior is unchanged.
- Windows Tests: npxCommand.test.ts's real `npx --version` smoke test
cold-starts slower than vitest's 5s default on Windows runners and
timed out. Give the test 60s headroom (and a 30s exec timeout). Kept
as a real execution check — mocking would reduce it to a tautology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): repair garbled npx smoke-test timeout comment
The explanatory comment for the 60s timeout was scrambled across the
callback/timeout arguments, failing oxfmt --check (and thus preflight,
which in turn skipped preview-parity and failed the regression gate).
Move it above the it() call so it no longer sits between call arguments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0c05025c04 |
ci(skills): run skills/**/*.test.mjs in CI (#1724)
skills/**/*.test.mjs files (e.g. skills/media-use/scripts/resolve.test.mjs and skills/media-use/scripts/lib/manifest.test.mjs) are bare `node --test` files with only `node:` built-in imports. They aren't part of any workspace package, and the existing `Test` job's path filter (the `code` filter in the `changes` job) excludes `skills/**`, so even on PRs that touch only skills/ those tests never run. This matters for regression guards. The shell-injection probe test added in HF#1723 feeds probe() a filename containing `clip"; touch INJECTED; echo ".mp4` and asserts no marker file is created. The test passes locally but under the current job graph it would never run in CI on a follow-up skills/ change that re-introduces the bug. Closing the gap with a dedicated `Test: skills` job rather than relaxing the `code` filter. The existing `Test` job's steps run `bun run test:scripts` (hardcoded file list) and `bun run --filter '*' test` (workspace packages only), neither of which would actually execute skills tests even if the filter let `skills/**` through. The dedicated job needs no `bun install`, just node 22, since the tests only import from `node:` and relative paths. The discovery step shells out to `find` and fails loudly when zero test files match, so a future rename or layout change can't silently turn this into a no-op pass. Spotted by Via in HF#1723 review thread, confirmed by James as a separate follow-up rather than a blocker for HF#1723. -- Jerrai (https://claude.com/claude-code) |
||
|
|
041f2fa196 |
fix(media-use): kill shell command injection in probe/heygen-search/eval
Swap execSync(<shell-string>) → execFileSync(file, [argv]) in probe.mjs, heygen-search.mjs, and eval.mjs so hostile filenames / queries / manifest metadata can't inject shell. Adds probe.test.mjs regression guard and a CI Test (skills) job so it actually runs. Closes the media-use High/Critical scanner alert. |
||
|
|
60d3eeb1f7 |
test(producer): add stream duration parity check to regression harness (#1652)
Probes the rendered output for video and audio stream durations after render and fails the test if they differ by more than 0.5s. Catches mux-level truncation regressions like the ffmpeg -shortest bug (#1648) where one stream gets silently cut short. Runs on all non-png-sequence fixtures with audio — no new meta.json field needed since this is a universal invariant, not a per-fixture threshold. |
||
|
|
25b717dd9c | fix(cli): resolve npx shims on Windows (#1626) | ||
|
|
7607a714a6 |
ci(publish): publish @hyperframes/sdk to npm (#1587)
The SDK is version-bumped by scripts/set-version.ts (it's in the PACKAGES list) but was never added to the publish_pkg list in publish.yml — so @hyperframes/sdk@0.6.112 sits on the version line yet is absent from npm (404), while core/player/engine/etc. all shipped at 0.6.112. Add the missing publish call so the SDK ships with every release. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a241f2591e | fix(studio): break all 7 circular dependency cycles and fix rules-of-hooks violation (#1422) | ||
|
|
a0ee97210b |
fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors (#1350)
* fix(sdk,core): css tokenizer, override-set replay, setattribute safety, persist errors * test(sdk,ci): smoke test + explicit sdk-tests CI gate Smoke test covers the full public surface: openComposition → setStyle/setText/dispatch(moveElement) → serialize applyPatches + ORIGIN_APPLY_PATCHES tagging batch() coalescing + transactional rollback on throw undo/redo round-trip persist adapter write + persist:error surfacing T3 embedded mode: override-set apply on open + getOverrides round-trip Adds sdk-tests CI job so SDK coverage is explicitly named and required — prevents a repeat of the demo-next vitest-never-ran incident. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): export adapter types, awaitable flush(), never-coalesce mode - Export PersistAdapter, PreviewAdapter, PersistVersionEntry from package root — callers can now write typed fakes without reaching into internals - Add flush(): Promise<void> to Composition interface + CompositionImpl — app-close handlers can await a clean drain of the persist queue - coalesceMs <= 0 disables coalescing entirely in createHistory — enables deterministic test scenarios without per-entry timestamp manipulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sdk): p2 edge cases — setText no-text-node, override-remove non-existent, flush in smoke - setText on element with no prior text node (firstTextIdx=-1 path) - applyOverrideSet null removal on non-existent prop is a no-op (no throw) - smoke persist test uses comp.flush() instead of setTimeout - can() JSDoc clarifies Phase 3b false-return is intentional feature-detection Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: trigger regression suite * fix(ci): add packages/sdk/package.json to Dockerfile.test workspace copy bun install --frozen-lockfile fails in the regression Docker build because the lockfile references the sdk workspace member but its package.json was not copied into the image before the install step. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c12987e301 |
fix(producer): revert Proxy-based wrapTimeline to plain-object approach (#1284)
* fix(producer): revert Proxy-based wrapTimeline to plain-object approach The `new Proxy` wrapper for GSAP timelines introduced in #1279 causes Chrome headless to hang indefinitely during page.goto — DOMContentLoaded never fires. The plain-object approach (explicit method allowlist) loads in <800ms on the same composition. The Proxy's generic get/set traps interact badly with Chrome's internal object inspection (Symbol checks, thenable probing, DevTools serialization) during HTML parsing, creating a permanent navigation hang. The maybePublishRenderReady listener fix from #1279 is preserved — only the wrapTimeline implementation is reverted. Compositions using GSAP methods outside the allowlist (eventCallback, labels, repeat, etc.) will see those calls silently dropped rather than forwarded. This is the same behavior as v0.6.81 and earlier. A safer forwarding approach can be explored separately without blocking renders. * fix(producer): address review — stale meta.json descriptions + silently-dropped methods doc - three-boundary: description referenced Proxy fix but the test uses onUpdate in to() vars (allowlist path), not eventCallback - three-boundary-deferred: same — pins Bug 2's deferred-race, not Bug 1 - Add inline doc comment listing silently-dropped GSAP methods and the onUpdate workaround * ci: add page.goto timing canary to CLI smoke test Parse page.goto completion times from the render log and fail if the slowest navigation exceeds 5s. Catches wrapTimeline regressions that block DOMContentLoaded before the 60s timeout fires. Refs: #1285 * fix(producer): forward all GSAP methods via dynamic enumeration at wrap time Instead of silently dropping methods outside a static allowlist, enumerate the real timeline's prototype chain at wrap time and generate plain-object forwarding stubs for every method not already covered. This achieves the same coverage as the `new Proxy` approach from #1279 without the Chrome headless navigation hang — no Proxy trap surfaces are exposed to Chrome internals. Methods prefixed with `_` (GSAP private) are skipped. All forwarded methods flush pending batch operations before delegating, matching the existing allowlist behavior. Closes #1285 * fix(producer): make proxy non-thenable + harden CI canary - Skip `then` in forwardRemainingMethods — GSAP timelines are thenable (tl.then resolves on completion), and forwarding it makes the proxy thenable too: Promise.resolve(proxy) or await proxy hangs forever for paused timelines - Add unit test: Promise.resolve(proxy) resolves immediately, real then() is never called - CI canary: exit 1 (not 0) when no page.goto timing is found in logs, so a log-format change loudly breaks CI instead of silently disabling the canary |
||
|
|
1bcd6ec3b3 |
fix(core): re-register hf-timelines-built listener in maybePublishRenderReady (#1279)
Compositions that defer gsap.timeline() registration past DOMContentLoaded (via setTimeout, template instantiation, or dynamic script loading) hit a race where __renderReady stays false forever: 1. At DOMContentLoaded, __hfTimelinesBuilding is false — init.ts skips the hf-timelines-built listener and sets __renderReady = true 2. The deferred script runs, calls gsap.timeline().to() which sets __hfTimelinesBuilding = true via the batching proxy 3. The deferred maybePublishRenderReady() sees building=true, sets __renderReady = false, but never registers a listener to retry 4. __renderReady stays false, __hf.duration returns 0, pollHfReady times out with "Composition has zero duration" Fix: when maybePublishRenderReady encounters __hfTimelinesBuilding=true, register a one-shot hf-timelines-built listener to retry — matching the pattern already used at init time for the synchronous batching case. Closes #1260 |
||
|
|
25420bf4cf |
ci: skip ffmpeg-static CDN download on ubuntu; retry Windows FFmpeg install (#1275)
* ci: skip ffmpeg-static CDN download on ubuntu; retry Windows FFmpeg install ubuntu-24.04 runners ship /usr/bin/ffmpeg. Set FFMPEG_BIN so ffmpeg-static's postinstall script skips its GitHub-release binary download, preventing bun install failures when that CDN is unavailable. For Windows: increase BtbN/FFmpeg-Builds download max-attempts 3→8 with longer backoff (30×attempt s) and set FFMPEG_BIN after install so bun install also skips ffmpeg-static's download in both render and test jobs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: fix FFMPEG_BIN approach — use writable copy via composite action /usr/bin/ffmpeg is not writable by the runner user. When bun runs ffmpeg-static's postinstall script in a context where process.exit(0) is intercepted, the skip-if-exists check has no effect and the download proceeds to the destination path. Pointing FFMPEG_BIN at a system path (/usr/bin/ffmpeg) therefore causes EACCES even when the CDN returns 200. Replace the top-level env var with a prepare-ffmpeg-bin composite action that copies the system ffmpeg to $RUNNER_TEMP (writable). Call it before every bun install step in the CI workflow. Whether the postinstall script skips or overwrites, the write target is now writable and the job succeeds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: skip apt fallback in prepare-ffmpeg-bin; use stub when ffmpeg absent ubuntu-24.04 GHA runners do not have ffmpeg pre-installed. The apt-get fallback triggered the install of ffmpeg and its dependencies, but the Azure apt mirror returned 404 for libcaca0, aborting the composite action. ffmpeg-static's postinstall only needs a regular file to exist at FFMPEG_BIN in order to reach the statSync check and call process.exit(0) — it does not need a real executable. Write a minimal shell stub when 'which ffmpeg' returns empty. Jobs that require an actual ffmpeg binary (cli-smoke-required) install it via apt before calling this action, so 'which ffmpeg' returns the real path and the copy branch runs instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
4da567df22 |
feat(gcp-cloud-run): Google Cloud Run + Workflows distributed render adapter (#1253)
* feat(gcp-cloud-run): add Google Cloud Run + Workflows distributed render adapter Adds @hyperframes/gcp-cloud-run, the GCP counterpart to @hyperframes/aws-lambda (issue #932). The OSS distributed primitives (plan, renderChunk x N, assemble) are unchanged; this package is the storage/compute/orchestration glue. Package: Cloud Run handler (one image, three actions), runs under bun; GCS transport; in-image chrome-headless-shell resolver; client SDK (renderToCloudRun, getRenderProgress, deploySite, computeRenderCost); Dockerfile; Cloud Workflows definition; Terraform module; CLI cloudrun deploy|sites|render|render-batch|progress|destroy with --output-resolution and --strict-variables; 62 unit tests + docs + live smoke script. Shared extraction (removes ~640 lines of adapter duplication): move the cloud-agnostic config validator + content-hash into producer/distributed; both adapters import them. Validated end-to-end on GCP at 37.4 dB PSNR vs baseline. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(cli): resolve @hyperframes/gcp-cloud-run in the CLI build + root build The CLI bundle (esbuild) couldn't resolve `@hyperframes/gcp-cloud-run/sdk`, failing Build/Typecheck/CLI-smoke (and the perf/windows/regression jobs that build first). Mirror the aws-lambda handling: mark the gcp adapter + its /sdk subpath external in tsup.config.ts with a source alias, and add gcp-cloud-run to the root `build` filter so its dist exists for publish + runtime. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ci): copy gcp-cloud-run manifest in Dockerfile.test for frozen install The regression test image runs `bun install --frozen-lockfile` after copying each workspace package.json individually. The CLI now depends on @hyperframes/gcp-cloud-run (workspace:*), so the frozen install fails to resolve it unless its manifest is present. Add the COPY line. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(cli): add machine-sizing flags to `cloudrun deploy` Closes the parity gap with `lambda deploy` (which exposes --memory etc.). `cloudrun deploy` now threads --cpu, --memory, --max-instances, and --timeout into the Terraform apply; omitted flags keep the module defaults (4 vCPU / 16Gi / 100 instances / 3600s). For finer control, apply the module directly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gcp-cloud-run): address PR review (security, waste, limits, alerts) - server.ts: bucket-allowlist guard no longer fails open silently. Unset env logs a one-time WARNING; "*" is an explicit opt-out; otherwise it enforces. - server.ts: stop double-shipping audio.aac. It already rides in the plan tarball every consumer downloads, so drop the redundant standalone upload (plan) + re-download/overwrite (assemble); assemble reads it from the untar, falling back to a supplied AudioGcsUri for compat. - server.ts: chunk extension via path.extname() instead of slice(lastIndexOf). - workflow.yaml: clamp parallel concurrency_limit to math.min(chunkCount, 20) — Cloud Workflows hard-caps concurrent iterations at 20. - Dockerfile: pin bun (bun-v1.3.9) so an interop change can't silently break the image rebuild. - terraform: add min_instances var (default 0); add a workflow-failure alert (finished_execution_count status=FAILED) alongside the request-count one. - costAccounting: document that displayCost excludes GCS storage/egress. Verified against the actual APIs: @google-cloud/workflows@4.4.0 ICreateExecutionRequest has no executionId (so the idempotency-token suggestion isn't available in this client); Workflows concurrency cap is 20; failure metric is workflows.googleapis.com/finished_execution_count (status label). 174 adapter tests pass, fallow/oxlint/oxfmt/terraform clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(gcp-cloud-run): address round-2 review — error code + CFR forwarding - workflow.yaml: rename the zero-chunk failure code PLAN_TOO_LARGE → PLAN_PRODUCED_ZERO_CHUNKS. The old code implied a size-ceiling breach (the opposite cause), misleading anyone triaging the alert. - workflow.yaml: forward Config.cfr to the assemble step (`Cfr: ${("cfr" in config) and config.cfr}`). It was read by the handler but never sent, so exact-CFR was silently off for every Cloud Run render. Uses the same `in`-operator guard already proven in the retryable predicate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(release): include gcp-cloud-run in set-version PACKAGES list set-version.ts (driven by release:prepare) bumps an explicit package list to the shared version on each release. gcp-cloud-run was wired into the build + publish.yml but missing here, so a release would leave it at a stale version and publish.yml would push the wrong version. Add it so the new package version-bumps + publishes in lockstep with the others. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
1f37920fe1 |
fix(cli): re-validate SSRF denylist on redirects + harden isPrivateUrl (#1212)
## Summary - Adds `safeFetch`, a redirect-aware wrapper around `fetch` that re-runs the SSRF denylist on every hop before following a redirect. - Routes `fetchBuffer` and the Lottie media fetch through `safeFetch` so redirect chains can't bounce through a public URL to reach an internal or cloud-metadata host. - Hardens `isPrivateUrl` to also block `0.0.0.0` / `0.0.0.0/8`, IPv6 loopback (`::1`), IPv4-mapped (`::ffff:…`), unique-local (`fc00::/7`), and link-local (`fe80::/10`) ranges. ## Security **F-002 MED** — `fetchBuffer` followed redirects without re-checking the denylist on the destination. A `30x` redirect from an allowlisted public URL to `169.254.169.254` or an internal host would succeed, leaking the response to the caller (e.g. captured page assets written to local disk). **F-003 MED** — `isPrivateUrl` did not cover `0.0.0.0` (maps to localhost on most OSes), IPv6 loopback, or IPv6 private ranges. An asset URL using those addresses would bypass the denylist. Alternate IPv4 encodings (decimal/octal/hex) are already normalized to dotted-quad by WHATWG URL parsing and remain blocked. ## Test plan - [x] Unit tests cover redirect-chain blocking (redirect to metadata IP rejected) - [x] Unit tests cover new `isPrivateUrl` address forms (`0.0.0.0`, `::1`, `fc00::1`, `fe80::1`, `::ffff:192.168.1.1`) - [x] Existing fetch and asset-download tests pass |
||
|
|
248f640734 |
feat(docs): add changelog release workflow (#1164)
* feat(docs): add changelog release workflow * fix(scripts): resolve CodeQL findings in release scripts - draft-changelog.ts: replace existsSync+writeFileSync check-then-act with an atomic exclusive-write flag (flag: wx) to fix the js/file-system-race TOCTOU finding; overwrite only under --force (flag: w). - set-version.ts: switch execSync shell-string git calls to execFileSync with argument arrays so the interpolated version/paths can never be interpreted by a shell, resolving the js/indirect-command-line-injection findings. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(scripts): lower writeReleaseNotes complexity below CRAP threshold The exclusive-write fix pushed writeReleaseNotes to cyclomatic 5 / CRAP 30.0 (fallow/high-crap-score, threshold 30.0). The '!force' guard in the catch is redundant — EEXIST is only reachable under the 'wx' flag (force=false), since 'w' overwrites without throwing. Dropping it returns the function to cyclomatic 4 / CRAP 20 with identical behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(docs): address changelog review feedback --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3bbfea38cf |
fix(engine): use captureBeyondViewport on all CDP screenshot paths (#1094)
* fix(engine): use captureBeyondViewport on all CDP screenshot paths Chrome's compositor rounds the viewport boundary inward under multi-tab load, clipping the bottom/right edge of tall portrait compositions (1080x1920). The explicit clip rect already constrains output to exact composition dimensions, making the viewport-boundary pre-clip from captureBeyondViewport:false both redundant and unreliable. Set captureBeyondViewport:true on all three CDP screenshot call sites: pageScreenshotCapture, captureScreenshotWithAlpha, and captureAlphaPng. Add portrait-edge-bleed regression test: 1080x1920 grid with bright magenta bottom rows, rendered with 4 workers. Any compositor clipping at the bottom edge drops PSNR sharply against the golden baseline. Closes #1009 * fix(engine): address review feedback on captureBeyondViewport - Add backref comments on captureScreenshotWithAlpha and captureAlphaPng pointing to pageScreenshotCapture for the rationale, so the next reader doesn't treat the flag as unintentional copy-paste - Note in test meta.json that the static grid fixture covers the capture-side clipping path but not the video-element compositor surface timing that produces the t≈37s self-healing in #1009 * test(producer): use video element in portrait-edge-bleed regression test Replace the static CSS grid with a 1080x1920 portrait video element — matches the original bug report shape where the compositor surface allocation timing causes the bottom-edge clipping. The video has a dark top region and bright magenta bottom 480px, so any viewport clipping at the bottom edge drops PSNR sharply. Baseline regenerated in Docker with 4 workers. |
||
|
|
3cd6cd6a1c |
test(producer): add parallel capture regression test (#1088)
* test(producer): add parallel capture regression test Add a regression fixture that forces workers: 2, ensuring the parallel capture code path (browser-per-worker in BeginFrame mode) is exercised in CI. All existing fixtures pin workers: 1, so this is the first test that would catch a regression in the multi-worker pool isolation fix from PR #1087. The composition is 5s @ 30fps (150 frames), which exceeds both MIN_FRAMES_PER_WORKER * 2 (60) and minParallelFrames (120), so the parallel coordinator will always split work across workers. Baseline output/output.mp4 must be generated inside Dockerfile.test before the fixture can run in CI. * test(producer): bump parallel capture test to 4 workers Matches realistic auto-mode worker counts (4-6 on typical machines), not just the minimum (2) that triggers the bug. * test(producer): add golden baseline for parallel capture regression Generated inside Dockerfile.test on amd64 Linux (Docker image hyperframes-producer:test) to match the CI rendering environment. * test(producer): address review feedback on parallel capture test - Add fixture to shard-5 in regression.yml so CI actually runs it - Reframe description: multi-worker path coverage (frame distribution, reorder buffer, per-worker browser lifecycle), not GPU-specific crash guard — SwiftShader CI can't reproduce the hardware compositor race - Remove dead @keyframes count-up (content doesn't apply to div) - Remove unused CSS animation reference on .counter - Regenerate golden baseline with cleaned-up HTML * fix(producer): replace rAF + CSS keyframes with GSAP in parallel-capture test The composition used requestAnimationFrame for a frame counter and CSS @keyframes for animations, which triggered screenshot capture mode (non-deterministic across workers) and caused 29 PSNR failures in CI. All animations now use the GSAP timeline, keeping the render in deterministic BeginFrame mode. Baseline regenerated in Docker. |
||
|
|
ea4d920589 |
refactor(studio): split oversized files and raise line limit to 600
Split PlayerControls.tsx into focused sub-components (SeekBar, WorkAreaOverlay, MuteButton, LoopButton, FullscreenButton, ShortcutsPanel, SpeedMenu) and extracted seek bar drag/progress tracking into useSeekBarDrag hook. Split manualEditsDom.ts patch-builder functions into manualEditsDomPatches.ts with data-driven helpers to reduce duplication and complexity. Extracted per-type reapply helpers from reapplyPositionEditsAfterSeek and factored out identity-matrix check from stripGsapTranslateFromTransform. Raised file-size limit from 500 to 600 lines, removed .filesize-allowlist. |
||
|
|
6d1236a0cc |
feat(cli): add --output-resolution to lambda render
Allows authored-at-1080p compositions to render at 4K/2K via Chrome deviceScaleFactor supersampling without re-laying-out the composition. Plain --width 3840 silently lays out at 1920×1080 because data-width/ data-height attrs override Config.width — this flag is the supported way to ask the renderer to supersample. Accepts canonical CanvasResolution names (landscape, landscape-4k, portrait, portrait-4k, square, square-4k) and aliases (1080p, 4k, uhd, hd, 1080p-portrait, 4k-portrait, 1080p-square, 4k-square). Wired through render + render-batch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
be9b61a8c9 |
Merge pull request #986 from heygen-com/fix/studio-edit-persistence-and-render-css
fix(studio): server-side DOM patching, render CSS scoping, and resilience |
||
|
|
692c1431c3 | fix(ci): resolve puppeteer from producer package in studio smoke test | ||
|
|
1d6ed9f41a | ci: add studio load smoke test — catches runtime errors on first page load | ||
|
|
07bcb4f73b |
fix(cli): stop dropping CI/agent telemetry, suppress HeyGen CI at workflow level
The CI=true early-exit in shouldTrack() was hiding most modern usage (coding agents in Codespaces, CI pipelines, agent sandboxes). Remove it. Each event still carries is_ci/is_docker/is_tty from system.ts, so CI vs laptop traffic can be separated in PostHog without being dropped at ingestion. HeyGen's own CI is suppressed via HYPERFRAMES_NO_TELEMETRY=1 added to each workflow that exercises the CLI. |
||
|
|
83e01e0d44 |
Merge pull request #965 from heygen-com/fix/sub-comp-timeline-t0
fix: activate nested child timelines on renderSeek (sub-comp at t=0) |
||
|
|
e2f7f6a58a |
fix: add regression fixtures with golden baselines + address review
- Create sub-comp-t0 and sub-comp-id-selector as proper regression tests under packages/producer/tests/ with golden MP4 baselines - Add both to shard-7 in regression.yml - Add clarifying comment on activateNestedChildTimelines scope - Confirm test fixture network safety in comment |
||
|
|
6d2569c6bb |
test(producer): add webm-vp9 distributed regression fixture (#952)
* feat(producer): enable webm in distributed mode via concat-copy PR 8.2 of the WebM distributed-rendering plan (v1.5 backlog #1; see DISTRIBUTED-RENDERING-PLAN.md §7.2). Wires libvpx-vp9 webm through the distributed pipeline now that PR 8.1 proved concat-copy works. Architectural decision: Path A (concat-copy) — based on PR 8.1's smoke test result (9/9 tests pass for both yuv420p and yuva420p VP9 streams). The simpler architecture wins; no re-encode in assemble, no encode- parallelism loss. Changes: - plan.ts: - DistributedRenderConfig.format and PlanResult.format now include "webm" — type-level acceptance matches the runtime gate. - rejectUnsupportedDistributedFormat() no longer trips on webm. HDR mp4 remains the only refused configuration. - resolveEncoderTriple() returns libvpx-vp9-software + yuva420p + preset="good" for format="webm". yuva420p preserves alpha — the format's main reason for existing for web delivery. - codec= remains rejected for non-mp4 formats (mov is always ProRes 4444; webm is always libvpx-vp9). The error message lists all four distributed-supported formats. - FormatNotSupportedInDistributedError docstring updated to reflect the new reality (only HDR is unsupported). - freezePlan.ts: LockedRenderConfig.encoder gains "libvpx-vp9-software". Mirrors libx265-software / prores-software / png-sequence in shape; the chunk worker reads this discriminant to decide encode args. - renderChunk.ts: drops the now-incorrect cast that excluded webm from buildSyntheticRenderJob's format input; tightens the preset-format cast to include webm. - assemble.ts: docstring + comment updates. The mp4/mov concat-copy path is format-agnostic — webm uses the exact same code (applyFaststart is a no-op for webm via the existing chunkEncoder.ts gate; muxVideoWithAudio already routes webm to libopus audio). - planFormatBanlist.test.ts: webm-rejection tests removed; replaced with "accepts webm" tests + a HDR+webm combo test that verifies HDR is the trip regardless of format. - plan.test.ts: new describe block pins the webm wiring contract: format="webm" produces an encoder=libvpx-vp9-software / pixelFormat=yuva420p planDir with closedGop=true and gopSize=chunkSize. - webm-concat-copy.test.ts (smoke): extended with a yuva420p variant that proves the alpha pixel format the distributed pipeline actually emits also round-trips through concat-copy. 9/9 tests pass locally. §8 format support matrix in DISTRIBUTED-RENDERING-PLAN.md is intentionally left unchanged at this PR — it flips to ✓ in PR 8.4 once the end-to-end fixture (PR 8.3) is green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(producer): include webm in plan-time needsAlpha + strengthen alpha smoke PR review feedback from Miguel and Vai on #951 caught a real bug: `plan.ts`'s `needsAlpha` disjunction excluded `"webm"`, so the plan stage froze `forceScreenshot: false` into the `LockedRenderConfig` even though distributed webm uses `yuva420p`. Every chunk worker captured opaque RGB via BeginFrame (which doesn't preserve alpha on Linux headless-shell), and libvpx-vp9 encoded uniformly-opaque alpha that the encoder then dropped — producing un-keyable webm. Two changes: 1. **plan.ts**: include `"webm"` in `needsAlpha`. Matches the in-process renderer's logic at `renderOrchestrator.ts:1469` (`const needsAlpha = isWebm || isMov || isPngSequence`); the two sites must stay in sync since the distributed pipeline's PSNR regression compares against the in-process baseline. 2. **Smoke test (yuva420p describe)**: source frames now use a real alpha gradient (`geq=a='X*255/W'` on top of `testsrc2`) instead of `testsrc2 + format=rgba` which was uniformly opaque. The decode- pix_fmt assertion is dropped (ffprobe reports `yuv420p` for VP9-with-alpha because the alpha lives in a Matroska `BlockAdditional` sidecar) and replaced with two stronger checks: - `TAG:ALPHA_MODE=1` is present on the stream — proves the encoder was actually configured for alpha - alpha plane variance after `-c:v libvpx-vp9 -i ... -pix_fmt rgba -vf extractplanes=a,signalstats` — proves the alpha sub-stream round-trips through concat-copy with spatially-varying content, not uniform/dropped alpha - decode-test gate is now exit-code-only (was `exitCode || stderr` which would flake on chatty ffmpeg `-v error` builds emitting non-fatal DTS/container notes) These checks would have caught the `needsAlpha` bug before review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(aws-lambda): widen narrow format types to include webm CI on PR #951 was failing at typecheck/build because the producer's `DistributedRenderConfig.format` widened to include webm in this PR but the aws-lambda package's narrow `"mp4" | "mov" | "png-sequence"` type literals in `events.ts`, `handler.ts`, and `validateConfig.ts` hadn't kept up. `renderToLambda.ts:87` passed `config.format` (now including webm) into a parameter typed against the narrow union, producing TS2345. This widening originally landed in PR #952 (test fixture PR) but needs to be atomic with the producer's widening here to keep each PR independently typecheck-clean. Also refactor `formatExtension` from a switch dispatch to a `Record<DistributedFormat, string>` lookup. Adding the webm case tipped the switch's CRAP to the 30.0 fallow threshold; the lookup table drops cyclomatic from 5 to 1 with the same compile-time exhaustiveness guarantee (TS errors on missing entries when `DistributedFormat` adds a new format). The runtime `_exhaustive: never` throw was only protecting against a string slipping past TS; `validateConfig.ts`'s `ALLOWED_FORMATS` already gates untrusted input at the SDK boundary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(producer): add webm-vp9 distributed regression fixture PR 8.3 of the WebM distributed-rendering plan (v1.5 backlog #1; see DISTRIBUTED-RENDERING-PLAN.md §7.2). End-to-end regression coverage for the webm distributed path PRs 8.1 and 8.2 wired up. Adds packages/producer/tests/distributed/webm-vp9/ matching the mp4-h264-sdr fixture pattern: a 2-second composition (60 frames @ 30fps) with text, a crossfade across the frame-30 chunk seam, and a continuous icon rotation — exercises chunk-boundary continuity for both display contents and VP9 closed-GOP alpha encoding. `chunkSize: 15` produces 4 chunks so 3 seams are tested, and the crossfade straddles the middle seam to surface alpha-plane discontinuities introduced by alt-ref drift. Baseline regenerated inside Dockerfile.test via `bun run --cwd packages/producer docker:test:update webm-vp9`. Runs in: - in-process mode: byte-identical match against baseline ✓ - distributed-simulated mode: PSNR 56.88-63.49 dB across 100 checkpoints, well above the 30 dB threshold ✓ Wiring updates required to let webm flow through the harness: - regression-harness-distributed.ts: - checkDistributedSupport() no longer rejects webm. HDR mp4 + NTSC fps + non-{24,30,60} fps remain rejected. - RunDistributedSimulatedInput.format widened to include webm. - Docstring + comments updated. - regression-harness-distributed.test.ts: webm-rejection test replaced with "accepts format=webm" test. - regression-harness.ts: the now-incorrect format cast at the distributed-input call site is dropped; comment about why webm was excluded is replaced with "webm is now distributed-supported". - regression-harness-lambda-local-types.ts: RunLambdaLocalInput.format widened to include webm so lambda-local mode can also exercise webm fixtures end-to-end. - aws-lambda webm support (Path A through the Lambda handler): - formatExtension.ts: DistributedFormat gains "webm" → ".webm" case. - events.ts: RenderChunkEvent / AssembleEvent / PlanLambdaResult Format widened to include webm. - sdk/validateConfig.ts: ALLOWED_FORMATS gains "webm". - handler.ts: downloadChunkObjects format param widened. The Lambda handler delegates to the producer's assemble() primitive which PR 8.2 already taught to handle webm (concat-copy + applyFaststart no-op + muxVideoWithAudio with libopus); no Lambda-side rendering changes are needed beyond the type/validation surfaces above. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(aws-lambda): drop stale webm rejection from validateConfig docblock PR #952 review nit (Miguel): the validateConfig.ts file-header comment still claimed the SDK rejects webm, but the runtime check no longer does (ALLOWED_FORMATS now includes 'webm'). Update the docblock to reflect that only force-hdr remains an SDK-side rejection. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(regression): add webm-vp9 to shard-3 + refactor formatExtension Three follow-ups bundled together (Vai's review feedback on PR #952 plus the fallow audit finding that surfaced when the webm case was added): 1. **Wire webm-vp9 into CI regression.** The fixture was added in this PR but never appeared in any `.github/workflows/regression.yml` shard's args allowlist, so the regression harness's positional-args gate skipped it in CI. Append `webm-vp9` to shard-3 (which already carries `mp4-h264-sdr` + `webm-transparency`) so the fixture runs. 2. **Fix stale "four hard gates" prose in checkDistributedSupport docstring.** Earlier in the stack I removed the webm bullet but didn't update the count. Two gates remain (fps + hdr). 3. **Refactor `formatExtension` from switch to lookup table.** Adding the webm case made the switch dispatch's CRAP score hit 30.0 (cyclomatic = 5, plus the function's small body). Replaced with a `Record<DistributedFormat, string>` lookup, which: - drops cyclomatic from 5 → 1, - keeps exhaustiveness enforcement at compile time (TS errors if a new format gets added to `DistributedFormat` without a matching key in the Record literal), - drops the runtime `_exhaustive: never` throw, which was only guarding against an arbitrary string slipping past TS — a caller-side concern, not this function's job. The function now reads as a table lookup, which matches what it actually does, and the fallow audit now reports zero new complexity findings (down from 1). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
efc5f0584b |
ci: post sticky PR comment with fallow audit findings (#954)
* ci: run fallow audit in lefthook pre-commit Mirrors the same `fallow audit --base ... --fail-on-issues` check that runs in CI, but locally against HEAD so issues surface at commit time instead of after the push round-trip. Scoped to `packages/**` source files via the glob — non-code edits (README, docs, top-level configs) skip the hook entirely. Measured locally: ~5s in parallel with the existing lint/format/typecheck checks. Doesn't extend wall-clock time because typecheck (~11s) is the long pole, and lefthook runs commands in parallel. The default `--gate new-only` means inherited findings don't block the commit — same gate behavior as CI, so local pre-commit and PR audit agree. * refactor: delete orphan declarations flagged by fallow After fallow's auto-fix de-exports unused symbols, oxlint surfaces them as no-unused-vars. This PR deletes those orphan declarations outright. Biggest cleanup: studio/src/icons/SystemIcons.tsx shrinks from 132 to 57 lines — 33 unused icon wrappers and their phosphor-icon imports deleted. Other deletions across 14 more files covering paired getter/setters, helper functions, dead env constants, internal components with no callers, and cascading unused imports. Cascade-causing files held back for follow-up PRs: renderOrchestrator barrel of captureCost re-exports, telemetry/portUtils/remote barrels, Button.tsx + ui/index.ts (would orphan whole file), studioMotion type re-exports. Test plan: typecheck clean across 8 packages, oxlint + oxfmt clean, fallow audit exit 0 (remaining findings inherited), cli + studio vitest suites pass. * ci: post sticky PR comment with fallow audit findings Reviewers shouldn't have to dig through CI logs to see what fallow flagged. With this change, on every PR the fallow job posts (or updates) a sticky comment containing the full audit report formatted as a collapsible markdown table. The comment uses fallow's built-in `pr-comment-github` format, which already emits a `<!-- fallow-id: fallow-results -->` sentinel. `marocchino/sticky-pull-request-comment@v2.9.1` matches that header so each run replaces the previous comment instead of stacking new ones. The job now runs in three steps: 1. Run `fallow audit ... --format pr-comment-github` with `continue-on-error: true` so the comment posts even when the audit fails. Exit code is captured. 2. Post (or update) the sticky comment with the captured output. 3. Re-emit the audit exit code so the job still fails-the-build on new findings. Bumps the workflow's `pull-requests` permission from read to write, needed for the sticky-comment poster to call the issues API. |
||
|
|
62c800aec3 | ci: add fallow audit job (PR-scoped, new-only gate) | ||
|
|
4051899533 |
chore(lambda): publish-readiness for @hyperframes/aws-lambda
The Lambda adapter has been on `main` since PR #909 but its package manifest still shipped TypeScript source (`main: ./src/index.ts`, `build: tsc --noEmit`, `version: 0.0.1`) and the publish workflow didn't list it. This wires it up to publish alongside the other `@hyperframes/*` packages on the next `v*` tag. Changes: - **packages/aws-lambda/build.mjs (new)** — mirrors `packages/producer/build.mjs`: esbuild bundles four entry points (`src/index.ts`, `src/handler.ts`, `src/sdk/index.ts`, `src/cdk/index.ts`) → `dist/`, then `tsc --emitDeclarationOnly` emits .d.ts via `tsconfig.build.json`. All runtime/peer deps (@aws-sdk/*, @hyperframes/producer*, @sparticuz/chromium, aws-cdk-lib, constructs, ffmpeg-static, ffprobe-static, puppeteer-core, tar) are external so consumers resolve them through their own node_modules. - **packages/aws-lambda/tsconfig.build.json (new)** — drops the workspace `paths` overrides so `@hyperframes/producer*` resolves through node_modules to producer's already-built `dist/` types instead of pulling its full source tree into emit (which would violate `rootDir`). - **packages/aws-lambda/tsconfig.json** — keeps `noEmit: true` + workspace `paths` for fast in-place typechecks; also excludes `src/**/__fixtures__/**` so test-only helpers (fakeS3) don't leak into emitted declarations. - **packages/aws-lambda/package.json**: * version bumped 0.0.1 → 0.6.18 (matches the repo's lockstep release cadence) * main / types / exports map points at `dist/...` * files: ["dist/", "scripts/", "README.md"] (scripts/ kept whole because build-zip.ts and verify-zip-size.ts both import scripts/_formatBytes.ts) * scripts.build = `node build.mjs` - **package.json** — root `build` filter includes `aws-lambda` so `bun run build` builds it in topological order after producer. - **.github/workflows/publish.yml** — one new `publish_pkg "@hyperframes/aws-lambda" "@hyperframes/aws-lambda"` line. First publish is automatic via the `--access public` flag in `publish_pkg`; the @hyperframes scope already owns the name. Verification: bun run build # full root build green bun run verify:packed-manifests # aws-lambda passes pnpm pack packages/cli # @hyperframes/aws-lambda # rewrites workspace:* → 0.6.18 npm install -g <cli-tgz> # smoke-install still works hyperframes lambda deploy # friendly missing-package # error still fires when # aws-lambda isn't installed |
||
|
|
c50f59a53b |
feat(lambda): add Lambda handler, ZIP bundling, and BeginFrame probe (#878)
* feat(lambda): add Lambda handler, ZIP bundling, and BeginFrame probe
Phase 6 of the distributed rendering plan: AWS Lambda turnkey adoption
(see DISTRIBUTED-RENDERING-PLAN.md §11 Phase 6 + §15).
This PR adds the new packages/aws-lambda/ workspace package that wraps
the OSS plan/renderChunk/assemble primitives in an AWS Lambda handler,
plus a build pipeline that bundles the handler + Chromium runtime +
ffmpeg into a deployable ZIP.
Architecture: ZIP deploy (not Docker image), Chrome via @sparticuz/chromium
with chrome-headless-shell fallback, dispatch on event.Action ∈ {plan,
renderChunk, assemble}.
The load-bearing concern — does @sparticuz/chromium's chrome-headless-shell
build honour CDP HeadlessExperimental.beginFrame? — is pinned by the new
scripts/probe-beginframe.ts regression guard. Probe boots the runtime
inside public.ecr.aws/lambda/nodejs:22, navigates to a static page, and
asserts beginFrame returns a PNG buffer. Verified locally + inside the
Docker container; both pass with hasDamage=true.
Sizes (sparticuz source): unzipped 157 MiB, zipped 99 MiB. Well under
the 240 MiB / 150 MiB in-house gates and the Lambda 250 MiB hard ceiling.
This is part of a stack of 8 PRs (3 in Phase 6a, 5 in Phase 6b); this is
PR 6.1.
* fix(lambda): address PR 878 review feedback
- Verify event.PlanHash against the untarred plan.json at the handler
boundary before invoking the producer primitive. Throws typed
PLAN_HASH_MISMATCH on divergence so Step Functions routes it as
non-retryable; previously the field was schema bloat the handler
ignored, leaving enforcement entirely inside the producer.
- Standardize on MiB throughout build-zip.ts, verify-zip-size.ts, and
the README. Lambda's hard ceiling is 250 MiB (AWS docs label "250 MB"
but use binary mebibytes); previously mixed units made the 248 MiB
budget look like a ~5 MB margin instead of the 2 MiB it actually is.
- stageChromeHeadlessShell now picks Chrome versions via numeric semver
comparison instead of lexicographic sort+reverse — the latter would
silently pick "99.x" over "131.x" once Chrome cached three-digit
majors that aren't width-aligned.
- Drop _setSparticuzChromiumForTests from the public index barrel.
Test-only DI seam imported directly from ./chromium.js in tests.
- Replace require("node:fs") inside walkSize() with the top-level fs
imports — file is ESM and the same module is already imported.
* docs(lambda): drop internal plan-doc refs from package README
* ci(windows): fix bun filter UNION bug excluding producer from Windows tests
`bun run --filter "!a" --filter "!b" test` composes as a UNION (any
package matching either negation runs), not an intersection. Effect:
@hyperframes/producer was still being tested on Windows even though
it's explicitly excluded — its regression harness (Docker + LFS golden
mp4 baselines) is Linux-only and was driving the 32min timeout.
Enumerate the packages we DO want to test instead.
|
||
|
|
00984133fc |
ci(preflight): extract preflight steps into a composite action
Same 5-step preflight body (setup-bun, setup-node, cache, install, lint, format:check) was duplicated across 5 workflows. Move it to .github/actions/preflight/action.yml so future tweaks (adding typecheck, swapping the cache key, etc.) are a single-file change. Net diff: +33 / -65. Addresses the "shared preflight" follow-up Vai called out on #877. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e29d7db331 |
ci(preflight): cache ~/.bun/install/cache keyed on bun.lock
Each of the 5 preflight gates was doing a cold bun install, costing ~30-60s of redundant install time per PR. Cache the install dir keyed on bun.lock so subsequent preflights (and reruns) hit warm. Addresses Vai's review on #877. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
18e49cb69c |
ci: fast-fail regression matrix + preflight gate before expensive jobs
Don't burn 60+ runner-minutes on regression shards, perf shards, preview-parity, Windows renders, or catalog-preview renders when the PR is already failing lint or format. - regression: matrix fail-fast: false → true (first failing shard cancels the rest), plus a new preflight (lint + format:check) job gating regression-shards. - player-perf: matrix fail-fast → true, plus preflight gate. - preview-regression, windows-render, catalog-previews: preflight gate added; heavy jobs now needs: [..., preflight]. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4bc24068f4 |
Merge pull request #864 from heygen-com/05-15-ci_codeql_bump_codeql-action_v3_v4_to_address_deprecation
ci(codeql): bump codeql-action v3 → v4 |
||
|
|
436aebdc13 | ci(codeql): bump codeql-action v3 → v4 to address deprecation | ||
|
|
952d2658b0 |
Merge pull request #860 from heygen-com/05-15-ci_security_add_advanced_codeql_setup_with_path-scoped_query_filters
ci(security): add advanced CodeQL setup with path-scoped query filters |
||
|
|
783e25a78f | ci(security): add advanced CodeQL setup with path-scoped query filters |