281 lines
12 KiB
YAML
281 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
|
with:
|
|
node-version: 20
|
|
|
|
- name: Setup pnpm
|
|
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
|
|
|
- name: Install ffmpeg
|
|
run: sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
|
|
- name: Install Playwright
|
|
run: npx playwright install chromium --with-deps
|
|
|
|
- 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@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5
|
|
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."
|