Skip to content

Commit 93e8e33

Browse files
committed
Refactor build-and-push workflow and Dockerfiles for improved service handling
- Updated the build-and-push workflow to support building all services when an API tag is pushed, enhancing flexibility in deployment. - Implemented a verification step for built images, ensuring critical dependencies are present and APP_VERSION is correctly set. - Enhanced Dockerfiles for commit-worker and user-worker to include robust error handling for npm installations and critical dependency checks. - Added versioning support in Dockerfiles by introducing a build argument `VERSION`, which is set as an environment variable `APP_VERSION` for runtime access.
1 parent c8f3c01 commit 93e8e33

3 files changed

Lines changed: 239 additions & 73 deletions

File tree

.github/workflows/build-and-push.yml

Lines changed: 137 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@ jobs:
1818
run: |
1919
TAG=${GITHUB_REF#refs/tags/}
2020
# Extract service name and version from tag
21-
# api-v1.2.3 -> service=api, version=1.2.3
21+
# api-v1.2.3 -> service=api, version=1.2.3 (builds all services)
22+
# commit-worker-v1.2.3 -> service=commit-worker, version=1.2.3
23+
# user-worker-v1.2.3 -> service=user-worker, version=1.2.3
2224
if [[ $TAG =~ ^api-v(.+)$ ]]; then
2325
echo "SERVICE=api" >> $GITHUB_OUTPUT
2426
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
27+
echo "BUILD_ALL=true" >> $GITHUB_OUTPUT
2528
echo "DOCKERFILE=backend/Dockerfile.prod" >> $GITHUB_OUTPUT
2629
elif [[ $TAG =~ ^commit-worker-v(.+)$ ]]; then
2730
echo "SERVICE=commit-worker" >> $GITHUB_OUTPUT
2831
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
32+
echo "BUILD_ALL=false" >> $GITHUB_OUTPUT
2933
echo "DOCKERFILE=backend/Dockerfile.cloudrun-commit-worker" >> $GITHUB_OUTPUT
3034
elif [[ $TAG =~ ^user-worker-v(.+)$ ]]; then
3135
echo "SERVICE=user-worker" >> $GITHUB_OUTPUT
3236
echo "VERSION=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
37+
echo "BUILD_ALL=false" >> $GITHUB_OUTPUT
3338
echo "DOCKERFILE=backend/Dockerfile.cloudrun-user-worker" >> $GITHUB_OUTPUT
3439
else
3540
echo "❌ Error: Invalid tag format"
@@ -58,85 +63,148 @@ jobs:
5863
fi
5964
echo "✅ package-lock.json found"
6065
61-
- name: Build and push image
66+
- name: Build and push images
6267
id: build
6368
run: |
64-
IMAGE_TAG=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/github-scraper/${{ steps.extract.outputs.SERVICE }}:${{ steps.extract.outputs.VERSION }}
65-
LATEST_TAG=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/github-scraper/${{ steps.extract.outputs.SERVICE }}:latest
66-
67-
echo "🔨 Building image..."
68-
docker buildx build \
69-
--platform linux/amd64 \
70-
-f ${{ steps.extract.outputs.DOCKERFILE }} \
71-
--build-arg VERSION=${{ steps.extract.outputs.VERSION }} \
72-
--cache-from type=registry,ref=${IMAGE_TAG} \
73-
--cache-from type=registry,ref=${LATEST_TAG} \
74-
-t ${IMAGE_TAG} \
75-
-t ${LATEST_TAG} \
76-
--push \
77-
--provenance=false \
78-
--sbom=false \
79-
./backend
80-
81-
echo "IMAGE_TAG=${IMAGE_TAG}" >> $GITHUB_OUTPUT
82-
echo "LATEST_TAG=${LATEST_TAG}" >> $GITHUB_OUTPUT
83-
echo "✅ Image built and pushed successfully"
84-
85-
- name: Verify image in registry
86-
run: |
87-
IMAGE_TAG="${{ steps.build.outputs.IMAGE_TAG }}"
88-
EXPECTED_VERSION="${{ steps.extract.outputs.VERSION }}"
89-
90-
echo "🔍 Verifying image in registry..."
91-
92-
# Pull and verify APP_VERSION
93-
docker pull ${IMAGE_TAG}
94-
95-
# Verify APP_VERSION is set
96-
APP_VERSION=$(docker inspect ${IMAGE_TAG} --format='{{range .Config.Env}}{{println .}}{{end}}' | grep "^APP_VERSION=" | cut -d= -f2 || echo "")
97-
98-
if [ "$APP_VERSION" != "$EXPECTED_VERSION" ]; then
99-
echo "❌ ERROR: APP_VERSION mismatch!"
100-
echo " Expected: ${EXPECTED_VERSION}"
101-
echo " Found: ${APP_VERSION:-not set}"
102-
exit 1
103-
fi
104-
echo "✅ APP_VERSION verified: ${APP_VERSION}"
105-
106-
# Verify node_modules exists (quick check)
107-
if ! docker run --rm --platform linux/amd64 --entrypoint /bin/sh ${IMAGE_TAG} -c "test -d /app/node_modules && test -f /app/node_modules/.bin/prisma && test -d /app/node_modules/fastify" 2>/dev/null; then
108-
echo "❌ ERROR: Critical dependencies missing in image!"
109-
echo " Checking what's in /app:"
110-
docker run --rm --platform linux/amd64 --entrypoint /bin/sh ${IMAGE_TAG} -c "ls -la /app" || true
111-
exit 1
69+
VERSION="${{ steps.extract.outputs.VERSION }}"
70+
BUILD_ALL="${{ steps.extract.outputs.BUILD_ALL }}"
71+
REGISTRY=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/github-scraper
72+
73+
# Define all services to build
74+
if [ "$BUILD_ALL" = "true" ]; then
75+
# Build all services when API tag is pushed
76+
SERVICES=("api:backend/Dockerfile.prod" "commit-worker:backend/Dockerfile.cloudrun-commit-worker" "user-worker:backend/Dockerfile.cloudrun-user-worker")
77+
echo "🔨 Building all services (API, Commit Worker, User Worker) with version ${VERSION}..."
78+
else
79+
# Build only the specific service
80+
SERVICES=("${{ steps.extract.outputs.SERVICE }}:${{ steps.extract.outputs.DOCKERFILE }}")
81+
echo "🔨 Building ${{ steps.extract.outputs.SERVICE }} with version ${VERSION}..."
11282
fi
113-
echo "✅ Dependencies verified: node_modules, prisma, fastify all present"
11483
115-
echo "✅ Image verification passed"
116-
echo "✅ Image ready: ${{ steps.extract.outputs.SERVICE }}:${{ steps.extract.outputs.VERSION }}"
117-
echo " Version injected via build arg: ${{ steps.extract.outputs.VERSION }}"
84+
BUILT_SERVICES=()
85+
86+
# Build each service
87+
for SERVICE_CONFIG in "${SERVICES[@]}"; do
88+
SERVICE_NAME=$(echo $SERVICE_CONFIG | cut -d: -f1)
89+
DOCKERFILE=$(echo $SERVICE_CONFIG | cut -d: -f2)
90+
IMAGE_TAG=${REGISTRY}/${SERVICE_NAME}:${VERSION}
91+
LATEST_TAG=${REGISTRY}/${SERVICE_NAME}:latest
92+
93+
echo ""
94+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
95+
echo "📦 Building ${SERVICE_NAME}:${VERSION}"
96+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
97+
98+
docker buildx build \
99+
--platform linux/amd64 \
100+
-f ${DOCKERFILE} \
101+
--build-arg VERSION=${VERSION} \
102+
--cache-from type=registry,ref=${IMAGE_TAG} \
103+
--cache-from type=registry,ref=${LATEST_TAG} \
104+
-t ${IMAGE_TAG} \
105+
-t ${LATEST_TAG} \
106+
--push \
107+
--provenance=false \
108+
--sbom=false \
109+
./backend
110+
111+
BUILT_SERVICES+=("${SERVICE_NAME}")
112+
echo "✅ ${SERVICE_NAME}:${VERSION} built and pushed"
113+
done
114+
115+
# Output built services as JSON array for later steps
116+
echo "BUILT_SERVICES=$(IFS=','; echo "${BUILT_SERVICES[*]}")" >> $GITHUB_OUTPUT
117+
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
118+
echo ""
119+
echo "✅ All images built and pushed successfully"
120+
121+
- name: Verify images in registry
122+
run: |
123+
REGISTRY=${{ secrets.GCP_REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/github-scraper
124+
EXPECTED_VERSION="${{ steps.build.outputs.VERSION }}"
125+
BUILT_SERVICES="${{ steps.build.outputs.BUILT_SERVICES }}"
126+
127+
echo "🔍 Verifying images in registry..."
128+
echo ""
129+
130+
# Convert comma-separated string to array
131+
IFS=',' read -ra SERVICES <<< "$BUILT_SERVICES"
132+
133+
for SERVICE_NAME in "${SERVICES[@]}"; do
134+
IMAGE_TAG=${REGISTRY}/${SERVICE_NAME}:${EXPECTED_VERSION}
135+
136+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
137+
echo "🔍 Verifying ${SERVICE_NAME}:${EXPECTED_VERSION}"
138+
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
139+
140+
# Pull and verify APP_VERSION
141+
docker pull ${IMAGE_TAG}
142+
143+
# Verify APP_VERSION is set
144+
APP_VERSION=$(docker inspect ${IMAGE_TAG} --format='{{range .Config.Env}}{{println .}}{{end}}' | grep "^APP_VERSION=" | cut -d= -f2 || echo "")
145+
146+
if [ "$APP_VERSION" != "$EXPECTED_VERSION" ]; then
147+
echo "❌ ERROR: APP_VERSION mismatch for ${SERVICE_NAME}!"
148+
echo " Expected: ${EXPECTED_VERSION}"
149+
echo " Found: ${APP_VERSION:-not set}"
150+
exit 1
151+
fi
152+
echo "✅ APP_VERSION verified: ${APP_VERSION}"
153+
154+
# Verify node_modules exists (quick check)
155+
if ! docker run --rm --platform linux/amd64 --entrypoint /bin/sh ${IMAGE_TAG} -c "test -d /app/node_modules && test -f /app/node_modules/.bin/prisma" 2>/dev/null; then
156+
echo "❌ ERROR: Critical dependencies missing in ${SERVICE_NAME} image!"
157+
echo " Checking what's in /app:"
158+
docker run --rm --platform linux/amd64 --entrypoint /bin/sh ${IMAGE_TAG} -c "ls -la /app" || true
159+
exit 1
160+
fi
161+
echo "✅ Dependencies verified: node_modules, prisma present"
162+
echo ""
163+
done
164+
165+
echo "✅ All images verified successfully"
118166
119167
- name: Cleanup old images
120168
if: success() # Only run if build succeeded
121169
run: |
122-
# Cleanup old images (keeps last 3 versions + latest + deployed version)
170+
# Cleanup old images for all built services
123171
export PROJECT_ID=${{ secrets.PROJECT_ID }}
124172
export REGION=${{ secrets.GCP_REGION }}
125173
export REPOSITORY=github-scraper
126-
./scripts/utils/cleanup-old-images.sh ${{ steps.extract.outputs.SERVICE }} --execute
174+
BUILT_SERVICES="${{ steps.build.outputs.BUILT_SERVICES }}"
175+
176+
# Convert comma-separated string to array
177+
IFS=',' read -ra SERVICES <<< "$BUILT_SERVICES"
178+
179+
for SERVICE_NAME in "${SERVICES[@]}"; do
180+
echo "🧹 Cleaning up old images for ${SERVICE_NAME}..."
181+
./scripts/utils/cleanup-old-images.sh ${SERVICE_NAME} --execute || echo "⚠️ Cleanup failed for ${SERVICE_NAME}, continuing..."
182+
done
127183
continue-on-error: true # Don't fail workflow if cleanup fails
128184

129-
- name: Trigger deployment in infra repo
185+
- name: Trigger deployments in infra repo
130186
run: |
131-
# Automatically trigger deployment in infra repo (Option C)
132-
curl -X POST https://api.github.com/repos/aalexmrt/github-scraper-infra/dispatches \
133-
-H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \
134-
-H "Accept: application/vnd.github.v3+json" \
135-
-d '{
136-
"event_type":"deploy",
137-
"client_payload":{
138-
"service":"${{ steps.extract.outputs.SERVICE }}",
139-
"version":"${{ steps.extract.outputs.VERSION }}"
140-
}
141-
}'
142-
echo "✅ Deployment triggered for ${{ steps.extract.outputs.SERVICE }}:${{ steps.extract.outputs.VERSION }}"
187+
# Automatically trigger deployment in infra repo for all built services
188+
BUILT_SERVICES="${{ steps.build.outputs.BUILT_SERVICES }}"
189+
VERSION="${{ steps.build.outputs.VERSION }}"
190+
191+
# Convert comma-separated string to array
192+
IFS=',' read -ra SERVICES <<< "$BUILT_SERVICES"
193+
194+
for SERVICE_NAME in "${SERVICES[@]}"; do
195+
echo "🚀 Triggering deployment for ${SERVICE_NAME}:${VERSION}..."
196+
curl -X POST https://api.github.com/repos/aalexmrt/github-scraper-infra/dispatches \
197+
-H "Authorization: token ${{ secrets.DEPLOY_TOKEN }}" \
198+
-H "Accept: application/vnd.github.v3+json" \
199+
-d "{
200+
\"event_type\":\"deploy\",
201+
\"client_payload\":{
202+
\"service\":\"${SERVICE_NAME}\",
203+
\"version\":\"${VERSION}\"
204+
}
205+
}"
206+
echo "✅ Deployment triggered for ${SERVICE_NAME}:${VERSION}"
207+
done
208+
209+
echo ""
210+
echo "✅ All deployments triggered successfully"

