Skip to content

ci(e2e): run the E2E specs a PR changes (#10518) - #10868

Closed
MumuTW wants to merge 2 commits into
stablyai:mainfrom
MumuTW:ci/e2e-changed-specs-gate
Closed

MumuTW wants to merge 2 commits into
stablyai:mainfrom
MumuTW:ci/e2e-changed-specs-gate

Conversation

@MumuTW

@MumuTW MumuTW commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

e2e.yml is reachable only from schedule and release-cut's workflow_call, so a PR that adds an E2E regression test never runs that test — it runs for the first time on main, where nothing gates on it. Twice now a spec has landed and never passed (#10518).

This adds a pull_request job that runs exactly the specs the PR touched. A PR that changes no spec pays nothing: the file list is resolved first, and every setup/build/run step is gated on the result being non-empty.

Gating the full sharded suite on every PR stays a separate decision, deliberately not made here: it is 22 minutes of wall clock (build + 10 shards + the docker job), and main is red today, so making it required would block every merge until #10519 closes.

Screenshots

No visual change.

Proof of fix

The upstream PR changes no spec, so its own e2e changed specs check can only demonstrate the skip path. The run path is demonstrated on a throwaway fork PR whose head is this commit plus one comment-only line in tests/e2e/markdown-explorer-find-focus.spec.ts:

Run path — MumuTW/orca run 30242976474, 2m53s total:

Running 1 changed spec(s):
  tests/e2e/markdown-explorer-find-focus.spec.ts
> ... npx playwright test --config tests/playwright.config.ts --project=electron-headless \
      --pass-with-no-tests tests/e2e/markdown-explorer-find-focus.spec.ts
  ✓  1 [electron-headless] › tests/e2e/markdown-explorer-find-focus.spec.ts:7:5 › Explorer-opened Markdown accepts the find shortcut without a document click (23.7s)
  1 passed (25.5s)

It selected exactly the touched file — not the whole suite, not the shard containing it.

Skip path — MumuTW/orca run 30243157972, same commit with no spec change, 16 seconds end to end:

success  Checkout
success  Select changed E2E specs
skipped  Install native build and headless UI tools
skipped  Setup Node.js
skipped  Setup pnpm
skipped  Use external node-gyp ...
skipped  Install dependencies
skipped  Build Electron app for E2E
skipped  Run changed E2E specs

That is what the vast majority of PRs will pay. This PR's own check on stablyai/orca is action_required pending maintainer approval, which is why the evidence above is from the fork.

Design notes

  • No paths: filter. The check reports on every PR, so it can be made a required check without wedging the merge box on PRs that touch no spec. A paths:-filtered workflow simply never reports, and a required check that never reports blocks the merge forever. Selection happens inside the job instead.
  • Changed files come from the pulls API (gh api --paginate .../files), not a git diff: the PR checkout is shallow, --paginate avoids the 100-file cap of gh pr view --json files, and select(.status != "removed") keeps a spec the PR deleted from reaching Playwright.
  • --pass-with-no-tests — a spec whose tests are all @headful is filtered out by the electron-headless project, and "no tests ran" must not fail the gate.
  • Same invocation as the scheduled suite — xvfb-run --auto-servernum env SKIP_BUILD=1 ORCA_E2E_FORWARD_APP_LOGS=1 pnpm run test:e2e, reusing the prebuilt output, so a spec cannot pass here and fail on the nightly for harness reasons. A unit test asserts these tokens against e2e.yml itself, so the two cannot drift silently.
  • Specs with their own runners self-skip without their env var (ORCA_E2E_SSH_DOCKER etc.), so selecting one is a cheap no-op rather than a false failure.

Testing

  • pnpm lint (oxlint, changed files)
  • pnpm typecheck — n/a, no TS product code changed; the added script is .mjs and its test runs under vitest
  • pnpm test — ran the config/scripts suite: 54 files, 433 passed, 4 skipped, including the 11 new cases
  • pnpm build — not run; no application code changed
  • Added or updated high-quality tests that would catch regressions

config/scripts/e2e-changed-specs-workflow.test.mjs covers both halves:

  • Selector behavior — keeps only tests/e2e/**/*.spec.ts, walks nested dirs, dedupes and sorts, emits nothing when no spec changed, and drops paths a fork PR could weaponize (; rm -rf /, $(id), backticks, spaces, --shard=1/2, .. traversal, absolute paths).
  • Workflow contract — no paths: filter, the exact types: list, the API-based selection command, permissions: {contents: read, pull-requests: read}, every work step gated on a non-empty selection, the trace upload gated on failure(), and the run invocation matching e2e.yml's own shard command token for token.

Also verified the command shape locally against the real Playwright config, since "the args are forwarded correctly" is the load-bearing claim:

$ npx playwright test --config tests/playwright.config.ts --project=electron-headless \
    --pass-with-no-tests --list tests/e2e/markdown-explorer-find-focus.spec.ts tests/e2e/onboarding.spec.ts
Total: 13 tests in 2 files

AI Review Report

Reviewed for: required-check semantics, shell injection through PR-controlled filenames, cost on the common path, and drift between this gate and the scheduled suite.

Flagged and changed as a result:

  1. paths: filter would break a future required check — a filtered workflow never reports on PRs that touch no spec, so the merge box waits forever. Moved selection inside the job and pinned that invariant with a test.
  2. mapfile is bash 4+ — replaced with a while IFS= read -r loop so the snippet is not silently tied to the runner's bash version, and the whole run snippet was then executed locally under bash 3.2.
  3. Trace upload was initially caught by the "everything is gated" assertion — it is correctly gated on failure() instead; the test now exempts it explicitly rather than loosening the rule.

Cross-platform: the job is ubuntu-latest-only by design (it mirrors e2e.yml, which is Linux-only), and the runner.os == 'Linux' guard on the node-gyp pin matches e2e.yml/pr.yml so the "this workaround is Linux-only" invariant stays consistent if a matrix is ever added. The selector script is pure Node with POSIX path handling and no shell. No shortcuts, labels, or Electron platform behavior touched.

Security Audit

The one real surface is command execution with attacker-controlled input: the selected paths become argv for a shell-invoked pnpm run test:e2e, and a fork PR chooses its own filenames.

  • Paths pass a strict allowlist (^tests/e2e/[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*\.spec\.ts$) plus an explicit .. rejection — everything else is dropped, not escaped. Six weaponized-path cases are unit-tested.
  • They reach the command as "${SPECS[@]}" read from a file, never interpolated into the command string.
  • permissions: is narrowed to contents: read + pull-requests: read; the checkout uses persist-credentials: false.
  • Trigger is pull_request, not pull_request_target, so fork code never runs with a privileged token or repo secrets.
  • No new dependency, no network access beyond the GitHub API and the existing install path.

Follow-up: none required for this change.

Notes

`e2e.yml` is reachable only from `schedule` and `release-cut`'s
`workflow_call`, so a PR that adds an E2E regression test never runs it. The
test runs for the first time on `main`, where nothing gates on it — twice now
a spec has landed and never passed.

Adds a `pull_request` job that runs exactly the specs the PR touched. A PR
that changes no spec pays nothing: the file list is resolved first, and every
setup/build/run step is gated on the result being non-empty.

- No `paths:` filter. The check reports on every PR, so it can be made
  required without wedging the merge box on PRs that touch no spec.
- Changed files come from the pulls API (paginated), not a git diff — the PR
  checkout is shallow and a deleted spec must not reach Playwright.
- Selected paths are argv for a shell-invoked command and a fork PR names its
  own files, so `select-changed-e2e-specs.mjs` allowlists
  `tests/e2e/**/*.spec.ts` with no shell metacharacters and no traversal, and
  the run step expands them from an array rather than the command string.
- `--pass-with-no-tests` keeps a spec whose tests are all `@headful` from
  failing the gate, since the `electron-headless` project filters them out.

Gating the full sharded suite on every PR stays a separate decision: it is 22
minutes of wall clock, and `main` is red today, so making it required would
block every merge until stablyai#10519 closes.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04660424-62a1-4b4b-ac06-7c660d226c98

📥 Commits

Reviewing files that changed from the base of the PR and between 00e379b and 272cdee.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .github/workflows/e2e-changed-specs.yml
  • config/scripts/e2e-changed-specs-workflow.test.mjs
  • config/scripts/select-changed-e2e-specs.mjs
  • package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • .github/workflows/e2e-changed-specs.yml
  • config/scripts/e2e-changed-specs-workflow.test.mjs
  • config/scripts/select-changed-e2e-specs.mjs

📝 Walkthrough

Walkthrough

Adds a GitHub Actions workflow that queries changed pull request files, selects matching E2E specs, conditionally builds and runs them, and uploads failure traces. Adds a selector CLI that validates, filters, deduplicates, and sorts spec paths from stdin. Adds Vitest coverage for selector behavior and workflow wiring, including triggers, API pagination, conditional steps, test command options, and artifact configuration.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: running E2E specs for PRs that modify them.
Description check ✅ Passed The description includes all required sections and gives a detailed summary, testing notes, review, security, and follow-up context.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f21bff3-18e8-435c-b517-4314021ba982

📥 Commits

Reviewing files that changed from the base of the PR and between a1a78da and 00e379b.

📒 Files selected for processing (3)
  • .github/workflows/e2e-changed-specs.yml
  • config/scripts/e2e-changed-specs-workflow.test.mjs
  • config/scripts/select-changed-e2e-specs.mjs

Comment thread .github/workflows/e2e-changed-specs.yml Outdated
Comment thread config/scripts/select-changed-e2e-specs.mjs
… selector

- Add node-gyp to devDependencies so the changed-specs workflow uses the
  pnpm-lock version instead of npm install -g.
- Remove .trim() on PR-controlled filenames before allowlist matching and add
  tests for leading/trailing whitespace rejection.
@MumuTW

MumuTW commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Closing this as superseded by #11131, which merged the upstream path-filtered PR E2E workflow and closed #10518. The changed-spec selector approach may still be useful as a future optimization, but it no longer needs to remain as a parallel implementation PR; it can be revived as a focused follow-up if maintainers want that direction.

@MumuTW MumuTW closed this Jul 29, 2026
@MumuTW
MumuTW deleted the ci/e2e-changed-specs-gate branch September 12, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants