Skip to content

Commit c8094de

Browse files
Merge branch 'main' into mark/oss-162-a2ui-recovery-client-ux
2 parents dce8fe8 + d59a035 commit c8094de

188 files changed

Lines changed: 15550 additions & 3645 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/showcase_build.yml

Lines changed: 208 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ on:
1515
branches: [main]
1616
paths:
1717
- "showcase/**"
18+
- "examples/integrations/**"
1819
- ".github/workflows/showcase_build.yml"
1920
- ".github/workflows/showcase_build_check.yml"
2021
workflow_dispatch:
@@ -52,6 +53,22 @@ on:
5253
- showcase-harness
5354
- showcase-aimock
5455
- webhooks
56+
# Per-starter image builds (model B, §b stage 1). These map to the
57+
# build-starters job below, NOT the showcase `build` job. "all"
58+
# builds the full showcase fleet AND all 12 starters; a specific
59+
# starter slug narrows to that one starter via STARTER_DISPATCH.
60+
- starter-langgraph-python
61+
- starter-mastra
62+
- starter-langgraph-js
63+
- starter-crewai-crews
64+
- starter-pydantic-ai
65+
- starter-adk
66+
- starter-agno
67+
- starter-llamaindex
68+
- starter-langgraph-fastapi
69+
- starter-strands-python
70+
- starter-ms-agent-framework-python
71+
- starter-ms-agent-framework-dotnet
5572

5673
# No top-level concurrency group. Every build run completes. This is the
5774
# whole point of the decoupling: rapid pushes to main no longer cancel
@@ -245,10 +262,23 @@ jobs:
245262
# done — a common "did my dispatch deploy?" footgun. The `all` and
246263
# empty (push-event) modes legitimately produce [] when there are
247264
# no changes and must still succeed.
248-
if [ "$MATRIX" = "[]" ] && [ -n "$DISPATCH" ] && [ "$DISPATCH" != "all" ]; then
249-
echo "::error::workflow_dispatch service='$DISPATCH' did not match any entry in ALL_SERVICES — check the dispatch_name spelling"
250-
exit 1
251-
fi
265+
#
266+
# The `starter-*` namespace is OWNED by the detect-starter-changes
267+
# job (which scopes its OWN fail-loud to `starter-*`). A
268+
# `service=starter-<slug>` dispatch legitimately yields [] here
269+
# (no showcase service is named `starter-*`), so it must SKIP — an
270+
# empty showcase matrix, NOT a fail-loud exit 1. Mirrors how the
271+
# starter job scopes its fail-loud to the `starter-*` namespace.
272+
case "$DISPATCH" in
273+
starter-*) ;; # starter namespace → empty showcase matrix + skip
274+
"" | "all") ;; # push / full-fleet → [] is legitimate
275+
*)
276+
if [ "$MATRIX" = "[]" ]; then
277+
echo "::error::workflow_dispatch service='$DISPATCH' did not match any entry in ALL_SERVICES — check the dispatch_name spelling"
278+
exit 1
279+
fi
280+
;;
281+
esac
252282
253283
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
254284
if [ "$MATRIX" = "[]" ]; then
@@ -436,6 +466,180 @@ jobs:
436466
if-no-files-found: error
437467
retention-days: 7
438468