backend/Dockerfile.cloudrun-commit-worker

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,25 @@ COPY prisma ./prisma/
1010
COPY tsconfig.json ./
1111

1212
# Install all dependencies (including dev dependencies for building)
13-
RUN npm ci
13+
# Verify package-lock.json exists, then retry npm ci up to 3 times on failure
14+
RUN if [ ! -f package-lock.json ]; then \
15+
echo "❌ ERROR: package-lock.json is missing! It must be committed to git."; \
16+
echo " Run: git add backend/package-lock.json && git commit -m 'Add package-lock.json'"; \
17+
exit 1; \
18+
fi && \
19+
for i in 1 2 3; do \
20+
if npm ci; then \
21+
echo "✅ Dependencies installed successfully (attempt $i)"; \
22+
break; \
23+
else \
24+
echo "⚠️ npm ci failed (attempt $i/3), retrying..."; \
25+
if [ $i -eq 3 ]; then \
26+
echo "❌ ERROR: npm ci failed after 3 attempts"; \
27+
exit 1; \
28+
fi; \
29+
sleep 10; \
30+
fi; \
31+
done
1432

1533
# Copy application code
1634
COPY src ./src
@@ -21,6 +39,11 @@ RUN npx prisma generate && npm run build
2139
# Production stage
2240
FROM node:22.11.0-alpine
2341

