Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 34 additions & 27 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,26 @@ concurrency:
jobs:
perf:
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
project:
- demo-e2e-agents
- demo-e2e-cache
- demo-e2e-codecall
- demo-e2e-config
- demo-e2e-direct
- demo-e2e-elicitation
- demo-e2e-errors
- demo-e2e-hooks
- demo-e2e-multiapp
- demo-e2e-notifications
- demo-e2e-openapi
- demo-e2e-providers
- demo-e2e-public
- demo-e2e-redis
- demo-e2e-remember
- demo-e2e-remote
- demo-e2e-serverless
- demo-e2e-skills
- demo-e2e-standalone
- demo-e2e-transport-recreation
- demo-e2e-ui
chunk:
- index: 0
projects: "demo-e2e-agents,demo-e2e-cache,demo-e2e-codecall"
- index: 1
projects: "demo-e2e-config,demo-e2e-direct,demo-e2e-elicitation"
- index: 2
projects: "demo-e2e-errors,demo-e2e-hooks,demo-e2e-multiapp"
- index: 3
projects: "demo-e2e-notifications,demo-e2e-openapi,demo-e2e-providers"
- index: 4
projects: "demo-e2e-public,demo-e2e-redis,demo-e2e-remember"
- index: 5
projects: "demo-e2e-remote,demo-e2e-serverless,demo-e2e-skills"
- index: 6
projects: "demo-e2e-standalone,demo-e2e-transport-recreation,demo-e2e-ui"

services:
redis:
Expand Down Expand Up @@ -85,8 +78,22 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true

- name: Run performance tests (${{ matrix.project }})
run: yarn nx run ${{ matrix.project }}:test:perf
- name: Run performance tests (chunk ${{ matrix.chunk.index }})
run: |
FAILED=0
IFS=',' read -ra PROJECTS <<< "${{ matrix.chunk.projects }}"
for project in "${PROJECTS[@]}"; do
echo "::group::Running perf tests for $project"
if ! yarn nx run "$project":test:perf; then
echo "::error::Performance tests failed for $project"
FAILED=$((FAILED + 1))
fi
echo "::endgroup::"
done
if [ "$FAILED" -gt 0 ]; then
echo "::error::$FAILED project(s) had performance test failures"
exit 1
fi
env:
NODE_OPTIONS: "--expose-gc --max-old-space-size=4096"
REDIS_HOST: localhost
Expand All @@ -95,7 +102,7 @@ jobs:
- name: Upload performance report
uses: actions/upload-artifact@v6
with:
name: perf-report-${{ matrix.project }}
name: perf-report-chunk-${{ matrix.chunk.index }}
path: perf-results/
retention-days: 30
if: always()
Expand All @@ -115,7 +122,7 @@ jobs:
uses: actions/download-artifact@v5
with:
path: perf-results
pattern: perf-report-*
pattern: perf-report-chunk-*
merge-multiple: true

- name: Write full report to workflow summary
Expand Down
54 changes: 32 additions & 22 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,15 +124,15 @@ jobs:
dist/
retention-days: 1

# Discover E2E projects dynamically using Nx tags
# Discover E2E projects dynamically using Nx tags, chunked for API rate limits
discover-e2e:
name: "Discover E2E Projects"
needs: setup
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
matrix: ${{ steps.discover.outputs.projects }}
matrix: ${{ steps.discover.outputs.chunks }}
steps:
- name: Checkout code
uses: actions/checkout@v6
Expand All @@ -146,16 +146,23 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Discover E2E projects
- name: Discover E2E projects and create chunks
id: discover
run: |
PROJECTS_JSON=$(npx nx show projects -p tag:type:e2e --json 2>/dev/null | jq -c '.' || echo "[]")
if [ "$PROJECTS_JSON" = "[]" ] || [ "$PROJECTS_JSON" = "null" ]; then
echo "::error::No E2E projects found with tag:type:e2e"
exit 1
fi
echo "Found $(echo $PROJECTS_JSON | jq 'length') E2E projects"
echo "projects=$PROJECTS_JSON" >> $GITHUB_OUTPUT
TOTAL=$(echo $PROJECTS_JSON | jq 'length')
echo "Found $TOTAL E2E projects"

