Skip to content

ci: pin the Chromium cache action to a commit #278

ci: pin the Chromium cache action to a commit

ci: pin the Chromium cache action to a commit #278

Workflow file for this run

name: CI
on:
pull_request:
push:
branches:
- main
permissions:
contents: read
concurrency:
group: ci-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# Skips jobs a pull request cannot affect: a web change does not run the
# server shards, a server change does not run the mobile tests, and the macOS
# lint (~6.7x a Linux minute) runs only for native mobile sources.
# .github/scripts/ci-changes.cjs maps changed files to workspace packages and
# their dependents. Pushes to main still run every Linux job. Detection fails
# open: if the diff cannot be resolved, everything runs.
changes:
name: Changes
runs-on: ubuntu-24.04 # blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
outputs:
typecheck: ${{ steps.detect.outputs.typecheck }}
build: ${{ steps.detect.outputs.build }}
test: ${{ steps.detect.outputs.test }}
test_packages: ${{ steps.detect.outputs.test_packages }}
test_web: ${{ steps.detect.outputs.test_web }}
test_server: ${{ steps.detect.outputs.test_server }}
rust: ${{ steps.detect.outputs.rust }}
release_smoke: ${{ steps.detect.outputs.release_smoke }}
mobile_native: ${{ steps.detect.outputs.mobile_native }}
steps:
# Only the package manifests the script reads to build the dependency graph.
# Depth 2 brings the base commit the change is diffed against.
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 2
sparse-checkout: |
/pnpm-workspace.yaml
/.github/scripts/ci-changes.cjs
/.github/actions/ci-changes/
/*/package.json
/*/*/package.json
!/.repos/
sparse-checkout-cone-mode: false
- name: Detect affected jobs
id: detect
uses: ./.github/actions/ci-changes
# Lint and the desktop build each get a runner so they run side by side.
# Typecheck shares the build's install. Lint always runs: it formats every
# file, docs included. The `Check` job at the end of this file passes only
# when every other job passes or was skipped by `changes`.
lint:
name: Lint
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
- name: Reject repository-owned PR assets
run: |
files="$(git ls-files .github/pr-assets)"
if test -n "$files"; then
printf 'PR evidence must be uploaded to GitHub, not committed:\n%s\n' "$files" >&2
exit 1
fi
- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron
# Files/dependencies are repo-wide; export checks cover clean workspaces only.
- name: Check unused code
run: vp run knip:check
- name: Check generated GitHub GraphQL operations
run: vp run --filter @t3tools/source-control-github generate --check
- name: Check
run: vp check
build:
name: Build and Typecheck
needs: changes
if: ${{ !cancelled() && (needs.changes.outputs.build != 'false' || needs.changes.outputs.typecheck != 'false') }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
- uses: ./.github/actions/setup-apt-mirrors
if: needs.changes.outputs.build != 'false'
# Runs in the background while Vite+ installs; the step before the build waits for it.
- name: Start installing browser secret helper build libraries
if: needs.changes.outputs.build != 'false'
run: |
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
(
set +e
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
echo "$?" > "$RUNNER_TEMP/apt-status"
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
- name: Ensure Electron runtime is installed
id: electron
run: vp run --filter @t3tools/desktop ensure:electron
# Runs in the background while the build runs, and reports in the last
# step: neither result hides the other.
- name: Start typecheck
id: typecheck
if: ${{ !cancelled() && steps.electron.outcome == 'success' && needs.changes.outputs.typecheck != 'false' }}
run: |
(
set +e
vpr typecheck
echo "$?" > "$RUNNER_TEMP/typecheck-status"
) > "$RUNNER_TEMP/typecheck.log" 2>&1 < /dev/null &
- name: Finish installing browser secret helper build libraries
id: apt
if: ${{ !cancelled() && needs.changes.outputs.build != 'false' }}
timeout-minutes: 5
run: |
until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done
cat "$RUNNER_TEMP/apt.log"
exit "$(cat "$RUNNER_TEMP/apt-status")"
- name: Build desktop pipeline
id: build
if: ${{ !cancelled() && steps.electron.outcome == 'success' && steps.apt.outcome == 'success' && needs.changes.outputs.build != 'false' }}
run: vp run build:desktop
- name: Verify preload bundle output
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: node apps/desktop/scripts/verify-preload-bundle.mjs
- name: Typecheck
if: ${{ !cancelled() && steps.typecheck.outcome == 'success' }}
timeout-minutes: 5
run: |
until test -s "$RUNNER_TEMP/typecheck-status"; do sleep 1; done
cat "$RUNNER_TEMP/typecheck.log"
exit "$(cat "$RUNNER_TEMP/typecheck-status")"
# Everything except `t3` (apps/server) and the web app, which has its own job
# below. `--parallel` drops the package dependency ordering that `vp run`
# applies by default: these `test` tasks declare no `dependsOn` and resolve
# workspace deps from source, so ordering only bought us idle runners between
# dependency layers. The concurrency limit stays at the default 4 so peak load
# per runner is unchanged.
test:
name: Test
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.test != 'false' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
# Blacksmith boots GitHub's Ubuntu runner image (gcc is usually present),
# but ACP process-tree live tests compile a small pthread fixture with `cc`
# and soft-skip when it is missing. Install build-essential so that path
# always runs in CI instead of silently no-oping.
- name: Install C toolchain for process-tree fixtures
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends build-essential
command -v cc
- uses: ./.github/actions/setup-apt-mirrors
# The desktop tests compile the browser secret helper. The install runs in
# the background while Vite+ installs; the step before the tests waits for it.
- name: Start installing browser secret helper build libraries
run: |
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
(
set +e
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
echo "$?" > "$RUNNER_TEMP/apt-status"
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
- name: Ensure Electron runtime is installed
run: vp run --filter @t3tools/desktop ensure:electron
- name: Test preview artifact validation
run: python3 -B .github/scripts/stage-preview-bundle.test.py
- name: Test release checks
run: node --test .github/scripts/check-nightly-release.test.cjs
- name: Finish installing browser secret helper build libraries
timeout-minutes: 5
run: |
until test -s "$RUNNER_TEMP/apt-status"; do sleep 1; done
cat "$RUNNER_TEMP/apt.log"
exit "$(cat "$RUNNER_TEMP/apt-status")"
# `changes` lists the affected packages, or nothing when all of them are.
- name: Test
env:
TEST_PACKAGES: ${{ needs.changes.outputs.test_packages }}
run: |
filters=(--filter '!t3' --filter '!@t3tools/monorepo' --filter '!@t3tools/web')
if [[ -n "$TEST_PACKAGES" ]]; then
filters=()
for package in $TEST_PACKAGES; do filters+=(--filter "$package"); done
fi
vp run --parallel --concurrency-limit 4 "${filters[@]}" test
# The web suite is the largest outside the server. In the shared Test job it
# started last and ran alone for about 40s after everything else finished.
test_web:
name: Test Web
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.test_web != 'false' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
# The browser project runs React tests in headless Chromium. Same cache key as
# test_server, so both jobs share one downloaded browser.
- name: Resolve Playwright version
id: playwright_version
run: echo "version=$(node -p "require('./apps/web/node_modules/playwright/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Chromium
id: chromium_cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}
- name: Install Chromium for web browser tests
if: steps.chromium_cache.outputs.cache-hit != 'true'
run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium
- name: Install missing Chromium system dependencies
run: |
browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
test -n "$browser"
if ldd "$browser" | grep '=> not found'; then
node apps/web/node_modules/playwright/cli.js install-deps chromium
fi
- name: Test
run: vp run --filter @t3tools/web test
# apps/server sets `fileParallelism: false`, so its test files run strictly
# one at a time. Sharding spreads them over separate runners instead of
# separate workers, so no two server test files ever share a machine and the
# isolation that flag buys is preserved exactly. One file at a time keeps
# about 1.5 cores busy, so 4 vCPU runners lose nothing against 8. The shards
# split files by recorded duration (apps/server/src/testUtils/weightedShardSequencer.ts).
# The shards are the longest jobs, so they gate themselves instead of waiting
# ~15s for `changes`: a shard the change cannot affect stops after checkout.
test_server:
name: Test Server ${{ matrix.shard }}
runs-on: ubuntu-24.04 # blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
# Depth 2 brings the base commit the change is diffed against.
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 2
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
- name: Detect affected jobs
id: changes
uses: ./.github/actions/ci-changes
- name: Setup Vite+
if: steps.changes.outputs.test_server != 'false'
uses: ./.github/actions/setup-ci-deps
# No Electron setup here: `t3` (apps/server) has no Electron dependency
# and none of its tests touch the runtime. Only the non-server `test`
# job, which covers @t3tools/desktop, needs the download.
# Cache the pinned browser and skip apt unless shared libraries are missing.
- name: Resolve Playwright version
id: playwright_version
if: steps.changes.outputs.test_server != 'false'
run: echo "version=$(node -p "require('./apps/server/node_modules/playwright-core/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Chromium
id: chromium_cache
if: steps.changes.outputs.test_server != 'false'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-shell-${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright_version.outputs.version }}
- name: Install Chromium for server browser integration tests
if: steps.changes.outputs.test_server != 'false' && steps.chromium_cache.outputs.cache-hit != 'true'
run: node apps/server/node_modules/playwright-core/cli.js install --only-shell chromium
- name: Install missing Chromium system dependencies
if: steps.changes.outputs.test_server != 'false'
run: |
browser=$(find "$HOME/.cache/ms-playwright" -name chrome-headless-shell -type f -print -quit)
test -n "$browser"
if ldd "$browser" | grep '=> not found'; then
node apps/server/node_modules/playwright-core/cli.js install-deps chromium
fi
- name: Test
if: steps.changes.outputs.test_server != 'false'
env:
T3CODE_TRANSFER_BUDGET_REPORT_PATH: ${{ runner.temp }}/t3code-transfer-budget.md
T3CODE_TRANSFER_BUDGET_RESULT_PATH: ${{ runner.temp }}/thread-transfer-result.json
run: vp run --filter t3 test --shard ${{ matrix.shard }}/${{ strategy.job-total }}
# integration/transferBudgetV2.integration.test.ts writes the report; exactly one shard
# produces these files. Gating the upload on their presence keeps a
# single `thread-transfer-results` artifact per run, which is the name
# thread-transfer-report.yml resolves.
- name: Detect transfer budget report
id: transfer_budget
if: always()
run: |
if test -f "${{ runner.temp }}/thread-transfer-result.json"; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
fi
- name: Publish transfer budget report
if: always() && steps.transfer_budget.outputs.present == 'true'
run: |
if test -f "${{ runner.temp }}/t3code-transfer-budget.md"; then
tee -a "$GITHUB_STEP_SUMMARY" < "${{ runner.temp }}/t3code-transfer-budget.md"
else
echo "Transfer budget report was not produced." >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload thread transfer result
if: always() && steps.transfer_budget.outputs.present == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: thread-transfer-results
path: ${{ runner.temp }}/thread-transfer-result.json
if-no-files-found: ignore
retention-days: 30
# Split out of Check and Test: both paid ~7-9s to install a Rust toolchain
# for checks that take under 3s, on the critical path of every PR.
rust:
name: Rust
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.rust != 'false' }}
runs-on: ubuntu-24.04 # blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
- name: Setup Rust
uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt
- name: Check Rust formatting
run: |
for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do
cargo fmt --manifest-path "native/$crate/Cargo.toml" -- --check
done
- name: Test Rust crates
run: |
for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do
cargo test --locked --manifest-path "native/$crate/Cargo.toml"
done
mobile_native_static_analysis:
name: Mobile Native Static Analysis
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.mobile_native != 'false' }}
runs-on: macos-26 # blacksmith-6vcpu-macos-26
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
sparse-checkout-cone-mode: false
- name: Setup Vite+
uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1.15.0
with:
node-version-file: package.json
cache: true
run-install: |
args:
- --filter=@t3tools/scripts...
- name: Install mobile native static analysis tools
run: brew bundle install --file apps/mobile/Brewfile
- name: Lint mobile native sources
run: vp run lint:mobile
release_smoke:
name: Release Smoke
needs: changes
if: ${{ !cancelled() && needs.changes.outputs.release_smoke != 'false' }}
runs-on: ubuntu-24.04 # blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
sparse-checkout: |
/*
!/.repos/
!/apps/mobile/modules/t3-terminal/Vendor/**/*.a
sparse-checkout-cone-mode: false
# A full install: a filtered one that misses the cache first saves its
# partial store under the shared lockfile key for every later job.
- name: Setup Vite+
uses: ./.github/actions/setup-ci-deps
- name: Exercise release-only workflow steps
run: node scripts/release-smoke.ts
# Branch protection requires this job by its name, `Check`. It fails when any
# other job fails, is cancelled, or is skipped without a reason, so splitting
# or resharding jobs needs no ruleset change. Add every new job to `needs`.
# `always()` rather than `!cancelled()`: a skipped job reports success to
# branch protection, so a cancelled run must still produce a failing check.
check:
name: Check
if: ${{ always() }}
needs:
[
changes,
lint,
build,
test,
test_web,
test_server,
rust,
mobile_native_static_analysis,
release_smoke,
]
runs-on: ubuntu-24.04 # blacksmith-2vcpu-ubuntu-2404
timeout-minutes: 5
steps:
- name: Require every job to pass
env:
RESULTS: ${{ toJSON(needs) }}
run: |
echo "$RESULTS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
# A job may skip only when `changes` said every gate it represents was
# not needed. Gates are keyed by job name, except merged Build and the
# macOS lint.
echo "$RESULTS" | jq -e '.changes.outputs as $gates
| {build: ["build", "typecheck"], mobile_native_static_analysis: ["mobile_native"]} as $renamed
| all(to_entries[];
.value.result == "success"
or (.value.result == "skipped"
and (($renamed[.key] // [.key]) | all(.[]; $gates[.] == "false"))))' > /dev/null
# thread-transfer-report.yml reads this artifact after CI, so a run whose
# server tests ran must have produced it.
- name: Require transfer measurements
if: needs.test_server.result == 'success' && needs.changes.outputs.test_server != 'false'
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: thread-transfer-results
path: ${{ runner.temp }}/thread-transfer