build: add Docker and CI/CD pipeline for web deployment
- Add multi-stage Dockerfile (Flutter build → nginx:alpine) - Add nginx.conf with SPA routing, gzip, and caching - Add Gitea workflow for release-triggered builds - Document release procedure in AGENTS.md Deploys to port 8092, auto-updates via Watchtower. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
6989241ae7
commit
aa3fe916b7
@@ -0,0 +1,34 @@
|
|||||||
|
name: Build and Push
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: git.schweitz.internal
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
git.schweitz.internal/jpmschweitzer/tatlock-ui:latest
|
||||||
|
git.schweitz.internal/jpmschweitzer/tatlock-ui:${{ github.ref_name }}
|
||||||
|
|
||||||
|
- name: Trigger Watchtower update
|
||||||
|
if: success()
|
||||||
|
run: |
|
||||||
|
curl -sf -H "Authorization: Bearer ${{ secrets.WATCHTOWER_TOKEN }}" \
|
||||||
|
http://watchtower:8080/v1/update
|
||||||
@@ -42,7 +42,36 @@ This document contains instructions and documentation references for AI assistan
|
|||||||
* Tag after updating `pubspec.yaml` version and CHANGELOG
|
* Tag after updating `pubspec.yaml` version and CHANGELOG
|
||||||
* Push tags with `git push --tags`
|
* Push tags with `git push --tags`
|
||||||
|
|
||||||
|
### 🚀 Release Procedure
|
||||||
|
|
||||||
|
This project uses version-tag-based CI/CD. Releases trigger automated Docker builds and deployments.
|
||||||
|
|
||||||
|
**Release Steps:**
|
||||||
|
|
||||||
|
1. Update version in `pubspec.yaml` (bump major.minor.patch, not build number)
|
||||||
|
2. Update `CHANGELOG.md` with changes under `## [x.x.x] - YYYY-MM-DD`
|
||||||
|
3. Commit changes: `git commit -m "chore: release vX.X.X"`
|
||||||
|
4. Create git tag: `git tag vX.X.X`
|
||||||
|
5. Push with tags: `git push origin master --tags`
|
||||||
|
6. Create release in Gitea UI (git.schweitz.net → Releases → New Release)
|
||||||
|
* Select the tag
|
||||||
|
* Add release notes (can copy from CHANGELOG)
|
||||||
|
* **Publish** the release (this triggers CI/CD)
|
||||||
|
|
||||||
|
**What happens on release:**
|
||||||
|
|
||||||
|
* Gitea CI builds Flutter web app in Docker
|
||||||
|
* Image pushed to `git.schweitz.internal/jpmschweitzer/tatlock-ui:latest` and `:vX.X.X`
|
||||||
|
* Watchtower detects new image and auto-updates running container
|
||||||
|
* App available at `http://tower:8092` (and eventually `home.schweitz.net`)
|
||||||
|
|
||||||
|
**Rollback:**
|
||||||
|
|
||||||
|
* In Portainer, update image tag to previous version (e.g., `:v0.2.0`)
|
||||||
|
* Or: `docker pull git.schweitz.internal/jpmschweitzer/tatlock-ui:v0.2.0`
|
||||||
|
|
||||||
### 🧪 Testing Requirements
|
### 🧪 Testing Requirements
|
||||||
|
|
||||||
* **Always add tests for new code before committing.** No exceptions.
|
* **Always add tests for new code before committing.** No exceptions.
|
||||||
* Tests should cover the happy path and key edge cases.
|
* Tests should cover the happy path and key edge cases.
|
||||||
* Run `flutter test` before committing to ensure all tests pass.
|
* Run `flutter test` before committing to ensure all tests pass.
|
||||||
@@ -50,6 +79,7 @@ This document contains instructions and documentation references for AI assistan
|
|||||||
* Code coverage should not decrease with new commits.
|
* Code coverage should not decrease with new commits.
|
||||||
|
|
||||||
### 📝 Changelog Maintenance
|
### 📝 Changelog Maintenance
|
||||||
|
|
||||||
* **Update `CHANGELOG.md`** with every user-facing change.
|
* **Update `CHANGELOG.md`** with every user-facing change.
|
||||||
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
|
* Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`.
|
||||||
|
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# Stage 1: Build Flutter web application
|
||||||
|
FROM ghcr.io/cirruslabs/flutter:stable AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependency files first for better caching
|
||||||
|
COPY pubspec.yaml pubspec.lock ./
|
||||||
|
|
||||||
|
# Get dependencies
|
||||||
|
RUN flutter pub get
|
||||||
|
|
||||||
|
# Copy the rest of the application
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Generate code with build_runner
|
||||||
|
RUN dart run build_runner build --delete-conflicting-outputs
|
||||||
|
|
||||||
|
# Build for web release
|
||||||
|
RUN flutter build web --release
|
||||||
|
|
||||||
|
# Stage 2: Serve with nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Install curl for healthcheck
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
# Copy custom nginx configuration
|
||||||
|
COPY nginx.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
# Copy built web app to nginx html directory
|
||||||
|
COPY --from=builder /app/build/web /usr/share/nginx/html
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:80/ || exit 1
|
||||||
|
|
||||||
|
# Run nginx in foreground
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
worker_processes auto;
|
||||||
|
error_log /var/log/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
access_log /var/log/nginx/access.log main;
|
||||||
|
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
types_hash_max_size 2048;
|
||||||
|
|
||||||
|
# Gzip compression
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
gzip_types
|
||||||
|
text/plain
|
||||||
|
text/css
|
||||||
|
text/javascript
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/x-javascript
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
image/svg+xml
|
||||||
|
font/woff
|
||||||
|
font/woff2;
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Security headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
|
||||||
|
# SPA routing: all routes fall back to index.html
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# No cache for index.html (ensures updates are picked up)
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Long cache for Flutter assets (content-hash naming ensures freshness)
|
||||||
|
location ~* \.(js|css|woff|woff2|ttf|eot|ico|png|jpg|jpeg|gif|svg|webp)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
access_log off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cache manifest and version files briefly
|
||||||
|
location ~* \.(json|webmanifest)$ {
|
||||||
|
expires 1h;
|
||||||
|
add_header Cache-Control "public";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Health check endpoint
|
||||||
|
location /health {
|
||||||
|
access_log off;
|
||||||
|
return 200 "OK";
|
||||||
|
add_header Content-Type text/plain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user