469+
# ────────────────────────────────────────────────────────────────────
470+
# Per-starter image publish (model B, §b stage 1 / Phase 1).
471+
#
472+
# Fully decoupled from the showcase `build`→`aggregate`→`redeploy` chain
473+
# above: starters are self-contained npm projects (no monorepo-source
474+
# build, no shared-module copy) and build from their own root Dockerfile
475+
# at examples/integrations/<slug>/Dockerfile (the single-image deployable:
476+
# Next.js frontend + agent, EXPOSE 3000, CMD entrypoint.sh — distinct from
477+
# the docker/Dockerfile.app + docker/Dockerfile.agent split stack used by
478+
# docker-compose.test.yml). Published to ghcr.io/copilotkit/starter-<slug>
479+
# (the `starter-` prefix is disjoint from `showcase-*`; S2's harness
480+
# discovery filters on namePrefix "starter-"). Railway deploy is the
481+
# gated S5 — NOT done here.
482+
#
483+
# The 12 starter slugs are the matrix source of truth (the smoke matrix in
484+
# test_smoke-starter.yml and STARTER_TO_COLUMN in
485+
# showcase/harness/src/probes/helpers/starter-mapping.ts). The dashboard
486+
# column remap lives in the harness (§a), so this layer uses raw starter
487+
# slugs.
488+
detect-starter-changes:
489+
runs-on: ubuntu-latest
490+
timeout-minutes: 5
491+
permissions:
492+
contents: read
493+
outputs:
494+
matrix: ${{ steps.starter-matrix.outputs.matrix }}
495+
has_changes: ${{ steps.starter-matrix.outputs.has_changes }}
496+
steps:
497+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
498+
with:
499+
persist-credentials: false
500+
501+
- name: Detect changed starter paths
502+
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
503+
id: filter
504+
with:
505+
filters: |
506+
workflow_config:
507+
- '.github/workflows/showcase_build.yml'
508+
langgraph_python:
509+
- 'examples/integrations/langgraph-python/**'
510+
mastra:
511+
- 'examples/integrations/mastra/**'
512+
langgraph_js:
513+
- 'examples/integrations/langgraph-js/**'
514+
crewai_crews:
515+
- 'examples/integrations/crewai-crews/**'
516+
pydantic_ai:
517+
- 'examples/integrations/pydantic-ai/**'
518+
adk:
519+
- 'examples/integrations/adk/**'
520+
agno:
521+
- 'examples/integrations/agno/**'
522+
llamaindex:
523+
- 'examples/integrations/llamaindex/**'
524+
langgraph_fastapi:
525+
- 'examples/integrations/langgraph-fastapi/**'
526+
strands_python:
527+
- 'examples/integrations/strands-python/**'
528+
ms_agent_framework_python:
529+
- 'examples/integrations/ms-agent-framework-python/**'
530+
ms_agent_framework_dotnet:
531+
- 'examples/integrations/ms-agent-framework-dotnet/**'
532+
533+
- name: Build starter matrix
534+
id: starter-matrix
535+
env:
536+
DISPATCH_SERVICE: ${{ github.event.inputs.service }}
537+
FILTER_CHANGES: ${{ steps.filter.outputs.changes }}
538+
run: |
539+
set -euo pipefail
540+
# One slot per starter. `slug` is the examples/integrations/<slug>
541+
# directory name; `image` is the published GHCR repo (starter-<slug>);
542+
# `filter_key` matches the paths-filter key above.
543+
ALL_STARTERS='[
544+
{"slug":"langgraph-python","image":"starter-langgraph-python","filter_key":"langgraph_python"},
545+
{"slug":"mastra","image":"starter-mastra","filter_key":"mastra"},
546+
{"slug":"langgraph-js","image":"starter-langgraph-js","filter_key":"langgraph_js"},
547+
{"slug":"crewai-crews","image":"starter-crewai-crews","filter_key":"crewai_crews"},
548+
{"slug":"pydantic-ai","image":"starter-pydantic-ai","filter_key":"pydantic_ai"},
549+
{"slug":"adk","image":"starter-adk","filter_key":"adk"},
550+
{"slug":"agno","image":"starter-agno","filter_key":"agno"},
551+
{"slug":"llamaindex","image":"starter-llamaindex","filter_key":"llamaindex"},
552+
{"slug":"langgraph-fastapi","image":"starter-langgraph-fastapi","filter_key":"langgraph_fastapi"},
553+
{"slug":"strands-python","image":"starter-strands-python","filter_key":"strands_python"},
554+
{"slug":"ms-agent-framework-python","image":"starter-ms-agent-framework-python","filter_key":"ms_agent_framework_python"},
555+
{"slug":"ms-agent-framework-dotnet","image":"starter-ms-agent-framework-dotnet","filter_key":"ms_agent_framework_dotnet"}
556+
]'
557+
558+
# Dispatch modes (mirror the showcase detect-changes job):
559+
# "all" → every starter (full-fleet rebuild).
560+
# "starter-<slug>" → that one starter (strip "starter-" prefix to match .image).
561+
# "<showcase service slug>" → no starters (this is a showcase-only dispatch).
562+
# "" (push event) → starters whose filter_key appears in CHANGES
563+
# (or workflow_config touched → rebuild all).
564+
DISPATCH="${DISPATCH_SERVICE:-}"
565+
CHANGES="${FILTER_CHANGES:-[]}"
566+
567+
# `.image` is exactly "starter-<slug>", which is also the
568+
# workflow_dispatch choice value, so a specific-starter dispatch
569+
# matches `$dispatch == .image` directly.
570+
MATRIX=$(echo "$ALL_STARTERS" | jq -c --arg dispatch "$DISPATCH" --argjson changes "$CHANGES" '
571+
[.[] |
572+
(.filter_key as $fk | select(
573+
$dispatch == "all" or
574+
($dispatch != "" and $dispatch != "all" and $dispatch == .image) or
575+
($dispatch == "" and (($changes | index("workflow_config") != null) or ($changes | index($fk) != null)))
576+
))]
577+
')
578+
579+
# Fail loudly on a typo'd starter dispatch (mirrors showcase job).
580+
# Only applies to the starter-* dispatch namespace; a showcase
581+
# service slug or "all" legitimately yields [] here.
582+
case "$DISPATCH" in
583+
starter-*)
584+
if [ "$MATRIX" = "[]" ]; then
585+
echo "::error::workflow_dispatch service='$DISPATCH' did not match any starter — check the slug"
586+
exit 1
587+
fi
588+
;;
589+
esac
590+
591+
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
592+
if [ "$MATRIX" = "[]" ]; then
593+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
594+
else
595+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
596+
fi
597+
598+
build-starters:
599+
needs: [detect-starter-changes]
600+
if: needs.detect-starter-changes.outputs.has_changes == 'true'
601+
runs-on: depot-ubuntu-24.04-4
602+
timeout-minutes: 20
603+
permissions:
604+
id-token: write
605+
contents: read
606+
packages: write
607+
strategy:
608+
fail-fast: false
609+
matrix:
610+
starter: ${{ fromJSON(needs.detect-starter-changes.outputs.matrix) }}
611+
612+
steps:
613+
- name: Checkout
614+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
615+
with:
616+
persist-credentials: false
617+
618+
- name: Setup Depot
619+
uses: depot/setup-action@15c09a5f77a0840ad4bce955686522a257853461 # v1
620+
621+
- name: Login to GHCR
622+
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
623+
with:
624+
registry: ghcr.io
625+
username: ${{ github.actor }}
626+
password: ${{ secrets.GITHUB_TOKEN }}
627+
628+
- name: Build and push starter image
629+
uses: depot/build-push-action@98e78adca7817480b8185f474a400b451d74e287 # v1.18.0
630+
with:
631+
project: m2kw2wmmcp
632+
context: examples/integrations/${{ matrix.starter.slug }}
633+
file: examples/integrations/${{ matrix.starter.slug }}/Dockerfile
634+
# Pin amd64: Railway and GHCR serve x86 hosts. Depot defaults to
635+
# the runner's native arch, so an arm64-only image would crash on
636+
# pull with "does not have a linux/amd64 variant available".
637+
platforms: linux/amd64
638+
push: true
639+
tags: |
640+
ghcr.io/copilotkit/${{ matrix.starter.image }}:latest
641+
ghcr.io/copilotkit/${{ matrix.starter.image }}:${{ github.sha }}
642+
439643
aggregate-build-results:
440644
name: Aggregate build results
441645
needs: [detect-changes, build]

