[WIP] Add support for eval-level default expect checks #461
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] | |
| paths-ignore: | |
| - 'docs/**' | |
| - '*.md' | |
| - '.github/workflows/docs.yml' | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - 'docs/**' | |
| - '*.md' | |
| - '.github/workflows/docs.yml' | |
| # Cancel superseded runs for the same PR so a new push does not leave the | |
| # previous run occupying a shared GitHub-hosted runner slot. Pushes to main | |
| # fall back to the unique run_id, giving every main run its own group so it is | |
| # never cancelled — not while executing (cancel-in-progress is false off PRs) | |
| # and not while queued (a shared group drops an older pending run when a newer | |
| # one lands, regardless of cancel-in-progress), so the full matrix and the | |
| # coverage-badge commit job always run to completion. The group is prefixed | |
| # with github.workflow because concurrency groups are repo-wide, not scoped to | |
| # a single workflow. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| # Default the workflow token to read-only. Jobs that execute PR-supplied | |
| # code (build/test, e2e, lint) must never be able to push to the repo, even | |
| # on same-repository PRs where GITHUB_TOKEN would otherwise be writable. | |
| # Only the dedicated badge-commit job below opts back into contents: write. | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| name: Build & Test | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| # Surface failures on every OS independently instead of cancelling the | |
| # whole matrix when one runner fails. | |
| fail-fast: false | |
| matrix: | |
| # Windows runners sit in a smaller, longer-queuing pool. PRs run | |
| # Linux-only to keep the shared-runner queue short; the full matrix | |
| # (including windows-latest) still runs on pushes to main. | |
| os: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest", "windows-latest"]') }} | |
| go-version: ["1.25.x"] | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| with: | |
| go-version: ${{ matrix.go-version }} | |
| cache: true | |
| - name: Verify dependencies | |
| run: go mod verify | |
| - name: Build | |
| run: go build ./... | |
| # Force bash on Windows runners too: pwsh's legacy native-argument | |
| # passing splits `-coverprofile=coverage.out` and feeds `.out` to go | |
| # as a package import path, producing `FAIL .out [setup failed]`. | |
| # Git Bash is preinstalled on windows-latest and parses args verbatim. | |
| - name: Test (with race detector and coverage) | |
| shell: bash | |
| run: go test -race -timeout 120s -covermode=atomic -coverpkg=./... -coverprofile=coverage.out ./... | |
| # Self-hosted coverage badge: parse the total from `go tool cover -func` | |
| # and rewrite .github/badges/coverage.json. The shields.io endpoint badge | |
| # in README.md reads this JSON via raw.githubusercontent.com. | |
| # Ubuntu-only: the Windows leg of the matrix uses a different shell | |
| # toolchain and we only need one canonical coverage number. | |
| - name: Compute coverage and update badge | |
| id: coverage | |
| if: matrix.os == 'ubuntu-latest' | |
| run: | | |
| pct=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}') | |
| if [ -z "$pct" ]; then | |
| echo "could not parse total coverage" >&2 | |
| exit 1 | |
| fi | |
| color=$(awk -v p="$pct" 'BEGIN { | |
| if (p+0 >= 80) print "brightgreen"; | |
| else if (p+0 >= 60) print "yellow"; | |
| else print "red"; | |
| }') | |
| mkdir -p .github/badges | |
| printf '{\n "schemaVersion": 1,\n "label": "coverage",\n "message": "%s%%",\n "color": "%s"\n}\n' \ | |
| "$pct" "$color" > .github/badges/coverage.json | |
| echo "pct=$pct" >> "$GITHUB_OUTPUT" | |
| echo "color=$color" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Total coverage: ${pct}% (${color})" | |
| # Hand the badge to the push-only update-coverage-badge job below via | |
| # an artifact. PRs (including forks) still run the compute step above | |
| # for visibility, but skip the upload so they can never trigger a push. | |
| # | |
| # include-hidden-files: true is required — the badge lives under | |
| # .github/, and actions/upload-artifact defaults to excluding files | |
| # whose path contains any dot-prefixed segment. Without this the | |
| # upload would silently match nothing and, combined with | |
| # if-no-files-found: error, fail the build job on every push to main. | |
| - name: Upload coverage badge artifact | |
| if: matrix.os == 'ubuntu-latest' && github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: coverage-badge | |
| path: .github/badges/coverage.json | |
| if-no-files-found: error | |
| include-hidden-files: true | |
| retention-days: 1 | |
| update-coverage-badge: | |
| name: Commit coverage badge | |
| needs: build | |
| # Gate the entire job — not just individual steps — on push-to-main so | |
| # the contents: write token below is never issued for pull_request runs. | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: badges | |
| - name: Download coverage badge artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: coverage-badge | |
| path: .github/badges/ | |
| - name: Commit updated coverage badge | |
| run: | | |
| if git diff --quiet -- .github/badges/coverage.json; then | |
| echo "coverage badge unchanged, nothing to commit" | |
| exit 0 | |
| fi | |
| pct=$(awk -F'"' '/"message"/ {print $4}' .github/badges/coverage.json) | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add .github/badges/coverage.json | |
| git commit -m "chore(ci): update coverage badge to ${pct} [skip ci]" | |
| git push origin badges | |
| e2e-windows: | |
| # Windows e2e is intentionally narrower than the Linux e2e: it does not | |
| # set SKILL_UP_FULL_E2E, so the LLM-dependent tests in e2e/cli_test.go, | |
| # e2e/agent_test.go and e2e/mcp_test.go self-skip. The mock-engine and | |
| # script-judge contract tests still exercise the Windows-specific code | |
| # paths added by issue #31 — shell selection, MSYS argv handling, | |
| # QuoteWindows, NewShellCmd's WaitDelay, the script-judge interpreter | |
| # dispatch — through the full CLI pipeline. Real-LLM coverage stays on | |
| # the Linux e2e job below. | |
| name: E2E (none runtime, Windows) | |
| # Skip on PRs to avoid the longer-queuing Windows runner pool; the | |
| # Windows-specific code paths are still exercised on every push to main. | |
| if: github.event_name != 'pull_request' | |
| runs-on: windows-latest | |
| 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 | |
| # Match the Build & Test step's shell choice so pwsh's legacy argv | |
| # passing does not split `-coverprofile`-style flags. | |
| - 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: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # Fork PRs run without access to repository secrets. Detect that early | |
| # and skip the rest of the job so we don't burn CI minutes installing | |
| # CLIs only to have every LLM-dependent test hit a 401. | |
| - 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 (likely a fork PR)." | |
| 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: | | |
| # Pin to the version declared in internal/agent/codex.go (codexDefaultVersion). | |
| 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' | |
| # SKILL_UP_FULL_E2E unlocks tests that drive a real model. DashScope's | |
| # Anthropic- and OpenAI-compatible endpoints back claude-code and codex | |
| # respectively; QODER_PERSONAL_ACCESS_TOKEN authenticates qodercli. | |
| # SKILL_UP_E2E_ARTIFACT_DIR tells e2e tests to copy their workspace | |
| # (stdout.json / response.md / transcript / result.json …) to a stable | |
| # path before t.TempDir cleanup, so the upload-artifact step below can | |
| # surface them for post-mortem. | |
| 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 }} | |
| # claude_code + dashscope provider → routed through the Anthropic-compat endpoint. | |
| DASHSCOPE_BASE_URL: https://dashscope.aliyuncs.com/apps/anthropic | |
| # Pin every anthropic-provider hop to qwen3.6-plus so no e2e path | |
| # accidentally calls real Claude, regardless of which model the | |
| # eval.yaml (or any hard-coded fallback elsewhere in the codebase) | |
| # would otherwise pick. | |
| ANTHROPIC_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| ANTHROPIC_BASE_URL: https://dashscope.aliyuncs.com/apps/anthropic | |
| ANTHROPIC_MODEL: qwen3.6-plus | |
| # Same idea for openai-provider hops (codex test pins "gpt-5.4" in | |
| # its eval.yaml; OPENAI_MODEL overrides that to 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: ubuntu-latest | |
| # Pulling the execd + sandbox images and bootstrapping the agent inside the | |
| # sandbox is slower than the none-runtime job; give it generous headroom. | |
| timeout-minutes: 35 | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| # The opensandbox runtime is self-hosted on the runner (see below), so no | |
| # OPENSANDBOX_* secret is needed — but the agent under test still calls a | |
| # real model, which needs DASHSCOPE_API_KEY. Fork PRs lack it: skip early. | |
| - 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 (likely a fork PR)." | |
| 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@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 | |
| - name: Install OpenSandbox server | |
| if: steps.secrets.outputs.available == 'true' | |
| run: | | |
| # Pin the server version: it must stay compatible with the execd | |
| # image pinned in the next step, and an unpinned "latest" would let | |
| # an unrelated future release silently change behavior or break CI. | |
| uv tool install 'opensandbox-server==0.1.13' | |
| # uv tool install drops console scripts under ~/.local/bin. | |
| echo "$HOME/.local/bin" >> "$GITHUB_PATH" | |
| - name: Start OpenSandbox server | |
| if: steps.secrets.outputs.available == 'true' | |
| env: | |
| # Image the sandbox containers boot from. node:22 is a full Debian | |
| # image that already ships node 22 + npm + curl + git, so skill-up's | |
| # in-sandbox agent bootstrap is a near no-op. Bare ubuntu:latest does | |
| # not work — it lacks curl/git/node and the bootstrap fails. | |
| OPENSANDBOX_IMAGE: node:22 | |
| # execd is the bootstrap binary OpenSandbox injects into every | |
| # sandbox. Pinned together with opensandbox-server 0.1.13 above; | |
| # bump both in lockstep if the server log reports an execd | |
| # compatibility error. | |
| OPENSANDBOX_EXECD_IMAGE: opensandbox/execd:v1.0.16 | |
| run: | | |
| docker version | |
| api_key="ci-$(openssl rand -hex 16)" | |
| config="$RUNNER_TEMP/sandbox.toml" | |
| # network_mode = "host" lets the natively-running server and the | |
| # sandbox containers it spawns all share the runner's loopback, so | |
| # the e2e process reaches both without bridge endpoint rewriting. | |
| cat > "$config" <<EOF | |
| [server] | |
| host = "127.0.0.1" | |
| port = 8080 | |
| 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' | |
| # Exercise the codex agent here: claude_code already has real-model | |
| # coverage in the none-runtime job, whereas codex is otherwise only | |
| # tested against a fake binary. This run is its sole real coverage. | |
| run: go test -tags e2e -timeout 1800s -count=1 -v -run TestAgent_Codex_OpenSandboxRuntime ./e2e | |
| env: | |
| # codex speaks the OpenAI wire API → supply its key, endpoint and | |
| # model via OPENAI_* (DashScope's OpenAI-compatible endpoint, | |
| # authenticated with the DashScope key). | |
| OPENAI_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }} | |
| OPENAI_BASE_URL: https://dashscope.aliyuncs.com/compatible-mode/v1 | |
| OPENAI_MODEL: qwen3.6-plus | |
| # Copies the in-sandbox agent workspace out before t.TempDir cleanup | |
| # so the upload step below can surface it for post-mortem. | |
| 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-docker: | |
| name: E2E (docker runtime) | |
| runs-on: ubuntu-latest | |
| # Container lifecycle + image pull on a clean runner; comfortably under | |
| # 15 min in practice but leave headroom for Docker Hub rate-limit retries. | |
| timeout-minutes: 20 | |
| # No API keys / LLM credentials needed: these tests drive the docker | |
| # runtime against a real daemon directly, no agent or model in the loop. | |
| # That also means they run on fork PRs, unlike the other two e2e jobs. | |
| 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 | |
| # ubuntu-latest runners ship docker pre-installed and running; this | |
| # makes the dependency explicit so the failure mode is obvious if | |
| # GitHub ever changes the image. | |
| run: | | |
| docker version | |
| docker info | |
| - name: Pre-pull base image | |
| # Pre-pulling outside the test binary keeps the per-test timeouts | |
| # tight and surfaces registry / rate-limit issues as their own step | |
| # rather than a misleading test failure. | |
| run: docker pull alpine:3.20 | |
| - name: Run docker integration tests | |
| # `docker_integration` is the build tag added in internal/runtime/ | |
| # docker_integration_test.go. Tests skip cleanly if the daemon is | |
| # missing, so this command is also safe to run locally. | |
| run: go test -tags docker_integration -timeout 600s -count=1 -v ./internal/runtime/ | |
| e2e-docker-full: | |
| name: E2E (docker runtime, full LLM) | |
| runs-on: ubuntu-latest | |
| 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 (likely a fork PR)." | |
| 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 | |
| lint: | |
| name: Lint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 | |
| with: | |
| go-version: "1.25.x" | |
| cache: true | |
| - name: Check formatting | |
| run: | | |
| if [ "$(gofmt -l . | wc -l)" -gt 0 ]; then | |
| echo "Following files are not formatted:" | |
| gofmt -l . | |
| exit 1 | |
| fi | |
| - name: golangci-lint | |
| uses: golangci/golangci-lint-action@82606bf257cbaff209d206a39f5134f0cfbfd2ee # v9 | |
| with: | |
| version: v2.11.4 | |
| - name: Install revive | |
| run: go install github.com/mgechev/revive@v1.10.0 | |
| - name: revive | |
| run: revive -config revive.toml ./... | |
| release-dryrun: | |
| name: GoReleaser Check | |
| runs-on: ubuntu-latest | |
| 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@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: check | |
| - name: GoReleaser snapshot build | |
| uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89 # v7 | |
| with: | |
| distribution: goreleaser | |
| version: "~> v2" | |
| args: release --snapshot --clean --skip=publish |