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
57 changes: 57 additions & 0 deletions .github/workflows/deploy-finance-on-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Deploy Finance on Preview

permissions:
contents: read

on:
pull_request:
types: [opened, synchronize]
paths:
- "apps/finance/**"
- "packages/**"
- "scripts/deploy/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/deploy-finance-on-preview.yml"
workflow_dispatch:
inputs:
BRANCH:
description: "Branch to deploy"
required: true
default: "main"
type: "string"

defaults:
run:
working-directory: scripts/deploy

concurrency:
group: finance-preview-${{ github.event.pull_request.number || inputs.BRANCH || github.ref }}
cancel-in-progress: true

jobs:
deploy:
name: Deploy Finance
runs-on: ubuntu-latest
environment: Preview
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ vars.VERCEL_TEAM_ID }}
PROJECT_NAME: corp-web-micro-finance
BRANCH: ${{ github.event_name == 'pull_request' && github.head_ref || inputs.BRANCH }}
TARGET_ENV: preview
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: scripts/deploy/package-lock.json

- name: Install deploy dependencies
run: npm ci

- name: Run deploy script
run: npm run deploy
48 changes: 48 additions & 0 deletions .github/workflows/deploy-finance-on-production.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Deploy Finance on Production

permissions:
contents: read

on:
workflow_dispatch:
inputs:
BRANCH:
description: "Branch to deploy"
required: true
default: "main"
type: "string"

defaults:
run:
working-directory: scripts/deploy

concurrency:
group: finance-production-${{ inputs.BRANCH }}
cancel-in-progress: false

jobs:
deploy:
name: Deploy Finance
runs-on: ubuntu-latest
environment: Production
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ vars.VERCEL_TEAM_ID }}
PROJECT_NAME: corp-web-micro-finance
BRANCH: ${{ inputs.BRANCH }}
TARGET_ENV: production
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: scripts/deploy/package-lock.json

- name: Install deploy dependencies
run: npm ci

- name: Run deploy script
run: npm run deploy
58 changes: 58 additions & 0 deletions .github/workflows/deploy-finance-on-staging.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Deploy Finance on Staging

permissions:
contents: read

on:
push:
branches:
- main
paths:
- "apps/finance/**"
- "packages/**"
- "scripts/deploy/**"
- "package.json"
- "package-lock.json"
- ".github/workflows/deploy-finance-on-staging.yml"
workflow_dispatch:
inputs:
BRANCH:
description: "Branch to deploy"
required: true
default: "main"
type: "string"

defaults:
run:
working-directory: scripts/deploy

concurrency:
group: finance-staging-${{ github.event_name == 'push' && github.ref || inputs.BRANCH }}
cancel-in-progress: true

jobs:
deploy:
name: Deploy Finance
runs-on: ubuntu-latest
environment: Staging
env:
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
VERCEL_TEAM_ID: ${{ vars.VERCEL_TEAM_ID }}
PROJECT_NAME: corp-web-micro-finance
BRANCH: ${{ github.event_name == 'push' && github.ref_name || inputs.BRANCH }}
TARGET_ENV: staging
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
cache-dependency-path: scripts/deploy/package-lock.json

- name: Install deploy dependencies
run: npm ci

- name: Run deploy script
run: npm run deploy
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,22 @@ npm run dev --workspace @querypie/microsite-finance

Create one Vercel project per app. For the finance site, use:

- Vercel project: `corp-web-micro-finance`
- Root directory: `apps/finance`
- Build command: `npm run build`
- Install command: `npm install`
- Output: Next.js default
- Automatic Vercel Git deployments: disabled in `apps/finance/vercel.json` because GitHub Actions owns deployment orchestration

Finance deployments are managed by project-specific GitHub Actions workflows:

- `Deploy Finance on Preview` — `.github/workflows/deploy-finance-on-preview.yml`
- `Deploy Finance on Staging` — `.github/workflows/deploy-finance-on-staging.yml`
- `Deploy Finance on Production` — `.github/workflows/deploy-finance-on-production.yml`

The workflows use `scripts/deploy` and require:

