@biffo/cli 0.233.0 → 0.234.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -103,6 +103,51 @@ jobs:
103
103
  # so it cannot assume a layout. Do not edit it here; edit it upstream.
104
104
  run: sh ../../scripts/js-dependency-audit.sh
105
105
 
106
+ e2e:
107
+ name: E2E (Playwright)
108
+ runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
109
+ timeout-minutes: 20
110
+ # `packages: read` so pnpm can install any private @tabsii-com/* package
111
+ # from GitHub Packages using the workflow token. Harmless if this sibling
112
+ # has none.
113
+ permissions:
114
+ contents: read
115
+ packages: read
116
+ # The official Playwright image, pinned to the @playwright/test version in
117
+ # apps/frontend/package.json. It ships Chromium and every browser system
118
+ # dependency, so this needs no `--with-deps`, no apt, and no root on the
119
+ # self-hosted runners.
120
+ container:
121
+ image: mcr.microsoft.com/playwright:v1.61.1-noble
122
+ defaults:
123
+ run:
124
+ working-directory: apps/frontend
125
+ steps:
126
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
127
+ - uses: pnpm/action-setup@v4
128
+ with:
129
+ version: 9.15.9
130
+ - uses: actions/setup-node@v4
131
+ with:
132
+ node-version: ${{ env.NODE_VERSION }}
133
+ cache: pnpm
134
+ cache-dependency-path: apps/frontend/pnpm-lock.yaml
135
+ - run: pnpm install --frozen-lockfile
136
+ env:
137
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138
+ # Browser system deps come from the container; this only drops the
139
+ # Chromium binary into the resolved cache path (a no-op if present).
140
+ - name: Install Chromium
141
+ run: pnpm exec playwright install chromium
142
+ # `playwright test` boots its own webServer: it builds the static export
143
+ # and serves it base-path aware, INCLUDING
144
+ # `/.well-known/biffo-identity.json`. Serving that document is what makes
145
+ # the harness represent the deployed app -- without it the runtime pool
146
+ # lookup (#403) resolves null and every spec dies on a redirect to the
147
+ # portal, which is how tabsii-geo's CI broke for four hours (#1208).
148
+ - name: E2E tests
149
+ run: pnpm run e2e
150
+
106
151
  python:
107
152
  name: Python (lint, types, test, security)
108
153
  runs-on: ${{ vars.RUNNER_LABEL || 'ubuntu-latest' }}
