Extended CI #2
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: Extended CI | |
| on: | |
| merge_group: | |
| workflow_dispatch: | |
| inputs: | |
| windows_guest: | |
| description: Run the real OpenSandbox Windows guest lifecycle test | |
| required: false | |
| default: false | |
| type: boolean | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| e2e-windows: | |
| name: E2E (none runtime, Windows) | |
| runs-on: self-hosted | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Run e2e tests (none runtime, quick mode) | |
| shell: bash | |
| run: go test -tags e2e -timeout 1200s -count=1 -v ./e2e | |
| env: | |
| SKILL_UP_E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts | |
| - name: Upload e2e workspace artifacts | |
| if: always() && hashFiles('e2e-artifacts/**') != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: e2e-windows-workspaces | |
| path: e2e-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| e2e: | |
| name: E2E (none runtime) | |
| runs-on: self-hosted | |
| timeout-minutes: 35 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Check secret availability | |
| id: secrets | |
| env: | |
| DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| run: | | |
| if [ -z "$DASHSCOPE_API_KEY" ]; then | |
| echo "::notice::Skipping e2e: DASHSCOPE_API_KEY not provisioned." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| if: steps.secrets.outputs.available == 'true' | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| if: steps.secrets.outputs.available == 'true' | |
| with: | |
| node-version: "22" | |
| - name: Install claude-code CLI | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| npm install -g --include=optional @anthropic-ai/claude-code | |
| claude --version | |
| - name: Install codex CLI | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| npm install -g --include=optional '@openai/codex@0.80.0' | |
| codex --version | |
| - name: Install qwen-code CLI | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| npm install -g @qwen-code/qwen-code | |
| qwen --version | |
| - name: Install qodercli (pinned v1.0.14) | |
| if: steps.secrets.outputs.available == 'true' | |
| env: | |
| QODER_VERSION: "1.0.14" | |
| QODER_SHA256: "4d83ec3d3a0948b73012cfdd6e2757be0a5da5c89a288425a5607fd42be1de7b" | |
| run: | | |
| TARBALL=$(mktemp) | |
| curl -fsSL "https://qoder-ide.oss-accelerate.aliyuncs.com/qodercli/releases/${QODER_VERSION}/qodercli-linux-x64.tar.gz" -o "$TARBALL" | |
| echo "$QODER_SHA256 $TARBALL" | sha256sum -c - | |
| mkdir -p "$HOME/.qoder/bin" | |
| tar -xzf "$TARBALL" -C "$HOME/.qoder/bin" | |
| rm -f "$TARBALL" | |
| echo "$HOME/.qoder/bin" >> "$GITHUB_PATH" | |
| - name: Verify qodercli on PATH | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| which qodercli | |
| qodercli --version || true | |
| - name: Run e2e tests (none runtime, full mode) | |
| if: steps.secrets.outputs.available == 'true' | |
| run: go test -tags e2e -timeout 1800s -count=1 -v ./e2e | |
| env: | |
| SKILL_UP_FULL_E2E: "1" | |
| SKILL_UP_E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-artifacts | |
| DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| DASHSCOPE_BASE_URL: https://dashscope.aliyuncs.com/apps/anthropic | |
| ANTHROPIC_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| ANTHROPIC_BASE_URL: https://dashscope.aliyuncs.com/apps/anthropic | |
| ANTHROPIC_MODEL: qwen3.6-plus | |
| OPENAI_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| OPENAI_BASE_URL: https://dashscope.aliyuncs.com/compatible-mode/v1 | |
| OPENAI_MODEL: qwen3.6-plus | |
| DASHSCOPE_MODEL: qwen3.6-plus | |
| QODER_PERSONAL_ACCESS_TOKEN: ${{ secrets.QODER_ACCESS_TOKEN }} | |
| - name: Upload e2e workspace artifacts | |
| if: always() && steps.secrets.outputs.available == 'true' && hashFiles('e2e-artifacts/**') != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: e2e-workspaces | |
| path: e2e-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| e2e-opensandbox: | |
| name: E2E (opensandbox runtime) | |
| runs-on: self-hosted | |
| timeout-minutes: 35 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Check secret availability | |
| id: secrets | |
| env: | |
| DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| run: | | |
| if [ -z "$DASHSCOPE_API_KEY" ]; then | |
| echo "::notice::Skipping opensandbox e2e: DASHSCOPE_API_KEY not provisioned." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| if: steps.secrets.outputs.available == 'true' | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Install uv | |
| if: steps.secrets.outputs.available == 'true' | |
| uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 | |
| - name: Install OpenSandbox server | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| uv tool install 'opensandbox-server==0.1.13' | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: Start OpenSandbox server | |
| if: steps.secrets.outputs.available == 'true' | |
| env: | |
| OPENSANDBOX_IMAGE: node:22 | |
| OPENSANDBOX_EXECD_IMAGE: opensandbox/execd:v1.0.16 | |
| run: | | |
| docker version | |
| api_key="ci-$(openssl rand -hex 16)" | |
| config="$RUNNER_TEMP/sandbox.toml" | |
| cat > "$config" <<EOF | |
| [server] | |
| host = "127.0.0.1" | |
| port = 8081 | |
| api_key = "$api_key" | |
| [runtime] | |
| type = "docker" | |
| execd_image = "$OPENSANDBOX_EXECD_IMAGE" | |
| [docker] | |
| network_mode = "host" | |
| EOF | |
| log="$RUNNER_TEMP/opensandbox-server.log" | |
| nohup opensandbox-server --config "$config" > "$log" 2>&1 & | |
| for i in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:8080/health 2>/dev/null | grep -q healthy; then | |
| echo "OpenSandbox server is healthy" | |
| break | |
| fi | |
| if [ "$i" -eq 60 ]; then | |
| echo "::error::OpenSandbox server did not become healthy within 120s" | |
| cat "$log" | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| { | |
| echo "OPENSANDBOX_API_KEY=$api_key" | |
| echo "OPENSANDBOX_BASE_URL=http://127.0.0.1:8080" | |
| echo "OPENSANDBOX_IMAGE=$OPENSANDBOX_IMAGE" | |
| } >> "$GITHUB_ENV" | |
| - name: Run e2e tests (opensandbox runtime) | |
| if: steps.secrets.outputs.available == 'true' | |
| run: go test -tags e2e -timeout 1800s -count=1 -v -run TestAgent_Codex_OpenSandboxRuntime ./e2e | |
| env: | |
| OPENAI_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| OPENAI_BASE_URL: https://dashscope.aliyuncs.com/compatible-mode/v1 | |
| OPENAI_MODEL: qwen3.6-plus | |
| SKILL_UP_E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-opensandbox-artifacts | |
| - name: Upload OpenSandbox server log | |
| if: always() && steps.secrets.outputs.available == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: opensandbox-server-log | |
| path: ${{ runner.temp }}/opensandbox-server.log | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Upload opensandbox e2e workspace artifacts | |
| if: always() && steps.secrets.outputs.available == 'true' && hashFiles('e2e-opensandbox-artifacts/**') != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: e2e-opensandbox-workspaces | |
| path: e2e-opensandbox-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| e2e-opensandbox-windows: | |
| name: E2E (OpenSandbox Windows guest) | |
| if: github.event_name == 'workflow_dispatch' && inputs.windows_guest | |
| runs-on: self-hosted | |
| timeout-minutes: 35 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Check Windows guest prerequisites | |
| id: prerequisites | |
| run: | | |
| available=true | |
| for device in /dev/kvm /dev/net/tun; do | |
| if [ ! -r "$device" ] || [ ! -w "$device" ]; then | |
| echo "::notice::Skipping Windows guest e2e: $device is unavailable or not readable/writable." | |
| available=false | |
| fi | |
| done | |
| if ! docker info >/dev/null 2>&1; then | |
| echo "::notice::Skipping Windows guest e2e: Docker is unavailable." | |
| available=false | |
| fi | |
| echo "available=$available" >> "$GITHUB_OUTPUT" | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| if: steps.prerequisites.outputs.available == 'true' | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Install uv | |
| if: steps.prerequisites.outputs.available == 'true' | |
| uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2 | |
| - name: Install OpenSandbox server with Windows profile | |
| if: steps.prerequisites.outputs.available == 'true' | |
| run: | | |
| uv tool install 'opensandbox-server==0.2.1' | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: Start OpenSandbox server | |
| if: steps.prerequisites.outputs.available == 'true' | |
| run: | | |
| api_key="ci-$(openssl rand -hex 16)" | |
| config="$RUNNER_TEMP/sandbox-windows.toml" | |
| cat > "$config" <<EOF | |
| [server] | |
| host = "127.0.0.1" | |
| port = 8080 | |
| api_key = "$api_key" | |
| [runtime] | |
| type = "docker" | |
| execd_image = "opensandbox/execd:v1.0.21" | |
| [docker] | |
| network_mode = "host" | |
| EOF | |
| log="$RUNNER_TEMP/opensandbox-windows-server.log" | |
| nohup opensandbox-server --config "$config" > "$log" 2>&1 & | |
| echo $! > "$RUNNER_TEMP/opensandbox-windows-server.pid" | |
| for i in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:8081/health 2>/dev/null | grep -q healthy; then | |
| break | |
| fi | |
| if [ "$i" -eq 60 ]; then | |
| echo "::error::OpenSandbox server did not become healthy within 120s" | |
| cat "$log" | |
| exit 1 | |
| fi | |
| sleep 2 | |
| done | |
| { | |
| echo "OPENSANDBOX_API_KEY=$api_key" | |
| echo "OPENSANDBOX_BASE_URL=http://127.0.0.1:8081" | |
| echo "OPENSANDBOX_WINDOWS_IMAGE=dockurr/windows:latest" | |
| } >> "$GITHUB_ENV" | |
| - name: Run real Windows guest lifecycle test | |
| if: steps.prerequisites.outputs.available == 'true' | |
| run: go test -tags e2e -timeout 1800s -count=1 -v -run TestCustomEngine_OpenSandboxWindowsGuest ./e2e | |
| env: | |
| SKILL_UP_WINDOWS_GUEST_E2E: "1" | |
| SKILL_UP_E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-opensandbox-windows-artifacts | |
| - name: Upload Windows guest diagnostics | |
| if: always() && steps.prerequisites.outputs.available == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: opensandbox-windows-guest | |
| path: | | |
| ${{ runner.temp }}/opensandbox-windows-server.log | |
| e2e-opensandbox-windows-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| - name: Stop OpenSandbox server | |
| if: always() && steps.prerequisites.outputs.available == 'true' | |
| run: | | |
| if [ -f "$RUNNER_TEMP/opensandbox-windows-server.pid" ]; then | |
| kill "$(cat "$RUNNER_TEMP/opensandbox-windows-server.pid")" 2>/dev/null || true | |
| fi | |
| e2e-docker: | |
| name: E2E (docker runtime) | |
| runs-on: self-hosted | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Verify docker is available | |
| run: | | |
| docker version | |
| docker info | |
| - name: Pre-pull base image | |
| run: docker pull alpine:3.20 | |
| - name: Run docker integration tests | |
| run: go test -tags docker_integration -timeout 600s -count=1 -v ./internal/runtime/ | |
| e2e-docker-full: | |
| name: E2E (docker runtime, full LLM) | |
| runs-on: self-hosted | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - name: Check secret availability | |
| id: secrets | |
| env: | |
| DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| run: | | |
| if [ -z "$DASHSCOPE_API_KEY" ]; then | |
| echo "::notice::Skipping docker full e2e: DASHSCOPE_API_KEY not provisioned." | |
| echo "available=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "available=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| if: steps.secrets.outputs.available == 'true' | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Pre-pull Docker image | |
| if: steps.secrets.outputs.available == 'true' | |
| run: docker pull node:22 | |
| - name: Run docker full e2e | |
| if: steps.secrets.outputs.available == 'true' | |
| run: go test -tags e2e -timeout 1800s -count=1 -v -run TestAgent_ClaudeCode_DockerRuntime ./e2e | |
| env: | |
| SKILL_UP_FULL_E2E: "1" | |
| SKILL_UP_E2E_ARTIFACT_DIR: ${{ github.workspace }}/e2e-docker-full-artifacts | |
| ANTHROPIC_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| ANTHROPIC_BASE_URL: https://dashscope.aliyuncs.com/apps/anthropic | |
| ANTHROPIC_MODEL: qwen3.6-plus | |
| - name: Upload docker full e2e artifacts | |
| if: always() && steps.secrets.outputs.available == 'true' && hashFiles('e2e-docker-full-artifacts/**') != '' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: e2e-docker-full-workspaces | |
| path: e2e-docker-full-artifacts/ | |
| if-no-files-found: ignore | |
| retention-days: 14 | |
| release-dryrun: | |
| name: GoReleaser Check | |
| runs-on: self-hosted | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: GoReleaser check | |
| uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: check | |
| - name: GoReleaser snapshot build | |
| uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --snapshot --clean --skip=publish |