Sharing Ownership of Cross-Team E2E Suites
An end-to-end suite crosses every team’s boundary by design — that is what makes it valuable and what makes it ownerless. A checkout journey touches the catalogue, the cart, payments and notifications, so when it fails, four teams each have a reasonable argument that it is not theirs. The suite ends up owned by whoever last complained, then by a platform team who did not write the tests, then by nobody. This guide covers splitting ownership by journey rather than by component, routing failures automatically, separating the shared harness from the journeys that use it, and keeping the arrangement honest when a failure genuinely spans teams. It sits under test ownership models.
Root Cause Analysis
The diffusion happens because an end-to-end test’s subject and its failure causes are at different granularities. The subject is a user journey, which is naturally owned by the team responsible for that journey’s outcome. The causes are distributed across every service the journey touches, so failure attribution does not follow ownership.
Teams respond to this reasonably and badly. The common outcome is to hand the whole suite to a platform or quality team, which puts the people least able to fix a failure in charge of triaging it: they can tell you the checkout test failed but not why the pricing rule changed. The suite then becomes a queue of tickets filed at other teams, with all the latency that implies.
The second failure is the opposite — declaring the suite “everyone’s responsibility”, which reliably means nobody’s. Without a named owner per test, a failure has no default assignee, and the default outcome of an unassigned failure is a retry.
Reproducible Setup
Make ownership explicit in two places: the file tree and the code owners file, so both humans and tooling can find it.
e2e/
journeys/
checkout/ owned by @acme/checkout
onboarding/ owned by @acme/growth
admin-billing/ owned by @acme/payments
harness/ owned by @acme/platform
fixtures/
helpers/
playwright.config.ts
# .github/CODEOWNERS
/e2e/harness/ @acme/platform
/e2e/journeys/checkout/ @acme/checkout
/e2e/journeys/onboarding/ @acme/growth
/e2e/journeys/admin-billing/ @acme/payments
Implementation
Step 1 — Own by journey, not by component. The team accountable for the journey’s business outcome owns its tests, even though the journey touches services they do not own. That is the point: it gives them a stake in the whole path their users take.
Step 2 — Tag each test with its owner in a machine-readable way. Directory ownership covers most cases; an annotation covers the rest and survives a file being moved.
// e2e/journeys/checkout/card-payment.spec.ts
import { test, expect } from '../../harness/fixtures';
test.describe('checkout: card payment', { tag: ['@team-checkout', '@critical'] }, () => {
test('a returning customer completes a card purchase', async ({ page, customer }) => {
// …
});
});
Step 3 — Route failures to the owner automatically. A failure with a default assignee gets investigated; one without gets retried.
// scripts/e2e/route-failures.ts
import { readFileSync } from 'node:fs';
const OWNERS: Record<string, string> = {
'@team-checkout': '@acme/checkout',
'@team-growth': '@acme/growth',
'@team-payments': '@acme/payments',
};
const report = JSON.parse(readFileSync('reports/playwright-results.json', 'utf8'));
const failures = collectFailures(report); // title, tags, file, error
const byTeam = new Map<string, typeof failures>();
for (const f of failures) {
const tag = f.tags.find((t: string) => t in OWNERS) ?? '@team-unassigned';
byTeam.set(OWNERS[tag] ?? '@acme/platform', [...(byTeam.get(OWNERS[tag]) ?? []), f]);
}
for (const [team, items] of byTeam) {
console.log(`::error title=${team}::${items.length} end-to-end failure(s) in your journeys`);
}
Step 4 — Give the harness a single custodian. Fixtures, helpers, configuration and the CI wiring are infrastructure: one team owns them, and journey teams consume them without modifying them.
// e2e/harness/fixtures.ts — owned by platform, used by every journey
import { test as base } from '@playwright/test';
export const test = base.extend<{ customer: Customer; org: Org }>({
customer: async ({}, use, info) => {
const c = await createCustomer(`t-${info.workerIndex}`);
await use(c);
await deleteCustomer(c.id);
},
});
export { expect } from '@playwright/test';
Step 5 — Define what happens when the cause is elsewhere. The journey owner investigates first because they know the expected behaviour; when the cause turns out to be another team’s, they hand over with the evidence rather than filing a bare ticket.
## Cross-team handover
The journey owner triages first and, if the cause is another team's, hands over with:
- the failing trace, linked
- the request and response that differed, from the trace
- the commit range in the other service, if known
The receiving team owns it from that point. The journey owner keeps the test.
There is a second reason to keep this boundary sharp. A journey team that can edit the harness will, sooner or later, adjust a shared fixture to make their own test pass — and that adjustment lands silently in every other team’s suite. Requiring harness changes to go through their custodian is not bureaucracy; it is the same argument as for any shared library, where the cost of a change is paid by people who are not in the room when it is made.
// a change like this belongs in the harness, reviewed by its custodian
export const test = base.extend<{ customer: Customer }>({
customer: async ({}, use, info) => {
// raising the default trial length here affects every journey's assumptions
const c = await createCustomer(`t-${info.workerIndex}`, { trialDays: 30 });
await use(c);
await deleteCustomer(c.id);
},
});
Step 6 — Review unassigned failures rather than letting them settle. Anything routed to the platform fallback is either a harness problem or a missing tag, and both are worth fixing quickly.
Verification
Verify every test has an owner, mechanically. An unowned test is one that will be retried rather than fixed.
npx playwright test --list --reporter=json \
| jq -r '.suites[] | .. | .title? // empty' > /tmp/titles.txt
grep -rLE "@team-(checkout|growth|payments|platform)" e2e/journeys --include="*.spec.ts"
# (no output — every spec file carries a team tag)
Then verify the routing works by breaking a journey deliberately on a branch and confirming the right team is named in the job summary, not the platform fallback.
npx tsx scripts/e2e/route-failures.ts
# ::error title=@acme/checkout::1 end-to-end failure(s) in your journeys
Finally, verify that ownership is having an effect rather than merely being recorded. Compare time-to-resolution before and after; if failures still sit for days, the routing is reaching a channel nobody reads, which is a delivery problem rather than an ownership one.
Troubleshooting
Symptom: journey teams resist owning tests that fail for other teams’ reasons. Diagnosis: a fair objection, and the answer is the handover rule from Step 5 — they own triage and the test, not every fix. Fix: make the handover explicit and lightweight, and measure how often it is used; if a journey is handed over constantly, its dependencies may deserve their own contract tests instead, as covered in contract testing.
Symptom: the platform team is still triaging everything. Diagnosis: routing exists but notifications reach a shared channel rather than the owning team. Fix: route to the team’s own channel or reviewer group, and make the platform fallback visibly exceptional so it prompts a fix rather than becoming the norm.
Symptom: journey directories have drifted from the real team structure. Diagnosis: a reorganisation happened and the code owners file did not follow. Fix: review ownership at the same cadence as the health review, and treat an entry pointing at a team that no longer exists as a build warning rather than a documentation issue.
Symptom: teams change the harness to work around it. Diagnosis: the custodian is a bottleneck, so consumers route around them. Fix: make harness changes a normal reviewed contribution rather than a request, with the custodian as reviewer; custodianship should mean responsibility for coherence, not a queue.
FAQ
Should a platform team own the whole end-to-end suite?
Only the harness. A platform team owning the journeys ends with people who cannot judge expected behaviour triaging failures about business rules, which is slow and demoralising. Custodianship of the shared machinery is a genuinely useful role; ownership of everyone’s journeys is not.
What if a journey has no obvious owner?
That is usually a signal about the journey rather than the test — a flow nobody is accountable for is a risk in itself. In the short term assign it to the team closest to the outcome; in the longer term, raise it, because an unowned user journey will have unowned defects too.
How does this interact with quarantining?
Ownership determines who decides. A quarantined test needs an owner to hold its deadline and to make the promote-or-retire call, which is exactly what quarantining flaky tests in CI requires. Without journey ownership, quarantine becomes the permanent home it is meant to avoid.
Does this apply to a single-team codebase?
The harness-versus-journey split still helps, even with one owner for both, because it separates changes that affect everything from changes that affect one flow. What you can skip is the routing and the handover protocol, which exist to cross boundaries you do not have.
Related
- Back to Test Ownership Models
- CODEOWNERS-driven test ownership in CI — the mechanism this arrangement relies on.
- End-to-End Test Architecture — the harness the custodian owns.
- Running a test health review cadence — where unassigned failures get resolved.