Files
heygen-com__hyperframes/.github/workflows/catalog-previews.yml
Vance Ingalls de4062a933 fix: create temp dirs with mkdtemp, not a name built from Date.now() (#3241)
* fix: create temp dirs with mkdtemp, not a name built from Date.now()

Closes nine open `js/insecure-temporary-file` alerts — the technically
correct ones. An audit of all 29 open alerts for that rule split them
three ways:

- 19 false positives: the write lands inside a directory the caller
  already made with `mkdtempSync`, and CodeQL's dataflow reaches
  `tmpdir()` without seeing the mkdtemp in between.
- 1 mitigated: `fontCompression.ts` writes with `flag: "wx"` and only
  takes the tmpdir branch inside Lambda, where /tmp is single-tenant.
- 9 real, and these are them. A name built from `Date.now()` under the
  shared temp dir, followed by `mkdirSync`, is guessable to the
  millisecond AND leaves a window between choosing the name and creating
  it, so on a shared machine another user can pre-create or symlink the
  path first.

`mkdtempSync` closes both halves: it picks the random suffix and creates
the directory 0700 in one syscall. Same shape, one line shorter, and the
alerts go away rather than being dismissed.

Six sites in `normalize.test.ts` (its `mkdirSync` import goes with them),
one in `generate-catalog-previews.ts` — that single construction accounted
for three alerts, since the other two were writes into the directory it
made.

No shared helper. `mkdtempSync` is already the stdlib primitive for
exactly this, and the two callers live in different packages, so a wrapper
would need a home in core to serve one CLI test and one build script —
more indirection than the line it saves.

Deliberately not touching the other 20: excluding the rule repo-wide would
hide this class of bug from future code, which is the reason these are
fixed rather than silenced.

* fix: track the wav temp dir for cleanup and finish the mkdtemp sweep

The wav helper pushed the file path into `dirs`, so `afterEach` removed
`tone.wav` and left the directory it had just made — four per suite run.
Push the directory and derive the file path from it. Measured: the old code
leaks 4 directories per run, the new code leaks 0.

Three sites still built a predictable name and then created it. CodeQL never
flagged them — its dataflow reaches the template preview writes through a
`readdir` walk and does not connect them back to the `tmpdir()` root — so the
alert list was narrower than the pattern, and closing only the alerts would
turn the rule green while the shape survived where nothing would re-flag it.
`generate-template-previews.ts` is the near-twin of the file this change
started from, and the other two are producer dev entry points. All three use
the path only through the variable, so the random suffix changes nothing.

Catalog previews now call the existing `createCatalogPreviewTempDir` instead
of repeating its body. That test was in no runner, so it pinned uniqueness and
mode 0700 on a function nothing called; adding it to `test:scripts` alongside
a real caller makes it load-bearing. The rationale for the primitive moves to
the helper, which is now the only place it lives.

* ci: re-run catalog previews when the temp-dir module changes

Routing the renderer through `createCatalogPreviewTempDir` made that module
part of its runtime path, and the workflow already states the rule for the
sibling case: a module the renderer imports has to appear in the trigger, or a
change to it alone never re-runs the job that exercises it. Add it to the
`paths:` filter and to the renderer canary, so a PR touching only the temp-dir
allocation still renders both shape canaries.

Verified against this branch's own range: the previous argument list does not
report the file, so a helper-only PR was invisible to both checks.
2026-08-14 11:20:37 -07:00

128 lines
4.7 KiB
YAML

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"
# Modules the renderer imports. Without them a change to path-traversal
# defence or temp-directory allocation alone never re-runs the job that
# exercises it.
- "scripts/registry-target-paths.mjs"
- "scripts/catalog-preview-temp.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
# The renderer shells out to ffmpeg for both halves of a preview: the
# poster transcode and the web encode of the mp4. Neither ran here before
# (`--skip-video` skipped the encode, and the poster copy was a plain
# file copy), so the job never needed it and ubuntu-latest does not ship
# it.
- name: Install ffmpeg
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ffmpeg
- name: Render changed block/component previews
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
# Find which blocks/components changed in this PR. The trailing slash
# in the grep matters: an item is a directory, and a file sitting
# directly under registry/components (CATALOG.md) does not match the
# sed, so without it the whole path survives and is passed to the
# renderer as if it were an item name.
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)
# A renderer change reaches every item, so it cannot be trusted to a
# PR that happens to also touch a block. Two canaries cover the two
# shapes the renderer has to tell apart: a block whose scene lives in
# a <template> (mounted through a wrapper) and one that registers its
# timeline at body level (rendered directly). Getting that wrong is
# silent — the wrong-shaped block renders blank, not red.
RENDERER_CHANGED=$(git diff --name-only --diff-filter=ACMR "$BASE_SHA"...HEAD \
-- scripts/generate-catalog-previews.ts scripts/registry-target-paths.mjs \
scripts/catalog-preview-temp.ts)
if [ -n "$RENDERER_CHANGED" ]; then
CHANGED_ITEMS=$(printf '%s\n' $CHANGED_ITEMS \
code-snippet-visual-studio-dark code-snippet-apple-terminal-pro | sort -u)
fi
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