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 <cursoragent@cursor.com>
This commit is contained in:
Frank Schwenk
2026-07-05 16:47:59 +02:00
parent 3c418e6ad1
commit deac59d55a
8 changed files with 197 additions and 8 deletions
+39
View File
@@ -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 }}"
+26 -5
View File
@@ -7,18 +7,25 @@ Experimentier-Site auf [playground.schwenk.online](https://playground.schwenk.on
1. Issue in Gitea öffnen, z. B. *„Onepager: Thema XY“* 1. Issue in Gitea öffnen, z. B. *„Onepager: Thema XY“*
2. Kommentar: `@cursor recherchiere zu Thema XY und generiere einen Onepager als plain HTML` 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/<slug>/` auf `main` 3. Gitea Action ([cursor-gitea-agent](https://gitea.schwenk.online/froxxxy/cursor-gitea-agent)) committet nach `public/<slug>/` 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/<slug>/`, Deploy nur manuell — siehe unten.
Content-Konventionen: siehe [SOUL.md](SOUL.md). Content-Konventionen: siehe [SOUL.md](SOUL.md).
## Repo-Layout ## Repo-Layout
``` ```
public/ # Webroot public/ # Webroot (statisch, auto-deploy per Push-Webhook)
apps/ # Web-Apps mit Build/Docker (manueller Deploy-Workflow)
nginx/ # nginx-Konfiguration nginx/ # nginx-Konfiguration
hooks/ # Webhook-Skripte + hooks.json.template hooks/ # Webhook-Skripte (nur git pull für static)
deploy-hook/ # Dockerfile für Webhook-Container (git pull) deploy-hook/ # Webhook-Container
compose.yml # nginx + deploy-hook auf Traefik-Netz 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. 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): 2. **Repo-Secrets** (Namen ohne `GITEA_`-Prefix):
- `CURSOR_API_KEY` — Cursor API key - `CURSOR_API_KEY` — Cursor API key
- `CURSOR_BOT_TOKEN` — Gitea API token des `cursor`-Users - `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): 3. **Push-Webhook** (Repo → Einstellungen → Webhooks):
- URL: `https://playground.schwenk.online/hooks/deploy` - URL: `https://playground.schwenk.online/hooks/deploy`
- Content type: `application/json` - Content type: `application/json`
@@ -90,3 +99,15 @@ docker compose up -d
## Beispiel-URL nach erstem Onepager ## Beispiel-URL nach erstem Onepager
`https://playground.schwenk.online/thema-xy/` — wenn Cursor `public/thema-xy/index.html` angelegt hat. `https://playground.schwenk.online/thema-xy/` — wenn Cursor `public/thema-xy/index.html` angelegt hat.
## Apps deployen (manuell)
Für `apps/<slug>/` mit npm/Docker/DB — **nicht** über den Push-Webhook.
1. `@cursor` legt App unter `apps/<slug>/` 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` = `<slug>`
Der Workflow ruft `scripts/deploy-app.sh` auf boka auf: `git pull`, optional `npm ci && build`, dann `docker compose --profile <slug> up -d --build`.
**Nur manuell** — kein Push-, kein Issue-Trigger. Optional pro App: `.gitea/workflows/deploy-<slug>.yml` aus `apps/_template/workflow-deploy.yml.template` (ebenfalls nur `workflow_dispatch`).
+19 -3
View File
@@ -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 - **Workdir (boka):** `/home/frank/playground.schwenk.online` — Git-Clone dieses Repos
- **Webroot:** `public/` → nginx - **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"` - **Cursor workflow:** `.gitea/workflows/cursor.yml` mit `push_to_default_branch: "true"`
## Project-Specific Rules ## Project-Specific Rules
### Static content (`public/`)
- Content lebt **verzeichnisbasiert** unter `public/<kebab-slug>/` — typisch `index.html`, keine feste Dateiliste. - Content lebt **verzeichnisbasiert** unter `public/<kebab-slug>/` — typisch `index.html`, keine feste Dateiliste.
- Cursor entscheidet selbst: neues Verzeichnis vs. `index2.html` / Assets im bestehenden Ordner. - Cursor entscheidet selbst: neues Verzeichnis vs. `index2.html` / Assets im bestehenden Ordner.
- Plain HTML/CSS only — kein npm, kein Bundler. - Plain HTML/CSS only — kein npm, kein Bundler.
- **Kein** `public/index.html` und keine Pflege einer Übersichtsseite, außer Fränky fordert es explizit. - **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. - Push auf `main` → Webhook deployt **nur** `git pull` (kein Build).
- `@cursor` in Issue-Kommentaren: Aufgabe erledigen, kurz im Issue kommentieren, auf `main` pushen.
### Apps (`apps/<slug>/`)
- Für Web-Apps, npm, Docker, Datenbank: Code unter `apps/<kebab-slug>/`.
- `compose.yml` um Service mit `profiles: ["<slug>"]` ergänzen (Vorlage: `apps/_template/README.md`).
- **Deploy:** `.gitea/workflows/deploy-app.yml`**nur** `workflow_dispatch` (manuell in Gitea). Optional eigene `.gitea/workflows/deploy-<slug>.yml` aus `apps/_template/workflow-deploy.yml.template`.
- Push-Webhook und `deploy-app` nicht vermischen: Webhook = statisch; App-Workflow = Build + `docker compose --profile <slug> 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.
View File
+47
View File
@@ -0,0 +1,47 @@
# App template (`apps/<slug>/`)
Cursor copies this folder when creating a non-trivial app (npm, Docker, database).
## Layout
```
apps/<slug>/
├── Dockerfile
├── package.json # optional — Node apps
├── package-lock.json
└── src/ # or project-specific layout
```
## `compose.yml` — add a profile
Append a service (replace `<slug>`):
```yaml
<slug>:
profiles: ["<slug>"]
build: ./apps/<slug>
restart: unless-stopped
labels:
- "traefik.enable=true"
- "traefik.http.routers.<slug>.rule=Host(`playground.schwenk.online`) && PathPrefix(`/<slug>`)"
- "traefik.http.routers.<slug>.entrypoints=websecure"
- "traefik.http.routers.<slug>.tls.certresolver=myresolver"
- "traefik.http.services.<slug>.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=<slug>`).
**Optional (stricter):** copy `workflow-deploy.yml.template` to `.gitea/workflows/deploy-<slug>.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` = `<slug>`.
Comment in the issue: *„App unter apps/<slug>/ — bitte Workflow Deploy App manuell starten.“*
@@ -0,0 +1,19 @@
# Deploy <slug>
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 "<slug>"
+2
View File
@@ -1,4 +1,6 @@
#!/usr/bin/env bash #!/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 set -euo pipefail
REPO_DIR="/repo" REPO_DIR="/repo"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Build and start a compose profile for apps/<slug>/ on boka.
# Called from .gitea/workflows/deploy-app.yml (workflow_dispatch only).
set -euo pipefail
APP="${1:?usage: deploy-app.sh <app-slug>}"
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}"