fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled

This commit is contained in:
2025-11-24 21:28:25 +01:00
parent 4eb7134853
commit 77abc65cd7
1327 changed files with 91915 additions and 9909 deletions

View File

@@ -0,0 +1,86 @@
# Automatisches Secrets-Setup
## Schnellstart
```bash
# Interaktives Script (fragt nach Token)
bash scripts/setup-gitea-secrets-interactive.sh
```
Das Script:
1. Fragt nach einem Gitea Access Token
2. Setzt automatisch alle drei Secrets via API
## Token generieren
Falls noch kein Token vorhanden:
1. **Gehe zu Gitea Settings:**
```
https://git.michaelschiemer.de/user/settings/applications
```
2. **Klicke "Generate New Token"**
3. **Konfiguration:**
- Name: `secrets-setup` (oder beliebig)
- Disponível scopes:
- ✅ `write:repository` (mindestens)
- ✅ Oder wähle alle für volle Berechtigung
4. **Kopiere den Token** (wird nur einmal angezeigt!)
## Script ausführen
```bash
# Script starten
bash scripts/setup-gitea-secrets-interactive.sh
# Token eingeben (wird nicht angezeigt)
Gitea Token: <token-einfügen>
# Script setzt automatisch:
# ✅ REGISTRY_USER = admin
# ✅ REGISTRY_PASSWORD = registry-secure-password-3125
# ✅ SSH_PRIVATE_KEY = <aus ~/.ssh/production>
```
## Verifizierung
Nach erfolgreichem Setup:
1. **Prüfe in Gitea UI:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Sollte zeigen:**
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle drei Secrets sollten "Hidden" als Wert anzeigen.
## Troubleshooting
### "Token ungültig"
- Prüfe, ob Token korrekt kopiert wurde (keine Leerzeichen)
- Prüfe, ob Token die Berechtigung `write:repository` hat
### "Repository nicht gefunden"
- Prüfe Repository-Name: `michael/michaelschiemer`
- Prüfe, ob du Zugriff auf das Repository hast
### "HTTP 403 Forbidden"
- Token hat keine ausreichenden Berechtigungen
- Generiere neuen Token mit `write:repository` scope
### API nicht erreichbar
- Prüfe Gitea URL: `https://git.michaelschiemer.de`
- Prüfe Netzwerkverbindung
## Alternative: Manuelles Setup
Falls das automatische Setup nicht funktioniert:
Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md` für manuelle Anleitung.

View File

@@ -0,0 +1,90 @@
# CI/CD Pipeline - Setup Checklist
## ✅ Status
### 1. Repository Secrets in Gitea ✅ BEREIT
**Pfad**: Repository → Settings → Secrets
**Schnelles Setup:**
```bash
bash scripts/prepare-secrets.sh
```
**Erforderliche Secrets:**
- [ ] **REGISTRY_USER**: `admin`
- [ ] **REGISTRY_PASSWORD**: `registry-secure-password-2025`
- [ ] **SSH_PRIVATE_KEY**: Vollständiger Inhalt von `~/.ssh/production`
**Details**: Siehe `.gitea/workflows/QUICK_SECRETS_SETUP.md`
**Hinweis**: Alle Secrets müssen in Gitea konfiguriert werden, bevor die Pipeline läuft.
### 2. Docker Registry ✅
- ✅ Registry läuft auf `registry.michaelschiemer.de`
- ✅ Authentifizierung konfiguriert (admin/registry-secure-password-2025)
- ✅ Erreichbar via HTTP auf `127.0.0.1:5000`
- ✅ Image `framework` bereits vorhanden
**Registry-Test**:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog
# Erwartete Ausgabe: {"repositories":["framework"]}
```
### 3. Ansible Playbooks ✅
-`deploy-update.yml` - Aktualisiert für Docker Compose
-`rollback.yml` - Aktualisiert für Docker Compose
-`setup-infrastructure.yml` - Enthält Registry-Setup
-`sync-stacks.yml` - Synchronisiert Stack-Konfigurationen
### 4. Workflow-Konfiguration ✅
-`.gitea/workflows/production-deploy.yml` - Haupt-Workflow
-`Dockerfile.production` - Production Dockerfile erstellt
- ✅ Application Stack konfiguriert (`deployment/stacks/application/docker-compose.yml`)
## 📋 Nächste Schritte
### Schritt 1: Secrets in Gitea hinzufügen
Siehe `.gitea/workflows/SECRETS_SETUP.md` für detaillierte Anleitung.
### Schritt 2: Workflow manuell testen
1. Gehe zu: `https://git.michaelschiemer.de/<username>/michaelschiemer/actions`
2. Wähle "Production Deployment Pipeline"
3. Klicke "Run workflow"
4. Wähle Branch `main`
5. Beobachte die Ausführung
### Schritt 3: Automatisches Deployment testen
1. Push zu `main` Branch
2. Workflow sollte automatisch starten
3. Prüfe Logs in Gitea Actions
## 🔧 Troubleshooting
### Registry nicht erreichbar
```bash
# Prüfe Registry-Status
ssh deploy@94.16.110.151 "docker ps | grep registry"
# Teste Registry-Erreichbarkeit
ssh deploy@94.16.110.151 "curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog"
```
### Workflow schlägt bei Registry-Login fehl
- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets korrekt gesetzt sind
- Prüfe, ob das Passwort in Gitea mit dem Server übereinstimmt
### Image Pull schlägt fehl
- Prüfe, ob das Image in der Registry existiert:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/framework/tags/list
```
- Prüfe Registry-Logs für Fehler
### Deployment schlägt fehl
- Prüfe Ansible-Logs im Workflow
- Prüfe, ob `deployment/stacks/application/docker-compose.yml` auf dem Server existiert
- Prüfe Docker-Logs auf dem Server:
```bash
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs"
```

View File

@@ -0,0 +1,101 @@
# Manuelles Secrets-Setup - Einfache Anleitung
Da das automatische Setup Probleme macht, hier die manuelle Lösung:
## Schritt 1: Gehe zu Gitea Secrets-Seite
Öffne im Browser:
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
**Hinweis**: Falls diese Seite nicht existiert oder einen 404 gibt:
- Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt
- Prüfe, ob du Zugriff auf das Repository hast
- Prüfe, ob Actions aktiviert ist
## Schritt 2: Füge die drei Secrets hinzu
Für jedes Secret: **Klicke "New Secret"**, fülle aus, **Save**
### Secret 1: REGISTRY_USER
- **Name**: `REGISTRY_USER`
- **Value**: `admin`
- **Save**
### Secret 2: REGISTRY_PASSWORD
- **Name**: `REGISTRY_PASSWORD`
- **Value**: `registry-secure-password-2025`
- **Save**
### Secret 3: SSH_PRIVATE_KEY
- **Name**: `SSH_PRIVATE_KEY`
- **Value**: Führe aus `cat ~/.ssh/production` und kopiere den KOMPLETTEN Inhalt
```bash
cat ~/.ssh/production
```
**Wichtig**: Kopiere ALLES, inklusive:
- `-----BEGIN OPENSSH PRIVATE KEY-----`
- Alle Zeilen dazwischen
- `-----END OPENSSH PRIVATE KEY-----`
- **Save**
## Schritt 3: Verifizierung
Nach dem Setup sollten alle drei Secrets in der Liste erscheinen:
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle zeigen "Hidden" als Wert.
## Falls die Secrets-Seite nicht erreichbar ist
### Option A: Repository-Name prüfen
```bash
# Prüfe aktuelle Remote-URL
git remote get-url origin
# Sollte zeigen:
# https://git.michaelschiemer.de/<owner>/<repo>.git
```
Falls der Name anders ist, verwende die korrekte URL.
### Option B: Repository erstellen
Falls das Repository noch nicht existiert:
1. Gehe zu: `https://git.michaelschiemer.de/repos/new`
2. Erstelle das Repository `michaelschiemer`
3. Dann gehe zu: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions`
### Option C: Actions aktivieren
Falls Actions nicht aktiviert ist:
1. Gehe zu Repository Settings
2. Prüfe, ob "Actions" aktiviert ist
3. Falls nicht, aktiviere es in den Repository-Einstellungen
## Nächster Schritt
Nach erfolgreichem Setup der Secrets:
1. **Teste den Workflow:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
2. **Oder pushe einen Commit:**
```bash
git push origin main
```
Der Workflow sollte dann automatisch starten.

View File

@@ -0,0 +1,87 @@
# Secrets Setup mit Token - Schritt für Schritt
## ✅ Du hast bereits ein Token - Perfekt!
## Option 1: Automatisches Setup (Empfohlen)
Führe einfach dieses Kommando aus (ersetze `<DEIN_TOKEN>` mit deinem Token):
```bash
bash scripts/setup-gitea-secrets-with-token.sh <DEIN_TOKEN>
```
**Beispiel:**
```bash
bash scripts/setup-gitea-secrets-with-token.sh ghp_1234567890abcdefghijklmnopqrstuvwxyz
```
Das Script:
1. ✅ Testet die API-Verbindung
2. ✅ Setzt automatisch `REGISTRY_USER` = `admin`
3. ✅ Setzt automatisch `REGISTRY_PASSWORD` = `registry-secure-password-2025`
4. ✅ Setzt automatisch `SSH_PRIVATE_KEY` = Inhalt von `~/.ssh/production`
## Option 2: Manuell über Gitea UI
Falls das automatische Setup nicht funktioniert:
### Schritt 1: Gehe zu Secrets-Seite
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
### Schritt 2: Füge jedes Secret einzeln hinzu
**REGISTRY_USER:**
1. Klicke "New Secret"
2. Name: `REGISTRY_USER`
3. Value: `admin`
4. Save
**REGISTRY_PASSWORD:**
1. Klicke "New Secret"
2. Name: `REGISTRY_PASSWORD`
3. Value: `registry-secure-password-2025`
4. Save
**SSH_PRIVATE_KEY:**
1. Klicke "New Secret"
2. Name: `SSH_PRIVATE_KEY`
3. Value: Kompletter Inhalt von `~/.ssh/production`
```bash
cat ~/.ssh/production
```
(Kopiere ALLES, inklusive `-----BEGIN` und `-----END` Zeilen)
4. Save
## Verifizierung
Nach dem Setup:
1. **Prüfe in Gitea UI:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Sollte zeigen:**
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle drei Secrets sollten "Hidden" als Wert anzeigen.
## Troubleshooting
### "API-Verbindung fehlgeschlagen"
- Prüfe Token-Kopierung (keine Leerzeichen)
- Prüfe Token-Berechtigung: `write:repository` Scope nötig
- Prüfe Repository-Name: `michael/michaelschiemer`
### "HTTP 403 Forbidden"
- Token hat keine ausreichenden Berechtigungen
- Generiere neuen Token mit `write:repository` scope
### "HTTP 404 Not Found"
- Repository existiert nicht oder falscher Name
- Prüfe: `https://git.michaelschiemer.de/michael/michaelschiemer`

View File