42+
# Accept version as build argument
43+
ARG VERSION
44+
# Set as environment variable so it's available at runtime
45+
ENV APP_VERSION=${VERSION}
46+
2447
WORKDIR /app
2548

2649
# Install git and other required tools for git operations
@@ -31,7 +54,33 @@ COPY package*.json ./
3154
COPY prisma ./prisma/
3255

3356
# Install only production dependencies
34-
RUN npm ci --only=production
57+
# Verify package-lock.json exists, then retry npm ci up to 3 times on failure
58+
RUN if [ ! -f package-lock.json ]; then \
59+
echo "❌ ERROR: package-lock.json is missing! It must be committed to git."; \
60+
echo " Run: git add backend/package-lock.json && git commit -m 'Add package-lock.json'"; \
61+
exit 1; \
62+
fi && \
63+
for i in 1 2 3; do \
64+
if npm ci --omit=dev; then \
65+
echo "✅ Production dependencies installed successfully (attempt $i)"; \
66+
break; \
67+
else \
68+
echo "⚠️ npm ci failed (attempt $i/3), retrying..."; \
69+
if [ $i -eq 3 ]; then \
70+
echo "❌ ERROR: npm ci failed after 3 attempts"; \
71+
exit 1; \
72+
fi; \
73+
sleep 10; \
74+
fi; \
75+
done && \
76+
# Verify critical dependencies are installed
77+
if [ ! -d "node_modules" ] || [ ! -f "node_modules/.bin/prisma" ]; then \
78+
echo "❌ ERROR: Critical dependencies missing after installation"; \
79+
echo " node_modules exists: $([ -d node_modules ] && echo yes || echo no)"; \
80+
echo " prisma binary exists: $([ -f node_modules/.bin/prisma ] && echo yes || echo no)"; \
81+
exit 1; \
82+
fi && \
83+
echo "✅ Verified: node_modules and prisma are present"
3584

