Files
copilotkit__copilotkit/.github/workflows/showcase_capture-previews.yml
Benjamin Taylor 5c0150392f ci: stop Playwright browser installs shelling out to apt
Every Playwright install in CI passed `--with-deps`, which runs `apt-get
update` before downloading the browser. apt on the runners cannot always
reach azure.archive.ubuntu.com; when it can't it retries for many minutes,
which is long enough to burn a job's whole `timeout-minutes` budget before
a single test runs. GitHub renders that kill as "The operation was
canceled", so it reads as a test failure rather than an infrastructure hang.

Chromium's system libraries are already present on the Ubuntu runner
images, and every one of these steps installs chromium only, so the browser
download is all they need. Six jobs lose their apt dependency:
test_unit, test_e2e-legacy-v1, test_e2e-showcase-on-demand,
test_showcase-frontend-matrix, showcase_eval and showcase_capture-previews.

Ports CopilotKit/website#529 to this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:06:44 -05:00

286 lines
12 KiB
YAML

name: "Showcase: Capture Previews"
on:
workflow_run:
workflows: ["Showcase: Build & Push"]
types: [completed]
branches: [main]
workflow_dispatch:
inputs:
slug:
description: "Integration slug to capture (leave empty for all)"
type: string
default: ""
demo:
description: "Demo ID to capture (leave empty for all)"
type: string
default: ""
push:
branches: [main]
paths:
- "showcase/integrations/*/src/app/demos/**"
- "showcase/integrations/*/manifest.yaml"
concurrency:
group: showcase-capture-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
capture:
name: Capture Preview MP4s
# Hoist the Slack webhook into an env var so step-level `if:`
# expressions can reference it — `secrets.*` is not a valid
# named-value inside `if:` and causes a workflow startup failure.
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
runs-on: ubuntu-latest
# Captures demo previews as MP4 and uploads them to a GitHub release.
# Loop prevention: capture commits registry.json with the message below,
# which could re-trigger via push or the completed-deploy workflow_run.
# Skip when the triggering commit came from this workflow itself.
if: >-
(github.event_name != 'workflow_run' ||
!startsWith(github.event.workflow_run.head_commit.message, 'Update preview URLs in registry')) &&
(github.event_name != 'push' ||
!startsWith(github.event.head_commit.message, 'Update preview URLs in registry'))
timeout-minutes: 30
steps:
- name: Mint devops-bot token
# PROTECT_OUR_MAIN requires a PR for pushes to main; the default
# GITHUB_TOKEN fails with GH013 on the `Commit registry updates`
# step below. devops-bot (app-id 1108748) is a configured bypass
# actor on that ruleset — mint its installation token here and use
# it for checkout + push. Mirrors the pattern in
# CopilotKit/internal-skills's sync-versions.yml.
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: "1108748"
private-key: ${{ secrets.DEVOPS_BOT_PRIVATE_KEY }}
permission-contents: write
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
ref: ${{ github.event.workflow_run.head_branch || github.ref }}
token: ${{ steps.app-token.outputs.token }}
persist-credentials: false
- name: Ensure preview release exists
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release view showcase-previews --repo ${{ github.repository }} >/dev/null 2>&1 || \
gh release create showcase-previews --repo ${{ github.repository }} \
--title "Showcase Preview Videos" \
--notes "Auto-generated demo preview videos for the showcase platform. Updated by CI." \
--latest=false
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
- name: Install ffmpeg
run: sudo apt-get update && sudo apt-get install -y ffmpeg
# No --with-deps: it shells out to apt, which on the runners cannot always
# reach azure.archive.ubuntu.com and retries for many minutes — long enough to
# burn this job's whole timeout before a test runs. Chromium's system libraries
# are already present on the Ubuntu runner image, so downloading the browser is
# all this step needs.
- name: Install Playwright
run: npx playwright install chromium
- name: Install script dependencies
working-directory: showcase/scripts
run: pnpm install --ignore-scripts || true
- name: Detect changed packages
id: detect
if: github.event_name == 'push'
run: |
# Find which package slugs had demo changes
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'showcase/integrations/*/src/app/demos/' 'showcase/integrations/*/manifest.yaml' | \
sed -n 's|showcase/integrations/\([^/]*\)/.*|\1|p' | sort -u | tr '\n' ',' | sed 's/,$//')
echo "slugs=$CHANGED" >> $GITHUB_OUTPUT
echo "Changed packages: $CHANGED"
- name: Generate registry
run: |
cd showcase/scripts
npm ci --silent
npx tsx generate-registry.ts
- name: Capture previews
env:
INPUT_SLUG: ${{ inputs.slug }}
INPUT_DEMO: ${{ inputs.demo }}
EVENT_NAME: ${{ github.event_name }}
DETECTED_SLUGS: ${{ steps.detect.outputs.slugs }}
run: |
# Build the args as an array (not a space-joined string) so a
# slug or demo value containing whitespace or shell metacharacters
# stays a single argument rather than being re-tokenized by the shell.
ARGS=()
# Use input slug/demo if provided (manual dispatch)
if [ -n "$INPUT_SLUG" ]; then
ARGS+=(--slug "$INPUT_SLUG")
fi
if [ -n "$INPUT_DEMO" ]; then
ARGS+=(--demo "$INPUT_DEMO")
fi
# Use detected slugs for push events (capture only changed)
if [ "$EVENT_NAME" = "push" ] && [ -n "$DETECTED_SLUGS" ]; then
# Capture each changed slug
IFS=',' read -ra SLUGS <<< "$DETECTED_SLUGS"
for slug in "${SLUGS[@]}"; do
npx tsx showcase/scripts/capture-previews.ts --slug "$slug" || true
done
else
npx tsx showcase/scripts/capture-previews.ts "${ARGS[@]}" || true
fi
- name: Configure git for push
run: |
git config user.name "devops-bot[bot]"
git config user.email "1108748+devops-bot[bot]@users.noreply.github.com"
git config --local url."https://x-access-token:${TOKEN}@github.com/".insteadOf "https://github.com/"
env:
TOKEN: ${{ steps.app-token.outputs.token }}
- name: Commit registry updates
run: |
cd showcase/shell/src/data
if git diff --quiet registry.json; then
echo "No registry changes"
exit 0
fi
git add -f registry.json
git commit -m "Update preview URLs in registry"
# Rebase-and-retry loop: the capture job can take ~30 minutes,
# during which other commits routinely land on main. Without
# this loop, `git push` loses the race and fails with "fetch
# first" (observed in runs 24799601181 and 24809331709 on
# 2026-04-22). Rebase our single registry commit onto the
# latest main and retry; bounded attempts so a persistent
# failure still surfaces rather than looping forever.
attempts=0
max_attempts=5
until git push; do
attempts=$((attempts + 1))
if [ "$attempts" -ge "$max_attempts" ]; then
echo "::error::push failed after $max_attempts rebase attempts"
exit 1
fi
echo "push rejected — rebasing onto latest main (attempt $attempts/$max_attempts)"
git pull --rebase origin "$(git rev-parse --abbrev-ref HEAD)"
done
# Slack failure alert. This workflow only runs on main-branch pushes,
# workflow_run completions, and manual dispatch — all of which
# constitute "production" events where silent failures (e.g. the
# GH013 PROTECT_OUR_MAIN regression that motivated PR #4159) must
# surface in #oss-alerts. Extracts failed step name + first
# meaningful error line so the payload is triage-ready rather than
# forcing a click-through, per the oss-alerts detail policy.
#
# Must NEVER fail the job (runs on failure() already; a crash here
# would compound the original failure and could block the notify
# step). All extraction uses best-effort fallbacks so a malformed
# jobs response or truncated log still yields sane defaults.
- name: Extract failure details for Slack
id: extract
if: failure() && env.SLACK_WEBHOOK != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
run: |
set +e # best-effort: never block the notify step below
# --- Find the currently-running job and its first failed step ---
# The jobs API returns every job in the run. Match by job name
# first (mirrors `jobs.capture.name`); fall back to first job
# with a failed step if name match misses (e.g. future rename).
jobs_json=$(gh api "/repos/${GH_REPO}/actions/runs/${RUN_ID}/jobs" --paginate 2>/dev/null)
job_id=$(printf '%s' "$jobs_json" | jq -r '
.jobs // []
| map(select(.name == "Capture Preview MP4s"))
| (.[0].id // empty)
' 2>/dev/null)
if [ -z "$job_id" ]; then
job_id=$(printf '%s' "$jobs_json" | jq -r '
.jobs // []
| map(select(.steps // [] | map(.conclusion) | index("failure")))
| (.[0].id // empty)
' 2>/dev/null)
fi
failed_step=$(printf '%s' "$jobs_json" | jq -r --arg id "$job_id" '
.jobs // []
| map(select((.id|tostring) == $id))
| (.[0].steps // [])
| map(select(.conclusion == "failure"))
| (.[0].name // "unknown step")
' 2>/dev/null)
[ -z "$failed_step" ] && failed_step="unknown step"
# --- Pull log and extract first meaningful error line ------------
# `gh run view --log-failed` output is TSV: job\tstep\ttimestamp +
# content. Strip the three leading columns, strip ANSI escape
# codes + BOM, skip runner/group/env header noise, grab first
# line matching a recognised error marker. Truncate to ~300
# chars so the Slack payload stays under the 800-char budget.
error_excerpt="see workflow run for details"
if [ -n "$job_id" ]; then
log_excerpt=$(gh run view "$RUN_ID" --repo "$GH_REPO" --log-failed --job="$job_id" 2>/dev/null \
| awk -F'\t' 'NF>=3 { sub(/^[\xEF\xBB\xBF]?[0-9T:.\-Z ]+/, "", $3); print $3 }' \
| sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' \
| grep -vE '^(##\[|shell: |env: |Run |[[:space:]]*$)' \
| grep -m1 -E '^\[(FAIL|ERROR)\]|^Error:|^error:|^::error' \
| head -c 300)
if [ -n "$log_excerpt" ]; then
error_excerpt="$log_excerpt"
fi
fi
# --- Emit to $GITHUB_ENV using heredoc delimiter -----------------
# Heredoc delimiter protects against values with `=` or newlines
# breaking the KEY=VALUE format.
{
echo "failed_step<<EOF_FAILED_STEP_b3f2"
printf '%s\n' "$failed_step"
echo "EOF_FAILED_STEP_b3f2"
echo "error_excerpt<<EOF_ERROR_EXCERPT_b3f2"
printf '%s\n' "$error_excerpt"
echo "EOF_ERROR_EXCERPT_b3f2"
} >> "$GITHUB_ENV"
exit 0 # belt-and-suspenders: never propagate a failure
- name: Notify Slack (failure)
if: failure() && env.SLACK_WEBHOOK != ''
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
webhook-type: incoming-webhook
# Defensive: wrap dynamic values via toJSON(format(...)) so that
# if github.repository or extracted failed_step / error_excerpt
# contain characters that would break the JSON payload (quotes,
# backslashes, newlines), the value is safely JSON-encoded.
# Matches the pattern used in showcase_validate.yml.
payload: |
{ "text": ${{ toJSON(format(':x: *Showcase: Capture Previews*: failed — {0}: {1} | <https://github.com/{2}/actions/runs/{3}|View run>', env.failed_step, env.error_excerpt, github.repository, github.run_id)) }} }
- name: Log (no Slack — webhook unset)
if: failure() && env.SLACK_WEBHOOK == ''
run: |
echo "::warning::showcase_capture-previews failed but SLACK_WEBHOOK_OSS_ALERTS is not set; no Slack notification sent."