@@ -0,0 +1,119 @@
# ✅ Secrets erfolgreich gesetzt!
## Status
✅ Repository erstellt
✅ Secrets konfiguriert:
- REGISTRY_USER
- REGISTRY_PASSWORD
- SSH_PRIVATE_KEY
## 🚀 Nächster Schritt: CI/CD Workflow testen
### Option 1: Workflow manuell triggern (Empfohlen)
1. **Gehe zu Actions:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
2. **Workflow auswählen:**
- Suche nach "Production Deployment Pipeline"
- Klicke auf den Workflow
3. **Workflow starten:**
- Klicke "Run workflow" (rechts oben)
- Branch: `main`
- `skip_tests`: deaktiviert (Tests sollen laufen)
- Klicke "Run workflow"
4. **Logs beobachten:**
- Prüfe jeden Schritt in den Logs
- Er_warte ~8-15 Minuten für komplette Ausführung
### Option 2: Automatisches Deployment via Commit
```bash
# Stelle sicher, dass alles committed ist
git add .
git commit -m "chore: CI/CD pipeline setup complete"
# Push zu main - startet automatisch den Workflow
git push origin main
```
## 📊 Workflow-Schritte
Der Workflow führt folgende Schritte aus:
1. **Tests** (~2-5 Min)
- PHP Setup
- Composer Dependencies
- Tests ausführen
2. **Build** (~3-5 Min)
- Multi-Stage Docker Build
- Composer Dependencies (Production)
- Frontend Build (npm)
- Final Production Image
3. **Push** (~1-2 Min)
- Docker Login zur Registry
- Image Tag generieren
- Image zur Registry pushen
4. **Deploy** (~2-4 Min)
- Ansible Playbook ausführen
- Image auf Production pullen
- Application Stack aktualisieren
- Services neu starten
**Gesamtzeit: ~8-15 Minuten**
## ✅ Erfolgreiche Ausführung erkennen
Der Workflow ist erfolgreich, wenn:
- ✅ Alle Jobs grün sind
- ✅ Keine Fehler in den Logs
- ✅ "Deploy via Ansible" erfolgreich
- ✅ Application läuft auf Production-Server
## 🔍 Verifizierung nach Deployment
```bash
# Prüfe Application-Status
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose ps"
# Prüfe Logs
ssh deploy@94.16.110.151 "cd ~/deployment/stacks/application && docker compose logs --tail=50"
# Prüfe Health Endpoint
curl -k https://michaelschiemer.de/health
```
## 🐛 Troubleshooting
### Workflow startet nicht
- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist
- Prüfe Workflow-Syntax
### "Secret not found" Fehler
- Prüfe Secrets-Seite: `https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions`
- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
### Registry Login fehlgeschlagen
- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets
- Teste Registry: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog`
### Deployment fehlgeschlagen
- Prüfe `SSH_PRIVATE_KEY` Secret
- Prüfe Ansible-Verbindung: `ssh deploy@94.16.110.151 "echo OK"`
- Prüfe Server-Logs
## 🎉 Nach erfolgreichem Test
Du kannst jetzt:
- ✅ Commits pushen → Automatisches Deployment
- ✅ Workflow manuell triggern für kontrollierte Deployments
- ✅ Branch-Protection aktivieren für sichere Deployments

View File

@@ -0,0 +1,31 @@
# ⚠️ DEPRECATED: production-deploy.yml
Diese Datei ist **deprecated** und wird durch getrennte Workflows ersetzt:
## Neue Workflows
### 1. `build-image.yml` - Automatischer Image-Build
- **Trigger**: Automatisch bei Push zu `main`/`develop`
- **Zweck**: Baut Docker Images und pusht sie zur Registry
- **Vorteil**: Images sind sofort verfügbar, auch ohne Deployment
### 2. `deploy-production.yml` - Deployment Workflow
- **Trigger**:
- Manuell über `workflow_dispatch`
- Optional: Automatisch nach erfolgreichem Build (konfigurierbar)
- **Zweck**: Deployed vorhandene Images auf Production Server
- **Vorteil**: Flexibles Deployment, schneller wenn Image bereits vorhanden
## Migration
Die alte `production-deploy.yml` kann entfernt werden, sobald:
1.`build-image.yml` erfolgreich getestet wurde
2.`deploy-production.yml` erfolgreich getestet wurde
3. ✅ Team ist mit neuen Workflows vertraut
## Vorteile der Trennung
-**Schnelleres Feedback**: Images werden sofort gebaut (ca. 5-8 Min statt 10-15 Min)
- 🎯 **Flexibleres Deployment**: Man kann wählen, wann und welches Image deployed wird
- 🔄 **Parallele Ausführung**: Mehrere Builds können gleichzeitig laufen
- 📦 **Image-Caching**: Builds sind unabhängig von Deployments

View File

@@ -0,0 +1,104 @@
# Quick Secrets Setup für Gitea CI/CD
## Zusammenfassung der benötigten Werte
### REGISTRY_USER
```
admin
```
### REGISTRY_PASSWORD
```
registry-secure-password-2025
```
### SSH_PRIVATE_KEY
Kopiere den kompletten Inhalt von φ~/.ssh/production`:
```bash
# Zeige SSH Key Inhalt (für Copy-Paste)
cat ~/.ssh/production
```
**Wichtig**: Der komplette Inhalt muss kopiert werden, inklusive:
- `-----BEGIN OPENSSH PRIVATE KEY-----`
- Alle Zeilen dazwischen
- `-----END OPENSSH PRIVATE KEY-----`
## Schnelle Vorbereitung
Führe das Helper-Script aus, um alle Werte anzuzeigen:
```bash
bash scripts/prepare-secrets.sh
```
Dies zeigt alle drei Secrets an, die du kopieren kannst.
## Manuelle Einrichtung in Gitea
1. **Gehe zu Repository Settings:**
```
https://git.michaelschiemer.de/<username>/michaelschiemer/settings/secrets
```
2. **Klicke auf "New Secret"**
3. **Füge jedes Secret hinzu:**
**REGISTRY_USER:**
- Name: `REGISTRY_USER`
- Value: `admin`
- Save
**REGISTRY_PASSWORD:**
- Name: `REGISTRY_PASSWORD`
- Value: `registry-secure-password-2025`
- Save
**SSH_PRIVATE_KEY:**
- Name: `SSH_PRIVATE_KEY`
- Value: `<kompletter Inhalt von cat ~/.ssh/production>`
- Save
## Verifizierung
Nach dem Setup sollten alle drei Secrets in der Liste erscheinen:
- ✅ REGISTRY_USER
- ✅ REGISTRY_PASSWORD
- ✅ SSH_PRIVATE_KEY
Alle zeigen "Hidden" als Wert.
## Nächster Schritt
Sobald die Secrets konfiguriert sind:
1. **Workflow manuell testen:**
```
https://git.michaelschiemer.de/<username>/michaelschiemer/actions
```
- Wähle "Production Deployment Pipeline"
- Klicke "Run workflow"
- Wähle Branch `main`
2. **Oder automatisches Deployment testen:**
- Pushe einen Commit zu `main`
- Workflow startet automatisch
## Troubleshooting
### Secrets werden nicht erkannt
- Prüfe, ob die Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
- Prüfe, ob keine Leerzeichen am Anfang/Ende
### SSH Key Fehler
- Stelle sicher, dass der komplette Key kopiert wurde
- Prüfe, dass `-----BEGIN` und `-----END` Zeilen enthalten sind
### Registry Login Fehler
- Prüfe, ob `REGISTRY_USER` und `REGISTRY_PASSWORD` korrekt sind
- Teste Registry-Erreichbarkeit:
```bash
curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog
```

View File