3685
# Copy built files from builder
3786
COPY --from=builder /app/dist ./dist

backend/Dockerfile.cloudrun-user-worker

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,25 @@ COPY prisma ./prisma/
1010
COPY tsconfig.json ./
1111

1212
# Install all dependencies (including dev dependencies for building)
13-
RUN npm ci
13+
# Verify package-lock.json exists, then retry npm ci up to 3 times on failure
14+
RUN if [ ! -f package-lock.json ]; then \
15+
echo "❌ ERROR: package-lock.json is missing! It must be committed to git."; \
16+
echo " Run: git add backend/package-lock.json && git commit -m 'Add package-lock.json'"; \
17+
exit 1; \
18+
fi && \
19+
for i in 1 2 3; do \
20+
if npm ci; then \
21+
echo "✅ Dependencies installed successfully (attempt $i)"; \
22+
break; \
23+
else \
24+
echo "⚠️ npm ci failed (attempt $i/3), retrying..."; \
25+
if [ $i -eq 3 ]; then \
26+
echo "❌ ERROR: npm ci failed after 3 attempts"; \
27+
exit 1; \
28+
fi; \
29+
sleep 10; \
30+
fi; \
31+
done
1432

1533
# Copy application code
1634
COPY src ./src
@@ -21,6 +39,11 @@ RUN npx prisma generate && npm run build
2139
# Production stage
2240
FROM node:22.11.0-alpine
2341

