feat: add Woodpecker CI + Gitea deployment templates
- .woodpecker.yml: full pipeline template (install → lint-fix → lint → test → deploy) - scripts/setup-project.sh: one-command VPS setup (user, dir, deploy, sudoers, systemd, nginx) - scripts/deploy.sh: deploy script template (rsync + npm ci + systemctl + health check) - scripts/ci-lint-fix.sh: ESLint auto-fix with [CI SKIP] commit-back - docs/ci-cd.md: complete CI/CD documentation and troubleshooting - .env.example: added WOODPECKER_TOKEN - DOCS.md: added CI/CD section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,10 @@ NODE_ENV=development
|
|||||||
# --- Email ---
|
# --- Email ---
|
||||||
# RESEND_API_KEY=
|
# RESEND_API_KEY=
|
||||||
|
|
||||||
|
# --- CI/CD (Woodpecker) ---
|
||||||
|
# WOODPECKER_TOKEN=
|
||||||
|
# WOODPECKER_URL=https://ci.spektr.design
|
||||||
|
|
||||||
# --- File Storage ---
|
# --- File Storage ---
|
||||||
# S3_BUCKET=
|
# S3_BUCKET=
|
||||||
# S3_REGION=
|
# S3_REGION=
|
||||||
|
|||||||
@@ -1,6 +1,69 @@
|
|||||||
|
# Woodpecker CI pipeline template.
|
||||||
|
#
|
||||||
|
# Runs on the local backend — image: bash specifies the shell.
|
||||||
|
# Triggers on push to main branch.
|
||||||
|
#
|
||||||
|
# Flow: install → lint-fix (optional) → lint + test → deploy
|
||||||
|
#
|
||||||
|
# Secrets (add in Woodpecker UI → repo or global settings):
|
||||||
|
# gitea_token — Gitea access token (for lint-fix auto-commit back)
|
||||||
|
#
|
||||||
|
# Deploy setup (one-time, as root on VPS):
|
||||||
|
# 1. Create deploy script:
|
||||||
|
# cp scripts/deploy.sh /usr/local/bin/deploy-<project-name>
|
||||||
|
# chmod 755 /usr/local/bin/deploy-<project-name>
|
||||||
|
# chown root:root /usr/local/bin/deploy-<project-name>
|
||||||
|
#
|
||||||
|
# 2. Find Woodpecker agent user:
|
||||||
|
# ps -o user= -p $(pgrep woodpecker-agent)
|
||||||
|
#
|
||||||
|
# 3. Set up sudoers (replace AGENT_USER and script name):
|
||||||
|
# echo 'AGENT_USER ALL=(root) NOPASSWD: /usr/local/bin/deploy-<project-name> *' \
|
||||||
|
# > /etc/sudoers.d/woodpecker-deploy
|
||||||
|
# chmod 0440 /etc/sudoers.d/woodpecker-deploy
|
||||||
|
# visudo -cf /etc/sudoers.d/woodpecker-deploy
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: bash
|
||||||
|
commands:
|
||||||
|
- npm ci
|
||||||
|
|
||||||
|
# Optional: auto-fix lint issues and commit back.
|
||||||
|
# Requires gitea_token secret. Remove this step if not needed.
|
||||||
|
# The [CI SKIP] in commit message prevents infinite loops.
|
||||||
|
#
|
||||||
|
# - name: lint-fix
|
||||||
|
# image: bash
|
||||||
|
# environment:
|
||||||
|
# GITEA_TOKEN:
|
||||||
|
# from_secret: gitea_token
|
||||||
|
# commands:
|
||||||
|
# - bash scripts/ci-lint-fix.sh
|
||||||
|
# depends_on: [install]
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
image: bash
|
||||||
|
commands:
|
||||||
|
- npx eslint .
|
||||||
|
depends_on: [install]
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
image: bash
|
image: bash
|
||||||
commands:
|
commands:
|
||||||
- echo "Hello from Woodpecker CI!"
|
- npm test
|
||||||
- echo "Build successful"
|
depends_on: [install]
|
||||||
|
|
||||||
|
# Uncomment when deploy script is set up on VPS:
|
||||||
|
#
|
||||||
|
# - name: deploy
|
||||||
|
# image: bash
|
||||||
|
# environment:
|
||||||
|
# CI: "true"
|
||||||
|
# commands:
|
||||||
|
# - sudo /usr/local/bin/deploy-<project-name> "${CI_WORKSPACE:-.}"
|
||||||
|
# depends_on: [lint, test]
|
||||||
|
|||||||
10
DOCS.md
10
DOCS.md
@@ -51,7 +51,11 @@ Technical index for developers and AI agents. Use this as the entry point to all
|
|||||||
- `docs/backend/security.md` — authN/Z, secret handling, webhook verification, audit/event logging.
|
- `docs/backend/security.md` — authN/Z, secret handling, webhook verification, audit/event logging.
|
||||||
- `docs/backend/payment-flow.md` — payment integration (provider-agnostic template; single source of truth for payment flows and webhooks).
|
- `docs/backend/payment-flow.md` — payment integration (provider-agnostic template; single source of truth for payment flows and webhooks).
|
||||||
|
|
||||||
### 4) Examples (`/docs/examples`)
|
### 4) CI/CD & Deployment
|
||||||
|
|
||||||
|
- `docs/ci-cd.md` — Woodpecker CI + Gitea setup, pipeline flow, deploy script, sudoers config, troubleshooting.
|
||||||
|
|
||||||
|
### 5) Examples (`/docs/examples`)
|
||||||
|
|
||||||
- `docs/examples/RECOMMENDATIONS-example.md` — filled-in example of `RECOMMENDATIONS.md` for a compliance classifier (Archetype C).
|
- `docs/examples/RECOMMENDATIONS-example.md` — filled-in example of `RECOMMENDATIONS.md` for a compliance classifier (Archetype C).
|
||||||
|
|
||||||
@@ -68,6 +72,10 @@ Technical index for developers and AI agents. Use this as the entry point to all
|
|||||||
- `package.json` — project metadata and Node.js engine requirement.
|
- `package.json` — project metadata and Node.js engine requirement.
|
||||||
- `.env.example` — environment variables template.
|
- `.env.example` — environment variables template.
|
||||||
- `.editorconfig` — editor formatting standards (indentation, line endings).
|
- `.editorconfig` — editor formatting standards (indentation, line endings).
|
||||||
|
- `.woodpecker.yml` — Woodpecker CI pipeline config (local backend).
|
||||||
|
- `scripts/setup-project.sh` — Automated VPS setup for new projects (user, dir, deploy, sudoers, systemd, nginx).
|
||||||
|
- `scripts/deploy.sh` — VPS deploy script template (rsync + npm ci + systemctl).
|
||||||
|
- `scripts/ci-lint-fix.sh` — ESLint auto-fix with commit-back for CI.
|
||||||
|
|
||||||
## Agent Profiles (`/agents`)
|
## Agent Profiles (`/agents`)
|
||||||
|
|
||||||
|
|||||||
114
docs/ci-cd.md
Normal file
114
docs/ci-cd.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# CI/CD — Woodpecker + Gitea
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Pipeline runs on Woodpecker CI with **local backend** (no Docker) on the same VPS.
|
||||||
|
Triggered by push to `main` branch via Gitea webhook.
|
||||||
|
|
||||||
|
## Pipeline Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
push to main → install → lint-fix (optional) → lint + test → deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
| Step | What it does |
|
||||||
|
|------|-------------|
|
||||||
|
| **install** | `npm ci` — clean install dependencies |
|
||||||
|
| **lint-fix** | ESLint `--fix`, auto-commits back with `[CI SKIP]` |
|
||||||
|
| **lint** | ESLint check — fails pipeline on errors |
|
||||||
|
| **test** | `npm test` — fails pipeline on test failures |
|
||||||
|
| **deploy** | Runs deploy script via sudo on the VPS |
|
||||||
|
|
||||||
|
### lint-fix auto-commit
|
||||||
|
|
||||||
|
When ESLint can auto-fix issues (formatting, `var` → `const`, `==` → `===`, etc.),
|
||||||
|
the pipeline commits the fix back to Gitea with `[CI SKIP]` in the message.
|
||||||
|
Woodpecker natively skips pipelines for commits containing `[CI SKIP]`.
|
||||||
|
|
||||||
|
Requires `gitea_token` secret — a Gitea access token with repo write permission.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### `.woodpecker.yml`
|
||||||
|
|
||||||
|
Located at project root. Uses `image: bash` for local backend (specifies shell, not Docker image).
|
||||||
|
|
||||||
|
### Secrets (Woodpecker UI)
|
||||||
|
|
||||||
|
| Secret | Where | Purpose |
|
||||||
|
|--------|-------|---------|
|
||||||
|
| `gitea_token` | Global or repo | Gitea access token for lint-fix auto-commit |
|
||||||
|
|
||||||
|
Add secrets in Woodpecker UI → Settings → Secrets (repo level) or global level.
|
||||||
|
|
||||||
|
## Deploy Setup (one-time on VPS)
|
||||||
|
|
||||||
|
### Automated setup (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# On VPS as root:
|
||||||
|
bash setup-project.sh <project-name> <domain> [port]
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
bash setup-project.sh my-app app.spektr.design 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates: service user, `/opt/<project>` directory, deploy script, sudoers rule, systemd service, nginx config.
|
||||||
|
|
||||||
|
After running, create `.env` and push code to Gitea.
|
||||||
|
|
||||||
|
### Manual setup
|
||||||
|
|
||||||
|
If you need more control, see `scripts/setup-project.sh` for individual steps:
|
||||||
|
1. Create service user (`useradd -r -s /sbin/nologin`)
|
||||||
|
2. Create `/opt/<project>` directory
|
||||||
|
3. Install deploy script to `/usr/local/bin/deploy-<project>`
|
||||||
|
4. Add sudoers rule for Woodpecker agent user
|
||||||
|
5. Create systemd service
|
||||||
|
6. Create nginx config
|
||||||
|
|
||||||
|
### Deploy script details
|
||||||
|
|
||||||
|
`scripts/deploy.sh` does:
|
||||||
|
1. rsync files (excludes node_modules, .env, .git)
|
||||||
|
2. `npm ci --omit=dev` as service user (with `HOME=` set to avoid npm cache errors)
|
||||||
|
3. `systemctl restart` + health check
|
||||||
|
|
||||||
|
The script runs as root via sudo. Only this specific script is allowed in sudoers.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Problem | Cause | Fix |
|
||||||
|
|---------|-------|-----|
|
||||||
|
| `secret "X" not found` | Secret not added in Woodpecker | Add in UI → Settings → Secrets |
|
||||||
|
| `Invalid or missing image` | Missing `image:` field | Use `image: bash` for local backend |
|
||||||
|
| `Permission denied` on deploy | Sudoers not configured | Follow deploy setup above |
|
||||||
|
| `command not found` for deploy script | Script not at `/usr/local/bin/` | Copy and chmod 755 |
|
||||||
|
| npm `EACCES mkdir /home/user` | Service user has no home dir | Set `HOME=/opt/<project>` in deploy script |
|
||||||
|
| Pipeline not triggered | Webhook issue or `[CI SKIP]` in message | Check Gitea webhook settings |
|
||||||
|
|
||||||
|
## Gitea Access Token
|
||||||
|
|
||||||
|
Create in Gitea: Settings → Applications → Generate New Token.
|
||||||
|
Permissions needed: `repo` (read/write).
|
||||||
|
|
||||||
|
## Woodpecker API
|
||||||
|
|
||||||
|
Check pipeline status programmatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List recent pipelines
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
https://ci.spektr.design/api/repos/<REPO_ID>/pipelines?page=1&per_page=5
|
||||||
|
|
||||||
|
# Get specific pipeline
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
https://ci.spektr.design/api/repos/<REPO_ID>/pipelines/<NUMBER>
|
||||||
|
```
|
||||||
|
|
||||||
|
Store token in `.env.local` (gitignored):
|
||||||
|
```
|
||||||
|
WOODPECKER_TOKEN=<your-token>
|
||||||
|
```
|
||||||
39
scripts/ci-lint-fix.sh
Executable file
39
scripts/ci-lint-fix.sh
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ci-lint-fix.sh — Run ESLint --fix and commit changes back to Gitea.
|
||||||
|
#
|
||||||
|
# Used by Woodpecker CI. If ESLint auto-fixes anything, commits with
|
||||||
|
# [CI SKIP] to prevent an infinite pipeline loop.
|
||||||
|
#
|
||||||
|
# Requires GITEA_TOKEN secret in Woodpecker.
|
||||||
|
#
|
||||||
|
# Customize LINT_TARGETS and REPO_URL for your project.
|
||||||
|
|
||||||
|
LINT_TARGETS="."
|
||||||
|
GITEA_HOST="${GITEA_HOST:-git.spektr.design}"
|
||||||
|
GITEA_ORG="${GITEA_ORG:-gitea}"
|
||||||
|
REPO_NAME="${CI_REPO_NAME:-$(basename "$(git rev-parse --show-toplevel)")}"
|
||||||
|
|
||||||
|
echo "--- Running ESLint --fix"
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
npx eslint --fix ${LINT_TARGETS} || true
|
||||||
|
|
||||||
|
if [ -z "$(git diff)" ]; then
|
||||||
|
echo "--- No auto-fixable issues found"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- ESLint auto-fixed issues, committing back..."
|
||||||
|
|
||||||
|
git config user.name "Woodpecker CI"
|
||||||
|
git config user.email "ci@${GITEA_HOST}"
|
||||||
|
|
||||||
|
REPO_URL="https://gitea:${GITEA_TOKEN}@${GITEA_HOST}/${GITEA_ORG}/${REPO_NAME}.git"
|
||||||
|
git remote set-url origin "${REPO_URL}"
|
||||||
|
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix: auto-fix eslint issues [CI SKIP]"
|
||||||
|
git push origin HEAD:main
|
||||||
|
|
||||||
|
echo "--- Auto-fix committed and pushed"
|
||||||
61
scripts/deploy.sh
Executable file
61
scripts/deploy.sh
Executable file
@@ -0,0 +1,61 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# deploy.sh — Deploy project from CI workspace to target directory.
|
||||||
|
#
|
||||||
|
# Works for both manual and Woodpecker CI deployment.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# Manual: sudo bash scripts/deploy.sh
|
||||||
|
# CI: sudo /usr/local/bin/deploy-<project-name> /path/to/workspace
|
||||||
|
#
|
||||||
|
# CI setup (one-time, as root on VPS):
|
||||||
|
# cp scripts/deploy.sh /usr/local/bin/deploy-<project-name>
|
||||||
|
# chmod 755 /usr/local/bin/deploy-<project-name>
|
||||||
|
# chown root:root /usr/local/bin/deploy-<project-name>
|
||||||
|
|
||||||
|
# --- Configure these for your project ---
|
||||||
|
INSTALL_DIR="/opt/<project-name>"
|
||||||
|
SERVICE_USER="<project-user>"
|
||||||
|
SERVICE_NAME="<project-name>"
|
||||||
|
HEALTH_URL="http://localhost:3000/api/health"
|
||||||
|
# -----------------------------------------
|
||||||
|
|
||||||
|
SOURCE_DIR="${1:-.}"
|
||||||
|
|
||||||
|
echo "=== Deploy ==="
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Error: must be root (sudo)"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${INSTALL_DIR}" ]]; then
|
||||||
|
echo "Error: ${INSTALL_DIR} not found"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[1/3] Syncing files..."
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude='node_modules' \
|
||||||
|
--exclude='.env' \
|
||||||
|
--exclude='.git' \
|
||||||
|
--exclude='.idea' \
|
||||||
|
"${SOURCE_DIR}/" "${INSTALL_DIR}/"
|
||||||
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}"
|
||||||
|
|
||||||
|
echo "[2/3] Installing dependencies..."
|
||||||
|
sudo -u "${SERVICE_USER}" HOME="${INSTALL_DIR}" bash -c "cd ${INSTALL_DIR} && npm ci --omit=dev"
|
||||||
|
|
||||||
|
echo "[3/3] Restarting service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart "${SERVICE_NAME}"
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if systemctl is-active --quiet "${SERVICE_NAME}"; then
|
||||||
|
echo "Service running"
|
||||||
|
else
|
||||||
|
echo "Error: service failed to start"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
curl -sf "${HEALTH_URL}" || { echo "FAIL: health check"; exit 1; }
|
||||||
|
|
||||||
|
echo "=== Deploy complete ==="
|
||||||
265
scripts/setup-project.sh
Executable file
265
scripts/setup-project.sh
Executable file
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# setup-project.sh — One-time VPS setup for a new project.
|
||||||
|
#
|
||||||
|
# Run as root on the VPS:
|
||||||
|
# bash setup-project.sh <project-name> <domain> [port]
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# bash setup-project.sh my-app app.spektr.design 3000
|
||||||
|
#
|
||||||
|
# What it does:
|
||||||
|
# 1. Creates service user (no login shell, no home dir)
|
||||||
|
# 2. Creates /opt/<project> directory
|
||||||
|
# 3. Installs deploy script to /usr/local/bin/
|
||||||
|
# 4. Adds sudoers rule for Woodpecker agent
|
||||||
|
# 5. Creates systemd service
|
||||||
|
# 6. Creates nginx config (location blocks for Webuzo)
|
||||||
|
#
|
||||||
|
# After running, you still need to:
|
||||||
|
# - Create .env file at /opt/<project>/.env
|
||||||
|
# - Set up SSL (certbot or Webuzo panel)
|
||||||
|
# - Push code to Gitea to trigger first deploy
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Args
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
PROJECT="${1:?Usage: setup-project.sh <project-name> <domain> [port]}"
|
||||||
|
DOMAIN="${2:?Usage: setup-project.sh <project-name> <domain> [port]}"
|
||||||
|
PORT="${3:-3000}"
|
||||||
|
|
||||||
|
INSTALL_DIR="/opt/${PROJECT}"
|
||||||
|
SERVICE_USER="${PROJECT}"
|
||||||
|
DEPLOY_SCRIPT="/usr/local/bin/deploy-${PROJECT}"
|
||||||
|
|
||||||
|
# Detect Woodpecker agent user
|
||||||
|
AGENT_USER=$(ps -o user= -p "$(pgrep woodpecker-agent)" 2>/dev/null || echo "")
|
||||||
|
if [[ -z "${AGENT_USER}" ]]; then
|
||||||
|
echo "Warning: Woodpecker agent not running. Defaulting to 'git'."
|
||||||
|
AGENT_USER="git"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Setting up project: ${PROJECT} ==="
|
||||||
|
echo " Domain: ${DOMAIN}"
|
||||||
|
echo " Port: ${PORT}"
|
||||||
|
echo " Install dir: ${INSTALL_DIR}"
|
||||||
|
echo " User: ${SERVICE_USER}"
|
||||||
|
echo " Agent user: ${AGENT_USER}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Checks
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo "Error: run as root (sudo)"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "${INSTALL_DIR}" ]]; then
|
||||||
|
echo "Warning: ${INSTALL_DIR} already exists. Continuing..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1. Service user
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[1/6] Creating service user..."
|
||||||
|
if id "${SERVICE_USER}" &>/dev/null; then
|
||||||
|
echo " User ${SERVICE_USER} already exists"
|
||||||
|
else
|
||||||
|
useradd -r -s /sbin/nologin "${SERVICE_USER}"
|
||||||
|
echo " Created user ${SERVICE_USER}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2. Project directory
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[2/6] Creating project directory..."
|
||||||
|
mkdir -p "${INSTALL_DIR}"
|
||||||
|
chown "${SERVICE_USER}:${SERVICE_USER}" "${INSTALL_DIR}"
|
||||||
|
echo " ${INSTALL_DIR} ready"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3. Deploy script
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[3/6] Installing deploy script..."
|
||||||
|
cat > "${DEPLOY_SCRIPT}" << DEPLOY
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
INSTALL_DIR="${INSTALL_DIR}"
|
||||||
|
SERVICE_USER="${SERVICE_USER}"
|
||||||
|
SERVICE_NAME="${PROJECT}"
|
||||||
|
HEALTH_URL="http://localhost:${PORT}/api/health"
|
||||||
|
SOURCE_DIR="\${1:-.}"
|
||||||
|
echo "=== Deploy ${PROJECT} ==="
|
||||||
|
if [[ \$EUID -ne 0 ]]; then
|
||||||
|
echo "Error: must be root"; exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -d "\${INSTALL_DIR}" ]]; then
|
||||||
|
echo "Error: \${INSTALL_DIR} not found"; exit 1
|
||||||
|
fi
|
||||||
|
echo "[1/3] Syncing files..."
|
||||||
|
rsync -a --delete --exclude='node_modules' --exclude='.env' --exclude='.git' --exclude='.idea' "\${SOURCE_DIR}/" "\${INSTALL_DIR}/"
|
||||||
|
chown -R "\${SERVICE_USER}:\${SERVICE_USER}" "\${INSTALL_DIR}"
|
||||||
|
echo "[2/3] Installing dependencies..."
|
||||||
|
sudo -u "\${SERVICE_USER}" HOME="\${INSTALL_DIR}" bash -c "cd \${INSTALL_DIR} && npm ci --omit=dev"
|
||||||
|
echo "[3/3] Restarting service..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl restart "\${SERVICE_NAME}"
|
||||||
|
sleep 2
|
||||||
|
if systemctl is-active --quiet "\${SERVICE_NAME}"; then
|
||||||
|
echo "Service running"
|
||||||
|
else
|
||||||
|
echo "Error: service failed"; exit 1
|
||||||
|
fi
|
||||||
|
curl -sf "\${HEALTH_URL}" || { echo "FAIL: health check"; exit 1; }
|
||||||
|
echo "=== Deploy complete ==="
|
||||||
|
DEPLOY
|
||||||
|
chmod 755 "${DEPLOY_SCRIPT}"
|
||||||
|
# Strip trailing whitespace (heredoc can add it)
|
||||||
|
sed -i 's/[[:space:]]*$//' "${DEPLOY_SCRIPT}"
|
||||||
|
echo " ${DEPLOY_SCRIPT} installed"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4. Sudoers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[4/6] Configuring sudoers..."
|
||||||
|
SUDOERS_FILE="/etc/sudoers.d/woodpecker-${PROJECT}"
|
||||||
|
echo "${AGENT_USER} ALL=(root) NOPASSWD: ${DEPLOY_SCRIPT} *" > "${SUDOERS_FILE}"
|
||||||
|
chmod 0440 "${SUDOERS_FILE}"
|
||||||
|
if visudo -cf "${SUDOERS_FILE}" &>/dev/null; then
|
||||||
|
echo " ${SUDOERS_FILE} — parsed OK"
|
||||||
|
else
|
||||||
|
echo " ERROR: sudoers syntax check failed!"
|
||||||
|
rm -f "${SUDOERS_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5. Systemd service
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[5/6] Creating systemd service..."
|
||||||
|
cat > "/etc/systemd/system/${PROJECT}.service" << SERVICE
|
||||||
|
[Unit]
|
||||||
|
Description=${PROJECT}
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=${SERVICE_USER}
|
||||||
|
Group=${SERVICE_USER}
|
||||||
|
WorkingDirectory=${INSTALL_DIR}
|
||||||
|
ExecStart=/usr/bin/node ${INSTALL_DIR}/src/index.js
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
StartLimitBurst=5
|
||||||
|
StartLimitIntervalSec=60
|
||||||
|
|
||||||
|
EnvironmentFile=-${INSTALL_DIR}/.env
|
||||||
|
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectHome=true
|
||||||
|
ProtectKernelTunables=true
|
||||||
|
ProtectKernelModules=true
|
||||||
|
ProtectControlGroups=true
|
||||||
|
|
||||||
|
LimitNOFILE=65536
|
||||||
|
MemoryMax=256M
|
||||||
|
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=${PROJECT}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
SERVICE
|
||||||
|
systemctl daemon-reload
|
||||||
|
echo " ${PROJECT}.service created"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6. Nginx config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo "[6/6] Creating nginx config..."
|
||||||
|
NGINX_DIR="/var/webuzo-data/nginx/custom/domains"
|
||||||
|
if [[ -d "${NGINX_DIR}" ]]; then
|
||||||
|
NGINX_CONF="${NGINX_DIR}/${DOMAIN}.conf"
|
||||||
|
else
|
||||||
|
NGINX_DIR="/etc/nginx/conf.d"
|
||||||
|
NGINX_CONF="${NGINX_DIR}/${PROJECT}.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "${NGINX_CONF}" << 'NGINX'
|
||||||
|
# Static files
|
||||||
|
location / {
|
||||||
|
root __INSTALL_DIR__/public;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# API proxy
|
||||||
|
location ^~ /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:__PORT__;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 30s;
|
||||||
|
proxy_read_timeout 30s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# WebSocket proxy (if needed)
|
||||||
|
location ^~ /ws/ {
|
||||||
|
proxy_pass http://127.0.0.1:__PORT__;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_pass_header Upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
NGINX
|
||||||
|
|
||||||
|
sed -i "s|__INSTALL_DIR__|${INSTALL_DIR}|g" "${NGINX_CONF}"
|
||||||
|
sed -i "s|__PORT__|${PORT}|g" "${NGINX_CONF}"
|
||||||
|
|
||||||
|
if nginx -t 2>/dev/null; then
|
||||||
|
systemctl reload nginx
|
||||||
|
echo " ${NGINX_CONF} — nginx reloaded"
|
||||||
|
else
|
||||||
|
echo " Warning: nginx config test failed. Check ${NGINX_CONF} manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Done
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Create .env: nano ${INSTALL_DIR}/.env"
|
||||||
|
echo " 2. Push code to Gitea — Woodpecker will deploy automatically"
|
||||||
|
echo " 3. Check: curl -s http://localhost:${PORT}/api/health"
|
||||||
|
echo " 4. Uncomment deploy step in .woodpecker.yml:"
|
||||||
|
echo " sudo /usr/local/bin/deploy-${PROJECT} \"\${CI_WORKSPACE:-.}\""
|
||||||
Reference in New Issue
Block a user