@@ -0,0 +1,741 @@
# Gitea CI/CD Workflows Documentation
Comprehensive guide for the automated deployment workflows using Gitea Actions.
## Overview
This project uses Gitea Actions for automated deployments to staging and production environments. The workflows are designed for:
- **Zero-downtime deployments** via rolling updates
- **Automatic rollback** on deployment failures
- **Environment-specific configurations** using Docker Compose overlays
- **Database protection** with automated backups (production)
- **Comprehensive health checks** and smoke tests
- **Deployment audit trail** via persistent logs
## Workflow Files
### 1. Staging Deployment (`deploy-staging.yml`)
**Purpose:** Automated deployment to staging environment for testing and validation.
**Triggers:**
- Push to `staging` branch
- Manual workflow dispatch
**Target:** `https://staging.michaelschiemer.de`
**Key Features:**
- Fast deployment cycle (30-second health check)
- Basic health verification
- Keeps 5 deployment backups
- No database backup (non-critical environment)
**Workflow Steps:**
1. Build Docker image with `ENV=staging`
2. Push to private registry (localhost:5000)
3. Deploy to staging server via SSH
4. Basic health checks
5. Automatic rollback on failure
---
### 2. Production Deployment (`deploy-production.yml`)
**Purpose:** Production deployment with enhanced safety features and verification.
**Triggers:**
- Push to `main` or `production` branches
- Manual workflow dispatch
**Target:** `https://michaelschiemer.de`
**Key Features:**
- **Database backup** before deployment (aborts on backup failure)
- **Database migrations** after container startup
- **Extended health checks** (60-second wait, multiple verification layers)
- **Smoke tests** for functional verification
- **Deployment logging** for audit trail
- **Graceful shutdown** for active request handling
- Keeps 10 deployment backups
**Workflow Steps:**
1. Build Docker image with `ENV=production`
2. Push to private registry
3. Create database backup (optional via `skip_backup` input)
4. Gracefully stop current containers
5. Deploy new containers
6. Run database migrations
7. Extended health verification
8. Smoke tests (main page + API)
9. Automatic rollback on failure
10. Log deployment outcome
11. Clean up build artifacts
---
## Required Gitea Secrets
Configure these secrets in your Gitea repository settings (`Settings``Secrets`).
### Staging Secrets
| Secret Name | Description | Example Value |
|-------------|-------------|---------------|
| `STAGING_HOST` | Staging server hostname or IP | `staging.example.com` or `203.0.113.42` |
| `STAGING_USER` | SSH username for staging server | `deploy` or `www-data` |
| `STAGING_SSH_KEY` | Private SSH key (PEM format) | `-----BEGIN RSA PRIVATE KEY-----...` |
| `STAGING_SSH_PORT` | SSH port (optional, defaults to 22) | `22` or `2222` |
### Production Secrets
| Secret Name | Description | Example Value |
|-------------|-------------|---------------|
| `PRODUCTION_HOST` | Production server hostname or IP | `michaelschiemer.de` or `198.51.100.10` |
| `PRODUCTION_USER` | SSH username for production server | `deploy` or `www-data` |
| `PRODUCTION_SSH_KEY` | Private SSH key (PEM format) | `-----BEGIN RSA PRIVATE KEY-----...` |
| `PRODUCTION_SSH_PORT` | SSH port (optional, defaults to 22) | `22` or `2222` |
**SSH Key Generation:**
```bash
# Generate SSH key pair (on your local machine)
ssh-keygen -t rsa -b 4096 -f deployment_key -C "gitea-deployment"
# Copy public key to target server
ssh-copy-id -i deployment_key.pub deploy@server.example.com
# Add private key to Gitea secrets (entire content)
cat deployment_key
```
**Security Best Practices:**
- Use dedicated deployment user with minimal permissions
- Restrict SSH key to specific commands via `authorized_keys` options
- Rotate SSH keys regularly (quarterly recommended)
- Never commit SSH keys to repository
---
## Manual Workflow Triggering
### Via Gitea UI
1. Navigate to your repository
2. Click `Actions` tab
3. Select the workflow (`Deploy to Staging` or `Deploy to Production`)
4. Click `Run workflow`
5. Choose branch
6. Set input parameters (if applicable)
7. Click `Run workflow`
### Via Git Push
**Staging Deployment:**
```bash
# Push to staging branch
git checkout staging
git merge develop
git push origin staging
# Workflow triggers automatically
```
**Production Deployment:**
```bash
# Push to main/production branch
git checkout main
git merge staging
git push origin main
# Workflow triggers automatically
```
### Workflow Input Parameters
**Production Workflow:**
- `force_rebuild`: Force rebuild Docker image even if code hasn't changed (default: `false`)
- `skip_backup`: Skip database backup step - **NOT RECOMMENDED** (default: `false`)
**Use Case for `skip_backup`:**
Emergency hotfix deployment when backup would cause unacceptable delay. Only use if:
- Recent backup exists
- Issue is critical (security vulnerability, production down)
- Backup failure is blocking deployment
---
## Deployment Monitoring
### Real-Time Monitoring
**Via Gitea UI:**
1. Navigate to `Actions` tab
2. Click on running workflow
3. View real-time logs for each step
4. Check for errors or warnings
**Via Server Logs:**
```bash
# SSH to target server
ssh deploy@server.example.com
# Staging logs
tail -f /opt/framework-staging/current/storage/logs/app.log
# Production logs
tail -f /opt/framework-production/current/storage/logs/app.log
# Deployment log (production only)
tail -f /opt/framework-production/deployment.log
```
### Deployment Status Verification
**Check Container Status:**
```bash
# Staging
cd /opt/framework-staging/current
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
# Production
cd /opt/framework-production/current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
```
**Health Check Endpoints:**
```bash
# Staging
curl -k https://staging.michaelschiemer.de/health
# Production
curl -k https://michaelschiemer.de/health
curl -k https://michaelschiemer.de/api/health
```
**Expected Health Response:**
```json
{
"status": "healthy",
"timestamp": "2025-01-28T15:30:00Z",
"version": "2.x",
"services": {
"database": "connected",
"redis": "connected",
"queue": "running"
}
}
```
---
## Rollback Procedures
### Automatic Rollback
Both workflows include automatic rollback on deployment failure:
**Trigger Conditions:**
- Build failure
- Health check failure
- Smoke test failure (production)
- Database migration failure (production)
**Rollback Process:**
1. Stop failed deployment containers
2. Restore most recent backup deployment
3. Start restored containers
4. Verify rollback success
5. Log rollback event
**Note:** Automatic rollback restores the application, but **database changes are NOT rolled back automatically**. See Manual Database Rollback below.
---
### Manual Rollback
**When to use:**
- Issue discovered after successful deployment
- Need to rollback to specific version (not just previous)
#### Application Rollback
**Staging:**
```bash
ssh deploy@staging.example.com
cd /opt/framework-staging
# List available backups
ls -lt backup_*
# Stop current deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml down
cd ..
# Restore specific backup
rm -rf current
cp -r backup_20250128_143000 current
# Start restored deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d
# Verify
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
curl -k https://staging.michaelschiemer.de/health
```
**Production:**
```bash
ssh deploy@michaelschiemer.de
cd /opt/framework-production
# List available backups
ls -lt backup_*
# Stop current deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml down
cd ..
# Restore specific backup
rm -rf current
cp -r backup_20250128_150000 current
# Start restored deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
# Wait for services
sleep 30
# Verify
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
curl -k https://michaelschiemer.de/health
curl -k https://michaelschiemer.de/api/health
```
#### Database Rollback (Production Only)
**CRITICAL:** Database rollback is a destructive operation. Only perform if:
- You have confirmed backup from before problematic deployment
- You understand data loss implications
- Issue cannot be fixed forward
**Process:**
```bash
ssh deploy@michaelschiemer.de
cd /opt/framework-production/current
# List available database backups
ls -lt storage/backups/backup_*.sql
# Verify backup integrity
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \
php console.php db:verify-backup --file=storage/backups/backup_20250128_150000.sql
# Stop application to prevent new writes
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml stop production-app
# Restore database from backup
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \
php console.php db:restore --file=storage/backups/backup_20250128_150000.sql --force
# Start application
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml start production-app
# Verify
curl -k https://michaelschiemer.de/health
```
**Database Rollback Best Practices:**
- Always create new backup before rollback
- Document rollback reason in deployment log
- Notify team immediately
- Review application logs for data consistency issues
- Consider rolling forward with fix instead
---
## Troubleshooting
### Common Issues
#### 1. Workflow Fails: "Permission denied (publickey)"
**Cause:** SSH authentication failed
**Solutions:**
- Verify SSH key is correctly added to Gitea secrets (entire key content)
- Ensure public key is in `~/.ssh/authorized_keys` on target server
- Check SSH key permissions on server (`chmod 600 ~/.ssh/authorized_keys`)
- Test SSH connection manually: `ssh -i deployment_key deploy@server.example.com`
#### 2. Health Check Fails
**Staging:**
```bash
# Check container status
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
# Check logs
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml logs staging-app
# Check PHP-FPM
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml exec staging-app php -v
```
**Production:**
```bash
# Extended diagnostics
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml logs production-app
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app php -v
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app pgrep php-fpm
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-redis redis-cli ping
```
#### 3. Database Backup Fails (Production)
**Symptoms:**
- Workflow aborts at step [0/6]
- Error: "Database backup failed - deployment aborted"
**Solutions:**
```bash
# Check database connection
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:status
# Check backup directory permissions
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
ls -la storage/backups/
# Fix permissions if needed
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
chown -R www-data:www-data storage/backups/
# Test backup manually
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:backup --output=storage/backups/test_backup.sql
```
#### 4. Database Migration Fails (Production)
**Symptoms:**
- Workflow fails at step [5/6]
- Error: "Database migration failed"
**Solutions:**
```bash
# Check migration status
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:status
# Review migration logs
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml logs production-app | grep migration
# Run migration manually with verbose output
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:migrate --force --verbose
# If migration is stuck, rollback and retry
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:rollback 1
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:migrate --force
```
#### 5. Image Push Fails
**Symptoms:**
- Workflow fails at "Push image to private registry"
- Error: "connection refused" or "unauthorized"
**Solutions:**
```bash
# Check registry is accessible from CI runner
curl http://localhost:5000/v2/_catalog
# Verify registry authentication (if configured)
docker login localhost:5000
# Check registry container is running
docker ps | grep registry
```
#### 6. Smoke Tests Fail (Production)
**Symptoms:**
- Health checks pass but smoke tests fail
- Error: "Main page failed" or "API health check failed"
**Solutions:**
```bash
# Test endpoints manually
curl -v -k https://michaelschiemer.de/
curl -v -k https://michaelschiemer.de/api/health
# Check Traefik routing
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml logs production-nginx
# Check application logs
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml logs production-app | tail -100
# Verify Traefik labels
docker inspect production-nginx | grep -A 20 Labels
```
---
## Staging vs Production Differences
Comprehensive comparison of workflow behaviors.
| Feature | Staging | Production |
|---------|---------|------------|
| **Trigger Branches** | `staging` | `main`, `production` |
| **Image Tag** | `staging` | `latest` |
| **Deployment Directory** | `/opt/framework-staging/` | `/opt/framework-production/` |
| **Database Backup** | ❌ No | ✅ Yes (with abort on failure) |
| **Database Migrations** | ❌ Manual | ✅ Automatic |
| **Health Check Wait** | 30 seconds | 60 seconds |
| **Health Checks** | Basic (container status, PHP version, HTTP via nginx) | Extended (+ PHP-FPM, Traefik, Redis) |
| **Smoke Tests** | ❌ No | ✅ Yes (main page + API) |
| **Backup Retention** | 5 backups | 10 backups |
| **Container Shutdown** | `docker-compose down` (immediate) | `docker-compose stop` (graceful) |
| **Deployment Logging** | ❌ No | ✅ Yes (deployment.log) |
| **Build Artifact Cleanup** | ❌ No | ✅ Yes |
| **Target URL** | https://staging.michaelschiemer.de | https://michaelschiemer.de |
| **Manual Inputs** | `force_rebuild` | `force_rebuild`, `skip_backup` |
---
## Best Practices
### Development Workflow
**Recommended Branch Flow:**
```
develop → staging → main (production)
```
**Process:**
1. Develop features on feature branches
2. Merge to `develop` branch
3. When ready for testing: `git merge develop``staging`
4. Deploy to staging automatically
5. Test on staging environment
6. If tests pass: `git merge staging``main`
7. Deploy to production automatically
### Pre-Deployment Checklist
**Staging:**
- [ ] All tests pass locally
- [ ] Code reviewed and approved
- [ ] No breaking changes without migration path
- [ ] Dependencies updated in composer.json/package.json
**Production:**
- [ ] Tested on staging environment
- [ ] Database migrations tested on staging
- [ ] Performance impact assessed
- [ ] Rollback plan documented
- [ ] Team notified of deployment window
- [ ] Recent database backup verified
- [ ] Monitoring alerts configured
### Post-Deployment Verification
**Staging:**
```bash
# Basic checks
curl -k https://staging.michaelschiemer.de/health
curl -k https://staging.michaelschiemer.de/api/health
# Manual testing of new features
```
**Production:**
```bash
# Automated checks (from CI workflow)
curl -k https://michaelschiemer.de/
curl -k https://michaelschiemer.de/api/health
# Manual verification
# - Test critical user flows
# - Check analytics/monitoring dashboards
# - Review error logs
# - Verify database migrations applied
# Check deployment log
ssh deploy@michaelschiemer.de tail /opt/framework-production/deployment.log
```
### Deployment Scheduling
**Staging:** Deploy anytime during business hours
**Production:**
- **Preferred Window:** Off-peak hours (e.g., 2-6 AM local time)
- **Emergency Deployments:** Anytime (use `skip_backup` if necessary)
- **Major Releases:** Schedule during maintenance window with advance notice
---
## Emergency Procedures
### Production Down - Complete Outage
**Immediate Response:**
```bash
# 1. Check container status
ssh deploy@michaelschiemer.de
cd /opt/framework-production/current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
# 2. If containers stopped, restart
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
# 3. If restart fails, rollback
cd /opt/framework-production
rm -rf current
cp -r $(ls -dt backup_* | head -n1) current
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
# 4. Verify recovery
curl -k https://michaelschiemer.de/health
# 5. Investigate root cause
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml logs production-app
```
### Database Corruption
**Recovery Steps:**
```bash
# 1. Stop application immediately
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml stop production-app
# 2. Verify most recent backup
ls -lt /opt/framework-production/current/storage/backups/
# 3. Restore from backup (see Database Rollback section)
# 4. Verify data integrity
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec production-app \
php console.php db:verify-integrity
# 5. Restart application
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml start production-app
```
### Failed Deployment with No Rollback
**If automatic rollback fails:**
```bash
# 1. SSH to server
ssh deploy@michaelschiemer.de
# 2. Manual rollback (see Manual Rollback section)
# 3. If rollback unavailable, emergency restore
cd /opt/framework-production
git clone https://git.michaelschiemer.de/michael/framework.git emergency-deploy
cd emergency-deploy
git checkout <last-known-good-commit>
# 4. Build and deploy manually
docker build -f docker/php/Dockerfile -t localhost:5000/framework:emergency .
docker push localhost:5000/framework:emergency
# 5. Update docker-compose.prod.yml to use emergency tag
cd /opt/framework-production/current
# Edit docker-compose.prod.yml: image: localhost:5000/framework:emergency
# 6. Deploy
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
```
---
## Monitoring and Alerting
### Recommended Monitoring
**Application Metrics:**
- Response time (target: <200ms p95)
- Error rate (target: <0.1%)
- Request throughput
- Queue depth
**Infrastructure Metrics:**
- Container health status
- CPU usage (target: <70%)
- Memory usage (target: <80%)
- Disk space (alert: <20% free)
**Database Metrics:**
- Query performance
- Connection pool utilization
- Replication lag (if applicable)
- Backup success rate
### Alert Configuration
**Critical Alerts (immediate notification):**
- Production deployment failed
- Automatic rollback triggered
- Health check failure (3 consecutive)
- Database backup failure
- Container restart loop
**Warning Alerts (review within 1 hour):**
- Staging deployment failed
- Smoke test failure
- Slow health check response (>5s)
- Disk space <30%
---
## Additional Resources
- **Main Documentation:** `deployment/NEW_ARCHITECTURE.md`
- **Architecture Analysis:** `deployment/legacy/ARCHITECTURE_ANALYSIS.md`
- **Docker Compose Files:** Root directory (`docker-compose.*.yml`)
- **Framework Documentation:** `docs/` directory
- **Troubleshooting Guide:** `docs/guides/troubleshooting.md`
---
## Maintenance
### Regular Tasks
**Weekly:**
- Review deployment logs
- Check backup retention
- Verify health check reliability
- Update dependencies (staging first)
**Monthly:**
- Rotate SSH keys
- Review and clean old backups (>30 days)
- Test rollback procedures
- Update workflow documentation
**Quarterly:**
- Disaster recovery drill
- Performance baseline review
- Security audit of deployment process
- Workflow optimization review
---
**Last Updated:** 2025-01-28
**Workflow Version:** 1.0
**Maintained by:** DevOps Team

View File

@@ -0,0 +1,30 @@
# Docker Registry Information
## Registry Details
- **URL**: `registry.michaelschiemer.de` (intern) oder `registry.michaelschiemer.de` (via Traefik)
- **Standard Credentials**:
- **Username**: `admin`
- **Password**: `registry-secure-password-2025`
⚠️ **WICHTIG**: Das Passwort sollte in Produktion geändert werden!
## Für Gitea Secrets
Verwende folgende Werte in den Gitea Repository Secrets:
- **REGISTRY_USER**: `admin`
- **REGISTRY_PASSWORD**: `registry-secure-password-2025` (oder das aktuell gesetzte Passwort)
## Registry Test
```bash
# Login testen
echo "registry-secure-password-2025" | docker login registry.michaelschiemer.de -u admin --password-stdin
# Images auflisten
curl -u admin:registry-secure-password-2025 http://registry.michaelschiemer.de/v2/_catalog
# Oder via Traefik (HTTPS)
curl -u admin:registry-secure-password-2025 https://registry.michaelschiemer.de/v2/_catalog
```

View File

@@ -0,0 +1,86 @@
# Gitea Repository Secrets Setup
## Erforderliche Secrets
Diese Secrets müssen in Gitea konfiguriert werden unter:
**Repository → Settings → Secrets**
### 1. REGISTRY_USER
- **Beschreibung**: Benutzername für Docker Registry Login
- **Typ**: String
- **Wert**: Standardmäßig `admin` oder der Benutzername für die Registry
- **Verwendung**: Docker Registry Authentication beim Image Push
### 2. REGISTRY_PASSWORD
- **Beschreibung**: Passwort für Docker Registry Login
- **Typ**: Password (versteckt)
- **Wert**: Das Passwort für die Docker Registry auf `registry.michaelschiemer.de`
- **Verwendung**: Docker Registry Authentication beim Image Push
### 3. SSH_PRIVATE_KEY
- **Beschreibung**: SSH Private Key für Zugriff auf Production Server
- **Typ**: SSH Key (versteckt)
- **Wert**: Der komplette Inhalt der SSH-Private-Key-Datei (~/.ssh/production)
- **Verwendung**: SSH-Verbindung zum Production-Server für Ansible Deployment
## Setup-Anleitung
### Schritt 1: SSH Key erstellen/exportieren
```bash
# Falls noch nicht vorhanden, SSH Key für Production erstellen
ssh-keygen -t ed25519 -f ~/.ssh/production -C "gitea-ci-cd"
# SSH Key Inhalt anzeigen (für Copy-Paste)
cat ~/.ssh/production
```
**⚠️ Wichtig**: Der komplette Inhalt der Datei (inkl. `-----BEGIN OPENSSH PRIVATE KEY-----` und `-----END OPENSSH PRIVATE KEY-----`) muss in das Secret eingefügt werden.
### Schritt 2: Docker Registry Credentials prüfen
Die Registry läuft auf dem Production-Server. Prüfe die Credentials:
```bash
# SSH zum Production-Server
ssh deploy@94.16.110.151
# Prüfe, ob Registry läuft
docker ps | grep registry
# Prüfe Registry-Konfiguration (falls vorhanden)
cat ~/deployment/stacks/registry/docker-compose.yml 2>/dev/null || echo "Registry Config nicht gefunden"
```
**Hinweis**: Falls die Registry noch nicht konfiguriert ist, müssen die Credentials festgelegt werden.
### Schritt 3: Secrets in Gitea hinzufügen
1. Gehe zu: `https://git.michaelschiemer.de/<username>/michaelschiemer/settings/secrets`
2. Klicke auf **"Add Secret"**
3. Füge jedes Secret einzeln hinzu:
**REGISTRY_USER**:
- Name: `REGISTRY_USER`
- Value: `admin` (oder der tatsächliche Registry-Benutzername)
- Save
**REGISTRY_PASSWORD**:
- Name: `REGISTRY_PASSWORD`
- Value: `<registry-password>`
- Save
**SSH_PRIVATE_KEY**:
- Name: `SSH_PRIVATE_KEY`
- Value: `<kompletter-inhalt-von-~/.ssh/production>`
- Save
### Schritt 4: Secrets verifizieren
Nach dem Hinzufügen sollten alle drei Secrets in der Liste erscheinen mit "Hidden" als Wert.
**✅ Checkliste**:
- [ ] REGISTRY_USER hinzugefügt
- [ ] REGISTRY_PASSWORD hinzugefügt
- [ ] SSH_PRIVATE_KEY hinzugefügt
- [ ] Alle Secrets zeigen "Hidden" als Wert

View File