@@ -0,0 +1,44 @@
1
+ // Shared between playwright.config.ts (which builds and serves the static
2
+ // export) and the specs.
3
+ //
4
+ // Every value here is read by BOTH sides, which is the point: the Cognito ids
5
+ // the harness advertises in `/.well-known/biffo-identity.json` must be the same
6
+ // ones `session.ts` seeds a localStorage session with, or the app resolves a
7
+ // pool it has no session for and silently redirects to the portal. That exact
8
+ // mismatch broke tabsii-geo's CI for four hours (#1208).
9
+
10
+ export const E2E_PORT = Number(process.env['E2E_PORT'] ?? 4330)
11
+
12
+ // The path CloudFront routes to this sibling, e.g. `/crm`. Read from the same
13
+ // env var `next.config.ts` uses, so the harness cannot disagree with the build.
14
+ export const BASE_PATH = process.env['NEXT_PUBLIC_BASE_PATH'] ?? ''
15
+
16
+ // Not real credentials, and never used against a real pool — they only have to
17
+ // be internally consistent. Shaped like the real thing so anything that parses
18
+ // a pool id does not choke.
19
+ export const COGNITO_USER_POOL_ID = 'us-east-1_E2ETESTPOOL'
20
+ export const COGNITO_CLIENT_ID = 'e2etestclientid0123456789'
21
+
22
+ // Injected into `next build` by the Playwright webServer.
23
+ //
24
+ // Deliberately NO `NEXT_PUBLIC_CORE_COGNITO_*`: a sibling resolves its pool at
25
+ // RUNTIME from the identity document (#403), exactly as the deployed app does.
26
+ // Baking a build-time fallback here would make the harness pass in a way the
27
+ // real app cannot, which is the failure this scaffold exists to prevent.
28
+ export const BUILD_ENV: Record<string, string> = {
29
+ NEXT_PUBLIC_BASE_PATH: BASE_PATH,
30
+ NEXT_PUBLIC_E2E: '1',
31
+ NEXT_PUBLIC_API_URL: '',
32
+ // LOAD-BEARING, and not obviously so. `src/app/page.tsx` only redirects a
33
+ // signed-out visitor `if (state.kind === 'signed_out' && CORE_PORTAL_URL)`.
34
+ // Leave this unset and the app has nowhere to send them, so it never
35
+ // redirects, so `smoke.spec.ts` passes whatever the auth wiring does —
36
+ // including with the identity document removed entirely.
37
+ //
38
+ // That is not hypothetical: the first draft of this scaffold omitted it, and
39
+ // the smoke test passed against a harness deliberately broken the same way
40
+ // tabsii-geo's was (#1208). A test that cannot fail is worse than no test,
41
+ // because it reports coverage. Verified after adding it: breaking the
42
+ // identity document makes the spec fail on the redirect.
43
+ NEXT_PUBLIC_CORE_PORTAL_URL: 'https://portal.example.test',
44
+ }
@@ -0,0 +1,90 @@
1
+ // Put the browser in a signed-in state without touching Cognito (crm#65 M1).
2
+ //
3
+ // `src/lib/auth.ts` reads its session through amazon-cognito-identity-js, whose
4
+ // storage keys are derived from the **Client ID only** — deliberately, so the
5
+ // portal's session carries over to this sibling on the same origin (ADR-0007;
6
+ // see the comment in auth.ts). `fixtures.ts`'s `COGNITO_CLIENT_ID` pins that
7
+ // Client ID to a fixed test value, so the key names are deterministic and can
8
+ // be written before first paint — and it is the SAME value
9
+ // `static-server.mjs` advertises in the `/.well-known/biffo-identity.json`
10
+ // document auth.ts now resolves the pool from at runtime (#403), so the pool
11
+ // this seeded session's keys are stored under is the one the app actually
12
+ // builds.
13
+ //
14
+ // Once the pool resolves, the library validates the cached session
15
+ // **locally**: `getCurrentUser()` reads `LastAuthUser`, `getSession()` reads
16
+ // the three token keys, and `CognitoUserSession.isValid()` compares
17
+ // `payload.exp` against now. Nothing is verified cryptographically, so an
18
+ // unsigned JWT with a future `exp` is sufficient.
19
+ //
20
+ // Deliberately NOT a second test-only route. The map harness
21
+ // (`src/app/e2e/map/page.tsx`) mounts a component in isolation, which is the
22
+ // gap #65 exists to close — these specs drive the real `/crm/` app shell.
23
+
24
+ import type { Page } from '@playwright/test'
25
+ import { COGNITO_CLIENT_ID } from './fixtures'
26
+
27
+ const USERNAME = 'user@e2e.test'
28
+
29
+ /** base64url, without padding — the JWT segment encoding. */
30
+ function b64url(value: string): string {
31
+ return Buffer.from(value)
32
+ .toString('base64')
33
+ .replace(/\+/g, '-')
34
+ .replace(/\//g, '_')
35
+ .replace(/=+$/, '')
36
+ }
37
+
38
+ /**
39
+ * A structurally valid, unsigned JWT.
40
+ *
41
+ * The signature segment is the literal `e2e` — it is never checked here, and
42
+ * making it obviously fake is the point: nothing in this file should look like
43
+ * a credential that might work somewhere real.
44
+ */
45
+ function token(extraClaims: Record<string, unknown>): string {
46
+ const now = Math.floor(Date.now() / 1000)
47
+ const payload = {
48
+ sub: 'e2e-sub',
49
+ email: USERNAME,
50
+ 'cognito:username': USERNAME,
51
+ aud: COGNITO_CLIENT_ID,
52
+ token_use: 'id',
53
+ auth_time: now,
54
+ iat: now,
55
+ exp: now + 60 * 60,
56
+ ...extraClaims,
57
+ }
58
+ return `${b64url(JSON.stringify({ alg: 'none', typ: 'JWT' }))}.${b64url(JSON.stringify(payload))}.e2e`
59
+ }
60
+
61
+ /**
62
+ * Seed the Cognito session keys before any app code runs.
63
+ *
64
+ * `addInitScript` runs on every navigation in the context, ahead of the page's
65
+ * own scripts — which matters because `page.tsx` calls `getCurrentSession()` in
66
+ * its first effect, and that now resolves the pool asynchronously (a fetch to
67
+ * `/.well-known/biffo-identity.json`, #403) before it ever reads localStorage.
68
+ * Writing localStorage after `goto` would race that and land on the signed-out
69
+ * screen intermittently.
70
+ */
71
+ export async function signIn(page: Page): Promise<void> {
72
+ const prefix = `CognitoIdentityServiceProvider.${COGNITO_CLIENT_ID}`
73
+ const entries: Record<string, string> = {
74
+ [`${prefix}.LastAuthUser`]: USERNAME,
75
+ [`${prefix}.${USERNAME}.idToken`]: token({ token_use: 'id' }),
76
+ [`${prefix}.${USERNAME}.accessToken`]: token({ token_use: 'access' }),
77
+ // Present but unused: `getSession` only reaches for it when the cached
78
+ // session has expired, which it has not.
79
+ [`${prefix}.${USERNAME}.refreshToken`]: 'e2e-refresh',
80
+ [`${prefix}.${USERNAME}.clockDrift`]: '0',
81
+ }
82
+
83
+ await page.addInitScript((items: Record<string, string>) => {
84
+ for (const [k, v] of Object.entries(items)) window.localStorage.setItem(k, v)
85
+ }, entries)
86
+ }
87
+
88
+ /** Every request the app has made to the fixture API, in order. */
89
+
90
+ /** Drop recorded requests and any units created by an earlier test. */
@@ -0,0 +1,51 @@
1
+ import { expect, test } from '@playwright/test'
2
+ import { BASE_PATH } from './fixtures'
3
+ import { signIn } from './session'
4
+
5
+ /**
6
+ * The one E2E every sibling starts with: **the app boots, and a signed-in
7
+ * session stays signed in.**
8
+ *
9
+ * ## Why this specific assertion
10
+ *
11
+ * It is the regression that actually happened. On 2026-08-03 `tabsii-geo`
12
+ * merged a change moving Cognito pool resolution to runtime (#403), its
13
+ * Playwright harness did not serve `/.well-known/biffo-identity.json`, so
14
+ * `getCurrentSession()` resolved null and the app redirected to the portal.
15
+ * Every spec died on *"Execution context was destroyed, most likely because of
16
+ * a navigation"*. `tabsii-crm` needed the identical fix for the identical
17
+ * reason. The branch stayed red for four hours (#1208).
18
+ *
19
+ * A sibling that renders its own base path while holding a session is a sibling
20
+ * whose build, base path, static export, identity document and auth wiring all
21
+ * agree. Almost every scaffolding mistake breaks one of those, and this catches
22
+ * it in about a second.
23
+ *
24
+ * ## Keep it, then add to it
25
+ *
26
+ * This is a floor, not a suite. Add specs for what your sibling actually does —
27
+ * `tabsii-crm` has brand, FDD and automation flows beside this. But do not
28
+ * delete it: it is the only test that fails loudly when the harness itself
29
+ * stops representing the deployed app, which is the failure mode that costs
30
+ * hours rather than minutes.
31
+ *
32
+ * **Make `E2E (Playwright)` a required status check on `dev`.** `tabsii-geo`
33
+ * had five E2E files and did not require them, so the regression above merged
34
+ * green — coverage that cannot block a merge is documentation.
35
+ */
36
+
37
+ test('a signed-in session renders the app without redirecting to the portal', async ({ page }) => {
38
+ // Seed the session BEFORE the first navigation. `identity.ts` fetches the
39
+ // identity document and reads localStorage during hydration, so writing the
40
+ // session after `goto` races that and lands on the signed-out path.
41
+ await signIn(page)
42
+
43
+ await page.goto(`${BASE_PATH}/`)
44
+
45
+ // Still here — not bounced to the portal.
46
+ await expect(page).toHaveURL(new RegExp(`${BASE_PATH}/?$`))
47
+
48
+ // And something actually rendered, rather than an empty shell that merely
49
+ // failed to redirect.
50
+ await expect(page.locator('body')).not.toBeEmpty()
51
+ })
@@ -0,0 +1,122 @@
1
+ // Minimal static file server for the Next.js static export (output: 'export').
2
+ //
3
+ // The export is built with the sibling's basePath, so every emitted asset URL is
4
+ // prefixed with /crm — but the files themselves live at the `out/` ROOT (Next
5
+ // does not nest the export under the basePath directory). In production the
6
+ // parent CloudFront distribution syncs `out/` into the sibling's S3 prefix; here we
7
+ // reproduce the same effect by stripping the leading the base path from each request
8
+ // path before resolving it against `out/`.
9
+ import { createServer } from 'node:http'
10
+
11
+ import { readFile, stat } from 'node:fs/promises'
12
+ import { join, extname, normalize } from 'node:path'
13
+ import { fileURLToPath } from 'node:url'
14
+
15
+ const ROOT = fileURLToPath(new URL('../out/', import.meta.url))
16
+ const BASE_PATH = process.env.E2E_BASE_PATH || process.env.NEXT_PUBLIC_BASE_PATH || ''
17
+ const PORT = Number(process.env.E2E_PORT || 4321)
18
+
19
+ // The document the CORE portal publishes at runtime (#403/#400) — auth.ts's
20
+ // identity.ts resolves the Cognito pool from this instead of a baked
21
+ // NEXT_PUBLIC_CORE_COGNITO_* build var. Real deployments serve it same-origin
22
+ // from the portal's own bucket; here it stands in for that, sourced from the
23
+ // SAME E2E_COGNITO_* values playwright.config.ts derives from e2e/fixtures.ts,
24
+ // so the pool/client id this document advertises always matches the one
25
+ // e2e/session.ts used to seed the localStorage session.
26
+ const IDENTITY_DOCUMENT_PATH = '/.well-known/biffo-identity.json'
27
+ const IDENTITY_DOCUMENT = JSON.stringify({
28
+ userPoolId: process.env.E2E_COGNITO_USER_POOL_ID || '',
29
+ clientId: process.env.E2E_COGNITO_CLIENT_ID || '',
30
+ })
31
+
32
+ const CONTENT_TYPES = {
33
+ '.html': 'text/html; charset=utf-8',
34
+ '.js': 'text/javascript; charset=utf-8',
35
+ '.mjs': 'text/javascript; charset=utf-8',
36
+ '.css': 'text/css; charset=utf-8',
37
+ '.json': 'application/json; charset=utf-8',
38
+ '.geojson': 'application/json; charset=utf-8',
39
+ '.svg': 'image/svg+xml',
40
+ '.png': 'image/png',
41
+ '.jpg': 'image/jpeg',
42
+ '.ico': 'image/x-icon',
43
+ '.txt': 'text/plain; charset=utf-8',
44
+ '.woff2': 'font/woff2',
45
+ '.woff': 'font/woff',
46
+ '.map': 'application/json; charset=utf-8',
47
+ }
48
+
49
+ async function resolveFile(urlPath) {
50
+ // Strip the sibling base path: the files live at out/ root.
51
+ let p = urlPath
52
+ if (p === BASE_PATH) p = '/'
53
+ else if (p.startsWith(`${BASE_PATH}/`)) p = p.slice(BASE_PATH.length)
54
+ if (p.endsWith('/')) p += 'index.html'
55
+
56
+ // Block path traversal, then resolve against ROOT.
57
+ const rel = normalize(p).replace(/^(\.\.[/\\])+/, '')
58
+ let filePath = join(ROOT, rel)
59
+
60
+ let s = await stat(filePath).catch(() => null)
61
+ if (s?.isDirectory()) {
62
+ filePath = join(filePath, 'index.html')
63
+ s = await stat(filePath).catch(() => null)
64
+ }
65
+ if (!s) {
66
+ // Extensionless route (trailingSlash export also emits foo/index.html).
67
+ const alt = `${filePath}.html`
68
+ if (await stat(alt).catch(() => null)) return alt
69
+ return null
70
+ }
71
+ return filePath
72
+ }
73
+
74
+ /** Read a JSON request body, tolerating an empty one. */
75
+
76
+ const server = createServer((req, res) => {
77
+ void (async () => {
78
+ try {
79
+ const url = new URL(req.url, 'http://localhost')
80
+ const urlPath = decodeURIComponent(url.pathname)
81
+
82
+ // The core identity document, served ROOT-relative and same-origin —
83
+ // NOT under BASE_PATH, matching how the real portal publishes it and
84
+ // how identity.ts fetches it (an absolute `/.well-known/...` path).
85
+ if (urlPath === IDENTITY_DOCUMENT_PATH) {
86
+ res.writeHead(200, {
87
+ 'content-type': 'application/json; charset=utf-8',
88
+ 'cache-control': 'no-store',
89
+ })
90
+ res.end(IDENTITY_DOCUMENT)
91
+ return
92
+ }
93
+
94
+ // Where a sibling's Core-API fixtures go, if it needs them. Under E2E
95
+ // `NEXT_PUBLIC_API_URL` is '', so the app's Core-API calls are
96
+ // same-origin relative paths and arrive here. Add an `api-fixtures.mjs`
97
+ // beside this file and dispatch to it before the static fallback below —
98
+ // tabsii-crm's is the worked example. A sibling with no BFF calls needs
99
+ // nothing here.
100
+
101
+ const filePath = await resolveFile(urlPath)
102
+ if (!filePath) {
103
+ res.writeHead(404, { 'content-type': 'text/plain' })
104
+ res.end('Not found')
105
+ return
106
+ }
107
+ const body = await readFile(filePath)
108
+ res.writeHead(200, {
109
+ 'content-type': CONTENT_TYPES[extname(filePath)] || 'application/octet-stream',
110
+ 'cache-control': 'no-store',
111
+ })
112
+ res.end(body)
113
+ } catch (err) {
114
+ res.writeHead(500, { 'content-type': 'text/plain' })
115
+ res.end(String(err))
116
+ }
117
+ })()
118
+ })
119
+
120
+ server.listen(PORT, '127.0.0.1', () => {
121
+ process.stdout.write(`static-server: http://127.0.0.1:${PORT}${BASE_PATH}/ (root: ${ROOT})\n`)
122
+ })
@@ -9,7 +9,8 @@
9
9
  "lint": "eslint .",
10
10
  "lint:fix": "eslint . --fix",
11
11
  "typecheck": "tsc --noEmit",
12
- "test": "vitest run"
12
+ "test": "vitest run",
13
+ "e2e": "playwright test"
13
14
  },
