Repository navigation
236 lines (205 loc) · 9.09 KB
/
Copy pathvalidate_docker_image.yml
File metadata and controls
236 lines (205 loc) · 9.09 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
name: validate_docker_image
# Builds and smoke-tests the release netclawd Docker image on every PR
# and dev/main/master push. Intentionally separate from pr_validation.yml
# (unit/integration tests + slopwatch) and smoke.yml (the native end-to-end
# smoke harness) because image construction is its own concern — when it
# breaks, the fix lives in the Dockerfile or the shared build script, not
# the .NET test suites.
on:
push:
branches:
- master
- dev
- main
paths:
- 'docker/Dockerfile'
- 'docker/entrypoint.sh'
- 'docker/netclaw-cli-launcher.sh'
- 'scripts/docker/**'
- 'src/**'
- 'global.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
- '.github/workflows/validate_docker_image.yml'
pull_request:
branches:
- master
- dev
- main
- feature/*
paths:
- 'docker/Dockerfile'
- 'docker/entrypoint.sh'
- 'docker/netclaw-cli-launcher.sh'
- 'scripts/docker/**'
- 'src/**'
- 'global.json'
- 'Directory.Build.props'
- 'Directory.Packages.props'
- '.github/workflows/validate_docker_image.yml'
permissions:
contents: read
# One run per ref (or per PR). A newer run cancels the older one so overlapping
# pushes do not stack image builds on the runner pool.
concurrency:
group: validate-docker-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
validate-docker-build:
name: Validate Docker Build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: "Checkout"
uses: actions/checkout@v7
with:
fetch-depth: 1
- name: "Install .NET SDK"
uses: actions/setup-dotnet@v6
with:
global-json-file: "./global.json"
- name: "Set up Docker Buildx"
uses: docker/setup-buildx-action@v4
- name: "Compute ephemeral image tag"
id: tag
shell: bash
run: |
if [[ -n "${{ github.event.pull_request.number }}" ]]; then
echo "tag=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
else
echo "tag=ci-${{ github.run_id }}" >> "$GITHUB_OUTPUT"
fi
- name: "Build netclawd image via shared script"
shell: bash
run: |
chmod +x scripts/docker/build-image.sh
IMAGE_REPO=netclawd-pr \
scripts/docker/build-image.sh "${{ steps.tag.outputs.tag }}"
- name: "Verify daemon reaches healthy state with minimal config"
shell: bash
run: |
set -euo pipefail
source scripts/docker/lib/smoke-lib.sh # shared minimal-config env + health poll (#1303)
docker rm -f netclaw-validate-ok >/dev/null 2>&1 || true
# Minimal config from the shared lib (no secrets; fake Ollama endpoint is never
# called during startup/health). Default bind port 5199; probe runs in-container.
docker run -d --name netclaw-validate-ok $(netclaw_smoke_env_args) \
"netclawd-pr:${{ steps.tag.outputs.tag }}" >/dev/null
if netclaw_wait_healthy netclaw-validate-ok 5199 60; then
echo "✓ Daemon reported healthy"
else
echo "ERROR: daemon did not reach a healthy state within 60s" >&2
docker logs netclaw-validate-ok >&2 2>&1 || true
docker rm -f netclaw-validate-ok >/dev/null 2>&1 || true
exit 1
fi
docker rm -f netclaw-validate-ok >/dev/null 2>&1 || true
- name: "Verify bind-mounted state ownership is repaired"
shell: bash
run: |
set -euo pipefail
source scripts/docker/lib/smoke-lib.sh # shared minimal-config env + health poll (#1303)
docker rm -f netclaw-validate-bind-mount >/dev/null 2>&1 || true
tmpdir="$(mktemp -d)"
state_dir="$tmpdir/data"
tools_dir="$tmpdir/tools"
mkdir -p "$state_dir" "$tools_dir"
chmod 755 "$state_dir" "$tools_dir"
cleanup() {
docker rm -f netclaw-validate-bind-mount >/dev/null 2>&1 || true
if ! rm -rf "$tmpdir" 2>/dev/null; then
docker run --rm \
-v "$tmpdir:/target" \
ubuntu:24.04 sh -c 'rm -rf /target/..?* /target/.[!.]* /target/*' \
>/dev/null 2>&1 || true
rmdir "$tmpdir" 2>/dev/null || true
fi
}
trap cleanup EXIT
owner_before="$(stat -c '%u:%g' "$state_dir")"
if [[ "$owner_before" == "1654:1654" ]]; then
echo "ERROR: bind-mount fixture unexpectedly starts with runtime ownership" >&2
exit 1
fi
docker run -d --name netclaw-validate-bind-mount \
-v "$state_dir:/home/netclaw/.netclaw" \
-v "$tools_dir:/tools" \
$(netclaw_smoke_env_args) \
"netclawd-pr:${{ steps.tag.outputs.tag }}" >/dev/null
if netclaw_wait_healthy netclaw-validate-bind-mount 5199 60; then
echo "✓ Bind-mounted daemon reported healthy"
else
echo "ERROR: bind-mounted daemon did not reach a healthy state within 60s" >&2
docker logs netclaw-validate-bind-mount >&2 2>&1 || true
exit 1
fi
docker exec netclaw-validate-bind-mount test -d /home/netclaw/.netclaw/identity
state_owner="$(docker exec netclaw-validate-bind-mount stat -c '%u:%g' /home/netclaw/.netclaw)"
if [[ "$state_owner" != "1654:1654" ]]; then
echo "ERROR: /home/netclaw/.netclaw owner is $state_owner, expected 1654:1654" >&2
exit 1
fi
tools_owner="$(docker exec netclaw-validate-bind-mount stat -c '%u:%g' /tools)"
if [[ "$tools_owner" != "1654:1654" ]]; then
echo "ERROR: /tools owner is $tools_owner, expected 1654:1654" >&2
exit 1
fi
daemon_uid="$(docker exec netclaw-validate-bind-mount sh -c 'pid=$(pgrep -o -x netclawd); stat -c %u /proc/$pid')"
if [[ "$daemon_uid" != "1654" ]]; then
echo "ERROR: netclawd is running as uid $daemon_uid, expected 1654" >&2
exit 1
fi
- name: "Regression #1279: single supervised daemon across restart paths"
shell: bash
run: |
set -euo pipefail
chmod +x scripts/docker/test-daemon-lifecycle.sh
scripts/docker/test-daemon-lifecycle.sh "netclawd-pr:${{ steps.tag.outputs.tag }}"
- name: "Regression ADR-004: root 'docker exec -- netclaw' must drop to the netclaw user"
shell: bash
run: |
set -euo pipefail
chmod +x scripts/docker/test-nonroot-cli.sh
scripts/docker/test-nonroot-cli.sh "netclawd-pr:${{ steps.tag.outputs.tag }}"
- name: "Lint: ARG TARGETARCH must have no default (#1091)"
shell: bash
run: |
set -euo pipefail
# #1091 root cause: writing `ARG TARGETARCH=<default>` inside a
# build stage shadows buildx's per-platform injection. Both
# manifest entries silently get the same arch (whatever the
# default is). Verified empirically: `docker buildx build
# --platform linux/386 ...` with `ARG TARGETARCH=amd64` after
# FROM resolves TARGETARCH to `amd64`, not `386`.
#
# Canonical Docker pattern is `ARG TARGETARCH` (no default).
# The same applies to TARGETOS, TARGETPLATFORM, TARGETVARIANT,
# BUILDARCH, BUILDOS, BUILDPLATFORM, BUILDVARIANT — all of the
# predefined platform ARGs auto-injected by buildx.
DOCKERFILE="docker/Dockerfile"
PREDEFINED='TARGETARCH|TARGETOS|TARGETPLATFORM|TARGETVARIANT|BUILDARCH|BUILDOS|BUILDPLATFORM|BUILDVARIANT'
if matches=$(grep -nE "^ARG (${PREDEFINED})=" "$DOCKERFILE"); then
echo "ERROR: predefined platform ARG declared with a default value (#1091)" >&2
echo "$matches" | sed 's/^/ /' >&2
echo "" >&2
echo " A default value shadows buildx's per-platform injection," >&2
echo " causing all manifest entries to use the same architecture." >&2
echo " Use \`ARG <NAME>\` (no default). For single-arch builds," >&2
echo " pass --build-arg <NAME>=<value> explicitly (see build-image.sh)." >&2
exit 1
fi
if ! grep -qE "^ARG TARGETARCH(\$|[[:space:]])" "$DOCKERFILE"; then
echo "ERROR: ARG TARGETARCH (no default) not declared in $DOCKERFILE" >&2
echo " Expected at least one bare \`ARG TARGETARCH\` declaration so" >&2
echo " buildx's per-platform value flows into the build stage." >&2
exit 1
fi
echo "OK: no predefined platform ARG has a default; ARG TARGETARCH is declared."
- name: "Cleanup validation containers"
if: always()
shell: bash
run: |
docker rm -f netclaw-validate-ok 2>/dev/null || true
docker rm -f netclaw-validate-bind-mount 2>/dev/null || true
docker rm -f netclaw-lifecycle-1279 2>/dev/null || true
docker rm -f netclaw-nonroot-cli 2>/dev/null || true