diff --git a/README.md b/README.md index 1de7388..ec592be 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,20 @@ pnpm test:compose pnpm build ``` +## TeamCity deployment scripts + +TeamCity invokes repository-owned scripts rather than duplicating deployment logic in step configuration: + +```text +scripts/teamcity/validate.sh # install, lint, test, compose-invariant check, build +scripts/teamcity/build-images.sh # build + push immutable IMAGE_TAG images +scripts/teamcity/deploy.sh # pull, migrate, compose up -d, smoke test +scripts/teamcity/smoke.sh # health/root checks against APP_BASE_URL +scripts/teamcity/rollback.sh # redeploy the previous IMAGE_TAG +``` + +`deploy.sh` never runs `docker compose down` as part of a routine deployment; `rollback.sh` never attempts an automatic database downgrade. + ## Production topology Production Docker Compose (`compose.yml`) publishes **exactly one** host port, on the `edge` (Nginx) service, which serves the built Angular app and reverse-proxies `/api/*` and `/health/*` to the internal `api` service. `api`, `worker`, `postgres`, and `redis` are reachable only over the internal Docker network. See `docs/architecture/deployment.md` for the full contract and `scripts/teamcity/` for the TeamCity-invoked build/deploy/rollback scripts. diff --git a/backend/apps/api/src/migration.ts b/backend/apps/api/src/migration.ts new file mode 100644 index 0000000..6882009 --- /dev/null +++ b/backend/apps/api/src/migration.ts @@ -0,0 +1,8 @@ +export function runMigrations(): Promise { + console.log('No migrations configured in Phase 01'); + return Promise.resolve(); +} + +if (require.main === module) { + void runMigrations(); +} diff --git a/scripts/teamcity/build-images.sh b/scripts/teamcity/build-images.sh new file mode 100644 index 0000000..d4e3ffb --- /dev/null +++ b/scripts/teamcity/build-images.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${REGISTRY:?REGISTRY is required}" +: "${BUILD_NUMBER:?BUILD_NUMBER is required}" +: "${BUILD_VCS_NUMBER:?BUILD_VCS_NUMBER is required}" + +IMAGE_TAG="${BUILD_NUMBER}-${BUILD_VCS_NUMBER}" +export IMAGE_TAG + +if [[ "$IMAGE_TAG" == "latest" ]]; then + echo "Refusing floating deployment tag" >&2 + exit 1 +fi + +docker build --pull -f docker/edge.Dockerfile -t "${REGISTRY}/travel-edge:${IMAGE_TAG}" . +docker build --pull -f docker/api.Dockerfile -t "${REGISTRY}/travel-api:${IMAGE_TAG}" . +docker build --pull -f docker/worker.Dockerfile -t "${REGISTRY}/travel-worker:${IMAGE_TAG}" . + +docker push "${REGISTRY}/travel-edge:${IMAGE_TAG}" +docker push "${REGISTRY}/travel-api:${IMAGE_TAG}" +docker push "${REGISTRY}/travel-worker:${IMAGE_TAG}" + +printf '%s\n' "$IMAGE_TAG" diff --git a/scripts/teamcity/deploy.sh b/scripts/teamcity/deploy.sh new file mode 100644 index 0000000..27bc5a2 --- /dev/null +++ b/scripts/teamcity/deploy.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${IMAGE_TAG:?IMAGE_TAG is required}" +: "${REGISTRY:?REGISTRY is required}" +: "${APP_BASE_URL:?APP_BASE_URL is required}" + +exec 9>/var/lock/travel-planner-deploy.lock +flock -n 9 || { echo "Another deployment is already running" >&2; exit 1; } + +if [[ -f .deployed-image-tag ]]; then + cp .deployed-image-tag .previous-image-tag +fi + +export IMAGE_TAG REGISTRY + +docker compose pull edge api worker postgres redis +docker compose up -d postgres redis --wait --wait-timeout 120 +docker compose run --rm --no-deps api node backend/dist/apps/api/src/migration.js +docker compose up -d --remove-orphans --wait --wait-timeout 120 +scripts/teamcity/smoke.sh +printf '%s\n' "$IMAGE_TAG" > .deployed-image-tag diff --git a/scripts/teamcity/rollback.sh b/scripts/teamcity/rollback.sh new file mode 100644 index 0000000..6419300 --- /dev/null +++ b/scripts/teamcity/rollback.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${REGISTRY:?REGISTRY is required}" +: "${APP_BASE_URL:?APP_BASE_URL is required}" + +if [[ ! -s .previous-image-tag ]]; then + echo "No previous image tag is available for rollback" >&2 + exit 1 +fi + +IMAGE_TAG="$(cat .previous-image-tag)" +export IMAGE_TAG REGISTRY + +docker compose pull edge api worker +docker compose up -d --remove-orphans --wait --wait-timeout 120 +scripts/teamcity/smoke.sh +printf '%s\n' "$IMAGE_TAG" > .deployed-image-tag diff --git a/scripts/teamcity/smoke.sh b/scripts/teamcity/smoke.sh new file mode 100644 index 0000000..1dd32ff --- /dev/null +++ b/scripts/teamcity/smoke.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${APP_BASE_URL:?APP_BASE_URL is required}" +curl --fail --silent --show-error "${APP_BASE_URL}/health/live" +curl --fail --silent --show-error "${APP_BASE_URL}/health/ready" +curl --fail --silent --show-error "${APP_BASE_URL}/" >/dev/null diff --git a/scripts/teamcity/validate.sh b/scripts/teamcity/validate.sh new file mode 100644 index 0000000..6c90edd --- /dev/null +++ b/scripts/teamcity/validate.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail + +corepack enable || true +pnpm install --frozen-lockfile +pnpm lint +pnpm test +pnpm test:compose +pnpm build