-
Notifications
You must be signed in to change notification settings - Fork 198
283 lines (264 loc) · 12.8 KB
/
Copy pathrelease.yml
File metadata and controls
283 lines (264 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
name: Build and Push Docker Images
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
inputs:
environment:
description: "Deployment environment"
required: true
default: "dev"
type: choice
options:
- dev
- prod
env:
BUILD_PLATFORMS: linux/amd64 #,linux/arm64
DOCKER_REGISTRY: ghcr.io
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
jobs:
# `next build` runs with `typescript.ignoreBuildErrors: true` and the Docker
# build never runs `check-types`, so without this gate a `v*` tag (or a
# workflow_dispatch on any branch) could push an image that never
# type-checked. This workflow no longer triggers on `pull_request` at all —
# ci.yml already runs lint/check-types/test on every PR and push-to-main,
# and a Docker build was the only thing this workflow added on a PR, which
# duplicated ci.yml's build-buildability signal for no benefit once ci.yml
# covers it. So this job only needs to re-run for events ci.yml does not
# cover: a version tag or a manual dispatch.
check:
name: Type-check, lint, test
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set up pnpm
uses: pnpm/action-setup@v6.0.10
- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set up Turbo cache
uses: rharkor/caching-for-turbo@v2.5.1
# `pnpm lint` covers everything, cheapest-first: check:agent-instructions,
# `turbo run lint` for apps/builder's i18n check, then repo-wide Biome
# last (slowest). Type-check and test are separate turbo tasks with no
# lint overlap, so they run standalone here instead of double-running
# lint via a combined `turbo run check-types lint test`.
- name: Lint
run: pnpm lint
- name: Type-check and test
run: pnpm turbo run check-types test
prepare-metadata:
runs-on: ubuntu-latest
# Only checks out and computes tag/label strings via docker/metadata-
# action — never touches the registry, so packages: write is unneeded.
permissions:
contents: read
environment: ${{ github.event.inputs.environment || 'dev' }}
outputs:
version: ${{ steps.meta.outputs.version }}
labels: ${{ steps.meta.outputs.labels }}
image_namespace: ${{ steps.image_namespace.outputs.owner_lc }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set image namespace (GHCR requires lowercase)
id: image_namespace
run: echo "owner_lc=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_OUTPUT"
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: |
${{ env.DOCKER_REGISTRY }}/${{ steps.image_namespace.outputs.owner_lc }}/chatbotx-builder
${{ env.DOCKER_REGISTRY }}/${{ steps.image_namespace.outputs.owner_lc }}/chatbotx-worker
${{ env.DOCKER_REGISTRY }}/${{ steps.image_namespace.outputs.owner_lc }}/chatbotx-realtime
${{ env.DOCKER_REGISTRY }}/${{ steps.image_namespace.outputs.owner_lc }}/chatbotx-mcp
${{ env.DOCKER_REGISTRY }}/${{ steps.image_namespace.outputs.owner_lc }}/chatbotx-javascript-executor
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
build-image:
name: Build ${{ matrix.label }}
runs-on: ${{ matrix.runner }}
needs:
- prepare-metadata
- check
# `check` deliberately does not run on push-to-main (see its `if:` above —
# this workflow no longer triggers on `pull_request` at all; ci.yml covers
# PRs). A skipped `needs` job skips its dependents by default, which
# silently stopped every merge to main from publishing an image between
# 2026-08-19 and this fix. This condition lets the build proceed when
# `check` is skipped, while still blocking it when `check` actually fails
# (tags / workflow_dispatch). NOT `always()` — that would ship images
# whose quality gate failed.
#
# Note for any future job-level `if:` edit here: `jobs.<job_id>.if` only
# has access to the `github`, `needs`, `vars`, and `inputs` contexts —
# `matrix` isn't expanded yet at that point, so a job-level
# `if: matrix.label == 'builder'` is invalid YAML that fails workflow
# validation for every trigger, not just this job (this broke the whole
# file for several hours — see git blame / incident notes before adding
# a `matrix` reference here). `runs-on` above CAN see `matrix` because
# it's evaluated after matrix expansion; step-level `if` can too.
if: >-
!cancelled()
&& needs.prepare-metadata.result == 'success'
&& needs.check.result != 'failure'
strategy:
fail-fast: false
matrix:
include:
# The builder compile is CPU-bound and `next build` only ever reports
# 3 workers on a 4-vCPU runner, so a bigger runner is the cheapest
# remaining win — but it needs one that actually exists. GitHub does
# NOT publish an `ubuntu-latest-N-cores` label: larger runners are an
# org-level feature that an admin must create and name, and
# `ubuntu-latest-4-cores` in GitHub's docs is only an example name.
# A public repo makes larger-runner minutes free, not the runners
# themselves. An unmatched label does not fail the job — it queues
# until it times out — so this stays on `ubuntu-latest` until a real
# provisioned label is confirmed (`gh api /orgs/ChatbotXIO/actions/
# runner-groups` needs admin:org). Then it is a one-word change here.
- label: builder
image: chatbotx-builder
dockerfile: ./apps/builder/docker/Dockerfile
runner: ubuntu-latest
- label: worker
image: chatbotx-worker
dockerfile: ./apps/worker/docker/Dockerfile
runner: ubuntu-latest
- label: realtime
image: chatbotx-realtime
dockerfile: ./apps/realtime/docker/Dockerfile
runner: ubuntu-latest
- label: mcp-server
image: chatbotx-mcp
dockerfile: ./apps/mcp-server/docker/Dockerfile
runner: ubuntu-latest
- label: javascript-executor
image: chatbotx-javascript-executor
dockerfile: ./apps/javascript-executor/docker/Dockerfile
runner: ubuntu-latest
# Baseline builder leg was ~16 min and the others ~1-2 min. Bounds a hung
# build, and an unprovisioned `runner` label, against the 24-hour default.
timeout-minutes: 45
permissions:
contents: read
packages: write
environment: ${{ github.event.inputs.environment || 'dev' }}
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ${{ env.DOCKER_REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push main image
if: github.ref_name == 'main'
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ needs.prepare-metadata.outputs.image_namespace }}/${{ matrix.image }}:main
labels: ${{ needs.prepare-metadata.outputs.labels }}
# Per-leg scope: the five matrix legs otherwise share one unscoped GHA
# cache namespace and evict each other against the 10GB LRU budget.
# mode=min (not max): `max` exports every intermediate layer of every
# stage — "preparing build cache for export" alone cost 236.8s of a
# 340.3s export — while a same-SHA rebuild still ran `pnpm install`
# cold and `next build` in full, so the intermediate layers were never
# usefully restored. See the phase table in the plan for measurements.
cache-from: type=gha,scope=${{ matrix.label }}
cache-to: type=gha,scope=${{ matrix.label }},mode=min
platforms: ${{ env.BUILD_PLATFORMS }}
# Turbo remote cache: deliberately NOT passed. buildx rejects an
# empty-valued secret outright, so passing the partial set that exists
# (TURBO_TOKEN/TURBO_TEAM, no TURBO_API) dropped TURBO_API and left
# turbo stalling on its built-in Vercel default —
# `Remote caching unavailable (Could not connect to vercel.com/api)`.
# The Dockerfile's --mount=type=secret lines are optional-safe, so
# restoring these three lines together is all it takes once a
# turborepo-remote-cache is provisioned and TURBO_API is set.
- name: Build and push version image
if: startsWith(github.ref, 'refs/tags/v')
uses: docker/build-push-action@v7
with:
context: .
file: ${{ matrix.dockerfile }}
push: true
tags: |
${{ env.DOCKER_REGISTRY }}/${{ needs.prepare-metadata.outputs.image_namespace }}/${{ matrix.image }}:${{ needs.prepare-metadata.outputs.version }}
${{ env.DOCKER_REGISTRY }}/${{ needs.prepare-metadata.outputs.image_namespace }}/${{ matrix.image }}:latest
labels: ${{ needs.prepare-metadata.outputs.labels }}
# Per-leg scope: the five matrix legs otherwise share one unscoped GHA
# cache namespace and evict each other against the 10GB LRU budget.
# mode=min (not max): `max` exports every intermediate layer of every
# stage — "preparing build cache for export" alone cost 236.8s of a
# 340.3s export — while a same-SHA rebuild still ran `pnpm install`
# cold and `next build` in full, so the intermediate layers were never
# usefully restored. See the phase table in the plan for measurements.
cache-from: type=gha,scope=${{ matrix.label }}
cache-to: type=gha,scope=${{ matrix.label }},mode=min
platforms: ${{ env.BUILD_PLATFORMS }}
# Turbo remote cache secrets omitted — see "Build and push main image".
output-image-urls:
runs-on: ubuntu-latest
needs:
- prepare-metadata
- build-image
# Requiring build-image to have succeeded (not just skipped/cancelled)
# keeps this job from announcing image URLs for a build that didn't run.
if: >-
!cancelled()
&& needs.build-image.result == 'success'
# Only prints strings computed from `needs` outputs — no registry call.
permissions:
contents: read
environment: ${{ github.event.inputs.environment || 'dev' }}
steps:
- name: Output image URLs
run: |
VERSION="${{ needs.prepare-metadata.outputs.version }}"
REGISTRY="${{ env.DOCKER_REGISTRY }}/${{ needs.prepare-metadata.outputs.image_namespace }}"
if [[ "${{ github.ref_name }}" == "main" ]]; then
printf '%s\n' \
"Builder image (main): ${REGISTRY}/chatbotx-builder:main" \
"Worker image (main): ${REGISTRY}/chatbotx-worker:main" \
"Realtime image (main): ${REGISTRY}/chatbotx-realtime:main" \
"MCP Server image (main): ${REGISTRY}/chatbotx-mcp:main" \
"JavaScript Executor image (main): ${REGISTRY}/chatbotx-javascript-executor:main"
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
printf '%s\n' \
"Builder image (version): ${REGISTRY}/chatbotx-builder:${VERSION}" \
"Builder image (latest): ${REGISTRY}/chatbotx-builder:latest" \
"Worker image (version): ${REGISTRY}/chatbotx-worker:${VERSION}" \
"Worker image (latest): ${REGISTRY}/chatbotx-worker:latest" \
"Realtime image (version): ${REGISTRY}/chatbotx-realtime:${VERSION}" \
"Realtime image (latest): ${REGISTRY}/chatbotx-realtime:latest" \
"MCP Server image (version): ${REGISTRY}/chatbotx-mcp:${VERSION}" \
"MCP Server image (latest): ${REGISTRY}/chatbotx-mcp:latest" \
"JavaScript Executor image (version): ${REGISTRY}/chatbotx-javascript-executor:${VERSION}" \
"JavaScript Executor image (latest): ${REGISTRY}/chatbotx-javascript-executor:latest"
else
echo "No publish tags for ref ${GITHUB_REF}."
fi