From 437a411550a230d61aaecd29136ffb8493f2dac2 Mon Sep 17 00:00:00 2001 From: Jarrod Flesch Date: Tue, 21 Jul 2026 14:58:21 -0400 Subject: [PATCH 01/14] feat: add e2e-pr-assets skill --- README.md | 17 + skills/e2e-pr-assets/README.md | 68 +++ skills/e2e-pr-assets/SKILL.md | 235 ++++++++ .../tools/e2e-pr-assets/README.md | 74 +++ .../tools/e2e-pr-assets/bin/e2e-attach-pr | 288 ++++++++++ .../e2e-pr-assets/bin/e2e-capture-screenshot | 51 ++ .../tools/e2e-pr-assets/bin/e2e-convert-video | 117 ++++ .../bin/e2e-github-login-profile | 101 ++++ .../tools/e2e-pr-assets/bin/e2e-infer-suite | 125 +++++ .../tools/e2e-pr-assets/bin/e2e-run | 60 ++ .../tools/e2e-pr-assets/bin/e2e-run-script | 511 ++++++++++++++++++ .../bin/e2e-upload-github-attachments | 205 +++++++ .../tools/e2e-pr-assets/bundle.json | 25 + .../tools/e2e-pr-assets/check.sh | 61 +++ .../tools/e2e-pr-assets/install.sh | 65 +++ .../test/keyboard-overlay.scenario.mjs | 99 ++++ 16 files changed, 2102 insertions(+) create mode 100644 skills/e2e-pr-assets/README.md create mode 100644 skills/e2e-pr-assets/SKILL.md create mode 100644 skills/e2e-pr-assets/tools/e2e-pr-assets/README.md create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-attach-pr create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-capture-screenshot create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-convert-video create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-github-login-profile create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-infer-suite create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-run create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-run-script create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-upload-github-attachments create mode 100644 skills/e2e-pr-assets/tools/e2e-pr-assets/bundle.json create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/check.sh create mode 100755 skills/e2e-pr-assets/tools/e2e-pr-assets/install.sh create mode 100644 skills/e2e-pr-assets/tools/e2e-pr-assets/test/keyboard-overlay.scenario.mjs diff --git a/README.md b/README.md index c2eaf1d..f5cbeb8 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,23 @@ npx skills add payloadcms/skills [View full documentation →](skills/cms-migration/SKILL.md) +### `e2e-pr-assets` + +> Contributor-focused workflow for attaching before/after admin UI evidence to Payload pull requests with GitHub-inline videos or screenshots. + +**When to use:** Preparing or updating a Payload PR that needs reviewer-facing visual proof from e2e flows, especially when the fix is easiest to understand through before/after media in the PR body. + +**Covers:** + +- **Recording Plans**: Local `Before` / `After` shot lists with hidden PR-body comments +- **Video Evidence**: Record, convert, verify, and upload GitHub-inline H.264 MP4s +- **Screenshot Evidence**: Capture and attach before/after PNGs when screenshots are the right fit +- **Temporary Scenarios**: Agent-authored recording scripts for clearer demos than noisy committed tests +- **PR Body Updates**: Idempotent before/after sections with explicit incorrect/correct proof lines +- **Media Hygiene**: Startup-frame trimming, first-frame verification, and cleanup guidance + +[View full documentation →](skills/e2e-pr-assets/README.md) + ## Resources - [Payload Documentation](https://payloadcms.com/docs) diff --git a/skills/e2e-pr-assets/README.md b/skills/e2e-pr-assets/README.md new file mode 100644 index 0000000..d51ac23 --- /dev/null +++ b/skills/e2e-pr-assets/README.md @@ -0,0 +1,68 @@ +# E2E PR Assets Skill + +Contributor-focused skill for producing reviewer-friendly before/after admin UI evidence for Payload pull requests. + +## What's Included + +The `e2e-pr-assets` skill bundles: + +- `SKILL.md` with the end-to-end workflow and decision points +- `tools/e2e-pr-assets/bin/` helper commands for recording, conversion, upload, and PR-body updates +- `tools/e2e-pr-assets/check.sh` and `tools/e2e-pr-assets/install.sh` bootstrap scripts +- `tools/e2e-pr-assets/test/keyboard-overlay.scenario.mjs` as a lightweight recording fixture + +## Usage + +Once installed, the agent can invoke this skill when a Payload PR needs visual regression evidence. It is especially useful when: + +- a before/after video tells the bug story more clearly than test output +- the existing e2e suite is too noisy for reviewer-facing evidence +- the PR body needs inline GitHub videos or screenshots +- reviewers need hidden implementation notes in the PR body edit view, not visible reviewer text + +## Setup + +From the installed skill root: + +```bash +bash tools/e2e-pr-assets/check.sh +``` + +If the check fails: + +```bash +bash tools/e2e-pr-assets/install.sh +bash tools/e2e-pr-assets/check.sh +``` + +When working directly from this repository instead of an installed local skill, prefix commands with `skills/e2e-pr-assets/`, for example: + +```bash +bash skills/e2e-pr-assets/tools/e2e-pr-assets/check.sh +``` + +## Workflow Highlights + +- Write a temporary local recording plan with separate `Before` and `After` sections. +- Record the bug state and fixed state using the same scenario steps. +- Convert videos to GitHub-inline H.264 MP4 files and verify the first decoded frame when the opening poster matters. +- Upload media through GitHub attachments, then update the PR body with idempotent `Before` / `After` sections. +- Keep explanatory shot lists hidden inside HTML comments so reviewers only see the videos unless they inspect edit mode. + +## Repository Layout + +```text +skills/e2e-pr-assets/ +├── SKILL.md +└── tools/e2e-pr-assets/ + ├── README.md + ├── check.sh + ├── install.sh + ├── bin/ + └── test/ +``` + +## Resources + +- [Payload GitHub](https://github.com/payloadcms/payload) +- [Payload Documentation](https://payloadcms.com/docs) diff --git a/skills/e2e-pr-assets/SKILL.md b/skills/e2e-pr-assets/SKILL.md new file mode 100644 index 0000000..afca6d1 --- /dev/null +++ b/skills/e2e-pr-assets/SKILL.md @@ -0,0 +1,235 @@ +--- +name: e2e-pr-assets +description: Use when a PR needs before/after admin UI evidence from e2e tests, either videos or screenshots, attached to the PR body +--- + +# E2E PR Media + +## Overview + +Captures and attaches **Before/After** media to the PR body in this format: + +### Before + + + + +### After + + + + +Supports two modes: + +- `video` (default): GitHub-inline H.264 `.mp4` links +- `screenshot`: embedded PNG images + +This project bundle has no required sub-skills. It requires the helper scripts in `tools/e2e-pr-assets/bin`. + +## Required Recording Plan + +Before recording any media, write a temporary local plan outside the repo, for example: + +- `~/.copilot/session-state//recordings/pr-/recording-plan.md` + +This plan is for the agent first. Its `Before` and `After` section contents may be copied into the final PR body only as hidden HTML comments under the matching headings, so reviewers do not see them unless they inspect the PR body in edit mode. + +The plan must have separate `Before` and `After` sections and should answer: + +- What exact UI story the video must tell, in order +- Which actions must be visible on screen +- What proof must be visible at the end of the `Before` run +- What proof must be visible at the end of the `After` run +- What must be shown in card view +- What must be shown in list view +- Which final visible result is incorrect in `Before` and which final visible result is correct in `After` + +Use this template: + +```md +# PR Recording Plan + +## Before +- Steps: +- Final visible proof: + - Include the incorrect outcome explicitly, for example: `Show the document card with the published title. (Incorrect, should be draft title)` +- Card view must show: +- List view must show: + +## After +- Steps: +- Final visible proof: + - Include the correct outcome explicitly, for example: `Show the document card with the draft title. (Correct, shows draft title)` +- Card view must show: +- List view must show: +``` + +Do not start recording until the plan is specific enough that another agent could follow it without guessing. + +## Requirements + +- `gh` authenticated for the target repo +- `pnpm` + project deps installed +- `node_modules` present for the target worktree/repo +- `jq` installed +- For video mode: `ffmpeg`, `ffprobe`, `bc` +- From repo root, run a check-first bootstrap: + - `bash tools/e2e-pr-assets/check.sh` + - If check fails, run `bash tools/e2e-pr-assets/install.sh`, then rerun `bash tools/e2e-pr-assets/check.sh` +- Before relying on `e2e-run --record`, inspect the repo's Playwright config: + - If the config hardcodes `video: 'off'` or otherwise ignores `PLAYWRIGHT_VIDEO`, do not spend time fighting the suite recorder. + - Prefer `e2e-run-script` for video capture in that case. +- Helper commands installed by the bundle: + - `e2e-infer-suite` + - `e2e-run` + - `e2e-run-script` + - `e2e-convert-video` + - `e2e-upload-github-attachments` + - `e2e-capture-screenshot` + - `e2e-attach-pr` + +## Video Mode Workflow + +Recorded `e2e-run-script` scenarios automatically show keycaps for `locator.press`, `page.press`, `page.keyboard.press/down/up`, and clicks with Playwright `modifiers`. For example, `previewButton.click({ modifiers: ['ControlOrMeta'] })` appears as `⌘ + Click` on macOS or `Ctrl + Click` elsewhere. Text supplied through `fill`, `type`, `pressSequentially`, and `insertText` is never shown. + +### Preferred: focused `_community` evidence fixture + +Use this path when the relevant e2e suite is noisy, has unrelated fields, or would make the evidence harder to understand. The goal is a minimal admin surface that shows only the behavior the PR fixes. + +1. Add a temporary, focused fixture to `test/_community`: + - Create a collection/global under `test/_community/collections/` or `test/_community/globals/`. + - Wire it into `test/_community/config.ts`. + - Add only fields needed for the evidence. For example, for localized draft bugs: localized `title` plus one non-localized shared field. + - Add seed data in `onInit` only when the video needs a pre-existing document. +2. Start the community dev server: + - `pnpm run dev _community` +3. Write the local recording plan described above. +4. Generate a temporary scenario script outside the repo: + - `~/.copilot/session-state//recordings/pr-/scenario.mjs` + - Do not commit this script. + - Mirror the `Before` / `After` steps from the recording plan in a top-of-file comment so the scenario explains the intended visible story. +5. Run/record the scenario with `e2e-run-script`. + - Record "before" against the buggy/unfixed state. + - Record "after" against the fixed state. + - Use the same fixture data and scenario steps for both recordings. + - Recorded runs include a visible in-page cursor overlay. Normal `page.click`, `page.fill`, `locator.click`, and `locator.fill` calls move it automatically; scenarios can also use the provided `cursor` helper for deliberate pointer movement before an explanatory pause. + - If the video goal includes comparing card view and list view, make both views explicit in the plan and the scenario; do not assume one implies the other. +6. After recording, remove every temporary `_community` collection/config/seed edit before finalizing: + - `git --no-pager diff -- test/_community` + - The diff must be empty unless the PR intentionally changes `_community`. + - Also check full repo status because Payload test/dev commands can rewrite `tsconfig.base.json` aliases: + - `git --no-pager diff -- tsconfig.base.json` + - `git --no-pager status --short` + - Do not leave evidence-only fixtures in the PR. + +### Alternative: agent-generated recording scenario against existing config + +Use this path when the committed e2e suite is too CI-focused, too brittle for evidence, or would require adding video-only behavior to tests. The user should describe the evidence they want; the agent writes the scenario. + +Prefer this path early when the goal is explanatory evidence, not just pass/fail proof. If the video needs to clearly tell a story in the UI, use a scenario instead of forcing the committed suite to double as a demo. + +1. Infer the suite/config context from the PR: + - `e2e-infer-suite --repo ` +2. Write the local recording plan described above. +3. Generate a temporary scenario script outside the repo, for example: + - `~/.copilot/session-state//recordings/pr-/scenario.mjs` + - Do not commit this script. + - Add a top-of-file comment with separate `Before` and `After` shot lists copied from the plan. +4. The scenario must export a function: + +```js +export default async function scenario({ baseURL, cursor, expect, keyboardOverlay, label, page, record, repoRoot }) { + // Seed data, log in, navigate the admin UI, and perform the exact evidence flow. + // Optional: await cursor?.moveTo('#important-field') before pausing for emphasis. + // Optional: await keyboardOverlay?.show('Custom action') when no Playwright action can express it. +} +``` + +5. If the scenario mutates document data outside the open edit view, avoid stale-data modal churn: + - Prefer seeding and local API mutations before opening the document page when possible. + - If you must mutate via local API while the document page is already open, reload the page before continuing visible interactions. + +6. Dry-run and iterate until the script works: + - `e2e-run-script --repo --label before --base-url http://localhost:3000` + - Dry-run success means the visible end state matches the recording plan for that label, not merely that the script exits successfully. +7. Record before/after: + - `e2e-run-script --repo --label before --record` + - `e2e-convert-video before` + - `e2e-run-script --repo --label after --record` + - `e2e-convert-video after` +8. Continue with MP4 verification and GitHub attachment upload below. + +### Existing committed e2e suite + +1. Infer suite from the PR, and if a touched e2e test exists, start there: + - `e2e-infer-suite --repo ` + - First result is the default suite candidate. +2. Write the local recording plan described above, even if you are reusing a committed e2e test. +3. Write/confirm regression e2e test included in the PR. +4. Dry-run before (no recording) and confirm it fails: + - `e2e-run --repo ` + - A passing dry-run is not enough for recording readiness; the suite must visibly cover the planned `Before` proof. +5. Record before (unless already cached and no re-record requested): + - `e2e-run --record --label before --repo ` + - `e2e-convert-video before` + - If no fresh `.webm` is produced, stop assuming the suite recorder works in this repo and switch to `e2e-run-script`. +6. Apply fix. +7. Dry-run after (no recording) and confirm it passes: + - `e2e-run --repo ` + - The suite must visibly cover the planned `After` proof before you accept the recording. +8. Record after and convert: + - `e2e-run --record --label after --repo ` + - `e2e-convert-video after` +9. Verify MP4 outputs before attaching: + - `/tmp/payload-e2e-before.mp4` and `/tmp/payload-e2e-after.mp4` exist and are non-empty. + - `ffprobe` reports `codec_name=h264` for both. + - Inspect the first decoded frame of each MP4 when the opening poster matters. The first visible frame should already be meaningful UI, not a white loading spinner or blank transition frame. + - Do not attach `.webm` files; GitHub does not render them inline in PR bodies. + - Do not use raw GitHub, release asset, or Git LFS URLs for video mode; GitHub treats them as normal links. + - Do not switch to screenshots unless the user explicitly changes the requested mode. +10. Upload through GitHub's attachment UI to get `https://github.com/user-attachments/assets/...` URLs: + - `e2e-upload-github-attachments /tmp/payload-e2e-before.mp4 /tmp/payload-e2e-after.mp4` + - If the configured browser profile is not signed in, the script automatically opens the login flow, waits for manual sign-in detection, then retries upload once. + - Optional profile override: `GITHUB_BROWSER_PROFILE=/path/to/profile` + - To disable automatic login flow, set `E2E_GITHUB_AUTO_LOGIN=0`. +11. Attach the returned user-attachment URLs to the PR body: + +- `urls=$(e2e-upload-github-attachments /tmp/payload-e2e-before.mp4 /tmp/payload-e2e-after.mp4)` +- `e2e-attach-pr --mode video --before-url "$(jq -r .before <<<"$urls")" --after-url "$(jq -r .after <<<"$urls")" --plan-file /path/to/recording-plan.md` +- `e2e-attach-pr` copies the `Before` and `After` sections from the plan file into hidden HTML comments directly below the visible `### Before` and `### After` headings. +- In those hidden comments, make the final visible proof line explicit: `Before` should call out the incorrect result and `After` should call out the correct result. +- Video mode is not complete until the PR body uses `github.com/user-attachments/assets` URLs. + +## Screenshot Mode Workflow + +1. Infer suite from the PR, and if a touched e2e test exists, start there: + - `e2e-infer-suite --repo ` +2. Capture/export before screenshot to `/tmp/payload-e2e-before.png`: + - `e2e-capture-screenshot before --source ` + - or `e2e-capture-screenshot before --repo ` (uses newest PNG in `test-results`) +3. Apply fix and capture/export after screenshot: + - `e2e-capture-screenshot after --source ` +4. Attach: + - `e2e-attach-pr --mode screenshot` + +## Notes + +- Use this bootstrap guard before running media commands: + +```bash +if ! bash tools/e2e-pr-assets/check.sh; then + bash tools/e2e-pr-assets/install.sh + bash tools/e2e-pr-assets/check.sh +fi +``` + +- `e2e-attach-pr` is idempotent: it replaces the prior generated media block instead of duplicating it. +- The recording plan remains a local, temporary planning artifact. When attaching media, pass it to `e2e-attach-pr --plan-file ...` so the `Before` and `After` notes land in hidden PR-body comments instead of visible reviewer-facing text. +- When the PR is demonstrating a bug and its fix, the hidden `Before` comment should name the incorrect outcome and the hidden `After` comment should name the correct outcome. +- Video conversion records at full viewport size, trims the initial blank Playwright lead-in by default (including longer startup blanks when a later first scene is detected), and auto-compresses to fit GitHub upload constraints when needed. +- If the opening poster is important, inspect the first decoded MP4 frame before upload rather than assuming the trim is visually correct. +- If `e2e-convert-video` fails with `h264_videotoolbox`, retry conversion with `libx264` instead of re-recording. Hardware H.264 availability is not the same as hardware H.264 reliability. +- Video mode is complete only when the PR body contains `github.com/user-attachments/assets` video links. `.webm` links or raw `.mp4` links are failed video-mode results. +- Media artifacts are stored in `/tmp` and are automatically removed after `e2e-attach-pr` completes by default. Disable with `E2E_MEDIA_AUTO_CLEANUP=0`. +- Payload dev/test commands may rewrite `tsconfig.base.json` aliases such as `@payload-config`. After recording, restore that drift before finalizing. +- Never store PR evidence media in repository branches such as `.github/e2e-assets/...` or `e2e-assets-`. Those commits pollute the PR timeline and file list. If GitHub attachment upload fails, fix the upload flow instead of committing media files. diff --git a/skills/e2e-pr-assets/tools/e2e-pr-assets/README.md b/skills/e2e-pr-assets/tools/e2e-pr-assets/README.md new file mode 100644 index 0000000..c515403 --- /dev/null +++ b/skills/e2e-pr-assets/tools/e2e-pr-assets/README.md @@ -0,0 +1,74 @@ +# e2e-pr-assets bundle + +This folder packages the `e2e-pr-assets` skill and its helper scripts so teammates can use the same workflow from this repository. + +## What is included + +- Project skill: `skills/e2e-pr-assets/SKILL.md` +- Helper scripts: `tools/e2e-pr-assets/bin/e2e-*` +- Bootstrap scripts: + - `tools/e2e-pr-assets/install.sh` + - `tools/e2e-pr-assets/check.sh` +- Bundle manifest: `tools/e2e-pr-assets/bundle.json` + +## Setup + +From the installed skill root: + +```bash +bash tools/e2e-pr-assets/check.sh +``` + +If the check fails, install then re-check: + +```bash +bash tools/e2e-pr-assets/install.sh +bash tools/e2e-pr-assets/check.sh +``` + +The install script links helper commands into `~/.local/bin` (or `$XDG_BIN_HOME`) and validates prerequisites. + +If you are running directly from this repository checkout instead of an installed local skill, prefix paths with `skills/e2e-pr-assets/`. + +## Validate manually + +```bash +bash tools/e2e-pr-assets/check.sh +bash tools/e2e-pr-assets/bin/e2e-run-script tools/e2e-pr-assets/test/keyboard-overlay.scenario.mjs --repo . --label keyboard-overlay-test --record +``` + +## Recording Plan + +Before recording, create a temporary local `recording-plan.md` outside the repo with separate `Before` and `After` sections. + +- Keep this plan out of the visible PR body. +- List the visible step order for each pass. +- State exactly what must be proven in card view and in list view. +- Mirror the same `Before` / `After` plan in a top-of-file comment in any temporary `scenario.mjs`. +- When attaching media, pass the plan to `e2e-attach-pr --plan-file ...` so the `Before` and `After` notes become hidden HTML comments below the matching headings. +- In those hidden comments, call out the verdict explicitly: `Before` should name the incorrect result, and `After` should name the correct result. + +## Notes + +- The scripts are repository-local and can be versioned/reviewed via PR. +- The install script creates symlinks rather than copying files, so updates in this repo are immediately reflected. +- Default security behavior: + - Browser auth profile defaults to `/tmp/github-upload-profile`. + - Temporary browser profile is removed automatically after upload flow unless explicitly disabled. + - Temporary media artifacts are removed automatically after `e2e-attach-pr` updates the PR body. +- `e2e-upload-github-attachments` is check-first for auth: + - Uses an ephemeral profile by default: `/tmp/github-upload-profile`. + - If the selected browser profile is already signed in to GitHub, upload proceeds immediately. + - If not signed in, it opens `e2e-github-login-profile`, waits for manual sign-in detection, then retries upload once automatically. + - On completion (success/failure), it removes temporary profile directories by default for security. + - Override profile with `GITHUB_BROWSER_PROFILE=/path/to/profile`. + - Disable auto-removal with `E2E_GITHUB_AUTO_REMOVE_PROFILE=0`. + - For non-temporary custom paths, removal is skipped unless `E2E_GITHUB_FORCE_REMOVE_PROFILE=1`. + - Disable auto-open behavior with `E2E_GITHUB_AUTO_LOGIN=0`. +- `e2e-attach-pr` cleans `/tmp/payload-e2e-before|after.{mp4,webm,png}` after a successful PR body update by default. + - Disable media cleanup with `E2E_MEDIA_AUTO_CLEANUP=0`. +- `e2e-attach-pr --plan-file /path/to/recording-plan.md` copies the plan's `Before` and `After` sections into hidden HTML comments in the PR body, directly below the visible `### Before` and `### After` headings. +- Prefer final proof lines like `Show the document card with the published title. (Incorrect, should be draft title)` and `Show the document card with the draft title. (Correct, shows draft title)` so the hidden comments explain the bug/fix clearly in edit mode. +- `e2e-convert-video` trims the initial blank lead-in by default and can extend that trim automatically when the first meaningful scene change happens later than the standard 1-second startup buffer. +- When the opening poster matters, inspect the first decoded MP4 frame after conversion and before upload. The first frame should already be meaningful UI, not a white loading spinner or blank transition frame. +- If `h264_videotoolbox` is available but fails during conversion, `e2e-convert-video` automatically retries with `libx264`. diff --git a/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-attach-pr b/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-attach-pr new file mode 100755 index 0000000..12cf691 --- /dev/null +++ b/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-attach-pr @@ -0,0 +1,288 @@ +#!/usr/bin/env bash +# e2e-attach-pr [--mode video|screenshot] [--plan-file /path/to/recording-plan.md] +# Uploads before/after media and appends them to the PR body, optionally +# adding hidden Before/After plan comments copied from a recording plan file. +# Requires: gh CLI (authenticated), jq + +set -euo pipefail + +PR="${1:-}" +REPO="${2:-}" +MODE="video" +BEFORE_URL_OVERRIDE="" +AFTER_URL_OVERRIDE="" +PLAN_FILE="" +AUTO_CLEANUP_MEDIA="${E2E_MEDIA_AUTO_CLEANUP:-1}" + +shift 2 || true +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + MODE="${2:-}" + shift 2 + ;; + --before-url) + BEFORE_URL_OVERRIDE="${2:-}" + shift 2 + ;; + --after-url) + AFTER_URL_OVERRIDE="${2:-}" + shift 2 + ;; + --plan-file) + PLAN_FILE="${2:-}" + shift 2 + ;; + *) + echo "Unknown option: $1" >&2 + echo "Usage: e2e-attach-pr [--mode video|screenshot] [--before-url URL --after-url URL] [--plan-file /path/to/recording-plan.md]" >&2 + exit 1 + ;; + esac +done + +if [[ -z "$PR" || -z "$REPO" ]]; then + echo "Usage: e2e-attach-pr [--mode video|screenshot] [--before-url URL --after-url URL] [--plan-file /path/to/recording-plan.md]" >&2 + exit 1 +fi + +if [[ "$MODE" != "video" && "$MODE" != "screenshot" ]]; then + echo "Error: --mode must be 'video' or 'screenshot'" >&2 + exit 1 +fi + +if [[ -n "$BEFORE_URL_OVERRIDE" || -n "$AFTER_URL_OVERRIDE" ]]; then + if [[ -z "$BEFORE_URL_OVERRIDE" || -z "$AFTER_URL_OVERRIDE" ]]; then + echo "Error: provide both --before-url and --after-url" >&2 + exit 1 + fi + + if [[ "$MODE" == "video" ]]; then + if [[ "$BEFORE_URL_OVERRIDE" != https://github.com/user-attachments/assets/* || "$AFTER_URL_OVERRIDE" != https://github.com/user-attachments/assets/* ]]; then + echo "Error: video URLs must be GitHub user-attachment URLs for inline rendering" >&2 + exit 1 + fi + fi +fi + +if [[ -n "$PLAN_FILE" && ! -f "$PLAN_FILE" ]]; then + echo "Error: plan file not found: $PLAN_FILE" >&2 + exit 1 +fi + +extract_plan_section() { + local section_heading="$1" + local plan_file="$2" + + awk -v heading="$section_heading" ' + BEGIN { + capture = 0 + found = 0 + } + $0 ~ "^#{1,6}[[:space:]]+" heading "[[:space:]]*$" { + capture = 1 + found = 1 + next + } + capture && $0 ~ "^#{1,6}[[:space:]]+" { + capture = 0 + } + capture { + print + } + END { + if (!found) { + exit 2 + } + } + ' "$plan_file" +} + +trim_section_text() { + awk ' + NR == 1 { + lines[NR] = $0 + next + } + { + lines[NR] = $0 + } + END { + start = 1 + while (start <= NR && lines[start] ~ /^[[:space:]]*$/) { + start++ + } + + end = NR + while (end >= start && lines[end] ~ /^[[:space:]]*$/) { + end-- + } + + for (i = start; i <= end; i++) { + print lines[i] + } + } + ' +} + +build_plan_comment() { + local label="$1" + local plan_file="$2" + + if [[ -z "$plan_file" ]]; then + return + fi + + local section_text="" + + if ! section_text="$(extract_plan_section "$label" "$plan_file" | trim_section_text)"; then + echo "Error: plan file must include a '${label}' markdown heading" >&2 + exit 1 + fi + + if [[ -z "$section_text" ]]; then + echo "Error: plan file section '${label}' must not be empty" >&2 + exit 1 + fi + + printf '' "$section_text" +} + +upload_asset() { + local label=$1 + local file="" + local content_type="" + local asset_name="" + + if [[ "$MODE" == "video" ]]; then + file="/tmp/payload-e2e-${label}.mp4" + content_type="video/mp4" + asset_name="e2e-${label}.mp4" + else + file="/tmp/payload-e2e-${label}.png" + content_type="image/png" + asset_name="e2e-${label}.png" + fi + + if [[ ! -f "$file" ]]; then + echo "Error: $file not found" >&2 + exit 1 + fi + + if [[ "$MODE" == "video" ]]; then + local codec + codec="$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of csv=p=0 "$file")" + if [[ "$codec" != "h264" ]]; then + echo "Error: $file must be H.264 MP4 for GitHub inline rendering; got codec '$codec'" >&2 + exit 1 + fi + fi + + echo "Uploading ${label} ${MODE} ($(( $(stat -f%z "$file" 2>/dev/null || stat -c%s "$file") / 1024 ))KB)..." >&2 + + local response url + response="$(mktemp)" + gh api \ + --hostname "uploads.github.com" \ + --method POST \ + -F "file=@${file};type=${content_type}" \ + "/repos/${REPO}/issues/${PR}/assets?name=${asset_name}" \ + > "$response" || true + + url="$(jq -r '.browser_download_url // empty' "$response")" + rm -f "$response" + + if [[ -z "$url" ]]; then + if [[ "$MODE" == "video" ]]; then + echo "Error: GitHub API upload did not return an inline attachment URL for ${label} video." >&2 + echo "Use e2e-upload-github-attachments ${PR} ${REPO} /tmp/payload-e2e-before.mp4 /tmp/payload-e2e-after.mp4 with a signed-in browser profile, then rerun e2e-attach-pr with --before-url and --after-url." >&2 + exit 1 + fi + + echo "Error: GitHub API upload did not return an attachment URL for ${label} ${MODE}." >&2 + echo "Upload media through GitHub's attachment UI, then rerun e2e-attach-pr with --before-url and --after-url." >&2 + exit 1 + fi + + printf '%s' "$url" +} + +cleanup_media() { + if [[ "$AUTO_CLEANUP_MEDIA" != "1" ]]; then + return + fi + + rm -f \ + /tmp/payload-e2e-before.mp4 \ + /tmp/payload-e2e-after.mp4 \ + /tmp/payload-e2e-before.webm \ + /tmp/payload-e2e-after.webm \ + /tmp/payload-e2e-before.png \ + /tmp/payload-e2e-after.png + echo "Cleaned up local media artifacts from /tmp." >&2 +} + +if [[ -n "$BEFORE_URL_OVERRIDE" ]]; then + BEFORE_URL="$BEFORE_URL_OVERRIDE" + AFTER_URL="$AFTER_URL_OVERRIDE" +else + BEFORE_URL=$(upload_asset "before") + AFTER_URL=$(upload_asset "after") +fi + +if [[ -z "$BEFORE_URL" || -z "$AFTER_URL" || "$BEFORE_URL" == "null" || "$AFTER_URL" == "null" ]]; then + echo "Error: one or both uploads failed" >&2 + exit 1 +fi + +CURRENT_BODY=$(gh pr view "$PR" --repo "$REPO" --json body --jq '.body') + +if [[ "$MODE" == "video" ]]; then + BEFORE_VALUE="$BEFORE_URL" + AFTER_VALUE="$AFTER_URL" +else + BEFORE_VALUE="![before]($BEFORE_URL)" + AFTER_VALUE="![after]($AFTER_URL)" +fi + +BEFORE_PLAN_COMMENT="$(build_plan_comment "Before" "$PLAN_FILE")" +AFTER_PLAN_COMMENT="$(build_plan_comment "After" "$PLAN_FILE")" + +BEFORE_AFTER_BLOCK=$(cat < +### Before +${BEFORE_PLAN_COMMENT} +$BEFORE_VALUE + +### After +${AFTER_PLAN_COMMENT} +$AFTER_VALUE + +EOF +) + +if echo "$CURRENT_BODY" | grep -qE "|"; then + NEW_BODY=$(printf "%s" "$CURRENT_BODY" | awk ' + BEGIN { skip=0 } + // { skip=1; next } + // { skip=0; next } + // { skip=1; next } + // { skip=0; next } + skip==0 { print } + ') +else + NEW_BODY="$CURRENT_BODY" +fi + +TMP_BODY=$(mktemp) +{ + printf "%s\n\n" "$NEW_BODY" + printf "%s\n" "$BEFORE_AFTER_BLOCK" +} > "$TMP_BODY" + +gh pr edit "$PR" --repo "$REPO" --body-file "$TMP_BODY" +rm -f "$TMP_BODY" + +cleanup_media + +echo "PR #${PR} updated with Before/After ${MODE} section" diff --git a/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-capture-screenshot b/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-capture-screenshot new file mode 100755 index 0000000..f3117da --- /dev/null +++ b/skills/e2e-pr-assets/tools/e2e-pr-assets/bin/e2e-capture-screenshot @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# e2e-capture-screenshot