From deac59d55a34427a7beee8efb35a2799e4da221d Mon Sep 17 00:00:00 2001 From: Frank Schwenk Date: Sun, 5 Jul 2026 16:47:59 +0200 Subject: [PATCH] feat: manual deploy-app workflow for apps/ with build support Add workflow_dispatch-only deploy-app action, deploy-app.sh script, apps/_template for Cursor, and SOUL/README split between static webhook deploy and app compose profiles. Co-authored-by: Cursor --- .gitea/workflows/deploy-app.yml | 39 +++++++++++++++++ README.md | 31 +++++++++++--- SOUL.md | 22 ++++++++-- apps/.gitkeep | 0 apps/_template/README.md | 47 +++++++++++++++++++++ apps/_template/workflow-deploy.yml.template | 19 +++++++++ hooks/deploy.sh | 2 + scripts/deploy-app.sh | 45 ++++++++++++++++++++ 8 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 .gitea/workflows/deploy-app.yml create mode 100644 apps/.gitkeep create mode 100644 apps/_template/README.md create mode 100644 apps/_template/workflow-deploy.yml.template create mode 100755 scripts/deploy-app.sh diff --git a/.gitea/workflows/deploy-app.yml b/.gitea/workflows/deploy-app.yml new file mode 100644 index 0000000..574c050 --- /dev/null +++ b/.gitea/workflows/deploy-app.yml @@ -0,0 +1,39 @@ +name: Deploy App (manual) + +on: + workflow_dispatch: + inputs: + app: + description: App slug (directory under apps/) + required: true + type: string + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout (validate app exists in repo) + uses: actions/checkout@v4 + + - name: Validate app slug + run: | + APP="${{ inputs.app }}" + if ! echo "$APP" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then + echo "Invalid app slug: $APP" + exit 1 + fi + if [ ! -d "apps/$APP" ]; then + echo "apps/$APP not found in repository" + exit 1 + fi + + - name: Build and deploy on boka + env: + DEPLOY_ROOT: ${{ secrets.DEPLOY_ROOT }} + DEPLOY_GIT_TOKEN: ${{ secrets.DEPLOY_GIT_TOKEN }} + DEPLOY_BRANCH: main + run: | + DEPLOY_ROOT="${DEPLOY_ROOT:-/home/frank/playground.schwenk.online}" + chmod +x scripts/deploy-app.sh + DEPLOY_ROOT="$DEPLOY_ROOT" DEPLOY_GIT_TOKEN="$DEPLOY_GIT_TOKEN" \ + scripts/deploy-app.sh "${{ inputs.app }}" diff --git a/README.md b/README.md index 4270498..2330462 100644 --- a/README.md +++ b/README.md @@ -7,18 +7,25 @@ Experimentier-Site auf [playground.schwenk.online](https://playground.schwenk.on 1. Issue in Gitea öffnen, z. B. *„Onepager: Thema XY“* 2. Kommentar: `@cursor recherchiere zu Thema XY und generiere einen Onepager als plain HTML` 3. Gitea Action ([cursor-gitea-agent](https://gitea.schwenk.online/froxxxy/cursor-gitea-agent)) committet nach `public//` auf `main` -4. Push-Webhook triggert `git pull` im Container-Stack → nginx liefert die Seite aus +4. Push-Webhook triggert `git pull` → nginx liefert statische Seiten aus + +**Apps** (npm, Docker, DB): Code unter `apps//`, Deploy nur manuell — siehe unten. Content-Konventionen: siehe [SOUL.md](SOUL.md). ## Repo-Layout ``` -public/ # Webroot +public/ # Webroot (statisch, auto-deploy per Push-Webhook) +apps/ # Web-Apps mit Build/Docker (manueller Deploy-Workflow) nginx/ # nginx-Konfiguration -hooks/ # Webhook-Skripte + hooks.json.template -deploy-hook/ # Dockerfile für Webhook-Container (git pull) -compose.yml # nginx + deploy-hook auf Traefik-Netz +hooks/ # Webhook-Skripte (nur git pull für static) +deploy-hook/ # Webhook-Container +scripts/ # deploy-app.sh (Gitea Action auf boka) +compose.yml # nginx + deploy-hook + optionale App-Profiles +.gitea/workflows/ + cursor.yml # @cursor in Issues + deploy-app.yml # manuell: Build + docker compose --profile ``` Auf boka ist `/home/frank/playground.schwenk.online` ein **Git-Clone** dieses Repos. @@ -29,6 +36,8 @@ Auf boka ist `/home/frank/playground.schwenk.online` ein **Git-Clone** dieses Re 2. **Repo-Secrets** (Namen ohne `GITEA_`-Prefix): - `CURSOR_API_KEY` — Cursor API key - `CURSOR_BOT_TOKEN` — Gitea API token des `cursor`-Users + - `DEPLOY_GIT_TOKEN` — für App-Deploy-Workflow (`git pull` auf boka); kann identisch mit Bot-Token sein + - `DEPLOY_ROOT` (optional) — Default `/home/frank/playground.schwenk.online` 3. **Push-Webhook** (Repo → Einstellungen → Webhooks): - URL: `https://playground.schwenk.online/hooks/deploy` - Content type: `application/json` @@ -90,3 +99,15 @@ docker compose up -d ## Beispiel-URL nach erstem Onepager `https://playground.schwenk.online/thema-xy/` — wenn Cursor `public/thema-xy/index.html` angelegt hat. + +## Apps deployen (manuell) + +Für `apps//` mit npm/Docker/DB — **nicht** über den Push-Webhook. + +1. `@cursor` legt App unter `apps//` an + `compose.yml`-Profile (Vorlage: `apps/_template/`) +2. Push auf `main` (Webhook zieht nur Dateien, baut die App nicht) +3. Gitea → Actions → **Deploy App (manual)** → Run workflow → `app` = `` + +Der Workflow ruft `scripts/deploy-app.sh` auf boka auf: `git pull`, optional `npm ci && build`, dann `docker compose --profile up -d --build`. + +**Nur manuell** — kein Push-, kein Issue-Trigger. Optional pro App: `.gitea/workflows/deploy-.yml` aus `apps/_template/workflow-deploy.yml.template` (ebenfalls nur `workflow_dispatch`). diff --git a/SOUL.md b/SOUL.md index 0c123b7..e0e6eb4 100644 --- a/SOUL.md +++ b/SOUL.md @@ -42,14 +42,30 @@ Fränky — und ggf. Gäste mit Link zu einem konkreten Pfad (`/thema-xy/`). - **Workdir (boka):** `/home/frank/playground.schwenk.online` — Git-Clone dieses Repos - **Webroot:** `public/` → nginx -- **Deploy:** Gitea Push-Webhook → `https://playground.schwenk.online/hooks/deploy` +- **Deploy static:** Push-Webhook → `hooks/deploy` → `git pull` only +- **Deploy apps:** `.gitea/workflows/deploy-app.yml` → manual `workflow_dispatch` - **Cursor workflow:** `.gitea/workflows/cursor.yml` mit `push_to_default_branch: "true"` ## Project-Specific Rules +### Static content (`public/`) + - Content lebt **verzeichnisbasiert** unter `public//` — typisch `index.html`, keine feste Dateiliste. - Cursor entscheidet selbst: neues Verzeichnis vs. `index2.html` / Assets im bestehenden Ordner. - Plain HTML/CSS only — kein npm, kein Bundler. - **Kein** `public/index.html` und keine Pflege einer Übersichtsseite, außer Fränky fordert es explizit. -- Bot-Commits nur unter `public/` und nur was zum Issue passt — keine Drive-by-Änderungen an Infra-Dateien. -- `@cursor` in Issue-Kommentaren: Aufgabe erledigen, kurz im Issue kommentieren, auf `main` pushen. +- Push auf `main` → Webhook deployt **nur** `git pull` (kein Build). + +### Apps (`apps//`) + +- Für Web-Apps, npm, Docker, Datenbank: Code unter `apps//`. +- `compose.yml` um Service mit `profiles: [""]` ergänzen (Vorlage: `apps/_template/README.md`). +- **Deploy:** `.gitea/workflows/deploy-app.yml` — **nur** `workflow_dispatch` (manuell in Gitea). Optional eigene `.gitea/workflows/deploy-.yml` aus `apps/_template/workflow-deploy.yml.template`. +- Push-Webhook und `deploy-app` nicht vermischen: Webhook = statisch; App-Workflow = Build + `docker compose --profile up -d --build`. +- Nach Implementierung im Issue kommentieren: Workflow manuell starten. + +### Allgemein + +- `@cursor` in Issue-Kommentaren: Aufgabe erledigen, kurz kommentieren, auf `main` pushen. +- Neue Workflows nur unter `.gitea/workflows/`; App-Deploy-Workflows **nur** mit `workflow_dispatch` (kein `push`-Trigger), außer Fränky sagt es explizit. +- Keine Drive-by-Änderungen an Infra ohne Bezug zum Issue. diff --git a/apps/.gitkeep b/apps/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apps/_template/README.md b/apps/_template/README.md new file mode 100644 index 0000000..19dfeb0 --- /dev/null +++ b/apps/_template/README.md @@ -0,0 +1,47 @@ +# App template (`apps//`) + +Cursor copies this folder when creating a non-trivial app (npm, Docker, database). + +## Layout + +``` +apps// +├── Dockerfile +├── package.json # optional — Node apps +├── package-lock.json +└── src/ # or project-specific layout +``` + +## `compose.yml` — add a profile + +Append a service (replace ``): + +```yaml + : + profiles: [""] + build: ./apps/ + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers..rule=Host(`playground.schwenk.online`) && PathPrefix(`/`)" + - "traefik.http.routers..entrypoints=websecure" + - "traefik.http.routers..tls.certresolver=myresolver" + - "traefik.http.services..loadbalancer.server.port=3000" + networks: + - traefik +``` + +Adjust port and PathPrefix to match the app. For Postgres/SQLite, add volumes and env in the same profile block. + +## Deploy workflow + +**Default:** use the shared manual workflow `.gitea/workflows/deploy-app.yml` (`workflow_dispatch`, input `app=`). + +**Optional (stricter):** copy `workflow-deploy.yml.template` to `.gitea/workflows/deploy-.yml` — triggers **only** manually for that app. + +## After implementation + +1. Push to `main` (static webhook updates repo files only — does **not** build the app). +2. Gitea → Actions → **Deploy App (manual)** → Run workflow → `app` = ``. + +Comment in the issue: *„App unter apps// — bitte Workflow Deploy App manuell starten.“* diff --git a/apps/_template/workflow-deploy.yml.template b/apps/_template/workflow-deploy.yml.template new file mode 100644 index 0000000..3f67bbc --- /dev/null +++ b/apps/_template/workflow-deploy.yml.template @@ -0,0 +1,19 @@ +# Deploy + +on: + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build and deploy on boka + env: + DEPLOY_ROOT: ${{ secrets.DEPLOY_ROOT }} + DEPLOY_GIT_TOKEN: ${{ secrets.DEPLOY_GIT_TOKEN }} + run: | + chmod +x scripts/deploy-app.sh + DEPLOY_ROOT="${DEPLOY_ROOT:-/home/frank/playground.schwenk.online}" \ + DEPLOY_GIT_TOKEN="$DEPLOY_GIT_TOKEN" \ + scripts/deploy-app.sh "" diff --git a/hooks/deploy.sh b/hooks/deploy.sh index 78cec00..972f3e6 100755 --- a/hooks/deploy.sh +++ b/hooks/deploy.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Static deploy only: git pull for public/ and repo files. +# App build/deploy: .gitea/workflows/deploy-app.yml (workflow_dispatch). set -euo pipefail REPO_DIR="/repo" diff --git a/scripts/deploy-app.sh b/scripts/deploy-app.sh new file mode 100755 index 0000000..0214685 --- /dev/null +++ b/scripts/deploy-app.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Build and start a compose profile for apps// on boka. +# Called from .gitea/workflows/deploy-app.yml (workflow_dispatch only). +set -euo pipefail + +APP="${1:?usage: deploy-app.sh }" +DEPLOY_ROOT="${DEPLOY_ROOT:-/home/frank/playground.schwenk.online}" +BRANCH="${DEPLOY_BRANCH:-main}" +APP_DIR="${DEPLOY_ROOT}/apps/${APP}" + +if ! echo "${APP}" | grep -qE '^[a-z0-9]+(-[a-z0-9]+)*$'; then + echo "Error: invalid app slug: ${APP}" + exit 1 +fi + +if [ ! -d "${APP_DIR}" ]; then + echo "Error: ${APP_DIR} does not exist" + exit 1 +fi + +git_cmd() { + git -c "safe.directory=${DEPLOY_ROOT}" -C "${DEPLOY_ROOT}" "$@" +} + +if [ -n "${DEPLOY_GIT_TOKEN:-}" ]; then + pull_url="https://oauth2:${DEPLOY_GIT_TOKEN}@gitea.schwenk.online/froxxxy/playground.git" + git_cmd pull "${pull_url}" "${BRANCH}" +else + git_cmd pull "https://gitea.schwenk.online/froxxxy/playground.git" "${BRANCH}" +fi + +if [ -f "${APP_DIR}/package.json" ] && [ -f "${APP_DIR}/package-lock.json" ]; then + echo "Running npm ci && npm run build in apps/${APP}..." + docker run --rm \ + -v "${APP_DIR}:/app" \ + -w /app \ + node:current-alpine \ + sh -c "npm ci && npm run build" +fi + +cd "${DEPLOY_ROOT}" +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-playground}" +docker compose --profile "${APP}" up -d --build + +echo "Deploy completed for apps/${APP}"