Files
copilotkit__copilotkit/.github/workflows/test_unit.yml
Benjamin Taylor 5c0150392f ci: stop Playwright browser installs shelling out to apt
Every Playwright install in CI passed `--with-deps`, which runs `apt-get
update` before downloading the browser. apt on the runners cannot always
reach azure.archive.ubuntu.com; when it can't it retries for many minutes,
which is long enough to burn a job's whole `timeout-minutes` budget before
a single test runs. GitHub renders that kill as "The operation was
canceled", so it reads as a test failure rather than an infrastructure hang.

Chromium's system libraries are already present on the Ubuntu runner
images, and every one of these steps installs chromium only, so the browser
download is all they need. Six jobs lose their apt dependency:
test_unit, test_e2e-legacy-v1, test_e2e-showcase-on-demand,
test_showcase-frontend-matrix, showcase_eval and showcase_capture-previews.

Ports CopilotKit/website#529 to this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:06:44 -05:00

263 lines
12 KiB
YAML

name: test / unit
on:
push:
branches: [main]
paths-ignore:
- "README.md"
- "examples/**"
- "showcase/**"
- "sdk-python/**"
pull_request:
branches: [main]
paths-ignore:
- "README.md"
- "examples/**"
- "showcase/**"
- "sdk-python/**"
workflow_dispatch:
inputs:
branch:
description: "Branch to run the workflow on"
required: true
default: "main"
type: string
env:
NODE_OPTIONS: "--max-old-space-size=4096"
NX_VERBOSE_LOGGING: true
NX_CI_EXECUTION_ID: ${{ github.head_ref }}-${{ github.sha }}-${{ github.run_attempt }}
NX_CI_EXECUTION_ENV: "Unit Tests"
# Least-privilege by default. Individual jobs/steps can widen when needed.
# id-token: write is required for Depot OIDC auth (runs-on: depot-ubuntu-*).
permissions:
contents: read
id-token: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
unit:
name: "Node ${{ matrix.node-version }}, React ${{ matrix.react-version }}"
runs-on: depot-ubuntu-24.04-4
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
node-version: [20.x, 22.x, 24.x]
# React 18 + 19 span the supported peer range
# ("^18 || ^19") declared by @copilotkit/react-core, react-ui, and
# a2ui-renderer. 19 is the repo default (frozen lockfile); 18 is
# installed by overriding the root pnpm.overrides in the install step.
react-version: ["18", "19"]
include:
- react-version: "18"
react: "18.3.1"
react-dom: "18.3.1"
types-react: "^18"
types-react-dom: "^18"
testing-library-react: "^14.3.1"
- react-version: "19"
react: "19.2.3"
react-dom: "19.2.3"
types-react: "^19.1.0"
types-react-dom: "^19.0.2"
testing-library-react: "^16.3.0"
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: ${{ github.event.inputs.branch || github.ref }}
persist-credentials: false
# Full history so `nx affected` can diff HEAD against the PR base /
# the previous push, instead of rebuilding+retesting every package
# on every run. A shallow clone has no merge-base to diff against.
fetch-depth: 0
- name: Setup pnpm
# Omit `version:` so pnpm/action-setup inherits from the repo's
# `packageManager` field in package.json (via corepack).
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
# Do NOT use cache: "pnpm" here — its key omits the Node.js version,
# so a better-sqlite3 binary compiled for one Node ABI (e.g. ABI 137
# from Node 24) would be served to a job running a different ABI
# (Node 20 = ABI 115, Node 22 = ABI 127), causing "Module did not
# self-register". We handle pnpm caching manually below with the
# node-version in the key.
# Fork-safety note: actions/cache is equally fork-safe — GitHub
# prevents fork PRs from writing to the base repo's cache at the platform level.
- name: Get pnpm store directory
id: pnpm-cache
run: echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store (scoped to Node.js version)
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.pnpm-cache.outputs.store-path }}
key: ${{ runner.os }}-pnpm-store-${{ matrix.node-version }}-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-${{ matrix.node-version }}-
- name: Install dependencies
env:
REACT_VERSION: ${{ matrix.react }}
REACT_DOM_VERSION: ${{ matrix.react-dom }}
TYPES_REACT_VERSION: ${{ matrix.types-react }}
TYPES_REACT_DOM_VERSION: ${{ matrix.types-react-dom }}
TESTING_LIBRARY_REACT_VERSION: ${{ matrix.testing-library-react }}
# React 19 is the repo default and installs against the committed
# lockfile. Other matrix legs (React 18) override the root
# pnpm.overrides so every package resolves to that React, then install
# unfrozen to let the lockfile float for the overridden versions.
run: |
if [ "${{ matrix.react-version }}" != "19" ]; then
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.pnpm.overrides.react = process.env.REACT_VERSION;
pkg.pnpm.overrides['react-dom'] = process.env.REACT_DOM_VERSION;
pkg.pnpm.overrides['@types/react'] = process.env.TYPES_REACT_VERSION;
pkg.pnpm.overrides['@types/react-dom'] = process.env.TYPES_REACT_DOM_VERSION;
pkg.pnpm.overrides['@testing-library/react'] = process.env.TESTING_LIBRARY_REACT_VERSION;
pkg.pnpm.overrides['streamdown>react'] = process.env.REACT_VERSION;
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
pnpm install --no-frozen-lockfile
else
pnpm install --frozen-lockfile
fi
- name: Verify installed React version matches matrix
env:
EXPECTED_REACT_VERSION: ${{ matrix.react }}
# Resolve from packages/react-core (which has react as a peerDep and
# therefore a node_modules/react symlink). The repo root doesn't
# declare react as a direct dep, so require('react') fails there.
working-directory: packages/react-core
run: |
INSTALLED=$(node -e "console.log(require('react/package.json').version)")
# matrix.react is an exact version ("18.3.1" etc.) — exact match is correct.
if [ "$INSTALLED" != "$EXPECTED_REACT_VERSION" ]; then
echo "::error::Expected React $EXPECTED_REACT_VERSION but got $INSTALLED"
exit 1
fi
echo "React $INSTALLED installed as expected."
- name: Configure Nx Cloud environment
run: |
echo "NX_CI_EXECUTION_ID=${{ github.run_id }}-${{ github.run_attempt }}-unit-v1-${{ matrix.node-version }}-react${{ matrix.react-version }}" >> $GITHUB_ENV
echo "NX_CLOUD_NO_TIMEOUTS=true" >> $GITHUB_ENV
echo "NX_CLOUD_DISTRIBUTED_EXECUTION=false" >> $GITHUB_ENV
echo "NX_NO_CLOUD=true" >> $GITHUB_ENV
echo "NX_TUI=false" >> $GITHUB_ENV
- name: Determine affected range
# Pass GitHub context through env (not inline ${{ }} in the script) to
# avoid template-injection — base_ref is attacker-influenceable.
env:
EVENT_NAME: ${{ github.event_name }}
BASE_REF: ${{ github.base_ref }}
BEFORE_SHA: ${{ github.event.before }}
run: |
if [ "$EVENT_NAME" = "pull_request" ]; then
# Diff against the merge-base with the (current tip of the) base
# branch so advances on main don't drag unrelated packages in.
git fetch --no-tags origin "$BASE_REF"
BASE=$(git merge-base FETCH_HEAD HEAD)
elif [ "$EVENT_NAME" = "push" ]; then
# BEFORE_SHA (github.event.before) is the previous tip of this branch.
BASE="$BEFORE_SHA"
if [ -z "$BASE" ] \
|| [ "$BASE" = "0000000000000000000000000000000000000000" ] \
|| ! git cat-file -e "${BASE}^{commit}" 2>/dev/null; then
# First push / force-push / unknown parent → previous commit.
BASE=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD)
fi
fi
echo "NX_BASE=${BASE}" >> "$GITHUB_ENV"
echo "NX_HEAD=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
echo "Affected range: ${BASE:-<full>}...$(git rev-parse HEAD)"
- name: Generate GraphQL codegen files
run: npx nx run @copilotkit/runtime-client-gql:graphql-codegen
- name: Select test projects
id: select
# PR/push → only packages affected since the base. workflow_dispatch
# (manual / nightly-style full run) → every package with tests.
# `--projects` scopes to packages/** in `nx show projects` (it does NOT
# in the `nx affected` run form, which also pulls in downstream
# examples/storybook — hence the show-projects → run-many split).
env:
EVENT_NAME: ${{ github.event_name }}
# The workflow sets NX_VERBOSE_LOGGING=true, which makes `nx show
# projects` print "[isolated-plugin] spawned worker…" to stdout and
# corrupt the --json payload we parse below. Force it off here.
NX_VERBOSE_LOGGING: "false"
run: |
# Editing this workflow can't surface as an "affected" nx package, so
# `nx affected` would select nothing and the build/test path would go
# unexercised on the very PR that changes it. Force a full run when
# this file itself changed in the range, same as a manual dispatch.
FULL=false
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
FULL=true
elif git diff --name-only "$NX_BASE" "$NX_HEAD" \
| grep -qx '.github/workflows/test_unit.yml'; then
FULL=true
echo "test_unit.yml changed in range → running ALL packages."
fi
if [ "$FULL" = "true" ]; then
PROJECTS=$(npx nx show projects --projects='packages/**' --exclude=@copilotkit/demo-agents -t test --json)
else
PROJECTS=$(npx nx show projects --affected --base="$NX_BASE" --head="$NX_HEAD" --projects='packages/**' --exclude=@copilotkit/demo-agents -t test --json)
fi
LIST=$(printf '%s' "$PROJECTS" | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>process.stdout.write(JSON.parse(d).join(',')))")
echo "projects=$LIST" >> "$GITHUB_OUTPUT"
if [ -n "$LIST" ]; then echo "has=true" >> "$GITHUB_OUTPUT"; else echo "has=false" >> "$GITHUB_OUTPUT"; fi
echo "Selected projects: ${LIST:-<none>}"
- name: Build and test affected packages
if: steps.select.outputs.has == 'true'
# run-many builds each selected package's upstream deps via `^build`,
# so unchanged dependencies are still compiled when something needs them.
env:
PROJECTS: ${{ steps.select.outputs.projects }}
run: npx nx run-many -t build,test --projects="$PROJECTS" --exclude=@copilotkit/demo-agents
- name: No affected packages
if: steps.select.outputs.has != 'true'
run: echo "No package code affected since the base — skipping build & test."
- name: Run release script tests
run: npx vitest run --config scripts/release/vitest.config.mts
- name: Verify packed Channels umbrella contract
if: matrix.node-version == '20.x'
run: pnpm run verify:channels-umbrella
# No --with-deps: it shells out to apt, which on the runners cannot always
# reach azure.archive.ubuntu.com and retries for many minutes — long enough to
# burn this job's whole timeout before a test runs. Chromium's system libraries
# are already present on the Ubuntu runner image, so downloading the browser is
# all this step needs.
- name: Install Chromium for packed Angular browser smoke
if: matrix.node-version == '22.x'
run: pnpm --dir showcase/scripts exec playwright install chromium
- name: Verify packed Angular consumer matrix
if: matrix.node-version == '22.x'
run: pnpm run verify:angular-package
- name: Verify packed Runtime managed Channels contract
if: matrix.node-version == '20.x'
run: pnpm run verify:runtime-package