From bc4ee373b5477e34d009dc95b76f1e74c4d2476c Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Wed, 28 Jan 2026 08:34:46 +1100 Subject: [PATCH] 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 --- .github/workflows/build-cross-image.yml | 332 ++++++++++++++++++++++++ 1 file changed, 332 insertions(+) diff --git a/.github/workflows/build-cross-image.yml b/.github/workflows/build-cross-image.yml index b06dc47a7..83b40f2be 100644 --- a/.github/workflows/build-cross-image.yml +++ b/.github/workflows/build-cross-image.yml @@ -19,6 +19,11 @@ on: description: 'Image version tag' required: true default: 'latest' + skip_tests: + description: 'Skip cross-compilation tests' + required: false + default: 'false' + type: boolean push: branches: - v3-alpha @@ -35,6 +40,8 @@ jobs: permissions: contents: read packages: write + outputs: + image_tag: ${{ steps.vars.outputs.image_version }} steps: - name: Checkout @@ -89,3 +96,328 @@ jobs: MACOS_SDK_VERSION=${{ steps.vars.outputs.sdk_version }} cache-from: type=gha cache-to: type=gha,mode=max + + # Test cross-compilation for all platforms + test-cross-compile: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # Darwin targets (Zig + macOS SDK) - no platform emulation needed + - os: darwin + arch: arm64 + platform: "" + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + platform: "" + expected_file: "Mach-O 64-bit.*x86_64" + # Linux targets (GCC) - need platform to match architecture + - os: linux + arch: amd64 + platform: "linux/amd64" + expected_file: "ELF 64-bit LSB.*x86-64" + - os: linux + arch: arm64 + platform: "linux/arm64" + expected_file: "ELF 64-bit LSB.*ARM aarch64" + # Windows targets (Zig + mingw) - no platform emulation needed + - os: windows + arch: amd64 + platform: "" + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + platform: "" + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Set up QEMU + if: matrix.platform != '' + uses: docker/setup-qemu-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a minimal CGO test program + cat > main.go << 'EOF' + package main + + /* + #include + + int add(int a, int b) { + return a + b; + } + */ + import "C" + import "fmt" + + func main() { + result := C.add(1, 2) + fmt.Printf("CGO test: 1 + 2 = %d\n", result) + } + EOF + + cat > go.mod << 'EOF' + module test-cgo + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (CGO) + run: | + cd test-project + PLATFORM_FLAG="" + if [ -n "${{ matrix.platform }}" ]; then + PLATFORM_FLAG="--platform ${{ matrix.platform }}" + fi + + docker run --rm $PLATFORM_FLAG \ + -v "$(pwd):/app" \ + -e APP_NAME="test-cgo" \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + ${{ matrix.os }} ${{ matrix.arch }} + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }}.exe 2>/dev/null || ls *.exe | head -1) + else + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }}" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY=$(ls test-cgo-${{ matrix.os }}-${{ matrix.arch }} 2>/dev/null || ls test-cgo* | grep -v '.exe' | head -1) + + echo "## Library Dependencies for $BINARY" + echo "" + + # Use readelf to show dynamic dependencies + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify expected libraries are linked + echo "" + echo "### Verifying required libraries..." + NEEDED=$(readelf -d "$BINARY" | grep NEEDED) + + MISSING="" + for lib in libwebkit2gtk-4.1.so libgtk-3.so libglib-2.0.so libc.so; do + if echo "$NEEDED" | grep -q "$lib"; then + echo "✅ $lib" + else + echo "❌ $lib MISSING" + MISSING="$MISSING $lib" + fi + done + + if [ -n "$MISSING" ]; then + echo "" + echo "ERROR: Missing required libraries:$MISSING" + exit 1 + fi + + # Test non-CGO builds (pure Go cross-compilation) + test-non-cgo: + needs: build + if: ${{ inputs.skip_tests != 'true' }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - os: darwin + arch: arm64 + expected_file: "Mach-O 64-bit.*arm64" + - os: darwin + arch: amd64 + expected_file: "Mach-O 64-bit.*x86_64" + - os: linux + arch: amd64 + expected_file: "ELF 64-bit LSB" + - os: linux + arch: arm64 + expected_file: "ELF 64-bit LSB.*ARM aarch64" + - os: windows + arch: amd64 + expected_file: "PE32\\+ executable.*x86-64" + - os: windows + arch: arm64 + expected_file: "PE32\\+ executable.*Aarch64" + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch || github.ref }} + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Create test non-CGO project + run: | + mkdir -p test-project + cd test-project + + # Create a pure Go test program (no CGO) + cat > main.go << 'EOF' + package main + + import "fmt" + + func main() { + fmt.Println("Pure Go cross-compilation test") + } + EOF + + cat > go.mod << 'EOF' + module test-pure-go + + go 1.21 + EOF + + - name: Build ${{ matrix.os }}/${{ matrix.arch }} (non-CGO) + run: | + cd test-project + + # For non-CGO, we can use any platform since Go handles cross-compilation + # We set CGO_ENABLED=0 to ensure pure Go build + docker run --rm \ + -v "$(pwd):/app" \ + -e APP_NAME="test-pure-go" \ + -e CGO_ENABLED=0 \ + --entrypoint /bin/sh \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build.outputs.image_tag || 'latest' }} \ + -c "GOOS=${{ matrix.os }} GOARCH=${{ matrix.arch }} go build -o bin/test-pure-go-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }} ." + + - name: Verify binary format + run: | + cd test-project/bin + ls -la + + # Find the built binary + if [ "${{ matrix.os }}" = "windows" ]; then + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}.exe" + else + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + fi + + echo "Binary: $BINARY" + FILE_OUTPUT=$(file "$BINARY") + echo "File output: $FILE_OUTPUT" + + # Verify the binary format matches expected + if echo "$FILE_OUTPUT" | grep -qE "${{ matrix.expected_file }}"; then + echo "✅ Binary format verified: ${{ matrix.os }}/${{ matrix.arch }} (non-CGO)" + else + echo "❌ Binary format mismatch!" + echo "Expected pattern: ${{ matrix.expected_file }}" + echo "Got: $FILE_OUTPUT" + exit 1 + fi + + - name: Check library dependencies (Linux only) + if: matrix.os == 'linux' + run: | + cd test-project/bin + BINARY="test-pure-go-${{ matrix.os }}-${{ matrix.arch }}" + + echo "## Library Dependencies for $BINARY (non-CGO)" + echo "" + + # Non-CGO builds should have minimal dependencies (just libc or statically linked) + echo "### NEEDED libraries:" + readelf -d "$BINARY" | grep NEEDED || echo "No dynamic dependencies (statically linked)" + + # Verify NO GTK/WebKit libraries (since CGO is disabled) + NEEDED=$(readelf -d "$BINARY" | grep NEEDED || true) + if echo "$NEEDED" | grep -q "libwebkit\|libgtk"; then + echo "❌ ERROR: Non-CGO binary should not link to GTK/WebKit!" + exit 1 + else + echo "✅ Confirmed: No GTK/WebKit dependencies (expected for non-CGO)" + fi + + # Summary job + test-summary: + needs: [build, test-cross-compile, test-non-cgo] + if: always() && inputs.skip_tests != 'true' + runs-on: ubuntu-latest + steps: + - name: Check test results + run: | + echo "## Cross-Compilation Test Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ "${{ needs.test-cross-compile.result }}" = "success" ]; then + echo "✅ **CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + if [ "${{ needs.test-non-cgo.result }}" = "success" ]; then + echo "✅ **Non-CGO Tests**: All passed" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Non-CGO Tests**: Failed" >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Tested Platforms" >> $GITHUB_STEP_SUMMARY + echo "| Platform | Architecture | CGO | Non-CGO |" >> $GITHUB_STEP_SUMMARY + echo "|----------|-------------|-----|---------|" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Darwin | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Linux | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | arm64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + echo "| Windows | amd64 | ✅ | ✅ |" >> $GITHUB_STEP_SUMMARY + + # Fail if any test failed + if [ "${{ needs.test-cross-compile.result }}" != "success" ] || [ "${{ needs.test-non-cgo.result }}" != "success" ]; then + echo "" + echo "❌ Some tests failed. Check the individual job logs for details." + exit 1 + fi