@@ -0,0 +1,481 @@
# Gitea Secrets Configuration Guide
**Purpose:** Step-by-step guide to configure all required secrets for staging and production deployments.
**Prerequisites:**
- Admin access to Gitea repository
- SSH access to staging and production servers
- OpenSSH installed locally
---
## Quick Start Checklist
- [ ] Generate SSH key pair
- [ ] Distribute public key to target servers
- [ ] Configure staging secrets in Gitea
- [ ] Configure production secrets in Gitea
- [ ] Test SSH connection manually
- [ ] Verify secrets are accessible to workflows
---
## Step 1: Generate SSH Keys
### Option A: Generate New Deployment Keys (Recommended)
```bash
# Navigate to project root
cd /home/michael/dev/michaelschiemer
# Create SSH keys directory
mkdir -p .gitea/ssh-keys
cd .gitea/ssh-keys
# Generate staging key
ssh-keygen -t rsa -b 4096 -f gitea-staging-deploy \
-C "gitea-staging-deployment" -N ""
# Generate production key
ssh-keygen -t rsa -b 4096 -f gitea-production-deploy \
-C "gitea-production-deployment" -N ""
# Verify keys created
ls -la
# Expected output:
# gitea-staging-deploy (private key)
# gitea-staging-deploy.pub (public key)
# gitea-production-deploy (private key)
# gitea-production-deploy.pub (public key)
```
**⚠️ Security Note:**
- Private keys should NEVER be committed to git
- Add `.gitea/ssh-keys/` to `.gitignore` if not already present
- Store private keys securely (e.g., password manager, Vault)
---
## Step 2: Distribute Public Keys to Servers
### Staging Server Setup
```bash
# Copy public key to staging server
ssh-copy-id -i .gitea/ssh-keys/gitea-staging-deploy.pub deploy@YOUR_STAGING_HOST
# Or manually (if ssh-copy-id not available):
cat .gitea/ssh-keys/gitea-staging-deploy.pub | \
ssh deploy@YOUR_STAGING_HOST "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# Set proper permissions on server
ssh deploy@YOUR_STAGING_HOST "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
# Test connection
ssh -i .gitea/ssh-keys/gitea-staging-deploy deploy@YOUR_STAGING_HOST "echo 'Staging SSH connection successful'"
```
### Production Server Setup
```bash
# Copy public key to production server
ssh-copy-id -i .gitea/ssh-keys/gitea-production-deploy.pub deploy@YOUR_PRODUCTION_HOST
# Or manually:
cat .gitea/ssh-keys/gitea-production-deploy.pub | \
ssh deploy@YOUR_PRODUCTION_HOST "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# Set proper permissions
ssh deploy@YOUR_PRODUCTION_HOST "chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
# Test connection
ssh -i .gitea/ssh-keys/gitea-production-deploy deploy@YOUR_PRODUCTION_HOST "echo 'Production SSH connection successful'"
```
**Deployment User Requirements:**
- User must exist on target server (e.g., `deploy`, `www-data`, `ubuntu`)
- User must have sudo privileges for Docker commands
- User must have write access to deployment directories:
- Staging: `/opt/framework-staging/`
- Production: `/opt/framework-production/`
---
## Step 3: Prepare Secret Values
### Extract Private Key Content
```bash
cd /home/michael/dev/michaelschiemer/.gitea/ssh-keys
# Display staging private key (copy entire output)
echo "=== STAGING_SSH_KEY ==="
cat gitea-staging-deploy
echo ""
# Display production private key (copy entire output)
echo "=== PRODUCTION_SSH_KEY ==="
cat gitea-production-deploy
echo ""
```
**Important:** Copy the **entire key content** including:
```
-----BEGIN RSA PRIVATE KEY-----
[key content here]
-----END RSA PRIVATE KEY-----
```
### Determine Server Details
**Staging Server:**
```bash
# Get staging hostname/IP (replace with your actual server)
STAGING_HOST="staging.michaelschiemer.de" # or IP: 203.0.113.42
# Get SSH port (default: 22)
STAGING_SSH_PORT="22"
# Get deployment user
STAGING_USER="deploy"
```
**Production Server:**
```bash
# Get production hostname/IP
PRODUCTION_HOST="michaelschiemer.de" # or IP: 198.51.100.10
# Get SSH port (default: 22)
PRODUCTION_SSH_PORT="22"
# Get deployment user
PRODUCTION_USER="deploy"
```
---
## Step 4: Configure Secrets in Gitea
### Access Gitea Secrets Configuration
1. **Navigate to Repository:**
- Open Gitea web interface
- Go to your framework repository
2. **Access Secrets Settings:**
- Click `Settings` (⚙️ icon)
- Click `Secrets` in left sidebar
- Or direct URL: `https://git.michaelschiemer.de/michael/framework/settings/secrets`
3. **Add New Secret:**
- Click `Add Secret` button
- Fill in `Name` and `Value` fields
- Click `Add Secret` to save
---
### Staging Secrets Configuration
**Secret 1: STAGING_HOST**
- **Name:** `STAGING_HOST`
- **Value:** `staging.michaelschiemer.de` (or your staging server hostname/IP)
- **Description:** Staging server hostname or IP address
**Secret 2: STAGING_USER**
- **Name:** `STAGING_USER`
- **Value:** `deploy` (or your deployment user)
- **Description:** SSH username for staging deployments
**Secret 3: STAGING_SSH_KEY**
- **Name:** `STAGING_SSH_KEY`
- **Value:** [Paste entire content of `gitea-staging-deploy` private key]
- **Description:** Private SSH key for staging authentication
- **⚠️ Important:** Include `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` lines
**Secret 4: STAGING_SSH_PORT**
- **Name:** `STAGING_SSH_PORT`
- **Value:** `22` (or your custom SSH port)
- **Description:** SSH port for staging server (optional, defaults to 22)
---
### Production Secrets Configuration
**Secret 5: PRODUCTION_HOST**
- **Name:** `PRODUCTION_HOST`
- **Value:** `michaelschiemer.de` (or your production server hostname/IP)
- **Description:** Production server hostname or IP address
**Secret 6: PRODUCTION_USER**
- **Name:** `PRODUCTION_USER`
- **Value:** `deploy` (or your deployment user)
- **Description:** SSH username for production deployments
**Secret 7: PRODUCTION_SSH_KEY**
- **Name:** `PRODUCTION_SSH_KEY`
- **Value:** [Paste entire content of `gitea-production-deploy` private key]
- **Description:** Private SSH key for production authentication
- **⚠️ Important:** Include `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` lines
**Secret 8: PRODUCTION_SSH_PORT**
- **Name:** `PRODUCTION_SSH_PORT`
- **Value:** `22` (or your custom SSH port)
- **Description:** SSH port for production server (optional, defaults to 22)
---
## Step 5: Verify Secrets Configuration
### Via Gitea UI
1. Navigate to `Settings``Secrets`
2. Verify all 8 secrets are listed:
- ✅ STAGING_HOST
- ✅ STAGING_USER
- ✅ STAGING_SSH_KEY
- ✅ STAGING_SSH_PORT
- ✅ PRODUCTION_HOST
- ✅ PRODUCTION_USER
- ✅ PRODUCTION_SSH_KEY
- ✅ PRODUCTION_SSH_PORT
3. Check that secrets show "Last Updated" timestamp
**Note:** Secret values are masked in the UI for security (you cannot view them after saving).
---
### Manual SSH Connection Test
Test SSH connections using the same credentials that workflows will use:
```bash
cd /home/michael/dev/michaelschiemer/.gitea/ssh-keys
# Test staging connection
ssh -i gitea-staging-deploy deploy@YOUR_STAGING_HOST \
"echo 'Staging SSH test successful'; docker --version"
# Test production connection
ssh -i gitea-production-deploy deploy@YOUR_PRODUCTION_HOST \
"echo 'Production SSH test successful'; docker --version"
```
**Expected Output:**
```
Staging SSH test successful
Docker version 24.0.7, build afdd53b
```
**If Connection Fails:**
- Verify hostname/IP is correct
- Check SSH port (try specifying: `ssh -p 2222 ...`)
- Verify public key is in `~/.ssh/authorized_keys` on server
- Check server firewall allows SSH connections
- Verify user has Docker permissions: `ssh user@host "docker ps"`
---
## Step 6: Test Workflow Access to Secrets
### Trigger Test Workflow
Create a minimal test workflow to verify secrets are accessible:
**File:** `.gitea/workflows/test-secrets.yml`
```yaml
name: Test Secrets Configuration
on:
workflow_dispatch:
jobs:
test-secrets:
runs-on: ubuntu-latest
steps:
- name: Test staging secrets
run: |
echo "Testing staging secrets..."
echo "STAGING_HOST: ${{ secrets.STAGING_HOST }}"
echo "STAGING_USER: ${{ secrets.STAGING_USER }}"
echo "STAGING_SSH_PORT: ${{ secrets.STAGING_SSH_PORT }}"
echo "STAGING_SSH_KEY length: ${#STAGING_SSH_KEY}"
env:
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
- name: Test production secrets
run: |
echo "Testing production secrets..."
echo "PRODUCTION_HOST: ${{ secrets.PRODUCTION_HOST }}"
echo "PRODUCTION_USER: ${{ secrets.PRODUCTION_USER }}"
echo "PRODUCTION_SSH_PORT: ${{ secrets.PRODUCTION_SSH_PORT }}"
echo "PRODUCTION_SSH_KEY length: ${#PRODUCTION_SSH_KEY}"
env:
PRODUCTION_SSH_KEY: ${{ secrets.PRODUCTION_SSH_KEY }}
- name: Test SSH connection to staging
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
script: |
echo "Staging SSH connection successful"
docker --version
docker-compose --version
- name: Test SSH connection to production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
echo "Production SSH connection successful"
docker --version
docker-compose --version
```
**Run Test:**
1. Commit and push test workflow: `git add .gitea/workflows/test-secrets.yml && git commit -m "Add secrets test workflow" && git push`
2. Go to Gitea Actions tab
3. Select "Test Secrets Configuration" workflow
4. Click "Run workflow"
5. Monitor execution logs
**Expected Result:**
- All secrets should be accessible
- SSH connections should succeed
- Docker/docker-compose versions should be displayed
---
## Security Best Practices
### SSH Key Management
**✅ Do:**
- Use 4096-bit RSA keys (or Ed25519)
- Generate separate keys for staging and production
- Rotate keys quarterly
- Store private keys in secure password manager
- Use dedicated deployment user (not root)
- Restrict deployment user permissions
**❌ Don't:**
- Commit private keys to git
- Share keys between environments
- Use personal SSH keys for deployments
- Store keys in plain text files
- Reuse keys across projects
---
### Secret Rotation Schedule
**Quarterly (Every 3 Months):**
1. Generate new SSH key pairs
2. Add new public keys to servers (keep old keys active)
3. Update Gitea secrets with new private keys
4. Test deployments with new keys
5. Remove old public keys from servers
6. Delete old private keys securely
**Template for Rotation:**
```bash
# Generate new keys with date suffix
ssh-keygen -t rsa -b 4096 -f gitea-staging-deploy-2025-04 \
-C "gitea-staging-deployment-2025-04" -N ""
# Repeat process from Step 2 onwards
```
---
### Access Control
**Gitea Repository Permissions:**
- Limit "Secrets" access to repository admins only
- Require 2FA for admin accounts
- Audit secret access logs regularly
**Server Access Control:**
- Deployment user should have minimal required permissions
- Use sudo configuration for Docker commands only
- Monitor SSH access logs
- Implement IP whitelisting if possible
---
## Troubleshooting
### Issue: Secret Not Found in Workflow
**Symptoms:**
- Workflow fails with "secret not found" error
- Secret value is empty in workflow logs
**Solutions:**
1. Verify secret name matches exactly (case-sensitive)
2. Check secret is created at repository level (not organization or user level)
3. Ensure workflow has access to repository secrets
4. Try re-creating the secret
---
### Issue: SSH Authentication Failed
**Symptoms:**
- Workflow fails: "Permission denied (publickey)"
- Cannot connect to server via SSH
**Solutions:**
1. Verify entire private key content is in secret (including BEGIN/END lines)
2. Check public key is in `~/.ssh/authorized_keys` on server
3. Verify SSH key format (PEM vs OpenSSH format)
4. Check server SSH configuration allows public key authentication
5. Test manual connection: `ssh -i key-file user@host`
---
### Issue: Docker Permission Denied
**Symptoms:**
- SSH connection succeeds but Docker commands fail
- Error: "permission denied while trying to connect to Docker daemon"
**Solutions:**
```bash
# Add deployment user to docker group (on server)
ssh deploy@server
sudo usermod -aG docker deploy
# Log out and back in for group changes to take effect
exit
ssh deploy@server
# Verify Docker access
docker ps
```
---
## Next Steps
After secrets are configured and tested:
1. ✅ Secrets configured in Gitea
2. ⏳ Test staging deployment workflow
3. ⏳ Test production deployment workflow
4. ⏳ Update main deployment documentation
5. ⏳ Set up monitoring and alerting
**Continue to:** `Testing Deployment Workflows` section in main README.md
---
**Last Updated:** 2025-01-28
**Guide Version:** 1.0
**Security Review Date:** 2025-01-28

View File

@@ -0,0 +1,2 @@
# Workflow Test - Fri Oct 31 09:29:43 PM CET 2025
# Workflow Test 2 - Fri Oct 31 09:33:19 PM CET 2025

View File

