From c53a91376cbdfb469025861cead31a94b57ea817 Mon Sep 17 00:00:00 2001 From: Will Fu-Hinthorn Date: Wed, 8 Apr 2026 16:07:25 -0700 Subject: [PATCH] Add Go CLI CI smoke coverage --- .github/workflows/_integration_test.yml | 45 +++++++++++++ .github/workflows/_test.yml | 29 ++++++++ .github/workflows/_test_release.yml | 45 +++++++++++++ .github/workflows/release.yml | 7 ++ .../cli/tests/unit_tests/test_go_cli_smoke.py | 67 +++++++++++++++++++ 5 files changed, 193 insertions(+) create mode 100644 libs/cli/tests/unit_tests/test_go_cli_smoke.py diff --git a/.github/workflows/_integration_test.yml b/.github/workflows/_integration_test.yml index 77486f25f..3855e94ae 100644 --- a/.github/workflows/_integration_test.yml +++ b/.github/workflows/_integration_test.yml @@ -9,6 +9,9 @@ on: permissions: contents: read +env: + GO_VERSION: "1.23" + jobs: build: runs-on: ubuntu-latest @@ -51,12 +54,23 @@ jobs: python-version: ${{ matrix.python-version }} enable-cache: "false" working-directory: libs/cli + - name: Set up Go + if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} - name: Install cli globally if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') run: pip install -e . + - name: Build Go CLI binary + if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') + run: make build-go - name: Build service ${{ matrix.example.name }} if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') working-directory: ${{ matrix.example.workdir }} + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t ${{ matrix.example.tag }} - name: Test service ${{ matrix.example.name }} @@ -64,6 +78,8 @@ jobs: working-directory: ${{ matrix.example.workdir }} env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | # Prepare environment file from local or parent example directory if [ -f .env.example ]; then cp .env.example .env; elif [ -f ../.env.example ]; then cp ../.env.example .env && cp ../.env.example ../.env; fi @@ -76,18 +92,27 @@ jobs: - name: Build JS service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/js-examples + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-e - name: Build JS monorepo service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/js-monorepo-example + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install" - name: Build Python monorepo service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/python-monorepo-example + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-g -c apps/agent/langgraph.json - name: Test Python monorepo service @@ -95,6 +120,8 @@ jobs: working-directory: libs/cli/python-monorepo-example env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | cp apps/agent/.env.example apps/agent/.env echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> apps/agent/.env @@ -103,6 +130,9 @@ jobs: - name: Build prerelease reqs service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/examples/graph_prerelease_reqs + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-h - name: Test prerelease reqs service @@ -110,6 +140,8 @@ jobs: working-directory: libs/cli/examples/graph_prerelease_reqs env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | cp ../.env.example .env echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env @@ -134,12 +166,18 @@ jobs: - name: Build and test prerelease reqs fail service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/examples/graph_prerelease_reqs_fail + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-i || [ $? -eq 1 ] - name: Build uv simple service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/uv-examples/simple + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-uv-simple - name: Test uv simple service @@ -147,6 +185,8 @@ jobs: working-directory: libs/cli/uv-examples/simple env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | cp .env.example .env echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env @@ -155,6 +195,9 @@ jobs: - name: Build uv monorepo service if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }} working-directory: libs/cli/uv-examples/monorepo/apps/agent + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | langgraph build -t langgraph-test-uv-monorepo - name: Test uv monorepo service @@ -162,6 +205,8 @@ jobs: working-directory: libs/cli/uv-examples/monorepo/apps/agent env: LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph run: | cp .env.example .env echo "LANGSMITH_API_KEY=${{ secrets.LANGSMITH_API_KEY }}" >> .env diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index f6f4b5829..aef3da518 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -11,6 +11,9 @@ on: permissions: contents: read +env: + GO_VERSION: "1.23" + jobs: build: runs-on: ubuntu-latest @@ -26,12 +29,23 @@ jobs: name: "test #${{ matrix.python-version }}" steps: - uses: actions/checkout@v6 + - name: Get changed files + id: changed-files + if: ${{ inputs.working-directory == 'libs/cli' && github.event_name != 'workflow_dispatch' }} + uses: Ana06/get-changed-files@25f79e676e7ea1868813e21465014798211fad8c # v2.3.0 + with: + filter: "libs/cli/**" - name: Set up Python ${{ matrix.python-version }} uses: ./.github/actions/uv_setup with: python-version: ${{ matrix.python-version }} cache-suffix: test-${{ inputs.working-directory }} working-directory: ${{ inputs.working-directory }} + - name: Set up Go + if: ${{ inputs.working-directory == 'libs/cli' && (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') }} + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} - name: Login to Docker Hub uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 if: ${{ !github.event.pull_request.head.repo.fork }} @@ -44,11 +58,26 @@ jobs: working-directory: ${{ inputs.working-directory }} run: uv sync --frozen --group test --no-dev + - name: Build Go CLI binary + if: ${{ inputs.working-directory == 'libs/cli' && (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') }} + shell: bash + working-directory: ${{ inputs.working-directory }} + run: make build-go + - name: Run tests shell: bash working-directory: ${{ inputs.working-directory }} run: make test + - name: Run Go CLI smoke tests + if: ${{ inputs.working-directory == 'libs/cli' && (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') }} + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + LANGGRAPH_USE_GO_CLI: "1" + LANGGRAPH_GO_CLI_PATH: ${{ github.workspace }}/libs/cli/langgraph_cli/bin/langgraph + run: TEST='tests/unit_tests/test_go_cli_smoke.py' make test + - name: Ensure the tests did not create any additional files shell: bash working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/_test_release.yml b/.github/workflows/_test_release.yml index a95e1dfe8..e995584f9 100644 --- a/.github/workflows/_test_release.yml +++ b/.github/workflows/_test_release.yml @@ -10,6 +10,7 @@ on: env: PYTHON_VERSION: "3.10" + GO_VERSION: "1.23" permissions: contents: read @@ -32,6 +33,17 @@ jobs: cache-suffix: "release" working-directory: ${{ inputs.working-directory }} + - name: Set up Go + if: inputs.working-directory == 'libs/cli' + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Cross-compile Go binaries + if: inputs.working-directory == 'libs/cli' + working-directory: ${{ inputs.working-directory }} + run: make build-go-all GO_BIN_DIR=build/go-bin + # We want to keep this build stage *separate* from the release stage, # so that there's no sharing of permissions between them. # The release stage has trusted publishing and GitHub repo contents write access, @@ -43,7 +55,40 @@ jobs: # > It is strongly advised to separate jobs for building [...] # > from the publish job. # https://github.com/pypa/gh-action-pypi-publish#non-goals + - name: Build CLI platform wheels + if: inputs.working-directory == 'libs/cli' + working-directory: ${{ inputs.working-directory }} + run: | + TARGETS=( + "linux-amd64 manylinux_2_17_x86_64.manylinux2014_x86_64" + "linux-arm64 manylinux_2_17_aarch64.manylinux2014_aarch64" + "linux-arm manylinux_2_17_armv7l.manylinux2014_armv7l" + "linux-386 manylinux_2_17_i686.manylinux2014_i686" + "linux-ppc64le manylinux_2_17_ppc64le.manylinux2014_ppc64le" + "linux-s390x manylinux_2_17_s390x.manylinux2014_s390x" + "linux-amd64 musllinux_1_2_x86_64" + "linux-arm64 musllinux_1_2_aarch64" + "linux-arm musllinux_1_2_armv7l" + "linux-386 musllinux_1_2_i686" + "darwin-amd64 macosx_11_0_x86_64" + "darwin-arm64 macosx_11_0_arm64" + "windows-amd64 win_amd64" + "windows-arm64 win_arm64" + "windows-386 win32" + ) + for entry in "${TARGETS[@]}"; do + key=$(echo "$entry" | awk '{print $1}') + plat=$(echo "$entry" | awk '{print $2}') + ext="" + if [[ "$key" == windows-* ]]; then ext=".exe"; fi + binary="build/go-bin/langgraph-${key}${ext}" + LANGGRAPH_GO_BINARY="$binary" LANGGRAPH_WHEEL_PLAT="$plat" uv build --wheel + done + rm -rf langgraph_cli/bin + uv build + - name: Build project for distribution + if: inputs.working-directory != 'libs/cli' run: uv build working-directory: ${{ inputs.working-directory }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e11ec201..32bbdb522 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -308,6 +308,13 @@ jobs: run: make test working-directory: ${{ inputs.working-directory }} + - name: Run Go CLI smoke tests + if: inputs.working-directory == 'libs/cli' + working-directory: ${{ inputs.working-directory }} + env: + LANGGRAPH_USE_GO_CLI: "1" + run: uv run pytest tests/unit_tests/test_go_cli_smoke.py + publish: needs: - build diff --git a/libs/cli/tests/unit_tests/test_go_cli_smoke.py b/libs/cli/tests/unit_tests/test_go_cli_smoke.py new file mode 100644 index 000000000..176f672a9 --- /dev/null +++ b/libs/cli/tests/unit_tests/test_go_cli_smoke.py @@ -0,0 +1,67 @@ +import os +import shutil +import subprocess + +import pytest + + +@pytest.fixture +def go_cli_env(): + if os.environ.get("LANGGRAPH_USE_GO_CLI") != "1": + pytest.skip("Go CLI smoke tests only run when LANGGRAPH_USE_GO_CLI=1") + + langgraph = shutil.which("langgraph") + assert langgraph is not None, "langgraph executable is not installed" + + env = os.environ.copy() + env["LANGGRAPH_USE_GO_CLI"] = "1" + return langgraph, env + + +def test_go_cli_help(go_cli_env): + langgraph, env = go_cli_env + + result = subprocess.run( + [langgraph, "--help"], + capture_output=True, + text=True, + env=env, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "Usage: langgraph" in result.stdout + + +def test_go_cli_validate(go_cli_env, tmp_path): + langgraph, env = go_cli_env + config_path = tmp_path / "langgraph.json" + config_path.write_text( + '{"dependencies": ["langchain"], "graphs": {"agent": "./agent.py:graph"}}' + ) + + result = subprocess.run( + [langgraph, "validate", "-c", str(config_path)], + capture_output=True, + text=True, + env=env, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "is valid" in result.stdout + + +def test_go_cli_dev_help(go_cli_env): + langgraph, env = go_cli_env + + result = subprocess.run( + [langgraph, "dev", "--help"], + capture_output=True, + text=True, + env=env, + check=False, + ) + + assert result.returncode == 0, result.stderr + assert "Run LangGraph API server in development mode" in result.stdout