Files
pbakaus_impeccable/.github/workflows/ci.yml
T
87d8f6d686 Track the rule registry as a generated artifact (#728)
* Track the rule registry as a generated artifact

`cargo xtask bundle` already wrote the registry to `dist/antipatterns.json`
and into `extension/detector/`, but neither is tracked, so a consumer
reading this repo from a source checkout or a tarball had no way to get the
rule list without a Rust toolchain. The Rust swap made that concrete:
impeccable.style imported `cli/engine/registry/antipatterns.mjs` for its
rule count and its Slop catalog, and that file is gone.

Write the same JSON to `crates/live/assets/antipatterns.json`, next to the
in-page bundle and tracked like it, and extend `cargo xtask bundle --check`
to fail when either asset is stale. The build's rule-count check now reads
the tracked copy first and falls back to the extension copy, so a fresh
checkout validates counts instead of skipping the check.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Registry: wire the staleness gate into CI, harden the count read

Two review findings on the tracked-registry change.

`cargo xtask bundle --check` was never run by CI, so a rule whose name,
category, or description changed without changing the rule count could
ship a stale `crates/live/assets/antipatterns.json`. The extension job
already runs `bun run build:extension` (and so `cargo xtask bundle`) and
then asserts a clean tree; adding that file to the path list covers it
with the gate that is already there. The bundle beside it stays out: its
bytes carry a wasm module built by whatever wasm-pack and wasm-opt the
runner installed, so diffing it would fail on toolchain drift rather than
on a real change.

`readDetectionRuleCount` counted `new Set(rules.map(r => r.id))`, so a
shape change would collapse to a set of one `undefined` and read as a
one-rule registry, flagging every count claim as stale. Count only
non-empty string ids, and say "no readable antipatterns.json" when the
file is present but unparseable.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

* Registry gate: make the trigger honest, name the real count condition

The tracked-registry diff check ran on every PR, but the step that
regenerates the registry (`bun run build:extension`, which is `cargo xtask
bundle`) only runs when the detector trigger fires, and that trigger did
not list `crates/bundle`. A PR that changed how the registry is
serialized therefore never rebuilt it, and the check compared the
committed file against an untouched tree and passed on stale bytes.

Two changes. The detector trigger now covers every input the bundle
reads: `crates/(bundle|core|foundation|wasm|xtask)/` plus
`crates/live/assets/` so a hand-edit of a tracked artifact is regenerated
over. And the registry check moved into its own step carrying the same
condition as the build it validates, so it no longer claims to check
something that was never regenerated; the provider-output check stays
unconditional, because `bun run build` runs on every PR.

Separately, `readDetectionRuleCount` returns the reason it found no
count. "no antipatterns.json" covered three different conditions, and a
registry that is present but unparseable sends anyone debugging a count
failure to the wrong place. It now reports the paths it looked at, or
names the file that is not readable as JSON, or names the file that
carries no rule ids.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
2026-09-04 12:43:53 -07:00

649 lines
22 KiB
YAML

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Nightly full live-e2e matrix. The smoke groups already gate every PR; the
# full sweep is too slow for that, so it runs once a day against main.
schedule:
- cron: '0 7 * * *'
concurrency:
# Scheduled runs get their own group: the 07:00 UTC nightly and a push to
# main share github.ref, and cancel-in-progress would let them kill each
# other mid-run.
group: ${{ github.workflow }}-${{ github.event_name == 'schedule' && 'nightly' || github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
changes:
runs-on: ubuntu-latest
outputs:
core: ${{ steps.plan.outputs.core }}
rust: ${{ steps.plan.outputs.rust }}
detector: ${{ steps.plan.outputs.detector }}
live: ${{ steps.plan.outputs.live }}
framework: ${{ steps.plan.outputs.framework }}
cli_remote_e2e: ${{ steps.plan.outputs.cli_remote_e2e }}
live_e2e: ${{ steps.plan.outputs.live_e2e }}
live_e2e_accept_cleanup: ${{ steps.plan.outputs.live_e2e_accept_cleanup }}
skill_behavior: ${{ steps.plan.outputs.skill_behavior }}
live_svelte_adapter_deepseek: ${{ steps.plan.outputs.live_svelte_adapter_deepseek }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Detect test plan
id: plan
env:
GITHUB_EVENT_BEFORE: ${{ github.event.before }}
run: node scripts/ci-test-plan.mjs
test-matrix:
name: test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
needs: changes
strategy:
fail-fast: false
matrix:
node-version: [22.18.0, 24]
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run core tests
run: bun run test:core
- name: Install Puppeteer browser
if: needs.changes.outputs.detector == 'true'
run: bunx puppeteer browsers install chrome
- name: Run detector tests
if: needs.changes.outputs.detector == 'true'
run: bun run test:detector
- name: Run live unit tests
if: needs.changes.outputs.live == 'true'
run: bun run test:live
- name: Run framework fixture tests
if: needs.changes.outputs.framework == 'true'
run: bun run test:framework
- name: Build
run: bun run build
# `bun run build:extension` runs `cargo xtask bundle`: the rule core
# compiled to wasm plus the page JS in browser-bundle/.
- name: Install the pinned toolchain
if: needs.changes.outputs.detector == 'true'
run: rustup show && rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
if: needs.changes.outputs.detector == 'true'
- name: Install wasm-pack
if: needs.changes.outputs.detector == 'true'
run: cargo install wasm-pack --locked
- name: Build extension
if: needs.changes.outputs.detector == 'true'
run: bun run build:extension
- name: Lint Firefox extension (web-ext)
if: needs.changes.outputs.detector == 'true'
# Pinned for reproducible CI. Fails on AMO errors; innerHTML style
# warnings in the panel renderer are non-blocking and not promoted to
# errors here.
run: npx --yes web-ext@10 lint --source-dir dist/extension-firefox
# Only meaningful right after the bundle was rebuilt, so it carries the
# same condition as that step rather than running unconditionally and
# comparing a committed artifact against an untouched tree. Every input
# `cargo xtask bundle` reads is in the detector trigger
# (scripts/test-suites.mjs), so a change to the registry rows or to how
# they are serialized lands here. The in-page bundle beside it is
# deliberately not checked: its bytes carry a wasm module built by
# whatever wasm-pack and wasm-opt the runner installed, so diffing it
# would fail on toolchain drift rather than on a real change.
- name: Verify the tracked rule registry is current
if: needs.changes.outputs.detector == 'true'
run: git diff --exit-code -- crates/live/assets/antipatterns.json
- name: Verify generated tracked outputs
# What `bun run build` writes, which runs on every PR.
# extension/detector/ is gitignored (built by `cargo xtask bundle`);
# it stays listed so a stray tracked copy shows up here.
run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin extension/detector
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
name: impeccable-build-node-${{ matrix.node-version }}
# Ship the packaged zips, not the unpacked Firefox staging tree.
path: |
dist/
!dist/extension-firefox/
retention-days: 7
# The Rust workspace: the engine binary, the rule core, and every crate
# behind them. Everything builds from source with no downloads.
rust:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.rust == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v7
# rust-toolchain.toml names the channel; `rustup show` installs it.
# Never override the toolchain here.
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --workspace --all-targets
- name: Test
run: cargo test --workspace
# The engine ships a windows-x64 binary (release-engine.yml), so the
# workspace has to build and pass its own tests there. Tests that need a
# browser or the oracle skip when those are absent.
rust-windows:
runs-on: windows-latest
needs: changes
if: needs.changes.outputs.rust == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- run: cargo build --workspace --all-targets
- run: cargo test --workspace --no-fail-fast
# Behavior gate: replays the tests/oracle/ goldens against a release build
# of the engine from THIS checkout (so a PR is judged on its own source,
# not on the last published binary). Without this job the oracle only ever
# runs on developer laptops: tests/oracle.test.mjs skips cleanly when no
# binary is present, so the default suite is silent about it on CI.
oracle:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.oracle == 'true' || needs.changes.outputs.rust == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: Build the engine from source
run: cargo build --release -p impeccable
- name: Replay oracle goldens
env:
IMPECCABLE_BIN: ${{ github.workspace }}/target/release/impeccable
run: node tests/oracle/run.mjs
# Release-order guard (triage decision D4). Verifies that the engine release for
# the pinned ENGINE_VERSION is fully published — the five dist binaries + .sha256
# AND the five @impeccable/cli-<os>-<arch> npm platform packages — before a skill
# release/merge that depends on them. The launcher, npm shim, and
# `impeccable install` all dead-end without those assets.
#
# A hard gate since engine-v0.1.0 shipped: the assets for the pinned
# ENGINE_VERSION must exist before a skill/CLI release or a merge that bumps the
# pin, or the launcher, the npm shim and `impeccable install` dead-end.
# release.mjs hard-fails `release:skill`/`release:cli` on the same check.
engine-release-ready:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Check engine release assets for pinned ENGINE_VERSION
id: check
run: node scripts/check-engine-release.mjs
- name: Annotate missing engine release
# Only when the check itself failed, so a checkout or setup failure keeps its own error.
if: failure() && steps.check.outcome == 'failure'
run: |
echo "::error title=Engine release not ready::The engine release for v$(cat ENGINE_VERSION) is not fully published (engine-v$(cat ENGINE_VERSION) release assets and/or the @impeccable/cli-<os>-<arch> npm platform packages). Publish the engine (bun run release:engine) and the platform packages (bun run release:platform-packages) before bumping ENGINE_VERSION on main or releasing the skill/CLI."
test:
runs-on: ubuntu-latest
needs: test-matrix
if: always()
steps:
- name: Verify Node test matrix
run: |
if [ "${{ needs.test-matrix.result }}" != "success" ]; then
echo "test-matrix result: ${{ needs.test-matrix.result }}"
exit 1
fi
echo "test matrix passed"
cli-remote-e2e:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.cli_remote_e2e == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
# The live verbs are the engine binary; build it from this checkout so the
# suite tests the branch, not the last published release.
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: Build the engine
run: cargo build --release -p impeccable
- name: Run remote CLI E2E smoke
run: bun run test:cli-remote-e2e
live-e2e-smoke:
name: live-e2e smoke (${{ matrix.group }})
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.live_e2e == 'true' && github.event_name != 'workflow_dispatch' && github.event_name != 'schedule'
timeout-minutes: 15
strategy:
fail-fast: true
matrix:
include:
- group: platform
fixtures: astro-vite7,monorepo-nested-vite,nextjs-app-router,vite8-sveltekit
- group: svelte
fixtures: vite8-sveltekit-stateful
- group: react
fixtures: vite8-react-css-modules,vite8-react-insert,vite8-react-plain
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache fixture npm downloads
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
restore-keys: |
${{ runner.os }}-fixture-npm-
- name: Cache Playwright Chromium
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-chromium-
- name: Install dependencies
run: bun install
- name: Install Playwright Chromium
run: npx playwright install chromium
# The live verbs are the engine binary; build it from this checkout so the
# suite tests the branch, not the last published release.
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: Build the engine
run: cargo build --release -p impeccable
- name: Run live E2E tests
run: bun run test:live-e2e
env:
IMPECCABLE_E2E_ONLY: ${{ matrix.fixtures }}
IMPECCABLE_E2E_SCENARIOS: core
IMPECCABLE_E2E_TEST_TIMEOUT_MS: 180000
IMPECCABLE_E2E_INSTALL_TIMEOUT_MS: 120000
IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS: 60000
IMPECCABLE_E2E_ARTIFACT_DIR: test-results/live-e2e/${{ matrix.group }}
- name: Upload live E2E failure artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: live-e2e-smoke-${{ matrix.group }}-artifacts
path: test-results/live-e2e
if-no-files-found: ignore
live-e2e-full:
name: live-e2e full (${{ matrix.group }})
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.live_e2e == 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule')
timeout-minutes: 25
strategy:
fail-fast: true
matrix:
include:
- group: platform
fixtures: astro-vite7,monorepo-nested-vite,nextjs-app-router
- group: svelte
fixtures: vite8-sveltekit,vite8-sveltekit-stateful
- group: react-a
fixtures: vite8-https,vite8-react-base-path,vite8-react-csp-meta,vite8-react-css-modules,vite8-react-emotion
- group: react-b
fixtures: vite8-react-insert,vite8-react-mapped-list,vite8-react-modal,vite8-react-plain
- group: stateful
fixtures: vite8-react-radix-dialog,vite8-react-router-spa,vite8-react-styled-components,vite8-react-tabs
- group: styling
fixtures: vite8-react-tailwindv3,vite8-react-tailwindv4,vite8-react-ts,vite8-react-tsx-repeated-aside,vite8-react-unocss,vite8-react-vanilla-extract
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache fixture npm downloads
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
restore-keys: |
${{ runner.os }}-fixture-npm-
- name: Cache Playwright Chromium
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-chromium-
- name: Install dependencies
run: bun install
- name: Install Playwright Chromium
run: npx playwright install chromium
# The live verbs are the engine binary; build it from this checkout so the
# suite tests the branch, not the last published release.
- name: Install the pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@v2
- name: Build the engine
run: cargo build --release -p impeccable
- name: Run live E2E tests
run: bun run test:live-e2e
env:
IMPECCABLE_E2E_ONLY: ${{ matrix.fixtures }}
IMPECCABLE_E2E_TEST_TIMEOUT_MS: 300000
IMPECCABLE_E2E_INSTALL_TIMEOUT_MS: 180000
IMPECCABLE_E2E_DEV_READY_TIMEOUT_MS: 120000
IMPECCABLE_E2E_ARTIFACT_DIR: test-results/live-e2e/${{ matrix.group }}
- name: Upload live E2E failure artifacts
if: failure()
uses: actions/upload-artifact@v7
with:
name: live-e2e-full-${{ matrix.group }}-artifacts
path: test-results/live-e2e
if-no-files-found: ignore
live-e2e-accept-cleanup:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.live_e2e_accept_cleanup == 'true'
timeout-minutes: 15
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
steps:
- name: Skip without provider key
if: ${{ env.ANTHROPIC_API_KEY == '' && env.DEEPSEEK_API_KEY == '' }}
run: echo "Skipping provider-backed accept-cleanup regression because no provider API key is configured."
- name: Checkout repository
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
uses: actions/checkout@v7
- name: Setup Node
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache fixture npm downloads
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
restore-keys: |
${{ runner.os }}-fixture-npm-
- name: Cache Playwright Chromium
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-chromium-
- name: Install dependencies
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
run: bun install
- name: Install Playwright Chromium
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
run: npx playwright install chromium
# The live verbs are the engine binary; build it from this checkout so the
# suite tests the branch, not the last published release.
- name: Install the pinned toolchain
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
run: rustup show
- uses: Swatinem/rust-cache@v2
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
- name: Build the engine
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
run: cargo build --release -p impeccable
- name: Run accept cleanup regression
if: ${{ env.ANTHROPIC_API_KEY != '' || env.DEEPSEEK_API_KEY != '' }}
run: |
if [ -n "$DEEPSEEK_API_KEY" ]; then
export IMPECCABLE_E2E_LLM_PROVIDER=deepseek
else
export IMPECCABLE_E2E_LLM_PROVIDER=anthropic
fi
bun run test:live-e2e-accept-cleanup
live-svelte-adapter-deepseek:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.live_svelte_adapter_deepseek == 'true'
timeout-minutes: 25
env:
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
steps:
- name: Skip without DeepSeek key
if: ${{ env.DEEPSEEK_API_KEY == '' }}
run: echo "Skipping Svelte adapter DeepSeek sweep because DEEPSEEK_API_KEY is not configured."
- name: Checkout repository
if: ${{ env.DEEPSEEK_API_KEY != '' }}
uses: actions/checkout@v7
- name: Setup Node
if: ${{ env.DEEPSEEK_API_KEY != '' }}
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
if: ${{ env.DEEPSEEK_API_KEY != '' }}
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache fixture npm downloads
if: ${{ env.DEEPSEEK_API_KEY != '' }}
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-fixture-npm-${{ hashFiles('tests/framework-fixtures/**/files/package.json') }}
restore-keys: |
${{ runner.os }}-fixture-npm-
- name: Cache Playwright Chromium
if: ${{ env.DEEPSEEK_API_KEY != '' }}
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-chromium-${{ hashFiles('package.json', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-playwright-chromium-
- name: Install dependencies
if: ${{ env.DEEPSEEK_API_KEY != '' }}
run: bun install
- name: Install Playwright Chromium
if: ${{ env.DEEPSEEK_API_KEY != '' }}
run: npx playwright install chromium
# The live verbs are the engine binary; build it from this checkout so the
# suite tests the branch, not the last published release.
- name: Install the pinned toolchain
if: ${{ env.DEEPSEEK_API_KEY != '' }}
run: rustup show
- uses: Swatinem/rust-cache@v2
if: ${{ env.DEEPSEEK_API_KEY != '' }}
- name: Build the engine
if: ${{ env.DEEPSEEK_API_KEY != '' }}
run: cargo build --release -p impeccable
- name: Run Svelte adapter DeepSeek sweep
if: ${{ env.DEEPSEEK_API_KEY != '' }}
run: bun run test:live-svelte-adapter-deepseek
skill-behavior:
runs-on: ubuntu-latest
needs: changes
if: needs.changes.outputs.skill_behavior == 'true' && github.event_name != 'pull_request'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_CLOUD_API_KEY: ${{ secrets.GOOGLE_CLOUD_API_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Setup Node
uses: actions/setup-node@v7
with:
node-version: 24
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
- name: Run skill behavior tests
run: bun run test:skill-behavior