sum-cli 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sum/__init__.py +1 -0
- sum/boilerplate/.env.example +124 -0
- sum/boilerplate/.gitea/workflows/ci.yml +33 -0
- sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
- sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
- sum/boilerplate/.github/workflows/ci.yml +36 -0
- sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
- sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
- sum/boilerplate/.gitignore +45 -0
- sum/boilerplate/README.md +259 -0
- sum/boilerplate/manage.py +34 -0
- sum/boilerplate/project_name/__init__.py +5 -0
- sum/boilerplate/project_name/home/__init__.py +5 -0
- sum/boilerplate/project_name/home/apps.py +20 -0
- sum/boilerplate/project_name/home/management/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
- sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
- sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
- sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
- sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
- sum/boilerplate/project_name/home/models.py +13 -0
- sum/boilerplate/project_name/settings/__init__.py +5 -0
- sum/boilerplate/project_name/settings/base.py +348 -0
- sum/boilerplate/project_name/settings/local.py +78 -0
- sum/boilerplate/project_name/settings/production.py +106 -0
- sum/boilerplate/project_name/urls.py +33 -0
- sum/boilerplate/project_name/wsgi.py +16 -0
- sum/boilerplate/pytest.ini +5 -0
- sum/boilerplate/requirements.txt +25 -0
- sum/boilerplate/static/client/.gitkeep +3 -0
- sum/boilerplate/templates/overrides/.gitkeep +3 -0
- sum/boilerplate/tests/__init__.py +3 -0
- sum/boilerplate/tests/test_health.py +51 -0
- sum/cli.py +42 -0
- sum/commands/__init__.py +10 -0
- sum/commands/backup.py +308 -0
- sum/commands/check.py +128 -0
- sum/commands/init.py +265 -0
- sum/commands/promote.py +758 -0
- sum/commands/run.py +96 -0
- sum/commands/themes.py +56 -0
- sum/commands/update.py +301 -0
- sum/config.py +61 -0
- sum/docs/USER_GUIDE.md +663 -0
- sum/exceptions.py +45 -0
- sum/setup/__init__.py +17 -0
- sum/setup/auth.py +184 -0
- sum/setup/database.py +58 -0
- sum/setup/deps.py +73 -0
- sum/setup/git_ops.py +463 -0
- sum/setup/infrastructure.py +576 -0
- sum/setup/orchestrator.py +354 -0
- sum/setup/remote_themes.py +371 -0
- sum/setup/scaffold.py +500 -0
- sum/setup/seed.py +110 -0
- sum/setup/site_orchestrator.py +441 -0
- sum/setup/venv.py +89 -0
- sum/system_config.py +330 -0
- sum/themes_registry.py +180 -0
- sum/utils/__init__.py +25 -0
- sum/utils/django.py +97 -0
- sum/utils/environment.py +76 -0
- sum/utils/output.py +78 -0
- sum/utils/project.py +110 -0
- sum/utils/prompts.py +36 -0
- sum/utils/validation.py +313 -0
- sum_cli-3.0.0.dist-info/METADATA +127 -0
- sum_cli-3.0.0.dist-info/RECORD +72 -0
- sum_cli-3.0.0.dist-info/WHEEL +5 -0
- sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
- sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
- sum_cli-3.0.0.dist-info/top_level.txt +1 -0
sum/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI v2 core package."""
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# SUM Client Environment Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Copy this file to .env and set values appropriate for your environment.
|
|
5
|
+
# All variables with defaults shown are optional unless noted otherwise.
|
|
6
|
+
|
|
7
|
+
# =============================================================================
|
|
8
|
+
# Django Core
|
|
9
|
+
# =============================================================================
|
|
10
|
+
|
|
11
|
+
# REQUIRED in production: Generate with `python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"`
|
|
12
|
+
DJANGO_SECRET_KEY=change-me-in-production
|
|
13
|
+
|
|
14
|
+
# REQUIRED in production: Comma-separated list of allowed hostnames
|
|
15
|
+
ALLOWED_HOSTS=localhost,127.0.0.1
|
|
16
|
+
|
|
17
|
+
# Settings module (default: project_name.settings.local)
|
|
18
|
+
# For production, set to: project_name.settings.production
|
|
19
|
+
# DJANGO_SETTINGS_MODULE=project_name.settings.local
|
|
20
|
+
|
|
21
|
+
# Wagtail admin base URL (for email links, etc.)
|
|
22
|
+
WAGTAILADMIN_BASE_URL=http://localhost:8001
|
|
23
|
+
|
|
24
|
+
# =============================================================================
|
|
25
|
+
# Database (PostgreSQL)
|
|
26
|
+
# =============================================================================
|
|
27
|
+
# REQUIRED: PostgreSQL is used for both local development and production.
|
|
28
|
+
# These must be configured before running migrations.
|
|
29
|
+
|
|
30
|
+
DJANGO_DB_NAME=sum_db
|
|
31
|
+
DJANGO_DB_USER=sum_user
|
|
32
|
+
DJANGO_DB_PASSWORD=sum_password
|
|
33
|
+
DJANGO_DB_HOST=localhost
|
|
34
|
+
DJANGO_DB_PORT=5432
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# =============================================================================
|
|
38
|
+
# Static/Media Roots (Golden Path VPS)
|
|
39
|
+
# =============================================================================
|
|
40
|
+
# For the VPS golden path, set these to point outside the app checkout so Caddy can serve
|
|
41
|
+
# static/media directly. Example:
|
|
42
|
+
# DJANGO_STATIC_ROOT=/srv/sum/<site_slug>/static
|
|
43
|
+
# DJANGO_MEDIA_ROOT=/srv/sum/<site_slug>/media
|
|
44
|
+
#
|
|
45
|
+
# DJANGO_STATIC_ROOT=
|
|
46
|
+
# DJANGO_MEDIA_ROOT=
|
|
47
|
+
|
|
48
|
+
# Optional: used by infrastructure/scripts/deploy.sh smoke checks
|
|
49
|
+
# SITE_DOMAIN=example.com
|
|
50
|
+
|
|
51
|
+
# =============================================================================
|
|
52
|
+
# Redis / Cache
|
|
53
|
+
# =============================================================================
|
|
54
|
+
# Used for caching and Celery broker in production.
|
|
55
|
+
# Local development uses in-memory cache by default.
|
|
56
|
+
|
|
57
|
+
REDIS_URL=redis://localhost:6379/0
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Celery (Async Tasks)
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Falls back to synchronous execution if not configured.
|
|
63
|
+
|
|
64
|
+
CELERY_BROKER_URL=redis://localhost:6379/0
|
|
65
|
+
CELERY_RESULT_BACKEND=redis://localhost:6379/0
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Email Configuration (SMTP)
|
|
69
|
+
# =============================================================================
|
|
70
|
+
# Defaults to console backend for development (emails printed to stdout).
|
|
71
|
+
# Configure SMTP for production email delivery.
|
|
72
|
+
#
|
|
73
|
+
# We recommend Resend (https://resend.com) for transactional email.
|
|
74
|
+
# Sign up, verify your domain, and get an API key from the dashboard.
|
|
75
|
+
#
|
|
76
|
+
# Resend SMTP settings:
|
|
77
|
+
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
|
78
|
+
EMAIL_HOST=smtp.resend.com
|
|
79
|
+
EMAIL_PORT=587
|
|
80
|
+
EMAIL_HOST_USER=resend
|
|
81
|
+
EMAIL_HOST_PASSWORD=re_YOUR_API_KEY_HERE
|
|
82
|
+
EMAIL_USE_TLS=True
|
|
83
|
+
EMAIL_USE_SSL=False
|
|
84
|
+
DEFAULT_FROM_EMAIL=noreply@yourdomain.com
|
|
85
|
+
|
|
86
|
+
# Lead notification recipient - where new lead alerts are sent.
|
|
87
|
+
# This MUST be set for lead notification emails to be delivered.
|
|
88
|
+
LEAD_NOTIFICATION_EMAIL=leads@yourdomain.com
|
|
89
|
+
|
|
90
|
+
# =============================================================================
|
|
91
|
+
# Zapier Integration (Optional)
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Configure per-site in Wagtail SiteSettings, or set globally here.
|
|
94
|
+
|
|
95
|
+
# ZAPIER_WEBHOOK_URL=https://hooks.zapier.com/hooks/catch/...
|
|
96
|
+
|
|
97
|
+
# =============================================================================
|
|
98
|
+
# Observability
|
|
99
|
+
# =============================================================================
|
|
100
|
+
|
|
101
|
+
# Sentry error tracking (optional, disabled if not set)
|
|
102
|
+
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
|
|
103
|
+
# SENTRY_ENVIRONMENT=development
|
|
104
|
+
# SENTRY_TRACES_SAMPLE_RATE=0.0
|
|
105
|
+
|
|
106
|
+
# Logging configuration (optional)
|
|
107
|
+
# LOG_LEVEL=INFO
|
|
108
|
+
# LOG_FORMAT=auto # "auto", "json", or blank for console-friendly
|
|
109
|
+
|
|
110
|
+
# =============================================================================
|
|
111
|
+
# Build / Version Info (typically set by CI)
|
|
112
|
+
# =============================================================================
|
|
113
|
+
# These are included in /health/ endpoint and Sentry releases.
|
|
114
|
+
|
|
115
|
+
# GIT_SHA=abc123
|
|
116
|
+
# BUILD_ID=build-123
|
|
117
|
+
# RELEASE=v0.1.0
|
|
118
|
+
|
|
119
|
+
# =============================================================================
|
|
120
|
+
# Security (Production Only)
|
|
121
|
+
# =============================================================================
|
|
122
|
+
# Set to True to enforce HTTPS redirects (default: True in production.py)
|
|
123
|
+
|
|
124
|
+
# SECURE_SSL_REDIRECT=True
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Check out repository
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
cache: "pip"
|
|
22
|
+
cache-dependency-path: requirements.txt
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
python -m pip install --upgrade pip
|
|
27
|
+
pip install -r requirements.txt
|
|
28
|
+
|
|
29
|
+
- name: Install test dependencies
|
|
30
|
+
run: pip install pytest pytest-django
|
|
31
|
+
|
|
32
|
+
- name: Run tests
|
|
33
|
+
run: pytest
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: Deploy Production
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
group: deploy-production
|
|
9
|
+
cancel-in-progress: false
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
deploy:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
timeout-minutes: 15
|
|
15
|
+
env:
|
|
16
|
+
PRODUCTION_HOST: ${{ secrets.PRODUCTION_HOST }}
|
|
17
|
+
PRODUCTION_USER: ${{ secrets.PRODUCTION_USER }}
|
|
18
|
+
PRODUCTION_PATH: ${{ secrets.PRODUCTION_PATH }}
|
|
19
|
+
PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }}
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- name: Verify required secrets are present
|
|
23
|
+
run: |
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
for var in PRODUCTION_HOST PRODUCTION_USER PRODUCTION_PATH PRODUCTION_URL; do
|
|
26
|
+
if [ -z "${!var}" ]; then
|
|
27
|
+
echo "Missing $var" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
fi
|
|
30
|
+
done
|
|
31
|
+
|
|
32
|
+
- name: Validate SSH key secret
|
|
33
|
+
if: ${{ secrets.PRODUCTION_SSH_KEY == '' }}
|
|
34
|
+
run: |
|
|
35
|
+
echo "::error::PRODUCTION_SSH_KEY is not set"
|
|
36
|
+
exit 1
|
|
37
|
+
|
|
38
|
+
- name: Set up SSH
|
|
39
|
+
run: |
|
|
40
|
+
set -euo pipefail
|
|
41
|
+
mkdir -p ~/.ssh
|
|
42
|
+
chmod 700 ~/.ssh
|
|
43
|
+
echo "${{ secrets.PRODUCTION_SSH_KEY }}" > ~/.ssh/id_rsa
|
|
44
|
+
chmod 600 ~/.ssh/id_rsa
|
|
45
|
+
ssh-keyscan -H "$PRODUCTION_HOST" > ~/.ssh/known_hosts
|
|
46
|
+
if [ ! -s ~/.ssh/known_hosts ]; then
|
|
47
|
+
echo "ssh-keyscan returned no host keys for $PRODUCTION_HOST" >&2
|
|
48
|
+
exit 1
|
|
49
|
+
fi
|
|
50
|
+
chmod 600 ~/.ssh/known_hosts
|
|
51
|
+
|
|
52
|
+
# Rollback: ssh $PRODUCTION_USER@$PRODUCTION_HOST "/srv/sum/bin/deploy.sh --site-slug <site-slug> --ref <previous-tag>"
|
|
53
|
+
- name: Deploy release tag
|
|
54
|
+
run: |
|
|
55
|
+
set -euo pipefail
|
|
56
|
+
SITE_SLUG=$(basename "$PRODUCTION_PATH")
|
|
57
|
+
RELEASE_TAG="${{ gitea.event.release.tag_name }}"
|
|
58
|
+
if [ -z "$RELEASE_TAG" ]; then
|
|
59
|
+
echo "Release tag not found in event payload" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes \
|
|
63
|
+
"${PRODUCTION_USER}@${PRODUCTION_HOST}" \
|
|
64
|
+
"/srv/sum/bin/deploy.sh --site-slug \"$SITE_SLUG\" --ref \"$RELEASE_TAG\""
|
|
65
|
+
|
|
66
|
+
- name: Health check (blocking)
|
|
67
|
+
run: |
|
|
68
|
+
set -euo pipefail
|
|
69
|
+
BASE_URL="${PRODUCTION_URL%/}"
|
|
70
|
+
for attempt in 1 2 3; do
|
|
71
|
+
echo "Health check attempt $attempt..."
|
|
72
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
73
|
+
"$BASE_URL/health/" >/dev/null; then
|
|
74
|
+
echo "Health check OK"
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
if [ "$attempt" -lt 3 ]; then
|
|
78
|
+
echo "Health check failed, retrying in 10s..."
|
|
79
|
+
sleep 10
|
|
80
|
+
fi
|
|
81
|
+
done
|
|
82
|
+
echo "::error::Health check failed after 3 attempts"
|
|
83
|
+
exit 1
|
|
84
|
+
|
|
85
|
+
- name: Sitemap check (non-blocking)
|
|
86
|
+
run: |
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
BASE_URL="${PRODUCTION_URL%/}"
|
|
89
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
90
|
+
"$BASE_URL/sitemap.xml" >/dev/null; then
|
|
91
|
+
echo "Sitemap check OK"
|
|
92
|
+
else
|
|
93
|
+
echo "::warning::Sitemap check failed (non-blocking)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
- name: Clean up SSH key
|
|
97
|
+
if: always()
|
|
98
|
+
run: rm -f ~/.ssh/id_rsa
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Name: deploy-staging.yml
|
|
2
|
+
# Path: boilerplate/.gitea/workflows/deploy-staging.yml
|
|
3
|
+
# Purpose: Deploy to staging on push to main.
|
|
4
|
+
#
|
|
5
|
+
# Required secrets:
|
|
6
|
+
# - STAGING_SSH_KEY: SSH private key for deploy user
|
|
7
|
+
# - STAGING_HOST: staging host/IP
|
|
8
|
+
# - STAGING_USER: SSH username
|
|
9
|
+
# - STAGING_PATH: site root (/srv/sum/<site-slug>)
|
|
10
|
+
# - STAGING_URL: staging URL (reserved for health checks)
|
|
11
|
+
|
|
12
|
+
name: Deploy Staging
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
branches: [main]
|
|
17
|
+
|
|
18
|
+
concurrency:
|
|
19
|
+
group: deploy-staging
|
|
20
|
+
cancel-in-progress: false
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
deploy:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 15
|
|
26
|
+
env:
|
|
27
|
+
STAGING_HOST: ${{ secrets.STAGING_HOST }}
|
|
28
|
+
STAGING_USER: ${{ secrets.STAGING_USER }}
|
|
29
|
+
STAGING_PATH: ${{ secrets.STAGING_PATH }}
|
|
30
|
+
STAGING_URL: ${{ secrets.STAGING_URL }}
|
|
31
|
+
steps:
|
|
32
|
+
- name: Validate required secrets
|
|
33
|
+
run: |
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
for name in STAGING_HOST STAGING_USER STAGING_PATH STAGING_URL; do
|
|
36
|
+
if [ -z "${!name}" ]; then
|
|
37
|
+
echo "::error::$name is not set"
|
|
38
|
+
exit 1
|
|
39
|
+
fi
|
|
40
|
+
done
|
|
41
|
+
|
|
42
|
+
- name: Validate SSH key secret
|
|
43
|
+
if: ${{ secrets.STAGING_SSH_KEY == '' }}
|
|
44
|
+
run: |
|
|
45
|
+
echo "::error::STAGING_SSH_KEY is not set"
|
|
46
|
+
exit 1
|
|
47
|
+
|
|
48
|
+
- name: Set up SSH
|
|
49
|
+
run: |
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
mkdir -p ~/.ssh
|
|
52
|
+
chmod 700 ~/.ssh
|
|
53
|
+
echo "${{ secrets.STAGING_SSH_KEY }}" > ~/.ssh/id_rsa
|
|
54
|
+
chmod 600 ~/.ssh/id_rsa
|
|
55
|
+
ssh-keyscan -H "$STAGING_HOST" > ~/.ssh/known_hosts
|
|
56
|
+
if [ ! -s ~/.ssh/known_hosts ]; then
|
|
57
|
+
echo "::error::ssh-keyscan produced no host keys for $STAGING_HOST"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
chmod 600 ~/.ssh/known_hosts
|
|
61
|
+
|
|
62
|
+
- name: Derive site slug
|
|
63
|
+
id: slug
|
|
64
|
+
run: |
|
|
65
|
+
set -euo pipefail
|
|
66
|
+
SITE_SLUG=$(basename "$STAGING_PATH")
|
|
67
|
+
echo "site_slug=$SITE_SLUG" >> "$GITEA_OUTPUT"
|
|
68
|
+
echo "Derived site slug: $SITE_SLUG"
|
|
69
|
+
|
|
70
|
+
# Rollback:
|
|
71
|
+
# 1) SSH to the server.
|
|
72
|
+
# 2) /srv/sum/bin/deploy.sh --site-slug <slug> --ref <previous-tag>
|
|
73
|
+
- name: Deploy to staging
|
|
74
|
+
env:
|
|
75
|
+
SITE_SLUG: ${{ steps.slug.outputs.site_slug }}
|
|
76
|
+
run: |
|
|
77
|
+
set -euo pipefail
|
|
78
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes "${STAGING_USER}@${STAGING_HOST}" \
|
|
79
|
+
"/srv/sum/bin/deploy.sh --site-slug \"$SITE_SLUG\" --ref \"${{ gitea.sha }}\""
|
|
80
|
+
|
|
81
|
+
- name: Health check (blocking)
|
|
82
|
+
run: |
|
|
83
|
+
set -euo pipefail
|
|
84
|
+
BASE_URL="${STAGING_URL%/}"
|
|
85
|
+
for attempt in 1 2 3; do
|
|
86
|
+
echo "Health check attempt $attempt..."
|
|
87
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
88
|
+
"$BASE_URL/health/" >/dev/null; then
|
|
89
|
+
echo "Health check OK"
|
|
90
|
+
exit 0
|
|
91
|
+
fi
|
|
92
|
+
if [ "$attempt" -lt 3 ]; then
|
|
93
|
+
echo "Health check failed, retrying in 10s..."
|
|
94
|
+
sleep 10
|
|
95
|
+
fi
|
|
96
|
+
done
|
|
97
|
+
echo "::error::Health check failed after 3 attempts"
|
|
98
|
+
exit 1
|
|
99
|
+
|
|
100
|
+
- name: Sitemap check (non-blocking)
|
|
101
|
+
run: |
|
|
102
|
+
set -euo pipefail
|
|
103
|
+
BASE_URL="${STAGING_URL%/}"
|
|
104
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
105
|
+
"$BASE_URL/sitemap.xml" >/dev/null; then
|
|
106
|
+
echo "Sitemap check OK"
|
|
107
|
+
else
|
|
108
|
+
echo "::warning::Sitemap check failed (non-blocking)"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
- name: Clean up SSH key
|
|
112
|
+
if: always()
|
|
113
|
+
run: rm -f ~/.ssh/id_rsa
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches:
|
|
7
|
+
- main
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Check out repository
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.12"
|
|
24
|
+
cache: "pip"
|
|
25
|
+
cache-dependency-path: requirements.txt
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: |
|
|
29
|
+
python -m pip install --upgrade pip
|
|
30
|
+
pip install -r requirements.txt
|
|
31
|
+
|
|
32
|
+
- name: Install test dependencies
|
|
33
|
+
run: pip install pytest pytest-django
|
|
34
|
+
|
|
35
|
+
- name: Run tests
|
|
36
|
+
run: pytest
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
name: Deploy Production
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: deploy-production
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
deploy:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
timeout-minutes: 15
|
|
18
|
+
environment: production
|
|
19
|
+
# Requires GitHub environment "production" with Required reviewers enabled.
|
|
20
|
+
env:
|
|
21
|
+
PRODUCTION_HOST: ${{ secrets.PRODUCTION_HOST }}
|
|
22
|
+
PRODUCTION_USER: ${{ secrets.PRODUCTION_USER }}
|
|
23
|
+
PRODUCTION_PATH: ${{ secrets.PRODUCTION_PATH }}
|
|
24
|
+
PRODUCTION_URL: ${{ secrets.PRODUCTION_URL }}
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- name: Verify required secrets are present
|
|
28
|
+
run: |
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
for var in PRODUCTION_HOST PRODUCTION_USER PRODUCTION_PATH PRODUCTION_URL; do
|
|
31
|
+
if [ -z "${!var}" ]; then
|
|
32
|
+
echo "Missing $var" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
done
|
|
36
|
+
|
|
37
|
+
- name: Validate SSH key secret
|
|
38
|
+
if: ${{ secrets.PRODUCTION_SSH_KEY == '' }}
|
|
39
|
+
run: |
|
|
40
|
+
echo "::error::PRODUCTION_SSH_KEY is not set"
|
|
41
|
+
exit 1
|
|
42
|
+
|
|
43
|
+
- name: Start SSH agent
|
|
44
|
+
uses: webfactory/ssh-agent@v0.9.0
|
|
45
|
+
with:
|
|
46
|
+
ssh-private-key: ${{ secrets.PRODUCTION_SSH_KEY }}
|
|
47
|
+
|
|
48
|
+
- name: Add host to known_hosts
|
|
49
|
+
run: |
|
|
50
|
+
set -euo pipefail
|
|
51
|
+
mkdir -p ~/.ssh
|
|
52
|
+
chmod 700 ~/.ssh
|
|
53
|
+
ssh-keyscan -H "$PRODUCTION_HOST" > ~/.ssh/known_hosts
|
|
54
|
+
if [ ! -s ~/.ssh/known_hosts ]; then
|
|
55
|
+
echo "ssh-keyscan returned no host keys for $PRODUCTION_HOST" >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
chmod 600 ~/.ssh/known_hosts
|
|
59
|
+
|
|
60
|
+
# Rollback: ssh $PRODUCTION_USER@$PRODUCTION_HOST "/srv/sum/bin/deploy.sh --site-slug <site-slug> --ref <previous-tag>"
|
|
61
|
+
- name: Deploy release tag
|
|
62
|
+
run: |
|
|
63
|
+
set -euo pipefail
|
|
64
|
+
SITE_SLUG=$(basename "$PRODUCTION_PATH")
|
|
65
|
+
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
|
66
|
+
if [ -z "$RELEASE_TAG" ]; then
|
|
67
|
+
echo "Release tag not found in event payload" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes \
|
|
71
|
+
"${PRODUCTION_USER}@${PRODUCTION_HOST}" \
|
|
72
|
+
"/srv/sum/bin/deploy.sh --site-slug \"$SITE_SLUG\" --ref \"$RELEASE_TAG\""
|
|
73
|
+
|
|
74
|
+
- name: Health check (blocking)
|
|
75
|
+
run: |
|
|
76
|
+
set -euo pipefail
|
|
77
|
+
BASE_URL="${PRODUCTION_URL%/}"
|
|
78
|
+
for attempt in 1 2 3; do
|
|
79
|
+
echo "Health check attempt $attempt..."
|
|
80
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
81
|
+
"$BASE_URL/health/" >/dev/null; then
|
|
82
|
+
echo "Health check OK"
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
if [ "$attempt" -lt 3 ]; then
|
|
86
|
+
echo "Health check failed, retrying in 10s..."
|
|
87
|
+
sleep 10
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
echo "::error::Health check failed after 3 attempts"
|
|
91
|
+
exit 1
|
|
92
|
+
|
|
93
|
+
- name: Sitemap check (non-blocking)
|
|
94
|
+
run: |
|
|
95
|
+
set -euo pipefail
|
|
96
|
+
BASE_URL="${PRODUCTION_URL%/}"
|
|
97
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
98
|
+
"$BASE_URL/sitemap.xml" >/dev/null; then
|
|
99
|
+
echo "Sitemap check OK"
|
|
100
|
+
else
|
|
101
|
+
echo "::warning::Sitemap check failed (non-blocking)"
|
|
102
|
+
fi
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Name: deploy-staging.yml
|
|
2
|
+
# Path: boilerplate/.github/workflows/deploy-staging.yml
|
|
3
|
+
# Purpose: Deploy to staging on push to main.
|
|
4
|
+
#
|
|
5
|
+
# Required secrets:
|
|
6
|
+
# - STAGING_SSH_KEY: SSH private key for deploy user
|
|
7
|
+
# - STAGING_HOST: staging host/IP
|
|
8
|
+
# - STAGING_USER: SSH username
|
|
9
|
+
# - STAGING_PATH: site root (/srv/sum/<site-slug>)
|
|
10
|
+
# - STAGING_URL: staging URL (reserved for health checks in BPCI-04)
|
|
11
|
+
|
|
12
|
+
name: Deploy Staging
|
|
13
|
+
|
|
14
|
+
on:
|
|
15
|
+
push:
|
|
16
|
+
branches: [main]
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
|
|
21
|
+
concurrency:
|
|
22
|
+
group: deploy-staging
|
|
23
|
+
cancel-in-progress: false
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
deploy:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
timeout-minutes: 15
|
|
29
|
+
env:
|
|
30
|
+
STAGING_HOST: ${{ secrets.STAGING_HOST }}
|
|
31
|
+
STAGING_USER: ${{ secrets.STAGING_USER }}
|
|
32
|
+
STAGING_PATH: ${{ secrets.STAGING_PATH }}
|
|
33
|
+
STAGING_URL: ${{ secrets.STAGING_URL }}
|
|
34
|
+
steps:
|
|
35
|
+
- name: Validate required secrets
|
|
36
|
+
run: |
|
|
37
|
+
set -euo pipefail
|
|
38
|
+
for name in STAGING_HOST STAGING_USER STAGING_PATH STAGING_URL; do
|
|
39
|
+
if [ -z "${!name}" ]; then
|
|
40
|
+
echo "::error::$name is not set"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
- name: Validate SSH key secret
|
|
46
|
+
if: ${{ secrets.STAGING_SSH_KEY == '' }}
|
|
47
|
+
run: |
|
|
48
|
+
echo "::error::STAGING_SSH_KEY is not set"
|
|
49
|
+
exit 1
|
|
50
|
+
|
|
51
|
+
- name: Set up SSH agent
|
|
52
|
+
uses: webfactory/ssh-agent@v0.9.0
|
|
53
|
+
with:
|
|
54
|
+
ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}
|
|
55
|
+
|
|
56
|
+
- name: Add staging host to known_hosts
|
|
57
|
+
run: |
|
|
58
|
+
set -euo pipefail
|
|
59
|
+
mkdir -p ~/.ssh
|
|
60
|
+
chmod 700 ~/.ssh
|
|
61
|
+
ssh-keyscan -H "$STAGING_HOST" > ~/.ssh/known_hosts
|
|
62
|
+
if [ ! -s ~/.ssh/known_hosts ]; then
|
|
63
|
+
echo "::error::ssh-keyscan produced no host keys for $STAGING_HOST"
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
chmod 600 ~/.ssh/known_hosts
|
|
67
|
+
|
|
68
|
+
- name: Derive site slug
|
|
69
|
+
id: slug
|
|
70
|
+
run: |
|
|
71
|
+
set -euo pipefail
|
|
72
|
+
SITE_SLUG=$(basename "$STAGING_PATH")
|
|
73
|
+
echo "site_slug=$SITE_SLUG" >> "$GITHUB_OUTPUT"
|
|
74
|
+
echo "Derived site slug: $SITE_SLUG"
|
|
75
|
+
|
|
76
|
+
# Rollback:
|
|
77
|
+
# 1) SSH to the server.
|
|
78
|
+
# 2) /srv/sum/bin/deploy.sh --site-slug <slug> --ref <previous-tag>
|
|
79
|
+
- name: Deploy to staging
|
|
80
|
+
env:
|
|
81
|
+
SITE_SLUG: ${{ steps.slug.outputs.site_slug }}
|
|
82
|
+
run: |
|
|
83
|
+
set -euo pipefail
|
|
84
|
+
ssh -o BatchMode=yes -o StrictHostKeyChecking=yes "${STAGING_USER}@${STAGING_HOST}" \
|
|
85
|
+
"/srv/sum/bin/deploy.sh --site-slug \"$SITE_SLUG\" --ref \"$GITHUB_SHA\""
|
|
86
|
+
|
|
87
|
+
- name: Health check (blocking)
|
|
88
|
+
run: |
|
|
89
|
+
set -euo pipefail
|
|
90
|
+
BASE_URL="${STAGING_URL%/}"
|
|
91
|
+
for attempt in 1 2 3; do
|
|
92
|
+
echo "Health check attempt $attempt..."
|
|
93
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
94
|
+
"$BASE_URL/health/" >/dev/null; then
|
|
95
|
+
echo "Health check OK"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
if [ "$attempt" -lt 3 ]; then
|
|
99
|
+
echo "Health check failed, retrying in 10s..."
|
|
100
|
+
sleep 10
|
|
101
|
+
fi
|
|
102
|
+
done
|
|
103
|
+
echo "::error::Health check failed after 3 attempts"
|
|
104
|
+
exit 1
|
|
105
|
+
|
|
106
|
+
- name: Sitemap check (non-blocking)
|
|
107
|
+
run: |
|
|
108
|
+
set -euo pipefail
|
|
109
|
+
BASE_URL="${STAGING_URL%/}"
|
|
110
|
+
if curl --fail --show-error --silent --location --max-redirs 0 --max-time 60 \
|
|
111
|
+
"$BASE_URL/sitemap.xml" >/dev/null; then
|
|
112
|
+
echo "Sitemap check OK"
|
|
113
|
+
else
|
|
114
|
+
echo "::warning::Sitemap check failed (non-blocking)"
|
|
115
|
+
fi
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Environment files - NEVER commit secrets
|
|
2
|
+
.env
|
|
3
|
+
.env.local
|
|
4
|
+
.env.*.local
|
|
5
|
+
|
|
6
|
+
# Python
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.so
|
|
11
|
+
.Python
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
ENV/
|
|
15
|
+
|
|
16
|
+
# Django
|
|
17
|
+
*.log
|
|
18
|
+
local_settings.py
|
|
19
|
+
db.sqlite3
|
|
20
|
+
|
|
21
|
+
# Static files (collected)
|
|
22
|
+
staticfiles/
|
|
23
|
+
|
|
24
|
+
# Media uploads
|
|
25
|
+
media/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
|
|
33
|
+
# OS
|
|
34
|
+
.DS_Store
|
|
35
|
+
Thumbs.db
|
|
36
|
+
|
|
37
|
+
# Testing
|
|
38
|
+
.coverage
|
|
39
|
+
htmlcov/
|
|
40
|
+
.pytest_cache/
|
|
41
|
+
|
|
42
|
+
# Build
|
|
43
|
+
dist/
|
|
44
|
+
build/
|
|
45
|
+
*.egg-info/
|