14
15
  "dependencies": {
15
16
  "amazon-cognito-identity-js": "^6.3.12",
@@ -19,6 +20,7 @@
19
20
  },
20
21
  "devDependencies": {
21
22
  "@eslint/eslintrc": "^3.2.0",
23
+ "@playwright/test": "^1.61.1",
22
24
  "@testing-library/dom": "^10.4.1",
23
25
  "@testing-library/jest-dom": "^6.9.1",
24
26
  "@testing-library/react": "^16.3.2",
@@ -0,0 +1,45 @@
1
+ import { defineConfig, devices } from '@playwright/test'
2
+ import {
3
+ BASE_PATH,
4
+ BUILD_ENV,
5
+ COGNITO_CLIENT_ID,
6
+ COGNITO_USER_POOL_ID,
7
+ E2E_PORT,
8
+ } from './e2e/fixtures'
9
+
10
+ // Browser E2E harness for this sibling's static-export frontend.
11
+ //
12
+ // The webServer builds the export and serves it base-path aware on a local
13
+ // port, including `/.well-known/biffo-identity.json` — the document the app
14
+ // resolves its Cognito pool from at runtime (#403). Serving it is not optional:
15
+ // without it `getCurrentSession()` resolves null, the app redirects to the
16
+ // portal, and every spec dies on a navigation. That is precisely how
17
+ // tabsii-geo's CI broke for four hours (#1208).
18
+ const baseURL = `http://127.0.0.1:${E2E_PORT}`
19
+ const isCI = !!process.env['CI']
20
+
21
+ export default defineConfig({
22
+ testDir: './e2e',
23
+ // Only *.spec.ts are tests; fixtures.ts / session.ts / static-server.mjs are helpers.
24
+ testMatch: /.*\.spec\.ts/,
25
+ fullyParallel: true,
26
+ forbidOnly: isCI,
27
+ retries: isCI ? 1 : 0,
28
+ ...(isCI ? { workers: 1 } : {}),
29
+ reporter: isCI ? [['github'], ['list']] : 'list',
30
+ use: { baseURL, trace: 'on-first-retry' },
31
+ projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
32
+ webServer: {
33
+ command: 'pnpm run build && node e2e/static-server.mjs',
34
+ url: `${baseURL}${BASE_PATH}/`,
35
+ reuseExistingServer: !isCI,
36
+ timeout: 180_000,
37
+ env: {
38
+ ...BUILD_ENV,
39
+ E2E_PORT: String(E2E_PORT),
40
+ E2E_BASE_PATH: BASE_PATH,
41
+ E2E_COGNITO_USER_POOL_ID: COGNITO_USER_POOL_ID,
42
+ E2E_COGNITO_CLIENT_ID: COGNITO_CLIENT_ID,
43
+ },
44
+ },
45
+ })
@@ -18,7 +18,7 @@ importers:
18
18
  version: 6.3.18
19
19
  next:
20
20
  specifier: ^15.5.22
21
- version: 15.5.22(@babel/core@7.29.7)(@types/node@22.20.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
21
+ version: 15.5.22(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
22
22
  react:
23
23
  specifier: ^19.0.0
24
24
  version: 19.2.7
@@ -29,6 +29,9 @@ importers:
29
29
  '@eslint/eslintrc':
30
30
  specifier: ^3.2.0
31
31
  version: 3.3.5
32
+ '@playwright/test':
33
+ specifier: ^1.61.1
34
+ version: 1.62.1
32
35
  '@testing-library/dom':
33
36
  specifier: ^10.4.1
34
37
  version: 10.4.1
@@ -705,6 +708,11 @@ packages:
705
708
  resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
706
709
  engines: {node: '>=12.4.0'}
707
710
 
711
+ '@playwright/test@1.62.1':
712
+ resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==}
713
+ engines: {node: '>=20'}
714
+ hasBin: true
715
+
708
716
  '@rolldown/pluginutils@1.0.0-beta.27':
709
717
  resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
710
718
 
@@ -1610,6 +1618,11 @@ packages:
1610
1618
  resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
1611
1619
  engines: {node: '>= 0.4'}
1612
1620
 
1621
+ fsevents@2.3.2:
1622
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
1623
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
1624
+ os: [darwin]
1625
+
1613
1626
  fsevents@2.3.3:
1614
1627
  resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
1615
1628
  engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -2104,6 +2117,16 @@ packages:
2104
2117
  resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==}
2105
2118
  engines: {node: '>=12'}
2106
2119
 
2120
+ playwright-core@1.62.1:
2121
+ resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==}
2122
+ engines: {node: '>=20'}
2123
+ hasBin: true
2124
+
2125
+ playwright@1.62.1:
2126
+ resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==}
2127
+ engines: {node: '>=20'}
2128
+ hasBin: true
2129
+
2107
2130
  possible-typed-array-names@1.1.0:
