-
Notifications
You must be signed in to change notification settings - Fork 39
320 lines (283 loc) · 14.1 KB
/
deploy-kiloclaw.yml
File metadata and controls
320 lines (283 loc) · 14.1 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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
name: Deploy KiloClaw
on:
workflow_dispatch:
workflow_call:
permissions:
contents: read
packages: write
jobs:
deploy:
runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }}
timeout-minutes: 30
name: Deploy KiloClaw
outputs:
image-tag: ${{ steps.image-hash.outputs.tag }}
image-hash: ${{ steps.image-hash.outputs.hash }}
image-digest: ${{ steps.image-digest.outputs.digest }}
openclaw-version: ${{ steps.openclaw-version.outputs.version }}
steps:
- name: Checkout code
uses: useblacksmith/checkout@41cdeedae8edb2e684ba22896a5fd2a3cb85db6b # v1
with:
dissociate: true
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
working-directory: services/kiloclaw
run: pnpm install --frozen-lockfile
# ── Compute source content hash ─────────────────────────────
# Hash the SOURCE FILES that are COPYed into the Docker image.
# This hash is used as the IMAGE TAG (a label we assign), NOT
# compared against Docker's image digest.
# Same source files → same tag name → registry has it → skip build.
#
# Files included (must match what the Dockerfile COPYs):
# - Dockerfile itself (base image, apt packages, npm versions)
# - ../../pnpm-workspace.yaml + ../../pnpm-lock.yaml + ../../patches/ (image package pnpm installs)
# - controller/ (COPY controller/ → compiled to controller.js)
# - container/ (COPY container/TOOLS.md → /usr/local/share/kiloclaw/)
# - plugins/kiloclaw-customizer/ (COPY plugin package for image install)
# - plugins/kilo-chat/ (COPY plugin package for image install)
# - plugins/kiloclaw-morning-briefing/ (COPY plugin package for image install)
# - openclaw-pairing-list.js, openclaw-device-pairing-list.js (COPY)
# - skills/ (COPY skills/ → /root/clawd/skills/)
#
# NOT included (not in the image):
# - src/ (worker code, deployed separately)
# - scripts/ (dev tooling, not COPYed)
# - wrangler.jsonc (worker config)
- name: Compute source content hash
id: image-hash
working-directory: services/kiloclaw
run: |
# Validate all expected paths exist before hashing
for path in Dockerfile ../../pnpm-workspace.yaml ../../pnpm-lock.yaml ../../patches controller container plugins/kiloclaw-customizer plugins/kilo-chat plugins/kiloclaw-morning-briefing skills \
openclaw-pairing-list.js openclaw-device-pairing-list.js; do
if [ ! -e "$path" ]; then
echo "::error::Required path not found: $path"
exit 1
fi
done
CONTENT_HASH="$(scripts/image-content-hash.sh --hash --dockerfile Dockerfile)"
if [ -z "$CONTENT_HASH" ] || [ ${#CONTENT_HASH} -ne 12 ]; then
echo "::error::Failed to compute valid content hash"
exit 1
fi
echo "hash=${CONTENT_HASH}" >> "$GITHUB_OUTPUT"
echo "tag=img-${CONTENT_HASH}" >> "$GITHUB_OUTPUT"
echo "Source content hash → image tag: img-${CONTENT_HASH}"
# ── Docker setup ────────────────────────────────────────────
# Always set up Docker + login so we can check the registry.
- name: Setup Docker Buildx
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1.6.0
- name: Login to Fly Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: registry.fly.io
username: x
password: ${{ secrets.FLY_API_TOKEN }}
# GHCR is the image source for the Northflank provider
# (NF_IMAGE_PATH_TEMPLATE=ghcr.io/kilo-org/kiloclaw:{tag}). We dual-push
# so every prod content-hash tag is available to both Fly and Northflank.
- name: Login to GHCR
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# ── Check if image tag already exists in registry ───────────
# Looks up by TAG NAME, not by digest comparison. If we previously
# built and pushed an image with this tag, it's already there.
# On error or timeout, defaults to building (safe fallback).
- name: Check if image tag exists in registry
id: check-image
run: |
IMAGE="registry.fly.io/kiloclaw-machines:${{ steps.image-hash.outputs.tag }}"
if timeout 15 docker manifest inspect "${IMAGE}" > /tmp/manifest.json 2>/dev/null; then
# Handle both manifest list (.manifests[]) and single manifest (.config.digest) formats
DIGEST=$(jq -r 'if .manifests then .manifests[0].digest else .config.digest end' /tmp/manifest.json 2>/dev/null || echo "")
if [ -n "$DIGEST" ] && [ "$DIGEST" != "null" ] && [[ "$DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
echo "✅ Image tag exists in registry (digest: ${DIGEST:0:19}...)"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "⚠️ Image found but could not extract valid digest, will rebuild"
fi
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "🔨 Image tag not found or registry check failed, will build"
fi
# ── Conditional Docker build ────────────────────────────────
# Only builds if the content-hash tag doesn't already exist in
# the registry (or the registry check failed).
- name: Build and push Docker image
id: docker-build
if: steps.check-image.outputs.exists != 'true'
uses: useblacksmith/build-push-action@cbd1f60d194a98cb3be5523b15134501eaf0fbf3 # v2.1.0
with:
context: services/kiloclaw
file: services/kiloclaw/Dockerfile
build-contexts: |
workspace=.
platforms: linux/amd64
push: true
tags: |
registry.fly.io/kiloclaw-machines:${{ steps.image-hash.outputs.tag }}
registry.fly.io/kiloclaw-machines:latest
ghcr.io/kilo-org/kiloclaw:${{ steps.image-hash.outputs.tag }}
ghcr.io/kilo-org/kiloclaw:sha-${{ github.sha }}
build-args: |
CONTROLLER_COMMIT=${{ github.sha }}
# ── Ensure GHCR has the tag when the build was skipped ──────
# When check-image finds the tag already in Fly registry, the build is
# skipped — but GHCR may still be missing the tag (e.g. first prod run
# after Northflank rollout, or a GHCR push that previously failed).
# Copy the existing Fly image to GHCR so Northflank can pull it.
- name: Check GHCR for image tag
id: check-ghcr
if: steps.check-image.outputs.exists == 'true'
run: |
IMAGE="ghcr.io/kilo-org/kiloclaw:${{ steps.image-hash.outputs.tag }}"
if timeout 15 docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "✅ GHCR already has ${IMAGE}"
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "🔁 GHCR missing ${IMAGE}, will copy from Fly"
fi
- name: Copy Fly image to GHCR
if: steps.check-image.outputs.exists == 'true' && steps.check-ghcr.outputs.exists != 'true'
run: |
FLY_IMAGE="registry.fly.io/kiloclaw-machines:${{ steps.image-hash.outputs.tag }}"
GHCR_IMAGE="ghcr.io/kilo-org/kiloclaw:${{ steps.image-hash.outputs.tag }}"
GHCR_SHA_IMAGE="ghcr.io/kilo-org/kiloclaw:sha-${{ github.sha }}"
echo "Pulling ${FLY_IMAGE}..."
docker pull "${FLY_IMAGE}"
echo "Tagging as ${GHCR_IMAGE} and ${GHCR_SHA_IMAGE}..."
docker tag "${FLY_IMAGE}" "${GHCR_IMAGE}"
docker tag "${FLY_IMAGE}" "${GHCR_SHA_IMAGE}"
echo "Pushing to GHCR..."
docker push "${GHCR_IMAGE}"
docker push "${GHCR_SHA_IMAGE}"
# ── Resolve image digest ────────────────────────────────────
# Gets the Docker digest from whichever source is available:
# new build output, existing registry image, or empty.
- name: Resolve image digest
id: image-digest
env:
BUILD_DIGEST: ${{ steps.docker-build.outputs.digest }}
CHECK_DIGEST: ${{ steps.check-image.outputs.digest }}
run: |
if [ -n "$BUILD_DIGEST" ]; then
echo "digest=$BUILD_DIGEST" >> "$GITHUB_OUTPUT"
elif [ -n "$CHECK_DIGEST" ]; then
echo "digest=$CHECK_DIGEST" >> "$GITHUB_OUTPUT"
else
echo "digest=" >> "$GITHUB_OUTPUT"
fi
# ── Extract OpenClaw version ────────────────────────────────
- name: Extract OpenClaw version
id: openclaw-version
run: |
VERSION=$(sed -n 's/.*openclaw@\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' services/kiloclaw/Dockerfile | head -1)
if [ -z "$VERSION" ]; then
echo "::error::Failed to extract OpenClaw version from Dockerfile"
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
# ── Deploy Worker (always runs) ─────────────────────────────
# Worker always deploys with the content-hash image tag.
# registerVersionIfNeeded() will no-op if the tag is already registered.
- name: Deploy Worker
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
workingDirectory: services/kiloclaw
command: >-
deploy
--var FLY_IMAGE_TAG:${{ steps.image-hash.outputs.tag }}
--var OPENCLAW_VERSION:${{ steps.openclaw-version.outputs.version }}
--var FLY_IMAGE_DIGEST:${{ steps.image-digest.outputs.digest }}
# ── Push prod image to dev registry ───────────────────────────
# Runs after the production deploy completes. Copies the prod image
# to the dev Fly registry so dev stays in sync automatically.
# continue-on-error ensures a dev push failure never blocks prod.
push-dev-image:
needs: [deploy]
runs-on: ${{ vars.RUNNER_DEFAULT_LABEL || 'ubuntu-latest' }}
timeout-minutes: 10
name: Push Dev Image
continue-on-error: true
steps:
- name: Setup Docker Buildx
uses: useblacksmith/setup-docker-builder@5241b2e9423e8b1fa37ed6050ecb62d0fb9a4e38 # v1.6.0
# Login with prod credentials FIRST to pull the prod image
- name: Login to prod Fly Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: registry.fly.io
username: x
password: ${{ secrets.FLY_API_TOKEN }}
- name: Pull prod image
run: |
PROD_IMAGE="registry.fly.io/kiloclaw-machines:${{ needs.deploy.outputs.image-tag }}"
echo "Pulling prod image: ${PROD_IMAGE}"
docker pull "${PROD_IMAGE}"
# Now switch to dev credentials for pushing
- name: Login to dev Fly Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: registry.fly.io
username: x
password: ${{ secrets.FLY_DEV_API_TOKEN }}
- name: Generate dev image tag
id: dev-tag
run: |
TIMESTAMP=$(date +%s)
echo "tag=dev-${TIMESTAMP}" >> "$GITHUB_OUTPUT"
echo "Dev tag: dev-${TIMESTAMP}"
- name: Tag and push to dev registry
run: |
PROD_IMAGE="registry.fly.io/kiloclaw-machines:${{ needs.deploy.outputs.image-tag }}"
DEV_IMAGE="registry.fly.io/kiloclaw-registry-dev:${{ steps.dev-tag.outputs.tag }}"
DEV_LATEST="registry.fly.io/kiloclaw-registry-dev:latest"
echo "Tagging as: ${DEV_IMAGE}"
docker tag "${PROD_IMAGE}" "${DEV_IMAGE}"
echo "Tagging as: ${DEV_LATEST}"
docker tag "${PROD_IMAGE}" "${DEV_LATEST}"
echo "Pushing to dev registry..."
docker push "${DEV_IMAGE}"
docker push "${DEV_LATEST}"
echo "✅ Pushed dev image: ${DEV_IMAGE}"
echo "✅ Pushed dev latest: ${DEV_LATEST}"
- name: Write dev .dev.vars summary
run: |
TAG="${{ steps.dev-tag.outputs.tag }}"
DIGEST="${{ needs.deploy.outputs.image-digest }}"
OPENCLAW="${{ needs.deploy.outputs.openclaw-version }}"
CONTENT="${{ needs.deploy.outputs.image-hash }}"
{
echo ""
echo "## .dev.vars entries for KiloClaw worker"
echo ""
echo "Add or update these in \`services/kiloclaw/.dev.vars\`, then restart wrangler dev:"
echo ""
echo '```'
echo "FLY_IMAGE_TAG=${TAG}"
if [ -n "$DIGEST" ]; then
echo "FLY_IMAGE_DIGEST=${DIGEST}"
fi
echo "OPENCLAW_VERSION=${OPENCLAW}"
echo "FLY_IMAGE_CONTENT_MODE=production"
echo "FLY_IMAGE_CONTENT_HASH=${CONTENT}"
echo '```'
echo ""
echo "**Image pushed to:** \`registry.fly.io/kiloclaw-registry-dev:${TAG}\`"
echo ""
} >> "$GITHUB_STEP_SUMMARY"