Merge pull request #297 from kw-coms/codex/p0-website-security-hardening #263
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 COMS Website | |
| on: | |
| push: | |
| branches: | |
| - main | |
| workflow_dispatch: | |
| inputs: | |
| deploy_ref: | |
| description: "Branch, tag, or commit SHA to deploy" | |
| required: false | |
| default: main | |
| concurrency: | |
| group: deploy-${{ github.workflow }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Deploy to COMS server | |
| env: | |
| DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} | |
| DEPLOY_USER: ${{ secrets.DEPLOY_USER }} | |
| DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || '22' }} | |
| DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} | |
| DEPLOY_PATH: ${{ secrets.DEPLOY_PATH || '~/apps/coms-website' }} | |
| DEPLOY_REPO: https://github.com/kw-coms/coms-website.git | |
| DEPLOY_REF: ${{ github.event.inputs.deploy_ref || github.ref_name }} | |
| PUBLIC_BASE_URL: ${{ secrets.PUBLIC_BASE_URL || 'https://coms.kw.ac.kr' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$DEPLOY_HOST" ] || [ -z "$DEPLOY_USER" ] || [ -z "$DEPLOY_SSH_KEY" ]; then | |
| echo "Missing deploy secrets. Set DEPLOY_HOST, DEPLOY_USER, and DEPLOY_SSH_KEY." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p ~/.ssh | |
| echo "$DEPLOY_SSH_KEY" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| ssh-keyscan -p "$DEPLOY_PORT" "$DEPLOY_HOST" >> ~/.ssh/known_hosts | |
| ssh -i ~/.ssh/deploy_key -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" \ | |
| "DEPLOY_PATH=${DEPLOY_PATH@Q} DEPLOY_REPO=${DEPLOY_REPO@Q} DEPLOY_REF=${DEPLOY_REF@Q} PUBLIC_BASE_URL=${PUBLIC_BASE_URL@Q} bash -c 'tmp=\$(mktemp /tmp/coms-deploy.XXXXXX); cat > \"\$tmp\"; bash \"\$tmp\"; status=\$?; rm -f \"\$tmp\"; exit \"\$status\"'" << 'REMOTE' | |
| set -euo pipefail | |
| DEPLOY_PATH="${DEPLOY_PATH/#\~/$HOME}" | |
| PUBLIC_BASE_URL="${PUBLIC_BASE_URL%/}" | |
| if ! command -v git >/dev/null 2>&1; then | |
| echo "git is required on the server." >&2 | |
| exit 1 | |
| fi | |
| if docker compose version >/dev/null 2>&1; then | |
| COMPOSE="docker compose" | |
| elif command -v docker-compose >/dev/null 2>&1; then | |
| COMPOSE="docker-compose" | |
| else | |
| echo "Docker Compose is required on the server." >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "$(dirname "$DEPLOY_PATH")" | |
| if [ ! -d "$DEPLOY_PATH/.git" ]; then | |
| git clone "$DEPLOY_REPO" "$DEPLOY_PATH" | |
| fi | |
| cd "$DEPLOY_PATH" | |
| git remote set-url origin "$DEPLOY_REPO" | |
| git fetch --prune origin "+refs/heads/*:refs/remotes/origin/*" "+refs/tags/*:refs/tags/*" | |
| if git rev-parse --verify --quiet "origin/$DEPLOY_REF^{commit}" >/dev/null; then | |
| git reset --hard "origin/$DEPLOY_REF" | |
| elif git rev-parse --verify --quiet "$DEPLOY_REF^{commit}" >/dev/null; then | |
| git reset --hard "$DEPLOY_REF" | |
| else | |
| git fetch origin "$DEPLOY_REF" | |
| git reset --hard FETCH_HEAD | |
| fi | |
| # Keep only persistent secrets/data; remove every other untracked or ignored build artifact. | |
| git clean -fdx -e .env -e backend-data/ -e backend/uploads/ -e uploads/ | |
| if [ ! -f .env ]; then | |
| echo "Missing $DEPLOY_PATH/.env on the server. Create it from .env.example before deploying." >&2 | |
| exit 1 | |
| fi | |
| if $COMPOSE --env-file .env -f docker-compose.yml exec -T db true >/dev/null 2>&1; then | |
| V38_SCRIPT="$($COMPOSE --env-file .env -f docker-compose.yml exec -T db sh -c \ | |
| 'if [ "$(psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT to_regclass('\''public.flyway_schema_history'\'')")" = "flyway_schema_history" ]; then | |
| psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Atc "SELECT script FROM flyway_schema_history WHERE version = '\''38'\'' AND success = true ORDER BY installed_rank DESC LIMIT 1" | |
| fi')" | |
| if [ -n "$V38_SCRIPT" ] && [ "$V38_SCRIPT" != "V38__mobile_push_tokens.sql" ]; then | |
| echo "Refusing deploy: Flyway V38 is recorded as $V38_SCRIPT, expected V38__mobile_push_tokens.sql." >&2 | |
| echo "Back up the database and repair its Flyway history before deploying this release." >&2 | |
| exit 1 | |
| fi | |
| fi | |
| (cd backend && ./gradlew clean test --no-daemon) | |
| $COMPOSE --env-file .env -f docker-compose.yml up -d --build --remove-orphans | |
| docker image prune -f | |
| for attempt in 1 2 3 4 5 6 7 8 9 10; do | |
| if curl -fsS http://127.0.0.1:8080/actuator/health; then | |
| break | |
| fi | |
| if [ "$attempt" -eq 10 ]; then | |
| $COMPOSE --env-file .env -f docker-compose.yml ps | |
| docker logs --tail=200 coms-backend | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| for attempt in 1 2 3 4 5 6 7 8 9 10; do | |
| if curl -fI "$PUBLIC_BASE_URL/" && curl -f "$PUBLIC_BASE_URL/api/server/time"; then | |
| break | |
| fi | |
| if [ "$attempt" -eq 10 ]; then | |
| $COMPOSE --env-file .env -f docker-compose.yml ps | |
| docker logs --tail=200 coms-backend | |
| docker logs --tail=100 coms-frontend | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| REMOTE |