Skip to content

Commit 96f6faf

Browse files
authored
fix(deploy): deploy test ECS by uploading images over SSH (#8)
* fix(deploy): upload images to ECS instead of pulling from Docker Hub * fix(deploy): allow container to read test config * fix(deploy): verify api health after test deployment * fix(deploy): verify api stability after test deployment * fix(deploy): clean stale compose state before test deployment * fix(deploy): log api deployment status explicitly * fix(deploy): prevent migrate step from consuming ssh input * ci(deploy): auto deploy test ECS on develop push * ci(ops): add test ECS ops workflow * ci(ops): add test ops workflow and reduce healthcheck noise * ci(ops): add OSS presign diagnostics
1 parent 0300f8d commit 96f6faf

3 files changed

Lines changed: 520 additions & 36 deletions

File tree

.github/workflows/deploy-test.yml

Lines changed: 172 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: deploy-test
22

33
on:
44
workflow_dispatch:
5+
push:
6+
branches:
7+
- develop
58

69
permissions:
710
contents: read
@@ -11,7 +14,10 @@ concurrency:
1114
cancel-in-progress: false
1215

1316
env:
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

1622
jobs:
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:
@@ -71,6 +76,9 @@ jobs:
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

Comments
 (0)