Skip to content

deploy-test

deploy-test #5

Workflow file for this run

name: deploy-test
on:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: deploy-test
cancel-in-progress: false
env:
API_IMAGE_REPOSITORY: cixing-api
POSTGRES_IMAGE: postgres:16-alpine
REDIS_IMAGE: redis:7-alpine
MAILPIT_IMAGE: axllent/mailpit:v1.29.7
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate required secrets
env:
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
TEST_SERVER_USERNAME: ${{ secrets.TEST_SERVER_USERNAME }}
TEST_SERVER_SSH_KEY: ${{ secrets.TEST_SERVER_SSH_KEY }}
TEST_CONFIG_YAML: ${{ secrets.TEST_CONFIG_YAML }}
TEST_POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
run: |
set -eu
test -n "$TEST_SERVER_HOST"
test -n "$TEST_SERVER_USERNAME"
test -n "$TEST_SERVER_SSH_KEY"
test -n "$TEST_CONFIG_YAML"
test -n "$TEST_POSTGRES_PASSWORD"
- name: Build API image
run: |
set -eu
IMAGE_LOCAL="${API_IMAGE_REPOSITORY}:${GITHUB_SHA}"
docker build --platform linux/amd64 -f deploy/docker/Dockerfile.api -t "$IMAGE_LOCAL" .
echo "CIXING_IMAGE=$IMAGE_LOCAL" >> "$GITHUB_ENV"
- name: Pull dependency images
run: |
set -eu
docker pull --platform linux/amd64 "$POSTGRES_IMAGE"
docker pull --platform linux/amd64 "$REDIS_IMAGE"
docker pull --platform linux/amd64 "$MAILPIT_IMAGE"
- name: Save deployment images
run: |
set -eu
mkdir -p /tmp/cixing-deploy/images
docker save "$CIXING_IMAGE" | gzip > /tmp/cixing-deploy/images/cixing-api.tar.gz
docker save "$POSTGRES_IMAGE" | gzip > /tmp/cixing-deploy/images/postgres.tar.gz
docker save "$REDIS_IMAGE" | gzip > /tmp/cixing-deploy/images/redis.tar.gz
docker save "$MAILPIT_IMAGE" | gzip > /tmp/cixing-deploy/images/mailpit.tar.gz
- name: Prepare deployment files
env:
TEST_CONFIG_YAML: ${{ secrets.TEST_CONFIG_YAML }}
TEST_POSTGRES_PASSWORD: ${{ secrets.TEST_POSTGRES_PASSWORD }}
run: |
set -eu
mkdir -p /tmp/cixing-deploy
cp deploy/compose/docker-compose.prod.yml /tmp/cixing-deploy/docker-compose.yml
printf '%s' "$TEST_CONFIG_YAML" > /tmp/cixing-deploy/config.yaml
{
echo "CIXING_IMAGE=${CIXING_IMAGE}"
echo "POSTGRES_IMAGE=${POSTGRES_IMAGE}"
echo "REDIS_IMAGE=${REDIS_IMAGE}"
echo "MAILPIT_IMAGE=${MAILPIT_IMAGE}"
echo "POSTGRES_PASSWORD=${TEST_POSTGRES_PASSWORD}"
echo "API_PORT=8080"
echo "MAILPIT_UI_PORT=8025"
} > /tmp/cixing-deploy/deploy.env
- name: Configure SSH
env:
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
TEST_SERVER_SSH_KEY: ${{ secrets.TEST_SERVER_SSH_KEY }}
run: |
set -eu
mkdir -p ~/.ssh
printf '%s\n' "$TEST_SERVER_SSH_KEY" > ~/.ssh/id_test_server
chmod 600 ~/.ssh/id_test_server
ssh-keyscan -H "$TEST_SERVER_HOST" >> ~/.ssh/known_hosts
- name: Upload deployment assets
env:
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
TEST_SERVER_USERNAME: ${{ secrets.TEST_SERVER_USERNAME }}
run: |
set -eu
ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" \
"sudo mkdir -p /opt/cixing/config /opt/cixing/images && sudo chown -R $TEST_SERVER_USERNAME /opt/cixing"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/docker-compose.yml \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/docker-compose.yml"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/deploy.env \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/deploy.env"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/config.yaml \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/config/config.yaml"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/cixing-api.tar.gz \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/cixing-api.tar.gz"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/postgres.tar.gz \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/postgres.tar.gz"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/redis.tar.gz \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/redis.tar.gz"
scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/mailpit.tar.gz \
"$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/mailpit.tar.gz"
- name: Deploy on ECS
env:
TEST_SERVER_HOST: ${{ secrets.TEST_SERVER_HOST }}
TEST_SERVER_USERNAME: ${{ secrets.TEST_SERVER_USERNAME }}
run: |
set -eu
ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" <<'REMOTE'
set -eu
cd /opt/cixing
DOCKER="docker"
if ! docker info >/dev/null 2>&1; then
DOCKER="sudo docker"
fi
compose() {
$DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml "$@"
}
cleanup_existing_compose_state() {
echo "cleaning existing compose state"
compose down --remove-orphans 2>/dev/null || true
legacy_container_ids="$($DOCKER ps -aq --filter label=com.docker.compose.project=cixing-prod)"
if [ -n "$legacy_container_ids" ]; then
$DOCKER rm -f $legacy_container_ids >/dev/null 2>&1 || true
fi
legacy_network_ids="$($DOCKER network ls -q --filter label=com.docker.compose.project=cixing-prod)"
if [ -n "$legacy_network_ids" ]; then
$DOCKER network rm $legacy_network_ids >/dev/null 2>&1 || true
fi
if command -v ss >/dev/null 2>&1; then
if ss -lntp 2>/dev/null | grep -q ':8080 '; then
echo "port 8080 is still occupied before deployment" >&2
ss -lntp 2>/dev/null | grep ':8080 ' >&2 || true
exit 1
fi
fi
}
dump_api_diagnostics() {
compose ps
compose logs --tail=200 api
}
cleanup_deployment_artifacts() {
echo "cleaning deployment artifacts"
rm -f /opt/cixing/images/*.tar.gz || true
current_image="$(awk -F= '$1=="CIXING_IMAGE"{print $2}' /opt/cixing/deploy.env)"
if [ -z "$current_image" ]; then
return 0
fi
for image_ref in $($DOCKER images --format '{{.Repository}}:{{.Tag}}' | grep '^cixing-api:' | grep -F -x -v "$current_image" || true); do
$DOCKER rmi "$image_ref" >/dev/null 2>&1 || true
done
}
api_ready() {
api_container_id="$(compose ps -q api)"
if [ -z "$api_container_id" ]; then
api_state="missing"
api_health="missing"
return 1
fi
if ! api_state="$($DOCKER inspect --format '{{.State.Status}}' "$api_container_id" 2>/dev/null)"; then
api_container_id=""
api_state="missing"
api_health="missing"
return 1
fi
if ! api_health="$($DOCKER inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$api_container_id" 2>/dev/null)"; then
api_container_id=""
api_state="missing"
api_health="missing"
return 1
fi
if [ "$api_state" != "running" ] || [ "$api_health" != "healthy" ]; then
return 1
fi
if ! $DOCKER exec "$api_container_id" /bin/sh -c 'wget -qO- http://127.0.0.1:8080/healthz >/dev/null'; then
api_health="healthz_failed"
return 1
fi
return 0
}
chmod 600 /opt/cixing/deploy.env
chmod 644 /opt/cixing/config/config.yaml
cleanup_existing_compose_state
gzip -dc /opt/cixing/images/cixing-api.tar.gz | $DOCKER load
gzip -dc /opt/cixing/images/postgres.tar.gz | $DOCKER load
gzip -dc /opt/cixing/images/redis.tar.gz | $DOCKER load
gzip -dc /opt/cixing/images/mailpit.tar.gz | $DOCKER load
compose up -d postgres redis mailpit
compose run --rm migrate up
compose up -d api
attempts=0
api_state=""
api_health=""
while [ "$attempts" -lt 30 ]; do
if api_ready; then
break
fi
if [ "$api_state" = "exited" ] || [ "$api_state" = "dead" ]; then
break
fi
attempts=$((attempts + 1))
sleep 4
done
if ! api_ready; then
dump_api_diagnostics
if [ "$api_health" = "healthz_failed" ]; then
echo "container /healthz check failed" >&2
elif [ "$api_state" = "missing" ]; then
echo "api container was not created" >&2
else
echo "api container state=$api_state health=$api_health" >&2
fi
exit 1
fi
sleep 12
if ! api_ready; then
dump_api_diagnostics
if [ "$api_health" = "healthz_failed" ]; then
echo "api container became unstable after initial readiness: /healthz failed" >&2
elif [ "$api_state" = "missing" ]; then
echo "api container disappeared after initial readiness" >&2
else
echo "api container became unstable after initial readiness: state=$api_state health=$api_health" >&2
fi
exit 1
fi
cleanup_deployment_artifacts
compose ps
compose logs --tail=80 api
REMOTE