Runs the Playwright e2e suites for a pull request in iblai/os, iblai/lms and
iblai/iblai-web-frontend, by dispatching the central pipeline in iblai/iblai-deploy-ops,
streaming its progress back into the caller's log, and publishing the report link onto the PR.
A pull_request event runs workflows from the PR's merge ref. A workflow copied into each repo
is therefore frozen per PR: a fix reaches no open PR until that PR rebases.
On 2026-08-04 that meant a merged fix reached none of ~27 open PRs, and every bug needed three separate PRs (os + lms + web-frontend) plus a rebase on each open PR to take effect.
uses: iblai/pr-e2e-action@v1 is resolved at run time from this repo, so a fix tagged v1
applies to every PR immediately — including PRs opened months ago, with no rebase.
e2e:
name: E2E (central pipeline)
needs: build-app-image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # needed to diff changed spec files
- uses: iblai/pr-e2e-action@v1
with:
source_repo: os # os | lms | web-frontend
pr_number: ${{ github.event.pull_request.number }}
merge_sha: ${{ github.sha }}
head_sha: ${{ github.event.pull_request.head.sha }}
base_sha: ${{ github.event.pull_request.base.sha }}
app_image: ${{ needs.build-app-image.outputs.image-uri }}
dispatch_token: ${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}| input | required | default | notes |
|---|---|---|---|
source_repo |
yes | — | selects platform + suite in the central pipeline |
pr_number |
yes | — | |
merge_sha |
yes | — | github.sha on a pull_request is the merge commit; the app image and the suite must come from the same one |
head_sha |
yes | — | branch tip |
base_sha |
yes | — | used to diff changed specs |
app_image |
yes | — | SPA image built from merge_sha |
mode |
no | auto |
auto | full | selective |
specs |
no | '' |
space-separated spec files; with mode=selective this wins over auto-detection |
dispatch_token |
yes | — | PAT with Actions: write on iblai/iblai-deploy-ops |
repo_token |
yes | — | GITHUB_TOKEN; only used to write the PR's sticky comment |
conclusion — the central run's conclusion · run_id — the dispatched run id
This action does not choose, and deliberately cannot. It sends no target_env; the central
pr-e2e.yml resolves ${{ vars.PR_E2E_ENV || inputs.target_env }} — one variable in
iblai/iblai-deploy-ops, consulted by every consumer (concurrency, environment, the runner
lookup, the report path, run-name).
Earlier versions probed the runners here and picked stg1 or stg2. That gave the caller a requested env that a central override could silently contradict, which was the precondition for both 2026-08-04 incidents — a live-lock and a bug that abandoned four healthy runs. With one source of truth the two values cannot disagree, because there is only one.
Operators move all PR traffic in one command, with no PR and no rebase:
gh variable set PR_E2E_ENV --body stg2 -R iblai/iblai-deploy-ops
gh variable delete PR_E2E_ENV -R iblai/iblai-deploy-ops # back to the stg1 defaultA second action in this repo handles PR comments, so nobody has to remember the label ritual:
| command | what it does |
|---|---|
/retest |
re-run the full suite |
/retest <spec> |
re-run only matching spec files — minutes instead of ~45 |
/status |
queue position and what is running |
/report |
link to the latest report |
/cancel |
stop this PR's run and free the environment |
<spec> is a substring, so /retest 34 and /retest 34-workflows both resolve to
e2e/journeys/34-workflows.spec.ts. Several are allowed. An unmatched token starts nothing and
says so, rather than silently running everything.
The handler's only trigger action is remove-then-add of run-tests. It never calls the central
pipeline directly. Two reasons, and the second is the one that bites:
- there is a single trigger path to reason about and secure, not two;
- a PR's required check only updates when the caller workflow runs. A command that produced a report without refreshing the check would look like it worked while leaving the PR unmergeable.
issue_comment workflows always run from the default branch, never from the PR — so a fork PR
cannot edit the handler to weaken its own checks. On top of that the action requires
write/maintain/admin via repos/{repo}/collaborators/{user}/permission, and refuses fork PRs.
Authorization deliberately does not use author_association: it reports MEMBER for any org
member, including people with no write access to the repo, who would then be able to occupy a
shared test environment.
The handler writes a marker comment and recycles the label. The root action reads that marker at the start of the run and honours it only if both hold:
- it was authored by
github-actions[bot]— a user cannot post as the bot, so hand-writing the comment cannot bypass the permission check; and - its
head_shamatches the commit under test — otherwise a selective request made against an earlier commit would silently narrow a later push's run to a few files, leaving the rest of the suite untested while the required check went green.
Either check failing falls back to normal auto-detection. A malformed marker is ignored.
Most failures are a single flaky spec, and re-running all 621 tests to re-check one is what makes the shared-environment queue back up. Pass an explicit spec list to run just that file:
mode: selective
specs: e2e/journeys/34-workflows.spec.tsRoughly 3–5 minutes instead of ~45. Auto-detection still applies when mode: auto: a PR that
touches only spec files (≤5) runs just those; anything touching app source runs the full suite.
A composite action is terminated when the job is cancelled, so an if: cancelled() step inside
this action would never run. Cleanup must therefore be a job step in the calling workflow:
- uses: iblai/pr-e2e-action@v1
id: e2e
with: { ... }
- name: Cancel the dispatched central run if this caller is cancelled
if: cancelled()
env: { GH_TOKEN: "${{ secrets.DEPLOY_OPS_DISPATCH_TOKEN }}" }
run: |
F="$RUNNER_TEMP/pr-e2e-run-id"
[ -s "$F" ] || { echo "nothing to cancel"; exit 0; }
gh run cancel "$(cat "$F")" -R iblai/iblai-deploy-ops || trueThis matters on the routine path, not an edge case: callers use cancel-in-progress: true, and a
label recycle fires two events, so the first caller is cancelled on essentially every re-tag.
Without this the dispatched central run orphans — holding an environment for a full suite, producing
a result nobody reads, and sometimes passing while the PR shows red.
The run id comes from $RUNNER_TEMP/pr-e2e-run-id, which this action writes the moment it
dispatches — not from steps.e2e.outputs.run_id. A composite action's declared outputs are only
published when it completes, and by definition it has not completed when cancelled.
v1.0.0,v1.1.0, … are immutablev1is a moving tag — this is what callers pin, and what makes fixes propagate- To roll back: re-point
v1at the previous immutable tag
Callers pin @v1 deliberately. That means a defect here affects all three repos at once — the
trade is that recovery is one commit and a tag move, rather than three PRs and a rebase per open PR.
No secrets, and no deploy logic. It dispatches and reports; all deployment, environment mutation and
test execution stay in the private iblai/iblai-deploy-ops. Runner labels, environment names and
that repo's name were already public in os's workflow before this action existed.