Skip to content

fix: retry transient Docker module downloads #56

fix: retry transient Docker module downloads

fix: retry transient Docker module downloads #56

Workflow file for this run

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