@@ -2,6 +2,9 @@ name: deploy-test
22
33on :
44 workflow_dispatch :
5+ push :
6+ branches :
7+ - develop
58
69permissions :
710 contents : read
@@ -11,7 +14,10 @@ concurrency:
1114 cancel-in-progress : false
1215
1316env :
14- IMAGE_REPOSITORY : cixing-api
17+ API_IMAGE_REPOSITORY : cixing-api
18+ POSTGRES_IMAGE : postgres:16-alpine
19+ REDIS_IMAGE : redis:7-alpine
20+ MAILPIT_IMAGE : axllent/mailpit:v1.29.7
1521
1622jobs :
1723 deploy :
@@ -23,42 +29,41 @@ jobs:
2329
2430 - name : Validate required secrets
2531 env :
26- DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
27- DOCKERHUB_TOKEN : ${{ secrets.DOCKERHUB_TOKEN }}
2832 TEST_SERVER_HOST : ${{ secrets.TEST_SERVER_HOST }}
2933 TEST_SERVER_USERNAME : ${{ secrets.TEST_SERVER_USERNAME }}
3034 TEST_SERVER_SSH_KEY : ${{ secrets.TEST_SERVER_SSH_KEY }}
3135 TEST_CONFIG_YAML : ${{ secrets.TEST_CONFIG_YAML }}
3236 TEST_POSTGRES_PASSWORD : ${{ secrets.TEST_POSTGRES_PASSWORD }}
3337 run : |
3438 set -eu
35- test -n "$DOCKERHUB_USERNAME"
36- test -n "$DOCKERHUB_TOKEN"
3739 test -n "$TEST_SERVER_HOST"
3840 test -n "$TEST_SERVER_USERNAME"
3941 test -n "$TEST_SERVER_SSH_KEY"
4042 test -n "$TEST_CONFIG_YAML"
4143 test -n "$TEST_POSTGRES_PASSWORD"
4244
43- - name : Login to Docker Hub
44- env :
45- DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
46- DOCKERHUB_TOKEN : ${{ secrets.DOCKERHUB_TOKEN }}
45+ - name : Build API image
4746 run : |
4847 set -eu
49- echo "$DOCKERHUB_TOKEN" | docker login docker.io -u "$DOCKERHUB_USERNAME" --password-stdin
48+ IMAGE_LOCAL="${API_IMAGE_REPOSITORY}:${GITHUB_SHA}"
49+ docker build --platform linux/amd64 -f deploy/docker/Dockerfile.api -t "$IMAGE_LOCAL" .
50+ echo "CIXING_IMAGE=$IMAGE_LOCAL" >> "$GITHUB_ENV"
5051
51- - name : Build and push image
52- env :
53- DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
52+ - name : Pull dependency images
5453 run : |
5554 set -eu
56- IMAGE_SHA="docker.io/${DOCKERHUB_USERNAME}/${IMAGE_REPOSITORY}:${GITHUB_SHA}"
57- IMAGE_LATEST="docker.io/${DOCKERHUB_USERNAME}/${IMAGE_REPOSITORY}:test-latest"
58- docker build -f deploy/docker/Dockerfile.api -t "$IMAGE_SHA" -t "$IMAGE_LATEST" .
59- docker push "$IMAGE_SHA"
60- docker push "$IMAGE_LATEST"
61- echo "CIXING_IMAGE=$IMAGE_SHA" >> "$GITHUB_ENV"
55+ docker pull --platform linux/amd64 "$POSTGRES_IMAGE"
56+ docker pull --platform linux/amd64 "$REDIS_IMAGE"
57+ docker pull --platform linux/amd64 "$MAILPIT_IMAGE"
58+
59+ - name : Save deployment images
60+ run : |
61+ set -eu
62+ mkdir -p /tmp/cixing-deploy/images
63+ docker save "$CIXING_IMAGE" | gzip > /tmp/cixing-deploy/images/cixing-api.tar.gz
64+ docker save "$POSTGRES_IMAGE" | gzip > /tmp/cixing-deploy/images/postgres.tar.gz
65+ docker save "$REDIS_IMAGE" | gzip > /tmp/cixing-deploy/images/redis.tar.gz
66+ docker save "$MAILPIT_IMAGE" | gzip > /tmp/cixing-deploy/images/mailpit.tar.gz
6267
6368 - name : Prepare deployment files
6469 env :
7176 printf '%s' "$TEST_CONFIG_YAML" > /tmp/cixing-deploy/config.yaml
7277 {
7378 echo "CIXING_IMAGE=${CIXING_IMAGE}"
79+ echo "POSTGRES_IMAGE=${POSTGRES_IMAGE}"
80+ echo "REDIS_IMAGE=${REDIS_IMAGE}"
81+ echo "MAILPIT_IMAGE=${MAILPIT_IMAGE}"
7482 echo "POSTGRES_PASSWORD=${TEST_POSTGRES_PASSWORD}"
7583 echo "API_PORT=8080"
7684 echo "MAILPIT_UI_PORT=8025"
@@ -87,43 +95,175 @@ jobs:
8795 chmod 600 ~/.ssh/id_test_server
8896 ssh-keyscan -H "$TEST_SERVER_HOST" >> ~/.ssh/known_hosts
8997
90- - name : Upload deployment files
98+ - name : Upload deployment assets
9199 env :
92100 TEST_SERVER_HOST : ${{ secrets.TEST_SERVER_HOST }}
93101 TEST_SERVER_USERNAME : ${{ secrets.TEST_SERVER_USERNAME }}
94102 run : |
95103 set -eu
96104 ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" \
97- "sudo mkdir -p /opt/cixing/config && sudo chown -R $TEST_SERVER_USERNAME /opt/cixing"
105+ "sudo mkdir -p /opt/cixing/config /opt/cixing/images && sudo chown -R $TEST_SERVER_USERNAME /opt/cixing"
98106 scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/docker-compose.yml \
99107 "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/docker-compose.yml"
100108 scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/deploy.env \
101109 "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/deploy.env"
102110 scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/config.yaml \
103111 "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/config/config.yaml"
112+ scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/cixing-api.tar.gz \
113+ "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/cixing-api.tar.gz"
114+ scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/postgres.tar.gz \
115+ "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/postgres.tar.gz"
116+ scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/redis.tar.gz \
117+ "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/redis.tar.gz"
118+ scp -i ~/.ssh/id_test_server /tmp/cixing-deploy/images/mailpit.tar.gz \
119+ "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST:/opt/cixing/images/mailpit.tar.gz"
104120
105121 - name : Deploy on ECS
106122 env :
107- DOCKERHUB_USERNAME : ${{ secrets.DOCKERHUB_USERNAME }}
108- DOCKERHUB_TOKEN : ${{ secrets.DOCKERHUB_TOKEN }}
109123 TEST_SERVER_HOST : ${{ secrets.TEST_SERVER_HOST }}
110124 TEST_SERVER_USERNAME : ${{ secrets.TEST_SERVER_USERNAME }}
111125 run : |
112126 set -eu
113- echo "$DOCKERHUB_TOKEN" | ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" \
114- "if docker info >/dev/null 2>&1; then docker login docker.io -u '$DOCKERHUB_USERNAME' --password-stdin; else sudo docker login docker.io -u '$DOCKERHUB_USERNAME' --password-stdin; fi"
115127 ssh -i ~/.ssh/id_test_server "$TEST_SERVER_USERNAME@$TEST_SERVER_HOST" <<'REMOTE'
116128 set -eu
117129 cd /opt/cixing
118130 DOCKER="docker"
119131 if ! docker info >/dev/null 2>&1; then
120132 DOCKER="sudo docker"
121133 fi
122- chmod 600 /opt/cixing/deploy.env /opt/cixing/config/config.yaml
123- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml pull
124- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml up -d postgres redis mailpit
125- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml run --rm migrate up
126- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml up -d api
127- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml ps
128- $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml logs --tail=80 api
134+ compose() {
135+ $DOCKER compose --env-file /opt/cixing/deploy.env -f /opt/cixing/docker-compose.yml "$@"
136+ }
137+ cleanup_existing_compose_state() {
138+ echo "cleaning existing compose state"
139+ compose down --remove-orphans 2>/dev/null || true
140+ legacy_container_ids="$($DOCKER ps -aq --filter label=com.docker.compose.project=cixing-prod)"
141+ if [ -n "$legacy_container_ids" ]; then
142+ $DOCKER rm -f $legacy_container_ids >/dev/null 2>&1 || true
143+ fi
144+ legacy_network_ids="$($DOCKER network ls -q --filter label=com.docker.compose.project=cixing-prod)"
145+ if [ -n "$legacy_network_ids" ]; then
146+ $DOCKER network rm $legacy_network_ids >/dev/null 2>&1 || true
147+ fi
148+ if command -v ss >/dev/null 2>&1; then
149+ if ss -lntp 2>/dev/null | grep -q ':8080 '; then
150+ echo "port 8080 is still occupied before deployment" >&2
151+ ss -lntp 2>/dev/null | grep ':8080 ' >&2 || true
152+ exit 1
153+ fi
154+ fi
155+ }
156+ dump_api_diagnostics() {
157+ compose ps
158+ compose logs --tail=200 api
159+ }
160+ cleanup_deployment_artifacts() {
161+ echo "cleaning deployment artifacts"
162+ rm -f /opt/cixing/images/*.tar.gz || true
163+ current_image="$(awk -F= '$1=="CIXING_IMAGE"{print $2}' /opt/cixing/deploy.env)"
164+ if [ -z "$current_image" ]; then
165+ return 0
166+ fi
167+ for image_ref in $($DOCKER images --format '{{.Repository}}:{{.Tag}}' | grep '^cixing-api:' | grep -F -x -v "$current_image" || true); do
168+ $DOCKER rmi "$image_ref" >/dev/null 2>&1 || true
169+ done
170+ }
171+ api_ready() {
172+ api_container_id="$(compose ps -q api)"
173+ if [ -z "$api_container_id" ]; then
174+ api_state="missing"
175+ api_health="missing"
176+ return 1
177+ fi
178+ if ! api_state="$($DOCKER inspect --format '{{.State.Status}}' "$api_container_id" 2>/dev/null)"; then
179+ api_container_id=""
180+ api_state="missing"
181+ api_health="missing"
182+ return 1
183+ fi
184+ if ! api_health="$($DOCKER inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$api_container_id" 2>/dev/null)"; then
185+ api_container_id=""
186+ api_state="missing"
187+ api_health="missing"
188+ return 1
189+ fi
190+ if [ "$api_state" != "running" ] || [ "$api_health" != "healthy" ]; then
191+ return 1
192+ fi
193+ if ! $DOCKER exec "$api_container_id" /bin/sh -c 'wget -qO- http://127.0.0.1:8080/healthz >/dev/null'; then
194+ api_health="healthz_failed"
195+ return 1
196+ fi
197+ return 0
198+ }
199+ chmod 600 /opt/cixing/deploy.env
200+ chmod 644 /opt/cixing/config/config.yaml
201+ cleanup_existing_compose_state
202+ gzip -dc /opt/cixing/images/cixing-api.tar.gz | $DOCKER load
203+ gzip -dc /opt/cixing/images/postgres.tar.gz | $DOCKER load
204+ gzip -dc /opt/cixing/images/redis.tar.gz | $DOCKER load
205+ gzip -dc /opt/cixing/images/mailpit.tar.gz | $DOCKER load
206+ echo "starting dependency services"
207+ compose up -d postgres redis mailpit
208+ echo "running database migrations"
209+ compose run --rm -T migrate up </dev/null
210+ echo "database migrations completed"
211+ echo "starting api service"
212+ compose up -d api
213+
214+ attempts=0
215+ api_state=""
216+ api_health=""
217+ echo "waiting for api readiness"
218+ while [ "$attempts" -lt 30 ]; do
219+ if api_ready; then
220+ break
221+ fi
222+ if [ "$api_state" = "exited" ] || [ "$api_state" = "dead" ]; then
223+ break
224+ fi
225+ attempts=$((attempts + 1))
226+ sleep 4
227+ done
228+
229+ if ! api_ready; then
230+ dump_api_diagnostics
231+ if [ "$api_health" = "healthz_failed" ]; then
232+ echo "container /healthz check failed" >&2
233+ elif [ "$api_state" = "missing" ]; then
234+ echo "api container was not created" >&2
235+ else
236+ echo "api container state=$api_state health=$api_health" >&2
237+ fi
238+ exit 1
239+ fi
240+
241+ echo "api initial readiness check passed: container=$api_container_id state=$api_state health=$api_health"
242+ echo "rechecking api stability after warmup"
243+ sleep 12
244+
245+ if ! api_ready; then
246+ dump_api_diagnostics
247+ if [ "$api_health" = "healthz_failed" ]; then
248+ echo "api container became unstable after initial readiness: /healthz failed" >&2
249+ elif [ "$api_state" = "missing" ]; then
250+ echo "api container disappeared after initial readiness" >&2
251+ else
252+ echo "api container became unstable after initial readiness: state=$api_state health=$api_health" >&2
253+ fi
254+ exit 1
255+ fi
256+
257+ final_api_container_id="$(compose ps -q api)"
258+ if [ -z "$final_api_container_id" ]; then
259+ dump_api_diagnostics
260+ echo "api container disappeared before final status output" >&2
261+ exit 1
262+ fi
263+ final_api_state="$($DOCKER inspect --format '{{.State.Status}}' "$final_api_container_id")"
264+ final_api_health="$($DOCKER inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' "$final_api_container_id")"
265+ echo "final api status: container=$final_api_container_id state=$final_api_state health=$final_api_health"
266+ cleanup_deployment_artifacts
267+ compose ps
268+ compose logs --tail=80 api
129269 REMOTE
0 commit comments