diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..0b21c27 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -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 diff --git a/AGENTS.md b/AGENTS.md index 09125e1..e734f39 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,36 @@ This document contains instructions and documentation references for AI assistan * Tag after updating `pubspec.yaml` version and CHANGELOG * 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 + * **Always add tests for new code before committing.** No exceptions. * Tests should cover the happy path and key edge cases. * 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. ### ๐Ÿ“ Changelog Maintenance + * **Update `CHANGELOG.md`** with every user-facing change. * Format: `## [Unreleased] - YYYY-MM-DD` followed by `### Added`, `### Changed`, or `### Fixed`. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..feafb58 --- /dev/null +++ b/Dockerfile @@ -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;"] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..5317ff8 --- /dev/null +++ b/nginx.conf @@ -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; + } + } +}