コンテナイメージ公開CIを追加
Validate and publish container image / build (pull_request) Failing after 1m31s

This commit is contained in:
2026-07-18 08:42:04 +09:00
parent 8fe18971f0
commit c14716c46b
9 changed files with 202 additions and 28 deletions
+9
View File
@@ -0,0 +1,9 @@
.git
.gitea
.env
.venv
__pycache__
*.py[cod]
README.md
start.sh
start.ps1
+54
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim FROM python:3.11-slim AS base
# 作業ディレクトリを設定 # 作業ディレクトリを設定
WORKDIR /app WORKDIR /app
@@ -15,14 +15,23 @@ RUN pip install --no-cache-dir -r requirements.txt
# アプリケーションファイルをコピー # アプリケーションファイルをコピー
COPY app.py . 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ユーザーを作成 # 非rootユーザーを作成
RUN useradd --create-home --shell /bin/bash appuser RUN useradd --create-home --shell /bin/bash appuser \
RUN chown -R appuser:appuser /app && chown -R appuser:appuser /app
USER appuser USER appuser
# ヘルスチェックを追加 # ヘルスチェックを追加
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ 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"] CMD ["python", "app.py"]
+43 -4
View File
@@ -80,7 +80,46 @@ cp .env.example .env
# .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` の設定 ### 1. `.env` の設定
@@ -101,13 +140,13 @@ DISCORD_CHANNEL_ID=123456789012345678
```bash ```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用シェルスクリプトでの実行 ## 🚀 Linux用シェルスクリプトでの実行
+22
View File
@@ -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
View File
@@ -1,21 +1,27 @@
version: '3.8'
services: services:
email-to-discord: email-to-discord:
build: . image: harbor.mukan.0am.jp/services/email-to-discord:latest
container_name: email-to-discord-monitor
restart: unless-stopped restart: unless-stopped
env_file: extra_hosts:
- .env - "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: logging:
driver: "json-file" driver: "json-file"
options: options:
max-size: "10m" max-size: "10m"
max-file: "3" max-file: "3"
# リソース制限
deploy: deploy:
resources: resources:
limits: limits:
+3 -3
View File
@@ -225,7 +225,7 @@ function Invoke-Cleanup {
# docker-compose で起動 # docker-compose で起動
function Start-Compose { function Start-Compose {
Log-Info "docker-compose でサービスを起動しています..." Log-Info "docker-compose でサービスを起動しています..."
docker-compose up -d docker-compose -f docker-compose.local.yml up --build -d
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
Log-Info "サービスが起動しました" Log-Info "サービスが起動しました"
Log-Blue "ログを確認するには: .\start.ps1 compose-logs" Log-Blue "ログを確認するには: .\start.ps1 compose-logs"
@@ -238,7 +238,7 @@ function Start-Compose {
# docker-compose で停止 # docker-compose で停止
function Stop-Compose { function Stop-Compose {
Log-Info "docker-compose でサービスを停止しています..." Log-Info "docker-compose でサービスを停止しています..."
docker-compose down docker-compose -f docker-compose.local.yml down
Log-Info "サービスを停止しました" Log-Info "サービスを停止しました"
} }
@@ -322,7 +322,7 @@ switch ($Action.ToLower()) {
Stop-Compose Stop-Compose
} }
"compose-logs" { "compose-logs" {
docker-compose logs -f docker-compose -f docker-compose.local.yml logs -f
} }
"python" { "python" {
Start-Python Start-Python
+6 -6
View File
@@ -230,9 +230,9 @@ compose_up() {
log_info "docker-compose でサービスを起動しています..." log_info "docker-compose でサービスを起動しています..."
if command -v docker-compose &> /dev/null; then if command -v docker-compose &> /dev/null; then
docker-compose up -d docker-compose -f docker-compose.local.yml up --build -d
else else
docker compose up -d docker compose -f docker-compose.local.yml up --build -d
fi fi
log_info "サービスが起動しました" log_info "サービスが起動しました"
@@ -244,9 +244,9 @@ compose_down() {
log_info "docker-compose でサービスを停止しています..." log_info "docker-compose でサービスを停止しています..."
if command -v docker-compose &> /dev/null; then if command -v docker-compose &> /dev/null; then
docker-compose down docker-compose -f docker-compose.local.yml down
else else
docker compose down docker compose -f docker-compose.local.yml down
fi fi
log_info "サービスを停止しました" log_info "サービスを停止しました"
@@ -327,9 +327,9 @@ main() {
;; ;;
"compose-logs") "compose-logs")
if command -v docker-compose &> /dev/null; then if command -v docker-compose &> /dev/null; then
docker-compose logs -f docker-compose -f docker-compose.local.yml logs -f
else else
docker compose logs -f docker compose -f docker-compose.local.yml logs -f
fi fi
;; ;;
"python") "python")
+35
View File
@@ -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()