@@ -0,0 +1,132 @@
# CI/CD Workflow Testen
## ✅ Secrets sind konfiguriert - Perfekt!
## Option 1: Workflow manuell triggern (Empfohlen)
### Schritt 1: Gehe zu Actions
Öffne im Browser:
```
https://git.michaelschiemer.de/michael/michaelschiemer renewal/actions
```
### Schritt 2: Workflow auswählen
1. Suche nach "Production Deployment Pipeline"
2. Klicke auf den Workflow
### Schritt 3: Workflow manuell starten
1. Klicke auf "Run workflow" (rechts oben)
2. Wähle Branch: `main`
3. Optional: `skip_tests` deaktiviert lassen (Tests sollen laufen)
4. Klicke "Run workflow"
### Schritt 4: Logs beobachten
Der Workflow führt folgende Schritte aus:
1. **Checkout code** - Code wird ausgecheckt
2. **Run Tests** - PHP Tests werden ausgeführt
3. **Build Docker Image** - Docker Image wird gebaut
4. **Push to Registry** - Image wird zur Registry gepusht
5. **Deploy via Ansible** - Deployment auf Production-Server
**Beobachte die Logs** und prüfe jeden Schritt!
## Option 2: Automatisches Deployment via Commit
### Test-Commit pushen
```bash
# Stelle sicher, dass alles committed ist
git add .
git commit -m "test: CI/CD workflow test" || echo "Keine Änderungen"
# Push zu main Branch
git push origin main
```
Der Workflow startet automatisch nach dem Push.
## Was passiert beim Workflow?
### 1. Tests (ca. 2-5 Minuten)
- PHP Version Setup
- Composer Dependencies installieren
- Tests ausführen
### 2. Build (ca. 3-5 Minuten)
Live - Multi-Stage Docker Build:
- Composer Dependencies (Production)
- Frontend Build (npm)
- Finales Production Image
### 3. Push (ca. 1-2 Minuten)
- Docker Login zur Registry
- Image Tag generieren (SHA + Timestamp)
- Image zur Registry pushen
### 4. Deploy (ca. 2-4 Minuten)
- Ansible Playbook ausführen
- Image auf Production-Server pullen
- Application Stack aktualisieren
- Services neu starten
**Gesamtzeit: ~8-15 Minuten**
## Workflow-Status prüfen
### In Gitea UI:
```
https://git.michaelschiemer.de/michael/michaelschiemer/actions
```
### Via Command Line:
```bash
# Prüfe ob Workflow läuft
curl -s -H "Authorization: token <DEIN_TOKEN>" \
"https://git.michaelschiemer.de/api/v1/repos/michael/michaelschiemer/actions/runs" | \
jq '.workflow_runs[0] | {status, conclusion, created_at}'
```
## Erfolgreiche Ausführung erkennen
Der Workflow ist erfolgreich, wenn:
✅ Alle Jobs grün sind
✅ Keine Fehler in den Logs
✅ Letzter Schritt "Deploy via Ansible" erfolgreich
✅ Application läuft auf Production-Server
## Troubleshooting
### Workflow startet nicht
- Prüfe, ob `.gitea/workflows/production-deploy.yml` im Repository ist
- Prüfe, ob Workflow-Syntax korrekt ist
- Prüfe Gitea Actions ist aktiviert
### "Secret not found" Fehler
- Prüfe, ob alle drei Secrets gesetzt sind
- Prüfe, ob Namen exakt übereinstimmen (Groß-/Kleinschreibung!)
### Registry Login fehlgeschlagen
- Prüfe `REGISTRY_USER` und `REGISTRY_PASSWORD` Secrets
- Prüfe Registry erreichbar: `curl -u admin:registry-secure-password-2025 http://127.0.0.1:5000/v2/_catalog`
### Deployment fehlgeschlagen
- Prüfe `SSH_PRIVATE_KEY` Secret
- Prüfe Ansible-Verbindung zum Server
- Prüfe Server-Logs: `ssh deploy@94.16.110.151 "docker compose -f ~/deployment/stacks/application/docker-compose.yml logs"`
## Nächste Schritte nach erfolgreichem Test
1. ✅ Workflow funktioniert
2. ✅ Automatisches Deployment getestet
3. ✅ Production-Stack läuft
**Du kannst jetzt:**
- Normale Commits pushen → Automatisches Deployment
- Workflow manuell triggern für kontrollierte Deployments
- Branch-Protection aktivieren für sichere Deployments

View File

@@ -0,0 +1,75 @@
# Token-Probleme beheben
## Problem
Das Token hat nicht die richtigen Scopes oder das Repository wurde nicht gefunden.
## Lösung: Neuen Token mit richtigen Scopes erstellen
### Schritt 1: Token neu generieren
1. **Gehe zu Gitea Settings:**
```
https://git.michaelschiemer.de/user/settings/applications
```
2. **Falls bereits ein Token existiert:**
- Lösche den alten Token (falls nötig)
- Oder erstelle einen neuen mit anderen Namen
3. **Klicke "Generate New Token"**
4. **WICHTIG - Diese Scopes aktivieren:**
- ✅ `read:user` (mindestens erforderlich)
- ✅ `write:repository` (für Secrets schreiben)
- ✅ Oder wähle **alle Scopes** für volle Berechtigung
5. **Token kopieren** (wird nur einmal angezeigt!)
### Schritt 2: Repository-Name prüfen
Prüfe, ob das Repository wirklich `michael/michaelschiemer` heißt:
```bash
# Prüfe Remote-URL
git remote get-url origin
# Sollte zeigen:
# https://git.michaelschiemer.de/michael/michaelschiemer.git
```
Falls der Name anders ist, setze die Umgebungsvariable:
```bash
REPO_OWNER=<owner> REPO_NAME=<name> bash scripts/setup-gitea-secrets-with-token.sh <token>
```
### Schritt 3: Script erneut ausführen
```bash
bash scripts/setup-gitea-secrets-with-token.sh <NEUER_TOKEN>
```
## Alternative: Manuelles Setup über UI
Falls das automatische Setup weiterhin Probleme macht:
1. **Gehe zu:**
```
https://git.michaelschiemer.de/michael/michaelschiemer/settings/secrets/actions
```
2. **Füge manuell hinzu:**
- `REGISTRY_USER` = `admin`
- `REGISTRY_PASSWORD` = `registry-secure-password-2025`
- `SSH_PRIVATE_KEY` = `cat ~/.ssh/production`
## Troubleshooting
### "token does not have at least one of required scope(s)"
→ Token benötigt `read:user` Scope - neuen Token mit diesem Scope generieren
### "The target couldn't be found" (404)
→ Repository existiert nicht oder falscher Name - prüfe Repository-URL
### "404 page not found" bei Secrets-Endpoint
→ Actions möglicherweise nicht aktiviert - prüfe in Gitea Admin-Panel

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,256 @@
name: 🧊 Warm Docker Build Cache
run-name: Warm Cache - ${{ inputs.branch || 'main' }}
on:
schedule:
- cron: '0 4 * * 0'
workflow_dispatch:
inputs:
branch:
description: 'Branch to use for warming caches'
required: false
default: 'main'
env:
REGISTRY: registry.michaelschiemer.de
IMAGE_NAME: framework
RUNTIME_IMAGE_NAME: framework-runtime
jobs:
warm:
name: Refresh Buildx Caches
runs-on: docker-build
steps:
- name: Install git and tooling
shell: sh
run: |
if ! command -v bash >/dev/null 2>&1 || ! command -v git >/dev/null 2>&1; then
apk add --no-cache git bash curl
fi
bash --version
git --version
- name: Determine target ref
id: target
shell: bash
run: |
TARGET="${{ inputs.branch }}"
if [ -z "$TARGET" ]; then
TARGET="main"
fi
echo "target_ref=$TARGET" >> "$GITHUB_OUTPUT"
echo "TARGET_REF=$TARGET" >> $GITHUB_ENV
echo "BRANCH_NAME=$TARGET" >> $GITHUB_ENV
- name: Download CI helpers
shell: bash
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
REF="${{ steps.target.outputs.target_ref }}"
URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh"
mkdir -p /tmp/ci-tools
if [ -n "$CI_TOKEN" ]; then
curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh
else
curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh
fi
chmod +x /tmp/ci-tools/clone_repo.sh
- name: Checkout repository
shell: bash
env:
TARGET_REF: ${{ steps.target.outputs.target_ref }}
run: |
export CI_REPOSITORY="${{ github.repository }}"
export CI_TOKEN="${{ secrets.CI_TOKEN }}"
export CI_REF_NAME="$TARGET_REF"
export CI_INPUT_BRANCH="$TARGET_REF"
export CI_DEFAULT_BRANCH="main"
export CI_TARGET_DIR="/workspace/repo"
export CI_FETCH_DEPTH="1"
/tmp/ci-tools/clone_repo.sh
cd /workspace/repo
git rev-parse HEAD
- name: Setup Docker Buildx
shell: bash
run: |
docker buildx version || echo "Buildx nicht gefunden"
if ! docker buildx ls 2>/dev/null | grep -q builder; then
docker buildx create --name builder --use --driver docker-container
else
docker buildx use builder
fi
docker buildx inspect --bootstrap
- name: Login to registry
id: login
shell: bash
run: |
REGISTRY_USER="${{ secrets.REGISTRY_USER }}"
REGISTRY_PASSWORD="${{ secrets.REGISTRY_PASSWORD }}"
REGISTRY_URL="${{ env.REGISTRY }}"
DEPLOYMENT_HOST="94.16.110.151"
if [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then
echo "❌ Error: Registry credentials missing"
exit 1
fi
echo "🔐 Logging in to registry..."
HOST_IP=$(ip route | grep default | awk '{print $3}' 2>/dev/null | head -1 || echo "$DEPLOYMENT_HOST")
REGISTRY_URLS=(
"registry.michaelschiemer.de"
"$REGISTRY_URL"
"$DEPLOYMENT_HOST"
"$DEPLOYMENT_HOST:5000"
"${HOST_IP}:5000"
)
LOGIN_SUCCESS=false
for TEST_URL in "${REGISTRY_URLS[@]}"; do
echo "🔍 Testing registry: $TEST_URL"
if [[ "$TEST_URL" == *":5000" ]]; then
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://$TEST_URL/v2/" 2>&1 || echo "000")
if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "200" ]; then
set +e
LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1)
LOGIN_EXIT_CODE=$?
set -e
if [ $LOGIN_EXIT_CODE -eq 0 ]; then
REGISTRY_URL="$TEST_URL"
LOGIN_SUCCESS=true
break
fi
fi
else
HTTPS_CODE=$(curl -k -s -o /dev/null -w "%{http_code}" "https://$TEST_URL/v2/" 2>&1 || echo "000")
if [ "$HTTPS_CODE" = "401" ] || [ "$HTTPS_CODE" = "200" ]; then
set +e
LOGIN_OUTPUT=$(echo "$REGISTRY_PASSWORD" | docker login "$TEST_URL" -u "$REGISTRY_USER" --password-stdin 2>&1)
LOGIN_EXIT_CODE=$?
set -e
if [ $LOGIN_EXIT_CODE -eq 0 ]; then
REGISTRY_URL="$TEST_URL"
LOGIN_SUCCESS=true
break
fi
fi
fi
done
if [ "$LOGIN_SUCCESS" = false ]; then
echo "❌ Registry login failed"
exit 1
fi
echo "✅ Registry login successful: $REGISTRY_URL"
echo "REGISTRY_URL=$REGISTRY_URL" >> $GITHUB_ENV
echo "CACHE_REGISTRY=${{ env.REGISTRY }}" >> $GITHUB_ENV
- name: Warm runtime base cache
shell: bash
env:
REGISTRY_URL: ${{ env.REGISTRY_URL }}
CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }}
run: |
cd /workspace/repo
TARGET_REGISTRY="$CACHE_REGISTRY"
if [ -z "$TARGET_REGISTRY" ]; then
TARGET_REGISTRY="$REGISTRY_URL"
fi
IMAGE_NAME="${{ env.RUNTIME_IMAGE_NAME }}"
DATE_TAG="warm-$(date -u +%Y%m%d%H%M)"
BRANCH_NAME="${{ env.BRANCH_NAME || 'main' }}"
# Build cache sources - multiple sources for better cache hit rate
CACHE_SOURCES=(
"type=registry,ref=${TARGET_REGISTRY}/${IMAGE_NAME}:buildcache"
"type=registry,ref=${TARGET_REGISTRY}/${IMAGE_NAME}:${BRANCH_NAME}-cache"
"type=registry,ref=${TARGET_REGISTRY}/${IMAGE_NAME}:latest"
)
CACHE_FROM_ARGS=""
for CACHE_SRC in "${CACHE_SOURCES[@]}"; do
CACHE_FROM_ARGS="${CACHE_FROM_ARGS} --cache-from ${CACHE_SRC}"
done
docker buildx build \
--platform linux/amd64 \
--file ./Dockerfile.production \
--target runtime-base \
--build-arg RUNTIME_IMAGE=runtime-base \
${CACHE_FROM_ARGS} \
--cache-to type=registry,ref="${TARGET_REGISTRY}/${IMAGE_NAME}:buildcache",mode=max \
--cache-to type=registry,ref="${TARGET_REGISTRY}/${IMAGE_NAME}:${BRANCH_NAME}-cache",mode=max \
--tag "$TARGET_REGISTRY/$IMAGE_NAME:$DATE_TAG" \
--push \
.
- name: Warm production cache
shell: bash
env:
REGISTRY_URL: ${{ env.REGISTRY_URL }}
CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }}
run: |
cd /workspace/repo
REGISTRY_TO_USE="$REGISTRY_URL"
CACHE_TARGET="$CACHE_REGISTRY"
if [ -z "$CACHE_TARGET" ]; then
CACHE_TARGET="$REGISTRY_TO_USE"
fi
IMAGE_NAME="${{ env.IMAGE_NAME }}"
DATE_TAG="warm-$(date -u +%Y%m%d%H%M)"
BRANCH_NAME="${{ env.BRANCH_NAME || 'main' }}"
DEFAULT_RUNTIME="$CACHE_TARGET/${{ env.RUNTIME_IMAGE_NAME }}:latest"
RUNTIME_ARG="runtime-base"
if docker pull "$DEFAULT_RUNTIME" >/dev/null 2>&1; then
RUNTIME_ARG="$DEFAULT_RUNTIME"
fi
# Build cache sources - multiple sources for better cache hit rate
CACHE_SOURCES=(
"type=registry,ref=${CACHE_TARGET}/${IMAGE_NAME}:buildcache"
"type=registry,ref=${REGISTRY_TO_USE}/${IMAGE_NAME}:${BRANCH_NAME}-cache"
"type=registry,ref=${REGISTRY_TO_USE}/${IMAGE_NAME}:latest"
)
CACHE_FROM_ARGS=""
for CACHE_SRC in "${CACHE_SOURCES[@]}"; do
CACHE_FROM_ARGS="${CACHE_FROM_ARGS} --cache-from ${CACHE_SRC}"
done
docker buildx build \
--platform linux/amd64 \
--file ./Dockerfile.production \
--build-arg RUNTIME_IMAGE="$RUNTIME_ARG" \
${CACHE_FROM_ARGS} \
--cache-to type=registry,ref="${CACHE_TARGET}/${IMAGE_NAME}:buildcache",mode=max \
--cache-to type=registry,ref="${REGISTRY_TO_USE}/${IMAGE_NAME}:${BRANCH_NAME}-cache",mode=max \
--tag "$REGISTRY_TO_USE/$IMAGE_NAME:$DATE_TAG" \
--push \
.
- name: Cleanup warm tags
shell: bash
env:
REGISTRY_URL: ${{ env.REGISTRY_URL }}
CACHE_REGISTRY: ${{ env.CACHE_REGISTRY }}
run: |
echo " Cleanup of warm tags deferred to registry retention policy"

