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: "unit" runs-on: depot-ubuntu-24.04-4 timeout-minutes: 15 strategy: fail-fast: false matrix: node-version: [20.x, 22.x, 24.x] steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.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 run: pnpm install --frozen-lockfile - name: Configure Nx Cloud environment run: | echo "NX_CI_EXECUTION_ID=${{ github.run_id }}-${{ github.run_attempt }}-unit-v1-${{ matrix.node-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:-}...$(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:-}" - 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