Skip to content

Commit bbc7053

Browse files
authored
refactor(scripts): consolidate build scripts into unified build.ts (#93)
2 parents 22efb1a + 52a1785 commit bbc7053

File tree

4 files changed

+199
-161
lines changed

4 files changed

+199
-161
lines changed

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"type": "module",
1010
"scripts": {
1111
"build": "pnpm --filter @chris-towles/blog run build",
12-
"build:container": "node --experimental-strip-types scripts/build-and-test.ts staging",
13-
"build:container:keep": "node --experimental-strip-types scripts/build-and-test.ts staging --keep",
12+
"build:container": "node --experimental-strip-types scripts/build.ts test staging",
13+
"build:container:keep": "node --experimental-strip-types scripts/build.ts test staging --keep",
1414
"dev": "pnpm --filter @chris-towles/blog run dev",
1515
"deploy": "pnpm --filter @chris-towles/blog run deploy:nuxthub",
1616
"changeset": "changeset",
@@ -20,9 +20,9 @@
2020
"gcp:staging:init": "cd infra/terraform/environments/staging && terraform init",
2121
"gcp:staging:plan": "cd infra/terraform/environments/staging && terraform plan",
2222
"gcp:staging:apply": "cd infra/terraform/environments/staging && terraform apply -auto-approve",
23-
"gcp:staging:deploy": "node --experimental-strip-types scripts/build-and-push.ts staging",
23+
"gcp:staging:deploy": "node --experimental-strip-types scripts/build.ts deploy staging",
2424
"gcp:staging:destroy": "cd infra/terraform/environments/staging && terraform destroy",
25-
"gcp:prod:deploy": "node --experimental-strip-types scripts/build-and-push.ts prod",
25+
"gcp:prod:deploy": "node --experimental-strip-types scripts/build.ts deploy prod",
2626
"db:generate": "pnpm --filter @chris-towles/blog run db:generate",
2727
"db:migrate": "pnpm --filter @chris-towles/blog run db:migrate",
2828
"docker:up": "docker compose up -d",

scripts/build-and-push.ts

Lines changed: 0 additions & 67 deletions
This file was deleted.

scripts/build-and-test.ts

Lines changed: 0 additions & 90 deletions
This file was deleted.

scripts/build.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#!/usr/bin/env node
2+
import 'zx/globals'
3+
4+
const args = process.argv.slice(2)
5+
const command = args[0]
6+
const environment = args[1] || 'staging'
7+
const tag = args[2] || 'latest'
8+
9+
// Configuration
10+
const IMAGE_NAME = 'blog-test'
11+
const CONTAINER_NAME = 'blog-test-container'
12+
const PORT = process.env.TEST_PORT || '3001'
13+
const MAX_WAIT = 60
14+
15+
function printUsage() {
16+
console.log(`
17+
Usage:
18+
build.ts test [environment] [--keep] - Build and test container locally
19+
build.ts deploy <environment> [tag] - Build, push and deploy to GCP Cloud Run
20+
21+
Arguments:
22+
environment - staging or prod (default: staging)
23+
tag - Docker image tag (default: latest)
24+
25+
Options:
26+
--keep - Keep container running after test (for test command)
27+
28+
Examples:
29+
build.ts test staging --keep
30+
build.ts deploy staging latest
31+
build.ts deploy prod v1.0.0
32+
`)
33+
}
34+
35+
async function cleanup() {
36+
console.log(chalk.yellow('\n🧹 Stopping and removing container...'))
37+
await $`docker rm -f ${CONTAINER_NAME}`.quiet().nothrow()
38+
}
39+
40+
async function waitForHealthy(): Promise<boolean> {
41+
console.log(chalk.yellow('\n⏳ Waiting for container to be ready...'))
42+
43+
const startTime = Date.now()
44+
let elapsed = 0
45+
46+
while (elapsed < MAX_WAIT) {
47+
// Check if container is still running
48+
const psResult = await $`docker ps --filter name=${CONTAINER_NAME} --format {{.Names}}`.quiet().nothrow()
49+
if (!psResult.stdout.includes(CONTAINER_NAME)) {
50+
console.log(chalk.red('❌ Container stopped unexpectedly'))
51+
await $`docker logs ${CONTAINER_NAME}`
52+
return false
53+
}
54+
55+
// Try to fetch homepage
56+
try {
57+
const response = await fetch(`http://localhost:${PORT}`, {
58+
signal: AbortSignal.timeout(2000)
59+
})
60+
61+
if (response.ok) {
62+
console.log(chalk.green('✅ Container is ready and home page is accessible!'))
63+
console.log(chalk.green(`✅ Home page returned HTTP ${response.status}`))
64+
console.log(chalk.green('\n🎉 All tests passed!'))
65+
return true
66+
} else {
67+
console.log(chalk.red(`❌ Home page returned HTTP ${response.status} (expected 200)`))
68+
return false
69+
}
70+
} catch {
71+
// Connection failed, wait and retry
72+
}
73+
74+
await sleep(2000)
75+
elapsed = Math.floor((Date.now() - startTime) / 1000)
76+
console.log(` Waiting... ${elapsed}s / ${MAX_WAIT}s`)
77+
}
78+
79+
console.log(chalk.red('❌ Timeout waiting for container to respond'))
80+
console.log(chalk.yellow('\nContainer logs:'))
81+
await $`docker logs ${CONTAINER_NAME}`
82+
return false
83+
}
84+
85+
async function testContainer() {
86+
try {
87+
console.log(chalk.yellow('🔨 Building Docker image...'))
88+
await $`docker build -t ${IMAGE_NAME} .`
89+
90+
console.log(chalk.yellow('\n🧹 Cleaning up any existing container...'))
91+
await $`docker rm -f ${CONTAINER_NAME}`.quiet().nothrow()
92+
93+
console.log(chalk.yellow(`\n🚀 Starting container on port ${PORT}...`))
94+
console.log(chalk.gray(`> docker run -d --name ${CONTAINER_NAME} -p ${PORT}:3000 ${IMAGE_NAME}`))
95+
await $`docker run -d --name ${CONTAINER_NAME} -p ${PORT}:3000 ${IMAGE_NAME}`
96+
97+
const success = await waitForHealthy()
98+
99+
if (!argv.keep) {
100+
await cleanup()
101+
} else {
102+
console.log(chalk.yellow('\nℹ️ Skipping cleanup as requested (--keep)'))
103+
}
104+
process.exit(success ? 0 : 1)
105+
} catch (error) {
106+
console.error(chalk.red('❌ Error:'), error)
107+
if (!argv.keep) {
108+
await cleanup()
109+
} else {
110+
console.log(chalk.yellow('\nℹ️ Skipping cleanup as requested (--keep)'))
111+
}
112+
process.exit(1)
113+
}
114+
}
115+
116+
async function deployContainer() {
117+
if (!['staging', 'prod'].includes(environment)) {
118+
console.error(chalk.red('❌ Invalid environment. Use "staging" or "prod"'))
119+
process.exit(1)
120+
}
121+
122+
const terraformDir = `infra/terraform/environments/${environment}`
123+
124+
if (!fs.existsSync(terraformDir)) {
125+
console.error(chalk.red(`❌ Terraform directory not found: ${terraformDir}`))
126+
process.exit(1)
127+
}
128+
129+
const rootDir = process.cwd()
130+
console.log(chalk.yellow(`🚀 Building and pushing container for ${environment}...`))
131+
132+
// Step 1: Get artifact registry URL from terraform output
133+
console.log(chalk.yellow('\n📦 Getting registry URL from Terraform...'))
134+
cd(terraformDir)
135+
let registry = ''
136+
try {
137+
registry = (await $`terraform output -raw container_image_base`).stdout.trim()
138+
} catch {
139+
console.log(chalk.yellow('⚠️ Could not get registry URL. Initializing infrastructure...'))
140+
await $`terraform init`
141+
await $`terraform apply -target=module.shared -auto-approve`
142+
registry = (await $`terraform output -raw container_image_base`).stdout.trim()
143+
}
144+
cd(rootDir)
145+
console.log(` Registry: ${registry}`)
146+
147+
// Step 2: Authenticate Docker
148+
console.log(chalk.yellow('\n🔐 Authenticating Docker...'))
149+
const registryHostname = registry.split('/')[0]
150+
await $`gcloud auth configure-docker ${registryHostname}`
151+
152+
// Step 3: Build the image
153+
const imageTag = `${registry}/blog:${tag}`
154+
console.log(chalk.yellow(`\n🔨 Building Docker image: ${imageTag}`))
155+
await $`docker build -t ${imageTag} .`
156+
157+
// Step 4: Push the image
158+
console.log(chalk.yellow(`\n📤 Pushing image to registry...`))
159+
await $`docker push ${imageTag}`
160+
161+
// Step 5: Update Cloud Run
162+
console.log(chalk.yellow(`\n☁️ Updating Cloud Run with new image...`))
163+
cd(terraformDir)
164+
$.verbose = true
165+
await $`terraform apply -auto-approve -var="container_image=${imageTag}"`
166+
$.verbose = false
167+
cd(rootDir)
168+
169+
console.log(chalk.green(`\n✅ Successfully deployed ${imageTag} to ${environment}!`))
170+
}
171+
172+
async function main() {
173+
if (!command || command === 'help' || command === '--help' || command === '-h') {
174+
printUsage()
175+
process.exit(0)
176+
}
177+
178+
switch (command) {
179+
case 'test':
180+
await testContainer()
181+
break
182+
case 'deploy':
183+
await deployContainer()
184+
break
185+
default:
186+
console.error(chalk.red(`❌ Unknown command: ${command}`))
187+
printUsage()
188+
process.exit(1)
189+
}
190+
}
191+
192+
main().catch((err) => {
193+
console.error(chalk.red('❌ Error:'), err)
194+
process.exit(1)
195+
})

0 commit comments

Comments
 (0)