Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
node_modules
npm-debug.log
dist
.git
.gitignore
README.md
deploy.md
.env
.env.local
.env.*.local
.DS_Store
.vscode
.idea
*.swp
*.swo
*~
.coverage
coverage/
.next
.nuxt
build/
.cache
.turbo
docs/
.github/
221 changes: 221 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
name: CI/CD Pipeline

on:
push:
branches: ["main", "develop"]
pull_request:
branches: ["main", "develop"]

env:
REGISTRY: docker.io
IMAGE_NAME: spectracleanse-api

jobs:
# ─────────────────────────────────────────────────────────────────────────
# LINTING & SECURITY
# ─────────────────────────────────────────────────────────────────────────
security:
name: Security & Audit
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci

- name: Run npm audit (info level allowed)
run: npm audit --audit-level=moderate || true

- name: Scan for secrets with TruffleHog
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug
Comment on lines +38 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 suggestion (security): Pin the TruffleHog action to a version or commit instead of using @main.

Referencing trufflesecurity/trufflehog@main means the workflow may change or break as the main branch evolves. For stability and supply-chain security, pin this action to a specific version tag or commit SHA (e.g. @v3 or @<sha>).

Suggested change
- name: Scan for secrets with TruffleHog
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug
- name: Scan for secrets with TruffleHog
uses: trufflesecurity/trufflehog@v3
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
extra_args: --debug


# ─────────────────────────────────────────────────────────────────────────
# BACKEND: TYPE-CHECK, LINT, SMOKE TEST
# ─────────────────────────────────────────────────────────────────────────
backend:
name: Backend – Type-check & Smoke Test
runs-on: ubuntu-latest
needs: security

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci

- name: Type-check TypeScript
run: npx tsc --noEmit

- name: Smoke test – server startup
timeout-minutes: 2
run: |
export JWT_SECRET=ci-test-secret-not-real
export STRIPE_SECRET_KEY=sk_test_ci_placeholder
export STRIPE_WEBHOOK_SECRET=whsec_ci_placeholder
export STRIPE_CREATOR_PRICE_ID=price_ci_creator
export STRIPE_STUDIO_PRICE_ID=price_ci_studio
export GEMINI_API_KEY=ci_gemini_placeholder
export FRONTEND_URL=http://localhost:5173
export DB_PATH=/tmp/spectra-ci.db
export PORT=3001

node server.js > /tmp/server.log 2>&1 &
SERVER_PID=$!

for i in $(seq 1 15); do
sleep 1
if curl -sf http://localhost:3001/api/health; then
echo "Server health check passed"
kill $SERVER_PID 2>/dev/null || true
exit 0
fi
done

echo "=== Server failed to start. Logs: ===" >&2
cat /tmp/server.log >&2
kill $SERVER_PID 2>/dev/null || true
exit 1

# ─────────────────────────────────────────────────────────────────────────
# FRONTEND: TYPE-CHECK & BUILD
# ─────────────────────────────────────────────────────────────────────────
frontend:
name: Frontend – Type-check & Build
runs-on: ubuntu-latest
needs: security

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
cache: "npm"
cache-dependency-path: package-lock.json

- name: Install dependencies
run: npm ci

- name: Type-check
run: npx tsc --noEmit

- name: Build frontend
env:
VITE_BACKEND_URL: https://spectracleanse.com
run: npm run build

- name: Upload frontend artifact
uses: actions/upload-artifact@v4
with:
name: frontend-dist
path: dist/
retention-days: 3

# ─────────────────────────────────────────────────────────────────────────
# DOCKER: BUILD & PUSH
# ─────────────────────────────────────────────────────────────────────────
docker-build:
name: Docker – Build & Push
runs-on: ubuntu-latest
needs: [backend, frontend]

permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-options: image=moby/buildkit:latest

- name: Extract image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ secrets.DOCKERHUB_USERNAME }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}

- name: Log in to Docker Hub (if pushing)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1

# ─────────────────────────────────────────────────────────────────────────
# DEPLOY: TRIGGER HYPERLIFT (only on main branch)
# ─────────────────────────────────────────────────────────────────────────
deploy:
name: Deploy to Hyperlift
runs-on: ubuntu-latest
needs: docker-build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Trigger Hyperlift webhook
if: ${{ secrets.HYPERLIFT_DEPLOY_HOOK != '' }}
run: |
curl -sf -X POST "${{ secrets.HYPERLIFT_DEPLOY_HOOK }}" \
-H "Content-Type: application/json" \
-d '{
"ref":"${{ github.sha }}",
"branch":"${{ github.ref_name }}",
"timestamp":"${{ github.event.head_commit.timestamp }}"
}'

- name: Deployment notification
run: |
echo "Deployment triggered for commit ${{ github.sha }}"
echo "Branch: ${{ github.ref_name }}"
echo "Author: ${{ github.actor }}"
66 changes: 66 additions & 0 deletions .github/workflows/staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Staging Validation

on:
workflow_run:
workflows: ["CI/CD Pipeline"]
branches: ["develop"]
types: [completed]

jobs:
staging-deploy:
name: Deploy to Staging
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
registry: docker.io
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push staging image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
push: true
tags: |
${{ secrets.DOCKERHUB_USERNAME }}/spectracleanse-api:staging-${{ github.event.workflow_run.head_commit.sha }}
Comment on lines +35 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Use workflow_run.head_sha instead of head_commit.sha for more robust tagging.

github.event.workflow_run.head_commit can be null (e.g., for some workflow_run events or deleted branches), so head_commit.sha isn’t always available. Using github.event.workflow_run.head_sha gives a reliable commit reference; please use that for both the image tag and the deployment payload.

${{ secrets.DOCKERHUB_USERNAME }}/spectracleanse-api:staging-latest
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Trigger staging deployment
if: ${{ secrets.STAGING_DEPLOY_HOOK != '' }}
run: |
curl -sf -X POST "${{ secrets.STAGING_DEPLOY_HOOK }}" \
-H "Content-Type: application/json" \
-d '{
"image":"${{ secrets.DOCKERHUB_USERNAME }}/spectracleanse-api:staging-${{ github.event.workflow_run.head_commit.sha }}",
"environment":"staging"
}'

- name: Run smoke tests on staging
run: |
echo "Waiting for staging deployment..."
sleep 10

if [ ! -z "${{ secrets.STAGING_URL }}" ]; then
for i in $(seq 1 10); do
if curl -sf "${{ secrets.STAGING_URL }}/api/health"; then
echo "Staging health check passed"
exit 0
fi
sleep 5
done
echo "Staging failed to respond"
exit 1
fi
Loading
Loading