Skip to content

Commit bc4ee37

Browse files
leaanthonyclaude
andcommitted
ci: add cross-compilation tests to Docker image workflow
- Add test-cross-compile job that tests CGO builds for all 6 platform/arch combos - Add test-non-cgo job for pure Go cross-compilation verification - Add test-summary job with GitHub Actions summary output - Add skip_tests input for manual workflow dispatch - Verify Linux binaries link to required GTK/WebKit libraries - Verify binary format matches expected architecture Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 5d39f1a commit bc4ee37

File tree

1 file changed

+332
-0
lines changed

1 file changed

+332
-0
lines changed

.github/workflows/build-cross-image.yml

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ on:
1919
description: 'Image version tag'
2020
required: true
2121
default: 'latest'
22+
skip_tests:
23+
description: 'Skip cross-compilation tests'
24+
required: false
25+
default: 'false'
26+
type: boolean
2227
push:
2328
branches:
2429
- v3-alpha
@@ -35,6 +40,8 @@ jobs:
3540
permissions:
3641
contents: read
3742
packages: write
43+
outputs:
44+
image_tag: ${{ steps.vars.outputs.image_version }}
3845

3946
steps:
4047
- name: Checkout
@@ -89,3 +96,328 @@ jobs:
8996
MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }}
9097
cache-from: type=gha
9198
cache-to: type=gha,mode=max
99+
100+
# Test cross-compilation for all platforms
101+
test-cross-compile:
102+
needs: build
103+
if: ${{ inputs.skip_tests != 'true' }}
104+
runs-on: ubuntu-latest
105+
strategy:
106+
fail-fast: false
107+
matrix:
108+
include:
109+
# Darwin targets (Zig + macOS SDK) - no platform emulation needed
110+
- os: darwin
111+
arch: arm64
112+
platform: ""
113+
expected_file: "Mach-O 64-bit.*arm64"
114+
- os: darwin
115+
arch: amd64
116+
platform: ""
117+
expected_file: "Mach-O 64-bit.*x86_64"
118+
# Linux targets (GCC) - need platform to match architecture
119+
- os: linux
120+
arch: amd64
121+
platform: "linux/amd64"
122+
expected_file: "ELF 64-bit LSB.*x86-64"
123+
- os: linux
124+
arch: arm64
125+
platform: "linux/arm64"
126+
expected_file: "ELF 64-bit LSB.*ARM aarch64"
127+
# Windows targets (Zig + mingw) - no platform emulation needed
128+
- os: windows
129+
arch: amd64
130+
platform: ""
131+
expected_file: "PE32\\+ executable.*x86-64"
132+
- os: windows
133+
arch: arm64
134+
platform: ""
135+
expected_file: "PE32\\+ executable.*Aarch64"
136+
137+
steps:
138+
- name: Checkout
139+
uses: actions/checkout@v4
140+
with:
141+
ref: ${{ inputs.branch || github.ref }}
142+
143+
- name: Set up QEMU
144+
if: matrix.platform != ''
145+
uses: docker/setup-qemu-action@v3
146+
147+
- name: Log in to Container Registry
148+
uses: docker/login-action@v3
149+
with:
150+
registry: ${{ env.REGISTRY }}
151+
username: ${{ github.actor }}
152+
password: ${{ secrets.GITHUB_TOKEN }}
153+
154+
- name: Create test CGO project
155+
run: |
156+
mkdir -p test-project
157+
cd test-project
158+
159+
# Create a minimal CGO test program
160+
cat > main.go << 'EOF'
161+
package main
162+
163+
/*
164+
#include <stdlib.h>
165+
166+
int add(int a, int b) {
167+
return a + b;
168+
}
169+
*/
170+
import "C"
171+
import "fmt"
172+
173+
func main() {
174+
result := C.add(1, 2)
175+
fmt.Printf("CGO test: 1 + 2 = %d\n", result)
176+
}
177+
EOF
178+
179+
cat > go.mod << 'EOF'
180+
module test-cgo
181+
182+
go 1.21
183+
EOF
184+
185+
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO)
186+
run: |
187+
cd test-project
188+
PLATFORM_FLAG=""
189+
if [ -n "${{ matrix.platform }}" ]; then
190+
PLATFORM_FLAG="--platform ${{ matrix.platform }}"
191+
fi
192+
193+
docker run --rm $PLATFORM_FLAG \
194+
-v "$(pwd):/app" \
195+
-e APP_NAME="test-cgo" \
196+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
197+
${{ matrix.os }} ${{ matrix.arch }}
198+
199+
- name: Verify binary format
200+
run: |
201+
cd test-project/bin
202+
ls -la
203+
204+
# Find the built binary
205+
if [ "${{ matrix.os }}" = "windows" ]; then
206+
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1)
207+
else
208+
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
209+
fi
210+
211+
echo "Binary: $BINARY"
212+
FILE_OUTPUT=$(file "$BINARY")
213+
echo "File output: $FILE_OUTPUT"
214+
215+
# Verify the binary format matches expected
216+
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
217+
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}"
218+
else
219+
echo "❌ Binary format mismatch!"
220+
echo "Expected pattern: ${{ matrix.expected_file }}"
221+
echo "Got: $FILE_OUTPUT"
222+
exit 1
223+
fi
224+
225+
- name: Check library dependencies (Linux only)
226+
if: matrix.os == 'linux'
227+
run: |
228+
cd test-project/bin
229+
BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1)
230+
231+
echo "## Library Dependencies for $BINARY"
232+
echo ""
233+
234+
# Use readelf to show dynamic dependencies
235+
echo "### NEEDED libraries:"
236+
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
237+
238+
# Verify expected libraries are linked
239+
echo ""
240+
echo "### Verifying required libraries..."
241+
NEEDED=$(readelf -d "$BINARY" | grep NEEDED)
242+
243+
MISSING=""
244+
for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do
245+
if echo "$NEEDED" | grep -q "$lib"; then
246+
echo "✅ $lib"
247+
else
248+
echo "❌ $lib MISSING"
249+
MISSING="$MISSING $lib"
250+
fi
251+
done
252+
253+
if [ -n "$MISSING" ]; then
254+
echo ""
255+
echo "ERROR: Missing required libraries:$MISSING"
256+
exit 1
257+
fi
258+
259+
# Test non-CGO builds (pure Go cross-compilation)
260+
test-non-cgo:
261+
needs: build
262+
if: ${{ inputs.skip_tests != 'true' }}
263+
runs-on: ubuntu-latest
264+
strategy:
265+
fail-fast: false
266+
matrix:
267+
include:
268+
- os: darwin
269+
arch: arm64
270+
expected_file: "Mach-O 64-bit.*arm64"
271+
- os: darwin
272+
arch: amd64
273+
expected_file: "Mach-O 64-bit.*x86_64"
274+
- os: linux
275+
arch: amd64
276+
expected_file: "ELF 64-bit LSB"
277+
- os: linux
278+
arch: arm64
279+
expected_file: "ELF 64-bit LSB.*ARM aarch64"
280+
- os: windows
281+
arch: amd64
282+
expected_file: "PE32\\+ executable.*x86-64"
283+
- os: windows
284+
arch: arm64
285+
expected_file: "PE32\\+ executable.*Aarch64"
286+
287+
steps:
288+
- name: Checkout
289+
uses: actions/checkout@v4
290+
with:
291+
ref: ${{ inputs.branch || github.ref }}
292+
293+
- name: Log in to Container Registry
294+
uses: docker/login-action@v3
295+
with:
296+
registry: ${{ env.REGISTRY }}
297+
username: ${{ github.actor }}
298+
password: ${{ secrets.GITHUB_TOKEN }}
299+
300+
- name: Create test non-CGO project
301+
run: |
302+
mkdir -p test-project
303+
cd test-project
304+
305+
# Create a pure Go test program (no CGO)
306+
cat > main.go << 'EOF'
307+
package main
308+
309+
import "fmt"
310+
311+
func main() {
312+
fmt.Println("Pure Go cross-compilation test")
313+
}
314+
EOF
315+
316+
cat > go.mod << 'EOF'
317+
module test-pure-go
318+
319+
go 1.21
320+
EOF
321+
322+
- name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)
323+
run: |
324+
cd test-project
325+
326+
# For non-CGO, we can use any platform since Go handles cross-compilation
327+
# We set CGO_ENABLED=0 to ensure pure Go build
328+
docker run --rm \
329+
-v "$(pwd):/app" \
330+
-e APP_NAME="test-pure-go" \
331+
-e CGO_ENABLED=0 \
332+
--entrypoint /bin/sh \
333+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \
334+
-c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ."
335+
336+
- name: Verify binary format
337+
run: |
338+
cd test-project/bin
339+
ls -la
340+
341+
# Find the built binary
342+
if [ "${{ matrix.os }}" = "windows" ]; then
343+
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe"
344+
else
345+
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
346+
fi
347+
348+
echo "Binary: $BINARY"
349+
FILE_OUTPUT=$(file "$BINARY")
350+
echo "File output: $FILE_OUTPUT"
351+
352+
# Verify the binary format matches expected
353+
if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then
354+
echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)"
355+
else
356+
echo "❌ Binary format mismatch!"
357+
echo "Expected pattern: ${{ matrix.expected_file }}"
358+
echo "Got: $FILE_OUTPUT"
359+
exit 1
360+
fi
361+
362+
- name: Check library dependencies (Linux only)
363+
if: matrix.os == 'linux'
364+
run: |
365+
cd test-project/bin
366+
BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}"
367+
368+
echo "## Library Dependencies for $BINARY (non-CGO)"
369+
echo ""
370+
371+
# Non-CGO builds should have minimal dependencies (just libc or statically linked)
372+
echo "### NEEDED libraries:"
373+
readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)"
374+
375+
# Verify NO GTK/WebKit libraries (since CGO is disabled)
376+
NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true)
377+
if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then
378+
echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!"
379+
exit 1
380+
else
381+
echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)"
382+
fi
383+
384+
# Summary job
385+
test-summary:
386+
needs: [build, test-cross-compile, test-non-cgo]
387+
if: always() && inputs.skip_tests != 'true'
388+
runs-on: ubuntu-latest
389+
steps:
390+
- name: Check test results
391+
run: |
392+
echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY
393+
echo "" >> $GITHUB_STEP_SUMMARY
394+
395+
if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then
396+
echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
397+
else
398+
echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
399+
fi
400+
401+
if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then
402+
echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY
403+
else
404+
echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY
405+
fi
406+
407+
echo "" >> $GITHUB_STEP_SUMMARY
408+
echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY
409+
echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY
410+
echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY
411+
echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
412+
echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
413+
echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
414+
echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
415+
echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
416+
echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY
417+
418+
# Fail if any test failed
419+
if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then
420+
echo ""
421+
echo "❌ Some tests failed. Check the individual job logs for details."
422+
exit 1
423+
fi

0 commit comments

Comments
 (0)