This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
.git
|
||||
.gitea
|
||||
.env
|
||||
.venv
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
README.md
|
||||
start.sh
|
||||
start.ps1
|
||||
@@ -0,0 +1,54 @@
|
||||
name: Validate and publish container image
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
REGISTRY: harbor.mukan.0am.jp
|
||||
IMAGE_NAME: harbor.mukan.0am.jp/services/email-to-discord
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build test stage
|
||||
run: |
|
||||
docker build \
|
||||
--target test \
|
||||
--tag "${IMAGE_NAME}:test-${{ gitea.sha }}" \
|
||||
.
|
||||
|
||||
- name: Build runtime image
|
||||
run: |
|
||||
docker build \
|
||||
--target runtime \
|
||||
--tag "${IMAGE_NAME}:${{ gitea.sha }}" \
|
||||
--tag "${IMAGE_NAME}:latest" \
|
||||
.
|
||||
|
||||
- name: Log in to Harbor
|
||||
if: gitea.event_name == 'push'
|
||||
env:
|
||||
HARBOR_USERNAME: ${{ secrets.HARBOR_USERNAME }}
|
||||
HARBOR_PASSWORD: ${{ secrets.HARBOR_PASSWORD }}
|
||||
run: |
|
||||
printf '%s' "${HARBOR_PASSWORD}" | docker login "${REGISTRY}" \
|
||||
--username "${HARBOR_USERNAME}" \
|
||||
--password-stdin
|
||||
|
||||
- name: Push runtime image
|
||||
if: gitea.event_name == 'push'
|
||||
run: |
|
||||
docker push "${IMAGE_NAME}:${{ gitea.sha }}"
|
||||
docker push "${IMAGE_NAME}:latest"
|
||||
+13
-4
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.11-slim AS base
|
||||
|
||||
# 作業ディレクトリを設定
|
||||
WORKDIR /app
|
||||
@@ -15,14 +15,23 @@ RUN pip install --no-cache-dir -r requirements.txt
|
||||
# アプリケーションファイルをコピー
|
||||
COPY app.py .
|
||||
|
||||
# テスト用ステージ(CIではこのステージまでビルドして単体テストを実行)
|
||||
FROM base AS test
|
||||
COPY docker-compose.yml docker-compose.local.yml ./
|
||||
COPY tests ./tests
|
||||
RUN python -m unittest discover -s tests -v
|
||||
|
||||
# 実行用ステージ
|
||||
FROM base AS runtime
|
||||
|
||||
# 非rootユーザーを作成
|
||||
RUN useradd --create-home --shell /bin/bash appuser
|
||||
RUN chown -R appuser:appuser /app
|
||||
RUN useradd --create-home --shell /bin/bash appuser \
|
||||
&& chown -R appuser:appuser /app
|
||||
USER appuser
|
||||
|
||||
# ヘルスチェックを追加
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD python -c "import sys; sys.exit(0)"
|
||||
CMD python -c "from pathlib import Path; import sys; sys.exit(0 if b'app.py' in Path('/proc/1/cmdline').read_bytes() else 1)"
|
||||
|
||||
# アプリケーションを実行
|
||||
CMD ["python", "app.py"]
|
||||
@@ -80,7 +80,46 @@ cp .env.example .env
|
||||
# .envファイルを編集して実際の値を設定
|
||||
```
|
||||
|
||||
## 🐳 Docker Composeでの実行
|
||||
## 🚢 Agents `container_tools` へのデプロイ
|
||||
|
||||
`container_tools` はイメージをビルドせず、レジストリからpullして起動します。このリポジトリでは、`main` へのpush時にGitea Actionsが次のイメージをHarborへ公開します。
|
||||
|
||||
```text
|
||||
harbor.mukan.0am.jp/services/email-to-discord:<commit-sha>
|
||||
harbor.mukan.0am.jp/services/email-to-discord:latest
|
||||
```
|
||||
|
||||
### 1. Gitea Actions Secretsの設定
|
||||
|
||||
リポジトリのActions Secretsへ次の2項目を登録します。実際の値をリポジトリやComposeへコミットしないでください。
|
||||
|
||||
- `HARBOR_USERNAME`
|
||||
- `HARBOR_PASSWORD`
|
||||
|
||||
Pull Requestではテストステージと実行イメージのビルドだけを行い、`main` へマージされたときだけHarborへpushします。
|
||||
|
||||
### 2. デプロイ用Composeの準備
|
||||
|
||||
通常の [`docker-compose.yml`](docker-compose.yml) は `container_tools` 用テンプレートです。次の値を実際のデプロイ情報へ置き換えます。
|
||||
|
||||
- イメージタグの `latest`(可能ならGitea Actionsが公開したコミットSHAへ変更)
|
||||
- `REPLACE_WITH_EMAIL_USER`
|
||||
- `REPLACE_WITH_EMAIL_PASSWORD`
|
||||
- `REPLACE_WITH_WEBHOOK_URL`
|
||||
- `REPLACE_WITH_WEBHOOK_TOKEN`
|
||||
- `REPLACE_WITH_DISCORD_CHANNEL_ID`
|
||||
|
||||
Discordへの直接通知も併用する場合だけ、空の `DISCORD_WEBHOOK_URL` に実際のURLを設定します。`WEBHOOK_SECRET` は設定しません。
|
||||
|
||||
Agentsの実行時情報にある `WEBHOOK_URL` が `http://127.0.0.1:<port>/webhook` の場合、コンテナ内の `127.0.0.1` はサテライト自身を指すため、`http://host.docker.internal:<port>/webhook` に置き換えてください。ComposeにはLinuxからDockerホストへ到達するための `host-gateway` を設定済みです。
|
||||
|
||||
### 3. デプロイ
|
||||
|
||||
置換後のCompose全文を、安定した `project_name` とともに `container_tools` へ渡します。デプロイ用Composeには、`container_tools` が禁止する `build`、`container_name`、`env_file`、host bind mountを含めていません。
|
||||
|
||||
## 🐳 ローカルDocker Composeでの実行
|
||||
|
||||
ローカルでソースからビルドする場合は、デプロイ用とは別の `docker-compose.local.yml` を使用します。
|
||||
|
||||
### 1. `.env` の設定
|
||||
|
||||
@@ -101,13 +140,13 @@ DISCORD_CHANNEL_ID=123456789012345678
|
||||
|
||||
```bash
|
||||
# コンテナをビルドして起動
|
||||
docker-compose up -d
|
||||
docker compose -f docker-compose.local.yml up --build -d
|
||||
|
||||
# ログの確認
|
||||
docker-compose logs -f
|
||||
docker compose -f docker-compose.local.yml logs -f
|
||||
|
||||
# コンテナの停止
|
||||
docker-compose down
|
||||
docker compose -f docker-compose.local.yml down
|
||||
```
|
||||
|
||||
## 🚀 Linux用シェルスクリプトでの実行
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
email-to-discord:
|
||||
build:
|
||||
context: .
|
||||
target: runtime
|
||||
container_name: email-to-discord-monitor
|
||||
restart: unless-stopped
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
env_file:
|
||||
- .env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
reservations:
|
||||
memory: 128M
|
||||
+16
-10
@@ -1,21 +1,27 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
email-to-discord:
|
||||
build: .
|
||||
container_name: email-to-discord-monitor
|
||||
image: harbor.mukan.0am.jp/services/email-to-discord:latest
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
|
||||
# ログの設定
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
environment:
|
||||
IMAP_SERVER: imap.gmail.com
|
||||
IMAP_PORT: "993"
|
||||
USE_SSL: "true"
|
||||
MAILBOX: INBOX
|
||||
EMAIL_USER: REPLACE_WITH_EMAIL_USER
|
||||
EMAIL_PASSWORD: REPLACE_WITH_EMAIL_PASSWORD
|
||||
DISCORD_WEBHOOK_URL: ""
|
||||
WEBHOOK_URL: REPLACE_WITH_WEBHOOK_URL
|
||||
WEBHOOK_TOKEN: REPLACE_WITH_WEBHOOK_TOKEN
|
||||
DISCORD_CHANNEL_ID: "REPLACE_WITH_DISCORD_CHANNEL_ID"
|
||||
WEBHOOK_SOURCE: email-monitor
|
||||
CHECK_INTERVAL: "60"
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# リソース制限
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
|
||||
@@ -225,7 +225,7 @@ function Invoke-Cleanup {
|
||||
# docker-compose で起動
|
||||
function Start-Compose {
|
||||
Log-Info "docker-compose でサービスを起動しています..."
|
||||
docker-compose up -d
|
||||
docker-compose -f docker-compose.local.yml up --build -d
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Log-Info "サービスが起動しました"
|
||||
Log-Blue "ログを確認するには: .\start.ps1 compose-logs"
|
||||
@@ -238,7 +238,7 @@ function Start-Compose {
|
||||
# docker-compose で停止
|
||||
function Stop-Compose {
|
||||
Log-Info "docker-compose でサービスを停止しています..."
|
||||
docker-compose down
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
Log-Info "サービスを停止しました"
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ switch ($Action.ToLower()) {
|
||||
Stop-Compose
|
||||
}
|
||||
"compose-logs" {
|
||||
docker-compose logs -f
|
||||
docker-compose -f docker-compose.local.yml logs -f
|
||||
}
|
||||
"python" {
|
||||
Start-Python
|
||||
|
||||
@@ -230,9 +230,9 @@ compose_up() {
|
||||
log_info "docker-compose でサービスを起動しています..."
|
||||
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
docker-compose up -d
|
||||
docker-compose -f docker-compose.local.yml up --build -d
|
||||
else
|
||||
docker compose up -d
|
||||
docker compose -f docker-compose.local.yml up --build -d
|
||||
fi
|
||||
|
||||
log_info "サービスが起動しました"
|
||||
@@ -244,9 +244,9 @@ compose_down() {
|
||||
log_info "docker-compose でサービスを停止しています..."
|
||||
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
docker-compose down
|
||||
docker-compose -f docker-compose.local.yml down
|
||||
else
|
||||
docker compose down
|
||||
docker compose -f docker-compose.local.yml down
|
||||
fi
|
||||
|
||||
log_info "サービスを停止しました"
|
||||
@@ -327,9 +327,9 @@ main() {
|
||||
;;
|
||||
"compose-logs")
|
||||
if command -v docker-compose &> /dev/null; then
|
||||
docker-compose logs -f
|
||||
docker-compose -f docker-compose.local.yml logs -f
|
||||
else
|
||||
docker compose logs -f
|
||||
docker compose -f docker-compose.local.yml logs -f
|
||||
fi
|
||||
;;
|
||||
"python")
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import re
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ContainerToolsDeploymentTests(unittest.TestCase):
|
||||
def test_deployment_compose_uses_prebuilt_image_only(self):
|
||||
compose = (REPOSITORY_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
|
||||
self.assertIn(
|
||||
"image: harbor.mukan.0am.jp/services/email-to-discord:latest",
|
||||
compose,
|
||||
)
|
||||
self.assertIn("host.docker.internal:host-gateway", compose)
|
||||
for forbidden_key in ("build", "container_name", "env_file"):
|
||||
self.assertIsNone(
|
||||
re.search(rf"^\s+{forbidden_key}:", compose, flags=re.MULTILINE),
|
||||
f"container_toolsで禁止されている'{forbidden_key}'が含まれています",
|
||||
)
|
||||
|
||||
def test_local_compose_keeps_source_build_support(self):
|
||||
compose = (REPOSITORY_ROOT / "docker-compose.local.yml").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
self.assertRegex(compose, r"(?m)^\s+build:")
|
||||
self.assertIn("target: runtime", compose)
|
||||
self.assertIn("host.docker.internal:host-gateway", compose)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user