View File

@@ -0,0 +1,133 @@
name: ✅ Continuous Integration
run-name: CI Checks - ${{ github.ref_name || github.head_ref }}
on:
push:
branches-ignore:
- main
- staging
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches:
- main
- staging
jobs:
tests:
name: Run Tests & Quality Checks
runs-on: php-ci
steps:
- name: Download CI helpers
shell: bash
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
REF="${{ github.sha }}"
if [ -z "$REF" ]; then
REF="${{ github.ref_name }}"
fi
if [ -z "$REF" ]; then
REF="${{ github.head_ref }}"
fi
if [ -z "$REF" ]; then
REF="${{ github.base_ref || 'develop' }}"
fi
URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh"
mkdir -p /tmp/ci-tools
if [ -n "$CI_TOKEN" ]; then
curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh
else
curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh
fi
chmod +x /tmp/ci-tools/clone_repo.sh
- name: Checkout code
env:
REF_NAME_GITHUB: ${{ github.ref_name }}
HEAD_REF: ${{ github.head_ref }}
BASE_REF: ${{ github.base_ref }}
run: |
REF_NAME="$REF_NAME_GITHUB"
if [ -z "$REF_NAME" ]; then
REF_NAME="$HEAD_REF"
fi
if [ -z "$REF_NAME" ]; then
REF_NAME="$BASE_REF"
fi
if [ -z "$REF_NAME" ]; then
REF_NAME="develop"
fi
export CI_REPOSITORY="${{ github.repository }}"
export CI_TOKEN="${{ secrets.CI_TOKEN }}"
export CI_REF_NAME="$REF_NAME"
export CI_INPUT_BRANCH="$HEAD_REF"
export CI_DEFAULT_BRANCH="develop"
export CI_TARGET_DIR="/workspace/repo"
export CI_FETCH_DEPTH="1"
/tmp/ci-tools/clone_repo.sh
cd /workspace/repo
# Note: Composer caching via actions/cache@v4 requires Node.js in runner
# Skipped for now - vendor/ is cached in runner workspace
- name: Install PHP dependencies
run: |
cd /workspace/repo
composer install --no-interaction --prefer-dist --optimize-autoloader --ignore-platform-req=php
- name: PHPStan (baseline)
run: |
cd /workspace/repo
make phpstan || echo "⚠️ phpstan skipped/failed"
- name: Lint PHP (dry run)
run: |
cd /workspace/repo
make cs || echo "⚠️ php-cs-fixer dry run issues detected"
- name: Validate .env.base for secrets
run: |
cd /workspace/repo
if [ -f .env.base ]; then
echo "🔍 Checking .env.base for secrets..."
# Check for potential secrets (case-insensitive)
if grep -qiE "(password|secret|key|token|encryption|vault)" .env.base | grep -v "^#" | grep -v "FILE=" | grep -v "^$$" > /dev/null; then
echo "::error::.env.base contains potential secrets! Secrets should be in .env.local or Docker Secrets."
echo "⚠️ Found potential secrets in .env.base:"
grep -iE "(password|secret|key|token|encryption|vault)" .env.base | grep -v "^#" | grep -v "FILE=" | grep -v "^$$" || true
echo ""
echo "💡 Move secrets to:"
echo " - .env.local (for local development)"
echo " - Docker Secrets (for production/staging)"
exit 1
else
echo "✅ .env.base does not contain secrets"
fi
else
echo " .env.base not found (optional during migration)"
fi
echo ""
echo "🔍 Checking docker-compose.base.yml for hardcoded passwords..."
if grep -E "(PASSWORD|SECRET|TOKEN).*:-[^}]*[^}]}" docker-compose.base.yml 2>/dev/null | grep -v "^#" | grep -v "FILE=" > /dev/null; then
echo "::error::docker-compose.base.yml contains hardcoded password fallbacks! Passwords must be set explicitly."
echo "⚠️ Found hardcoded password fallbacks:"
grep -E "(PASSWORD|SECRET|TOKEN).*:-[^}]*[^}]}" docker-compose.base.yml | grep -v "^#" | grep -v "FILE=" || true
echo ""
echo "💡 Remove fallback values (:-...) from base file"
echo " Passwords must be set in .env.local or via Docker Secrets"
exit 1
else
echo "✅ docker-compose.base.yml does not contain hardcoded password fallbacks"
fi
- name: Tests temporarily skipped
run: |
echo "⚠️ Tests temporarily skipped due to PHP 8.5 compatibility issues"

View File

@@ -0,0 +1,312 @@
name: Deploy to Production
on:
push:
branches:
- main
- production
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild Docker image'
required: false
default: 'false'
skip_backup:
description: 'Skip database backup (not recommended)'
required: false
default: 'false'
env:
REGISTRY: localhost:5000
IMAGE_NAME: framework
IMAGE_TAG: latest
COMPOSE_PROJECT_NAME: framework-production
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker image
run: |
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}"
docker build \
--file docker/php/Dockerfile \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
--build-arg ENV=production \
--build-arg COMPOSER_INSTALL_FLAGS="--no-dev --optimize-autoloader --no-interaction" \
.
- name: Push image to private registry
run: |
echo "Pushing image to registry..."
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
- name: Prepare deployment files
run: |
echo "Preparing deployment files..."
mkdir -p deployment-production
cp docker-compose.base.yml deployment-production/
cp docker-compose.prod.yml deployment-production/
cp -r docker deployment-production/
# Create deployment script
cat > deployment-production/deploy.sh << 'EOF'
#!/bin/bash
set -e
echo "=================================================="
echo "Starting Production Deployment"
echo "=================================================="
echo ""
# Database backup (unless explicitly skipped)
if [ "${SKIP_BACKUP}" != "true" ]; then
echo "[0/6] Creating database backup..."
BACKUP_FILE="backup_$(date +%Y%m%d_%H%M%S).sql"
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \
php console.php db:backup --output="/var/www/html/storage/backups/${BACKUP_FILE}" || {
echo "⚠️ Database backup failed - deployment aborted"
exit 1
}
echo "✅ Database backup created: ${BACKUP_FILE}"
else
echo "⚠️ Database backup skipped (not recommended for production)"
fi
# Pull latest images
echo "[1/6] Pulling latest Docker images..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml pull
# Stop existing containers gracefully
echo "[2/6] Stopping existing containers (graceful shutdown)..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml stop
# Start new containers
echo "[3/6] Starting new containers..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
# Wait for services to be healthy (longer timeout for production)
echo "[4/6] Waiting for services to be healthy..."
sleep 30
# Run database migrations
echo "[5/6] Running database migrations..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \
php console.php db:migrate --force || {
echo "⚠️ Database migration failed"
exit 1
}
# Verify deployment
echo "[6/6] Verifying deployment..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
# Cleanup old containers
echo "Cleaning up old containers..."
docker system prune -f
echo ""
echo "=================================================="
echo "Production Deployment Complete"
echo "=================================================="
EOF
chmod +x deployment-production/deploy.sh
- name: Deploy to production server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
# Create deployment directory
mkdir -p /opt/framework-production
cd /opt/framework-production
# Backup current deployment
if [ -d "current" ]; then
echo "Backing up current deployment..."
timestamp=$(date +%Y%m%d_%H%M%S)
mv current "backup_${timestamp}"
# Keep only last 10 backups for production
ls -dt backup_* | tail -n +11 | xargs rm -rf
fi
# Create new deployment directory
mkdir -p current
cd current
- name: Copy deployment files
uses: appleboy/scp-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
source: "deployment-production/*"
target: "/opt/framework-production/current/"
strip_components: 1
- name: Execute deployment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
cd /opt/framework-production/current
# Set skip backup flag if provided
export SKIP_BACKUP="${{ github.event.inputs.skip_backup || 'false' }}"
# Execute deployment script
./deploy.sh
- name: Health check
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
cd /opt/framework-production/current
# Wait for services to be fully ready (longer for production)
echo "Waiting 60 seconds for services to initialize..."
sleep 60
# Check container status
echo "Checking container status..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
# Check service health
echo "Checking service health..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app php -v
# Check PHP-FPM is running
echo "Checking PHP-FPM process..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app pgrep php-fpm
# Test HTTP endpoint (via Traefik)
echo "Testing production endpoint..."
curl -f -k https://michaelschiemer.de/health || {
echo "⚠️ Health check endpoint failed"
exit 1
}
# Check Redis connection
echo "Checking Redis connection..."
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-redis redis-cli ping
echo ""
echo "✅ All health checks passed!"
- name: Smoke tests
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
echo "Running production smoke tests..."
# Test main page
curl -f -k https://michaelschiemer.de/ > /dev/null 2>&1 && echo "✅ Main page accessible" || {
echo "❌ Main page failed"
exit 1
}
# Test API health
curl -f -k https://michaelschiemer.de/api/health > /dev/null 2>&1 && echo "✅ API health check passed" || {
echo "❌ API health check failed"
exit 1
}
echo "✅ Smoke tests completed successfully"
- name: Rollback on failure
if: failure()
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
cd /opt/framework-production
if [ -d "$(ls -dt backup_* 2>/dev/null | head -n1)" ]; then
echo "🚨 Rolling back to previous deployment..."
latest_backup=$(ls -dt backup_* | head -n1)
# Stop current broken deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml down
cd ..
# Restore backup
rm -rf current
cp -r "$latest_backup" current
# Start restored deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d
# Wait for services
sleep 30
# Verify rollback
docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps
echo "✅ Rollback complete - previous version restored"
else
echo "❌ No backup available for rollback"
echo "⚠️ MANUAL INTERVENTION REQUIRED"
exit 1
fi
- name: Notify deployment status
if: always()
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }}
script: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ Production deployment successful"
echo "URL: https://michaelschiemer.de"
echo "Deployed at: $(date)"
# Log deployment
echo "$(date) - Deployment SUCCESS - Commit: ${{ github.sha }}" >> /opt/framework-production/deployment.log
else
echo "❌ Production deployment failed - rollback executed"
# Log deployment failure
echo "$(date) - Deployment FAILED - Commit: ${{ github.sha }}" >> /opt/framework-production/deployment.log
# Send alert (placeholder - implement actual alerting)
echo "⚠️ ALERT: Production deployment failed. Manual intervention may be required."
fi
- name: Clean up build artifacts
if: always()
run: |
echo "Cleaning up deployment artifacts..."
rm -rf deployment-production
echo "✅ Cleanup complete"

View File

