返回 AiToEarn
backend-build.yml
根目录 / .github / workflows / backend-build.yml
1 name: Build Backend Images
2
3 on:
4 push:
5 branches:
6 - main
7 paths:
8 - 'project/aitoearn-backend/**'
9 - '.github/workflows/backend-build.yml'
10 workflow_dispatch:
11 inputs:
12 build_aitoearn_ai:
13 description: 'aitoearn-ai'
14 required: false
15 type: boolean
16 default: false
17 build_aitoearn_server:
18 description: 'aitoearn-server'
19 required: false
20 type: boolean
21 default: false
22 push_latest:
23 description: 'Push as latest tag'
24 required: false
25 type: boolean
26 default: false
27 workflow_call:
28 inputs:
29 base_ref:
30 description: 'Base branch for affected detection (for PR preview)'
31 required: false
32 type: string
33 default: ''
34 manual_apps:
35 description: 'Comma-separated list of apps to build (for manual deployment)'
36 required: false
37 type: string
38 default: ''
39 outputs:
40 has_changes:
41 description: 'Whether any apps were affected'
42 value: ${{ jobs.generate-matrix.outputs.has_changes }}
43
44 permissions:
45 contents: read
46 actions: write
47
48 env:
49 DOCKER_HUB_USERNAME: aitoearn
50 NX_NO_CLOUD: true
51 MONOREPO_DIR: project/aitoearn-backend
52
53 jobs:
54 generate-matrix:
55 runs-on: ubuntu-latest
56 outputs:
57 matrix: ${{ steps.set-matrix.outputs.matrix }}
58 has_changes: ${{ steps.set-matrix.outputs.has_changes }}
59 version: ${{ steps.version.outputs.version }}
60
61 steps:
62 - name: Checkout
63 uses: actions/checkout@v5
64 with:
65 fetch-depth: 0
66
67 - name: Install pnpm
68 uses: pnpm/action-setup@v4
69 with:
70 version: 10
71
72 - name: Setup Node.js
73 uses: actions/setup-node@v4
74 with:
75 node-version: 22
76 cache: pnpm
77 cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
78
79 - name: Install dependencies
80 working-directory: ${{ env.MONOREPO_DIR }}
81 run: pnpm install --frozen-lockfile
82
83 - name: Parse manual app selection
84 id: parse-manual
85 run: |
86 MANUAL_APPS=""
87
88 # If manual_apps passed via workflow_call, use directly
89 if [ -n "${{ inputs.manual_apps }}" ]; then
90 MANUAL_APPS="${{ inputs.manual_apps }}"
91 echo "✅ Using workflow_call manual apps: $MANUAL_APPS"
92 else
93 # Parse workflow_dispatch boolean inputs
94 if [ "${{ inputs.build_aitoearn_ai }}" = "true" ]; then
95 MANUAL_APPS="aitoearn-ai"
96 fi
97
98 if [ "${{ inputs.build_aitoearn_server }}" = "true" ]; then
99 if [ -n "$MANUAL_APPS" ]; then
100 MANUAL_APPS="$MANUAL_APPS,aitoearn-server"
101 else
102 MANUAL_APPS="aitoearn-server"
103 fi
104 fi
105
106 if [ -n "$MANUAL_APPS" ]; then
107 echo "✅ Manually selected apps: $MANUAL_APPS"
108 else
109 echo "⚠️ No apps manually selected"
110 fi
111 fi
112
113 echo "manual_apps=$MANUAL_APPS" >> $GITHUB_OUTPUT
114
115 - name: Detect affected apps
116 id: detect-changes
117 working-directory: ${{ env.MONOREPO_DIR }}
118 run: |
119 MANUAL_APPS="${{ steps.parse-manual.outputs.manual_apps }}"
120
121 # Check if manual mode
122 if [ -n "$MANUAL_APPS" ]; then
123 echo "🎯 Manual build mode: Using specified app list"
124 AFFECTED_APPS="$MANUAL_APPS"
125 echo "✅ Manually selected apps: $AFFECTED_APPS"
126 echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
127
128 echo ""
129 echo "Details:"
130 for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do
131 echo " - $APP"
132 done
133 else
134 # Set base branch based on trigger type
135 if [ -n "${{ inputs.base_ref }}" ]; then
136 BASE_REF="origin/${{ inputs.base_ref }}"
137 echo "📋 PR mode: Detecting changes from $BASE_REF to HEAD"
138 else
139 BASE_REF="HEAD~1"
140 if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
141 echo "📋 Manual trigger mode: Detecting changes from last commit"
142 else
143 echo "📋 Push mode: Detecting changes from last commit"
144 fi
145 fi
146
147 echo "Base: $BASE_REF"
148 echo "Head: HEAD"
149
150 echo ""
151 echo "📊 Diagnostic:"
152 git log --oneline -3
153 echo ""
154 echo "Changed files:"
155 git diff --name-only $BASE_REF HEAD 2>/dev/null || true
156 echo ""
157
158 NX_OUTPUT=$(pnpm nx show projects --affected --type=app --sep "," --exclude="@yikart/source" --base=$BASE_REF --head=HEAD 2>&1)
159 NX_EXIT_CODE=$?
160
161 echo "Nx exit code: $NX_EXIT_CODE"
162 echo "Nx output: $NX_OUTPUT"
163
164 if [ $NX_EXIT_CODE -ne 0 ]; then
165 echo "⚠️ Nx affected detection failed (exit code: $NX_EXIT_CODE)"
166 echo "Falling back to building ALL apps"
167 AFFECTED_APPS="aitoearn-ai,aitoearn-server"
168 else
169 AFFECTED_APPS="$NX_OUTPUT"
170 fi
171
172 if [ -z "$AFFECTED_APPS" ]; then
173 echo "ℹ️ No affected apps detected"
174 echo "affected_apps=" >> $GITHUB_OUTPUT
175 else
176 echo "✅ Affected apps: $AFFECTED_APPS"
177 echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
178
179 echo ""
180 echo "Details:"
181 for APP in $(echo "$AFFECTED_APPS" | tr ',' ' '); do
182 echo " - $APP"
183 done
184 fi
185 fi
186
187 - name: Generate version
188 id: version
189 run: |
190 VERSION="$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
191 echo "version=$VERSION" >> $GITHUB_OUTPUT
192 echo "📦 Version: $VERSION"
193
194 - name: Generate matrix
195 id: set-matrix
196 working-directory: ${{ env.MONOREPO_DIR }}
197 run: |
198 AFFECTED_APPS="${{ steps.detect-changes.outputs.affected_apps }}"
199 VERSION="${{ steps.version.outputs.version }}"
200
201 if [ -z "$AFFECTED_APPS" ]; then
202 echo "has_changes=false" >> $GITHUB_OUTPUT
203 echo "matrix={\"include\":[]}" >> $GITHUB_OUTPUT
204 echo "⚠️ No changes, skipping build"
205 exit 0
206 fi
207
208 echo "has_changes=true" >> $GITHUB_OUTPUT
209
210 # Determine if should push latest tag
211 PUSH_LATEST=false
212 if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/main" ]; then
213 PUSH_LATEST=true
214 echo "📌 Main branch push: Will push latest tag"
215 elif [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.push_latest }}" = "true" ]; then
216 PUSH_LATEST=true
217 echo "📌 Manual trigger with push_latest: Will push latest tag"
218 fi
219
220 echo "push_latest=$PUSH_LATEST" >> $GITHUB_OUTPUT
221
222 # Build matrix JSON array
223 MATRIX_JSON="{"
224 MATRIX_JSON="$MATRIX_JSON\"include\":["
225
226 FIRST=true
227 IFS=',' read -ra APPS <<< "$AFFECTED_APPS"
228
229 for APP in "${APPS[@]}"; do
230 IMAGE_TAG="${{ env.DOCKER_HUB_USERNAME }}/$APP:$VERSION"
231
232 # Add to matrix
233 if [ "$FIRST" = true ]; then
234 FIRST=false
235 else
236 MATRIX_JSON="$MATRIX_JSON,"
237 fi
238
239 MATRIX_JSON="$MATRIX_JSON{\"app\":\"$APP\",\"image\":\"$IMAGE_TAG\",\"push_latest\":$PUSH_LATEST}"
240 done
241
242 MATRIX_JSON="$MATRIX_JSON]}"
243
244 echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
245
246 echo ""
247 echo "🎯 Matrix configuration:"
248 echo "$MATRIX_JSON" | jq .
249
250 build-and-push:
251 needs: generate-matrix
252 if: needs.generate-matrix.outputs.has_changes == 'true'
253 runs-on: ubuntu-latest
254 strategy:
255 matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
256 fail-fast: false
257
258 steps:
259 - name: Checkout
260 uses: actions/checkout@v5
261
262 - name: Install pnpm
263 uses: pnpm/action-setup@v4
264 with:
265 version: 10
266
267 - name: Setup Node.js
268 uses: actions/setup-node@v4
269 with:
270 node-version: 22
271 cache: pnpm
272 cache-dependency-path: ${{ env.MONOREPO_DIR }}/pnpm-lock.yaml
273
274 - name: Install dependencies
275 working-directory: ${{ env.MONOREPO_DIR }}
276 run: pnpm install --frozen-lockfile
277
278 - name: Restore Nx cache
279 uses: actions/cache@v4
280 with:
281 path: ${{ env.MONOREPO_DIR }}/.nx/cache
282 key: nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}-${{ github.sha }}
283 restore-keys: |
284 nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-${{ matrix.app }}-
285 nx-${{ runner.os }}-${{ hashFiles('project/aitoearn-backend/pnpm-lock.yaml') }}-
286 nx-${{ runner.os }}-
287
288 - name: Build application
289 working-directory: ${{ env.MONOREPO_DIR }}
290 run: |
291 echo "🔨 Building app: ${{ matrix.app }}"
292 pnpm nx run ${{ matrix.app }}:build
293
294 - name: Prepare Docker context
295 working-directory: ${{ env.MONOREPO_DIR }}
296 run: pnpm nx run ${{ matrix.app }}:docker-context
297
298 - name: Login to Docker Hub
299 uses: docker/login-action@v3
300 with:
301 username: ${{ env.DOCKER_HUB_USERNAME }}
302 password: ${{ secrets.DOCKER_HUB_TOKEN }}
303
304 - name: Set up QEMU
305 uses: docker/setup-qemu-action@v3
306
307 - name: Set up Docker Buildx
308 uses: docker/setup-buildx-action@v3
309 with:
310 driver-opts: |
311 network=host
312 image=moby/buildkit:latest
313 buildkitd-flags: --debug
314
315 - name: Prepare tags
316 id: tags
317 run: |
318 TAGS="${{ matrix.image }}"
319 if [ "${{ matrix.push_latest }}" = "true" ]; then
320 TAGS="$TAGS"$'\n'"${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest"
321 fi
322 echo "tags<<EOF" >> $GITHUB_OUTPUT
323 echo "$TAGS" >> $GITHUB_OUTPUT
324 echo "EOF" >> $GITHUB_OUTPUT
325
326 - name: Build and push Docker image
327 uses: docker/build-push-action@v6
328 with:
329 context: ${{ env.MONOREPO_DIR }}/tmp/docker-context
330 file: ${{ env.MONOREPO_DIR }}/tmp/docker-context/Dockerfile
331 platforms: linux/amd64,linux/arm64
332 build-args: |
333 APP_NAME=${{ matrix.app }}
334 tags: ${{ steps.tags.outputs.tags }}
335 push: true
336 cache-from: |
337 type=gha,scope=${{ matrix.app }}
338 type=registry,ref=${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:buildcache
339 cache-to: |
340 type=gha,mode=max,scope=${{ matrix.app }}
341 type=inline
342 provenance: false
343 sbom: false
344
345 - name: Build summary
346 run: |
347 echo "### ✅ Build successful: ${{ matrix.app }}" >> $GITHUB_STEP_SUMMARY
348 echo "- **Image**: \`${{ matrix.image }}\`" >> $GITHUB_STEP_SUMMARY
349 if [ "${{ matrix.push_latest }}" = "true" ]; then
350 echo "- **Latest**: \`${{ env.DOCKER_HUB_USERNAME }}/${{ matrix.app }}:latest\`" >> $GITHUB_STEP_SUMMARY
351 fi
352
353
353 lines YAML