Repository navigation
fix: retry transient Docker module downloads #56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main, develop ] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| GO_VERSION: '1.25' | |
| jobs: | |
| test: | |
| name: Test | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_DB: golanggraph | |
| POSTGRES_USER: testuser | |
| POSTGRES_PASSWORD: testpass | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| redis: | |
| image: redis:7-alpine | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| # This workflow owns Go caching explicitly below; avoid restoring the | |
| # same paths twice through setup-go's default cache. | |
| cache: false | |
| - name: Cache Go modules | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-v2-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-v2- | |
| - name: Install dependencies | |
| run: | | |
| go mod download | |
| go mod tidy | |
| - name: Run tests | |
| run: | | |
| # The whole module: ./pkg/... alone skips the LangGraph conformance | |
| # suite and the Studio end-to-end suite under ./test/.... | |
| go test -v -race -timeout 15m \ | |
| -coverpkg=./pkg/... -coverprofile=coverage.out -covermode=atomic \ | |
| ./... | |
| env: | |
| POSTGRES_HOST: localhost | |
| POSTGRES_PORT: 5432 | |
| POSTGRES_DB: golanggraph | |
| POSTGRES_USER: testuser | |
| POSTGRES_PASSWORD: testpass | |
| REDIS_HOST: localhost | |
| REDIS_PORT: 6379 | |
| - name: Enforce source coverage floor | |
| run: | | |
| coverage=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub(/%/, "", $3); print $3}') | |
| echo "Source coverage: ${coverage}% (minimum: 65%)" | |
| awk -v coverage="$coverage" 'BEGIN { exit !(coverage + 0 >= 65) }' | |
| - name: Fuzz smoke test | |
| run: | | |
| # A short run of each target: enough to catch a regression that makes a | |
| # fuzz target crash immediately, without lengthening CI. | |
| set -e | |
| for pkg_target in \ | |
| "./pkg/core FuzzStateJSONRoundTrip" \ | |
| "./pkg/core FuzzStateMarshalUnmarshal" \ | |
| "./pkg/core FuzzDeepCopy" \ | |
| "./pkg/core FuzzGraphRouting" \ | |
| "./pkg/core FuzzGraphConstruction" \ | |
| "./pkg/tools FuzzToolArguments" \ | |
| "./pkg/tools FuzzPathResolution" \ | |
| "./pkg/tools FuzzCommandPolicy" \ | |
| "./pkg/tools FuzzURLPolicy" \ | |
| "./pkg/server FuzzAPIRequestBodies" \ | |
| "./pkg/server FuzzAPIPaths" ; do | |
| set -- $pkg_target | |
| echo "fuzzing $2 in $1" | |
| go test -run '^$' -fuzz "^$2$" -fuzztime=20s "$1" | |
| done | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| files: ./coverage.out | |
| flags: unittests | |
| name: codecov-umbrella | |
| fail_ci_if_error: false | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| continue-on-error: true | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Cache Go modules | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-v2-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-v2- | |
| - name: Install dependencies | |
| run: | | |
| go mod download | |
| go mod tidy | |
| - name: Run golangci-lint | |
| uses: golangci/golangci-lint-action@v9 | |
| with: | |
| version: v2.13.1 | |
| args: --timeout=10m | |
| security: | |
| name: Security Scan | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Run Gosec Security Scanner | |
| run: | | |
| go install github.com/securego/gosec/v2/cmd/gosec@latest | |
| # Gosec reports findings with exit 1, which is indistinguishable from | |
| # an analyzer-only error on some runner/toolchain combinations. Always | |
| # emit SARIF, then make the actual release decision from its findings. | |
| gosec -no-fail -concurrency=1 -exclude=G301,G306,G304,G204,G104,G302 -exclude-dir=examples -exclude-dir=cmd/examples -fmt sarif -out results.sarif ./... | |
| findings=$(jq '[.runs[].results[]?] | length' results.sarif) | |
| if [ "$findings" -ne 0 ]; then | |
| jq -r '.runs[].results[]? | [.ruleId, .level, .message.text, .locations[0].physicalLocation.artifactLocation.uri, (.locations[0].physicalLocation.region.startLine // 0)] | @tsv' results.sarif | |
| exit 1 | |
| fi | |
| - name: Upload SARIF file | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: results.sarif | |
| continue-on-error: true | |
| build: | |
| name: Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Cache Go modules | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-v2-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-v2- | |
| - name: Install dependencies | |
| run: | | |
| go mod download | |
| go mod tidy | |
| - name: Build main binary | |
| run: | | |
| CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o bin/golanggraph ./cmd/golanggraph | |
| - name: Build examples | |
| run: | | |
| mkdir -p bin/examples | |
| go build -o bin/examples/examples ./cmd/examples | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: bin/ | |
| integration-test: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, build] | |
| services: | |
| postgres: | |
| image: postgres:15-alpine | |
| env: | |
| POSTGRES_DB: golanggraph | |
| POSTGRES_USER: testuser | |
| POSTGRES_PASSWORD: testpass | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 5432:5432 | |
| redis: | |
| image: redis:7-alpine | |
| options: >- | |
| --health-cmd "redis-cli ping" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| ports: | |
| - 6379:6379 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: build-artifacts | |
| path: bin/ | |
| - name: Make binaries executable | |
| run: chmod +x bin/golanggraph bin/examples/examples | |
| - name: Run integration tests | |
| run: | | |
| go test -v -race -timeout 15m ./test/e2e/... | |
| env: | |
| POSTGRES_HOST: localhost | |
| POSTGRES_PORT: 5432 | |
| POSTGRES_DB: golanggraph | |
| POSTGRES_USER: testuser | |
| POSTGRES_PASSWORD: testpass | |
| REDIS_HOST: localhost | |
| REDIS_PORT: 6379 | |
| docker-e2e: | |
| name: Docker End-to-End | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, build] | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Write disposable multi-agent configuration | |
| run: | | |
| cat > "$RUNNER_TEMP/multi-agent.yaml" <<'EOF' | |
| name: docker-ci | |
| version: "1.0.0" | |
| agents: | |
| alpha: | |
| id: alpha | |
| name: Alpha | |
| type: chat | |
| model: gemma3:1b | |
| provider: ollama | |
| systemprompt: "You are the Docker end-to-end smoke-test agent." | |
| maxtokens: 1000 | |
| beta: | |
| id: beta | |
| name: Beta | |
| type: chat | |
| model: gemma3:1b | |
| provider: ollama | |
| systemprompt: "You are the second Docker end-to-end smoke-test agent." | |
| maxtokens: 1000 | |
| deployment: | |
| type: docker | |
| environment: production | |
| replicas: 1 | |
| health_check: | |
| enabled: true | |
| path: /health | |
| port: 8080 | |
| initial_delay_seconds: 1 | |
| period_seconds: 1 | |
| timeout_seconds: 1 | |
| success_threshold: 1 | |
| failure_threshold: 1 | |
| agent_specific: | |
| alpha: | |
| enabled: true | |
| path: /health | |
| port: 8080 | |
| initial_delay_seconds: 1 | |
| period_seconds: 1 | |
| timeout_seconds: 1 | |
| success_threshold: 1 | |
| failure_threshold: 1 | |
| beta: | |
| enabled: true | |
| path: /health | |
| port: 8080 | |
| initial_delay_seconds: 1 | |
| period_seconds: 1 | |
| timeout_seconds: 1 | |
| success_threshold: 1 | |
| failure_threshold: 1 | |
| EOF | |
| - name: Generate and start the production Compose deployment | |
| run: | | |
| set -euo pipefail | |
| go run ./cmd/golanggraph multi-agent generate docker "$RUNNER_TEMP/multi-agent.yaml" \ | |
| --output "$RUNNER_TEMP/deploy" | |
| docker compose -f "$RUNNER_TEMP/deploy/docker-compose.yml" -p golanggraph-ci \ | |
| up --detach --build | |
| - name: Verify live health and agent endpoints | |
| run: | | |
| set -euo pipefail | |
| for endpoint in "8080 alpha" "8081 beta"; do | |
| read -r port agent <<< "$endpoint" | |
| for attempt in $(seq 1 20); do | |
| if curl --fail --silent --show-error "http://127.0.0.1:${port}/health" > "health-${agent}.json"; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| jq -e --arg agent "$agent" '.status == "healthy" and .agent_count == 1 and .agents == [$agent]' "health-${agent}.json" | |
| curl --fail --silent --show-error "http://127.0.0.1:${port}/agents" > "agents-${agent}.json" | |
| jq -e --arg agent "$agent" '.total_count == 1 and .agents[0].id == $agent and .agents[0].status == "active"' "agents-${agent}.json" | |
| done | |
| - name: Print container logs on failure | |
| if: failure() | |
| run: docker compose -f "$RUNNER_TEMP/deploy/docker-compose.yml" -p golanggraph-ci logs | |
| - name: Remove disposable Compose deployment and image | |
| if: always() | |
| run: | | |
| docker compose -f "$RUNNER_TEMP/deploy/docker-compose.yml" -p golanggraph-ci down --volumes --rmi local || true | |
| kubernetes-e2e: | |
| name: Kubernetes End-to-End | |
| runs-on: ubuntu-latest | |
| needs: [test, lint, build] | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v5 | |
| - name: Set up Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| cache: false | |
| - name: Install pinned local Kubernetes runtime | |
| run: go install github.com/k3d-io/k3d/v5@v5.7.4 | |
| - name: Write disposable Kubernetes agent configuration | |
| run: | | |
| cat > "$RUNNER_TEMP/multi-agent.yaml" <<'EOF' | |
| name: kubernetes-ci | |
| version: "1.0.0" | |
| agents: | |
| alpha: | |
| id: alpha | |
| name: Alpha | |
| type: chat | |
| model: gemma3:1b | |
| provider: ollama | |
| system_prompt: "Preserve this Kubernetes E2E prompt." | |
| max_tokens: 1000 | |
| max_iterations: 5 | |
| deployment: | |
| type: kubernetes | |
| environment: production | |
| replicas: 1 | |
| EOF | |
| - name: Generate Kubernetes deployment and image | |
| run: | | |
| set -euo pipefail | |
| go run ./cmd/golanggraph multi-agent generate k8s "$RUNNER_TEMP/multi-agent.yaml" \ | |
| --output "$RUNNER_TEMP/k8s" --namespace golanggraph-ci | |
| kubectl kustomize "$RUNNER_TEMP/k8s" >/dev/null | |
| docker build --tag golanggraph-multi-agent:latest --file Dockerfile . | |
| - name: Deploy generated manifests to an isolated cluster | |
| run: | | |
| set -euo pipefail | |
| "$HOME/go/bin/k3d" cluster create golanggraph-ci --wait --timeout 2m | |
| "$HOME/go/bin/k3d" image import golanggraph-multi-agent:latest --cluster golanggraph-ci | |
| kubectl config use-context k3d-golanggraph-ci | |
| kubectl apply -k "$RUNNER_TEMP/k8s" | |
| kubectl -n golanggraph-ci rollout status deployment/golanggraph-multi-agent --timeout=2m | |
| - name: Verify live Kubernetes health and agent endpoints | |
| run: | | |
| set -euo pipefail | |
| kubectl -n golanggraph-ci port-forward service/golanggraph-multi-agent 18082:80 \ | |
| > "$RUNNER_TEMP/kubernetes-port-forward.log" 2>&1 & | |
| echo $! > "$RUNNER_TEMP/kubernetes-port-forward.pid" | |
| for attempt in $(seq 1 20); do | |
| if curl --fail --silent --show-error http://127.0.0.1:18082/health > health.json; then | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| jq -e '.status == "healthy" and .agent_count == 1 and .agents == ["alpha"]' health.json | |
| curl --fail --silent --show-error http://127.0.0.1:18082/agents > agents.json | |
| jq -e '.total_count == 1 and .agents[0].id == "alpha" and .agents[0].status == "active"' agents.json | |
| kubectl -n golanggraph-ci get configmap golanggraph-multi-agent-config \ | |
| -o jsonpath='{.data.multi-agent\.yaml}' | grep -F 'system_prompt: Preserve this Kubernetes E2E prompt.' | |
| - name: Print Kubernetes diagnostics on failure | |
| if: failure() | |
| run: | | |
| kubectl -n golanggraph-ci get pods,service,configmap || true | |
| kubectl -n golanggraph-ci describe deployment/golanggraph-multi-agent || true | |
| kubectl -n golanggraph-ci logs deployment/golanggraph-multi-agent || true | |
| cat "$RUNNER_TEMP/kubernetes-port-forward.log" || true | |
| - name: Remove disposable Kubernetes deployment and image | |
| if: always() | |
| run: | | |
| if [[ -f "$RUNNER_TEMP/kubernetes-port-forward.pid" ]]; then | |
| kill "$(cat "$RUNNER_TEMP/kubernetes-port-forward.pid")" || true | |
| fi | |
| "$HOME/go/bin/k3d" cluster delete golanggraph-ci || true | |
| docker image rm golanggraph-multi-agent:latest || true |