# Chunk projects into groups of 4 to reduce API rate limit pressure
CHUNK_SIZE=4
CHUNKS=$(echo $PROJECTS_JSON | jq -c --argjson n "$CHUNK_SIZE" '[range(0; length; $n) as $i | .[$i:$i+$n]] | to_entries | map({index: .key, projects: .value})')
CHUNK_COUNT=$(echo $CHUNKS | jq 'length')
echo "Created $CHUNK_COUNT chunks of up to $CHUNK_SIZE projects each"
echo "chunks=$CHUNKS" >> $GITHUB_OUTPUT

# Unit tests (depends on build)
unit-tests:
Expand All @@ -182,7 +189,7 @@ jobs:
run: yarn install --frozen-lockfile

- name: Download build artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: dist

Expand All @@ -209,16 +216,16 @@ jobs:
retention-days: 1
if-no-files-found: warn

# E2E tests - matrix strategy for parallelization
# E2E tests - chunked matrix strategy to reduce API rate limit pressure
e2e-tests:
name: "E2E Tests (${{ matrix.project }})"
name: "E2E Tests (chunk ${{ matrix.chunk.index }})"
needs: [setup, build, discover-e2e]
runs-on: ubuntu-latest
timeout-minutes: 15
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
project: ${{ fromJson(needs.discover-e2e.outputs.matrix) }}
chunk: ${{ fromJson(needs.discover-e2e.outputs.matrix) }}
env:
NX_DAEMON: "false"
RUN_COVERAGE: ${{ github.ref == 'refs/heads/main' }}
Expand All @@ -238,7 +245,7 @@ jobs:
run: yarn install --frozen-lockfile

- name: Download build artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: dist

Expand All @@ -248,35 +255,38 @@ jobs:
- name: Set Nx SHAs
uses: nrwl/nx-set-shas@v5

- name: Run E2E tests (${{ matrix.project }})
- name: Run E2E tests (chunk ${{ matrix.chunk.index }})
id: test
continue-on-error: true
run: |
PROJECTS='${{ join(matrix.chunk.projects, ',') }}'
echo "Running E2E tests for: $PROJECTS"
if [ "$RUN_COVERAGE" = "true" ]; then
npx nx run ${{ matrix.project }}:test --coverage
npx nx run-many -t test --projects="$PROJECTS" --coverage --parallel=1
else
npx nx run ${{ matrix.project }}:test
npx nx run-many -t test --projects="$PROJECTS" --parallel=1
fi

- name: Reset Nx cache on failure
if: steps.test.outcome == 'failure'
run: npx nx reset

- name: Retry E2E tests (${{ matrix.project }})
- name: Retry E2E tests (chunk ${{ matrix.chunk.index }})
if: steps.test.outcome == 'failure'
run: |
PROJECTS='${{ join(matrix.chunk.projects, ',') }}'
if [ "$RUN_COVERAGE" = "true" ]; then
npx nx run ${{ matrix.project }}:test --coverage
npx nx run-many -t test --projects="$PROJECTS" --coverage --parallel=1
else
npx nx run ${{ matrix.project }}:test
npx nx run-many -t test --projects="$PROJECTS" --parallel=1
fi

- name: Upload E2E coverage artifacts
if: env.RUN_COVERAGE == 'true' && always()
uses: actions/upload-artifact@v6
with:
name: e2e-coverage-${{ matrix.project }}
path: coverage/e2e/${{ matrix.project }}/
name: e2e-coverage-chunk-${{ matrix.chunk.index }}
path: coverage/e2e/
retention-days: 1
if-no-files-found: warn

