返回 AiToEarn
build.yml
1 name: Build Docker Images
2
3 on:
4 workflow_call:
5 inputs:
6 base_ref:
7 description: "Base branch for affected detection"
8 required: false
9 type: string
10 default: ""
11 manual_apps:
12 description: "Comma-separated list of apps to build"
13 required: false
14 type: string
15 default: ""
16 outputs:
17 apps_json:
18 description: "JSON array of built apps with image and port info"
19 value: ${{ jobs.generate-matrix.outputs.apps_json }}
20 has_changes:
21 description: "Whether any apps were affected"
22 value: ${{ jobs.generate-matrix.outputs.has_changes }}
23 workflow_dispatch:
24 inputs:
25 build_aitoearn_server:
26 description: "aitoearn-server"
27 required: false
28 type: boolean
29 default: false
30 build_aitoearn_ai:
31 description: "aitoearn-ai"
32 required: false
33 type: boolean
34 default: false
35
36 permissions:
37 contents: read
38 actions: write
39
40 env:
41 NX_NO_CLOUD: true
42 NODE_OPTIONS: "--max-old-space-size=4096"
43
44 jobs:
45 generate-matrix:
46 runs-on: ubuntu-latest
47 outputs:
48 matrix: ${{ steps.set-matrix.outputs.matrix }}
49 apps_json: ${{ steps.set-matrix.outputs.apps_json }}
50 has_changes: ${{ steps.set-matrix.outputs.has_changes }}
51 version: ${{ steps.version.outputs.version }}
52
53 steps:
54 - name: Checkout
55 uses: actions/checkout@v5
56 with:
57 fetch-depth: 0
58
59 - name: Parse manual app selection
60 id: parse-manual
61 run: |
62 MANUAL_APPS=""
63
64 if [ -n "${{ inputs.manual_apps }}" ]; then
65 MANUAL_APPS="${{ inputs.manual_apps }}"
66 echo "Using workflow_call apps: $MANUAL_APPS"
67 else
68 if [ "${{ inputs.build_aitoearn_server }}" = "true" ]; then
69 MANUAL_APPS="aitoearn-server"
70 fi
71
72 if [ "${{ inputs.build_aitoearn_ai }}" = "true" ]; then
73 if [ -n "$MANUAL_APPS" ]; then
74 MANUAL_APPS="$MANUAL_APPS,aitoearn-ai"
75 else
76 MANUAL_APPS="aitoearn-ai"
77 fi
78 fi
79
80 if [ -n "$MANUAL_APPS" ]; then
81 echo "Manual apps: $MANUAL_APPS"
82 else
83 echo "No manual apps selected"
84 fi
85 fi
86
87 echo "manual_apps=$MANUAL_APPS" >> $GITHUB_OUTPUT
88
89 - name: Install pnpm
90 if: steps.parse-manual.outputs.manual_apps == ''
91 uses: pnpm/action-setup@v4
92
93 - name: Setup Node.js
94 if: steps.parse-manual.outputs.manual_apps == ''
95 uses: actions/setup-node@v4
96 with:
97 node-version: 24
98 cache: pnpm
99
100 - name: Cache node_modules
101 if: steps.parse-manual.outputs.manual_apps == ''
102 id: cache-node-modules-matrix
103 uses: actions/cache@v4
104 with:
105 path: |
106 node_modules
107 apps/*/node_modules
108 libs/*/node_modules
109 key: node-modules-${{ hashFiles('pnpm-lock.yaml') }}
110
111 - name: Install dependencies
112 if: steps.parse-manual.outputs.manual_apps == '' && steps.cache-node-modules-matrix.outputs.cache-hit != 'true'
113 run: pnpm install --frozen-lockfile
114
115 - name: Detect affected apps
116 id: detect-changes
117 run: |
118 if [ -n "${{ steps.parse-manual.outputs.manual_apps }}" ]; then
119 AFFECTED_APPS="${{ steps.parse-manual.outputs.manual_apps }}"
120 echo "Manual build mode: $AFFECTED_APPS"
121 echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
122 else
123 if [ -n "${{ inputs.base_ref }}" ]; then
124 BASE_REF="origin/${{ inputs.base_ref }}"
125 echo "Detecting affected apps from $BASE_REF to HEAD"
126 else
127 BASE_REF="HEAD~1"
128 echo "Detecting affected apps from HEAD~1 to HEAD"
129 fi
130
131 echo "Base: $BASE_REF"
132 echo "Head: HEAD"
133 echo "Changed files:"
134 git diff --name-only $BASE_REF HEAD 2>/dev/null || true
135
136 NX_OUTPUT=$(pnpm nx show projects --affected --type=app --sep "," --base=$BASE_REF --head=HEAD 2>&1)
137 NX_EXIT_CODE=$?
138
139 echo "Nx exit code: $NX_EXIT_CODE"
140 echo "Nx output: $NX_OUTPUT"
141
142 if [ $NX_EXIT_CODE -ne 0 ]; then
143 echo "Nx affected detection failed; falling back to all open-source apps"
144 AFFECTED_APPS="aitoearn-ai,aitoearn-server"
145 else
146 AFFECTED_APPS="$NX_OUTPUT"
147 fi
148
149 if [ -z "$AFFECTED_APPS" ]; then
150 echo "No affected apps"
151 echo "affected_apps=" >> $GITHUB_OUTPUT
152 else
153 echo "Affected apps: $AFFECTED_APPS"
154 echo "affected_apps=$AFFECTED_APPS" >> $GITHUB_OUTPUT
155 fi
156 fi
157
158 - name: Generate version
159 id: version
160 run: |
161 VERSION="$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
162 echo "version=$VERSION" >> $GITHUB_OUTPUT
163 echo "Version: $VERSION"
164
165 - name: Generate matrix and apps JSON
166 id: set-matrix
167 run: |
168 AFFECTED_APPS="${{ steps.detect-changes.outputs.affected_apps }}"
169 VERSION="${{ steps.version.outputs.version }}"
170
171 if [ -z "$AFFECTED_APPS" ]; then
172 echo "has_changes=false" >> $GITHUB_OUTPUT
173 echo "matrix={\"include\":[]}" >> $GITHUB_OUTPUT
174 echo "apps_json={}" >> $GITHUB_OUTPUT
175 exit 0
176 fi
177
178 echo "has_changes=true" >> $GITHUB_OUTPUT
179
180 MATRIX_JSON="{\"include\":["
181 APPS_JSON="{"
182 FIRST=true
183 IFS=',' read -ra APPS <<< "$AFFECTED_APPS"
184
185 for APP in "${APPS[@]}"; do
186 CONFIG_FILE="apps/$APP/config/config.js"
187
188 if [ ! -f "$CONFIG_FILE" ]; then
189 echo "Error: app config not found: $CONFIG_FILE"
190 exit 1
191 fi
192
193 PORT=$(grep -oP 'port:\s*\K\d+' "$CONFIG_FILE" 2>/dev/null | head -1 | tr -d '[:space:]')
194
195 if ! [[ "$PORT" =~ ^[0-9]+$ ]]; then
196 echo "Error: unable to read valid port from $CONFIG_FILE"
197 exit 1
198 fi
199
200 IMAGE_TAG="$APP:$VERSION"
201
202 if [ "$FIRST" = true ]; then
203 FIRST=false
204 else
205 MATRIX_JSON="$MATRIX_JSON,"
206 APPS_JSON="$APPS_JSON,"
207 fi
208
209 MATRIX_JSON="$MATRIX_JSON{\"app\":\"$APP\",\"image\":\"$IMAGE_TAG\"}"
210 APPS_JSON="$APPS_JSON\"$APP\":{\"image\":\"$IMAGE_TAG\",\"port\":\"$PORT\",\"config_path\":\"$CONFIG_FILE\"}"
211 done
212
213 MATRIX_JSON="$MATRIX_JSON]}"
214 APPS_JSON="$APPS_JSON}"
215
216 echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT
217 echo "apps_json=$APPS_JSON" >> $GITHUB_OUTPUT
218 echo "$MATRIX_JSON" | jq .
219 echo "$APPS_JSON" | jq .
220
221 build:
222 needs: generate-matrix
223 if: needs.generate-matrix.outputs.has_changes == 'true'
224 runs-on: ubuntu-latest
225 strategy:
226 matrix: ${{ fromJson(needs.generate-matrix.outputs.matrix) }}
227 fail-fast: false
228
229 steps:
230 - name: Checkout
231 uses: actions/checkout@v5
232
233 - name: Install pnpm
234 uses: pnpm/action-setup@v4
235
236 - name: Setup Node.js
237 uses: actions/setup-node@v4
238 with:
239 node-version: 24
240 cache: pnpm
241
242 - name: Cache node_modules
243 id: cache-node-modules
244 uses: actions/cache@v4
245 with:
246 path: |
247 node_modules
248 apps/*/node_modules
249 libs/*/node_modules
250 key: node-modules-${{ hashFiles('pnpm-lock.yaml') }}
251
252 - name: Install dependencies
253 if: steps.cache-node-modules.outputs.cache-hit != 'true'
254 run: pnpm install --frozen-lockfile
255
256 - name: Cache Nx build outputs
257 uses: actions/cache@v4
258 with:
259 path: |
260 .nx/cache
261 .nx/workspace-data
262 key: nx-${{ matrix.app }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ github.sha }}
263 restore-keys: |
264 nx-${{ matrix.app }}-${{ hashFiles('pnpm-lock.yaml') }}-
265 nx-${{ matrix.app }}-
266
267 - name: Generate GraphQL types
268 run: pnpm --filter aitoearn-task graphql-codegen
269
270 - name: Build application
271 run: pnpm nx run ${{ matrix.app }}:build
272
273 - name: Prepare Docker context
274 run: pnpm nx run ${{ matrix.app }}:docker-context
275
276 - name: Set up Docker Buildx
277 uses: docker/setup-buildx-action@v3
278
279 - name: Build Docker image
280 uses: docker/build-push-action@v6
281 with:
282 context: ./tmp/docker-context
283 file: ./tmp/docker-context/Dockerfile
284 platforms: linux/amd64
285 build-args: |
286 APP_NAME=${{ matrix.app }}
287 tags: ${{ matrix.image }}
288 load: true
289 push: false
290 cache-from: |
291 type=gha,scope=${{ matrix.app }}
292 cache-to: |
293 type=gha,mode=max,scope=${{ matrix.app }}
294
295 - name: Build summary
296 run: |
297 echo "### Build succeeded: ${{ matrix.app }}" >> $GITHUB_STEP_SUMMARY
298 echo "- Image: \`${{ matrix.image }}\`" >> $GITHUB_STEP_SUMMARY
299
299 lines YAML