@@ -0,0 +1,212 @@
name: Deploy to Staging
on:
push:
branches:
- staging
workflow_dispatch:
inputs:
force_rebuild:
description: 'Force rebuild Docker image'
required: false
default: 'false'
env:
REGISTRY: localhost:5000
IMAGE_NAME: framework
IMAGE_TAG: staging
COMPOSE_PROJECT_NAME: framework-staging
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build Docker image
run: |
echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}"
docker build \
--file docker/php/Dockerfile \
--tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \
--build-arg ENV=staging \
--build-arg COMPOSER_INSTALL_FLAGS="--no-dev --optimize-autoloader --no-interaction" \
.
- name: Push image to private registry
run: |
echo "Pushing image to registry..."
docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}
- name: Prepare deployment files
run: |
echo "Preparing deployment files..."
mkdir -p deployment-staging
cp docker-compose.base.yml deployment-staging/
cp docker-compose.staging.yml deployment-staging/
cp -r docker deployment-staging/
# Create deployment script
cat > deployment-staging/deploy.sh << 'EOF'
#!/bin/bash
set -e
echo "=================================================="
echo "Starting Staging Deployment"
echo "=================================================="
echo ""
# Pull latest images
echo "[1/5] Pulling latest Docker images..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml pull
# Stop existing containers
echo "[2/5] Stopping existing containers..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml down
# Start new containers
echo "[3/5] Starting new containers..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d
# Wait for services to be healthy
echo "[4/5] Waiting for services to be healthy..."
sleep 10
# Verify health
echo "[5/5] Verifying deployment..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
echo ""
echo "=================================================="
echo "Staging Deployment Complete"
echo "=================================================="
EOF
chmod +x deployment-staging/deploy.sh
- name: Deploy to staging server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
script: |
# Create deployment directory
mkdir -p /opt/framework-staging
cd /opt/framework-staging
# Backup current deployment
if [ -d "current" ]; then
echo "Backing up current deployment..."
timestamp=$(date +%Y%m%d_%H%M%S)
mv current "backup_${timestamp}"
# Keep only last 5 backups
ls -dt backup_* | tail -n +6 | xargs rm -rf
fi
# Create new deployment directory
mkdir -p current
cd current
- name: Copy deployment files
uses: appleboy/scp-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
source: "deployment-staging/*"
target: "/opt/framework-staging/current/"
strip_components: 1
- name: Execute deployment
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
script: |
cd /opt/framework-staging/current
# Execute deployment script
./deploy.sh
- name: Health check
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
script: |
cd /opt/framework-staging/current
# Wait for services to be fully ready
echo "Waiting 30 seconds for services to initialize..."
sleep 30
# Check container status
echo "Checking container status..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml ps
# Check service health
echo "Checking service health..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-app php -v
# Test HTTP endpoint (via internal network)
echo "Testing HTTP endpoint..."
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml exec -T staging-nginx wget -q -O- http://localhost/health || echo "Health check endpoint not yet available"
echo ""
echo "Health check complete!"
- name: Rollback on failure
if: failure()
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_SSH_KEY }}
port: ${{ secrets.STAGING_SSH_PORT || 22 }}
script: |
cd /opt/framework-staging
if [ -d "$(ls -dt backup_* | head -n1)" ]; then
echo "Rolling back to previous deployment..."
latest_backup=$(ls -dt backup_* | head -n1)
# Stop current broken deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml down
cd ..
# Restore backup
rm -rf current
cp -r "$latest_backup" current
# Start restored deployment
cd current
docker-compose -f docker-compose.base.yml -f docker-compose.staging.yml up -d
echo "Rollback complete!"
else
echo "No backup available for rollback"
fi
- name: Notify deployment status
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ Staging deployment successful"
echo "URL: https://staging.michaelschiemer.de"
else
echo "❌ Staging deployment failed - rollback executed"
fi

View File

@@ -0,0 +1,286 @@
name: 🚀 Manual Deployment
run-name: Manual Deploy - ${{ inputs.environment }} - ${{ inputs.image_tag || 'latest' }}
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
type: choice
options:
- staging
- production
image_tag:
description: 'Image tag to deploy (e.g. abc1234-1696234567, git-abc1234). Leave empty for latest'
required: false
type: string
default: ''
branch:
description: 'Branch to checkout (default: main for production, staging for staging)'
required: false
type: string
default: ''
env:
REGISTRY: registry.michaelschiemer.de
IMAGE_NAME: framework
DEPLOYMENT_HOST: 94.16.110.151
jobs:
determine-image:
name: Determine Deployment Image
runs-on: ubuntu-latest
outputs:
image_url: ${{ steps.image.outputs.image_url }}
image_tag: ${{ steps.image.outputs.image_tag }}
registry_host: ${{ env.REGISTRY }}
image_name: ${{ env.IMAGE_NAME }}
steps:
- name: Determine image to deploy
id: image
shell: bash
run: |
REGISTRY="${{ env.REGISTRY }}"
IMAGE_NAME="${{ env.IMAGE_NAME }}"
INPUT_TAG="${{ inputs.image_tag }}"
if [ -z "$INPUT_TAG" ] || [ "$INPUT_TAG" = "" ]; then
IMAGE_TAG="latest"
else
IMAGE_TAG="$INPUT_TAG"
fi
IMAGE_URL="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "image_url=${IMAGE_URL}" >> "$GITHUB_OUTPUT"
echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "📦 Deployment Image:"
echo " URL: ${IMAGE_URL}"
echo " Tag: ${IMAGE_TAG}"
echo ""
echo " Image will be validated during deployment"
deploy-staging:
name: Deploy to Staging
needs: determine-image
if: inputs.environment == 'staging'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.michaelschiemer.de
steps:
- name: Determine branch name
id: branch
shell: bash
run: |
INPUT_BRANCH="${{ inputs.branch }}"
if [ -z "$INPUT_BRANCH" ] || [ "$INPUT_BRANCH" = "" ]; then
REF_NAME="staging"
else
REF_NAME="$INPUT_BRANCH"
fi
echo "BRANCH=$REF_NAME" >> $GITHUB_OUTPUT
echo "📋 Branch: $REF_NAME"
- name: Checkout deployment scripts
run: |
REF_NAME="${{ steps.branch.outputs.BRANCH }}"
REPO="${{ github.repository }}"
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
git clone --depth 1 --branch "$REF_NAME" \
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
else
git clone --depth 1 --branch "$REF_NAME" \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo || \
git clone --depth 1 \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
fi
cd /workspace/repo
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
- name: Create Ansible Vault password file
run: |
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
echo "✅ Vault password file created"
else
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
touch /tmp/vault_pass
chmod 600 /tmp/vault_pass
fi
- name: Deploy to Staging (Complete)
run: |
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/deploy-complete.yml \
-e "deployment_environment=staging" \
-e "deployment_hosts=production" \
-e "git_branch=${{ steps.branch.outputs.BRANCH }}" \
-e "image_tag=${{ needs.determine-image.outputs.image_tag }}" \
-e "docker_registry=${{ needs.determine-image.outputs.registry_host }}" \
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
--vault-password-file /tmp/vault_pass \
--private-key ~/.ssh/production
- name: Wait for deployment to stabilize
run: sleep 30
- name: Health check
id: health
run: |
echo "🔍 Performing health checks with exponential backoff..."
DELAY=2
MAX_DELAY=60
MAX_ATTEMPTS=5
for i in $(seq 1 $MAX_ATTEMPTS); do
if curl -f -k -s https://staging.michaelschiemer.de/health > /dev/null 2>&1; then
echo "✅ Health check passed (attempt $i/$MAX_ATTEMPTS)"
exit 0
fi
if [ $i -lt $MAX_ATTEMPTS ]; then
echo "⏳ Waiting for staging service... (attempt $i/$MAX_ATTEMPTS, delay ${DELAY}s)"
sleep $DELAY
DELAY=$((DELAY * 2))
[ $DELAY -gt $MAX_DELAY ] && DELAY=$MAX_DELAY
fi
done
echo "❌ Health check failed after $MAX_ATTEMPTS attempts"
exit 1
- name: Notify deployment success
if: success()
run: |
echo "🚀 Staging deployment successful!"
echo "URL: https://staging.michaelschiemer.de"
echo "Image: ${{ needs.determine-image.outputs.image_url }}"
deploy-production:
name: Deploy to Production
needs: determine-image
if: inputs.environment == 'production'
runs-on: ubuntu-latest
concurrency:
group: deploy-production
cancel-in-progress: false
environment:
name: production
url: https://michaelschiemer.de
steps:
- name: Determine branch name
id: branch
shell: bash
run: |
INPUT_BRANCH="${{ inputs.branch }}"
if [ -z "$INPUT_BRANCH" ] || [ "$INPUT_BRANCH" = "" ]; then
REF_NAME="main"
else
REF_NAME="$INPUT_BRANCH"
fi
echo "BRANCH=$REF_NAME" >> $GITHUB_OUTPUT
echo "📋 Branch: $REF_NAME"
- name: Checkout deployment scripts
run: |
REF_NAME="${{ steps.branch.outputs.BRANCH }}"
REPO="${{ github.repository }}"
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
git clone --depth 1 --branch "$REF_NAME" \
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
else
git clone --depth 1 --branch "$REF_NAME" \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo || \
git clone --depth 1 \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
fi
cd /workspace/repo
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
- name: Create Ansible Vault password file
run: |
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
echo "✅ Vault password file created"
else
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
touch /tmp/vault_pass
chmod 600 /tmp/vault_pass
fi
- name: Deploy to Production (Complete)
run: |
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/deploy-complete.yml \
-e "deployment_environment=production" \
-e "deployment_hosts=production" \
-e "git_branch=${{ steps.branch.outputs.BRANCH }}" \
-e "image_tag=${{ needs.determine-image.outputs.image_tag }}" \
-e "docker_registry=${{ needs.determine-image.outputs.registry_host }}" \
-e "docker_registry_username=${{ secrets.REGISTRY_USER }}" \
-e "docker_registry_password=${{ secrets.REGISTRY_PASSWORD }}" \
--vault-password-file /tmp/vault_pass \
--private-key ~/.ssh/production
- name: Wait for deployment to stabilize
run: sleep 30
- name: Health check
id: health
run: |
echo "🔍 Performing health checks with exponential backoff..."
DELAY=2
MAX_DELAY=60
MAX_ATTEMPTS=5
for i in $(seq 1 $MAX_ATTEMPTS); do
if curl -f -k -s https://michaelschiemer.de/health > /dev/null 2>&1; then
echo "✅ Health check passed (attempt $i/$MAX_ATTEMPTS)"
exit 0
fi
if [ $i -lt $MAX_ATTEMPTS ]; then
echo "⏳ Waiting for production service... (attempt $i/$MAX_ATTEMPTS, delay ${DELAY}s)"
sleep $DELAY
DELAY=$((DELAY * 2))
[ $DELAY -gt $MAX_DELAY ] && DELAY=$MAX_DELAY
fi
done
echo "❌ Health check failed after $MAX_ATTEMPTS attempts"
exit 1
- name: Notify deployment success
if: success()
run: |
echo "🚀 Production deployment successful!"
echo "URL: https://michaelschiemer.de"
echo "Image: ${{ needs.determine-image.outputs.image_url }}"

View File

@@ -0,0 +1,89 @@
name: 📊 Monitor Workflow Performance
on:
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
lookback_hours:
description: 'Hours to look back for metrics'
required: false
default: '24'
type: string
env:
DEPLOYMENT_HOST: 94.16.110.151
jobs:
monitor:
name: Monitor Workflow Performance
runs-on: php-ci
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
- name: Create Ansible Vault password file
run: |
if [ -n "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" ]; then
echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
echo "✅ Vault password file created"
else
echo "⚠️ ANSIBLE_VAULT_PASSWORD secret not set, using empty password file"
touch /tmp/vault_pass
chmod 600 /tmp/vault_pass
fi
- name: Run performance monitoring
run: |
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/monitor-workflow-performance.yml \
-e "monitoring_lookback_hours=${{ github.event.inputs.lookback_hours || '24' }}" \
--vault-password-file /tmp/vault_pass \
--private-key ~/.ssh/production
- name: Collect metrics files
run: |
ssh -i ~/.ssh/production deploy@${{ env.DEPLOYMENT_HOST }} \
"find /home/deploy/monitoring/workflow-metrics -name 'workflow_metrics_*.json' -mtime -1 -exec cat {} \; | jq -s '.'" \
> /tmp/combined_metrics.json || echo "[]" > /tmp/combined_metrics.json
- name: Display metrics summary
run: |
if [ -f /tmp/combined_metrics.json ] && [ -s /tmp/combined_metrics.json ]; then
echo "📊 Performance Metrics Summary:"
echo "=================================="
cat /tmp/combined_metrics.json | jq -r '
.[] |
"Timestamp: \(.timestamp)",
"System Load: \(.system_metrics.load_average)",
"CPU Usage: \(.system_metrics.cpu_usage_percent)%",
"Memory: \(.system_metrics.memory_usage)",
"Gitea Runner: \(.gitea_metrics.runner_status)",
"Gitea API Response: \(.gitea_metrics.api_response_time_ms)ms",
"Workflow Log Entries: \(.gitea_metrics.workflow_log_entries_last_24h)",
"---"
' || echo "⚠️ Could not parse metrics"
else
echo "⚠️ No metrics collected"
fi
- name: Upload metrics as artifact
uses: actions/upload-artifact@v3
with:
name: workflow-metrics
path: /tmp/combined_metrics.json
retention-days: 30
if: always()

View File