Expand All @@ -302,15 +312,15 @@ jobs:
run: yarn install --frozen-lockfile

- name: Download unit coverage artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: unit-coverage
path: coverage/unit/

- name: Download E2E coverage artifacts
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
pattern: e2e-coverage-*
pattern: e2e-coverage-chunk-*
path: coverage/e2e/
merge-multiple: true

Expand Down
45 changes: 3 additions & 42 deletions apps/demo/src/apps/weather/tools/get-weather.tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
* and other UI-capable hosts.
*/

import React from 'react';
import { Tool, ToolContext } from '@frontmcp/sdk';
import { Card, Badge } from '@frontmcp/ui/components';
import { z } from 'zod';

// Define input/output schemas
Expand All @@ -29,45 +27,8 @@ const outputSchema = z.object({
});

// Infer types from schemas for proper typing
type WeatherInput = z.infer<z.ZodObject<typeof inputSchema>>;
type WeatherOutput = z.infer<typeof outputSchema>;

// Weather condition icon mapping (using emoji for simplicity)
const iconMap: Record<string, string> = {
sunny: '☀️',
cloudy: '☁️',
rainy: '🌧️',
snowy: '❄️',
stormy: '⛈️',
windy: '💨',
foggy: '🌫️',
};

function WeatherWidget({ output }: { output: WeatherOutput }) {
const tempSymbol = output.units === 'celsius' ? '°C' : '°F';
const weatherIcon = iconMap[output.icon] || '🌤️';
const badgeVariant = output.conditions === 'sunny' ? 'success' : output.conditions === 'rainy' ? 'info' : 'default';

return (
<Card title={output.location} subtitle="Current Weather" elevation={2}>
<div style={{ textAlign: 'center', padding: '24px 0' }}>
<div style={{ fontSize: '3.75rem', marginBottom: '8px' }}>{weatherIcon}</div>
<div style={{ fontSize: '3rem', fontWeight: 300, marginBottom: '8px' }}>
{output.temperature}
{tempSymbol}
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<Badge label={output.conditions} variant={badgeVariant} />
</div>
</div>
<div style={{ marginTop: '16px' }}>
<div>Humidity: {output.humidity}%</div>
<div>Wind Speed: {output.windSpeed} km/h</div>
<div>Units: {output.units === 'celsius' ? 'Celsius' : 'Fahrenheit'}</div>
</div>
</Card>
);
}
export type WeatherInput = z.infer<z.ZodObject<typeof inputSchema>>;
export type WeatherOutput = z.infer<typeof outputSchema>;

@Tool({
name: 'get_weather',
Expand All @@ -84,7 +45,7 @@ function WeatherWidget({ output }: { output: WeatherOutput }) {
displayMode: 'inline',
servingMode: 'static',
uiType: 'react',
template: WeatherWidget,
template: { file: 'apps/demo/src/apps/weather/tools/get-weather.ui.tsx' },
},
codecall: {
visibleInListTools: true,
Expand Down
35 changes: 35 additions & 0 deletions apps/demo/src/apps/weather/tools/get-weather.ui-2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export const Card = ({
title,
subtitle,
children,
}: {
title?: string;
subtitle?: string;
elevation?: number;
children?: React.ReactNode;
}) => {
return (
<div style={{ border: '1px solid #ccc', borderRadius: '8px', padding: '16px', maxWidth: '400px' }}>
<h2>{title}</h2>
<h4 style={{ color: '#666' }}>{subtitle}</h4>
{children}
</div>
);
};

export const Badge = ({ label, variant }: { label: string; variant: 'success' | 'info' | 'default' }) => {
return (
<span
style={{
backgroundColor: variant === 'success' ? '#4caf50' : variant === 'info' ? '#2196f3' : '#9e9e9e',
color: 'white',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '0.875rem',
fontWeight: 'bold',
}}
>
{label}
</span>
);
};
Loading
Loading