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.
Files changed (72) hide show
  1. sum/__init__.py +1 -0
  2. sum/boilerplate/.env.example +124 -0
  3. sum/boilerplate/.gitea/workflows/ci.yml +33 -0
  4. sum/boilerplate/.gitea/workflows/deploy-production.yml +98 -0
  5. sum/boilerplate/.gitea/workflows/deploy-staging.yml +113 -0
  6. sum/boilerplate/.github/workflows/ci.yml +36 -0
  7. sum/boilerplate/.github/workflows/deploy-production.yml +102 -0
  8. sum/boilerplate/.github/workflows/deploy-staging.yml +115 -0
  9. sum/boilerplate/.gitignore +45 -0
  10. sum/boilerplate/README.md +259 -0
  11. sum/boilerplate/manage.py +34 -0
  12. sum/boilerplate/project_name/__init__.py +5 -0
  13. sum/boilerplate/project_name/home/__init__.py +5 -0
  14. sum/boilerplate/project_name/home/apps.py +20 -0
  15. sum/boilerplate/project_name/home/management/__init__.py +0 -0
  16. sum/boilerplate/project_name/home/management/commands/__init__.py +0 -0
  17. sum/boilerplate/project_name/home/management/commands/populate_demo_content.py +644 -0
  18. sum/boilerplate/project_name/home/management/commands/seed.py +129 -0
  19. sum/boilerplate/project_name/home/management/commands/seed_showroom.py +1661 -0
  20. sum/boilerplate/project_name/home/migrations/__init__.py +3 -0
  21. sum/boilerplate/project_name/home/models.py +13 -0
  22. sum/boilerplate/project_name/settings/__init__.py +5 -0
  23. sum/boilerplate/project_name/settings/base.py +348 -0
  24. sum/boilerplate/project_name/settings/local.py +78 -0
  25. sum/boilerplate/project_name/settings/production.py +106 -0
  26. sum/boilerplate/project_name/urls.py +33 -0
  27. sum/boilerplate/project_name/wsgi.py +16 -0
  28. sum/boilerplate/pytest.ini +5 -0
  29. sum/boilerplate/requirements.txt +25 -0
  30. sum/boilerplate/static/client/.gitkeep +3 -0
  31. sum/boilerplate/templates/overrides/.gitkeep +3 -0
  32. sum/boilerplate/tests/__init__.py +3 -0
  33. sum/boilerplate/tests/test_health.py +51 -0
  34. sum/cli.py +42 -0
  35. sum/commands/__init__.py +10 -0
  36. sum/commands/backup.py +308 -0
  37. sum/commands/check.py +128 -0
  38. sum/commands/init.py +265 -0
  39. sum/commands/promote.py +758 -0
  40. sum/commands/run.py +96 -0
  41. sum/commands/themes.py +56 -0
  42. sum/commands/update.py +301 -0
  43. sum/config.py +61 -0
  44. sum/docs/USER_GUIDE.md +663 -0
  45. sum/exceptions.py +45 -0
  46. sum/setup/__init__.py +17 -0
  47. sum/setup/auth.py +184 -0
  48. sum/setup/database.py +58 -0
  49. sum/setup/deps.py +73 -0
  50. sum/setup/git_ops.py +463 -0
  51. sum/setup/infrastructure.py +576 -0
  52. sum/setup/orchestrator.py +354 -0
  53. sum/setup/remote_themes.py +371 -0
  54. sum/setup/scaffold.py +500 -0
  55. sum/setup/seed.py +110 -0
  56. sum/setup/site_orchestrator.py +441 -0
  57. sum/setup/venv.py +89 -0
  58. sum/system_config.py +330 -0
  59. sum/themes_registry.py +180 -0
  60. sum/utils/__init__.py +25 -0
  61. sum/utils/django.py +97 -0
  62. sum/utils/environment.py +76 -0
  63. sum/utils/output.py +78 -0
  64. sum/utils/project.py +110 -0
  65. sum/utils/prompts.py +36 -0
  66. sum/utils/validation.py +313 -0
  67. sum_cli-3.0.0.dist-info/METADATA +127 -0
  68. sum_cli-3.0.0.dist-info/RECORD +72 -0
  69. sum_cli-3.0.0.dist-info/WHEEL +5 -0
  70. sum_cli-3.0.0.dist-info/entry_points.txt +2 -0
  71. sum_cli-3.0.0.dist-info/licenses/LICENSE +29 -0
  72. 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/