From 095927f5d0cf982b57ff908b063f243529f6b73e Mon Sep 17 00:00:00 2001 From: mifi Date: Thu, 12 Feb 2026 17:52:59 -0300 Subject: [PATCH] Update compose file; add Woodpecker pipelines --- .woodpecker/build.yml | 146 +++++++++++++++++++++++++++++++++++++++++ .woodpecker/ci.yml | 89 +++++++++++++++++++++++++ .woodpecker/deploy.yml | 70 ++++++++++++++++++++ docker-compose.yml | 45 ++++++++++++- 4 files changed, 349 insertions(+), 1 deletion(-) create mode 100644 .woodpecker/build.yml create mode 100644 .woodpecker/ci.yml create mode 100644 .woodpecker/deploy.yml diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml new file mode 100644 index 0000000..1ed537e --- /dev/null +++ b/.woodpecker/build.yml @@ -0,0 +1,146 @@ +# Deploy: build image, push to registry, trigger Portainer stack redeploy. +# Runs on push/tag/manual to main only, after ci workflow succeeds. +when: + - branch: main + event: [push, tag, manual] + - event: deployment + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' + +depends_on: + - ci + +steps: + - name: Docker image build + image: docker:latest + environment: + REGISTRY_REPO: git.mifi.dev/mifi-holdings/mail-postfixadmin + DOCKER_API_VERSION: '1.43' + DOCKER_BUILDKIT: '1' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e + - echo "=== Building Docker image (BuildKit) ===" + - 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"' + - 'echo "Registry repo: $REGISTRY_REPO"' + - | + build() { + docker build \ + --progress=plain \ + --platform=linux/amd64 \ + --tag $REGISTRY_REPO:${CI_COMMIT_SHA} \ + --tag $REGISTRY_REPO:latest \ + --label "git.commit=${CI_COMMIT_SHA}" \ + --label "git.branch=${CI_COMMIT_BRANCH}" \ + . + } + for attempt in 1 2 3; do + echo "Build attempt $attempt/3" + if build; then + echo "✓ Docker image built successfully" + exit 0 + fi + echo "Build attempt $attempt failed, retrying in 30s..." + sleep 30 + done + echo "All build attempts failed" + exit 1 + + - name: Send Docker Image Build Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Docker image build + when: + - status: [success] + + - name: Send Docker Image Build Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Docker image build failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Docker image build + when: + - status: [failure] + + - name: Push to registry + image: docker:latest + environment: + DOCKER_API_VERSION: '1.43' + REGISTRY_URL: git.mifi.dev + REGISTRY_REPO: git.mifi.dev/mifi-holdings/mail-postfixadmin + REGISTRY_USERNAME: + from_secret: gitea_registry_username + REGISTRY_PASSWORD: + from_secret: gitea_package_token + volumes: + - /var/run/docker.sock:/var/run/docker.sock + commands: + - set -e + - echo "=== Pushing to registry ===" + - 'echo "Registry: $REGISTRY_URL"' + - 'echo "Repository: $REGISTRY_REPO"' + - | + echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \ + -u "$REGISTRY_USERNAME" \ + --password-stdin + - docker push $REGISTRY_REPO:${CI_COMMIT_SHA} + - docker push $REGISTRY_REPO:latest + - echo "✓ Images pushed successfully" + depends_on: + - Docker image build + + - name: Send Push to Registry Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Push to registry + when: + - status: [success] + + - name: Send Push to Registry Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Push to registry failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Push to registry + when: + - status: [failure] diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml new file mode 100644 index 0000000..d2e1fba --- /dev/null +++ b/.woodpecker/ci.yml @@ -0,0 +1,89 @@ +# CI: lint and format check. Runs on every PR and every push to main. +when: + - event: pull_request + - branch: main + event: push + +steps: + - name: install + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + + - name: Prettier Format Check + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm format:check + depends_on: + - install + + - name: Send Prettier Format Check Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Prettier Format Check failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - Prettier Format Check + when: + - status: [failure] + + - name: Lint Check + image: node:22-alpine + commands: + - corepack enable + - corepack prepare pnpm@10.29.2 --activate + - pnpm install --frozen-lockfile + - pnpm lint + depends_on: + - install + + - name: Send Lint Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Lint failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - Lint Check + when: + - status: [failure] + + - name: Send CI Pipeline Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_tests_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] CI pipeline success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" $MATTERMOST_POST_API_URL + depends_on: + - install + - Prettier Format Check + - Lint Check + when: + - status: [success] diff --git a/.woodpecker/deploy.yml b/.woodpecker/deploy.yml new file mode 100644 index 0000000..7055f94 --- /dev/null +++ b/.woodpecker/deploy.yml @@ -0,0 +1,70 @@ +# Deploy: build image, push to registry, trigger Portainer stack redeploy. +# Runs on push/tag/manual to main only, after ci workflow succeeds. +skip_clone: true +# Use writable workspace when clone is skipped (no root clone step to create /woodpecker/src) +workspace: + base: /tmp + path: deploy +when: + - branch: main + event: [push, tag, manual] + - event: deployment + evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"' + +depends_on: + - build + +steps: + - name: Trigger Portainer stack redeploy + image: curlimages/curl:latest + environment: + PORTAINER_WEBHOOK_URL: + from_secret: portainer_webhook_url + commands: + - set -e + - echo "=== Triggering Portainer stack redeploy ===" + - | + resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL") + body=$(echo "$resp" | head -n -1) + code=$(echo "$resp" | tail -n 1) + if [ "$code" != "200" ] && [ "$code" != "204" ]; then + echo "Webhook failed (HTTP $code): $body" + exit 1 + fi + echo "✓ Portainer redeploy triggered (HTTP $code)" + + - name: Send Deploy Status Notification (success) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy success 🎉"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [success] + + - name: Send Deploy Status Notification (failure) + image: curlimages/curl + environment: + MATTERMOST_BOT_ACCESS_TOKEN: + from_secret: mattermost_bot_access_token + MATTERMOST_CHANNEL_ID: + from_secret: mattermost_pushes_channel_id + MATTERMOST_POST_API_URL: + from_secret: mattermost_post_api_url + commands: + - | + BODY=$(printf '{"channel_id":"%s","message":"[%s - Build #%s] Production Deploy failure 💩"}' "$MATTERMOST_CHANNEL_ID" "$CI_REPO" "$CI_PIPELINE_NUMBER") + curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" -H "Authorization: Bearer $MATTERMOST_BOT_ACCESS_TOKEN" "$MATTERMOST_POST_API_URL" + depends_on: + - Trigger Portainer stack redeploy + when: + - status: [failure] diff --git a/docker-compose.yml b/docker-compose.yml index c67756d..ef44255 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,51 @@ +# Stack env vars in Portainer are used for substitution below; each must be passed into the container. +# Set at least PFA_SETUP_PASSWORD, PFA_DATABASE_PASSWORD, PFA_ADMIN_SMTP_PASSWORD in the stack. services: postfixadmin: image: git.mifi.dev/mifi-holdings/mail-postfixadmin:latest container_name: mifi-mail-postfixadmin - # ENV: set PFA_* in Portainer stack → ENV (advanced); paste from .env or .env.example + environment: + - PFA_SETUP_PASSWORD=${PFA_SETUP_PASSWORD} + - PFA_DATABASE_PASSWORD=${PFA_DATABASE_PASSWORD} + - PFA_ADMIN_SMTP_PASSWORD=${PFA_ADMIN_SMTP_PASSWORD} + - PFA_DATABASE_HOST=${PFA_DATABASE_HOST:-mail.mifi.holdings} + - PFA_DATABASE_USER=${PFA_DATABASE_USER:-postfixadmin} + - PFA_DATABASE_NAME=${PFA_DATABASE_NAME:-postfix} + - PFA_DATABASE_TYPE=${PFA_DATABASE_TYPE:-mysqli} + - PFA_ENCRYPT=${PFA_ENCRYPT:-php_crypt:BLOWFISH:13:{BLF-CRYPT}} + - PFA_SITE_URL=${PFA_SITE_URL:-https://postmaster.mifi.holdings} + - PFA_SITE_NAME=${PFA_SITE_NAME:-mifi Ventures Email Service} + - PFA_WELCOME_TEXT=${PFA_WELCOME_TEXT:-Welcome to mifi Ventures Email Service — help} + - PFA_SHOW_HEADER_TEXT=${PFA_SHOW_HEADER_TEXT:-YES} + - PFA_HEADER_TEXT=${PFA_HEADER_TEXT:-mifi Ventures Email Service} + - PFA_FOOTER_TEXT=${PFA_FOOTER_TEXT:-mifi Ventures Mail} + - PFA_FOOTER_LINK=${PFA_FOOTER_LINK:-https://mail.mifi.holdings} + - PFA_DEFAULT_LANGUAGE=${PFA_DEFAULT_LANGUAGE:-en} + - PFA_DEFAULT_CHARSET=${PFA_DEFAULT_CHARSET:-UTF-8} + - PFA_SPECIAL_ALIAS_CONTROL=${PFA_SPECIAL_ALIAS_CONTROL:-NO} + - PFA_BACKUP=${PFA_BACKUP:-NO} + - PFA_FETCHMAIL=${PFA_FETCHMAIL:-NO} + - PFA_SENDMAIL=${PFA_SENDMAIL:-NO} + - PFA_APP_PASSWORDS=${PFA_APP_PASSWORDS:-YES} + - PFA_QUOTA=${PFA_QUOTA:-YES} + - PFA_USED_QUOTAS=${PFA_USED_QUOTAS:-YES} + - PFA_MAILBOXES=${PFA_MAILBOXES:-100} + - PFA_MAXQUOTA=${PFA_MAXQUOTA:-10240} + - PFA_DOMAIN_QUOTA_DEFAULT=${PFA_DOMAIN_QUOTA_DEFAULT:-102400} + - PFA_ADMIN_EMAIL=${PFA_ADMIN_EMAIL:-noreply@mifi.holdings} + - PFA_ADMIN_NAME=${PFA_ADMIN_NAME:-mifi Ventures Postmaster} + - PFA_SMTP_SERVER=${PFA_SMTP_SERVER:-mail.mifi.holdings} + - PFA_SMTP_PORT=${PFA_SMTP_PORT:-587} + - PFA_SMTP_TYPE=${PFA_SMTP_TYPE:-starttls} + - PFA_SMTP_AUTH=${PFA_SMTP_AUTH:-true} + - PFA_EDIT_MAILBOX=${PFA_EDIT_MAILBOX:-true} + - PFA_EDIT_ALIAS=${PFA_EDIT_ALIAS:-true} + - PFA_FORGOTTEN_ADMIN_PASSWORD_RESET=${PFA_FORGOTTEN_ADMIN_PASSWORD_RESET:-true} + - PFA_FORGOTTEN_USER_PASSWORD_RESET=${PFA_FORGOTTEN_USER_PASSWORD_RESET:-true} + - PFA_ADMIN_2FA=${PFA_ADMIN_2FA:-true} + - PFA_API_ENABLED=${PFA_API_ENABLED:-true} + - PFA_API_ALLOW_FROM=${PFA_API_ALLOW_FROM:-127.0.0.1,::1} + - PFA_SESSION_TIMEOUT=${PFA_SESSION_TIMEOUT:-1800} healthcheck: test: curl --fail http://localhost || exit 1 retries: 5