From 562c3770fb6f1c712f29d727edaca0ebef00582d Mon Sep 17 00:00:00 2001 From: Latte Date: Sun, 8 Mar 2026 15:05:31 +0100 Subject: [PATCH] Consolidate CI and deployment workflows Add a deploy workflow that builds and pushes the Docker image from the server working directory and runs docker compose on main. Trim CI to a focused Node build in an alpine container (apk deps, npm ci, npm run build) and remove the old docker workflow. Simplify Dockerfile to a builder+runtime flow (npm ci, npm prune, copy node_modules) and make docker-compose use the registry image and proxy network. --- .gitea/workflows/ci.yml | 271 ++------------------------------- .gitea/workflows/deploy.yml | 33 +++++ .gitea/workflows/docker.yml | 288 ------------------------------------ Dockerfile | 23 +-- docker-compose.yml | 12 +- 5 files changed, 62 insertions(+), 565 deletions(-) create mode 100644 .gitea/workflows/deploy.yml delete mode 100644 .gitea/workflows/docker.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e839adf..ad78431 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,270 +1,27 @@ -# ============================================================================= -# CI Workflow — Continuous Integration -# ============================================================================= -# Triggers on push and pull_request. -# -# Detection logic: -# 1. Python: if requirements.txt exists → install deps, lint, test. -# 2. Node/JS: if package.json exists → install deps, lint, test, build. -# 3. Neither detected → print a message and exit 0 (never fail). -# -# Controlled by .ci/config.env: -# ENABLE_CI — master switch (default: true) -# CI_STRICT — if true, lint/test failures fail the workflow -# if false, failures are warnings only -# -# See docs/CI.md for full details. -# ============================================================================= - name: CI on: push: + branches: + - dev pull_request: + branches: + - main jobs: - ci: - runs-on: ubuntu-latest + build: + runs-on: [ovh-vps-1, hiddenden] + container: + image: node:20-alpine steps: - # ----------------------------------------------------------------------- - # Step 1: Checkout the code - # ----------------------------------------------------------------------- - name: Checkout uses: actions/checkout@v4 - # ----------------------------------------------------------------------- - # Step 2: Load configuration from .ci/config.env - # ----------------------------------------------------------------------- - - name: Load config - id: config - run: | - # Source config.env to get ENABLE_CI, CI_STRICT, etc. - if [ -f .ci/config.env ]; then - # Export all non-comment, non-empty lines - set -a - source .ci/config.env - set +a - echo "Config loaded from .ci/config.env" - else - echo "WARNING: .ci/config.env not found, using defaults" - ENABLE_CI=true - CI_STRICT=true - fi + - name: Install build deps + run: apk add --no-cache python3 make g++ - # Pass values to subsequent steps via environment file - echo "ENABLE_CI=${ENABLE_CI:-true}" >> "$GITHUB_ENV" - echo "CI_STRICT=${CI_STRICT:-true}" >> "$GITHUB_ENV" + - name: Install dependencies + run: npm ci - # ----------------------------------------------------------------------- - # Step 3: Check master switch - # ----------------------------------------------------------------------- - - name: Check if CI is enabled - run: | - if [ "$ENABLE_CI" != "true" ]; then - echo "CI is disabled (ENABLE_CI=$ENABLE_CI). Exiting." - exit 0 - fi - - # ----------------------------------------------------------------------- - # Step 4: Detect project type - # ----------------------------------------------------------------------- - - name: Detect project type - id: detect - run: | - HAS_PYTHON=false - HAS_NODE=false - - if [ -f requirements.txt ] || [ -f setup.py ] || [ -f pyproject.toml ]; then - HAS_PYTHON=true - echo "Detected: Python project" - fi - - if [ -f package.json ]; then - HAS_NODE=true - echo "Detected: Node.js project" - fi - - if [ "$HAS_PYTHON" = "false" ] && [ "$HAS_NODE" = "false" ]; then - echo "No Python or Node.js project detected. CI will skip gracefully." - fi - - echo "HAS_PYTHON=${HAS_PYTHON}" >> "$GITHUB_ENV" - echo "HAS_NODE=${HAS_NODE}" >> "$GITHUB_ENV" - - # ----------------------------------------------------------------------- - # Step 5: Python — Setup & Install - # ----------------------------------------------------------------------- - - name: Set up Python - if: env.HAS_PYTHON == 'true' - uses: actions/setup-python@v5 - with: - python-version: "3.x" - - - name: Install Python dependencies - if: env.HAS_PYTHON == 'true' - run: | - python -m pip install --upgrade pip - if [ -f requirements.txt ]; then - pip install -r requirements.txt - fi - # Install optional dev tools (won't fail if not needed) - pip install ruff black flake8 pytest 2>/dev/null || true - - # ----------------------------------------------------------------------- - # Step 6: Python — Lint - # Runs ruff, then black --check, then flake8. Each is optional: - # only runs if the tool is installed AND relevant config exists. - # ----------------------------------------------------------------------- - - name: Python lint - if: env.HAS_PYTHON == 'true' - run: | - EXIT_CODE=0 - - # --- ruff --- - if command -v ruff >/dev/null 2>&1; then - echo ">>> ruff check ." - ruff check . || EXIT_CODE=$? - else - echo "SKIP: ruff not installed" - fi - - # --- black --- - if command -v black >/dev/null 2>&1; then - echo ">>> black --check ." - black --check . || EXIT_CODE=$? - else - echo "SKIP: black not installed" - fi - - # --- flake8 --- - if command -v flake8 >/dev/null 2>&1; then - echo ">>> flake8 ." - flake8 . || EXIT_CODE=$? - else - echo "SKIP: flake8 not installed" - fi - - if [ "$EXIT_CODE" -ne 0 ]; then - if [ "$CI_STRICT" = "true" ]; then - echo "ERROR: Lint failed (CI_STRICT=true)" - exit 1 - else - echo "WARNING: Lint issues found (CI_STRICT=false, continuing)" - fi - fi - - # ----------------------------------------------------------------------- - # Step 7: Python — Test - # Runs pytest if a tests/ directory or pytest config is detected. - # ----------------------------------------------------------------------- - - name: Python test - if: env.HAS_PYTHON == 'true' - run: | - # Check if tests exist - HAS_TESTS=false - if [ -d tests ] || [ -d test ]; then - HAS_TESTS=true - fi - # Check for pytest config in pyproject.toml or pytest.ini - if [ -f pytest.ini ] || [ -f setup.cfg ]; then - HAS_TESTS=true - fi - if [ -f pyproject.toml ] && grep -q '\[tool\.pytest' pyproject.toml 2>/dev/null; then - HAS_TESTS=true - fi - - if [ "$HAS_TESTS" = "true" ]; then - echo ">>> pytest" - if ! pytest; then - if [ "$CI_STRICT" = "true" ]; then - echo "ERROR: Tests failed (CI_STRICT=true)" - exit 1 - else - echo "WARNING: Tests failed (CI_STRICT=false, continuing)" - fi - fi - else - echo "SKIP: No tests detected (no tests/ dir or pytest config)" - fi - - # ----------------------------------------------------------------------- - # Step 8: Node.js — Setup & Install - # ----------------------------------------------------------------------- - - name: Set up Node.js - if: env.HAS_NODE == 'true' - uses: actions/setup-node@v4 - with: - # Keep CI on Node 20 to match runtime/Docker and better-sqlite3 compatibility. - node-version: "20.x" - - - name: Install Node dependencies - if: env.HAS_NODE == 'true' - run: | - # Lockfile is currently not authoritative; use install to refresh dependency tree. - npm install - - # ----------------------------------------------------------------------- - # Step 9: Node.js — Lint (only if "lint" script exists in package.json) - # ----------------------------------------------------------------------- - - name: Node lint - if: env.HAS_NODE == 'true' - run: | - if grep -q '"lint"' package.json 2>/dev/null; then - echo ">>> npm run lint" - if ! npm run lint; then - if [ "$CI_STRICT" = "true" ]; then - echo "ERROR: Lint failed (CI_STRICT=true)" - exit 1 - else - echo "WARNING: Lint issues found (CI_STRICT=false, continuing)" - fi - fi - else - echo "SKIP: no 'lint' script in package.json" - fi - - # ----------------------------------------------------------------------- - # Step 10: Node.js — Test (only if "test" script exists) - # ----------------------------------------------------------------------- - - name: Node test - if: env.HAS_NODE == 'true' - run: | - if grep -q '"test"' package.json 2>/dev/null; then - echo ">>> npm test" - if ! npm test; then - if [ "$CI_STRICT" = "true" ]; then - echo "ERROR: Tests failed (CI_STRICT=true)" - exit 1 - else - echo "WARNING: Tests failed (CI_STRICT=false, continuing)" - fi - fi - else - echo "SKIP: no 'test' script in package.json" - fi - - # ----------------------------------------------------------------------- - # Step 11: Node.js — Build (only if "build" script exists) - # ----------------------------------------------------------------------- - - name: Node build - if: env.HAS_NODE == 'true' - run: | - if grep -q '"build"' package.json 2>/dev/null; then - echo ">>> npm run build" - npm run build - else - echo "SKIP: no 'build' script in package.json" - fi - - # ----------------------------------------------------------------------- - # Step 12: Summary - # ----------------------------------------------------------------------- - - name: CI Summary - if: always() - run: | - echo "==============================" - echo " CI Complete" - echo " Python detected: $HAS_PYTHON" - echo " Node detected: $HAS_NODE" - echo " Strict mode: $CI_STRICT" - echo "==============================" + - name: Build + run: npm run build diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..69234a6 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,33 @@ +name: Deploy + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: [ovh-vps-1, deploy, hiddenden] + steps: + - name: Pull latest code + working-directory: /home/ubuntu/appdata/Cozy-Den + run: | + git fetch origin + git reset --hard origin/main + + - name: Login to Gitea registry + run: echo "${{ secrets.GITEA_TOKEN }}" | docker login git.hiddenden.cafe -u ${{ secrets.GITEA_USERNAME }} --password-stdin + + - name: Build & push image + working-directory: /home/ubuntu/appdata/Cozy-Den + run: | + docker build -t git.hiddenden.cafe/hiddenden/cozy-den:latest . + docker push git.hiddenden.cafe/hiddenden/cozy-den:latest + + - name: Deploy + working-directory: /home/ubuntu/appdata/Cozy-Den + env: + SECRET_KEY: ${{ secrets.SECRET_KEY }} + ADMIN_SECRET_TOKEN: ${{ secrets.ADMIN_SECRET_TOKEN }} + COOKIE_SECURE: "true" + run: docker compose up -d diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml deleted file mode 100644 index d658ded..0000000 --- a/.gitea/workflows/docker.yml +++ /dev/null @@ -1,288 +0,0 @@ -# ============================================================================= -# Docker Workflow — Build & Push to Gitea Container Registry -# ============================================================================= -# -# PURPOSE: -# Build Docker images generically; optionally push to the Gitea Container -# Registry at git.hiddenden.cafe. -# -# TRIGGERS: -# - pull_request → build only (never push) -# - push to main → build; push only if ENABLE_DOCKER=true AND DOCKER_PUSH=true -# - tag v* → build; push only if DOCKER_PUSH=true AND DOCKER_PUSH_ON_TAG=true -# -# DETECTION: -# - Dockerfile exists → docker build -# - docker-compose.yml exists → docker compose build -# - Neither → exit 0 gracefully -# -# NAMING (Gitea convention): -# Image ref: ${REGISTRY_HOST}/${IMAGE_OWNER}/${IMAGE_NAME}:${TAG} -# Example: git.hiddenden.cafe/myorg/myrepo:1.2.3 -# -# AUTHENTICATION: -# Uses PAT-based secrets (recommended for Gitea Actions): -# - REGISTRY_USERNAME — your Gitea username or bot account -# - REGISTRY_TOKEN — a Personal Access Token with package:write scope -# Set these in: Repository Settings → Secrets (or Organization Secrets). -# NEVER echo secrets in logs. -# -# CONFIG: -# All settings loaded from .ci/config.env. See docs/DOCKER.md. -# -# ============================================================================= - -name: Docker - -on: - push: - branches: - - main - tags: - - "v*" - pull_request: - -jobs: - docker: - runs-on: ubuntu-latest - steps: - # ----------------------------------------------------------------------- - # Step 1: Checkout - # ----------------------------------------------------------------------- - - name: Checkout - uses: actions/checkout@v4 - - # ----------------------------------------------------------------------- - # Step 2: Load configuration - # ----------------------------------------------------------------------- - - name: Load config - run: | - if [ -f .ci/config.env ]; then - set -a - source .ci/config.env - set +a - echo "Config loaded from .ci/config.env" - else - echo "WARNING: .ci/config.env not found, using defaults" - fi - - # Export with defaults - echo "ENABLE_DOCKER=${ENABLE_DOCKER:-true}" >> "$GITHUB_ENV" - echo "DOCKER_PUSH=${DOCKER_PUSH:-false}" >> "$GITHUB_ENV" - echo "DOCKER_PUSH_ON_BRANCH=${DOCKER_PUSH_ON_BRANCH:-true}" >> "$GITHUB_ENV" - echo "DOCKER_PUSH_ON_TAG=${DOCKER_PUSH_ON_TAG:-true}" >> "$GITHUB_ENV" - echo "REGISTRY_HOST=${REGISTRY_HOST:-git.hiddenden.cafe}" >> "$GITHUB_ENV" - echo "IMAGE_OWNER_CFG=${IMAGE_OWNER:-auto}" >> "$GITHUB_ENV" - echo "IMAGE_NAME_CFG=${IMAGE_NAME:-auto}" >> "$GITHUB_ENV" - echo "DOCKER_TAG_STRATEGY=${DOCKER_TAG_STRATEGY:-semver+latest}" >> "$GITHUB_ENV" - echo "DEFAULT_BRANCH=${DEFAULT_BRANCH:-main}" >> "$GITHUB_ENV" - - # ----------------------------------------------------------------------- - # Step 3: Check if Docker is enabled - # ----------------------------------------------------------------------- - - name: Check if Docker is enabled - run: | - if [ "$ENABLE_DOCKER" != "true" ]; then - echo "Docker is disabled (ENABLE_DOCKER=$ENABLE_DOCKER). Exiting." - # We still exit 0 — graceful skip. - echo "SKIP_DOCKER=true" >> "$GITHUB_ENV" - fi - - # ----------------------------------------------------------------------- - # Step 4: Detect Dockerfile or docker-compose.yml - # ----------------------------------------------------------------------- - - name: Detect Docker files - if: env.SKIP_DOCKER != 'true' - run: | - if [ -f Dockerfile ]; then - echo "DOCKER_MODE=dockerfile" >> "$GITHUB_ENV" - echo "Detected: Dockerfile" - elif [ -f docker-compose.yml ] || [ -f docker-compose.yaml ]; then - echo "DOCKER_MODE=compose" >> "$GITHUB_ENV" - echo "Detected: docker-compose.yml" - else - echo "No Dockerfile or docker-compose.yml found. Skipping." - echo "SKIP_DOCKER=true" >> "$GITHUB_ENV" - fi - - # ----------------------------------------------------------------------- - # Step 5: Derive image owner and name dynamically - # - # Logic: - # FULL_REPO is derived from (in priority order): - # 1. $GITEA_REPOSITORY (Gitea native env var) - # 2. github.repository (Gitea Actions compatibility fallback) - # Format: "owner/repo" - # - # If IMAGE_OWNER=auto → use the owner part - # If IMAGE_NAME=auto → use the repo part - # Otherwise, use the explicit config values. - # ----------------------------------------------------------------------- - - name: Derive image naming - if: env.SKIP_DOCKER != 'true' - run: | - # Determine FULL_REPO (owner/repo) - # Gitea Actions sets GITEA_REPOSITORY natively in some versions. - # It also maps github.repository for compatibility. - FULL_REPO="${GITEA_REPOSITORY:-${{ github.repository }}}" - - if [ -z "$FULL_REPO" ]; then - echo "ERROR: Could not determine repository (GITEA_REPOSITORY and github.repository are both empty)" - exit 1 - fi - - # Split into OWNER and REPO - OWNER="$(echo "$FULL_REPO" | cut -d'/' -f1)" - REPO="$(echo "$FULL_REPO" | cut -d'/' -f2)" - - echo "Derived FULL_REPO=$FULL_REPO OWNER=$OWNER REPO=$REPO" - - # Apply config overrides - if [ "$IMAGE_OWNER_CFG" = "auto" ]; then - FINAL_OWNER="$OWNER" - else - FINAL_OWNER="$IMAGE_OWNER_CFG" - fi - - if [ "$IMAGE_NAME_CFG" = "auto" ]; then - FINAL_NAME="$REPO" - else - FINAL_NAME="$IMAGE_NAME_CFG" - fi - - # Docker requires all image references to be lowercase - FINAL_OWNER="$(echo "$FINAL_OWNER" | tr '[:upper:]' '[:lower:]')" - FINAL_NAME="$(echo "$FINAL_NAME" | tr '[:upper:]' '[:lower:]')" - - # Construct the full image reference (without tag) - IMAGE_REF="${REGISTRY_HOST}/${FINAL_OWNER}/${FINAL_NAME}" - echo "Image reference: ${IMAGE_REF}:" - - echo "IMAGE_REF=${IMAGE_REF}" >> "$GITHUB_ENV" - - # ----------------------------------------------------------------------- - # Step 6: Determine tags based on trigger and strategy - # - # Tag rules: - # - PR: build only, tag = "pr-" (local only) - # - Push to branch: tag = branch name (e.g., "main") - # - Tag v1.2.3: tag = "1.2.3"; also "latest" if strategy includes it - # ----------------------------------------------------------------------- - - name: Determine tags - if: env.SKIP_DOCKER != 'true' - run: | - TAGS="" - SHOULD_PUSH=false - REF="${GITHUB_REF:-}" - - echo "Event: ${{ github.event_name }}" - echo "Ref: ${REF}" - - # --- Pull Request: build only --- - if [ "${{ github.event_name }}" = "pull_request" ]; then - TAGS="${IMAGE_REF}:pr-${{ github.event.number }}" - SHOULD_PUSH=false - echo "PR build — will NOT push" - - # --- Tag push (v*) --- - elif echo "${REF}" | grep -qE '^refs/tags/v'; then - # Extract version: refs/tags/v1.2.3 → 1.2.3 - VERSION="$(echo "${REF}" | sed 's|refs/tags/v||')" - TAGS="${IMAGE_REF}:${VERSION}" - - # Optionally add :latest - if echo "$DOCKER_TAG_STRATEGY" | grep -qi 'latest'; then - TAGS="${TAGS},${IMAGE_REF}:latest" - fi - - if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_TAG" = "true" ]; then - SHOULD_PUSH=true - fi - echo "Tag push — version=${VERSION}, push=${SHOULD_PUSH}" - - # --- Branch push --- - elif echo "${REF}" | grep -q '^refs/heads/'; then - BRANCH="$(echo "${REF}" | sed 's|refs/heads/||')" - TAGS="${IMAGE_REF}:${BRANCH}" - - if [ "$DOCKER_PUSH" = "true" ] && [ "$DOCKER_PUSH_ON_BRANCH" = "true" ]; then - SHOULD_PUSH=true - fi - echo "Branch push — branch=${BRANCH}, push=${SHOULD_PUSH}" - - else - echo "Unknown ref type: ${REF}. Building with tag 'dev'." - TAGS="${IMAGE_REF}:dev" - SHOULD_PUSH=false - fi - - echo "DOCKER_TAGS=${TAGS}" >> "$GITHUB_ENV" - echo "SHOULD_PUSH=${SHOULD_PUSH}" >> "$GITHUB_ENV" - echo "Final tags: ${TAGS}" - echo "Will push: ${SHOULD_PUSH}" - - # ----------------------------------------------------------------------- - # Step 7: Build the Docker image - # ----------------------------------------------------------------------- - - name: Build Docker image - if: env.SKIP_DOCKER != 'true' - run: | - if [ "$DOCKER_MODE" = "dockerfile" ]; then - # Build with the first tag; additional tags are added after - PRIMARY_TAG="$(echo "$DOCKER_TAGS" | cut -d',' -f1)" - echo ">>> docker build -t ${PRIMARY_TAG} ." - docker build -t "${PRIMARY_TAG}" . - - # Tag additional images if present - IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS" - for tag in "${TAG_ARRAY[@]:1}"; do - echo ">>> docker tag ${PRIMARY_TAG} ${tag}" - docker tag "${PRIMARY_TAG}" "${tag}" - done - - elif [ "$DOCKER_MODE" = "compose" ]; then - echo ">>> docker compose build" - docker compose build - fi - - # ----------------------------------------------------------------------- - # Step 8: Login to registry (only when pushing) - # - # Uses PAT-based auth. Requires secrets: - # REGISTRY_USERNAME — Gitea username or bot - # REGISTRY_TOKEN — PAT with package:write scope - # ----------------------------------------------------------------------- - - name: Login to container registry - if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true' - run: | - echo "Logging in to ${REGISTRY_HOST}..." - # Use --password-stdin to avoid leaking the token in process list - echo "${{ secrets.REGISTRY_TOKEN }}" | \ - docker login "${REGISTRY_HOST}" \ - -u "${{ secrets.REGISTRY_USERNAME }}" \ - --password-stdin - - # ----------------------------------------------------------------------- - # Step 9: Push Docker image(s) - # ----------------------------------------------------------------------- - - name: Push Docker image - if: env.SKIP_DOCKER != 'true' && env.SHOULD_PUSH == 'true' - run: | - IFS=',' read -ra TAG_ARRAY <<< "$DOCKER_TAGS" - for tag in "${TAG_ARRAY[@]}"; do - echo ">>> docker push ${tag}" - docker push "${tag}" - done - - # ----------------------------------------------------------------------- - # Step 10: Summary - # ----------------------------------------------------------------------- - - name: Docker Summary - if: always() - run: | - echo "==============================" - echo " Docker Workflow Complete" - echo " Mode: ${DOCKER_MODE:-skipped}" - echo " Tags: ${DOCKER_TAGS:-none}" - echo " Pushed: ${SHOULD_PUSH:-false}" - echo "==============================" diff --git a/Dockerfile b/Dockerfile index 31407f7..e903e27 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,26 @@ -# Stage 1: Build the Astro app +# Stage 1: Build FROM node:20-alpine AS builder WORKDIR /app -# Install build dependencies for native modules (e.g. better-sqlite3) +# Native module build deps (better-sqlite3) RUN apk add --no-cache python3 make g++ COPY package*.json ./ -RUN npm install +RUN npm ci COPY . . -RUN npm run build +RUN npm run build && npm prune --omit=dev -# Stage 2: Install production dependencies only -FROM node:20-alpine AS deps - -WORKDIR /app - -RUN apk add --no-cache python3 make g++ - -COPY package*.json ./ -RUN npm install --omit=dev - -# Stage 3: Runtime image +# Stage 2: Runtime FROM node:20-alpine AS runtime WORKDIR /app -# Data directory for SQLite database RUN mkdir -p /data COPY --from=builder /app/dist ./dist -COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ ENV NODE_ENV=production diff --git a/docker-compose.yml b/docker-compose.yml index d20523d..53bda97 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,12 @@ services: cozy-den: - build: . + image: git.hiddenden.cafe/hiddenden/cozy-den:latest container_name: cozy-den - ports: - - "3000:3000" + # ports: + # - "3000:3000" restart: unless-stopped + networks: + - proxy environment: - NODE_ENV=production - HOST=0.0.0.0 @@ -18,3 +20,7 @@ services: volumes: guestbook_data: + +networks: + proxy: + external: true -- 2.52.0