2108
2131
  resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
2109
2132
  engines: {node: '>= 0.4'}
@@ -3123,6 +3146,10 @@ snapshots:
3123
3146
 
3124
3147
  '@nolyfill/is-core-module@1.0.39': {}
3125
3148
 
3149
+ '@playwright/test@1.62.1':
3150
+ dependencies:
3151
+ playwright: 1.62.1
3152
+
3126
3153
  '@rolldown/pluginutils@1.0.0-beta.27': {}
3127
3154
 
3128
3155
  '@rollup/rollup-android-arm-eabi@4.62.2':
@@ -4188,6 +4215,9 @@ snapshots:
4188
4215
  dependencies:
4189
4216
  is-callable: 1.2.7
4190
4217
 
4218
+ fsevents@2.3.2:
4219
+ optional: true
4220
+
4191
4221
  fsevents@2.3.3:
4192
4222
  optional: true
4193
4223
 
@@ -4568,7 +4598,7 @@ snapshots:
4568
4598
 
4569
4599
  natural-compare@1.4.0: {}
4570
4600
 
4571
- next@15.5.22(@babel/core@7.29.7)(@types/node@22.20.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
4601
+ next@15.5.22(@babel/core@7.29.7)(@playwright/test@1.62.1)(@types/node@22.20.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
4572
4602
  dependencies:
4573
4603
  '@next/env': 15.5.22
4574
4604
  '@swc/helpers': 0.5.15
@@ -4586,6 +4616,7 @@ snapshots:
4586
4616
  '@next/swc-linux-x64-musl': 15.5.22
4587
4617
  '@next/swc-win32-arm64-msvc': 15.5.22
4588
4618
  '@next/swc-win32-x64-msvc': 15.5.22
4619
+ '@playwright/test': 1.62.1
4589
4620
  sharp: 0.35.3(@types/node@22.20.0)
4590
4621
  transitivePeerDependencies:
4591
4622
  - '@babel/core'
@@ -4694,6 +4725,14 @@ snapshots:
4694
4725
 
4695
4726
  picomatch@4.0.5: {}
4696
4727
 
4728
+ playwright-core@1.62.1: {}
4729
+
4730
+ playwright@1.62.1:
4731
+ dependencies:
4732
+ playwright-core: 1.62.1
4733
+ optionalDependencies:
4734
+ fsevents: 2.3.2
4735
+
4697
4736
  possible-typed-array-names@1.1.0: {}
4698
4737
 
4699
4738
  postcss@8.5.23:
package/dist/index.js CHANGED
@@ -1370,6 +1370,7 @@ var DEFAULT_STATUS_CHECKS = [
1370
1370
  "Secret Scan",
1371
1371
  "Terraform Validate & Security"
1372
1372
  ];
1373
+ var SIBLING_STATUS_CHECKS = [...DEFAULT_STATUS_CHECKS, "E2E (Playwright)"];
1373
1374
  var REPO_MERGE_SETTINGS = {
1374
1375
  allow_auto_merge: true,
1375
1376
  delete_branch_on_merge: true
@@ -6602,7 +6603,7 @@ async function configureSiblingGithub(github, config, coreConfig, session, coreI
6602
6603
  await github.createBranch(org, repo, "staging", "dev");
6603
6604
  await github.createBranch(org, repo, "main", "dev");
6604
6605
  await github.setDefaultBranch(org, repo, "dev");
6605
- await github.configureBranchProtection(config);
6606
+ await github.configureBranchProtection(config, void 0, SIBLING_STATUS_CHECKS);
6606
6607
  await github.createEnvironments(config);
6607
6608
  await github.sealBranchProtection(org, repo);
6608
6609
  await github.enableVulnerabilityAlerts(org, repo);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.233.0",
3
+ "version": "0.234.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",