- Secret: `VERCEL_TOKEN`
- Variable: `VERCEL_TEAM_ID`

Common company links should continue to point to `https://querypie.ai`.
12 changes: 12 additions & 0 deletions apps/finance/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "nextjs",
"installCommand": "npm install",
"buildCommand": "npm run build",
"devCommand": "npm run dev",
"outputDirectory": ".next",
"regions": ["hnd1"],
"git": {
"deploymentEnabled": false
}
}
144 changes: 144 additions & 0 deletions scripts/deploy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { Vercel } from '@vercel/sdk';
import dotenv from 'dotenv';

dotenv.config({ path: '.env' });

const vercel = new Vercel({
bearerToken: process.env.VERCEL_TOKEN,
});

const teamId = process.env.VERCEL_TEAM_ID;
const targetEnv = process.env.TARGET_ENV;
const branch = process.env.BRANCH;
const projectName = process.env.PROJECT_NAME;
const repo = process.env.GITHUB_REPO || 'corp-web-micro';
const org = process.env.GITHUB_ORG || 'querypie';

const POLL_INTERVAL_MS = 5_000;
const POLL_TIMEOUT_MS = 10 * 60 * 1_000;
const MAX_RETRIES = 2;
const RETRY_DELAY_MS = 15_000;

const IN_PROGRESS_STATUSES = new Set([
'QUEUED',
'INITIALIZING',
'ANALYZING',
'BUILDING',
'DEPLOYING',
]);

function requireEnv(name, value) {
if (!value) {
throw new Error(`${name} is required`);
}
}

function deploymentTarget() {
if (targetEnv === 'preview') {
return undefined;
}

return targetEnv;
}

async function createDeployment() {
requireEnv('VERCEL_TOKEN', process.env.VERCEL_TOKEN);
requireEnv('VERCEL_TEAM_ID', teamId);
requireEnv('PROJECT_NAME', projectName);
requireEnv('TARGET_ENV', targetEnv);
requireEnv('BRANCH', branch);

return vercel.deployments.createDeployment({
teamId,
requestBody: {
name: projectName,
target: deploymentTarget(),
gitSource: {
type: 'github',
repo,
ref: branch,
org,
},
},
});
}

async function pollDeployment(deploymentId) {
const startTime = Date.now();

while (true) {
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));

if (Date.now() - startTime > POLL_TIMEOUT_MS) {
throw new Error(`Deployment polling timed out after ${POLL_TIMEOUT_MS / 1_000}s`);
}

let statusResponse;
try {
statusResponse = await vercel.deployments.getDeployment({
idOrUrl: deploymentId,
withGitRepoInfo: 'true',
});
} catch (error) {
if (error.statusCode === 404) {
const err = new Error(
'Deployment was removed (HTTP 404). ' +
'This typically happens when Vercel auto-cancels a deployment ' +
'because a newer one was triggered for the same branch.',
);
err.cancelled = true;
throw err;
}
throw error;
}

const { status, url } = statusResponse;
console.log(`Deployment status: ${status}`);

if (IN_PROGRESS_STATUSES.has(status)) {
continue;
}

if (status === 'READY') {
return url;
}

const err = new Error(`Deployment ended with status: ${status}`);
err.cancelled = status === 'CANCELED';
throw err;
}
}

async function createAndCheckDeployment() {
console.log(
`Creating deployment: project=[${projectName}], target=[${targetEnv}], branch=[${branch}], repo=[${org}/${repo}]`,
);

const createResponse = await createDeployment();
console.log(`Deployment created: ID ${createResponse.id}, status ${createResponse.status}`);

const url = await pollDeployment(createResponse.id);
console.log(`Deployment successful: ${url}`);
}

(async () => {
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
await createAndCheckDeployment();
return;
} catch (error) {
if (error.cancelled && attempt < MAX_RETRIES) {
console.log(
`Attempt ${attempt}/${MAX_RETRIES} failed: ${error.message}
` +
`Retrying in ${RETRY_DELAY_MS / 1_000}s...`,
);
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
continue;
}

console.error(`Error: ${error.message}`);
process.exit(1);
}
}
})();
Loading
Loading