From 2c68cc22ee5e3505f908d287bd84ab292352b01a Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 19 Apr 2026 22:06:07 +0200 Subject: [PATCH 1/5] refactor: update build process and Docker configuration for improved deployment - Changed the build command in `package.json` from `npm run build:web` to `bun run build:web` for consistency with the new build tool. - Updated `docker-compose.yml` to reflect changes in service naming and port exposure, ensuring the application listens on port 1420. - Enhanced the `Dockerfile` to use `bun` for building the application, improving build efficiency and aligning with the updated package structure. - Revised healthcheck and logging configurations in the Dockerfile for better monitoring and reliability during deployment. --- deploy/Dockerfile | 41 ++++++++++++++++++++----------------- deploy/docker-compose.yml | 40 +++++++++++++++++++++++------------- doc/guides/deploying-web.md | 15 +++++++------- package.json | 2 +- 4 files changed, 57 insertions(+), 41 deletions(-) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 83b8383..e6ffe53 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,41 +1,44 @@ -# pengine-web: **website only** — same static bundle the Tauri desktop shell loads from -# `dist/`, without building or running the native app. -# -# Pipeline build step: `npm run build:web` (see `package.json`). Local/Tauri may use `bun run build` -# to the same `build:web` script. `build.frontendDist` → `../dist` (this image serves that on :80). -# -# Intentionally omitted: `tauri build`, Cargo, `src-tauri/` (see /.dockerignore). +# pengine-web: Vite → `dist/`, served by static-web-server (not Next.js — no `.next` or `node server.js`). # # Build: docker build -f deploy/Dockerfile . -# Context: repository root (see /.dockerignore). +# Context: repository root (see /.dockerignore). `src-tauri/` is excluded. + +# --- Build stage --- +FROM oven/bun:1.2.23-alpine AS builder -FROM node:22-alpine WORKDIR /app ENV HUSKY=0 ENV NODE_ENV=production +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile --ignore-scripts + +COPY . . + ARG VITE_APP_ORIGIN=https://pengine.net ENV VITE_APP_ORIGIN=${VITE_APP_ORIGIN} -COPY . . -RUN npm install --frozen-lockfile --ignore-scripts -RUN npm run build +RUN bun run build:web + +# --- Production: static file server (default CMD/ENTRYPOINT from upstream image) --- +FROM ghcr.io/static-web-server/static-web-server:2-alpine AS runner -FROM ghcr.io/static-web-server/static-web-server:2-alpine USER root -RUN apk add --no-cache wget +RUN apk add --no-cache curl wget USER sws -COPY --chown=sws:sws --from=build /app/dist /home/sws/public +COPY --chown=sws:sws --from=builder /app/dist /home/sws/public ENV SERVER_HOST=0.0.0.0 -ENV SERVER_PORT=80 +# Same port as Vite dev (`vite.config.ts`) for a consistent ops story. +ENV SERVER_PORT=1420 ENV SERVER_ROOT=/home/sws/public ENV SERVER_FALLBACK_PAGE=/home/sws/public/index.html ENV SERVER_LOG_LEVEL=error -EXPOSE 80 +EXPOSE 1420 -HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ - CMD wget -qO- http://127.0.0.1/ >/dev/null || exit 1 +# SPA: fallback page is already set; short start — no Node boot delay +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD wget -qO- http://127.0.0.1:1420/ >/dev/null || exit 1 diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 3cfdad0..9c6c3aa 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,19 +1,31 @@ -# Runs the pengine web image on the deploy host (plain HTTP on :80 in-container, -# published to 127.0.0.1:1420). Reverse-proxy / TLS is not part of this repo — -# configure that in your ops repo (e.g. nginx → http://127.0.0.1:1420). -# -# PENGINE_WEB_IMAGE is set by CI (e.g. ghcr.io//pengine-web:1.0.1). -# Docker Compose substitutes it from the environment at `up` time. - services: - web: - image: ${PENGINE_WEB_IMAGE:?PENGINE_WEB_IMAGE is required} - container_name: pengine + pengine-web: + image: ${PENGINE_WEB_IMAGE:-ghcr.io/pengine-ai/pengine-web:latest} + container_name: pengine-app + hostname: pengine restart: unless-stopped - ports: - - "127.0.0.1:1420:80" + expose: + - "1420" + environment: + # static-web-server (see deploy/Dockerfile); override here if needed behind a proxy + - SERVER_HOST=0.0.0.0 + - SERVER_PORT=1420 + - SERVER_ROOT=/home/sws/public + - SERVER_FALLBACK_PAGE=/home/sws/public/index.html + - SERVER_LOG_LEVEL=error + networks: + pengui-network: + aliases: + - pengine + - app healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"] + test: ["CMD", "wget", "-qO-", "http://127.0.0.1:1420/"] interval: 30s - timeout: 3s + timeout: 5s retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" diff --git a/doc/guides/deploying-web.md b/doc/guides/deploying-web.md index 28e7986..5c117d2 100644 --- a/doc/guides/deploying-web.md +++ b/doc/guides/deploying-web.md @@ -9,10 +9,11 @@ workflow. It has two jobs: 1. **Build and push image (if not in GHCR)** — If `ghcr.io//pengine-web:` is **missing**, checks out that ref and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from the repo root (see [`/.dockerignore`](../../.dockerignore)): `bun install` → [`npm run build:web`](../../package.json) → [static-web-server](https://github.com/static-web-server/static-web-server) with **`SERVER_FALLBACK_PAGE`**. CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. Pushes `:`, `:sha-`, and `:latest`. If the package **already exists**, this job **skips** the build and logs that fact. **Packaging** loads `deploy/Dockerfile` via the **GitHub API** from the **default branch** (with fallbacks), then builds — so it always matches `main`, not an old copy on the tag. **`deploy/docker-compose.yml`** for the host is fetched the same way in the deploy job. **App sources** (`package.json`, `src/`, …) still come from the **tag** checkout. 2. **Deploy to host** — fetches [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) via the **GitHub Contents API** (no checkout on the runner). It tries your **`tag`** ref first, then the **default branch**, **`main`**, **`master`**. **`PENGINE_WEB_IMAGE`** on the host still uses your **`tag`**. Then SSH copies the file to `~/pengine`, and the host runs **`docker login` → `docker compose pull` → `docker compose up`**. -The container publishes on `127.0.0.1:1420`. **TLS and reverse-proxy (e.g. nginx) -for the public site are not defined in this repository** — maintain that in -your ops / infrastructure repo and point it at `http://127.0.0.1:1420` (or -adjust the published port in `docker-compose.yml` to match your layout). +The app listens on **port 1420** inside the container (static-web-server — same +as Vite dev in `vite.config.ts`). Add a **host** port mapping in +`docker-compose.yml` if the reverse proxy runs on the same machine (e.g. +`ports: "127.0.0.1:8080:1420"`). **TLS and reverse-proxy** for the public site are +not defined in this repository — point nginx (or similar) at that upstream URL. ## Triggers @@ -117,8 +118,8 @@ mkdir -p ~/pengine ``` Configure your external reverse-proxy (from your other repository) to forward -HTTPS traffic to `http://127.0.0.1:1420` — that is the contract between host -ingress and this deployment. +HTTPS traffic to the host port you mapped to container **1420** (for example +`http://127.0.0.1:8080` if you use `8080:1420`). ## Verifying a deploy @@ -136,6 +137,6 @@ already in GHCR (deploy-only), or rebuild from that git tag if needed. ```bash docker build -f deploy/Dockerfile --build-arg VITE_APP_ORIGIN=https://pengine.net -t pengine-web:local . -docker run --rm -p 8080:80 pengine-web:local +docker run --rm -p 8080:1420 pengine-web:local # Open http://127.0.0.1:8080 ``` diff --git a/package.json b/package.json index fc3f33e..cfcf53c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build:web": "tsc && vite build", - "build": "npm run build:web", + "build": "bun run build:web", "preview": "vite preview", "tauri": "tauri", "test:e2e": "playwright test", From deea0fdf44af8be21fab92acfcf612399e19462b Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 19 Apr 2026 22:10:57 +0200 Subject: [PATCH 2/5] refactor: update Docker configuration and deployment documentation for port changes - Changed the exposed port in `docker-compose.yml` from 1420 to 1422 to align with the updated Vite preview configuration. - Updated healthcheck command in the Dockerfile to reflect the new port. - Revised deployment documentation to indicate the new port for the application and updated commands for running the container. - Switched the build process in the Dockerfile to use `npm` instead of `bun`, ensuring consistency across environments. --- deploy/Dockerfile | 44 ++++++++++++++----------------------- deploy/docker-compose.yml | 14 +++++------- doc/guides/deploying-web.md | 18 +++++++-------- 3 files changed, 31 insertions(+), 45 deletions(-) diff --git a/deploy/Dockerfile b/deploy/Dockerfile index e6ffe53..313732d 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -1,44 +1,34 @@ -# pengine-web: Vite → `dist/`, served by static-web-server (not Next.js — no `.next` or `node server.js`). -# -# Build: docker build -f deploy/Dockerfile . -# Context: repository root (see /.dockerignore). `src-tauri/` is excluded. - -# --- Build stage --- -FROM oven/bun:1.2.23-alpine AS builder - +FROM node:22-alpine AS builder WORKDIR /app ENV HUSKY=0 ENV NODE_ENV=production -COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile --ignore-scripts +COPY package.json package-lock.json ./ +RUN npm ci --ignore-scripts COPY . . ARG VITE_APP_ORIGIN=https://pengine.net ENV VITE_APP_ORIGIN=${VITE_APP_ORIGIN} -RUN bun run build:web +RUN npm run build:web + +FROM node:22-alpine AS runner +WORKDIR /app + +RUN apk add --no-cache curl -# --- Production: static file server (default CMD/ENTRYPOINT from upstream image) --- -FROM ghcr.io/static-web-server/static-web-server:2-alpine AS runner +ENV NODE_ENV=production -USER root -RUN apk add --no-cache curl wget -USER sws +COPY --from=builder /app /app -COPY --chown=sws:sws --from=builder /app/dist /home/sws/public +RUN chown -R node:node /app +USER node -ENV SERVER_HOST=0.0.0.0 -# Same port as Vite dev (`vite.config.ts`) for a consistent ops story. -ENV SERVER_PORT=1420 -ENV SERVER_ROOT=/home/sws/public -ENV SERVER_FALLBACK_PAGE=/home/sws/public/index.html -ENV SERVER_LOG_LEVEL=error +EXPOSE 1422 -EXPOSE 1420 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \ + CMD curl -fsS http://127.0.0.1:1422/ >/dev/null || exit 1 -# SPA: fallback page is already set; short start — no Node boot delay -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget -qO- http://127.0.0.1:1420/ >/dev/null || exit 1 +CMD ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "1422"] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 9c6c3aa..a6872f5 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -5,25 +5,21 @@ services: hostname: pengine restart: unless-stopped expose: - - "1420" + - "1422" environment: - # static-web-server (see deploy/Dockerfile); override here if needed behind a proxy - - SERVER_HOST=0.0.0.0 - - SERVER_PORT=1420 - - SERVER_ROOT=/home/sws/public - - SERVER_FALLBACK_PAGE=/home/sws/public/index.html - - SERVER_LOG_LEVEL=error + # Vite preview (`npm run preview` in image); port fixed in Dockerfile CMD + - NODE_ENV=production networks: pengui-network: aliases: - pengine - app healthcheck: - test: ["CMD", "wget", "-qO-", "http://127.0.0.1:1420/"] + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:1422/"] interval: 30s timeout: 5s retries: 3 - start_period: 10s + start_period: 25s logging: driver: "json-file" options: diff --git a/doc/guides/deploying-web.md b/doc/guides/deploying-web.md index 5c117d2..100a6e6 100644 --- a/doc/guides/deploying-web.md +++ b/doc/guides/deploying-web.md @@ -6,13 +6,13 @@ The pengine web bundle is deployed to a remote host via the [`Deploy web app`](../../.github/workflows/web-deploy.yml) GitHub Actions workflow. It has two jobs: -1. **Build and push image (if not in GHCR)** — If `ghcr.io//pengine-web:` is **missing**, checks out that ref and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from the repo root (see [`/.dockerignore`](../../.dockerignore)): `bun install` → [`npm run build:web`](../../package.json) → [static-web-server](https://github.com/static-web-server/static-web-server) with **`SERVER_FALLBACK_PAGE`**. CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. Pushes `:`, `:sha-`, and `:latest`. If the package **already exists**, this job **skips** the build and logs that fact. **Packaging** loads `deploy/Dockerfile` via the **GitHub API** from the **default branch** (with fallbacks), then builds — so it always matches `main`, not an old copy on the tag. **`deploy/docker-compose.yml`** for the host is fetched the same way in the deploy job. **App sources** (`package.json`, `src/`, …) still come from the **tag** checkout. +1. **Build and push image (if not in GHCR)** — If `ghcr.io//pengine-web:` is **missing**, checks out that ref and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from the repo root (see [`/.dockerignore`](../../.dockerignore)): **`npm ci`** → [`npm run build:web`](../../package.json) → runtime runs **[`vite preview`](https://vite.dev/guide/cli#vite-preview)** (same script as local **`bun run preview`** / **`npm run preview`**). CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. Pushes `:`, `:sha-`, and `:latest`. If the package **already exists**, this job **skips** the build and logs that fact. **Packaging** loads `deploy/Dockerfile` via the **GitHub API** from the **default branch** (with fallbacks), then builds — so it always matches `main`, not an old copy on the tag. **`deploy/docker-compose.yml`** for the host is fetched the same way in the deploy job. **App sources** (`package.json`, `src/`, …) still come from the **tag** checkout. 2. **Deploy to host** — fetches [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) via the **GitHub Contents API** (no checkout on the runner). It tries your **`tag`** ref first, then the **default branch**, **`main`**, **`master`**. **`PENGINE_WEB_IMAGE`** on the host still uses your **`tag`**. Then SSH copies the file to `~/pengine`, and the host runs **`docker login` → `docker compose pull` → `docker compose up`**. -The app listens on **port 1420** inside the container (static-web-server — same -as Vite dev in `vite.config.ts`). Add a **host** port mapping in -`docker-compose.yml` if the reverse proxy runs on the same machine (e.g. -`ports: "127.0.0.1:8080:1420"`). **TLS and reverse-proxy** for the public site are +The app listens on **port 1422** inside the container (**Vite preview** — matches +`preview.port` in `vite.config.ts`; dev server stays on **1420**). Add a **host** +port mapping in `docker-compose.yml` if the reverse proxy runs on the same machine +(e.g. `ports: "127.0.0.1:8080:1422"`). **TLS and reverse-proxy** for the public site are not defined in this repository — point nginx (or similar) at that upstream URL. ## Triggers @@ -118,8 +118,8 @@ mkdir -p ~/pengine ``` Configure your external reverse-proxy (from your other repository) to forward -HTTPS traffic to the host port you mapped to container **1420** (for example -`http://127.0.0.1:8080` if you use `8080:1420`). +HTTPS traffic to the host port you mapped to container **1422** (for example +`http://127.0.0.1:8080` if you use `8080:1422`). ## Verifying a deploy @@ -137,6 +137,6 @@ already in GHCR (deploy-only), or rebuild from that git tag if needed. ```bash docker build -f deploy/Dockerfile --build-arg VITE_APP_ORIGIN=https://pengine.net -t pengine-web:local . -docker run --rm -p 8080:1420 pengine-web:local -# Open http://127.0.0.1:8080 +docker run --rm -p 1422:1422 pengine-web:local +# Open http://localhost:1422/ ``` From 9867aed70d286f0847f17b73f9867e59ce061b32 Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 19 Apr 2026 22:21:10 +0200 Subject: [PATCH 3/5] refactor: enhance web deployment workflow and update Docker configuration - Updated the GitHub Actions workflow to streamline the deployment process by removing outdated fetch commands for `deploy/Dockerfile` and `deploy/docker-compose.yml`, ensuring the latest files are used. - Added a checkout step to retrieve necessary files for the deployment job. - Modified `docker-compose.yml` to include a specific command for Vite preview, aligning with the updated deployment strategy. - Revised deployment documentation to reflect changes in the fetching logic and the new command for the Vite preview service. --- .github/workflows/web-deploy.yml | 76 +++++--------------------------- deploy/docker-compose.yml | 3 +- doc/guides/deploying-web.md | 2 +- 3 files changed, 13 insertions(+), 68 deletions(-) diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index ec330ea..c7d24eb 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -1,7 +1,10 @@ name: Deploy web app -# Job 1: If ghcr.io/.../pengine-web: is missing → checkout, build, push to GHCR. -# Job 2: Fetch compose (API), SSH → docker compose pull + up. +# Job 1: If ghcr.io/.../pengine-web: is missing → checkout, build, push to GHCR +# (Node `npm run build:web` + image CMD `npm run preview` for Vite preview on :1422). +# Job 2: Checkout (for compose file) → scp deploy/docker-compose.yml → SSH: +# docker login (GHCR) → docker compose pull / up. Compose comes from the workflow’s ref +# (branch you run the workflow on), not the deploy input tag. on: workflow_dispatch: @@ -73,38 +76,6 @@ jobs: ref: ${{ steps.meta.outputs.ref }} fetch-depth: 1 - # Old tags often carry a stale deploy/Dockerfile. Git fetch+checkout of main is unreliable on - # shallow clones. Overwrite deploy/Dockerfile from the GitHub API (same as main’s file). - - name: Fetch deploy/Dockerfile from default branch (Contents API) - if: steps.registry.outputs.exists != 'true' - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - set -euo pipefail - REPO="${{ github.repository }}" - TOKEN="${{ secrets.GITHUB_TOKEN }}" - mkdir -p deploy - USED="" - for r in "${DEFAULT_BRANCH:-}" "main" "master"; do - [ -z "${r:-}" ] && continue - if curl -fsSL \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -o /tmp/dockerfile-api.json \ - "https://api.github.com/repos/${REPO}/contents/deploy/Dockerfile?ref=${r}"; then - USED="$r" - echo "Fetched deploy/Dockerfile from ref: ${USED}" - break - fi - echo "No deploy/Dockerfile at ref=${r}, trying next…" - done - if [ -z "${USED}" ]; then - echo "::error::Could not fetch deploy/Dockerfile from API (tried default branch, main, master)." - exit 1 - fi - jq -r .content /tmp/dockerfile-api.json | base64 -d > deploy/Dockerfile - test -s deploy/Dockerfile - - name: Git short SHA if: steps.registry.outputs.exists != 'true' id: git @@ -136,34 +107,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 steps: - - name: Fetch deploy/docker-compose.yml (Contents API) - env: - REF: ${{ needs.build-package.outputs.ref }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - set -euo pipefail - REPO="${{ github.repository }}" - TOKEN="${{ secrets.GITHUB_TOKEN }}" - USED="" - for r in "${REF}" "${DEFAULT_BRANCH}" "main" "master"; do - [ -z "${r:-}" ] && continue - if curl -fsSL \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -o /tmp/compose-api.json \ - "https://api.github.com/repos/${REPO}/contents/deploy/docker-compose.yml?ref=${r}"; then - USED="$r" - echo "Fetched deploy/docker-compose.yml from ref: ${USED}" - break - fi - echo "No deploy/docker-compose.yml at ref=${r} (404 or no access), trying fallback…" - done - if [ -z "${USED}" ]; then - echo "::error::Could not fetch deploy/docker-compose.yml (tried tag/branch '${REF}', default branch, main, master)." - exit 1 - fi - jq -r .content /tmp/compose-api.json | base64 -d > docker-compose.upload.yml - test -s docker-compose.upload.yml + # Need repo files for scp (deploy job does not inherit build-package checkout). + - uses: actions/checkout@v4 - name: Ensure remote directory uses: appleboy/ssh-action@v1.2.5 @@ -183,11 +128,11 @@ jobs: key: ${{ secrets.DEPLOY_SSH_KEY }} port: 22 timeout: 2m - source: "docker-compose.upload.yml" - target: "~/pengine/" + source: deploy/docker-compose.yml + target: ~/pengine/ overwrite: true - - name: SSH — docker compose pull on host + - name: SSH — docker login, compose pull, up uses: appleboy/ssh-action@v1.2.5 env: GHCR_USER: ${{ github.repository_owner }} @@ -203,7 +148,6 @@ jobs: envs: GHCR_USER,GHCR_TOKEN,PENGINE_WEB_IMAGE script: | set -euo pipefail - mv -f ~/pengine/docker-compose.upload.yml ~/pengine/docker-compose.yml cd ~/pengine echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin export PENGINE_WEB_IMAGE diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index a6872f5..aec15cf 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -4,10 +4,11 @@ services: container_name: pengine-app hostname: pengine restart: unless-stopped + # Vite preview — serves `dist/` (must match deploy/Dockerfile CMD). + command: ["npm", "run", "preview", "--", "--host", "0.0.0.0", "--port", "1422"] expose: - "1422" environment: - # Vite preview (`npm run preview` in image); port fixed in Dockerfile CMD - NODE_ENV=production networks: pengui-network: diff --git a/doc/guides/deploying-web.md b/doc/guides/deploying-web.md index 100a6e6..97a04d1 100644 --- a/doc/guides/deploying-web.md +++ b/doc/guides/deploying-web.md @@ -7,7 +7,7 @@ The pengine web bundle is deployed to a remote host via the workflow. It has two jobs: 1. **Build and push image (if not in GHCR)** — If `ghcr.io//pengine-web:` is **missing**, checks out that ref and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from the repo root (see [`/.dockerignore`](../../.dockerignore)): **`npm ci`** → [`npm run build:web`](../../package.json) → runtime runs **[`vite preview`](https://vite.dev/guide/cli#vite-preview)** (same script as local **`bun run preview`** / **`npm run preview`**). CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. Pushes `:`, `:sha-`, and `:latest`. If the package **already exists**, this job **skips** the build and logs that fact. **Packaging** loads `deploy/Dockerfile` via the **GitHub API** from the **default branch** (with fallbacks), then builds — so it always matches `main`, not an old copy on the tag. **`deploy/docker-compose.yml`** for the host is fetched the same way in the deploy job. **App sources** (`package.json`, `src/`, …) still come from the **tag** checkout. -2. **Deploy to host** — fetches [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) via the **GitHub Contents API** (no checkout on the runner). It tries your **`tag`** ref first, then the **default branch**, **`main`**, **`master`**. **`PENGINE_WEB_IMAGE`** on the host still uses your **`tag`**. Then SSH copies the file to `~/pengine`, and the host runs **`docker login` → `docker compose pull` → `docker compose up`**. +2. **Deploy to host** — fetches [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) via the **GitHub Contents API** (no checkout on the runner). It tries the **default branch**, then **`main`**, **`master`**, then your workflow **`tag`** — so the host usually gets the **current** compose file (including the explicit **`vite preview`** `command:`) even when the tag is older. **`PENGINE_WEB_IMAGE`** on the host still uses your **`tag`** for the image. Then SSH copies the file to `~/pengine`, and the host runs **`docker login` → `docker compose pull` → `docker compose up`**, followed by a **curl** check to **`http://127.0.0.1:1422/`** inside the container. The app listens on **port 1422** inside the container (**Vite preview** — matches `preview.port` in `vite.config.ts`; dev server stays on **1420**). Add a **host** From 6a64af7674d6e73dea5edc882d04afe4cb644bbe Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 19 Apr 2026 22:24:03 +0200 Subject: [PATCH 4/5] refactor: improve web deployment workflow to handle missing Dockerfile - Enhanced the GitHub Actions workflow to fetch the `deploy/Dockerfile` from the default branch if it is missing in the current checkout, ensuring a reliable build process. - Updated comments in the workflow to clarify the new fetching logic and its importance for builds with older tags that lack the `deploy/` directory. --- .github/workflows/web-deploy.yml | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index c7d24eb..348802c 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -1,7 +1,8 @@ name: Deploy web app -# Job 1: If ghcr.io/.../pengine-web: is missing → checkout, build, push to GHCR -# (Node `npm run build:web` + image CMD `npm run preview` for Vite preview on :1422). +# Job 1: If ghcr.io/.../pengine-web: is missing → checkout **sources** at that tag, overlay +# **deploy/Dockerfile** from the default branch (API), then build and push to GHCR. +# Old tags often have no `deploy/` tree; without the overlay, buildx fails (lstat deploy/…). # Job 2: Checkout (for compose file) → scp deploy/docker-compose.yml → SSH: # docker login (GHCR) → docker compose pull / up. Compose comes from the workflow’s ref # (branch you run the workflow on), not the deploy input tag. @@ -76,6 +77,38 @@ jobs: ref: ${{ steps.meta.outputs.ref }} fetch-depth: 1 + # Tags/commits before `deploy/` existed have no Dockerfile in the checkout — load the canonical + # file from the repo default branch (same pattern as shallow clones that cannot git-fetch main). + - name: Fetch deploy/Dockerfile from default branch (Contents API) + if: steps.registry.outputs.exists != 'true' + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: | + set -euo pipefail + REPO="${{ github.repository }}" + TOKEN="${{ secrets.GITHUB_TOKEN }}" + mkdir -p deploy + USED="" + for r in "${DEFAULT_BRANCH:-}" "main" "master"; do + [ -z "${r:-}" ] && continue + if curl -fsSL \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -o /tmp/dockerfile-api.json \ + "https://api.github.com/repos/${REPO}/contents/deploy/Dockerfile?ref=${r}"; then + USED="$r" + echo "Fetched deploy/Dockerfile from ref: ${USED}" + break + fi + echo "No deploy/Dockerfile at ref=${r}, trying next…" + done + if [ -z "${USED}" ]; then + echo "::error::Could not fetch deploy/Dockerfile from API (tried default branch, main, master)." + exit 1 + fi + jq -r .content /tmp/dockerfile-api.json | base64 -d > deploy/Dockerfile + test -s deploy/Dockerfile + - name: Git short SHA if: steps.registry.outputs.exists != 'true' id: git From 96b5d680ef6e2ba064d8ff44971ed49bbfff290b Mon Sep 17 00:00:00 2001 From: MaximEdogawa Date: Sun, 19 Apr 2026 22:31:34 +0200 Subject: [PATCH 5/5] refactor: simplify web deployment workflow and update documentation - Revised the GitHub Actions workflow to remove checks for existing images in GHCR, ensuring that the image is always built and pushed for the specified tag. - Updated the deployment documentation to clarify the new workflow behavior, emphasizing that the image is always pushed to GHCR regardless of its prior existence. - Enhanced clarity in job descriptions and comments to reflect the streamlined process for building and deploying the web application. --- .github/workflows/web-deploy.yml | 67 +------------------------------- doc/guides/deploying-web.md | 18 ++++----- 2 files changed, 10 insertions(+), 75 deletions(-) diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 348802c..4101b87 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -1,17 +1,9 @@ name: Deploy web app - -# Job 1: If ghcr.io/.../pengine-web: is missing → checkout **sources** at that tag, overlay -# **deploy/Dockerfile** from the default branch (API), then build and push to GHCR. -# Old tags often have no `deploy/` tree; without the overlay, buildx fails (lstat deploy/…). -# Job 2: Checkout (for compose file) → scp deploy/docker-compose.yml → SSH: -# docker login (GHCR) → docker compose pull / up. Compose comes from the workflow’s ref -# (branch you run the workflow on), not the deploy input tag. - on: workflow_dispatch: inputs: tag: - description: "Registry / deploy tag (e.g. 1.0.2). Git ref for build must exist when the image is not yet in GHCR." + description: "Git tag / registry tag (e.g. 1.0.2). Image is built and pushed every run." required: true type: string @@ -28,7 +20,7 @@ env: jobs: build-package: - name: Build and push image (if not in GHCR) + name: Build and push image runs-on: ubuntu-latest timeout-minutes: 45 outputs: @@ -52,73 +44,18 @@ jobs: username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Check if package already exists in GHCR - id: registry - env: - TAG: ${{ steps.meta.outputs.tag }} - run: | - set -euo pipefail - if docker buildx imagetools inspect "${{ env.IMAGE }}:${TAG}" >/dev/null 2>&1; then - echo "exists=true" >>"$GITHUB_OUTPUT" - echo "Package ${{ env.IMAGE }}:${TAG} already in GHCR — skipping build and push." - else - echo "exists=false" >>"$GITHUB_OUTPUT" - echo "Package ${{ env.IMAGE }}:${TAG} not in GHCR — will build and push." - fi - - - name: No build needed - if: steps.registry.outputs.exists == 'true' - run: | - echo "Skipping Docker build; using existing image from registry." - - uses: actions/checkout@v4 - if: steps.registry.outputs.exists != 'true' with: ref: ${{ steps.meta.outputs.ref }} fetch-depth: 1 - # Tags/commits before `deploy/` existed have no Dockerfile in the checkout — load the canonical - # file from the repo default branch (same pattern as shallow clones that cannot git-fetch main). - - name: Fetch deploy/Dockerfile from default branch (Contents API) - if: steps.registry.outputs.exists != 'true' - env: - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - run: | - set -euo pipefail - REPO="${{ github.repository }}" - TOKEN="${{ secrets.GITHUB_TOKEN }}" - mkdir -p deploy - USED="" - for r in "${DEFAULT_BRANCH:-}" "main" "master"; do - [ -z "${r:-}" ] && continue - if curl -fsSL \ - -H "Authorization: Bearer ${TOKEN}" \ - -H "Accept: application/vnd.github+json" \ - -o /tmp/dockerfile-api.json \ - "https://api.github.com/repos/${REPO}/contents/deploy/Dockerfile?ref=${r}"; then - USED="$r" - echo "Fetched deploy/Dockerfile from ref: ${USED}" - break - fi - echo "No deploy/Dockerfile at ref=${r}, trying next…" - done - if [ -z "${USED}" ]; then - echo "::error::Could not fetch deploy/Dockerfile from API (tried default branch, main, master)." - exit 1 - fi - jq -r .content /tmp/dockerfile-api.json | base64 -d > deploy/Dockerfile - test -s deploy/Dockerfile - - name: Git short SHA - if: steps.registry.outputs.exists != 'true' id: git run: echo "short=$(git rev-parse --short HEAD)" >>"$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@v3 - if: steps.registry.outputs.exists != 'true' - name: Build and push package to GHCR - if: steps.registry.outputs.exists != 'true' uses: docker/build-push-action@v6 with: context: . diff --git a/doc/guides/deploying-web.md b/doc/guides/deploying-web.md index 97a04d1..feade93 100644 --- a/doc/guides/deploying-web.md +++ b/doc/guides/deploying-web.md @@ -6,8 +6,8 @@ The pengine web bundle is deployed to a remote host via the [`Deploy web app`](../../.github/workflows/web-deploy.yml) GitHub Actions workflow. It has two jobs: -1. **Build and push image (if not in GHCR)** — If `ghcr.io//pengine-web:` is **missing**, checks out that ref and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from the repo root (see [`/.dockerignore`](../../.dockerignore)): **`npm ci`** → [`npm run build:web`](../../package.json) → runtime runs **[`vite preview`](https://vite.dev/guide/cli#vite-preview)** (same script as local **`bun run preview`** / **`npm run preview`**). CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. Pushes `:`, `:sha-`, and `:latest`. If the package **already exists**, this job **skips** the build and logs that fact. **Packaging** loads `deploy/Dockerfile` via the **GitHub API** from the **default branch** (with fallbacks), then builds — so it always matches `main`, not an old copy on the tag. **`deploy/docker-compose.yml`** for the host is fetched the same way in the deploy job. **App sources** (`package.json`, `src/`, …) still come from the **tag** checkout. -2. **Deploy to host** — fetches [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) via the **GitHub Contents API** (no checkout on the runner). It tries the **default branch**, then **`main`**, **`master`**, then your workflow **`tag`** — so the host usually gets the **current** compose file (including the explicit **`vite preview`** `command:`) even when the tag is older. **`PENGINE_WEB_IMAGE`** on the host still uses your **`tag`** for the image. Then SSH copies the file to `~/pengine`, and the host runs **`docker login` → `docker compose pull` → `docker compose up`**, followed by a **curl** check to **`http://127.0.0.1:1422/`** inside the container. +1. **Build and push image** — Checks out the input tag and builds [`deploy/Dockerfile`](../../deploy/Dockerfile) from that revision. Steps: **`npm ci`** → [`npm run build:web`](../../package.json) → runtime **[`vite preview`](https://vite.dev/guide/cli#vite-preview)**. CI passes `VITE_APP_ORIGIN=https://pengine.net` as a **build-arg**. **Always** pushes to GHCR: `:`, `:sha-`, and `:latest`. +2. **Deploy to host** — checks out the workflow branch and **scp**’s [`deploy/docker-compose.yml`](../../deploy/docker-compose.yml) to the server, then SSH runs **`docker login`** → **`docker compose pull`** → **`docker compose up`**. **`PENGINE_WEB_IMAGE`** is **`ghcr.io//pengine-web:`** from the workflow input. The app listens on **port 1422** inside the container (**Vite preview** — matches `preview.port` in `vite.config.ts`; dev server stays on **1420**). Add a **host** @@ -17,12 +17,10 @@ not defined in this repository — point nginx (or similar) at that upstream URL ## Triggers -- **Manual only** — Actions → *Deploy web app* → *Run workflow*. You must enter a - **`tag`** (e.g. `1.0.1` or `v1.0.1`) that exists as a **git tag** on the remote. - The workflow checks out that tag, uses the same value (with optional leading `v` - stripped) as the **GHCR image tag**, and deploys with **`docker compose pull`** - on the host. If that image is **already** in the registry, the **build** job - skips; the **deploy** job still runs. +- **Manual only** — Actions → *Deploy web app* → *Run workflow*. Enter a **`tag`** + (e.g. `1.0.1` or `v1.0.1`) that exists as a **git ref** on the remote. The workflow + checks out that ref, builds the image, **always pushes** it to GHCR for that tag, + then deploys with **`docker compose pull`** on the host. - **App release** — [`App Release`](../../.github/workflows/app-release.yml) is separate (`v*` tags for desktop); web deploy uses the **`tag`** input above. @@ -130,8 +128,8 @@ ssh "$DEPLOY_USER@$DEPLOY_HOST" 'docker ps --filter name=pengine' curl -fsSL https://pengine.net/ | head ``` -To roll back, run *Deploy web app* again with an older **`tag`** whose image is -already in GHCR (deploy-only), or rebuild from that git tag if needed. +To roll back, run *Deploy web app* again with an older **`tag`** — the workflow +rebuilds and overwrites that tag’s image in GHCR, then deploys it. ## Local image build