adding pr support
Some checks failed
Some checks failed
This commit is contained in:
168
.gitea/workflows/pull-request.yaml
Normal file
168
.gitea/workflows/pull-request.yaml
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
name: Build Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
env:
|
||||||
|
ARCHIVE_NAME: vision-start-pr-${{ gitea.event.pull_request.number }}.zip
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build Pull Request Archive
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Set up required tools
|
||||||
|
run: sudo apt-get install zip wget -y
|
||||||
|
|
||||||
|
- name: Install JS dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run build
|
||||||
|
run: |
|
||||||
|
bash scripts/prepare_release.sh
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Prepare archive
|
||||||
|
run: |
|
||||||
|
mv dist vision-start/
|
||||||
|
mv extension vision-start/
|
||||||
|
mv manifest.json vision-start/
|
||||||
|
|
||||||
|
- name: Create zip archive
|
||||||
|
run: |
|
||||||
|
cd vision-start
|
||||||
|
zip -r ../${{ env.ARCHIVE_NAME }} *
|
||||||
|
|
||||||
|
- name: Upload build artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pr-release-zip
|
||||||
|
path: ${{ env.ARCHIVE_NAME }}
|
||||||
|
|
||||||
|
capture_screenshots:
|
||||||
|
name: Capture Pull Request Screenshots
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Set up required tools
|
||||||
|
run: sudo apt-get install unzip -y
|
||||||
|
|
||||||
|
- name: Install JS dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Download build artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pr-release-zip
|
||||||
|
|
||||||
|
- name: Unpack build artifact
|
||||||
|
run: unzip -q ${{ env.ARCHIVE_NAME }}
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Capture screenshots
|
||||||
|
run: npm run capture:screenshots
|
||||||
|
|
||||||
|
- name: Upload screenshot artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pr-screenshots
|
||||||
|
path: |
|
||||||
|
screenshots/home.png
|
||||||
|
screenshots/editing.png
|
||||||
|
screenshots/configuration.png
|
||||||
|
|
||||||
|
publish_screenshot_preview:
|
||||||
|
name: Publish Pull Request Screenshot Preview
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: capture_screenshots
|
||||||
|
if: ${{ gitea.event.pull_request.head.repo.full_name == gitea.repository }}
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Set up required tools
|
||||||
|
run: sudo apt-get install curl jq -y
|
||||||
|
|
||||||
|
- name: Download screenshot artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: pr-screenshots
|
||||||
|
path: screenshots
|
||||||
|
|
||||||
|
- name: Update pull request screenshot preview
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
|
||||||
|
GITEA_TOKEN: ${{ gitea.token }}
|
||||||
|
REPOSITORY: ${{ gitea.repository }}
|
||||||
|
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
OWNER="${REPOSITORY%%/*}"
|
||||||
|
REPO="${REPOSITORY#*/}"
|
||||||
|
MARKER='<!-- vision-start-pr-screenshots -->'
|
||||||
|
COMMENTS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/$PR_NUMBER/comments"
|
||||||
|
COMMENTS=$(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$COMMENTS_URL")
|
||||||
|
COMMENT_ID=$(printf '%s' "$COMMENTS" | jq -r --arg marker "$MARKER" '.[] | select(.body | contains($marker)) | .id' | tail -n 1)
|
||||||
|
|
||||||
|
if [ -z "$COMMENT_ID" ] || [ "$COMMENT_ID" = "null" ]; then
|
||||||
|
PAYLOAD=$(jq -n --arg body "$MARKER" '{body: $body}')
|
||||||
|
COMMENT_ID=$(curl -fsS -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data "$PAYLOAD" \
|
||||||
|
"$COMMENTS_URL" | jq -r '.id')
|
||||||
|
fi
|
||||||
|
|
||||||
|
ASSETS_URL="$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID/assets"
|
||||||
|
for ASSET_ID in $(curl -fsS -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL" | jq -r '.[].id'); do
|
||||||
|
curl -fsS -X DELETE -H "Authorization: token $GITEA_TOKEN" "$ASSETS_URL/$ASSET_ID"
|
||||||
|
done
|
||||||
|
|
||||||
|
upload_asset() {
|
||||||
|
curl -fsS -X POST \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-F "attachment=@$1;type=image/png" \
|
||||||
|
"$ASSETS_URL" | jq -r '.browser_download_url'
|
||||||
|
}
|
||||||
|
|
||||||
|
HOME_URL=$(upload_asset screenshots/home.png)
|
||||||
|
EDITING_URL=$(upload_asset screenshots/editing.png)
|
||||||
|
CONFIGURATION_URL=$(upload_asset screenshots/configuration.png)
|
||||||
|
BODY=$(printf '%s\n\n%s\n\n%s\n%s\n\n%s\n%s\n\n%s\n%s' \
|
||||||
|
"$MARKER" \
|
||||||
|
'## Visual preview' \
|
||||||
|
'### Home' \
|
||||||
|
"" \
|
||||||
|
'### Editing' \
|
||||||
|
"" \
|
||||||
|
'### Configuration' \
|
||||||
|
"")
|
||||||
|
PAYLOAD=$(jq -n --arg body "$BODY" '{body: $body}')
|
||||||
|
curl -fsS -X PATCH \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
--data "$PAYLOAD" \
|
||||||
|
"$GITEA_API_URL/repos/$OWNER/$REPO/issues/comments/$COMMENT_ID"
|
||||||
@@ -102,7 +102,7 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build, virus-total-check]
|
needs: [build, virus-total-check, capture_screenshots]
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -110,6 +110,11 @@ jobs:
|
|||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: release-zip
|
name: release-zip
|
||||||
|
- name: Download screenshot artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-screenshots
|
||||||
|
path: release-screenshots
|
||||||
- name: Release zip
|
- name: Release zip
|
||||||
uses: akkuman/gitea-release-action@v1
|
uses: akkuman/gitea-release-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -120,7 +125,11 @@ jobs:
|
|||||||
**Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }}
|
**Virus Total Detection Ratio:** ${{ needs.virus-total-check.outputs.detection-ratio }}
|
||||||
name: ${{ gitea.ref_name }}
|
name: ${{ gitea.ref_name }}
|
||||||
tag_name: ${{ gitea.ref_name }}
|
tag_name: ${{ gitea.ref_name }}
|
||||||
files: vision-start-${{ gitea.ref_name }}.zip
|
files: |
|
||||||
|
vision-start-${{ gitea.ref_name }}.zip
|
||||||
|
release-screenshots/home.png
|
||||||
|
release-screenshots/editing.png
|
||||||
|
release-screenshots/configuration.png
|
||||||
|
|
||||||
build_vision_start:
|
build_vision_start:
|
||||||
name: Build Vision Start Image
|
name: Build Vision Start Image
|
||||||
@@ -184,3 +193,36 @@ jobs:
|
|||||||
cd ${{ secrets.PROD_DIR }}
|
cd ${{ secrets.PROD_DIR }}
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d --force-recreate
|
docker compose up -d --force-recreate
|
||||||
|
|
||||||
|
capture_screenshots:
|
||||||
|
name: Capture Vision Start Screenshots
|
||||||
|
runs-on: ubuntu-amd64
|
||||||
|
needs: deploy_vision_start
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Install JS dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Capture release screenshots
|
||||||
|
env:
|
||||||
|
SCREENSHOT_BASE_URL: http://vision-start.ivanch.me
|
||||||
|
run: npm run capture:screenshots
|
||||||
|
|
||||||
|
- name: Upload release screenshots
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-screenshots
|
||||||
|
path: |
|
||||||
|
screenshots/home.png
|
||||||
|
screenshots/editing.png
|
||||||
|
screenshots/configuration.png
|
||||||
|
|||||||
48
package-lock.json
generated
48
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"playwright": "1.61.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
@@ -2387,6 +2388,53 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"capture:screenshots": "node scripts/capture_screenshots.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hello-pangea/dnd": "^18.0.1",
|
"@hello-pangea/dnd": "^18.0.1",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"@types/react": "^19.1.8",
|
"@types/react": "^19.1.8",
|
||||||
"@types/react-dom": "^19.1.5",
|
"@types/react-dom": "^19.1.5",
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
|
"playwright": "1.61.1",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ Live instances / artifacts:
|
|||||||
| Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` |
|
| Container | Node 22 Alpine build stage → nginx Alpine serving `dist/` |
|
||||||
| Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` |
|
| Extension packaging | Manifest V3 (`manifest.json`) consuming `dist/` + `manifest.json` zipped as `vision-start-<tag>.zip` |
|
||||||
| CI/CD | Gitea Actions workflows (`.gitea/workflows/`) |
|
| CI/CD | Gitea Actions workflows (`.gitea/workflows/`) |
|
||||||
|
| Release screenshots | Playwright 1.61.1 + Chromium capture the deployed production page at a fixed 1280×800 viewport |
|
||||||
|
|
||||||
Entry points: `index.html` → `index.tsx` → `App.tsx`.
|
Entry points: `index.html` → `index.tsx` → `App.tsx`.
|
||||||
|
|
||||||
@@ -125,13 +126,16 @@ vision-start/
|
|||||||
│ ├── favicon.ico
|
│ ├── favicon.ico
|
||||||
│ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build)
|
│ └── icon-metadata.json # Dashboard Icons metadata (gitignored; fetched at release build)
|
||||||
│
|
│
|
||||||
├── screenshots/ # README screenshots (dark page, editing, configuration)
|
├── screenshots/ # README/release screenshots (home, editing, configuration; regenerated at 1280×800)
|
||||||
├── scripts/
|
├── scripts/
|
||||||
|
│ ├── capture_screenshots.mjs # Loads demoData, captures home/edit/configuration with Playwright, and can target a deployed URL
|
||||||
|
│ ├── demoData.json # Base64-encoded localStorage fixture used for release screenshots
|
||||||
│ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons
|
│ ├── prepare_release.sh # Downloads icon-metadata.json from homarr-labs/dashboard-icons
|
||||||
│ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict
|
│ └── check_virustotal.sh # Uploads the release zip to VirusTotal, waits for & reports the verdict
|
||||||
│
|
│
|
||||||
├── .gitea/workflows/
|
├── .gitea/workflows/
|
||||||
│ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging
|
│ ├── main.yaml # On push to main: build, push staging Docker image, SSH-deploy to staging
|
||||||
|
│ ├── pull-request.yaml # On PR updates: build/zip, capture screenshots, and publish an inline visual preview
|
||||||
│ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod
|
│ └── release.yaml # On v* tag: build, zip, VirusTotal check, Gitea release, push latest image, SSH-deploy to prod
|
||||||
│
|
│
|
||||||
├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf)
|
├── Dockerfile # Node 22 build → nginx serving dist/ (with gzip via nginx.conf)
|
||||||
@@ -209,6 +213,8 @@ Build, then combine `dist/` + `manifest.json` into a folder and "Load unpacked"
|
|||||||
|
|
||||||
### CI/CD (Gitea Actions)
|
### CI/CD (Gitea Actions)
|
||||||
- `release.yaml` validates each `vX.Y.Z` tag and replaces tracked `v0.0.0`/`0.0.0` placeholders in each build checkout before producing the extension archive and production image.
|
- `release.yaml` validates each `vX.Y.Z` tag and replaces tracked `v0.0.0`/`0.0.0` placeholders in each build checkout before producing the extension archive and production image.
|
||||||
|
- **`pull-request.yaml`** — Triggers on pull request open, reopen, and synchronization. It builds and uploads a PR extension archive, unpacks that archive in a separate Playwright job to generate the three demo screenshots, and uploads both artifacts. For same-repository PRs, it maintains one Gitea PR comment with inline image attachments; fork PRs retain artifacts but skip the comment because their workflow token is read-only.
|
||||||
|
- After `deploy_vision_start` succeeds, `release.yaml` uses Playwright Chromium against the deployed production page and seeds each browser context from `scripts/demoData.json`. It regenerates `home.png`, `editing.png`, and `configuration.png` at exactly 1280×800, uploads them as artifacts, and attaches them as individual Gitea release assets.
|
||||||
- **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`.
|
- **`main.yaml`** — Triggers on push to `main` (and `workflow_dispatch`). Builds, pushes a `staging` multi-arch (amd64/arm64) image to `git.ivanch.me/ivanch/vision-start:staging`, then SSH-deploys on the staging host via `docker compose up -d --force-recreate`.
|
||||||
- **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production.
|
- **`release.yaml`** — Triggers on `v*` tags. Builds, zips `dist/` + `manifest.json` as `vision-start-<tag>.zip`, runs `scripts/check_virustotal.sh` against it (publishes analysis URL + detection ratio on the release body), creates a Gitea release, pushes a `latest` multi-arch image, and SSH-deploys to production.
|
||||||
|
|
||||||
@@ -248,7 +254,7 @@ External assets fetched at build time by `scripts/prepare_release.sh`:
|
|||||||
| Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` |
|
| Icon fetch / picker / metadata | `components/utils/iconService.ts`, `components/WebsiteEditModal.tsx`, `public/icon-metadata.json` |
|
||||||
| chrome.storage.local access | `components/utils/StorageLocalManager.ts` |
|
| chrome.storage.local access | `components/utils/StorageLocalManager.ts` |
|
||||||
| Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) |
|
| Export/import config | `components/services/ConfigurationService.ts` (`exportConfig`, `importConfig`) |
|
||||||
| Release packaging | `scripts/prepare_release.sh`, `scripts/check_virustotal.sh`, `.gitea/workflows/release.yaml` |
|
| Build/release/PR pipelines | `scripts/prepare_release.sh`, `scripts/capture_screenshots.mjs`, `scripts/check_virustotal.sh`, `.gitea/workflows/pull-request.yaml`, `.gitea/workflows/release.yaml` |
|
||||||
| Docker build | `Dockerfile` |
|
| Docker build | `Dockerfile` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
190
scripts/capture_screenshots.mjs
Normal file
190
scripts/capture_screenshots.mjs
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { mkdir, readFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
|
||||||
|
const viewport = { width: 1280, height: 800 };
|
||||||
|
const host = '127.0.0.1';
|
||||||
|
const port = 4173;
|
||||||
|
const configuredBaseUrl = process.env.SCREENSHOT_BASE_URL?.replace(/\/+$/, '');
|
||||||
|
const baseUrl = configuredBaseUrl || `http://${host}:${port}`;
|
||||||
|
const outputDirectory = process.env.SCREENSHOT_OUTPUT_DIR || 'screenshots';
|
||||||
|
const demoDataPath = process.env.SCREENSHOT_DEMO_DATA || 'scripts/demoData.json';
|
||||||
|
const viteCli = path.resolve('node_modules/vite/bin/vite.js');
|
||||||
|
const imgurImageCache = new Map();
|
||||||
|
|
||||||
|
const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||||
|
|
||||||
|
const waitForServer = async (preview) => {
|
||||||
|
const deadline = Date.now() + 30_000;
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
if (previewError) throw previewError;
|
||||||
|
|
||||||
|
if (preview && preview.exitCode !== null) {
|
||||||
|
throw new Error(`Vite preview exited with code ${preview.exitCode}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(baseUrl);
|
||||||
|
if (response.ok) return;
|
||||||
|
lastError = new Error(`Vite preview returned ${response.status}.`);
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
await delay(250);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Screenshot target did not respond at ${baseUrl}. ${lastError?.message || ''}`.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDemoData = async () => {
|
||||||
|
const encodedData = JSON.parse(await readFile(demoDataPath, 'utf8'));
|
||||||
|
|
||||||
|
if (!encodedData || typeof encodedData !== 'object' || Array.isArray(encodedData)) {
|
||||||
|
throw new Error(`${demoDataPath} must contain an object of base64-encoded localStorage values.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(encodedData).map(([key, value]) => {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`${demoDataPath} contains a non-string value for ${key}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value: Buffer.from(value, 'base64').toString('utf8') };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fulfillImgurRequest = async (route) => {
|
||||||
|
const url = route.request().url();
|
||||||
|
let image = imgurImageCache.get(url);
|
||||||
|
|
||||||
|
if (!image) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
await route.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = {
|
||||||
|
body: Buffer.from(await response.arrayBuffer()),
|
||||||
|
contentType: response.headers.get('content-type') || 'image/jpeg',
|
||||||
|
};
|
||||||
|
imgurImageCache.set(url, image);
|
||||||
|
} catch {
|
||||||
|
await route.continue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
headers: { 'content-type': image.contentType },
|
||||||
|
body: image.body,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopPreview = async (preview) => {
|
||||||
|
if (preview.exitCode !== null) return;
|
||||||
|
|
||||||
|
const exited = new Promise((resolve) => preview.once('exit', resolve));
|
||||||
|
preview.kill('SIGTERM');
|
||||||
|
await Promise.race([exited, delay(5_000)]);
|
||||||
|
|
||||||
|
if (preview.exitCode === null) preview.kill('SIGKILL');
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertPngDimensions = (image, filename) => {
|
||||||
|
const width = image.readUInt32BE(16);
|
||||||
|
const height = image.readUInt32BE(20);
|
||||||
|
|
||||||
|
if (width !== viewport.width || height !== viewport.height) {
|
||||||
|
throw new Error(`${filename} was generated at ${width}x${height}, expected ${viewport.width}x${viewport.height}.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const capture = async (page, filename) => {
|
||||||
|
const image = await page.screenshot({
|
||||||
|
path: path.join(outputDirectory, filename),
|
||||||
|
type: 'png',
|
||||||
|
fullPage: false,
|
||||||
|
scale: 'css',
|
||||||
|
animations: 'disabled',
|
||||||
|
});
|
||||||
|
|
||||||
|
assertPngDimensions(image, filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async (page) => {
|
||||||
|
await page.goto(baseUrl, { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.locator('main').waitFor({ state: 'visible' });
|
||||||
|
await page.evaluate(() => document.fonts.ready);
|
||||||
|
await delay(1_500);
|
||||||
|
};
|
||||||
|
|
||||||
|
let previewError;
|
||||||
|
const preview = configuredBaseUrl
|
||||||
|
? undefined
|
||||||
|
: spawn(
|
||||||
|
process.execPath,
|
||||||
|
[viteCli, 'preview', '--host', host, '--port', String(port), '--strictPort'],
|
||||||
|
{ stdio: 'inherit' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (preview) {
|
||||||
|
preview.once('error', (error) => {
|
||||||
|
previewError = error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await waitForServer(preview);
|
||||||
|
|
||||||
|
await mkdir(outputDirectory, { recursive: true });
|
||||||
|
const demoData = await loadDemoData();
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const context = await browser.newContext({
|
||||||
|
viewport,
|
||||||
|
screen: viewport,
|
||||||
|
deviceScaleFactor: 1,
|
||||||
|
locale: 'en-US',
|
||||||
|
timezoneId: 'America/Sao_Paulo',
|
||||||
|
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
|
});
|
||||||
|
await context.route('https://i.imgur.com/**', fulfillImgurRequest);
|
||||||
|
await context.addInitScript((storageItems) => {
|
||||||
|
storageItems.forEach(({ key, value }) => localStorage.setItem(key, value));
|
||||||
|
}, demoData);
|
||||||
|
const page = await context.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadPage(page);
|
||||||
|
await capture(page, 'home.png');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Edit page' }).click();
|
||||||
|
await capture(page, 'editing.png');
|
||||||
|
|
||||||
|
await loadPage(page);
|
||||||
|
await page.getByRole('button', { name: 'Open configuration' }).click();
|
||||||
|
await page.getByRole('dialog').waitFor({ state: 'visible' });
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const drawer = document.querySelector('.liquid-drawer');
|
||||||
|
if (!drawer) return false;
|
||||||
|
const { left, right } = drawer.getBoundingClientRect();
|
||||||
|
return left < window.innerWidth && right <= window.innerWidth;
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
{ timeout: 5_000 },
|
||||||
|
);
|
||||||
|
await capture(page, 'configuration.png');
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (preview) await stopPreview(preview);
|
||||||
|
}
|
||||||
6
scripts/demoData.json
Normal file
6
scripts/demoData.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"categories": "W3siaWQiOiIxIiwibmFtZSI6IlNlYXJjaCIsIndlYnNpdGVzIjpbeyJpZCI6IjEiLCJuYW1lIjoiR29vZ2xlIiwidXJsIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbSIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1nb29nbGUuY29tJnN6PTEyOCIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQxODAxMDgiLCJuYW1lIjoiWW91VHViZSIsInVybCI6Imh0dHBzOi8vd3d3LnlvdXR1YmUuY29tLyIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy95b3V0dWJlLnN2ZyIsImNhdGVnb3J5SWQiOiIxIn0seyJpZCI6IjE3NTMwNDQyMjI2OTQiLCJuYW1lIjoiRHJpdmUiLCJ1cmwiOiJodHRwczovL2RyaXZlLmdvb2dsZS5jb20vZHJpdmUvdS8wL215LWRyaXZlIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dvb2dsZS1kcml2ZS5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDE0ODIwIiwibmFtZSI6IkdtYWlsIiwidXJsIjoiaHR0cHM6Ly9tYWlsLmdvb2dsZS5jb20vbWFpbC91LzAvP3RhYj13bSNpbmJveCIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9nbWFpbC5zdmciLCJjYXRlZ29yeUlkIjoiMSJ9LHsiaWQiOiIxNzUzMDQ0NDI3NTYwIiwibmFtZSI6Ikxhc3QuZm0iLCJ1cmwiOiJodHRwczovL3d3dy5sYXN0LmZtL2hvbWUiLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49d3d3Lmxhc3QuZm0mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjEifV19LHsiaWQiOiIxNzUzMDQ0NDQ2NzU1IiwibmFtZSI6IkNvZGUiLCJ3ZWJzaXRlcyI6W3siaWQiOiIxNzUzMDQ0NDU5MjcyIiwibmFtZSI6IkdpdGh1YiIsInVybCI6Imh0dHBzOi8vZ2l0aHViLmNvbS9mZWVkIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2dpdGh1Yi1saWdodC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4ODcwNjgzIiwibmFtZSI6IkFXUyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9hd3MtbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ0NDY3NTUifSx7ImlkIjoiMTc4MzcyODkxNTg1MyIsIm5hbWUiOiJEb2NrZXIgSHViIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2RvY2tlci5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDQ0Njc1NSJ9LHsiaWQiOiIxNzgzNzI4OTkyMzkwIiwibmFtZSI6IkZpZ21hIiwidXJsIjoiIiwiaWNvbiI6Imh0dHBzOi8vY2RuLmpzZGVsaXZyLm5ldC9naC9ob21hcnItbGFicy9kYXNoYm9hcmQtaWNvbnMvc3ZnL2ZpZ21hLnN2ZyIsImNhdGVnb3J5SWQiOiIxNzUzMDQ0NDQ2NzU1In1dfSx7ImlkIjoiMTc1MzA0NDY2ODAxMiIsIm5hbWUiOiJNZWRpYSIsIndlYnNpdGVzIjpbeyJpZCI6IjE3NTMwNDQ3MDYwMjQiLCJuYW1lIjoiTmV0ZmxpeCIsInVybCI6Imh0dHA6Ly90di5oYXZlbi8iLCJpY29uIjoiaHR0cHM6Ly9jZG4uanNkZWxpdnIubmV0L2doL2hvbWFyci1sYWJzL2Rhc2hib2FyZC1pY29ucy9zdmcvbmV0ZmxpeC5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzUzMDQ0NzQwOTg2IiwibmFtZSI6IlByaW1lIiwidXJsIjoiaHR0cDovL29tdi5oYXZlbiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9wcmltZS12aWRlby1hbHQtZGFyay5zdmciLCJjYXRlZ29yeUlkIjoiMTc1MzA0NDY2ODAxMiJ9LHsiaWQiOiIxNzgzNzI4Nzg0MjU2IiwibmFtZSI6IkhCTyIsInVybCI6IiIsImljb24iOiJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvZ2gvaG9tYXJyLWxhYnMvZGFzaGJvYXJkLWljb25zL3N2Zy9oYm8tbGlnaHQuc3ZnIiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc1MzA0NDgyODg0MCIsIm5hbWUiOiJUcmFrdCIsInVybCI6Imh0dHBzOi8vYXBwLnRyYWt0LnR2LyIsImljb24iOiJodHRwczovL3d3dy5nb29nbGUuY29tL3MyL2Zhdmljb25zP2RvbWFpbj1hcHAudHJha3QudHYmc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifSx7ImlkIjoiMTc4MzcyODcyOTQyNCIsIm5hbWUiOiJMZXR0ZXJib3hkIiwidXJsIjoiaHR0cHM6Ly9sZXR0ZXJib3hkLmNvbS8iLCJpY29uIjoiaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zMi9mYXZpY29ucz9kb21haW49bGV0dGVyYm94ZC5jb20mc3o9MTI4IiwiY2F0ZWdvcnlJZCI6IjE3NTMwNDQ2NjgwMTIifV19XQ==",
|
||||||
|
"config": "eyJ0aXRsZSI6IiIsImN1cnJlbnRXYWxscGFwZXJzIjpbIkFic3RyYWN0IFJlZCJdLCJ3YWxscGFwZXJGcmVxdWVuY3kiOiIxZCIsIndhbGxwYXBlckJsdXIiOjAsIndhbGxwYXBlckJyaWdodG5lc3MiOjEwOCwid2FsbHBhcGVyT3BhY2l0eSI6MTAwLCJ0aXRsZVNpemUiOiJtZWRpdW0iLCJhbGlnbm1lbnQiOiJtaWRkbGUiLCJob3Jpem9udGFsQWxpZ25tZW50IjoibWlkZGxlIiwidGlsZVNpemUiOiJzbWFsbCIsImNsb2NrIjp7ImVuYWJsZWQiOnRydWUsInNpemUiOiJ0aW55IiwiZm9udCI6Im1vbm9zcGFjZSIsImZvcm1hdCI6Img6bW0gQSJ9LCJzZXJ2ZXJXaWRnZXQiOnsiZW5hYmxlZCI6dHJ1ZSwicGluZ0ZyZXF1ZW5jeSI6MTUsInNlcnZlcnMiOlt7ImlkIjoiMTc4MzcyOTE4MzU1MyIsIm5hbWUiOiJBZEd1YXJkIiwiYWRkcmVzcyI6Imh0dHBzOi8vZ29vZ2xlLmNvbSJ9LHsiaWQiOiIxNzgzNzI5MjI3NDQyIiwibmFtZSI6IlByb3htb3giLCJhZGRyZXNzIjoiaHR0cHM6Ly9nb29nbGUuY29tIn1dfX0=",
|
||||||
|
"userWallpapers": "W3sibmFtZSI6ImRhcmstYWJzdHJhY3QtMjU2MHgxNDQwLWNvbnRlbXBvcmFyeS1kZXNpZ24tc2xlZWstbGluZXMtMjY0MjYuanBnIn1d",
|
||||||
|
"wallpaperState": "eyJsYXN0V2FsbHBhcGVyQ2hhbmdlIjoiMjA0Ni0wNy0xMVQwMDoyMzozMS4zODdaIiwiY3VycmVudEluZGV4IjowfQ=="
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user