Files
bitvid/.github/workflows/ci.yml
T
google-labs-jules[bot]andPR0M3TH3AN 1bf491a999 fix(ci): install firefox for headless e2e and safeguard app init
- Updates `.github/workflows/ci.yml` to install `firefox` for the `e2e-headless` job, resolving the "Executable doesn't exist" error.
- Updates `js/app.js` to make `_initAccessControl` async (returning a Promise) and adds a 15-second timeout safeguard. This ensures the app waits for ACLs to load (fixing the race condition) but doesn't hang indefinitely if the network is slow or unresponsive (fixing E2E timeouts).

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-22 18:18:46 +00:00

485 lines
17 KiB
YAML

name: CI
on:
push:
branches:
- main
- unstable
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install ripgrep
run: |
sudo rm -f /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get update && sudo apt-get install -y ripgrep
- name: Run lint suite
run: npm run lint
- name: Build assets
run: npm run build
- name: Verify deploy artifact markup and hashed references
run: npm run verify:dist:deploy-artifact
- name: Smoke check hashed dist asset references
run: npm run smoke:dist-assets
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run cache-upgrade smoke test
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
run: npm run test:e2e:cache-upgrade
- name: Generate dist hash state
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
run: |
mkdir -p artifacts
node scripts/hash-dist.mjs --dist dist --output artifacts/dist-hashes-current.json
- name: Upload dist hash artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
uses: actions/upload-artifact@v4
with:
name: dist-hashes-current
path: artifacts/dist-hashes-current.json
if-no-files-found: error
- name: Run visual regression tests
id: visual
run: npm run test:visual
continue-on-error: true
- name: Upload Playwright artifacts
if: steps.visual.outcome == 'failure'
uses: actions/upload-artifact@v4
with:
name: playwright-visual-diffs
path: |
playwright-report
test-results
if-no-files-found: ignore
- name: Fail if visual regressions detected
if: steps.visual.outcome == 'failure'
run: exit 1
deploy-cache-state:
if: github.event_name == 'push' && github.ref == 'refs/heads/unstable'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 20
concurrency:
group: deploy-cache-state-unstable
cancel-in-progress: false
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Download dist hash artifact
uses: actions/download-artifact@v4
with:
name: dist-hashes-current
path: artifacts
- name: Fetch prior dist hash state and metadata
env:
DIST_HASH_STATE_URL: ${{ secrets.DIST_HASH_STATE_URL }}
run: |
mkdir -p artifacts
if [ -n "$DIST_HASH_STATE_URL" ]; then
curl --fail --silent --show-error --dump-header artifacts/dist-hashes-previous.headers "$DIST_HASH_STATE_URL" -o artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=$(awk 'tolower($1)=="etag:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_VERSION=$(awk 'tolower($1)=="x-dist-hash-version:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_LAST_MODIFIED=$(awk 'BEGIN{IGNORECASE=1} /^Last-Modified:/ {sub(/^Last-Modified:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' artifacts/dist-hashes-previous.headers)
echo "Loaded prior dist hash state from authoritative store."
else
echo '{"combined":"","files":[]}' > artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=''
DIST_HASH_STATE_VERSION=''
DIST_HASH_STATE_LAST_MODIFIED=''
echo "No DIST_HASH_STATE_URL configured; using empty baseline."
fi
{
echo "DIST_HASH_STATE_ETAG=$DIST_HASH_STATE_ETAG"
echo "DIST_HASH_STATE_VERSION=$DIST_HASH_STATE_VERSION"
echo "DIST_HASH_STATE_LAST_MODIFIED=$DIST_HASH_STATE_LAST_MODIFIED"
} >> "$GITHUB_ENV"
echo "State metadata snapshot: etag='${DIST_HASH_STATE_ETAG:-<none>}', version='${DIST_HASH_STATE_VERSION:-<none>}', last-modified='${DIST_HASH_STATE_LAST_MODIFIED:-<none>}'"
- name: Diff dist hash states
run: |
node scripts/diff-dist-hashes.mjs \
--current artifacts/dist-hashes-current.json \
--previous artifacts/dist-hashes-previous.json > artifacts/dist-hashes-changed.txt
echo "Changed dist paths (initial baseline):"
cat artifacts/dist-hashes-changed.txt || true
- name: Plan cache purge scope
run: |
node scripts/plan-cache-purge.mjs \
--changed-file artifacts/dist-hashes-changed.txt \
--output-file artifacts/dist-purge-targets.txt > artifacts/dist-purge-plan.json
echo "Initial purge plan:"
cat artifacts/dist-purge-plan.json
- name: Record authoritative dist hash state with CAS and resolve races
env:
DIST_HASH_STATE_URL: ${{ secrets.DIST_HASH_STATE_URL }}
DIST_HASH_STATE_UPLOAD_URL: ${{ secrets.DIST_HASH_STATE_UPLOAD_URL }}
run: |
if [ -z "$DIST_HASH_STATE_UPLOAD_URL" ]; then
echo "No DIST_HASH_STATE_UPLOAD_URL configured; skipping authoritative hash-state update."
exit 0
fi
set +e
node scripts/store-dist-hash-state.mjs \
--input artifacts/dist-hashes-current.json \
--upload-url "$DIST_HASH_STATE_UPLOAD_URL" \
--if-match "$DIST_HASH_STATE_ETAG" \
--if-version "$DIST_HASH_STATE_VERSION" \
--if-unmodified-since "$DIST_HASH_STATE_LAST_MODIFIED"
STORE_EXIT=$?
set -e
if [ "$STORE_EXIT" -eq 0 ]; then
echo "CAS write succeeded on first attempt; retaining initial purge plan."
exit 0
fi
if [ "$STORE_EXIT" -ne 3 ]; then
echo "Hash-state upload failed with exit code $STORE_EXIT"
exit "$STORE_EXIT"
fi
echo "CAS conflict detected. Re-fetching state and recomputing purge targets before any CDN purge."
if [ -z "$DIST_HASH_STATE_URL" ]; then
echo "DIST_HASH_STATE_URL is required for race resolution after CAS conflict."
exit 1
fi
curl --fail --silent --show-error --dump-header artifacts/dist-hashes-previous.headers "$DIST_HASH_STATE_URL" -o artifacts/dist-hashes-previous.json
DIST_HASH_STATE_ETAG=$(awk 'tolower($1)=="etag:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_VERSION=$(awk 'tolower($1)=="x-dist-hash-version:" {print $2}' artifacts/dist-hashes-previous.headers | tr -d '\r' | tail -n 1)
DIST_HASH_STATE_LAST_MODIFIED=$(awk 'BEGIN{IGNORECASE=1} /^Last-Modified:/ {sub(/^Last-Modified:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' artifacts/dist-hashes-previous.headers)
echo "Race-resolution metadata: etag='${DIST_HASH_STATE_ETAG:-<none>}', version='${DIST_HASH_STATE_VERSION:-<none>}', last-modified='${DIST_HASH_STATE_LAST_MODIFIED:-<none>}'"
node scripts/diff-dist-hashes.mjs \
--current artifacts/dist-hashes-current.json \
--previous artifacts/dist-hashes-previous.json > artifacts/dist-hashes-changed.txt
echo "Changed dist paths (race-resolved baseline):"
cat artifacts/dist-hashes-changed.txt || true
node scripts/plan-cache-purge.mjs \
--changed-file artifacts/dist-hashes-changed.txt \
--output-file artifacts/dist-purge-targets.txt > artifacts/dist-purge-plan.json
echo "Race-resolved purge plan:"
cat artifacts/dist-purge-plan.json
node scripts/store-dist-hash-state.mjs \
--input artifacts/dist-hashes-current.json \
--upload-url "$DIST_HASH_STATE_UPLOAD_URL" \
--if-match "$DIST_HASH_STATE_ETAG" \
--if-version "$DIST_HASH_STATE_VERSION" \
--if-unmodified-since "$DIST_HASH_STATE_LAST_MODIFIED"
echo "CAS write succeeded after race resolution; proceeding with recomputed purge targets."
- name: Purge changed Cloudflare URLs
env:
CLOUDFLARE_ZONE_ID: ${{ secrets.CLOUDFLARE_ZONE_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }}
run: |
if [ ! -s artifacts/dist-purge-targets.txt ]; then
echo "No purge targets detected. Skipping purge."
exit 0
fi
if [ -z "$CLOUDFLARE_ZONE_ID" ] || [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then
echo "Cloudflare secrets not configured; skipping targeted purge."
exit 0
fi
node scripts/purge-cloudflare-changed.mjs \
--changed-file artifacts/dist-purge-targets.txt \
--base-url "$CLOUDFLARE_PURGE_BASE_URL"
- name: Verify deployed index.html version markup
env:
CLOUDFLARE_PURGE_BASE_URL: ${{ secrets.CLOUDFLARE_PURGE_BASE_URL }}
run: |
if [ -z "$CLOUDFLARE_PURGE_BASE_URL" ]; then
echo "CLOUDFLARE_PURGE_BASE_URL is required to verify deployed index.html"
exit 1
fi
DEPLOY_INDEX_URL="${CLOUDFLARE_PURGE_BASE_URL%/}/index.html" npm run verify:deployed:index-html
unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node-version: [22]
shard: [1, 2, 3]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run unit tests (shard ${{ matrix.shard }}/3)
env:
UNIT_TEST_TIMEOUT_MS: 300000
run: npm run test:unit:shard${{ matrix.shard }}
dm-unit-tests:
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run DM unit tests
run: npm run test:dm:unit
e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 35
strategy:
fail-fast: false
matrix:
node-version: [22]
project: [e2e, e2e-firefox]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Build app
run: npm run build
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox
- name: Run deterministic E2E suite (${{ matrix.project }})
run: npx playwright test tests/e2e --project=${{ matrix.project }}
dm-integration-tests:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Run DM integration tests
run: npm run test:dm:integration
e2e-headless:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
matrix:
node-version: [22]
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Cache npm directory
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-npm-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-npm-
- name: Cache node_modules
id: node-modules-cache
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-node-${{ matrix.node-version }}-modules-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-modules-
- name: Install dependencies
if: steps.node-modules-cache.outputs.cache-hit != 'true'
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox
- name: Run headless E2E tests
run: npm run test:e2e