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:
@@ -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 }}"
|
||||||
@@ -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`).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>"
|
||||||
@@ -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"
|
||||||
|
|||||||
Executable
+45
@@ -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}"
|
||||||
Reference in New Issue
Block a user