a71f801c3f
- Prisma schema: User, Session, RepoConfig, Preview, Job, WebhookToken, NoConfigComment, AdminSettings - Backend: auth (login/logout/me/first-user setup), webhook handler with HMAC verification, EC2 service, SSH service, deploy pipeline, job queue worker, cron workers - Frontend: Login with first-user detection, Dashboard, PreviewDetail with live log streaming, Settings, Repos config, Admin panel, SetupWizard, Privacy page - Docker Compose and Dockerfile for self-hosted deployment - Uses bcryptjs for Node 24 compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
479 lines
26 KiB
Markdown
479 lines
26 KiB
Markdown
# PR Previews (PP)
|
||
> PP is a self-hosted service that connects to a Gitea instance via webhook. When a PR is opened or updated, PP automatically provisions an AWS EC2 instance, builds and runs the project, and comments a live preview URL back on the PR. Designed for teams and agents iterating on code without needing local environments.
|
||
|
||
## Purpose
|
||
- Provide a shareable live preview of any PR before it's merged.
|
||
- Remove the need for reviewers to pull and run code locally.
|
||
- Works well in agent-driven workflows where humans review from mobile or external environments.
|
||
|
||
---
|
||
|
||
## Tech Stack
|
||
- **Backend:** `rj-web` from `../shsf` (same setup pattern), logging + auth middleware + Prisma from `../shsf` adapted to this project's needs.
|
||
- **Frontend:** React + Vite + Tailwind CSS (dark/light mode toggle, responsive).
|
||
- **Database:** PostgreSQL via Prisma (schema at `./prisma/schema.prisma`).
|
||
- **CI/CD:** Reuse `../shsf` CI/CD pipeline; build and push Docker image to the same registry under `/pp-previews/core`.
|
||
|
||
## Project Structure
|
||
```
|
||
./backend/ # Node backend (rj-web)
|
||
./frontend/ # React + Vite frontend
|
||
./prisma/schema.prisma # Prisma schema
|
||
./docker-compose.yml # Compose + Dockerfile for PP itself
|
||
```
|
||
|
||
---
|
||
|
||
## Implementation Notes
|
||
You're building this from scratch. Refer to `../shsf` for patterns on rj-web setup, logging, auth middleware, and Prisma client initialization.
|
||
|
||
**Testing:** No automated tests for V1. Build to fully working state locally first. The first real test will be against a live Gitea instance with a real repo. Build as if it will be tested end-to-end immediately.
|
||
|
||
---
|
||
|
||
## Data Model (Prisma)
|
||
|
||
### User
|
||
PP account.
|
||
- id, username, passwordHash, isAdmin, isFounder, createdAt
|
||
- giteaUsername, giteaPAT (encrypted), giteaInstanceUrl (e.g. `https://gitea.example.com`, no trailing slash)
|
||
- awsAccessKeyId (encrypted), awsSecretAccessKey (encrypted), awsRegion
|
||
|
||
### RepoConfig
|
||
Per-repo settings, one per user per repo. A repo can only be configured by one user — first to save wins; others receive an error.
|
||
- id, userId, repoOwner, repoName
|
||
- isEnabled (bool)
|
||
- giteaWebhookId (string, stored after auto-registration so PP can delete/update it later)
|
||
- denyList (string[], Gitea usernames to skip — e.g. dependabot)
|
||
- instanceType (string, default from AdminSettings.defaultInstanceType)
|
||
- inactivityHours (float, default 12, range 0.5–72)
|
||
- port (int, default 3000 — the port the app listens on inside the EC2 instance)
|
||
- envVars (JSON key-value)
|
||
- useDockerCompose (bool)
|
||
- composeFilePath (string, nullable — path within repo, default `docker-compose.yml`)
|
||
- aptPackages (string[])
|
||
- setupCommands (string[])
|
||
- buildCommands (string[])
|
||
- postBuildCommands (string[])
|
||
- runCommand (string, single — non-compose mode only)
|
||
|
||
### Preview
|
||
One record per PR. Persists across pushes; the EC2 instance is reused on each new push (only build + run steps re-run).
|
||
- id, repoConfigId, prNumber, prTitle
|
||
- commitSha (updated on each push)
|
||
- instanceId (AWS EC2 instance ID)
|
||
- instanceIp (public IP)
|
||
- port (copied from RepoConfig at deploy time)
|
||
- status (enum: `PROVISIONING` | `BUILDING` | `RUNNING` | `FAILED` | `STOPPED` | `IGNORED`)
|
||
- logs (text, appended each redeploy, capped at AdminSettings.logSizeLimitBytes — oldest lines truncated with a `--- logs truncated ---` marker)
|
||
- pid (int, nullable — non-compose mode only, the background process PID on the EC2 instance)
|
||
- sshPrivateKey (encrypted — ephemeral, generated per launch, deleted on instance termination)
|
||
- sshKeyName (string — AWS key pair name, for deletion on terminate)
|
||
- giteaCommentId (int, nullable — the Gitea comment ID of PP's status comment, for in-place edits)
|
||
- lastActivityAt (DateTime — updated on each push webhook AND each PR comment webhook)
|
||
- createdAt, updatedAt, stoppedAt
|
||
|
||
### Job
|
||
Durable async job queue for deployments and stops.
|
||
- id, previewId (nullable), type (enum: `DEPLOY` | `STOP` | `INACTIVITY_STOP`), status (enum: `PENDING` | `RUNNING` | `DONE` | `FAILED`)
|
||
- payload (JSON), error (text, nullable)
|
||
- createdAt, startedAt, finishedAt
|
||
|
||
### WebhookToken
|
||
Per-user HMAC secret for verifying Gitea webhook payloads.
|
||
- id, userId, token (random secret), createdAt
|
||
|
||
### NoConfigComment
|
||
Tracks which PRs have already received a "no previews configured" comment to avoid repeat spam.
|
||
- id, userId, repoOwner, repoName, prNumber, createdAt
|
||
|
||
### AdminSettings
|
||
Single row global config (seeded on first run).
|
||
- id
|
||
- defaultInstanceType (string, default `t2.medium`)
|
||
- maxConcurrentInstancesPerUser (int, default 5)
|
||
- logSizeLimitBytes (int, default 1048576 — 1MB)
|
||
- previewRetentionDays (int, default 30 — STOPPED/FAILED records older than this are purged)
|
||
- webhookRateLimitPerMinute (int, default 10 — per user)
|
||
- contactEmail (string — shown in privacy policy footer)
|
||
|
||
---
|
||
|
||
## Backend
|
||
|
||
### Auth
|
||
- Session-based auth (reuse shsf middleware).
|
||
- Registration endpoint is disabled — only admins can create users via the admin panel.
|
||
- First registered user automatically becomes admin and is flagged as `isFounder = true`. The founder cannot be demoted or deleted.
|
||
|
||
### Webhook Endpoint
|
||
`POST /webhook/:userId`
|
||
|
||
**Rate limiting:** Each user is limited to `AdminSettings.webhookRateLimitPerMinute` webhook events per minute. Excess requests receive `429 Too Many Requests` and are dropped (not queued).
|
||
|
||
**Signature verification:** Gitea sends `X-Gitea-Signature-256: sha256=<hmac>`. PP verifies using the user's `WebhookToken.token` before doing anything else. Invalid signature → `401`.
|
||
|
||
**Accepted Gitea events** (set via `X-Gitea-Event` header):
|
||
|
||
- `pull_request` with action `opened` → trigger new deploy
|
||
- `pull_request` with action `synchronize` → trigger redeploy on existing Preview
|
||
- `pull_request` with action `reopened` → same as `opened`
|
||
- `pull_request` with action `closed` → stop and terminate the preview
|
||
- `issue_comment` with action `created` → parse for `/pp` commands (see PP Commands section)
|
||
|
||
**Webhook handler returns `200 OK` immediately** after enqueueing a job. All processing is async.
|
||
|
||
**Activity tracking:** Update `Preview.lastActivityAt = now` on both `synchronize` and `issue_comment` events.
|
||
|
||
### PP Commands
|
||
PP listens for comments on PRs (via `issue_comment` webhook). A comment is a PP command if it starts with `/pp ` as the first line. Commands are only acted on if posted by: the PR author, or a Gitea user with `owner` or `admin` role on the repo (checked via `GET /api/v1/repos/{owner}/{repo}/teams` or collaborator endpoint).
|
||
|
||
| Command | Behaviour |
|
||
|---|---|
|
||
| `/pp rebuild` | Re-runs build + start steps on the existing EC2 instance (same as a new push but without a git pull). |
|
||
| `/pp stop` | Stops and terminates the preview instance. Sets status to `STOPPED`. |
|
||
| `/pp start` | If `STOPPED`: re-provisions a new EC2 and deploys. If `IGNORED`: un-ignores AND immediately triggers a fresh deploy. |
|
||
| `/pp logs` | PP posts a new comment with the last 50 lines of `Preview.logs` in a fenced code block. |
|
||
| `/pp ignore` | Sets `Preview.status = IGNORED`. Future pushes and commands (except `/pp start`) are silently ignored. If no Preview exists yet, creates one in `IGNORED` state so future pushes are skipped. |
|
||
|
||
PP does **not** react to its own comments (check that the commenter's username != `User.giteaUsername` for the configured user).
|
||
|
||
### AWS EC2 Management
|
||
|
||
**Required IAM permissions** (document in the setup wizard UI with a copy-pasteable JSON policy):
|
||
```json
|
||
{
|
||
"Version": "2012-10-17",
|
||
"Statement": [{
|
||
"Effect": "Allow",
|
||
"Action": [
|
||
"ec2:RunInstances",
|
||
"ec2:TerminateInstances",
|
||
"ec2:DescribeInstances",
|
||
"ec2:CreateSecurityGroup",
|
||
"ec2:DeleteSecurityGroup",
|
||
"ec2:AuthorizeSecurityGroupIngress",
|
||
"ec2:DescribeSecurityGroups",
|
||
"ec2:ImportKeyPair",
|
||
"ec2:DeleteKeyPair",
|
||
"ec2:CreateTags",
|
||
"sts:GetCallerIdentity"
|
||
],
|
||
"Resource": "*"
|
||
}]
|
||
}
|
||
```
|
||
|
||
**EC2 instance tagging** — every instance PP launches must be tagged:
|
||
```
|
||
pp:managed = true
|
||
pp:userId = <User.id>
|
||
pp:repo = <repoOwner>/<repoName>
|
||
pp:prNumber = <prNumber>
|
||
pp:previewId = <Preview.id>
|
||
```
|
||
|
||
**EC2 setup per preview:**
|
||
1. Generate an ephemeral RSA key pair. Store the private key encrypted in `Preview.sshPrivateKey`. Import the public key to AWS as `pp-preview-<previewId>` and store the name in `Preview.sshKeyName`.
|
||
2. Create a security group named `pp-preview-<previewId>` in the default VPC. Allow inbound: TCP 22 (SSH) and TCP `<port>` from `0.0.0.0/0`.
|
||
3. Launch instance:
|
||
- AMI: Ubuntu 22.04 LTS (hardcode a per-region AMI map, or resolve via SSM `resolve:ssm:/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id`)
|
||
- Instance type from `RepoConfig.instanceType`
|
||
- Key pair: `pp-preview-<previewId>`
|
||
- Security group: `pp-preview-<previewId>`
|
||
- User data bootstrap script (runs once on first boot):
|
||
```bash
|
||
#!/bin/bash
|
||
apt-get update -y
|
||
apt-get install -y curl git unzip build-essential
|
||
# Docker
|
||
curl -fsSL https://get.docker.com | sh
|
||
systemctl enable docker
|
||
# Docker Compose v2 (plugin)
|
||
apt-get install -y docker-compose-plugin
|
||
# NVM + Node LTS
|
||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
|
||
export NVM_DIR="/root/.nvm" && source "$NVM_DIR/nvm.sh"
|
||
nvm install --lts
|
||
nvm alias default lts/*
|
||
```
|
||
- All instance tags as above.
|
||
4. Poll `DescribeInstances` until status = `running` and the instance has a public IP.
|
||
5. Poll TCP port 22 until it accepts connections (max 5 minutes, then fail).
|
||
|
||
**Orphan cleanup on startup:** On PP startup, call `DescribeInstances` with filter `tag:pp:managed=true` and compare against all non-`STOPPED` Preview records. Any instance with no matching Preview, or whose matching Preview is `STOPPED`, is terminated and its security group deleted.
|
||
|
||
**On instance termination:**
|
||
1. Delete the AWS key pair (`DeleteKeyPair` using `Preview.sshKeyName`).
|
||
2. Delete the security group `pp-preview-<previewId>`.
|
||
3. Terminate the instance.
|
||
4. Null out `Preview.sshPrivateKey` and `Preview.sshKeyName` in the DB.
|
||
|
||
### Deployment Flow
|
||
|
||
Each PR has exactly one Preview record. The EC2 instance is provisioned once and reused on subsequent pushes.
|
||
|
||
#### First push (PR opened / no existing Preview, or previous Preview is STOPPED)
|
||
Set Preview status to `PROVISIONING`.
|
||
|
||
1. Check `AdminSettings.maxConcurrentInstancesPerUser` — count PROVISIONING + BUILDING + RUNNING previews for this user. If at limit: post a Gitea comment on the PR explaining the limit is reached, mark job FAILED, stop.
|
||
2. **Provision EC2** — as described above.
|
||
3. SSH in. All subsequent commands run over SSH.
|
||
4. **Install apt packages** (if any): `sudo apt-get install -y <aptPackages>`. Only runs on first provision.
|
||
5. **Clone repo:**
|
||
- Normal PR: `git clone https://<giteaUsername>:<PAT>@<giteaInstanceUrl>/<owner>/<repo>.git /opt/app`
|
||
- Fork PR: clone from the fork's URL (`payload.pull_request.head.repo.clone_url`, injecting PAT auth).
|
||
- `cd /opt/app && git fetch origin pull/<prNumber>/head:pp-pr && git checkout pp-pr`
|
||
6. **Detect Node version:** Check for `/opt/app/.nvmrc`. If present: `nvm install && nvm use`. Otherwise: `nvm use default`.
|
||
7. **Write `.env`:** Write `/opt/app/.env` from `RepoConfig.envVars`.
|
||
8. **Setup commands** (if any): run each in order. Only runs on first provision.
|
||
9. Set Preview status to `BUILDING`.
|
||
10. **Build commands:** run each in order.
|
||
11. **Post-build commands** (if any): run each in order.
|
||
12. **Start:**
|
||
- Docker Compose: `cd /opt/app && docker compose -f <composeFilePath> up -d --build --force-recreate`
|
||
- Non-compose: run `runCommand` in background via `nohup ... > /opt/app/pp.log 2>&1 &`, capture PID → save to `Preview.pid`.
|
||
13. Set Preview status to `RUNNING`. Store `instanceIp`, `port`, `commitSha`, `lastActivityAt = now`.
|
||
14. **Comment on PR** — post initial status comment (see PR Commenting section). Store the comment ID in `Preview.giteaCommentId`.
|
||
|
||
#### Subsequent push (synchronize / existing RUNNING or FAILED Preview)
|
||
1. SSH into existing instance using `Preview.instanceIp` and decrypted `Preview.sshPrivateKey`.
|
||
2. **Stop current process:**
|
||
- Non-compose: `kill <Preview.pid>` (SIGTERM, wait 5s, SIGKILL if still running).
|
||
- Docker Compose: `cd /opt/app && docker compose -f <composeFilePath> down`.
|
||
3. **Pull latest:**
|
||
```bash
|
||
cd /opt/app && git fetch origin pull/<prNumber>/head:pp-pr && git checkout pp-pr && git reset --hard FETCH_HEAD
|
||
```
|
||
4. **Re-detect Node version** (`.nvmrc` may have changed).
|
||
5. **Overwrite `.env`** — re-write from current `RepoConfig.envVars`.
|
||
6. Set Preview status to `BUILDING`. Append `\n--- Redeploy: <commitSha> ---\n` to `Preview.logs`.
|
||
7. **Build commands** — re-run each.
|
||
8. **Post-build commands** — re-run if defined.
|
||
9. **Start** — same as first provision start step. Update `Preview.pid`, `commitSha`, `lastActivityAt = now`. Set status to `RUNNING`.
|
||
10. **Update PR comment** — edit the existing comment in-place using `Preview.giteaCommentId` (see PR Commenting section).
|
||
|
||
**If any step fails:** Set status to `FAILED`. Append the error output to `Preview.logs`. Edit the PR comment to show failure with the last few log lines. Stop processing.
|
||
|
||
All log output is streamed to `Preview.logs` via WebSocket to the frontend (rj-web WebSocket support). Enforce log size cap: if `Preview.logs` exceeds `AdminSettings.logSizeLimitBytes`, drop oldest lines and prepend `--- logs truncated ---`.
|
||
|
||
### Inactivity Detection
|
||
- `Preview.lastActivityAt` is updated on each `synchronize` webhook AND each `issue_comment` webhook for the PR.
|
||
- A background cron job runs every 30 minutes. Any Preview where `lastActivityAt + inactivityHours < now` and `status = RUNNING` gets a `STOP` job enqueued.
|
||
- On stop: terminate the EC2 instance, post a final PR comment, set status to `STOPPED`.
|
||
|
||
### Job Queue
|
||
Webhook handler returns `200 OK` immediately and enqueues a Job. A background worker processes jobs:
|
||
- **Per-PR serialisation:** at most one active Job per `previewId` at a time.
|
||
- **New deploy cancels running deploy:** if a `DEPLOY` job is already `RUNNING` for a PR and a new `DEPLOY` arrives, the running job's SSH session is aborted and the new job takes over immediately.
|
||
- **On PP startup:** any Jobs left in `RUNNING` state (from a crash) are reset to `PENDING` and requeued.
|
||
- Job status (PENDING / RUNNING / DONE / FAILED) is visible on the preview detail page in the UI.
|
||
|
||
### Repo Discovery & Webhook Auto-Registration
|
||
- When the user saves their Gitea credentials, PP fetches all repos via `GET <giteaInstanceUrl>/api/v1/repos/search?limit=50&token=<PAT>` (paginate as needed).
|
||
- These appear in the Repo Configuration page for the user to enable.
|
||
- When the user **enables** a repo: PP calls `POST <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks` to register a webhook with:
|
||
- `type: gitea`
|
||
- `config.url`: `https://<PP_BASE_URL>/webhook/<userId>`
|
||
- `config.secret`: `WebhookToken.token`
|
||
- `config.content_type: json`
|
||
- `events: ["pull_request", "issue_comment"]`
|
||
- Store the returned hook ID in `RepoConfig.giteaWebhookId`.
|
||
- When the user **disables or deletes** a repo config: PP warns the user that the Gitea webhook will be deleted, and on confirmation calls `DELETE <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks/<giteaWebhookId>`.
|
||
- **Repos without a RepoConfig** that trigger a webhook: PP checks `NoConfigComment` — if no entry exists for `userId + repoOwner + repoName + prNumber`, PP posts one comment ("No previews configured for this repo...") and creates a `NoConfigComment` record.
|
||
- **Repo ownership conflict:** If a user tries to enable a repo that already has a `RepoConfig` owned by another user, return an error: "This repo is already configured by another user."
|
||
|
||
### Webhook Secret Rotation
|
||
When the user regenerates their webhook secret:
|
||
1. Generate a new `WebhookToken.token`.
|
||
2. For every `RepoConfig` belonging to this user that has a `giteaWebhookId`, call `PATCH <giteaInstanceUrl>/api/v1/repos/{owner}/{repo}/hooks/<giteaWebhookId>` to update the secret.
|
||
3. Old secret is invalid immediately.
|
||
|
||
### AWS Credential Validation
|
||
When the user saves AWS credentials, PP calls `sts:GetCallerIdentity`. Show inline success ("Connected as arn:aws:iam::...") or error ("Invalid credentials").
|
||
|
||
### Gitea URL Validation
|
||
When the user saves their Gitea instance URL, PP calls `GET <giteaInstanceUrl>/api/v1/version`. Show inline success (Gitea version) or error.
|
||
|
||
### DB Cleanup (background cron, daily)
|
||
Delete Preview records where `status IN (STOPPED, FAILED)` and `stoppedAt < now - AdminSettings.previewRetentionDays days`. Also delete associated Job records.
|
||
|
||
---
|
||
|
||
## Admin Panel (`/admin`)
|
||
|
||
Accessible only to `isAdmin` users. Has its own UI section, separate from the regular user dashboard.
|
||
|
||
### User Management
|
||
- Create users (username + password).
|
||
- Edit username or password of any user.
|
||
- Delete users (cannot delete `isFounder` user).
|
||
- Promote users to admin (cannot demote `isFounder` user).
|
||
- Registration endpoint is always disabled — admin creates all accounts.
|
||
|
||
### Global Settings
|
||
- **Default EC2 instance type** — shown as the default in all repo configs.
|
||
- **Max concurrent EC2 instances per user** — when a user hits this, new deploys are rejected with a PR comment.
|
||
- **Webhook rate limit** — max events per user per minute (default 10).
|
||
- **Log size cap** — max bytes per Preview.logs before truncation (default 1MB).
|
||
- **Preview retention** — days before STOPPED/FAILED records are purged (default 30).
|
||
- **Contact email** — shown in the privacy policy footer.
|
||
|
||
### Admin Preview Dashboard
|
||
- Shows all previews across all users (separate from the user's own dashboard).
|
||
- Per preview: user, repo, PR number, status, instance IP, created at.
|
||
- Admin can stop any preview.
|
||
|
||
---
|
||
|
||
## User Panel
|
||
|
||
### Account Settings (`/settings`)
|
||
- **Username** — editable.
|
||
- **Password** — change password.
|
||
- **Gitea Instance URL** — e.g. `https://gitea.example.com`. Validated on save (`GET /api/v1/version`).
|
||
- **Gitea Username** — the account that PP will comment as.
|
||
- **Gitea PAT** — write-only display (show `••••••••` after save). Must have scopes: `repository` (read), `issue` (write), `admin:repo_hook` (for webhook management). Show a tooltip explaining required scopes with a link to Gitea's token settings page.
|
||
- **AWS Access Key ID** — write-only display after save. Validated on save via `sts:GetCallerIdentity`.
|
||
- **AWS Secret Access Key** — write-only display after save.
|
||
- **AWS Region** — dropdown of all AWS regions.
|
||
- **Webhook URL** — read-only. `https://<PP_BASE_URL>/webhook/<userId>`. This is the "Target URL" in Gitea's webhook settings. Copy button included.
|
||
- **Webhook Secret** — read-only (separate from the URL). This is the "Secret" field in Gitea's webhook settings. Regenerate button (rotates secret and auto-updates all registered hooks in Gitea with a confirmation prompt).
|
||
|
||
Note displayed in settings: _"PP will post PR comments as your Gitea account (@username). Make sure your PAT has 'issue' write permission."_
|
||
|
||
### Repo Configuration (`/repos`)
|
||
- List of repos fetched from Gitea (refresh button). Each repo shows a toggle to enable/disable previews.
|
||
- Enabling a repo auto-registers the webhook in Gitea. Disabling warns and then deletes it.
|
||
- A repo already configured by another PP user shows a lock icon and "Claimed by another user" — the toggle is disabled.
|
||
- Expanded per-repo config:
|
||
- **Deny list** — tag input of Gitea usernames to skip.
|
||
- **EC2 instance type** — searchable select (t2.micro, t2.medium, t3.medium, t3.large, m5.large, c5.large) + custom text input. Show estimated hourly cost next to each option.
|
||
- **Inactivity kill timer** — slider 0.5h to 72h with labelled stops.
|
||
- **App port** — number input (default 3000).
|
||
- **Environment variables** — key/value editor, values masked. Add/remove rows.
|
||
- **Docker Compose toggle** — if on: show compose file path input (default `docker-compose.yml`), hide manual command fields.
|
||
- **Commands** (shown for both modes unless noted):
|
||
- Apt packages — tag input (compose + non-compose)
|
||
- Setup commands — ordered list, add/remove/reorder (compose + non-compose, run once on first provision)
|
||
- Build commands — ordered list (non-compose only; compose uses `docker compose up --build`)
|
||
- Post-build commands — ordered list, optional (non-compose only)
|
||
- Run command — single text input (non-compose only)
|
||
|
||
### Preview Dashboard (`/` or `/previews`)
|
||
- Cards for all of this user's previews across repos.
|
||
- Card shows: repo name, PR number + title, status badge, preview URL (clickable when RUNNING), commit SHA, last updated.
|
||
- Click into a preview to see:
|
||
- Full log viewer (WebSocket — live stream while active, monospace, auto-scroll, ANSI codes stripped).
|
||
- Current job status and queue position if PENDING.
|
||
- All previous redeploy separators visible inline in the log.
|
||
- Status history (PROVISIONING → BUILDING → RUNNING etc. with timestamps).
|
||
|
||
---
|
||
|
||
## PR Commenting
|
||
|
||
PP comments using the configured user's Gitea PAT (comments appear as their account). There is no bot account.
|
||
|
||
**One comment per deploy cycle, edited in-place.** PP stores the comment ID in `Preview.giteaCommentId` and calls `PATCH /api/v1/repos/{owner}/{repo}/issues/comments/{id}` to update it. A new comment is posted fresh at the start of each new push/redeploy cycle (replacing `giteaCommentId` with the new comment's ID).
|
||
|
||
**Comment format — Markdown:**
|
||
|
||
```markdown
|
||
## 🚀 PR Preview — `<repoOwner>/<repoName>` #<prNumber>
|
||
|
||
**Status:** 🟡 Provisioning EC2 instance...
|
||
**Commit:** `<sha>`
|
||
**Updated:** <timestamp>
|
||
|
||
---
|
||
_Powered by [PR Previews](<PP_BASE_URL>)_
|
||
```
|
||
|
||
Status line updates as the deploy progresses:
|
||
- `🟡 Provisioning EC2 instance...`
|
||
- `🟡 Building... (EC2 ready at <ip>)`
|
||
- `🟢 Live at http://<ip>:<port>`
|
||
- `🔴 Failed — last log lines:` _(followed by a fenced code block with the last 10 lines of logs)_
|
||
- `⚫ Stopped (inactivity timeout / PR closed / manual stop)`
|
||
|
||
On a new redeploy, PP posts a **new** comment (fresh cycle) and updates `giteaCommentId`.
|
||
|
||
---
|
||
|
||
## Setup Wizard (first login)
|
||
|
||
Shown to a user on first login if any of the required fields (Gitea URL, PAT, AWS credentials) are unset. Skippable at any point. Steps:
|
||
|
||
1. **Welcome** — brief explanation of what PP does.
|
||
2. **Gitea Connection** — Gitea instance URL + PAT. Validate on "Next". Show required PAT scopes with a link to Gitea settings.
|
||
3. **AWS Setup** — Access Key ID, Secret, Region. Show the required IAM policy JSON (copy button). Validate credentials on "Next".
|
||
4. **Your Webhook** — show the webhook URL and secret. Explain where to find these in Gitea's UI (per-repo or org-level webhook settings). Note: PP auto-registers per-repo webhooks when you enable a repo — this step is just for reference.
|
||
5. **Enable your first repo** — show the repo list, let the user enable one and fill in its config. On save, PP registers the webhook automatically.
|
||
6. **Done** — link to the preview dashboard.
|
||
|
||
---
|
||
|
||
## UI Design
|
||
|
||
- Dark/light mode toggle, persisted to localStorage.
|
||
- Animated route transitions and status badge updates.
|
||
- Status badges: ⚫ grey (stopped/ignored), 🟡 yellow (provisioning/building), 🟢 green (running), 🔴 red (failed).
|
||
- Log viewer: monospace font, auto-scroll to bottom, ANSI codes stripped, redeploy separator lines visually distinct (dimmed/italic).
|
||
- Responsive layout — usable on mobile for monitoring previews on the go.
|
||
- Toast notifications for save success/failure.
|
||
- Confirmation dialogs for: deleting a user, stopping a preview, regenerating webhook secret, disabling a repo (which deletes the Gitea webhook).
|
||
- Inline validation feedback on settings fields (Gitea URL, AWS credentials, PAT) that tests the connection on save.
|
||
- Contextual tooltips on instance type selector (cost estimates), PAT field (required scopes), inactivity timer (what happens when it fires).
|
||
|
||
---
|
||
|
||
## Docker / Deployment
|
||
|
||
`./docker-compose.yml` runs PP itself (not preview instances — those are EC2):
|
||
- `pp-backend` — Node backend.
|
||
- `pp-frontend` — Vite build served statically (or via backend).
|
||
- `pp-db` — PostgreSQL.
|
||
- Volume for Prisma migrations.
|
||
|
||
**Required environment variables:**
|
||
| Variable | Purpose |
|
||
|---|---|
|
||
| `DATABASE_URL` | PostgreSQL connection string |
|
||
| `SESSION_SECRET` | Session signing secret |
|
||
| `PP_BASE_URL` | Public URL of this PP instance (used in webhook URLs and PR comment links) |
|
||
| `ENCRYPTION_KEY` | AES-256 key for encrypting PAT, AWS secrets, SSH private keys at rest |
|
||
|
||
---
|
||
|
||
## Security Notes
|
||
- Gitea PAT, AWS credentials, and SSH private keys are encrypted at rest with AES-256 using `ENCRYPTION_KEY`.
|
||
- Webhook HMAC-SHA256 signature is verified before any payload is processed. Invalid signatures return `401`.
|
||
- Webhook rate limiting (default 10 req/min/user) prevents floods.
|
||
- EC2 security groups are scoped per preview — created on provision, deleted on termination.
|
||
- SSH keys are ephemeral per launch. Private key is stored encrypted in the DB and nulled out on instance termination.
|
||
- Users can only see and manage their own previews, repos, and credentials.
|
||
- Admins can see all previews in a dedicated admin UI section.
|
||
- The repo config ownership model (first-to-save) prevents two users from deploying conflicting previews for the same repo.
|
||
|
||
---
|
||
|
||
## Privacy Policy
|
||
A generic self-hosted app privacy policy is included in the footer. It covers:
|
||
- What data PP stores (user credentials, Gitea PAT encrypted at rest, AWS credentials encrypted at rest, preview logs, PR metadata).
|
||
- That credentials are used only to operate the preview service and are never shared with third parties.
|
||
- That EC2 instances are launched in the user's own AWS account — PP does not have access to user data on those instances beyond what it deploys.
|
||
- Log retention policy (`previewRetentionDays`).
|
||
- How to contact the instance administrator (configurable contact email in admin settings).
|
||
- No analytics, no tracking, no external data sharing.
|
||
|
||
---
|
||
|
||
## Out of Scope for V1
|
||
- GitHub/GitLab support (Gitea only).
|
||
- Custom domains for previews (EC2 public IP + port only).
|
||
- Preview instance autoscaling.
|
||
- Billing or usage tracking.
|
||
- Automated tests (manual QA first, tests added in V2).
|
||
- Multi-port previews (one port per repo config only).
|