deploy-test #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |