ci: add GitLab CI/CD pipeline with prod, staging, and MR preview environments
- main → production, staging branch → staging, MR → ephemeral preview
- All env config (URLs, Appwrite credentials) injected from GitLab CI/CD
variables at build time — no .env files committed
- Preview environments auto-deploy per MR with URL {mr-iid}.{BASE_DOMAIN}
and are torn down when the MR is closed
- Update .gitignore to exclude .env.production/.staging/.preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,6 +5,8 @@ dist-ssr
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
.env.production
|
.env.production
|
||||||
|
.env.staging
|
||||||
|
.env.preview
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
coverage
|
coverage
|
||||||
|
|||||||
174
.gitlab-ci.yml
Normal file
174
.gitlab-ci.yml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# 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
|
||||||
Reference in New Issue
Block a user