.github/workflows/showcase_promote.yml

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ name: "Showcase: Promote (staging → prod)"
1717
# target service(s). Feature-level probes,
1818
# not naked 200.
1919
# 4. notify → Slack #oss-alerts on any red. Never #engr.
20+
# success → #team-showcase.
2021

2122
on:
2223
workflow_dispatch:
@@ -172,6 +173,7 @@ jobs:
172173
- working-directory: showcase/scripts
173174
run: npm ci
174175
- name: Live-probe staging for promote precondition
176+
working-directory: showcase/scripts
175177
env:
176178
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
177179
SERVICES_CSV: ${{ needs.resolve-targets.outputs.services_csv }}
@@ -182,7 +184,9 @@ jobs:
182184
fi
183185
# Spec §7.2 P3: the live staging probe at promote time is
184186
# authoritative. CI verify history is a leading indicator only.
185-
npx tsx showcase/scripts/verify-deploy.ts --env staging --services "$SERVICES_CSV"
187+
# Run from showcase/scripts (where `npm ci` installed tsx) so npx
188+
# uses the local install instead of network-fetching it.
189+
npx tsx verify-deploy.ts --env staging --services "$SERVICES_CSV"
186190
187191
promote:
188192
needs: [resolve-targets, verify-staging-precondition]
@@ -278,6 +282,7 @@ jobs:
278282
- working-directory: showcase/scripts
279283
run: npm ci
280284
- name: Run verify-deploy --env prod
285+
working-directory: showcase/scripts
281286
env:
282287
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
283288
# Scope verification to the services that ACTUALLY promoted (from the
@@ -286,7 +291,12 @@ jobs:
286291
# guarantee a red verify and mask the health of the ones that did
287292
# promote.
288293
SERVICES_CSV: ${{ needs.promote.outputs.succeeded_csv }}
294+
# The promote job's result, so the empty-CSV branch can tell a genuine
295+
# all-failed run (empty succeeded set is expected) apart from a
296+
# contract violation (promote reported success yet emitted no CSV).
297+
PROMOTE_RESULT: ${{ needs.promote.result }}
289298
run: |
299+
set -euo pipefail
290300
if [ -z "$RAILWAY_TOKEN" ]; then
291301
echo "::error::RAILWAY_TOKEN is not set"
292302
exit 1
@@ -297,11 +307,22 @@ jobs:
297307
# --services (which would either error or vacuously pass). The promote
298308
# job already exited non-zero in that case, so the overall run is red
299309
# via the notify state machine regardless.
310+
#
311+
# BUT: an empty succeeded set is only legitimate when promote did NOT
312+
# succeed. If promote reported success and STILL emitted no CSV, the
313+
# "promote already failed" assumption that justifies the vacuous skip
314+
# is violated — fail loud instead of silently exiting 0.
300315
if [ -z "$SERVICES_CSV" ]; then
316+
if [ "$PROMOTE_RESULT" = "success" ]; then
317+
echo "::error::promote reported success but succeeded_csv is empty — contract violation"
318+
exit 1
319+
fi
301320
echo "::notice::succeeded_csv is empty (no services promoted, or promote did not run); skipping prod verification. The run is red via the promote job result if anything failed."
302321
exit 0
303322
fi
304-
npx tsx showcase/scripts/verify-deploy.ts --env prod --services "$SERVICES_CSV"
323+
# Run from showcase/scripts (where `npm ci` installed tsx) so npx uses
324+
# the local install instead of network-fetching it.
325+
npx tsx verify-deploy.ts --env prod --services "$SERVICES_CSV"
305326
306327
notify:
307328
# Slack #oss-alerts only. Never #engr (engr is sacred — release alerts only).
@@ -314,6 +335,7 @@ jobs:
314335
actions: read
315336
env:
316337
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
338+
SLACK_WEBHOOK_TS: ${{ secrets.SLACK_WEBHOOK_TEAM_SHOWCASE }}
317339
steps:
318340
# Best-effort promote invariant (do NOT reintroduce a PRE gate below):
319341
# resolve-targets = HARD precondition (must succeed).
@@ -372,10 +394,15 @@ jobs:
372394
with:
373395
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
374396
webhook-type: incoming-webhook
397+
# Newlines are injected via fromJSON('"\n"') (a real LF char), NOT a
398+
# literal '\n' in the template: GitHub Actions expression string
399+
# literals do not interpret backslash escapes, so a literal '\n' would
400+
# survive toJSON as the two chars \\n and Slack would render it
401+
# verbatim as "\n" instead of a line break.
375402
payload: |
376403
{
377404
"text": ${{ toJSON(format(
378-
'{0} *Showcase Promote Failed*\nServices: `{1}`\nresolve-targets={2} pre-staging={3} promote={4} verify-prod={5}\n<{6}/{7}/actions/runs/{8}|View run>',
405+
'{0} *Showcase Promote Failed*{9}Services: `{1}`{9}resolve-targets={2} pre-staging={3} promote={4} verify-prod={5}{9}<{6}/{7}/actions/runs/{8}|View run>',
379406
steps.state.outputs.icon,
380407
steps.state.outputs.csv,
381408
needs.resolve-targets.result,
@@ -384,7 +411,28 @@ jobs:
384411
needs.verify-prod.result,
385412
github.server_url,
386413
github.repository,
387-
github.run_id
414+
github.run_id,
415+
fromJSON('"\n"')
416+
)) }}
417+
}
418+
- name: Post to #team-showcase
419+
if: steps.state.outputs.state == 'success' && inputs.service != '__select_a_service__' && env.SLACK_WEBHOOK_TS != ''
420+
uses: slackapi/slack-github-action@b0fa283ad8fea605de13dc3f449259339835fc52 # v2.1.0
421+
with:
422+
webhook: ${{ secrets.SLACK_WEBHOOK_TEAM_SHOWCASE }}
423+
webhook-type: incoming-webhook
424+
# Newlines via fromJSON('"\n"') (real LF), not a literal '\n' — see the
425+
# #oss-alerts step above for why a literal '\n' renders verbatim.
426+
payload: |
427+
{
428+
"text": ${{ toJSON(format(
429+
'{0} *Showcase Promoted to Prod*{5}Services: `{1}`{5}<{2}/{3}/actions/runs/{4}|View run>',
430+
steps.state.outputs.icon,
431+
steps.state.outputs.csv,
432+
github.server_url,
433+
github.repository,
434+
github.run_id,
435+
fromJSON('"\n"')
388436
)) }}
389437
}
390438
- name: Log (no Slack — webhook unset)
@@ -393,3 +441,10 @@ jobs:
393441
CSV: ${{ steps.state.outputs.csv }}
394442
run: |
395443
echo "::warning::Showcase promote failed for '$CSV' but SLACK_WEBHOOK_OSS_ALERTS is not set; no Slack notification sent."
444+
- name: Log (no Slack — team-showcase webhook unset)
445+
if: steps.state.outputs.state == 'success' && inputs.service != '__select_a_service__' && env.SLACK_WEBHOOK_TS == ''
446+
env:
447+
CSV: ${{ steps.state.outputs.csv }}
448+
ICON: ${{ steps.state.outputs.icon }}
449+
run: |
450+
echo "::notice::$ICON Showcase promoted to prod for '$CSV' but SLACK_WEBHOOK_TEAM_SHOWCASE is not set; no #team-showcase notification sent."

0 commit comments

Comments
 (0)