42+
# Accept version as build argument
43+
ARG VERSION
44+
# Set as environment variable so it's available at runtime
45+
ENV APP_VERSION=${VERSION}
46+
2447
WORKDIR /app
2548

2649
# Install git (not strictly needed for user worker, but keeping consistent)
@@ -31,7 +54,33 @@ COPY package*.json ./
3154
COPY prisma ./prisma/
3255

3356
# Install only production dependencies
34-
RUN npm ci --only=production
57+
# Verify package-lock.json exists, then retry npm ci up to 3 times on failure
58+
RUN if [ ! -f package-lock.json ]; then \
59+
echo "❌ ERROR: package-lock.json is missing! It must be committed to git."; \
60+
echo " Run: git add backend/package-lock.json && git commit -m 'Add package-lock.json'"; \
61+
exit 1; \
62+
fi && \
63+
for i in 1 2 3; do \
64+
if npm ci --omit=dev; then \
65+
echo "✅ Production dependencies installed successfully (attempt $i)"; \
66+
break; \
67+
else \
68+
echo "⚠️ npm ci failed (attempt $i/3), retrying..."; \
69+
if [ $i -eq 3 ]; then \
70+
echo "❌ ERROR: npm ci failed after 3 attempts"; \
71+
exit 1; \
72+
fi; \
73+
sleep 10; \
74+
fi; \
75+
done && \
76+
# Verify critical dependencies are installed
77+
if [ ! -d "node_modules" ] || [ ! -f "node_modules/.bin/prisma" ]; then \
78+
echo "❌ ERROR: Critical dependencies missing after installation"; \
79+
echo " node_modules exists: $([ -d node_modules ] && echo yes || echo no)"; \
80+
echo " prisma binary exists: $([ -f node_modules/.bin/prisma ] && echo yes || echo no)"; \
81+
exit 1; \
82+
fi && \
83+
echo "✅ Verified: node_modules and prisma are present"
3584

3685
# Copy built files from builder
3786
COPY --from=builder /app/dist ./dist

0 commit comments

Comments
 (0)