diff --git a/.github/workflows/copilot-instructions.md b/.github/copilot-instructions.md
similarity index 100%
rename from .github/workflows/copilot-instructions.md
rename to .github/copilot-instructions.md
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
index 0e802a5..24b059d 100644
--- a/.github/workflows/docker-publish.yml
+++ b/.github/workflows/docker-publish.yml
@@ -68,11 +68,68 @@ jobs:
SECRET_KEY: test-key
DEBUG: 'False'
ALLOWED_HOSTS: localhost,127.0.0.1,testserver,blog.iohub.link
+ # Run the suite against the same engine as production (service container above)
+ TEST_DB_ENGINE: postgres
+ POSTGRES_HOST: localhost
run: |
python -m pytest -v --cov=techblog_cms --cov-report=xml
+ # Boot the real compose stack (db + redis + django) and hit it end-to-end,
+ # so entrypoint, migrations, cache auth, and the container healthcheck are
+ # proven to work together on every PR. nginx is excluded: its config
+ # requires TLS certificates that only exist on the production host.
+ smoke:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Generate a self-consistent .env
+ run: |
+ cat > .env <<'ENV'
+ SECRET_KEY=ci-smoke-secret-key
+ DEBUG=False
+ ALLOWED_HOSTS=localhost,127.0.0.1,django
+ POSTGRES_DB=techblogdb
+ POSTGRES_USER=techblog
+ POSTGRES_PASSWORD=techblogpass
+ DATABASE_URL=postgres://techblog:techblogpass@db:5432/techblogdb
+ REDIS_PASSWORD=ci-smoke-redis-pass
+ REDIS_URL=redis://redis:6379/1
+ ENV
+
+ - name: Start db, redis, and django
+ run: docker compose up -d --build db redis django
+
+ - name: Wait for the django healthcheck
+ run: |
+ cid=$(docker compose ps -q django)
+ for i in $(seq 1 30); do
+ status=$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo starting)
+ [ "$status" = healthy ] && exit 0
+ sleep 5
+ done
+ echo "django never became healthy (last status: $status)"
+ docker compose logs django
+ exit 1
+
+ - name: Probe the running application
+ run: |
+ docker compose exec -T django python - <<'PY'
+ import requests
+ secure = {'X-Forwarded-Proto': 'https'}
+ for path, headers in [('/health/', {}), ('/ready/', {}), ('/', secure), ('/articles/', secure)]:
+ r = requests.get('http://localhost:8000' + path, headers=headers,
+ allow_redirects=False, timeout=15)
+ print(path, r.status_code)
+ assert r.status_code == 200, f'{path} returned {r.status_code}'
+ PY
+
+ - name: Show logs on failure
+ if: failure()
+ run: docker compose logs django db redis
+
build:
- needs: test
+ needs: [test, smoke]
runs-on: ubuntu-latest
steps:
diff --git a/Dockerfile.django b/Dockerfile.django
index a3622b5..cf58f1c 100644
--- a/Dockerfile.django
+++ b/Dockerfile.django
@@ -42,8 +42,9 @@ COPY . .
# Create a non-root user to run the application.
RUN groupadd -r appgroup && useradd -r -g appgroup appuser
-RUN chown -R appuser:appgroup /app
-RUN mkdir -p /app/logs && chown -R appuser:appgroup /app/logs
+# static/media/logs must exist owned by appuser so fresh named volumes
+# mounted there inherit that ownership on first use.
+RUN mkdir -p /app/static /app/media /app/logs && chown -R appuser:appgroup /app
# Entrypoint to run migrations and collectstatic
RUN chmod +x /app/docker/entrypoint.sh
diff --git a/assitant.agent.prompt.md b/assitant.agent.prompt.md
deleted file mode 100644
index 6bf2c3e..0000000
--- a/assitant.agent.prompt.md
+++ /dev/null
@@ -1,90 +0,0 @@
----
-description: New prompt created from chat session
-mode: agent
-tools: ['extensions', 'codebase', 'usages', 'vscodeAPI', 'problems', 'changes', 'testFailure', 'terminalSelection', 'terminalLastCommand', 'openSimpleBrowser', 'fetch', 'findTestFiles', 'searchResults', 'githubRepo', 'getPythonEnvironmentInfo', 'getPythonExecutableCommand', 'installPythonPackage', 'configurePythonEnvironment', 'runTests', 'runCommands', 'runTasks', 'weather', 'editFiles', 'runNotebooks', 'search', 'new']
-model: Grok Code Fast 1 (Preview)
----
-
-このレポジトリの内容を解析し、どういう実装がされているか解説してください。
-
-
-このレポジトリは、DjangoベースのTech Blog CMS(コンテンツ管理システム)の実装例です。Docker Composeを使用してコンテナ化され、Nginxによるリバースプロキシ、PostgreSQLデータベース、Redisキャッシュ、Let's EncryptによるSSL証明書の自動取得・更新を統合しています。以下に主要な実装内容を解説します。
-
-### 全体アーキテクチャ
-- **Webフレームワーク**: Django 4.2を使用。WSGIアプリケーションとしてGunicornで実行。
-- **コンテナ化**: Docker Composeで複数サービスを管理(Djangoアプリ、Nginx、PostgreSQL、Redis、Certbot)。
-- **セキュリティ**: HTTPS強制、セキュリティヘッダー、CSRF保護、環境変数による設定管理、強力なパスワード要件。
-- **CI/CD**: GitHub Actionsでテスト、ビルド、脆弱性スキャン(Trivy)を自動化。
-- **テスト**: pytestを使用したユニットテストと統合テスト。
-
-### 主要コンポーネントの解説
-1. **Djangoアプリケーション (techblog_cms/)**:
- - **モデル (techblog_cms/models.py)**: Category, Tag, Articleモデルを実装。記事の作成・管理、カテゴリ分類、タグ付け機能をサポート。
- - **ビュー (techblog_cms/views.py)**:
- - `health_check`: JSONレスポンスでヘルスチェック。
- - `home_view`: 公開記事のトップ10を表示。
- - `article_list_view`: 全公開記事を表示。
- - `categories_view`: カテゴリ一覧を表示。
- - `category_view`: カテゴリ別記事を表示。
- - `article_detail_view`: 記事詳細を表示(ログイン時は下書きも表示)。
- - `login_view`: ユーザー認証(CSRF保護付き)。
- - `logout_view`: ログアウト処理。
- - `dashboard_view`: 管理ダッシュボード(ページネーション付き記事一覧)。
- - `article_editor_view`: 記事作成・編集。
- - `article_delete_view`: 記事削除。
- - `admin_guard`: 管理画面アクセス制御。
- - **URL設定 (techblog_cms/urls.py)**: 記事管理、カテゴリ、ログイン/ログアウト、管理画面などのURLマッピング。
- - **設定 (techblog_cms/settings.py)**: 環境変数でDEBUG、ALLOWED_HOSTS、DB接続を制御。PostgreSQLとRedisを統合。テストモードでのCSRF無効化。
- - **テンプレート**:
- - `home.html`: ホーム画面。
- - `article_list.html`: 記事一覧。
- - `category_list.html`: カテゴリ一覧。
- - `category_detail.html`: カテゴリ詳細。
- - `article_detail.html`: 記事詳細。
- - `login.html`: ログインフォーム。
- - `dashboard.html`: 管理ダッシュボード(ページネーション付き)。
- - `article_editor.html`: 記事編集フォーム。
- - `article_delete_confirm.html`: 削除確認。
- - `article_delete_success.html`: 削除成功。
- - **WSGI (techblog_cms/wsgi.py)**: Djangoアプリのエントリーポイント。
-
-2. **Docker設定**:
- - **Djangoコンテナ (Dockerfile.django)**: Python 3.11ベースで依存関係をインストールし、非rootユーザーで実行。
- - **Nginxコンテナ (Dockerfile.nginx)**: 静的ファイル配信とリバースプロキシ。SSL証明書をマウント。
- - **Compose (docker-compose.yml)**: サービス間ネットワーク、ボリューム、環境変数を定義。Certbotで証明書更新を自動化。
-
-3. **データベースとキャッシュ**:
- - PostgreSQL: コンテナで実行され、永続ボリュームでデータを保持。
- - Redis: パスワード認証付きでキャッシュを提供。
-
-4. **SSL証明書管理**:
- - **初期化スクリプト (scripts/init-letsencrypt.sh)**: CertbotでLet's Encrypt証明書を取得し、Nginxに適用。
- - **更新スクリプト (scripts/renew-cert.sh)**: 証明書を定期更新。
-
-5. **テストと品質管理**:
- - **テスト設定 (conftest.py, pytest.ini)**: Djangoテスト環境をSQLiteでセットアップ。
- - **テストケース (techblog_cms/tests/test_views.py)**: ホーム画面のステータスコードとテンプレート使用を検証。
- - **CIパイプライン (.github/workflows/docker-publish.yml)**: プッシュ時にテストを実行し、イメージをビルド・プッシュ。
-
-6. **その他のファイル**:
- - **デプロイスクリプト (deploy.sh)**: Gitプル、コンテナ再ビルド、起動を自動化。
- - **環境設定 (.env.example)**: 秘密鍵やDB接続を環境変数で管理。
- - **ドキュメント (README.md)**: セットアップ手順、技術スタック、ライセンスを記載。
-
-この実装は、プロダクションレディなDjangoアプリのベストプラクティスを反映しており、セキュリティとスケーラビリティを重視しています。記事管理機能、ページネーション、ユーザー認証、CSRF保護などの機能を備えています。
-
-
-AIへの指示とレポジトリの理解のため、AGENTS.mdの作成を行ってください。
-
-
-AGENTS.mdファイルを作成しました。このファイルには、レポジトリの概要、技術スタック、プロジェクト構造、開発ガイドライン、AIアシスタント向けの具体的な指示をまとめています。これにより、AIがこのプロジェクトを効果的に支援できるようになります。
-
-主な内容:
-- レポジトリの目的と技術スタックの説明
-- プロジェクト構造の詳細
-- コーディング標準とセキュリティ考慮事項
-- AIへのコード生成・支援時のガイドライン
-- 貢献プロセス
-
-このファイルは今後、プロジェクトの変更に合わせて更新していくことをおすすめします。
-
diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh
index f2117d8..57f4f2a 100755
--- a/docker/entrypoint.sh
+++ b/docker/entrypoint.sh
@@ -10,16 +10,18 @@ if [ ! -f /app/logs/error.log ]; then
fi
# ログディレクトリの権限設定
+# chown は root 実行時のみ成功する。非 root(compose の user: appuser)では
+# ボリューム所有権はイメージ側の設定(Dockerfile)で継承されるため、失敗しても致命的でない。
echo "Setting up log directory..."
mkdir -p /app/logs
-chown -R appuser:appgroup /app/logs
+chown -R appuser:appgroup /app/logs || true
chmod 755 /app/logs || true
chmod 664 /app/logs/access.log || true
# 静的ファイルディレクトリの作成と権限設定(collectstatic 前に実施)
echo "Preparing static directory..."
mkdir -p /app/static
-chown -R appuser:appgroup /app/static
+chown -R appuser:appgroup /app/static || true
chmod -R 755 /app/static || true
# -------------------------------------------
diff --git a/techblog_cms/context_processors.py b/techblog_cms/context_processors.py
index 0a0e71c..e219fdb 100644
--- a/techblog_cms/context_processors.py
+++ b/techblog_cms/context_processors.py
@@ -1,6 +1,3 @@
-import os
-import sys
-
from django.core.cache import cache
from .models import Category, Tag
@@ -8,13 +5,6 @@
SIDEBAR_CACHE_TTL = 300
-def testing_mode(request):
- """Add IS_TESTING variable to template context"""
- IS_TESTING = os.environ.get('TESTING') == 'True' or 'PYTEST_CURRENT_TEST' in os.environ or any(
- x.endswith('pytest') for x in sys.modules.keys()
- )
- return {'IS_TESTING': IS_TESTING}
-
def sidebar(request):
categories = cache.get('sidebar:categories')
diff --git a/techblog_cms/settings.py b/techblog_cms/settings.py
index 331e7a9..9a4ec72 100644
--- a/techblog_cms/settings.py
+++ b/techblog_cms/settings.py
@@ -55,7 +55,6 @@
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
- 'techblog_cms.context_processors.testing_mode',
'techblog_cms.context_processors.sidebar',
],
},
@@ -66,19 +65,32 @@
# Database
# Detect testing mode either via explicit env var or when running under pytest
-IS_TESTING = os.environ.get('TESTING') == 'True' or 'PYTEST_CURRENT_TEST' in os.environ or any(
+IS_TESTING = os.environ.get('TESTING') == 'True' or 'PYTEST_CURRENT_TEST' in os.environ or 'test' in sys.argv or any(
x.endswith('pytest') for x in sys.modules.keys()
)
logger.debug("IS_TESTING: %s", IS_TESTING)
if IS_TESTING:
- # Testing uses SQLite and in-memory cache for simplicity and isolation.
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
+ # Tests default to SQLite for speed; set TEST_DB_ENGINE=postgres (as CI
+ # does) to run them against the same engine as production.
+ if os.environ.get('TEST_DB_ENGINE') == 'postgres':
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.postgresql',
+ 'NAME': os.environ.get('POSTGRES_DB', 'techblogdb'),
+ 'USER': os.environ.get('POSTGRES_USER', 'techblog'),
+ 'PASSWORD': os.environ.get('POSTGRES_PASSWORD', 'techblogpass'),
+ 'HOST': os.environ.get('POSTGRES_HOST', 'localhost'),
+ 'PORT': os.environ.get('POSTGRES_PORT', '5432'),
+ }
+ }
+ else:
+ DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:',
+ }
}
- }
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
@@ -86,6 +98,9 @@
}
}
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
+ PASSWORD_HASHERS = [
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ ]
DEBUG = True
APPEND_SLASH = False
else:
@@ -321,13 +336,3 @@
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD', default='')
DEFAULT_FROM_EMAIL = config('DEFAULT_FROM_EMAIL', default='noreply@techblog.com')
ADMINS = [('Admin', config('ADMIN_EMAIL', default='admin@techblog.com'))]
-
-# Testing configuration
-if 'test' in sys.argv:
- DATABASES['default'] = {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
- }
- PASSWORD_HASHERS = [
- 'django.contrib.auth.hashers.MD5PasswordHasher',
- ]
diff --git a/techblog_cms/templates/article_editor.html b/techblog_cms/templates/article_editor.html
index df96afd..ed9730a 100644
--- a/techblog_cms/templates/article_editor.html
+++ b/techblog_cms/templates/article_editor.html
@@ -12,9 +12,7 @@
{% if article %}記事編集{% else %}記
{% endif %}