vibed-infra 0.13.0 → 0.14.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.
- package/README.md +1 -1
- package/github/__pycache__/notify-vps-pull.cpython-312.pyc +0 -0
- package/github/workflows/docker-build-reusable.yml +8 -0
- package/lib/__pycache__/generate.cpython-312.pyc +0 -0
- package/lib/__pycache__/github_oidc.cpython-312.pyc +0 -0
- package/lib/__pycache__/load_config.cpython-312.pyc +0 -0
- package/lib/__pycache__/product_config.cpython-312.pyc +0 -0
- package/lib/__pycache__/webhook.cpython-312.pyc +0 -0
- package/lib/persistlog/__pycache__/__init__.cpython-312.pyc +0 -0
- package/package.json +1 -1
- package/skills/infra-cicd/SKILL.md +2 -0
- package/skills/infra-e2e-cicd/SKILL.md +107 -0
- package/skills/infra-e2e-cicd/reference/docker-optimization.md +124 -0
- package/skills/infra-e2e-cicd/reference/human-harnesses.md +183 -0
- package/skills/infra-e2e-cicd/reference/playwright-ui.md +122 -0
- package/skills/infra-e2e-cicd/reference/product-ci-workflow.yml +131 -0
- package/skills/infra-packager/SKILL.md +1 -1
- package/skills/infra-update-agent/SKILL.md +2 -0
- package/templates/update-agent/__pycache__/webhook_server.cpython-312.pyc +0 -0
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ See [`skills/infra-update-agent/SKILL.md`](skills/infra-update-agent/SKILL.md).
|
|
|
68
68
|
| `templates/persist-logs/` | Shipper install + persist sidecar |
|
|
69
69
|
| `templates/monitor/` | Machine-wide `monitor-vibed.sh` |
|
|
70
70
|
| `lib/persistlog/` | Python append / seal / replay |
|
|
71
|
-
| `skills/` | system-gateway, agent-vps-prep, infra-update-agent, infra-cicd, infra-packager, persist-logs, dns-configure |
|
|
71
|
+
| `skills/` | system-gateway, agent-vps-prep, infra-update-agent, infra-cicd, infra-e2e-cicd, infra-packager, persist-logs, dns-configure |
|
|
72
72
|
|
|
73
73
|
## Environment
|
|
74
74
|
|
|
Binary file
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Reusable GHCR docker build — one image per workflow_call.
|
|
2
|
+
# Keep in sync with .github/workflows/docker-build-reusable.yml (GitHub uses .github/).
|
|
2
3
|
#
|
|
3
4
|
# Caller (repeat per image or use a matrix in the caller workflow):
|
|
4
5
|
#
|
|
@@ -37,6 +38,10 @@ on:
|
|
|
37
38
|
required: false
|
|
38
39
|
type: boolean
|
|
39
40
|
default: true
|
|
41
|
+
build-args:
|
|
42
|
+
required: false
|
|
43
|
+
type: string
|
|
44
|
+
default: BUILD_PROFILE=production
|
|
40
45
|
secrets:
|
|
41
46
|
VIBED_WEBHOOK_SECRET:
|
|
42
47
|
required: false
|
|
@@ -75,6 +80,9 @@ jobs:
|
|
|
75
80
|
file: ${{ inputs.dockerfile }}
|
|
76
81
|
push: true
|
|
77
82
|
tags: ${{ steps.meta.outputs.tags }}
|
|
83
|
+
build-args: ${{ inputs.build-args }}
|
|
84
|
+
cache-from: type=gha
|
|
85
|
+
cache-to: type=gha,mode=max
|
|
78
86
|
labels: |
|
|
79
87
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
80
88
|
org.opencontainers.image.source=https://github.com/${{ github.repository }}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -41,6 +41,8 @@ On the default branch, the reusable workflow mints a GitHub Actions OIDC JWT (au
|
|
|
41
41
|
2. Build/push images to GHCR (reusable workflow notifies the VPS; requires `id-token: write`).
|
|
42
42
|
3. Optional job: `test-dist.sh --profile api` on a Docker-enabled runner.
|
|
43
43
|
|
|
44
|
+
**Product e2e (Playwright, temp DB, human-action harnesses):** see [`infra-e2e-cicd`](../infra-e2e-cicd/SKILL.md). vibed-infra CI validates the packager; product repos add browser e2e for app critical paths.
|
|
45
|
+
|
|
44
46
|
## Checklist
|
|
45
47
|
|
|
46
48
|
- [ ] Four YAML templates under `templates/` (`gateway.publicIp` / `tlsEmail` / `sites[]` set for production)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: infra-e2e-cicd
|
|
3
|
+
description: >-
|
|
4
|
+
E2E CI/CD for vibed-infra products: GHCR build/tag (UI/API/nodes), OIDC
|
|
5
|
+
auto-update, Docker pull-size optimization, Playwright critical paths, temp DB,
|
|
6
|
+
CI-only human-action harnesses. Use when setting up product GitHub Actions
|
|
7
|
+
or browser e2e tests.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# E2E CI/CD for vibed-infra products
|
|
11
|
+
|
|
12
|
+
Best practices for product repos that use the infra packager. An agent should read this skill after [`infra-packager`](../infra-packager/SKILL.md) and apply the reference templates to the product repo.
|
|
13
|
+
|
|
14
|
+
## Separation of concerns
|
|
15
|
+
|
|
16
|
+
| Layer | What it proves | Where |
|
|
17
|
+
|-------|----------------|-------|
|
|
18
|
+
| **vibed-infra CI** | Install scripts, named volumes, OIDC webhook, multi-app gateway | This repo — `npm test`, `test:dist`, `test:e2e-multi` |
|
|
19
|
+
| **Product CI** | App critical paths, Playwright UI flows, temp DB, human-action harnesses | Product repo — see [reference/product-ci-workflow.yml](reference/product-ci-workflow.yml) |
|
|
20
|
+
|
|
21
|
+
vibed-infra CI does **not** run Playwright. Products add browser e2e for their own critical paths.
|
|
22
|
+
|
|
23
|
+
## Product CI job graph
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
validate (package drift + unit tests)
|
|
27
|
+
→ build-api / build-ui / build-nodes (GHCR push + OIDC notify)
|
|
28
|
+
→ e2e-playwright (temp DB, VIBED_E2E_HARNESS=1)
|
|
29
|
+
→ harness-absence check (production-profile image returns 404 on /__e2e__/*)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
On default-branch push, each GHCR build job notifies the VPS via OIDC (see [`infra-update-agent`](../infra-update-agent/SKILL.md)). Tags: `:main`, `main-<sha>`, semver on release tag.
|
|
33
|
+
|
|
34
|
+
## Critical paths
|
|
35
|
+
|
|
36
|
+
1. **Identify** user journeys that must never break: sign-in, core CRUD, checkout, wallet sign, etc.
|
|
37
|
+
2. **One Playwright spec per journey** — keep specs focused; use smoke vs full suite tags (`@smoke`).
|
|
38
|
+
3. **Map selectors** to stable `data-testid` attributes (see [reference/playwright-ui.md](reference/playwright-ui.md)).
|
|
39
|
+
4. **Never point e2e at prod or VPS DB** — always use a temp database per CI job.
|
|
40
|
+
|
|
41
|
+
## Temp DB
|
|
42
|
+
|
|
43
|
+
- **Postgres**: GitHub Actions `services:` block or `docker compose` service; unique database name per job (`e2e_${{ github.run_id }}`).
|
|
44
|
+
- **SQLite**: tmpfile path in env; delete on job end.
|
|
45
|
+
- Run migrations **before** Playwright starts (`npx prisma migrate`, `alembic upgrade`, etc.).
|
|
46
|
+
- Seed minimal fixtures in the e2e setup hook, not in production migrations.
|
|
47
|
+
|
|
48
|
+
## VPS vs CI images
|
|
49
|
+
|
|
50
|
+
| | VPS (production) | CI (e2e) |
|
|
51
|
+
|---|------------------|----------|
|
|
52
|
+
| Tag | `:main` from GHCR | `:local` or `:main-<sha>` built in job |
|
|
53
|
+
| `BUILD_PROFILE` | `production` (default in reusable workflow) | `e2e` only in e2e compose |
|
|
54
|
+
| `VIBED_E2E_HARNESS` | unset | `1` |
|
|
55
|
+
| Harness routes | **404 / absent** | available for simulators |
|
|
56
|
+
|
|
57
|
+
Production images pushed to GHCR use `BUILD_PROFILE=production` by default (reusable workflow). Harness code lives under `e2e/` and is never imported by the production server entrypoint.
|
|
58
|
+
|
|
59
|
+
## Reference docs
|
|
60
|
+
|
|
61
|
+
| Topic | File |
|
|
62
|
+
|-------|------|
|
|
63
|
+
| Full product CI workflow | [reference/product-ci-workflow.yml](reference/product-ci-workflow.yml) |
|
|
64
|
+
| Docker pull-size optimization | [reference/docker-optimization.md](reference/docker-optimization.md) |
|
|
65
|
+
| Playwright-friendly UI + config | [reference/playwright-ui.md](reference/playwright-ui.md) |
|
|
66
|
+
| Passkey / wallet / captcha harnesses | [reference/human-harnesses.md](reference/human-harnesses.md) |
|
|
67
|
+
|
|
68
|
+
## GHCR build (one job per image)
|
|
69
|
+
|
|
70
|
+
Use the reusable workflow from vibed-infra (see [`infra-cicd`](../infra-cicd/SKILL.md)):
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
build-api:
|
|
74
|
+
uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
|
|
75
|
+
permissions:
|
|
76
|
+
contents: read
|
|
77
|
+
packages: write
|
|
78
|
+
id-token: write
|
|
79
|
+
with:
|
|
80
|
+
dockerfile: app/api/Dockerfile
|
|
81
|
+
image: ghcr.io/${{ github.repository_owner }}/my-api
|
|
82
|
+
build-args: BUILD_PROFILE=production
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Repeat for UI and nodes/worker images. Image names must match `*-config.yaml` and include `ghcr.io/{owner}/…` so OIDC notify matches the update-agent registry.
|
|
86
|
+
|
|
87
|
+
## Checklist
|
|
88
|
+
|
|
89
|
+
- [ ] Critical paths listed and covered by Playwright specs
|
|
90
|
+
- [ ] `./package.sh` + `git diff --exit-code dist` in CI validate job
|
|
91
|
+
- [ ] GHCR jobs for api, ui, nodes with `id-token: write`
|
|
92
|
+
- [ ] Dockerfiles follow [docker-optimization.md](reference/docker-optimization.md) (pull size first)
|
|
93
|
+
- [ ] UI uses `data-testid` on interactive elements
|
|
94
|
+
- [ ] E2e uses temp DB; migrations run before tests
|
|
95
|
+
- [ ] Harness code only under `e2e/`; production build excludes it
|
|
96
|
+
- [ ] CI asserts `/__e2e__/health` returns 404 on production-profile image
|
|
97
|
+
- [ ] `VIBED_E2E_HARNESS` never set in VPS `.env` or `dist/` templates
|
|
98
|
+
|
|
99
|
+
## Agent workflow
|
|
100
|
+
|
|
101
|
+
1. Read `infra-packager` → wire templates + commit `dist/`.
|
|
102
|
+
2. Apply [product-ci-workflow.yml](reference/product-ci-workflow.yml) to `.github/workflows/ci.yml`.
|
|
103
|
+
3. Optimize Dockerfiles per [docker-optimization.md](reference/docker-optimization.md).
|
|
104
|
+
4. Add Playwright config + specs per [playwright-ui.md](reference/playwright-ui.md).
|
|
105
|
+
5. Add harness stubs from [human-harnesses.md](reference/human-harnesses.md) under `e2e/` only.
|
|
106
|
+
6. Add prod-image harness-absence check to CI.
|
|
107
|
+
7. Wire GHCR matrix with `id-token: write` per `infra-update-agent`.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Docker build optimization (pull size first, then build speed)
|
|
2
|
+
|
|
3
|
+
Every push to `main` triggers a digest-gated `docker pull` on the VPS. Optimize layers so **most pulls transfer minimal bytes**, then speed up CI builds.
|
|
4
|
+
|
|
5
|
+
## Priority rules
|
|
6
|
+
|
|
7
|
+
| Priority | Rule |
|
|
8
|
+
|----------|------|
|
|
9
|
+
| 1 | **Multi-stage**: slim runtime (`alpine`, `distroless`); compilers and devDeps stay in builder stage |
|
|
10
|
+
| 2 | **Layer order**: base → OS packages → lockfiles → install deps → **source last** |
|
|
11
|
+
| 3 | **`.dockerignore`**: exclude `node_modules`, `.git`, `e2e/`, `tests/`, `*.md`, `.env*` |
|
|
12
|
+
| 4 | **Pin base image** by digest in production Dockerfiles |
|
|
13
|
+
| 5 | **Avoid busting dep layers**: copy `package-lock.json` / `requirements.txt` before `COPY .` |
|
|
14
|
+
| 6 | **CI-only** (secondary): BuildKit cache mounts + GHA cache in reusable workflow |
|
|
15
|
+
|
|
16
|
+
GHA cache (`cache-from` / `cache-to: type=gha`) speeds CI rebuilds. It does **not** change what the VPS pulls — layer ordering and multi-stage builds do.
|
|
17
|
+
|
|
18
|
+
## `.dockerignore` (product root)
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
.git
|
|
22
|
+
node_modules
|
|
23
|
+
e2e
|
|
24
|
+
tests
|
|
25
|
+
**/*.md
|
|
26
|
+
.env*
|
|
27
|
+
dist
|
|
28
|
+
.github
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API — before (pull-unfriendly)
|
|
32
|
+
|
|
33
|
+
```dockerfile
|
|
34
|
+
FROM python:3.12-alpine
|
|
35
|
+
WORKDIR /app
|
|
36
|
+
COPY . .
|
|
37
|
+
RUN pip install -r requirements.txt
|
|
38
|
+
CMD ["python", "server.py"]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Every source change busts the dependency layer; full tree copied early.
|
|
42
|
+
|
|
43
|
+
## API — after (pull-friendly)
|
|
44
|
+
|
|
45
|
+
```dockerfile
|
|
46
|
+
FROM python:3.12-alpine@sha256:… AS runtime
|
|
47
|
+
RUN apk add --no-cache wget \
|
|
48
|
+
&& adduser -D -u 1000 app
|
|
49
|
+
WORKDIR /app
|
|
50
|
+
COPY requirements.txt .
|
|
51
|
+
RUN pip install --no-cache-dir -r requirements.txt
|
|
52
|
+
COPY server.py .
|
|
53
|
+
ARG BUILD_PROFILE=production
|
|
54
|
+
ENV BUILD_PROFILE=$BUILD_PROFILE
|
|
55
|
+
RUN mkdir -p /data /config && chown -R app:app /data /config /app
|
|
56
|
+
USER 1000
|
|
57
|
+
EXPOSE 8080
|
|
58
|
+
CMD ["python3", "server.py"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Lockfile copied first → dependency layer cached across commits. Only `server.py` (and siblings) bust on app changes.
|
|
62
|
+
|
|
63
|
+
## UI — static site with nginx (multi-stage)
|
|
64
|
+
|
|
65
|
+
```dockerfile
|
|
66
|
+
# builder — not shipped to VPS
|
|
67
|
+
FROM node:22-alpine@sha256:… AS builder
|
|
68
|
+
WORKDIR /src
|
|
69
|
+
COPY package.json package-lock.json ./
|
|
70
|
+
RUN npm ci
|
|
71
|
+
COPY . .
|
|
72
|
+
ARG BUILD_PROFILE=production
|
|
73
|
+
ENV VITE_BUILD_PROFILE=$BUILD_PROFILE
|
|
74
|
+
RUN npm run build
|
|
75
|
+
|
|
76
|
+
# runtime — minimal pull
|
|
77
|
+
FROM nginx:1.27-alpine@sha256:…
|
|
78
|
+
COPY --from=builder /src/dist /usr/share/nginx/html
|
|
79
|
+
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
80
|
+
EXPOSE 80
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The VPS pulls only the nginx layer plus changed static assets — not Node, `node_modules`, or source.
|
|
84
|
+
|
|
85
|
+
## Worker / nodes
|
|
86
|
+
|
|
87
|
+
Same rules as API. Keep worker images separate from API so a UI change does not force an API pull (and vice versa).
|
|
88
|
+
|
|
89
|
+
## `BUILD_PROFILE` in Dockerfile
|
|
90
|
+
|
|
91
|
+
Products gate harness imports on build arg:
|
|
92
|
+
|
|
93
|
+
```dockerfile
|
|
94
|
+
ARG BUILD_PROFILE=production
|
|
95
|
+
ENV BUILD_PROFILE=$BUILD_PROFILE
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- GHCR pushes (VPS): `BUILD_PROFILE=production` (reusable workflow default).
|
|
99
|
+
- Local e2e compose: `BUILD_PROFILE=e2e` only in `docker-compose.e2e.yml`.
|
|
100
|
+
|
|
101
|
+
Never set `BUILD_PROFILE=e2e` in VPS install `.env` or production GHCR workflow.
|
|
102
|
+
|
|
103
|
+
## Reusable workflow (CI build speed)
|
|
104
|
+
|
|
105
|
+
The vibed-infra reusable workflow passes:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
build-args: BUILD_PROFILE=production
|
|
109
|
+
cache-from: type=gha
|
|
110
|
+
cache-to: type=gha,mode=max
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Products can override `build-args` per job if needed.
|
|
114
|
+
|
|
115
|
+
## Verify pull efficiency
|
|
116
|
+
|
|
117
|
+
On the VPS after a small code change:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
docker pull ghcr.io/owner/my-api:main
|
|
121
|
+
# Should report "Already exists" for most layers; only changed layers download
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Use `docker history ghcr.io/owner/my-api:main` to confirm large layers (deps, base) are stable across commits.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# CI human-action harnesses (passkeys, wallet, captcha)
|
|
2
|
+
|
|
3
|
+
Simulate human interactions in Playwright CI only. **Harness code must be completely unavailable on VPS production builds.**
|
|
4
|
+
|
|
5
|
+
## Three-layer VPS exclusion contract
|
|
6
|
+
|
|
7
|
+
1. **Source layout** — all harness code under `e2e/` or `test/harness/`; never in `app/` production paths imported by the server entrypoint.
|
|
8
|
+
2. **Build gate** — `BUILD_PROFILE=production` excludes harness modules; GHCR images use production profile only (reusable workflow default).
|
|
9
|
+
3. **Runtime gate** — `VIBED_E2E_HARNESS` unset on VPS; CI compose sets `VIBED_E2E_HARNESS=1`.
|
|
10
|
+
|
|
11
|
+
## Verification (product CI)
|
|
12
|
+
|
|
13
|
+
After building a production-profile image, assert harness routes are absent:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cid=$(docker run -d -p 18080:8080 prod-api:ci)
|
|
17
|
+
trap "docker rm -f $cid" EXIT
|
|
18
|
+
sleep 2
|
|
19
|
+
code=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:18080/__e2e__/health || echo "000")
|
|
20
|
+
test "$code" = "404" || test "$code" = "000" || (echo "harness leaked: HTTP $code" && exit 1)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Never add `VIBED_E2E_HARNESS`, `E2E_CAPTCHA_SECRET`, or harness URLs to VPS `.env` templates or committed `dist/`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Passkeys / WebAuthn
|
|
28
|
+
|
|
29
|
+
**CI approach:** Playwright CDP virtual authenticator; optional test-only registration endpoint when harness is on.
|
|
30
|
+
|
|
31
|
+
**VPS exclusion:** Route module in `e2e/harness/webauthn_routes.py` (or equivalent); imported only when `BUILD_PROFILE=e2e` or dynamic import gated on `VIBED_E2E_HARNESS`.
|
|
32
|
+
|
|
33
|
+
### Playwright virtual authenticator stub (`e2e/harness/webauthn.ts`)
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { type BrowserContext, type CDPSession } from "@playwright/test";
|
|
37
|
+
|
|
38
|
+
export async function enableVirtualAuthenticator(context: BrowserContext) {
|
|
39
|
+
const page = context.pages()[0] ?? await context.newPage();
|
|
40
|
+
const client = await context.newCDPSession(page);
|
|
41
|
+
await client.send("WebAuthn.enable");
|
|
42
|
+
const { authenticatorId } = await client.send("WebAuthn.addVirtualAuthenticator", {
|
|
43
|
+
options: {
|
|
44
|
+
protocol: "ctap2",
|
|
45
|
+
transport: "internal",
|
|
46
|
+
hasResidentKey: true,
|
|
47
|
+
hasUserVerification: true,
|
|
48
|
+
isUserVerified: true,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return { client, authenticatorId };
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Test-only server route stub (`e2e/harness/webauthn_routes.py`)
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
# Import ONLY from e2e entrypoint when VIBED_E2E_HARNESS=1 — never from prod server.py
|
|
59
|
+
import os
|
|
60
|
+
from flask import Blueprint, jsonify
|
|
61
|
+
|
|
62
|
+
bp = Blueprint("e2e_webauthn", __name__, url_prefix="/__e2e__/webauthn")
|
|
63
|
+
|
|
64
|
+
@bp.route("/register", methods=["POST"])
|
|
65
|
+
def register_credential():
|
|
66
|
+
if os.environ.get("VIBED_E2E_HARNESS") != "1":
|
|
67
|
+
return jsonify(error="not found"), 404
|
|
68
|
+
# Return a fixed test credential payload for the app's WebAuthn verify step
|
|
69
|
+
return jsonify({"credentialId": "e2e-test-credential", "publicKey": "…"})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Wire `bp` in an `e2e/server_extensions.py` that production `server.py` never imports.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## MetaMask / wallet signing
|
|
77
|
+
|
|
78
|
+
**CI approach:** Inject `window.ethereum` mock via Playwright `addInitScript`; optionally use [Synpress](https://github.com/Synthetixio/synpress) for extension-based flows in CI only.
|
|
79
|
+
|
|
80
|
+
**VPS exclusion:** Mock script lives in `e2e/harness/`; never bundled in production UI build.
|
|
81
|
+
|
|
82
|
+
### Wallet mock stub (`e2e/harness/ethereum-mock.ts`)
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { type Page } from "@playwright/test";
|
|
86
|
+
|
|
87
|
+
const TEST_ADDRESS = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";
|
|
88
|
+
|
|
89
|
+
export async function injectEthereumMock(page: Page) {
|
|
90
|
+
await page.addInitScript(({ address }) => {
|
|
91
|
+
(window as unknown as { ethereum?: unknown }).ethereum = {
|
|
92
|
+
isMetaMask: true,
|
|
93
|
+
selectedAddress: address,
|
|
94
|
+
request: async ({ method }: { method: string }) => {
|
|
95
|
+
if (method === "eth_requestAccounts") return [address];
|
|
96
|
+
if (method === "eth_accounts") return [address];
|
|
97
|
+
if (method === "personal_sign") return "0x" + "ab".repeat(32);
|
|
98
|
+
if (method === "eth_chainId") return "0x1";
|
|
99
|
+
throw new Error(`unsupported: ${method}`);
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}, { address: TEST_ADDRESS });
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Example spec usage
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
test("wallet connect and sign @smoke", async ({ page }) => {
|
|
110
|
+
await injectEthereumMock(page);
|
|
111
|
+
await page.goto("/connect");
|
|
112
|
+
await page.getByTestId("connect-wallet").click();
|
|
113
|
+
await expect(page.getByTestId("wallet-address")).toContainText("0x7099");
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Do not ship MetaMask extension or mock provider in production Docker images.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Captcha
|
|
122
|
+
|
|
123
|
+
**CI approach:** Server accepts `X-E2E-Captcha-Bypass: $E2E_CAPTCHA_SECRET` when harness env is set; Playwright sets header globally or per request.
|
|
124
|
+
|
|
125
|
+
**VPS exclusion:** Bypass handler in separate module; eliminated from production build via `BUILD_PROFILE=production`.
|
|
126
|
+
|
|
127
|
+
### Server bypass stub (`e2e/harness/captcha_bypass.py`)
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
import os
|
|
131
|
+
|
|
132
|
+
def captcha_verified(request) -> bool:
|
|
133
|
+
if os.environ.get("VIBED_E2E_HARNESS") != "1":
|
|
134
|
+
return False # production path uses real captcha provider only
|
|
135
|
+
secret = os.environ.get("E2E_CAPTCHA_SECRET", "")
|
|
136
|
+
return secret and request.headers.get("X-E2E-Captcha-Bypass") == secret
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Use in your captcha middleware:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
if captcha_verified(request):
|
|
143
|
+
return # skip provider verify in CI only
|
|
144
|
+
# … real hCaptcha / Turnstile verify …
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Playwright global header (`playwright.config.ts`)
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
use: {
|
|
151
|
+
extraHTTPHeaders: process.env.VIBED_E2E_HARNESS === "1"
|
|
152
|
+
? { "X-E2E-Captcha-Bypass": process.env.E2E_CAPTCHA_SECRET ?? "" }
|
|
153
|
+
: {},
|
|
154
|
+
},
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Set `E2E_CAPTCHA_SECRET` only in CI secrets or e2e compose — never on VPS.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Harness health endpoint (CI only)
|
|
162
|
+
|
|
163
|
+
Optional liveness for e2e compose debugging:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
@bp.route("/health")
|
|
167
|
+
def harness_health():
|
|
168
|
+
if os.environ.get("VIBED_E2E_HARNESS") != "1":
|
|
169
|
+
return "", 404
|
|
170
|
+
return jsonify(ok=True)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Production profile must return **404** for `/__e2e__/health` — verified in the `harness-absence` CI job.
|
|
174
|
+
|
|
175
|
+
## Summary
|
|
176
|
+
|
|
177
|
+
| Harness | CI simulator | VPS must not have |
|
|
178
|
+
|---------|--------------|-------------------|
|
|
179
|
+
| Passkeys | CDP virtual authenticator + optional `/__e2e__/webauthn/*` | Virtual auth routes, test credentials |
|
|
180
|
+
| Wallet | `window.ethereum` mock in Playwright | Extension, mock provider in bundle |
|
|
181
|
+
| Captcha | `X-E2E-Captcha-Bypass` header + env secret | Bypass env vars, bypass middleware in prod build |
|
|
182
|
+
|
|
183
|
+
Products wire app-specific flows on top of these stubs; vibed-infra documents the pattern, not a shared npm package.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Playwright-friendly UI and CI setup
|
|
2
|
+
|
|
3
|
+
Design the product UI so Playwright tests are stable, fast, and maintainable.
|
|
4
|
+
|
|
5
|
+
## UI design rules
|
|
6
|
+
|
|
7
|
+
1. **`data-testid` on interactive elements** — prefer over CSS classes, auto-generated IDs, or visible text that changes with i18n.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<form id="form" data-testid="note-form">
|
|
11
|
+
<input data-testid="note-input" … />
|
|
12
|
+
<button type="submit" data-testid="note-submit">Post</button>
|
|
13
|
+
</form>
|
|
14
|
+
<ul id="list" data-testid="note-list"></ul>
|
|
15
|
+
<p id="status" data-testid="status"></p>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. **Stable routes** — use predictable URLs for auth and core flows (`/login`, `/dashboard`). Avoid hash-only navigation for critical paths.
|
|
19
|
+
|
|
20
|
+
3. **Reduce flakiness**
|
|
21
|
+
- Set `aria-busy="true"` while loading; tests wait for `[aria-busy="false"]`.
|
|
22
|
+
- Disable animations in e2e: `prefers-reduced-motion: reduce` via test env CSS, or `VIBED_E2E_HARNESS=1` body class.
|
|
23
|
+
- Avoid `setTimeout`-driven UI; use explicit ready states.
|
|
24
|
+
|
|
25
|
+
4. **Auth state** — expose login via API or cookie the test can set; do not rely only on `localStorage` without a test hook.
|
|
26
|
+
|
|
27
|
+
5. **Error surfaces** — use `data-testid="error-message"` so failures are assertable without parsing console logs.
|
|
28
|
+
|
|
29
|
+
## Playwright config stub (`playwright.config.ts`)
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { defineConfig, devices } from "@playwright/test";
|
|
33
|
+
|
|
34
|
+
export default defineConfig({
|
|
35
|
+
testDir: "e2e/specs",
|
|
36
|
+
fullyParallel: true,
|
|
37
|
+
forbidOnly: !!process.env.CI,
|
|
38
|
+
retries: process.env.CI ? 2 : 0,
|
|
39
|
+
workers: process.env.CI ? 1 : undefined,
|
|
40
|
+
reporter: process.env.CI ? [["github"], ["html"]] : "list",
|
|
41
|
+
use: {
|
|
42
|
+
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3000",
|
|
43
|
+
trace: "on-first-retry",
|
|
44
|
+
screenshot: "only-on-failure",
|
|
45
|
+
},
|
|
46
|
+
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tag smoke tests: `test("user can post a note @smoke", …)`.
|
|
51
|
+
|
|
52
|
+
## Example spec stub
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { test, expect } from "@playwright/test";
|
|
56
|
+
|
|
57
|
+
test("user can post a note @smoke", async ({ page }) => {
|
|
58
|
+
await page.goto("/");
|
|
59
|
+
await expect(page.getByTestId("status")).toContainText("Live");
|
|
60
|
+
await page.getByTestId("note-input").fill("hello e2e");
|
|
61
|
+
await page.getByTestId("note-submit").click();
|
|
62
|
+
await expect(page.getByTestId("note-list")).toContainText("hello e2e");
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## E2e stack (`docker-compose.e2e.yml` stub)
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
services:
|
|
70
|
+
api:
|
|
71
|
+
image: my-api:local
|
|
72
|
+
build:
|
|
73
|
+
context: app/api
|
|
74
|
+
args:
|
|
75
|
+
BUILD_PROFILE: e2e
|
|
76
|
+
environment:
|
|
77
|
+
VIBED_E2E_HARNESS: "1"
|
|
78
|
+
E2E_CAPTCHA_SECRET: ci-test-secret
|
|
79
|
+
DATABASE_URL: postgresql://e2e:e2e@postgres:5432/e2e
|
|
80
|
+
ports:
|
|
81
|
+
- "8080:8080"
|
|
82
|
+
depends_on:
|
|
83
|
+
postgres:
|
|
84
|
+
condition: service_healthy
|
|
85
|
+
|
|
86
|
+
ui:
|
|
87
|
+
image: my-ui:local
|
|
88
|
+
ports:
|
|
89
|
+
- "3000:80"
|
|
90
|
+
depends_on:
|
|
91
|
+
- api
|
|
92
|
+
|
|
93
|
+
postgres:
|
|
94
|
+
image: postgres:16-alpine
|
|
95
|
+
environment:
|
|
96
|
+
POSTGRES_USER: e2e
|
|
97
|
+
POSTGRES_PASSWORD: e2e
|
|
98
|
+
POSTGRES_DB: e2e
|
|
99
|
+
healthcheck:
|
|
100
|
+
test: ["CMD", "pg_isready", "-U", "e2e"]
|
|
101
|
+
interval: 5s
|
|
102
|
+
retries: 5
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Set `PLAYWRIGHT_BASE_URL=http://localhost:3000` in the Playwright job.
|
|
106
|
+
|
|
107
|
+
## CI job essentials
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
- run: npx playwright install --with-deps chromium
|
|
111
|
+
- run: npx playwright test --grep @smoke
|
|
112
|
+
env:
|
|
113
|
+
PLAYWRIGHT_BASE_URL: http://localhost:3000
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Upload `playwright-report/` as a CI artifact on failure.
|
|
117
|
+
|
|
118
|
+
## What not to do
|
|
119
|
+
|
|
120
|
+
- Do not use production GHCR `:main` images for e2e with harness enabled — build `:local` or use `BUILD_PROFILE=e2e`.
|
|
121
|
+
- Do not add test-only routes to production UI bundles without build-profile gating.
|
|
122
|
+
- Do not run Playwright against the VPS or production database.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Product CI workflow (copy to `.github/workflows/ci.yml`)
|
|
2
|
+
|
|
3
|
+
Example for a vibed-infra-packaged product with API, UI, and worker images.
|
|
4
|
+
Adjust paths, image names, and e2e commands to match the product repo.
|
|
5
|
+
|
|
6
|
+
```yaml
|
|
7
|
+
name: CI
|
|
8
|
+
|
|
9
|
+
on:
|
|
10
|
+
push:
|
|
11
|
+
branches: [main]
|
|
12
|
+
pull_request:
|
|
13
|
+
branches: [main]
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
validate:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
- uses: actions/setup-node@v4
|
|
21
|
+
with:
|
|
22
|
+
node-version: "22"
|
|
23
|
+
- run: npm ci
|
|
24
|
+
- run: npm test
|
|
25
|
+
- name: Package drift check
|
|
26
|
+
run: |
|
|
27
|
+
./package.sh
|
|
28
|
+
git diff --exit-code dist/
|
|
29
|
+
|
|
30
|
+
build-api:
|
|
31
|
+
needs: validate
|
|
32
|
+
uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
packages: write
|
|
36
|
+
id-token: write
|
|
37
|
+
with:
|
|
38
|
+
dockerfile: app/api/Dockerfile
|
|
39
|
+
image: ghcr.io/${{ github.repository_owner }}/my-api
|
|
40
|
+
build-args: BUILD_PROFILE=production
|
|
41
|
+
secrets: inherit
|
|
42
|
+
|
|
43
|
+
build-ui:
|
|
44
|
+
needs: validate
|
|
45
|
+
uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
packages: write
|
|
49
|
+
id-token: write
|
|
50
|
+
with:
|
|
51
|
+
dockerfile: app/ui/Dockerfile
|
|
52
|
+
image: ghcr.io/${{ github.repository_owner }}/my-ui
|
|
53
|
+
build-args: BUILD_PROFILE=production
|
|
54
|
+
secrets: inherit
|
|
55
|
+
|
|
56
|
+
build-nodes:
|
|
57
|
+
needs: validate
|
|
58
|
+
uses: naiemk/vibed-infra/.github/workflows/docker-build-reusable.yml@main
|
|
59
|
+
permissions:
|
|
60
|
+
contents: read
|
|
61
|
+
packages: write
|
|
62
|
+
id-token: write
|
|
63
|
+
with:
|
|
64
|
+
dockerfile: app/worker/Dockerfile
|
|
65
|
+
image: ghcr.io/${{ github.repository_owner }}/my-worker
|
|
66
|
+
build-args: BUILD_PROFILE=production
|
|
67
|
+
secrets: inherit
|
|
68
|
+
|
|
69
|
+
e2e-playwright:
|
|
70
|
+
needs: [build-api, build-ui, build-nodes]
|
|
71
|
+
runs-on: ubuntu-latest
|
|
72
|
+
services:
|
|
73
|
+
postgres:
|
|
74
|
+
image: postgres:16-alpine
|
|
75
|
+
env:
|
|
76
|
+
POSTGRES_USER: e2e
|
|
77
|
+
POSTGRES_PASSWORD: e2e
|
|
78
|
+
POSTGRES_DB: e2e
|
|
79
|
+
ports:
|
|
80
|
+
- 5432:5432
|
|
81
|
+
options: >-
|
|
82
|
+
--health-cmd "pg_isready -U e2e"
|
|
83
|
+
--health-interval 5s
|
|
84
|
+
--health-timeout 5s
|
|
85
|
+
--health-retries 5
|
|
86
|
+
env:
|
|
87
|
+
DATABASE_URL: postgresql://e2e:e2e@localhost:5432/e2e
|
|
88
|
+
VIBED_E2E_HARNESS: "1"
|
|
89
|
+
E2E_CAPTCHA_SECRET: ci-test-secret
|
|
90
|
+
steps:
|
|
91
|
+
- uses: actions/checkout@v4
|
|
92
|
+
- uses: actions/setup-node@v4
|
|
93
|
+
with:
|
|
94
|
+
node-version: "22"
|
|
95
|
+
- run: npm ci
|
|
96
|
+
- name: Build local e2e images
|
|
97
|
+
run: bash scripts/build-images.sh
|
|
98
|
+
- name: Start e2e stack
|
|
99
|
+
run: docker compose -f docker-compose.e2e.yml up -d --wait
|
|
100
|
+
- name: Run DB migrations
|
|
101
|
+
run: npm run db:migrate
|
|
102
|
+
- name: Install Playwright browsers
|
|
103
|
+
run: npx playwright install --with-deps chromium
|
|
104
|
+
- name: Playwright e2e (critical paths)
|
|
105
|
+
run: npx playwright test --grep @smoke
|
|
106
|
+
- name: Teardown
|
|
107
|
+
if: always()
|
|
108
|
+
run: docker compose -f docker-compose.e2e.yml down -v
|
|
109
|
+
|
|
110
|
+
harness-absence:
|
|
111
|
+
needs: validate
|
|
112
|
+
runs-on: ubuntu-latest
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/checkout@v4
|
|
115
|
+
- name: Build production-profile API image
|
|
116
|
+
run: docker build --build-arg BUILD_PROFILE=production -t prod-api:ci app/api
|
|
117
|
+
- name: Assert harness routes absent
|
|
118
|
+
run: |
|
|
119
|
+
cid=$(docker run -d -p 18080:8080 prod-api:ci)
|
|
120
|
+
trap "docker rm -f $cid" EXIT
|
|
121
|
+
sleep 2
|
|
122
|
+
code=$(curl -sf -o /dev/null -w "%{http_code}" http://localhost:18080/__e2e__/health || echo "000")
|
|
123
|
+
test "$code" = "404" || test "$code" = "000" || (echo "harness leaked: HTTP $code" && exit 1)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Notes
|
|
127
|
+
|
|
128
|
+
- **PR builds**: GHCR push still runs; OIDC notify only fires on default branch (handled inside reusable workflow).
|
|
129
|
+
- **Disable notify on PR-only builds**: pass `notify: false` to the reusable workflow when `github.event_name == 'pull_request'`.
|
|
130
|
+
- **Matrix alternative**: if GitHub limits reusable workflow calls, inline the build-push steps from [`docker-build-reusable.yml`](../../../.github/workflows/docker-build-reusable.yml) into a matrix job.
|
|
131
|
+
- **Infra dist e2e** (optional): add `bash test-dist.sh --profile api` on a Docker runner to prove install scripts — separate from Playwright.
|
|
@@ -36,7 +36,7 @@ exec bash "$PACKAGER/package.sh" --product "$ROOT" --out "$ROOT/dist"
|
|
|
36
36
|
git add dist && git commit && git push
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
6. **CI** — product repo: package drift check + GHCR build (notifies VPS). Packager repo: `npm test`, `oidc-webhook-e2e`, `test-dist.sh`, `npm run test:e2e-multi`.
|
|
39
|
+
6. **CI** — product repo: package drift check + GHCR build (notifies VPS) + Playwright e2e for critical paths ([`infra-e2e-cicd`](../infra-e2e-cicd/SKILL.md)). Packager repo: `npm test`, `oidc-webhook-e2e`, `test-dist.sh`, `npm run test:e2e-multi`.
|
|
40
40
|
|
|
41
41
|
7. **VPS** — DNS first (paste `dist/DNS-SKILL.md` into AU agent), then wget. Install registers the app with the update-agent so the next GHCR push can pull immediately:
|
|
42
42
|
|
|
@@ -63,6 +63,8 @@ jobs:
|
|
|
63
63
|
|
|
64
64
|
URLs come from `gateway.publicIp` + `gateway.sites[].host` already in config (`dist/packageconfig.yaml` `webhook.url` / `fallbackUrl`). Disable notify with `notify: false`.
|
|
65
65
|
|
|
66
|
+
For Playwright e2e, temp DB, and CI-only harnesses on the product side, see [`infra-e2e-cicd`](../infra-e2e-cicd/SKILL.md).
|
|
67
|
+
|
|
66
68
|
Local curl can send `X-Vibed-Secret` from `tokens/{app}` (compiled hash). Optional `WEBHOOK_SECRET` still works as an override.
|
|
67
69
|
|
|
68
70
|
## Tests
|
|
Binary file
|