name: regression run-name: ${{ github.event_name == 'workflow_dispatch' && format('Checks (catalog_publish={0})', inputs.catalog_publish) || '' }} permissions: contents: read # Suppress hyperframes CLI telemetry from HeyGen's own CI runs. # External users' CI continues to emit telemetry unless they set this themselves. env: HYPERFRAMES_NO_TELEMETRY: "1" # Graphite can update a branch and its PR base separately for the same head # SHA. Keep only the newest expensive regression run for each PR/ref. concurrency: group: regression-${{ github.event.pull_request.number || github.ref }}${{ github.event_name == 'schedule' && '-nightly' || '' }} # A body or title edit queues behind running checks instead of cancelling them. cancel-in-progress: ${{ github.event.action != 'edited' || github.event.changes.base != null }} on: merge_group: types: [checks_requested] workflow_dispatch: inputs: catalog_publish: description: "Run required checks for a generated catalog publication." type: boolean default: false pull_request: # Re-run when Graphite changes only the PR base for the same head SHA. types: [opened, synchronize, reopened, edited] push: branches: - main schedule: # Daily at 06:17 UTC: the full suite, including fixtures too slow for every PR. - cron: "17 6 * * *" jobs: changes: name: Detect changes runs-on: ubuntu-latest timeout-minutes: 8 permissions: actions: read contents: read outputs: code: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.catalog_publish)) && 'true' || steps.edit.outputs.passed == 'true' && 'false' || steps.release.outputs.release_only == 'true' && 'false' || steps.filter.outputs.code }} matrix: ${{ steps.shards.outputs.matrix }} steps: # Force git-based change detection instead of the pull_request REST API. # The API path can fail the whole workflow on transient listFiles # timeouts before any regression shard even starts. - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 timeout-minutes: 3 - name: Fetch the history the diffs read timeout-minutes: 2 uses: ./.github/actions/fetch-history - name: Check whether this commit already passed before a title or body edit id: edit uses: ./.github/actions/edit-already-passed # HEAD is the PR merge ref; its first parent is the current base tip, unlike the payload's # base.sha, which goes stale and would count the base's newer commits as the PR's changes. - name: Find the pull request's current base id: base if: github.event_name == 'pull_request' run: | git rev-parse --verify -q HEAD^2 > /dev/null || { echo "::error::HEAD is not the pull request merge commit"; exit 1; } sha="$(git rev-parse --verify HEAD^1)" echo "sha=$sha" >> "$GITHUB_OUTPUT" - name: Find a release-only diff id: release uses: ./.github/actions/release-only-diff - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4 id: filter if: github.event_name != 'schedule' && steps.edit.outputs.passed != 'true' with: token: "" base: ${{ steps.base.outputs.sha }} filters: | code: - "packages/core/**" - "packages/producer/**" - "packages/engine/**" - "Dockerfile*" - ".github/workflows/regression.yml" # Bin-pack the shard matrix from recorded per-fixture timings rather than # a hand-written list. Fails if any fixture on disk is neither scheduled # nor explicitly excluded, so a new fixture cannot silently never run. - name: Plan regression shards id: shards env: NIGHTLY: ${{ github.event_name == 'schedule' && '--nightly' || '' }} run: | echo "matrix=$(node packages/producer/scripts/plan-regression-shards.mjs $NIGHTLY)" >> "$GITHUB_OUTPUT" node packages/producer/scripts/plan-regression-shards.mjs $NIGHTLY --pretty preflight: name: Preflight (lint + format) needs: changes if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest timeout-minutes: 5 steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - uses: ./.github/actions/preflight regression-shards: needs: [changes, preflight] if: needs.changes.outputs.code == 'true' runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: true # Bin-packed at run time from packages/producer/tests/shard-schedule.json. # To rebalance, refresh the timings in that file — no YAML edit needed. # To change shard count, set "shardCount" there. matrix: ${{ fromJSON(needs.changes.outputs.matrix) }} steps: - name: Checkout (with LFS) uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: lfs: true - name: Validate LFS files run: | echo "Checking golden baseline MP4s are real files (not LFS pointers)..." for mp4 in packages/producer/tests/*/output/output.mp4; do if [ -f "$mp4" ]; then size=$(stat --format=%s "$mp4") if [ "$size" -lt 1000 ]; then echo "ERROR: $mp4 appears to be an LFS pointer ($size bytes)" exit 1 fi echo "OK: $mp4 ($size bytes)" fi done - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 with: # Docker Hub rate-limits anonymous pulls from shared runners; pull through Google's mirror. driver-opts: image=mirror.gcr.io/moby/buildkit:buildx-stable-1 buildkitd-config-inline: | [registry."docker.io"] mirrors = ["mirror.gcr.io"] - name: Build test Docker image (cached) # Normal duration is ~3 min; a bun install that errors and hangs # instead of exiting (run 35468046055) otherwise runs unbounded until # someone notices and cancels it. Fail fast so a shard fails, not stalls. timeout-minutes: 20 uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: Dockerfile.test load: true tags: hyperframes-producer:test cache-from: type=gha,scope=regression-test-image # One writer: the first shard of a main push. Every shard rebuilds the same layers with # different digests, so each export added ~1.3 GB and evicted the rest of the 10 GB cache. cache-to: ${{ github.event_name == 'push' && strategy.job-index == 0 && 'type=gha,mode=max,scope=regression-test-image' || '' }} - name: "Run regression shard: ${{ matrix.shard }} (${{ matrix.mode }})" run: | echo "Shard: ${{ matrix.shard }}" echo "Mode: ${{ matrix.mode }}" echo "Args: ${{ matrix.args }}" docker run --rm \ -e HYPERFRAMES_NO_TELEMETRY=1 \ --security-opt seccomp=unconfined \ --shm-size=4g \ -v ${{ github.workspace }}/packages/producer/tests:/app/packages/producer/tests \ hyperframes-producer:test \ --mode=${{ matrix.mode }} \ ${{ matrix.args }} - name: Upload failure artifacts if: failure() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: regression-failures-${{ matrix.shard }} path: packages/producer/tests/*/failures/ if-no-files-found: ignore # Summary job — matches the required check name in branch protection regression: runs-on: ubuntu-latest needs: [changes, regression-shards] if: always() steps: - name: Check results env: CHANGES_RESULT: ${{ needs.changes.result }} CODE_CHANGED: ${{ needs.changes.outputs.code }} SHARDS_RESULT: ${{ needs.regression-shards.result }} run: | if [ "$CHANGES_RESULT" != "success" ]; then echo "::error::Change detection did not succeed." exit 1 fi case "$CODE_CHANGED" in false) echo "No regression code changes." ;; true) if [ "$SHARDS_RESULT" != "success" ]; then echo "::error::Regression shards did not succeed." exit 1 fi echo "All regression shards succeeded." ;; *) echo "::error::Change detection returned no valid decision."; exit 1 ;; esac