# ───────────────────────────────────────────────────────────────────────────── # BudgetWise CI/CD Pipeline # # Branch → Environment mapping: # main → production (https://{PROD_URL}) # staging → staging (https://{STAGING_URL}) # merge request → preview (https://{MR_IID}.{BASE_DOMAIN}) # # All config lives in GitLab CI/CD variables (Settings → CI/CD → Variables). # No .env files are committed — the build generates them at runtime. # # Required variables: # SSH_PRIVATE_KEY Private key with access to DEPLOY_HOST # DEPLOY_HOST Server hostname or IP # DEPLOY_USER SSH username on the server # DEPLOY_PATH Base deploy path e.g. /var/www/budget-app # # PROD_URL e.g. https://budget.kushalgaywala.com # PROD_APPWRITE_ENDPOINT e.g. https://appwrite.example.com/v1 # PROD_APPWRITE_PROJECT_ID # # STAGING_URL e.g. https://staging.budget.kushalgaywala.com # STAGING_APPWRITE_ENDPOINT # STAGING_APPWRITE_PROJECT_ID # # BASE_DOMAIN MR preview base e.g. budget.kushalgaywala.com # Preview URLs: https://{mr-iid}.{BASE_DOMAIN} # Preview shares staging Appwrite credentials. # # Server directory layout: # $DEPLOY_PATH/production/ ← prod # $DEPLOY_PATH/staging/ ← staging # $DEPLOY_PATH/mr-{iid}/ ← MR preview (removed on MR close) # ───────────────────────────────────────────────────────────────────────────── stages: - build - deploy .deploy_base: image: alpine:latest before_script: - apk add --no-cache rsync openssh-client - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh && chmod 700 ~/.ssh - ssh-keyscan -H "$DEPLOY_HOST" >> ~/.ssh/known_hosts # ── Production ──────────────────────────────────────────────────────────────── build:production: stage: build image: node:20-alpine variables: VITE_APP_URL: $PROD_URL VITE_APPWRITE_ENDPOINT: $PROD_APPWRITE_ENDPOINT VITE_APPWRITE_PROJECT_ID: $PROD_APPWRITE_PROJECT_ID script: - npm ci - npm run build - echo "VITE_APP_URL=$PROD_URL" > deploy.env artifacts: paths: - dist/ reports: dotenv: deploy.env expire_in: 1 hour rules: - if: $CI_COMMIT_BRANCH == "main" deploy:production: extends: .deploy_base stage: deploy environment: name: production url: $VITE_APP_URL script: - rsync -avz --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/production/" needs: - job: build:production artifacts: true rules: - if: $CI_COMMIT_BRANCH == "main" # ── Staging ─────────────────────────────────────────────────────────────────── build:staging: stage: build image: node:20-alpine variables: VITE_APP_URL: $STAGING_URL VITE_APPWRITE_ENDPOINT: $STAGING_APPWRITE_ENDPOINT VITE_APPWRITE_PROJECT_ID: $STAGING_APPWRITE_PROJECT_ID script: - npm ci - npx tsc --noEmit - npx vite build --mode staging - echo "VITE_APP_URL=$STAGING_URL" > deploy.env artifacts: paths: - dist/ reports: dotenv: deploy.env expire_in: 1 hour rules: - if: $CI_COMMIT_BRANCH == "staging" deploy:staging: extends: .deploy_base stage: deploy environment: name: staging url: $VITE_APP_URL script: - rsync -avz --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/staging/" needs: - job: build:staging artifacts: true rules: - if: $CI_COMMIT_BRANCH == "staging" # ── Preview / MR ────────────────────────────────────────────────────────────── # One ephemeral environment per MR: https://{mr-iid}.{BASE_DOMAIN} # Uses staging Appwrite credentials. # Environment is torn down automatically when the MR is closed. build:preview: stage: build image: node:20-alpine variables: VITE_APPWRITE_ENDPOINT: $STAGING_APPWRITE_ENDPOINT VITE_APPWRITE_PROJECT_ID: $STAGING_APPWRITE_PROJECT_ID script: - npm ci - npx tsc --noEmit - export MR_URL="https://$CI_MERGE_REQUEST_IID.$BASE_DOMAIN" - VITE_APP_URL=$MR_URL npx vite build --mode preview - echo "VITE_APP_URL=$MR_URL" > deploy.env artifacts: paths: - dist/ reports: dotenv: deploy.env expire_in: 1 day rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" deploy:preview: extends: .deploy_base stage: deploy environment: name: review/mr-$CI_MERGE_REQUEST_IID url: $VITE_APP_URL on_stop: stop:preview script: - rsync -avz --delete dist/ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/mr-$CI_MERGE_REQUEST_IID/" needs: - job: build:preview artifacts: true rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" stop:preview: extends: .deploy_base stage: deploy environment: name: review/mr-$CI_MERGE_REQUEST_IID action: stop script: - ssh "$DEPLOY_USER@$DEPLOY_HOST" "rm -rf $DEPLOY_PATH/mr-$CI_MERGE_REQUEST_IID" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: manual allow_failure: true