Some checks failed
Chore App Build, Test, and Push Docker Images / build-and-push (push) Has been cancelled
504 lines
21 KiB
YAML
504 lines
21 KiB
YAML
name: Promote Master to Production
|
|
run-name: ${{ gitea.actor }} promoting ${{ github.event.inputs.target_ref || 'master' }} [${{ gitea.sha }}]
|
|
|
|
on:
|
|
workflow_dispatch:
|
|
inputs:
|
|
target_ref:
|
|
description: "Git ref/branch to promote"
|
|
required: true
|
|
default: "master"
|
|
run_backend_tests:
|
|
description: "Run backend pytest gate"
|
|
required: true
|
|
default: "true"
|
|
run_frontend_tests:
|
|
description: "Run frontend unit test gate"
|
|
required: true
|
|
default: "true"
|
|
run_playwright_tests:
|
|
description: "Run Playwright E2E gate"
|
|
required: true
|
|
default: "true"
|
|
skip_tests:
|
|
description: "Skip the entire tests stage"
|
|
required: true
|
|
default: "false"
|
|
deploy:
|
|
description: "Run production deploy over SSH"
|
|
required: true
|
|
default: "true"
|
|
create_tag:
|
|
description: "Create and push git release tag after successful deploy"
|
|
required: true
|
|
default: "true"
|
|
require_manual_approval:
|
|
description: "Require approval token for deploy job"
|
|
required: true
|
|
default: "false"
|
|
approval_token:
|
|
description: "Approval token value when manual approval is required"
|
|
required: false
|
|
default: ""
|
|
rollback_on_failed_healthcheck:
|
|
description: "Auto-rollback if post-deploy health checks fail"
|
|
required: true
|
|
default: "false"
|
|
|
|
concurrency:
|
|
group: promotion-production
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
prepare:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
outputs:
|
|
target_ref: ${{ steps.resolve.outputs.target_ref }}
|
|
version: ${{ steps.version.outputs.version }}
|
|
release_tag: ${{ steps.version.outputs.release_tag }}
|
|
commit_sha: ${{ steps.commit.outputs.commit_sha }}
|
|
steps:
|
|
- name: Resolve target ref
|
|
id: resolve
|
|
run: |
|
|
target_ref="${{ github.event.inputs.target_ref }}"
|
|
if [ -z "$target_ref" ]; then
|
|
target_ref="master"
|
|
fi
|
|
echo "target_ref=$target_ref" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Check out repository code
|
|
uses: actions/checkout@v3
|
|
with:
|
|
ref: ${{ steps.resolve.outputs.target_ref }}
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve version and release tag
|
|
id: version
|
|
run: |
|
|
version=$(python -c "import sys; sys.path.append('./backend'); from config.version import BASE_VERSION; print(BASE_VERSION)")
|
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
echo "release_tag=v$version" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve promoted commit SHA
|
|
id: commit
|
|
run: |
|
|
echo "commit_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Guard against duplicate release tags
|
|
if: ${{ github.event.inputs.create_tag != 'false' }}
|
|
run: |
|
|
release_tag="${{ steps.version.outputs.release_tag }}"
|
|
if git ls-remote --exit-code --tags origin "refs/tags/${release_tag}" >/dev/null 2>&1; then
|
|
echo "Release tag ${release_tag} already exists on origin; aborting promotion."
|
|
exit 1
|
|
fi
|
|
|
|
tests:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 120
|
|
needs: prepare
|
|
steps:
|
|
- name: Skip tests stage
|
|
if: ${{ github.event.inputs.skip_tests == 'true' }}
|
|
run: echo "Skipping tests stage because skip_tests=true"
|
|
|
|
- name: Check out promoted ref
|
|
if: ${{ github.event.inputs.skip_tests != 'true' }}
|
|
uses: actions/checkout@v3
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.target_ref }}
|
|
|
|
- name: Set up Python for backend tests
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && (github.event.inputs.run_backend_tests != 'false' || github.event.inputs.run_playwright_tests != 'false') }}
|
|
uses: actions/setup-python@v4
|
|
with:
|
|
python-version: "3.11"
|
|
|
|
- name: Install backend dependencies (runner python)
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_backend_tests != 'false' }}
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install -r backend/requirements.txt
|
|
|
|
- name: Run backend unit tests
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_backend_tests != 'false' }}
|
|
run: |
|
|
cd backend
|
|
pytest -q
|
|
|
|
- name: Set up Node.js
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && (github.event.inputs.run_frontend_tests != 'false' || github.event.inputs.run_playwright_tests != 'false') }}
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20.19.0"
|
|
cache: "npm"
|
|
cache-dependency-path: frontend/package-lock.json
|
|
|
|
- name: Install frontend dependencies
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && (github.event.inputs.run_frontend_tests != 'false' || github.event.inputs.run_playwright_tests != 'false') }}
|
|
run: npm ci
|
|
working-directory: frontend
|
|
|
|
- name: Run frontend unit tests
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_frontend_tests != 'false' }}
|
|
run: npm run test:unit --if-present
|
|
working-directory: frontend
|
|
|
|
- name: Create backend venv for Playwright webServer
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_playwright_tests != 'false' }}
|
|
run: |
|
|
python -m venv backend/.venv
|
|
backend/.venv/bin/python -m pip install --upgrade pip
|
|
backend/.venv/bin/python -m pip install -r backend/requirements.txt
|
|
|
|
- name: Install Playwright browsers
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_playwright_tests != 'false' }}
|
|
run: npx playwright install --with-deps
|
|
working-directory: frontend
|
|
|
|
- name: Run Playwright tests
|
|
if: ${{ github.event.inputs.skip_tests != 'true' && github.event.inputs.run_playwright_tests != 'false' }}
|
|
run: npx playwright test --reporter=line
|
|
working-directory: frontend
|
|
env:
|
|
CI: "true"
|
|
PLAYWRIGHT_BASE_URL: "https://localhost:5173"
|
|
E2E_ACCESS_TOKEN_EXPIRY_MINUTES: "180"
|
|
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
needs:
|
|
- prepare
|
|
- tests
|
|
if: ${{ github.event.inputs.deploy != 'false' }}
|
|
steps:
|
|
- name: Validate manual approval token
|
|
if: ${{ github.event.inputs.require_manual_approval != 'false' }}
|
|
run: |
|
|
if [ -z "${{ secrets.PROD_APPROVAL_TOKEN }}" ]; then
|
|
echo "PROD_APPROVAL_TOKEN secret is required when manual approval is enabled."
|
|
exit 1
|
|
fi
|
|
if [ -z "${{ github.event.inputs.approval_token }}" ]; then
|
|
echo "approval_token input is required when manual approval is enabled."
|
|
exit 1
|
|
fi
|
|
if [ "${{ github.event.inputs.approval_token }}" != "${{ secrets.PROD_APPROVAL_TOKEN }}" ]; then
|
|
echo "Approval token does not match PROD_APPROVAL_TOKEN."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Deploy to production homeserver
|
|
uses: appleboy/ssh-action@v1.0.3
|
|
with:
|
|
host: ${{ secrets.DEPLOY_PROD_HOST }}
|
|
username: ${{ secrets.DEPLOY_PROD_USER }}
|
|
key: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
|
|
port: 22
|
|
script: |
|
|
set -euo pipefail
|
|
|
|
repo_dir="${{ secrets.DEPLOY_PROD_PATH }}"
|
|
if [ -z "$repo_dir" ]; then
|
|
repo_dir="$HOME/chore"
|
|
fi
|
|
|
|
repo_parent=$(dirname "$repo_dir")
|
|
if [ ! -d "$repo_parent" ]; then
|
|
mkdir -p "$repo_parent"
|
|
fi
|
|
if [ ! -w "$repo_parent" ]; then
|
|
echo "Deploy user $(whoami) cannot write to ${repo_parent}."
|
|
echo "Set DEPLOY_PROD_PATH to a writable location or grant write access before rerunning promotion."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -d "$repo_dir/.git" ]; then
|
|
cd "$repo_dir"
|
|
git fetch --all --tags
|
|
else
|
|
git clone --branch "${{ needs.prepare.outputs.target_ref }}" https://git.ryankegel.com/ryan/chore.git "$repo_dir"
|
|
cd "$repo_dir"
|
|
fi
|
|
|
|
git checkout "${{ needs.prepare.outputs.target_ref }}"
|
|
git reset --hard "origin/${{ needs.prepare.outputs.target_ref }}"
|
|
|
|
timestamp=$(date +%Y%m%d-%H%M%S)
|
|
mkdir -p backups
|
|
|
|
# Capture currently running images for rollback.
|
|
backend_prev_id=""
|
|
frontend_prev_id=""
|
|
if docker ps --format '{{.Names}}' | grep -q '^chores-app-backend-prod$'; then
|
|
backend_prev_id=$(docker inspect -f '{{.Image}}' chores-app-backend-prod)
|
|
docker tag "$backend_prev_id" git.ryankegel.com:3000/kegel/chores/backend:predeploy-backup || true
|
|
fi
|
|
if docker ps --format '{{.Names}}' | grep -q '^chores-app-frontend-prod$'; then
|
|
frontend_prev_id=$(docker inspect -f '{{.Image}}' chores-app-frontend-prod)
|
|
docker tag "$frontend_prev_id" git.ryankegel.com:3000/kegel/chores/frontend:predeploy-backup || true
|
|
fi
|
|
|
|
cat > .rollback-meta << EOF
|
|
BACKEND_PREV_ID=${backend_prev_id}
|
|
FRONTEND_PREV_ID=${frontend_prev_id}
|
|
CREATED_AT=${timestamp}
|
|
PROMOTED_SHA=${{ needs.prepare.outputs.commit_sha }}
|
|
RELEASE_TAG=${{ needs.prepare.outputs.release_tag }}
|
|
EOF
|
|
|
|
# Backup TinyDB volume before replacement.
|
|
docker run --rm \
|
|
-v chores-app-backend-data:/data \
|
|
-v "$PWD/backups:/backup" \
|
|
alpine sh -c "tar czf /backup/chores-backend-data-${timestamp}.tar.gz -C /data ." || true
|
|
|
|
# Keep long-lived cryptographic values stable; fall back to previous .env if secrets are absent.
|
|
PREV_SECRET_KEY=""
|
|
PREV_DIGEST_TOKEN_SECRET=""
|
|
PREV_VAPID_PUBLIC_KEY=""
|
|
PREV_VAPID_PRIVATE_KEY=""
|
|
PREV_REFRESH_TOKEN_EXPIRY_DAYS=""
|
|
if [ -f .env ]; then
|
|
PREV_SECRET_KEY=$(grep '^SECRET_KEY=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
PREV_DIGEST_TOKEN_SECRET=$(grep '^DIGEST_TOKEN_SECRET=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
PREV_VAPID_PUBLIC_KEY=$(grep '^VAPID_PUBLIC_KEY=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
PREV_VAPID_PRIVATE_KEY=$(grep '^VAPID_PRIVATE_KEY=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
PREV_REFRESH_TOKEN_EXPIRY_DAYS=$(grep '^REFRESH_TOKEN_EXPIRY_DAYS=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
fi
|
|
|
|
SECRET_KEY_VALUE="${{ secrets.PROD_SECRET_KEY }}"
|
|
DIGEST_TOKEN_SECRET_VALUE="${{ secrets.PROD_DIGEST_TOKEN_SECRET }}"
|
|
VAPID_PUBLIC_KEY_VALUE="${{ secrets.PROD_VAPID_PUBLIC_KEY }}"
|
|
VAPID_PRIVATE_KEY_VALUE="${{ secrets.PROD_VAPID_PRIVATE_KEY }}"
|
|
REFRESH_TOKEN_EXPIRY_DAYS_VALUE="${{ secrets.PROD_REFRESH_TOKEN_EXPIRY_DAYS }}"
|
|
|
|
if [ -z "$SECRET_KEY_VALUE" ]; then
|
|
if [ -n "$PREV_SECRET_KEY" ]; then
|
|
SECRET_KEY_VALUE="$PREV_SECRET_KEY"
|
|
else
|
|
SECRET_KEY_VALUE=$(openssl rand -hex 32)
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$DIGEST_TOKEN_SECRET_VALUE" ]; then
|
|
if [ -n "$PREV_DIGEST_TOKEN_SECRET" ]; then
|
|
DIGEST_TOKEN_SECRET_VALUE="$PREV_DIGEST_TOKEN_SECRET"
|
|
else
|
|
DIGEST_TOKEN_SECRET_VALUE=$(openssl rand -hex 32)
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$VAPID_PUBLIC_KEY_VALUE" ] && [ -n "$PREV_VAPID_PUBLIC_KEY" ]; then
|
|
VAPID_PUBLIC_KEY_VALUE="$PREV_VAPID_PUBLIC_KEY"
|
|
fi
|
|
if [ -z "$VAPID_PRIVATE_KEY_VALUE" ] && [ -n "$PREV_VAPID_PRIVATE_KEY" ]; then
|
|
VAPID_PRIVATE_KEY_VALUE="$PREV_VAPID_PRIVATE_KEY"
|
|
fi
|
|
|
|
if [ -z "$VAPID_PUBLIC_KEY_VALUE" ] || [ -z "$VAPID_PRIVATE_KEY_VALUE" ]; then
|
|
echo "VAPID keys are required (set PROD_VAPID_PUBLIC_KEY/PROD_VAPID_PRIVATE_KEY or keep previous .env values)."
|
|
exit 1
|
|
fi
|
|
|
|
if [ -z "$REFRESH_TOKEN_EXPIRY_DAYS_VALUE" ]; then
|
|
if [ -n "$PREV_REFRESH_TOKEN_EXPIRY_DAYS" ]; then
|
|
REFRESH_TOKEN_EXPIRY_DAYS_VALUE="$PREV_REFRESH_TOKEN_EXPIRY_DAYS"
|
|
else
|
|
REFRESH_TOKEN_EXPIRY_DAYS_VALUE="90"
|
|
fi
|
|
fi
|
|
|
|
if ! printf '%s' "$REFRESH_TOKEN_EXPIRY_DAYS_VALUE" | grep -Eq '^[0-9]+$'; then
|
|
echo "REFRESH_TOKEN_EXPIRY_DAYS must be a positive integer, got: $REFRESH_TOKEN_EXPIRY_DAYS_VALUE"
|
|
exit 1
|
|
fi
|
|
|
|
cat > .env << EOF
|
|
FRONTEND_URL=${{ secrets.PROD_FRONTEND_URL }}
|
|
BACKEND_HOST_PORT=${{ secrets.PROD_BACKEND_HOST_PORT }}
|
|
FRONTEND_HOST_PORT=${{ secrets.PROD_FRONTEND_HOST_PORT }}
|
|
SECRET_KEY=${SECRET_KEY_VALUE}
|
|
REFRESH_TOKEN_EXPIRY_DAYS=${REFRESH_TOKEN_EXPIRY_DAYS_VALUE}
|
|
DIGEST_TOKEN_SECRET=${DIGEST_TOKEN_SECRET_VALUE}
|
|
VAPID_PUBLIC_KEY=${VAPID_PUBLIC_KEY_VALUE}
|
|
VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY_VALUE}
|
|
ADMIN_EMAIL=${{ secrets.PROD_ADMIN_EMAIL }}
|
|
ADMIN_PASSWORD=${{ secrets.PROD_ADMIN_PASSWORD }}
|
|
ADMIN_FIRST_NAME=${{ secrets.PROD_ADMIN_FIRST_NAME }}
|
|
ADMIN_LAST_NAME=${{ secrets.PROD_ADMIN_LAST_NAME }}
|
|
EOF
|
|
|
|
chmod 600 .env .rollback-meta
|
|
|
|
backend_host_port=$(grep '^BACKEND_HOST_PORT=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
frontend_host_port=$(grep '^FRONTEND_HOST_PORT=' .env | head -n1 | cut -d '=' -f2- || true)
|
|
if [ -z "$backend_host_port" ]; then
|
|
backend_host_port="5001"
|
|
fi
|
|
if [ -z "$frontend_host_port" ]; then
|
|
frontend_host_port="4601"
|
|
fi
|
|
|
|
docker-compose down
|
|
|
|
wait_for_container_removal() {
|
|
container_name="$1"
|
|
attempts=0
|
|
while docker ps -a --format '{{.Names}}' | grep -qx "$container_name"; do
|
|
attempts=$((attempts + 1))
|
|
if [ "$attempts" -ge 12 ]; then
|
|
echo "Container ${container_name} still exists after waiting for teardown."
|
|
docker ps -a --filter "name=^${container_name}$"
|
|
return 1
|
|
fi
|
|
echo "Waiting for ${container_name} to be removed..."
|
|
sleep 5
|
|
done
|
|
}
|
|
|
|
wait_for_port_release() {
|
|
port="$1"
|
|
attempts=0
|
|
while docker ps -a --format '{{.ID}} {{.Names}} {{.Ports}} {{.Status}}' | grep -E "(^|[[:space:]])0\.0\.0\.0:${port}->|:::${port}->" >/dev/null 2>&1; do
|
|
attempts=$((attempts + 1))
|
|
if [ "$attempts" -ge 12 ]; then
|
|
echo "Port ${port} is still allocated after teardown. Blocking container(s):"
|
|
docker ps -a --format '{{.ID}} {{.Names}} {{.Ports}} {{.Status}}' | grep -E "(^|[[:space:]])0\.0\.0\.0:${port}->|:::${port}->" || true
|
|
return 1
|
|
fi
|
|
echo "Waiting for port ${port} to be released..."
|
|
sleep 5
|
|
done
|
|
}
|
|
|
|
wait_for_container_removal chores-app-frontend-prod
|
|
wait_for_container_removal chores-app-backend-prod
|
|
wait_for_port_release "$frontend_host_port"
|
|
wait_for_port_release "$backend_host_port"
|
|
|
|
docker-compose pull
|
|
docker-compose up -d
|
|
|
|
sleep 20
|
|
|
|
backend_running=$(docker inspect -f '{{.State.Running}}' chores-app-backend-prod 2>/dev/null || echo false)
|
|
frontend_running=$(docker inspect -f '{{.State.Running}}' chores-app-frontend-prod 2>/dev/null || echo false)
|
|
|
|
backend_ok=false
|
|
if curl -fsS "http://localhost:${backend_host_port}/version" >/dev/null 2>&1; then
|
|
backend_ok=true
|
|
fi
|
|
|
|
frontend_ok=false
|
|
if curl -kfsS "https://localhost:${frontend_host_port}/" >/dev/null 2>&1; then
|
|
frontend_ok=true
|
|
fi
|
|
|
|
if [ "$backend_running" != "true" ] || [ "$frontend_running" != "true" ] || [ "$backend_ok" != "true" ] || [ "$frontend_ok" != "true" ]; then
|
|
echo "Post-deploy health check failed."
|
|
docker-compose ps
|
|
echo "--- Backend logs (last 200 lines) ---"
|
|
docker logs --tail 200 chores-app-backend-prod || true
|
|
echo "--- Frontend logs (last 120 lines) ---"
|
|
docker logs --tail 120 chores-app-frontend-prod || true
|
|
|
|
if [ "${{ github.event.inputs.rollback_on_failed_healthcheck }}" = "true" ]; then
|
|
echo "Attempting rollback using predeploy-backup image tags..."
|
|
docker tag git.ryankegel.com:3000/kegel/chores/backend:predeploy-backup git.ryankegel.com:3000/kegel/chores/backend:latest || true
|
|
docker tag git.ryankegel.com:3000/kegel/chores/frontend:predeploy-backup git.ryankegel.com:3000/kegel/chores/frontend:latest || true
|
|
docker-compose up -d
|
|
sleep 20
|
|
curl -fsS "http://localhost:${backend_host_port}/version" >/dev/null
|
|
fi
|
|
|
|
exit 1
|
|
fi
|
|
|
|
docker-compose ps
|
|
|
|
create-release-tag:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
needs:
|
|
- prepare
|
|
- tests
|
|
- deploy
|
|
if: ${{ github.event.inputs.create_tag != 'false' && github.event.inputs.deploy != 'false' }}
|
|
steps:
|
|
- name: Check out promoted ref
|
|
uses: actions/checkout@v3
|
|
with:
|
|
ref: ${{ needs.prepare.outputs.target_ref }}
|
|
fetch-depth: 0
|
|
|
|
- name: Create and push annotated release tag
|
|
run: |
|
|
tag_name="${{ needs.prepare.outputs.release_tag }}"
|
|
git config user.name "gitea-actions"
|
|
git config user.email "gitea-actions@local"
|
|
git tag -a "$tag_name" -m "Release $tag_name"
|
|
git push origin "$tag_name"
|
|
|
|
notify-on-failure:
|
|
runs-on: ubuntu-latest
|
|
needs:
|
|
- prepare
|
|
- tests
|
|
- deploy
|
|
- create-release-tag
|
|
if: ${{ always() }}
|
|
steps:
|
|
- name: Send email when promotion fails
|
|
if: ${{ needs.prepare.result == 'failure' || needs.tests.result == 'failure' || needs.deploy.result == 'failure' || needs.create-release-tag.result == 'failure' }}
|
|
uses: dawidd6/action-send-mail@v3
|
|
with:
|
|
server_address: smtp.gmail.com
|
|
server_port: 465
|
|
username: ${{ secrets.MAIL_USER }}
|
|
password: ${{ secrets.MAIL_PASSWORD }}
|
|
secure: true
|
|
to: ${{ secrets.MAIL_TO }}
|
|
from: Gitea <git@git.ryankegel.com>
|
|
subject: Promotion failed - ${{ gitea.repository }} [${{ needs.prepare.outputs.target_ref }}@${{ needs.prepare.outputs.commit_sha }}]
|
|
convert_markdown: true
|
|
html_body: |
|
|
### Production promotion failed
|
|
|
|
- Repository: ${{ gitea.repository }}
|
|
- Ref: ${{ needs.prepare.outputs.target_ref }}
|
|
- Commit: ${{ needs.prepare.outputs.commit_sha }}
|
|
- Intended tag: ${{ needs.prepare.outputs.release_tag }}
|
|
- prepare: ${{ needs.prepare.result }}
|
|
- tests: ${{ needs.tests.result }}
|
|
- deploy: ${{ needs.deploy.result }}
|
|
- tag: ${{ needs.create-release-tag.result }}
|
|
|
|
- name: Send email when promotion succeeds
|
|
if: ${{ needs.prepare.result == 'success' && needs.tests.result == 'success' && (needs.deploy.result == 'success' || needs.deploy.result == 'skipped') && (needs.create-release-tag.result == 'success' || needs.create-release-tag.result == 'skipped') }}
|
|
uses: dawidd6/action-send-mail@v3
|
|
with:
|
|
server_address: smtp.gmail.com
|
|
server_port: 465
|
|
username: ${{ secrets.MAIL_USER }}
|
|
password: ${{ secrets.MAIL_PASSWORD }}
|
|
secure: true
|
|
to: ${{ secrets.MAIL_TO }}
|
|
from: Gitea <git@git.ryankegel.com>
|
|
subject: Promotion succeeded - ${{ gitea.repository }} [${{ needs.prepare.outputs.target_ref }}@${{ needs.prepare.outputs.commit_sha }}]
|
|
convert_markdown: true
|
|
html_body: |
|
|
### Production promotion succeeded
|
|
|
|
- Repository: ${{ gitea.repository }}
|
|
- Ref: ${{ needs.prepare.outputs.target_ref }}
|
|
- Commit: ${{ needs.prepare.outputs.commit_sha }}
|
|
- Release tag: ${{ needs.prepare.outputs.release_tag }}
|
|
- prepare: ${{ needs.prepare.result }}
|
|
- tests: ${{ needs.tests.result }}
|
|
- deploy: ${{ needs.deploy.result }}
|
|
- tag: ${{ needs.create-release-tag.result }}
|