chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
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."
|
||||
Reference in New Issue
Block a user