diff --git a/.gitea/workflows/e2e-chat.yml b/.gitea/workflows/e2e-chat.yml index 986e5dab0..57b7da591 100644 --- a/.gitea/workflows/e2e-chat.yml +++ b/.gitea/workflows/e2e-chat.yml @@ -1,8 +1,10 @@ name: E2E Chat # Comprehensive Playwright E2E for the unified chat stack (desktop -# ChatTab + mobile MobileChat). Runs on every PR that touches canvas, -# workspace-server, or this workflow file. +# ChatTab + mobile MobileChat). Heavy browser execution is intentionally +# outside the normal required PR path: PRs run it only after entering the +# `merge-queue`, while push/main, nightly, and manual dispatch preserve +# coverage without making every PR pay the full runtime/browser cost. # # Architecture: # 1. Ephemeral Postgres + Redis (docker, unique container names) @@ -22,6 +24,11 @@ on: branches: [main, staging] pull_request: branches: [main, staging] + schedule: + # Nightly at 09:00 UTC. Keeps coverage for the currently non-required + # heavy browser lane without spending runner time on every PR. + - cron: '0 9 * * *' + workflow_dispatch: concurrency: group: e2e-chat-${{ github.event.pull_request.head.sha || github.sha }} @@ -50,7 +57,14 @@ jobs: with: fetch-depth: 0 - id: decide + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + QUEUE_LABEL: merge-queue run: | + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "chat=true" >> "$GITHUB_OUTPUT" + exit 0 + fi BASE="${GITHUB_BASE_REF:-${{ github.event.before }}}" if [ "${{ github.event_name }}" = "pull_request" ] && [ -n "${{ github.event.pull_request.base.sha }}" ]; then BASE="${{ github.event.pull_request.base.sha }}" @@ -67,9 +81,26 @@ jobs: exit 0 fi CHANGED=$(git diff --name-only "$BASE" HEAD) - if echo "$CHANGED" | grep -qE '^(canvas/|workspace-server/|\.gitea/workflows/e2e-chat\.yml$)'; then + if ! echo "$CHANGED" | grep -qE '^(canvas/|workspace-server/|\.gitea/workflows/e2e-chat\.yml$)'; then + echo "chat=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "chat=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + authfile=$(mktemp) + chmod 600 "$authfile" + printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$authfile" + labels=$(curl -fsS -K "$authfile" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \ + | python3 -c 'import json,sys; print("\n".join(label.get("name","") for label in json.load(sys.stdin)))') + rm -f "$authfile" + if printf '%s\n' "$labels" | grep -qx "$QUEUE_LABEL"; then echo "chat=true" >> "$GITHUB_OUTPUT" else + echo "PR is not in merge-queue; skipping heavy E2E Chat for normal PR path." echo "chat=false" >> "$GITHUB_OUTPUT" fi diff --git a/.gitea/workflows/e2e-staging-canvas.yml b/.gitea/workflows/e2e-staging-canvas.yml index c0c57a70c..696863c2a 100644 --- a/.gitea/workflows/e2e-staging-canvas.yml +++ b/.gitea/workflows/e2e-staging-canvas.yml @@ -16,9 +16,9 @@ name: E2E Staging Canvas (Playwright) # e2e-staging-saas.yml (which tests the API shape) by exercising the # actual browser + canvas bundle against live staging. # -# Triggers: push to main/staging or PR touching canvas sources + this workflow, -# manual dispatch, and weekly cron to catch browser/runtime drift even -# when canvas is quiet. +# Triggers: push to main, PR touching canvas sources + this workflow only +# after the PR enters `merge-queue`, manual dispatch, and scheduled cron to +# catch browser/runtime drift even when canvas is quiet. # Added staging to push/pull_request branches so the auto-promote gate # check (--event push --branch staging) can see a completed run for this # workflow — mirrors what PR #1891 does for e2e-api.yml. @@ -37,9 +37,10 @@ on: pull_request: branches: [main] schedule: - # Weekly on Sunday 08:00 UTC — catches Chrome / Playwright / Next.js + # Nightly at 08:00 UTC — catches Chrome / Playwright / Next.js # release-note-shaped regressions that don't ride in with a PR. - - cron: '0 8 * * 0' + - cron: '0 8 * * *' + workflow_dispatch: concurrency: # Per-SHA grouping (changed 2026-04-28 from a single global group). The @@ -79,10 +80,13 @@ jobs: with: fetch-depth: 0 - id: decide + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + QUEUE_LABEL: merge-queue # Inline replacement for dorny/paths-filter — see e2e-api.yml. - # Cron triggers always run real work (no diff context). + # Cron and manual triggers always run real work (no diff context). run: | - if [ "${{ github.event_name }}" = "schedule" ]; then + if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then echo "canvas=true" >> "$GITHUB_OUTPUT" exit 0 fi @@ -102,9 +106,26 @@ jobs: exit 0 fi CHANGED=$(git diff --name-only "$BASE" HEAD) - if echo "$CHANGED" | grep -qE '^(canvas/|\.gitea/workflows/e2e-staging-canvas\.yml$)'; then + if ! echo "$CHANGED" | grep -qE '^(canvas/|\.gitea/workflows/e2e-staging-canvas\.yml$)'; then + echo "canvas=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + if [ "${{ github.event_name }}" != "pull_request" ]; then + echo "canvas=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + authfile=$(mktemp) + chmod 600 "$authfile" + printf 'header = "Authorization: token %s"\n' "$GITEA_TOKEN" > "$authfile" + labels=$(curl -fsS -K "$authfile" \ + "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels" \ + | python3 -c 'import json,sys; print("\n".join(label.get("name","") for label in json.load(sys.stdin)))') + rm -f "$authfile" + if printf '%s\n' "$labels" | grep -qx "$QUEUE_LABEL"; then echo "canvas=true" >> "$GITHUB_OUTPUT" else + echo "PR is not in merge-queue; skipping heavy E2E Staging Canvas for normal PR path." echo "canvas=false" >> "$GITHUB_OUTPUT" fi diff --git a/tests/test_heavy_e2e_pr_gating.py b/tests/test_heavy_e2e_pr_gating.py new file mode 100644 index 000000000..c16e6acb0 --- /dev/null +++ b/tests/test_heavy_e2e_pr_gating.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] + + +def workflow_on(path: Path): + doc = yaml.safe_load(path.read_text()) + return doc.get("on") or doc.get(True) + + +def test_browser_e2e_workflows_are_not_unconditional_pr_heavy_lanes(): + workflows = [ + ROOT / ".gitea/workflows/e2e-chat.yml", + ROOT / ".gitea/workflows/e2e-staging-canvas.yml", + ] + + for path in workflows: + text = path.read_text() + events = workflow_on(path) + + assert "workflow_dispatch" in events + assert "schedule" in events + assert "merge-queue" in text + assert "/issues/${{ github.event.pull_request.number }}/labels" in text + assert "PR is not in merge-queue" in text