Author SHA1 Message Date
westfarn 07cba8bc7f Add unit tests ensuring /preview_email/ requires authentication.
CI / test (pull_request) Successful in 11s
Unit Tests / test (pull_request) Successful in 10s
Locks in login_required behavior for issue #16 so unauthenticated access stays redirected.
2026-07-10 12:53:17 -05:00
westfarn 8ccf17655d updateing deploy for westfarn user
Unit Tests / test (push) Successful in 11s
2026-07-08 12:26:07 -05:00
westfarn 04d842d799 Test who runs the job
Unit Tests / test (push) Successful in 10s
2026-07-08 12:21:50 -05:00
westfarn ae0f8a8bc5 Updated deploy to use act_runner keys
Deploy Company Site / test (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
Deploy Company Site / docker (push) Successful in 16s
Deploy Company Site / deploy (push) Failing after 0s
2026-07-08 11:51:33 -05:00
westfarn 16dfb3faae Updated deploy to use act_runner keys
Unit Tests / test (push) Successful in 10s
2026-07-08 11:46:43 -05:00
westfarn a5fa08d4a0 update docker unittests
Unit Tests / test (push) Successful in 10s
2026-07-08 06:12:21 -05:00
westfarn 9a383c0ee9 Update workflow to use server-infra
Unit Tests / test (push) Successful in 10s
2026-07-08 06:05:11 -05:00
westfarn 6f97f6084d Adding images
Unit Tests / test (push) Successful in 11s
2026-07-07 15:28:45 -05:00
westfarn 426cc82f04 Dockerize Django app with dev/beta/prod env config and uv (#6)
Unit Tests / test (push) Successful in 10s
## Summary

- Containerize the Django app with Docker and docker-compose (dev + production)
- Refactor settings into `dev` / `beta` / `prod` environments driven by environment variables
- Connect to PostgreSQL via `DATABASE_URL` or `DB_*` vars
- Migrate package management from pip to uv (`pyproject.toml`, `uv.lock`)
- Split Gitea workflows: PRs run unit tests only; pushes to `master` run tests, Docker validation, and deploy
- Update deploy script to rsync code, preserve server `.env`, validate config, and run Docker compose

Closes #4

## Test plan

- [x] `uv run python manage.py test` passes locally (10/10)
- [x] `DJANGO_ENV=beta` and `DJANGO_ENV=prod` load with correct logging levels
- [x] `scripts/validate-env.sh` rejects missing production variables
- [ ] `docker compose up --build` starts app + Postgres locally
- [ ] Containerized unit tests pass in CI Docker job
- [ ] Server `.env` created from `.env.prod.example` before first production deploy
- [ ] CI workflow runs on this PR (tests only, no deploy)

Reviewed-on: #6
2026-07-07 11:23:50 -07:00
8 changed files with 102 additions and 43 deletions
+13 -32
View File
@@ -1,6 +1,6 @@
name: Deploy Company Site name: Deploy Company Site
# Deploy pipeline runs only on pushes to master (never on pull requests). # Runs after Unit Tests completes on master. Direct pushes only (not PRs).
on: on:
workflow_run: workflow_run:
workflows: [Unit Tests] workflows: [Unit Tests]
@@ -8,34 +8,14 @@ on:
branches: [master] branches: [master]
jobs: jobs:
test:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install dependencies
run: uv sync --frozen
- name: Run unit tests
env:
DJANGO_ENV: dev
DJANGO_SECRET_KEY: test-secret-key
run: |
cd company_site
uv run python manage.py test
docker: docker:
if: gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push'
runs-on: self-hosted runs-on: self-hosted
needs: test
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ gitea.event.workflow_run.head_sha }}
- name: Build Docker image - name: Build Docker image
run: docker compose build run: docker compose build
@@ -53,12 +33,13 @@ jobs:
deploy: deploy:
if: gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push' if: gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push'
runs-on: self-hosted runs-on: self-hosted
needs: [test, docker] needs: docker
env:
SERVER_INFRA_ROOT: /home/westfarn/Documents/repos/server-infra
steps: steps:
- name: Checkout - name: Deploy company_site prod
uses: actions/checkout@v4 run: |
with: "${SERVER_INFRA_ROOT}/scripts/deploy.sh" \
ref: ${{ gitea.event.workflow_run.head_sha }} --app company_site \
--env prod \
- name: Deploy to live site --ref "${{ gitea.event.workflow_run.head_sha }}"
run: bash scripts/deploy.sh "${{ gitea.workspace }}"
+10 -5
View File
@@ -13,13 +13,18 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Python environment - name: Install uv
run: | run: |
python3 -m venv .venv curl -LsSf https://astral.sh/uv/install.sh | sh
.venv/bin/pip install --upgrade pip echo "$HOME/.local/bin" >> "$GITHUB_PATH"
.venv/bin/pip install -r requirements.txt
- name: Install dependencies
run: uv sync --frozen
- name: Run unit tests - name: Run unit tests
env:
DJANGO_ENV: dev
DJANGO_SECRET_KEY: test-secret-key
run: | run: |
cd company_site cd company_site
../.venv/bin/python manage.py test uv run python manage.py test
+11 -1
View File
@@ -1,5 +1,6 @@
"""Shared Django settings for all environments.""" """Shared Django settings for all environments."""
import json
import os import os
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -22,6 +23,15 @@ def env_list(key: str, default: str = "") -> list[str]:
value = os.environ.get(key, default) value = os.environ.get(key, default)
if not value: if not value:
return [] return []
value = value.strip()
# Accept a JSON array (e.g. '["a","b"]') as well as a comma-separated list.
if value.startswith("["):
try:
parsed = json.loads(value)
except ValueError:
parsed = None
if isinstance(parsed, list):
return [str(item).strip() for item in parsed if str(item).strip()]
return [item.strip() for item in value.split(",") if item.strip()] return [item.strip() for item in value.split(",") if item.strip()]
@@ -147,7 +157,7 @@ STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles" STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = { STORAGES = {
"staticfiles": { "staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", "BACKEND": "company_site.storage.TolerantManifestStaticFilesStorage",
}, },
} }
+32
View File
@@ -0,0 +1,32 @@
"""Custom static files storage.
WhiteNoise's manifest storage post-processes JS/CSS during ``collectstatic`` and
strictly resolves every referenced file, including ``sourceMappingURL`` comments
in vendored bundles. Some third-party assets reference ``.map`` files that are
not shipped, which makes ``collectstatic`` fail hard.
``TolerantManifestStaticFilesStorage`` leaves such unresolved references
untouched instead of raising, so a missing source map can't break the build.
"""
from whitenoise.storage import CompressedManifestStaticFilesStorage
class TolerantManifestStaticFilesStorage(CompressedManifestStaticFilesStorage):
# Don't 500 at runtime when a {% static %} reference isn't in the manifest;
# fall back to the plain name (mirrors non-manifest storage behaviour).
manifest_strict = False
def _stored_name(self, name, hashed_files):
"""Tolerate missing references during collectstatic post-processing."""
try:
return super()._stored_name(name, hashed_files)
except ValueError:
return name
def stored_name(self, name):
"""Tolerate missing manifest entries at request time."""
try:
return super().stored_name(name)
except ValueError:
return name
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+29 -1
View File
@@ -1,12 +1,40 @@
from unittest.mock import patch from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import Client, TestCase, override_settings from django.test import Client, TestCase, override_settings
from django.urls import reverse from django.urls import reverse
from .models import Contact from .models import Contact, EmailMessage
from .seo import SERVICE_URL_NAMES, get_service_entries from .seo import SERVICE_URL_NAMES, get_service_entries
class PreviewEmailAuthTests(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(username="previewer", password="pass")
self.email = EmailMessage.objects.create(
subject="Preview subject",
body="Preview body content",
recipient="recipient@example.com",
)
self.url = reverse("preview_email", kwargs={"pk": self.email.pk})
def test_unauthenticated_user_is_redirected_to_login(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login/", response.url)
def test_authenticated_user_can_preview_email(self):
self.client.login(username="previewer", password="pass")
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Preview subject")
self.assertContains(response, "Preview body content")
@override_settings( @override_settings(
DEBUG=True, DEBUG=True,
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend", EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
+7 -4
View File
@@ -18,11 +18,14 @@ services:
build: . build: .
ports: ports:
- "8000:8000" - "8000:8000"
env_file: # No required env_file — CI has no .env. Defaults below; for local secrets:
- .env # docker compose --env-file .env up
environment: environment:
DJANGO_ENV: dev DJANGO_ENV: ${DJANGO_ENV:-dev}
DATABASE_URL: postgres://company_site:company_site@db:5432/company_site DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-only-change-me}
DJANGO_DEBUG: ${DJANGO_DEBUG:-true}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}
DATABASE_URL: ${DATABASE_URL:-postgres://company_site:company_site@db:5432/company_site}
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy