Skip to content

Deploy

Deploy #10

Workflow file for this run

name: Deploy
on:
workflow_run:
workflows: ["CI"]
types: [completed]
branches: [main]
jobs:
deploy-backend:
name: Deploy Backend to Railway
runs-on: ubuntu-latest
# Only deploy if the CI run that triggered this workflow actually passed.
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Verify Railway secrets
run: |
test -n "$RAILWAY_API_TOKEN" || (echo "Missing RAILWAY_API_TOKEN GitHub repository secret" && exit 1)
test -n "$RAILWAY_PROJECT_ID" || (echo "Missing RAILWAY_PROJECT_ID GitHub repository secret" && exit 1)
test -n "$RAILWAY_SERVICE_ID" || (echo "Missing RAILWAY_SERVICE_ID GitHub repository secret" && exit 1)
env:
RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
RAILWAY_SERVICE_ID: ${{ secrets.RAILWAY_SERVICE_ID }}
- name: Install Railway CLI
run: npm install -g @railway/cli@5.2.0
# Deploy from the repo root (not backend/) because the Railway service
# is already configured with Root Directory=/backend, so Railway applies
# its own configured root when given the full repo as build context.
- name: Deploy to Railway
run: railway up --project "$RAILWAY_PROJECT_ID" --service "$RAILWAY_SERVICE_ID" --environment production --ci
env:
RAILWAY_API_TOKEN: ${{ secrets.RAILWAY_API_TOKEN }}
RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
RAILWAY_SERVICE_ID: ${{ secrets.RAILWAY_SERVICE_ID }}
- name: Verify production CORS
run: |
for attempt in {1..6}; do
headers="$(mktemp)"
status="$(
curl -sS -o /dev/null -D "$headers" -w "%{http_code}" \
-X OPTIONS "https://api.simpleinvoice.khangtran.dev/auth/login" \
-H "Origin: https://simpleinvoice.khangtran.dev" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type"
)"
if [ "$status" = "204" ] && grep -qi '^access-control-allow-origin: https://simpleinvoice.khangtran.dev' "$headers"; then
exit 0
fi
echo "Production CORS check failed on attempt $attempt; retrying..."
cat "$headers"
sleep 10
done
echo "Production API did not allow https://simpleinvoice.khangtran.dev after deploy."
exit 1
deploy-frontend:
name: Deploy Frontend to Cloudflare Pages
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
defaults:
run:
working-directory: frontend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: frontend/package-lock.json
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
env:
VITE_API_BASE_URL: https://api.simpleinvoice.khangtran.dev
- name: Verify Cloudflare secrets
run: |
test -n "$CLOUDFLARE_ACCOUNT_ID" || (echo "Missing CLOUDFLARE_ACCOUNT_ID GitHub repository secret" && exit 1)
test -n "$CLOUDFLARE_API_TOKEN" || (echo "Missing CLOUDFLARE_API_TOKEN GitHub repository secret" && exit 1)
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
# Cloudflare Pages Direct Upload: we build dist/ ourselves and push it
# straight to Cloudflare. No Cloudflare Git integration is used, and
# this is not `wrangler deploy` (that's for Workers, not Pages).
- name: Deploy to Cloudflare Pages
run: npx wrangler pages deploy dist --project-name simple-invoice --branch main
env:
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}