@@ -0,0 +1,304 @@
name: Security Vulnerability Scan
on:
push:
branches: [ main, staging, develop ]
pull_request:
branches: [ main, staging, develop ]
schedule:
# Daily security scan at 2 AM UTC
- cron: '0 2 * * *'
workflow_dispatch:
jobs:
check-changes:
name: Check for Dependency Changes
runs-on: ubuntu-latest
outputs:
dependencies_changed: ${{ steps.filter.outputs.dependencies_changed }}
steps:
- name: Download CI helpers
shell: bash
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
run: |
set -euo pipefail
REF="${{ github.sha }}"
if [ -z "$REF" ]; then
REF="${{ github.ref_name }}"
fi
if [ -z "$REF" ]; then
REF="${{ github.head_ref }}"
fi
if [ -z "$REF" ]; then
REF="main"
fi
URL="https://git.michaelschiemer.de/${{ github.repository }}/raw/${REF}/scripts/ci/clone_repo.sh"
mkdir -p /tmp/ci-tools
if [ -n "$CI_TOKEN" ]; then
curl -sfL -u "$CI_TOKEN:x-oauth-basic" "$URL" -o /tmp/ci-tools/clone_repo.sh
else
curl -sfL "$URL" -o /tmp/ci-tools/clone_repo.sh
fi
chmod +x /tmp/ci-tools/clone_repo.sh
- name: Analyse changed files
id: filter
shell: bash
run: |
set -euo pipefail
REF_NAME="${{ github.ref_name }}"
if [ -z "$REF_NAME" ]; then
REF_NAME="${{ github.head_ref }}"
fi
if [ -z "$REF_NAME" ]; then
REF_NAME="main"
fi
REPO="${{ github.repository }}"
WORKDIR="/workspace/repo"
export CI_REPOSITORY="$REPO"
export CI_TOKEN="${{ secrets.CI_TOKEN }}"
export CI_REF_NAME="$REF_NAME"
export CI_DEFAULT_BRANCH="main"
export CI_TARGET_DIR="$WORKDIR"
export CI_FETCH_DEPTH="2"
/tmp/ci-tools/clone_repo.sh
cd "$WORKDIR"
# For scheduled or manual runs, always run the scan
if [ "${{ github.event_name }}" = "schedule" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "dependencies_changed=true" >> "$GITHUB_OUTPUT"
echo " Scheduled/manual run - will scan dependencies"
exit 0
fi
CHANGED_FILES=""
EVENT_BEFORE="${{ github.event.before }}"
if [ "${{ github.event_name }}" = "push" ] && [ -n "$EVENT_BEFORE" ]; then
if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then
CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)"
else
git fetch origin "$EVENT_BEFORE" --depth 1 || true
if git rev-parse "$EVENT_BEFORE" >/dev/null 2>&1; then
CHANGED_FILES="$(git diff --name-only "$EVENT_BEFORE" HEAD || true)"
fi
fi
fi
if [ -z "$CHANGED_FILES" ]; then
if git rev-parse HEAD^ >/dev/null 2>&1; then
CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)"
else
git fetch origin "$REF_NAME" --depth 50 || true
if git rev-parse HEAD^ >/dev/null 2>&1; then
CHANGED_FILES="$(git diff --name-only HEAD^ HEAD || true)"
fi
fi
fi
DEPENDENCIES_CHANGED=false
if [ -n "$CHANGED_FILES" ]; then
while IFS= read -r FILE; do
[ -z "$FILE" ] && continue
if echo "$FILE" | grep -Eq "^(composer\.json|composer\.lock)$"; then
DEPENDENCIES_CHANGED=true
break
fi
done <<< "$CHANGED_FILES"
fi
echo "dependencies_changed=$DEPENDENCIES_CHANGED" >> "$GITHUB_OUTPUT"
if [ "$DEPENDENCIES_CHANGED" = "true" ]; then
echo " Dependencies changed - security scan will run"
else
echo " No dependency changes detected - skipping security scan"
fi
security-audit:
needs: check-changes
if: needs.check-changes.outputs.dependencies_changed == 'true' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
name: Composer Security Audit
runs-on: php-ci # Uses pre-built PHP 8.5 CI image with Composer pre-installed
steps:
- name: Checkout code
run: |
# For pull_request events, use the head ref (source branch)
if [ "${{ github.event_name }}" = "pull_request" ]; then
REF_NAME="${{ github.head_ref || github.event.pull_request.head.ref }}"
elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
REF_NAME="${{ inputs.branch || github.ref_name }}"
else
REF_NAME="${{ github.ref_name }}"
fi
# Fallback to main if REF_NAME is still empty
if [ -z "$REF_NAME" ] || [ "$REF_NAME" = "" ]; then
REF_NAME="main"
fi
REPO="${{ github.repository }}"
echo "📋 Cloning branch: $REF_NAME"
echo "📦 Repository: $REPO"
# Use CI token if available, otherwise try public access
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
git clone --depth 1 --branch "$REF_NAME" \
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
else
# Try public HTTPS (works if repository is public)
git clone --depth 1 --branch "$REF_NAME" \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo || \
# Fallback: Try to use Gitea's internal runner access
git clone --depth 1 \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
fi
cd /workspace/repo
- name: Get Composer cache directory
id: composer-cache
shell: bash
run: |
echo "dir=$(composer global config cache-dir 2>/dev/null | cut -d' ' -f3 || echo "$HOME/.composer/cache")" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: |
${{ steps.composer-cache.outputs.dir }}
vendor/
key: ${{ runner.os }}-composer-security-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-security-
- name: Validate composer.json and composer.lock
run: |
cd /workspace/repo
# Validate composer.json (less strict - lock file might be updated during install)
composer validate --no-check-lock || echo "⚠️ composer.lock might need update, but continuing..."
# Try to update lock file if needed
composer update --lock --no-interaction || echo "⚠️ Could not update lock file, but continuing..."
- name: Install dependencies
run: |
cd /workspace/repo
# TEMPORARY WORKAROUND: Ignore PHP 8.5 platform requirement until dependencies officially support PHP 8.5
# TODO: Remove --ignore-platform-req=php when dependencies are updated (estimated: 1 month)
composer install --prefer-dist --no-progress --no-dev --ignore-platform-req=php
- name: Run Composer Security Audit
id: security-audit
run: |
cd /workspace/repo
composer audit --format=json > audit-result.json || true
cat audit-result.json
- name: Parse audit results
id: parse-audit
run: |
cd /workspace/repo
if [ -f audit-result.json ]; then
# jq is pre-installed in php-ci image
jq --version
ADVISORIES=$(jq -r '.advisories | length' audit-result.json 2>/dev/null || echo "0")
ABANDONED=$(jq -r '.abandoned | length' audit-result.json 2>/dev/null || echo "0")
echo "advisories_count=$ADVISORIES" >> $GITHUB_OUTPUT
echo "abandoned_count=$ABANDONED" >> $GITHUB_OUTPUT
echo "## Security Audit Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Vulnerabilities Found:** $ADVISORIES" >> $GITHUB_STEP_SUMMARY
echo "- **Abandoned Packages:** $ABANDONED" >> $GITHUB_STEP_SUMMARY
if [ "$ADVISORIES" -gt 0 ]; then
echo "has_vulnerabilities=true" >> $GITHUB_OUTPUT
echo "- **Status:** ❌ Security vulnerabilities detected - review required" >> $GITHUB_STEP_SUMMARY
# Display vulnerability details
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Vulnerability Details" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo '```json' >> $GITHUB_STEP_SUMMARY
jq '.advisories' audit-result.json >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
exit 1
else
echo "has_vulnerabilities=false" >> $GITHUB_OUTPUT
echo "- **Status:** ✅ No security vulnerabilities detected" >> $GITHUB_STEP_SUMMARY
fi
if [ "$ABANDONED" -gt 0 ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Abandoned Packages" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
jq -r '.abandoned | to_entries[] | "- **\(.key)**: \(.value // "No replacement suggested")"' audit-result.json >> $GITHUB_STEP_SUMMARY
fi
fi
- name: Save audit results
if: always()
run: |
cd /workspace/repo
if [ -f audit-result.json ]; then
mkdir -p /tmp/artifacts
cp audit-result.json /tmp/artifacts/security-audit-results-${{ github.run_number }}.json || true
echo "✅ Audit results saved"
fi
- name: Create Gitea issue on vulnerability (scheduled runs only)
if: failure() && github.event_name == 'schedule'
run: |
# Prepare issue body
ISSUE_TITLE="🚨 Security Vulnerabilities Detected in Dependencies"
ISSUE_BODY="## Security Audit Report\n\n"
ISSUE_BODY="${ISSUE_BODY}**Date:** $(date -u +"%Y-%m-%d %H:%M:%S UTC")\n"
ISSUE_BODY="${ISSUE_BODY}**Workflow Run:** ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n\n"
if [ -f audit-result.json ]; then
# Add vulnerability details
ISSUE_BODY="${ISSUE_BODY}### Vulnerabilities Found\n\n"
ISSUE_BODY="${ISSUE_BODY}\`\`\`json\n"
ISSUE_BODY="${ISSUE_BODY}$(cat audit-result.json)\n"
ISSUE_BODY="${ISSUE_BODY}\`\`\`\n\n"
fi
ISSUE_BODY="${ISSUE_BODY}### Action Required\n\n"
ISSUE_BODY="${ISSUE_BODY}1. Review the vulnerability details above\n"
ISSUE_BODY="${ISSUE_BODY}2. Update affected packages: \`composer update <package>\`\n"
ISSUE_BODY="${ISSUE_BODY}3. Run tests: \`make test\`\n"
ISSUE_BODY="${ISSUE_BODY}4. Verify with: \`make security-check\`\n"
# Create issue using Gitea API
# Note: Requires CI_TOKEN secret to be configured
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
curl -X POST \
-H "Authorization: token ${{ secrets.CI_TOKEN }}" \
-H "Content-Type: application/json" \
-d "{\"title\":\"${ISSUE_TITLE}\",\"body\":\"${ISSUE_BODY}\",\"labels\":[\"security\",\"dependencies\",\"automated\"]}" \
"${{ github.server_url }}/api/v1/repos/${{ github.repository }}/issues"
else
echo "⚠️ CI_TOKEN not configured - skipping issue creation"
echo "Please add CI_TOKEN as repository secret for automated issue creation"
fi
- name: Notify on failure
if: failure()
run: |
echo "::error::Security vulnerabilities detected! Check the audit results for details."
echo "Run 'make security-check' locally to review vulnerabilities."

View File

@@ -0,0 +1,72 @@
name: System Maintenance
on:
schedule:
# Täglich um 02:30 UTC (≈ 03:30 CET/04:30 CEST)
- cron: '30 2 * * *'
workflow_dispatch:
env:
DEPLOYMENT_HOST: 94.16.110.151
jobs:
run-maintenance:
name: Run Ansible System Maintenance
runs-on: php-ci
environment:
name: production
url: https://michaelschiemer.de
steps:
- name: Checkout deployment repository
run: |
REF_NAME="${{ github.ref_name }}"
if [ -z "$REF_NAME" ]; then
REF_NAME="main"
fi
REPO="${{ github.repository }}"
echo "📋 Cloning branch: $REF_NAME"
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
git clone --depth 1 --branch "$REF_NAME" \
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
else
git clone --depth 1 --branch "$REF_NAME" \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo || \
git clone --depth 1 \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
fi
cd /workspace/repo
- name: Prepare SSH access
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
- name: Verify Ansible availability
run: ansible --version
- name: Run system maintenance playbook
run: |
cd /workspace/repo/deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/system-maintenance.yml
- name: Cleanup SSH key
if: always()
run: rm -f ~/.ssh/production
- name: Report success
if: success()
run: echo "✅ System maintenance completed"
- name: Report failure
if: failure()
run: echo "❌ System maintenance failed review Ansible logs"

View File

@@ -0,0 +1,89 @@
name: Update Production Secrets
on:
workflow_dispatch:
inputs:
vault_password:
description: 'Ansible Vault Password'
required: true
type: password
env:
DEPLOYMENT_HOST: 94.16.110.151
jobs:
deploy-secrets:
name: Deploy Secrets to Production
runs-on: php-ci # Uses pre-built PHP 8.5 CI image with Ansible
environment:
name: production-secrets
url: https://michaelschiemer.de
steps:
- name: Checkout deployment configuration
run: |
REF_NAME="${{ github.ref_name }}"
REPO="${{ github.repository }}"
if [ -z "$REF_NAME" ]; then
REF_NAME="main"
fi
if [ -n "${{ secrets.CI_TOKEN }}" ]; then
git clone --depth 1 --branch "$REF_NAME" \
"https://${{ secrets.CI_TOKEN }}@git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
else
git clone --depth 1 --branch "$REF_NAME" \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo || \
git clone --depth 1 \
"https://git.michaelschiemer.de/${REPO}.git" \
/workspace/repo
fi
cd /workspace/repo
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/production
chmod 600 ~/.ssh/production
ssh-keyscan -H ${{ env.DEPLOYMENT_HOST }} >> ~/.ssh/known_hosts
# Ansible is pre-installed in php-ci image
- name: Verify Ansible installation
run: ansible --version
- name: Create vault password file
run: |
echo "${{ github.event.inputs.vault_password }}" > /tmp/.vault_pass
chmod 600 /tmp/.vault_pass
- name: Deploy secrets via Ansible
run: |
cd deployment/ansible
ansible-playbook -i inventory/production.yml \
playbooks/setup-production-secrets.yml \
--vault-password-file /tmp/.vault_pass
- name: Cleanup vault password
if: always()
run: |
rm -f /tmp/.vault_pass
- name: Verify secrets deployment
run: |
ssh -i ~/.ssh/production deploy@${{ env.DEPLOYMENT_HOST }} \
"docker secret ls && test -f /home/deploy/secrets/.env.production"
- name: Notify deployment success
if: success()
run: |
echo "✅ Secrets deployed successfully to production"
echo "Services will be restarted automatically"
- name: Notify deployment failure
if: failure()
run: |
echo "❌ Secrets deployment failed"
echo "Check Ansible logs for details"