mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
ci(reskinnable-demo): gate the demo's four gates and its agent's lockfile
Nothing in CI built or type-checked this app. Leaving the root pnpm
workspace — which was right, it contains the canary line — also removed it
from every repo-wide sweep, because Nx discovers projects THROUGH the
workspace and there is no `workspaceLayout` in `nx.json`:
nx run-many -t build static_compat.yml does not see it
nx run-many -t check-types static_quality.yml does not see it
nx run-many -t test test_unit.yml does not see it
Both static workflows also carry `paths-ignore: ["examples/**"]`, so this
is a new workflow rather than an edit to either. It follows
`test_unit-showcase.yml`, which exists for the same reason applied to a
different directory.
Two jobs.
`gates` runs lint, typecheck, unit (2460 tests) and build, cheapest first,
inside the app because no root task reaches it. Measured locally at ~2
minutes of gates; the 20m budget is install headroom.
The build gate is the one that matters most and the one a developer can
least run: `next build` corrupts a concurrently running dev server's
PostCSS/Turbopack cache — measured, `globals.css` transforms to garbage
and every route 500s, and a dev-server restart does not clear it because
the corruption is on disk. CI is the only safe home for it. It needs no
env: the route constructs one agent per skin at module load and none of
them requires a key (banking's is an `HttpAgent` whose URL is never called
during a build).
`agent-resolve` syncs `agent/uv.lock` with `--frozen` and then asserts the
subagent surface is actually present. That is not hypothetical: until this
branch pinned it, `ag-ui-langgraph>=0.0.43` resolved the RELEASE, which
accepts no `emit_subagent_events` and exports no subagent symbols — the
demo's headline feature, failing silently because the flag is set as an
attribute on an object nobody reads. The assertion is on the CAPABILITY
rather than the version string, since a version assertion goes stale the
moment the pin moves and the surface is what must stay true.
One uncertainty is made into a gate rather than left to trust: this app
pins pnpm@10.10.0 while the root pins pnpm@10.33.4, and the older resolver
is load-bearing here — it still reads `pnpm.overrides` from package.json,
the only place `@ag-ui/core`, `@ag-ui/encoder` and `@ag-ui/proto` are
pinned to the canary (`@ag-ui/client` is a direct dependency; those three
are not). `pnpm/action-setup` is pointed at the app's package.json, and
because an unexpected action input is a WARNING in Actions rather than an
error, a step then compares `pnpm --version` against the pin and fails
loudly if they differ.
No `continue-on-error` and no `|| true` anywhere in the file.
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
name: test / reskinnable-demo
|
||||
|
||||
# The four gates for examples/showcases/reskinnable-demo, plus a resolve check
|
||||
# for its Python agent.
|
||||
#
|
||||
# WHY THIS WORKFLOW EXISTS
|
||||
# ------------------------
|
||||
# This app left the root pnpm workspace so its `@ag-ui/*` / `@copilotkit/*`
|
||||
# canary line could not leak into the rest of the monorepo. Nx discovers
|
||||
# projects THROUGH the pnpm workspace (there is no `workspaceLayout` in
|
||||
# `nx.json`), so leaving it also removed the app from the repo-wide sweeps:
|
||||
#
|
||||
# sweep workflow sees this app?
|
||||
# ------------------------------------- ------------------- --------------
|
||||
# nx run-many -t build static_compat.yml NO
|
||||
# nx run-many -t check-types static_quality.yml NO
|
||||
# nx run-many -t test --projects=... test_unit.yml NO
|
||||
#
|
||||
# Both static workflows additionally carry `paths-ignore: ["examples/**"]`, so
|
||||
# a new workflow is the right shape rather than an edit to either. Verified on
|
||||
# the commit this branched from: `nx show project` cannot find the standalone
|
||||
# showcases, and no workflow file named this app.
|
||||
#
|
||||
# Net effect until this landed: nothing in CI built or type-checked the
|
||||
# reference sales demo. A change anywhere in `packages/*` could break it, or a
|
||||
# change inside it could break itself, and the check list stayed green — the
|
||||
# four gates were "whoever remembers to run them locally".
|
||||
#
|
||||
# WHY THE BUILD GATE MATTERS MOST, and cannot be a local habit:
|
||||
# `next build` writes into the app's `distDir` and rewrites `next-env.d.ts`.
|
||||
# Run alongside a running `pnpm dev` it corrupts the dev server's PostCSS /
|
||||
# Turbopack cache — measured: `globals.css` transforms to garbage and every
|
||||
# route 500s, and a dev-server restart does NOT clear it because the corruption
|
||||
# is on disk. So the one gate most likely to catch real breakage is the one a
|
||||
# developer is least able to run while working. CI is the only safe home for it.
|
||||
#
|
||||
# NO `continue-on-error` AND NO `|| true` ANYWHERE IN THIS FILE, BY DESIGN.
|
||||
# A gate that cannot fail is not a gate. Keep it that way.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "examples/showcases/reskinnable-demo/**"
|
||||
- ".github/workflows/test_reskinnable-demo.yml"
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "examples/showcases/reskinnable-demo/**"
|
||||
- ".github/workflows/test_reskinnable-demo.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||
|
||||
jobs:
|
||||
gates:
|
||||
name: "four gates (lint, typecheck, unit, build)"
|
||||
runs-on: ubuntu-latest
|
||||
# ~2 minutes of gates locally (lint 18s, typecheck 12s, 2460 unit tests 43s,
|
||||
# build 54s). The rest of the budget is install headroom on a cold store.
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
# Every step runs inside the app. It is NOT a workspace member, so
|
||||
# there is no root-level task that reaches it.
|
||||
working-directory: examples/showcases/reskinnable-demo
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup pnpm
|
||||
# Omit `version:` so pnpm/action-setup inherits from `packageManager`
|
||||
# via corepack. NOTE: the root pins pnpm@10.33.4 and THIS APP pins
|
||||
# pnpm@10.10.0 — two different resolvers in one repo, and the older one
|
||||
# is load-bearing here: 10.10.0 still reads `pnpm.overrides` from
|
||||
# package.json, which is the only place `@ag-ui/core`, `@ag-ui/encoder`
|
||||
# and `@ag-ui/proto` are pinned to the canary (`@ag-ui/client` is a
|
||||
# direct dependency, those three are not). Because every step below
|
||||
# runs inside the app directory, corepack resolves the app's pin.
|
||||
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
|
||||
with:
|
||||
package_json_file: examples/showcases/reskinnable-demo/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
cache: "pnpm"
|
||||
# THIS APP'S lockfile, not the root one. They are different files
|
||||
# since the app left the workspace, and keying on the root lockfile
|
||||
# would restore a cache that has none of these canary versions in it.
|
||||
cache-dependency-path: "examples/showcases/reskinnable-demo/pnpm-lock.yaml"
|
||||
|
||||
- name: Assert the app's own pnpm is in use
|
||||
# Turns the one genuine uncertainty in this job into a gate instead of a
|
||||
# silent wrong answer. The two `packageManager` pins mean the resolver
|
||||
# that runs here decides whether the three override-only `@ag-ui/*`
|
||||
# canary pins are honoured, and an unexpected action input is a WARNING
|
||||
# in Actions, not an error — so a typo above would quietly hand this job
|
||||
# the root's 10.33.4 and the check list would stay green.
|
||||
run: |
|
||||
set -euo pipefail
|
||||
expected="$(node -p "require('./package.json').packageManager.split('@')[1].split('+')[0]")"
|
||||
actual="$(pnpm --version)"
|
||||
echo "expected pnpm ${expected}, got ${actual}"
|
||||
[ "${expected}" = "${actual}" ] || {
|
||||
echo "::error::pnpm ${actual} is not this app's pinned ${expected}; the @ag-ui/* override pins may not be honoured"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Install dependencies
|
||||
# `--frozen-lockfile` is the point of the job as much as the gates are:
|
||||
# it fails if the committed lockfile no longer satisfies package.json,
|
||||
# which is how the canary pins stop silently drifting.
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Typecheck
|
||||
# The ONLY full type-check in this tree. `pnpm build` type-checks just
|
||||
# the app's module graph, so it never visits the test files — and
|
||||
# Vitest transpiles without checking types at all.
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Unit tests
|
||||
run: pnpm test:unit
|
||||
|
||||
- name: Build
|
||||
# Default `distDir` on purpose. `next.config.mjs` honours
|
||||
# `NEXT_DIST_DIR`, but that is a LOCAL workaround for building beside a
|
||||
# running dev server; in CI it only adds a tsconfig include entry.
|
||||
#
|
||||
# No env needed — measured. The route builds one agent per skin at
|
||||
# module load, and none of them requires OPENAI_API_KEY to construct
|
||||
# (banking's is an HttpAgent pointed at a URL that is never called
|
||||
# during a build).
|
||||
run: pnpm build
|
||||
|
||||
agent-resolve:
|
||||
name: "agent deps resolve + subagent surface"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
defaults:
|
||||
run:
|
||||
working-directory: examples/showcases/reskinnable-demo/agent
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup uv
|
||||
uses: astral-sh/setup-uv@ae62891fec2bb8e7d6c99fc78c9fec3a63790f8d # v10.0.0
|
||||
|
||||
- name: Sync from the committed lockfile
|
||||
# `--frozen` fails rather than re-resolving, so a `pyproject.toml` edit
|
||||
# without a matching `uv lock` is a red check instead of a silent drift.
|
||||
run: uv sync --frozen
|
||||
|
||||
- name: Assert the subagent surface is present
|
||||
# THE GATE THIS JOB EXISTS FOR, and it is not hypothetical: before the
|
||||
# pins landed, `ag-ui-langgraph>=0.0.43` resolved the RELEASE, which
|
||||
# accepts no `emit_subagent_events` and exports no subagent symbols.
|
||||
# That is the demo's headline feature, and it failed SILENTLY — the flag
|
||||
# is set as an attribute (copilotkit's subclass takes four kwargs), so
|
||||
# the assignment succeeds against an object nobody reads and the service
|
||||
# starts clean.
|
||||
#
|
||||
# Asserting the CAPABILITY rather than the version string on purpose: a
|
||||
# version assertion goes stale the moment the pin moves, and the thing
|
||||
# that must stay true is the surface, not the number.
|
||||
run: |
|
||||
uv run python -c '
|
||||
import inspect, sys
|
||||
from ag_ui_langgraph import LangGraphAgent
|
||||
import ag_ui.core as core
|
||||
|
||||
accepts = "emit_subagent_events" in inspect.getsource(LangGraphAgent.__init__)
|
||||
symbols = [n for n in dir(core) if "ubagent" in n]
|
||||
print("emit_subagent_events accepted:", accepts)
|
||||
print("subagent symbols:", symbols)
|
||||
if not accepts or not symbols:
|
||||
sys.exit("subagent surface missing — the banking harness console would be silently dead")
|
||||
'
|
||||
|
||||
- name: Import the agent module
|
||||
# Cheap smoke: catches a syntax error or a dropped dependency in
|
||||
# `agent/` that no other gate in this repo would ever see, since the
|
||||
# app is TypeScript and this service is Python.
|
||||
run: uv run python -c "import agent, main; print('agent + main import clean')"
|
||||
Reference in New Issue
Block a user