mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
ci: address CLI smoke review feedback
This commit is contained in:
+38
-25
@@ -220,83 +220,96 @@ jobs:
|
|||||||
cli-smoke-required:
|
cli-smoke-required:
|
||||||
name: "CLI smoke (required)"
|
name: "CLI smoke (required)"
|
||||||
needs: changes
|
needs: changes
|
||||||
|
if: needs.changes.outputs.code == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
steps:
|
steps:
|
||||||
- name: Skip when no code changed
|
|
||||||
if: needs.changes.outputs.code != 'true'
|
|
||||||
run: echo "No code changes; CLI smoke required check passes."
|
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
with:
|
with:
|
||||||
lfs: true
|
lfs: true
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: Install FFmpeg
|
- name: Install FFmpeg
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y ffmpeg
|
sudo apt-get install -y ffmpeg
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
- name: Build monorepo
|
- name: Build monorepo
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: bun run build
|
run: bun run build
|
||||||
|
|
||||||
- name: Smoke-test CLI from monorepo source
|
- name: Create smoke input video
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: |
|
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
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
rm -rf /tmp/hf-cli-inside
|
rm -rf /tmp/hf-cli-inside
|
||||||
|
|
||||||
bun run --filter @hyperframes/cli dev -- init /tmp/hf-cli-inside \
|
bun run --filter @hyperframes/cli dev -- init /tmp/hf-cli-inside \
|
||||||
--example blank \
|
--example warm-grain \
|
||||||
|
--video /tmp/hf-cli-input.mp4 \
|
||||||
|
--skip-transcribe \
|
||||||
--non-interactive \
|
--non-interactive \
|
||||||
--skip-skills
|
--skip-skills
|
||||||
|
|
||||||
bun run --filter @hyperframes/cli dev -- lint /tmp/hf-cli-inside
|
bun run --filter @hyperframes/cli dev -- lint /tmp/hf-cli-inside
|
||||||
bun run --filter @hyperframes/cli dev -- validate /tmp/hf-cli-inside --timeout 1000
|
bun run --filter @hyperframes/cli dev -- validate /tmp/hf-cli-inside --timeout 1000
|
||||||
bun run --filter @hyperframes/cli dev -- render /tmp/hf-cli-inside \
|
bun run --filter @hyperframes/cli dev -- render /tmp/hf-cli-inside \
|
||||||
--quality draft \
|
--quality standard \
|
||||||
--workers 1 \
|
--workers auto \
|
||||||
--strict-all \
|
--strict \
|
||||||
--output /tmp/hf-cli-inside/renders/inside.mp4
|
--output /tmp/hf-cli-inside/renders/inside.mp4
|
||||||
|
|
||||||
test -s /tmp/hf-cli-inside/renders/inside.mp4
|
test -s /tmp/hf-cli-inside/renders/inside.mp4
|
||||||
|
|
||||||
- name: Pack CLI tarball
|
- name: Pack CLI tarball
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
mkdir -p /tmp/hf-cli-pack
|
mkdir -p /tmp/hf-cli-pack
|
||||||
cd packages/cli
|
cd packages/cli
|
||||||
npm pack --pack-destination /tmp/hf-cli-pack
|
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
|
- name: Install packed CLI outside monorepo
|
||||||
if: needs.changes.outputs.code == 'true'
|
run: |
|
||||||
run: npm install -g --prefix /tmp/hf-cli-global /tmp/hf-cli-pack/hyperframes-cli-*.tgz
|
set -euo pipefail
|
||||||
|
npm install -g --prefix /tmp/hf-cli-global "$HF_CLI_TARBALL"
|
||||||
|
|
||||||
- name: Smoke-test packed CLI outside monorepo
|
- name: Smoke-test packed CLI outside monorepo
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
export PATH="/tmp/hf-cli-global/bin:$PATH"
|
export PATH="/tmp/hf-cli-global/bin:$PATH"
|
||||||
rm -rf /tmp/hf-cli-outside
|
rm -rf /tmp/hf-cli-outside
|
||||||
|
|
||||||
hyperframes init /tmp/hf-cli-outside \
|
hyperframes init /tmp/hf-cli-outside \
|
||||||
--example blank \
|
--example warm-grain \
|
||||||
|
--video /tmp/hf-cli-input.mp4 \
|
||||||
|
--skip-transcribe \
|
||||||
--non-interactive \
|
--non-interactive \
|
||||||
--skip-skills
|
--skip-skills
|
||||||
|
|
||||||
hyperframes lint /tmp/hf-cli-outside
|
hyperframes lint /tmp/hf-cli-outside
|
||||||
hyperframes validate /tmp/hf-cli-outside --timeout 1000
|
hyperframes validate /tmp/hf-cli-outside --timeout 1000
|
||||||
hyperframes render /tmp/hf-cli-outside \
|
hyperframes render /tmp/hf-cli-outside \
|
||||||
--quality draft \
|
--quality standard \
|
||||||
--workers 1 \
|
--workers auto \
|
||||||
--strict-all \
|
--strict \
|
||||||
--output /tmp/hf-cli-outside/renders/outside.mp4
|
--output /tmp/hf-cli-outside/renders/outside.mp4
|
||||||
|
|
||||||
test -s /tmp/hf-cli-outside/renders/outside.mp4
|
test -s /tmp/hf-cli-outside/renders/outside.mp4
|
||||||
|
|||||||
@@ -2,109 +2,6 @@ import { describe, it, expect } from "vitest";
|
|||||||
import { lintHyperframeHtml } from "../hyperframeLinter.js";
|
import { lintHyperframeHtml } from "../hyperframeLinter.js";
|
||||||
|
|
||||||
describe("composition rules", () => {
|
describe("composition rules", () => {
|
||||||
describe("subcomposition guidance", () => {
|
|
||||||
it("warns when a composition file is over 300 lines", () => {
|
|
||||||
const html = Array.from({ length: 301 }, (_, i) =>
|
|
||||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
|
|
||||||
expect(finding).toBeDefined();
|
|
||||||
expect(finding?.severity).toBe("warning");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not warn when a composition file is exactly 300 lines", () => {
|
|
||||||
const html = Array.from({ length: 300 }, (_, i) =>
|
|
||||||
i === 0 ? "<html><body>" : `<!-- filler ${i} -->`,
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "composition_file_too_large");
|
|
||||||
expect(finding).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("warns when more than 3 timed elements share the same track", () => {
|
|
||||||
const html = `<!DOCTYPE html>
|
|
||||||
<html><body>
|
|
||||||
<div data-composition-id="c1" data-width="1920" data-height="1080" data-start="0">
|
|
||||||
<div class="clip" data-start="0" data-duration="1" data-track-index="0">A</div>
|
|
||||||
<div class="clip" data-start="1" data-duration="1" data-track-index="0">B</div>
|
|
||||||
<div class="clip" data-start="2" data-duration="1" data-track-index="0">C</div>
|
|
||||||
<div class="clip" data-start="3" data-duration="1" data-track-index="0">D</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["c1"] = gsap.timeline({ paused: true });
|
|
||||||
</script>
|
|
||||||
</body></html>`;
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
|
|
||||||
expect(finding).toBeDefined();
|
|
||||||
expect(finding?.severity).toBe("warning");
|
|
||||||
expect(finding?.message).toContain("Track 0 has 4 timed elements");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not warn when 3 timed elements share the same track", () => {
|
|
||||||
const html = `<!DOCTYPE html>
|
|
||||||
<html><body>
|
|
||||||
<div data-composition-id="c1" data-width="1920" data-height="1080" data-start="0">
|
|
||||||
<div class="clip" data-start="0" data-duration="1" data-track-index="0">A</div>
|
|
||||||
<div class="clip" data-start="1" data-duration="1" data-track-index="0">B</div>
|
|
||||||
<div class="clip" data-start="2" data-duration="1" data-track-index="0">C</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["c1"] = gsap.timeline({ paused: true });
|
|
||||||
</script>
|
|
||||||
</body></html>`;
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
|
|
||||||
expect(finding).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not warn when timed elements are split across tracks", () => {
|
|
||||||
const html = `<!DOCTYPE html>
|
|
||||||
<html><body>
|
|
||||||
<div data-composition-id="c1" data-width="1920" data-height="1080" data-start="0">
|
|
||||||
<div class="clip" data-start="0" data-duration="1" data-track-index="0">A</div>
|
|
||||||
<div class="clip" data-start="1" data-duration="1" data-track-index="0">B</div>
|
|
||||||
<div class="clip" data-start="2" data-duration="1" data-track-index="1">C</div>
|
|
||||||
<div class="clip" data-start="3" data-duration="1" data-track-index="1">D</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["c1"] = gsap.timeline({ paused: true });
|
|
||||||
</script>
|
|
||||||
</body></html>`;
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
|
|
||||||
expect(finding).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not count the root composition element as a timed track element", () => {
|
|
||||||
const html = `<!DOCTYPE html>
|
|
||||||
<html><body>
|
|
||||||
<div data-composition-id="c1" data-width="1920" data-height="1080" data-start="0" data-track-index="0">
|
|
||||||
<div class="clip" data-start="0" data-duration="1" data-track-index="0">A</div>
|
|
||||||
<div class="clip" data-start="1" data-duration="1" data-track-index="0">B</div>
|
|
||||||
<div class="clip" data-start="2" data-duration="1" data-track-index="0">C</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["c1"] = gsap.timeline({ paused: true });
|
|
||||||
</script>
|
|
||||||
</body></html>`;
|
|
||||||
|
|
||||||
const result = lintHyperframeHtml(html);
|
|
||||||
const finding = result.findings.find((f) => f.code === "timeline_track_too_dense");
|
|
||||||
expect(finding).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reports info for composition with external CDN script dependency", () => {
|
it("reports info for composition with external CDN script dependency", () => {
|
||||||
const html = `<template id="rockets-template">
|
const html = `<template id="rockets-template">
|
||||||
<div data-composition-id="rockets" data-width="1920" data-height="1080">
|
<div data-composition-id="rockets" data-width="1920" data-height="1080">
|
||||||
|
|||||||
@@ -1,54 +1,7 @@
|
|||||||
import type { LintContext, HyperframeLintFinding } from "../context";
|
import type { LintContext, HyperframeLintFinding } from "../context";
|
||||||
import { readAttr, truncateSnippet } from "../utils";
|
import { readAttr, truncateSnippet } from "../utils";
|
||||||
|
|
||||||
const MAX_COMPOSITION_LINES = 300;
|
|
||||||
const MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
||||||
|
|
||||||
export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
export const compositionRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||||
// composition_file_too_large
|
|
||||||
({ rawSource }) => {
|
|
||||||
const lineCount = rawSource.split(/\r\n|\r|\n/).length;
|
|
||||||
if (lineCount <= MAX_COMPOSITION_LINES) return [];
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
code: "composition_file_too_large",
|
|
||||||
severity: "warning",
|
|
||||||
message: `This composition file has ${lineCount} lines. Large single-file compositions are hard for agents to inspect and revise reliably.`,
|
|
||||||
fixHint:
|
|
||||||
"Split coherent scenes or layers into smaller .html files under compositions/, then mount them from the parent with data-composition-src so each piece can be validated independently.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
|
|
||||||
// timeline_track_too_dense
|
|
||||||
({ tags }) => {
|
|
||||||
const trackCounts = new Map<string, number>();
|
|
||||||
|
|
||||||
for (const tag of tags) {
|
|
||||||
if (readAttr(tag.raw, "data-composition-id")) continue;
|
|
||||||
const startStr = readAttr(tag.raw, "data-start");
|
|
||||||
const trackStr = readAttr(tag.raw, "data-track-index");
|
|
||||||
if (!startStr || !trackStr) continue;
|
|
||||||
|
|
||||||
trackCounts.set(trackStr, (trackCounts.get(trackStr) ?? 0) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const findings: HyperframeLintFinding[] = [];
|
|
||||||
for (const [track, count] of trackCounts) {
|
|
||||||
if (count <= MAX_TIMED_ELEMENTS_PER_TRACK) continue;
|
|
||||||
findings.push({
|
|
||||||
code: "timeline_track_too_dense",
|
|
||||||
severity: "warning",
|
|
||||||
message: `Track ${track} has ${count} timed elements in this file. Dense tracks usually mean too much scene structure is packed into one composition.`,
|
|
||||||
fixHint:
|
|
||||||
"Move coherent scene groups into separate .html files under compositions/ and mount them from the parent with data-composition-src so the timeline stays easier to inspect, revise, and validate.",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return findings;
|
|
||||||
},
|
|
||||||
|
|
||||||
// timed_element_missing_visibility_hidden
|
// timed_element_missing_visibility_hidden
|
||||||
({ tags }) => {
|
({ tags }) => {
|
||||||
const findings: HyperframeLintFinding[] = [];
|
const findings: HyperframeLintFinding[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user