--- name: Test Code Samples permissions: contents: write pull-requests: write on: pull_request: paths: - "src/code-samples/**" - ".github/workflows/test-code-samples.yml" workflow_dispatch: schedule: # Full run + trace refresh on the 1st of each month at 00:00 UTC - cron: "0 0 1 * *" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: test-code-samples: name: test-code-samples (${{ matrix.product }}) # GitHub does not expose repository secrets to workflows triggered from fork PRs. # These tests require provider API keys for some samples, so skip on forks. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false) runs-on: ubuntu-latest strategy: # One leg per product so a LangSmith failure never turns the OSS check # red, or the other way round. To rerun one leg, re-run its job. fail-fast: false matrix: product: [oss, langsmith] # Scheduled and manual runs test all samples; PR runs may touch many files in large refactors. # Full runs (~600 samples + optional trace/snippet regen) exceeded 90m in practice. timeout-minutes: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 150 || 60 }} services: postgres: image: pgvector/pgvector:pg17 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: postgres ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Resolve sample paths for this leg id: paths env: PRODUCT: ${{ matrix.product }} run: | # Each product directory under src/code-samples/ belongs to exactly one # leg. Add new directories here; the check below fails the run when a # directory is covered by no leg so it cannot silently go untested. OSS_PATHS="src/code-samples/langchain src/code-samples/langgraph src/code-samples/deepagents" LANGSMITH_PATHS="src/code-samples/langsmith" case "$PRODUCT" in oss) PATHS="$OSS_PATHS" ;; langsmith) PATHS="$LANGSMITH_PATHS" ;; *) echo "Unknown product: $PRODUCT"; exit 1 ;; esac echo "paths=$PATHS" >> "$GITHUB_OUTPUT" echo "Leg '$PRODUCT' runs samples under: $PATHS" uncovered="" for dir in src/code-samples/*/; do dir="${dir%/}" [[ "$(basename "$dir")" == "node_modules" ]] && continue case " $OSS_PATHS $LANGSMITH_PATHS " in *" $dir "*) ;; *) uncovered="$uncovered $dir" ;; esac done if [[ -n "$uncovered" ]]; then echo "::error::Sample directories covered by no matrix leg:$uncovered. Add them to OSS_PATHS or LANGSMITH_PATHS in this workflow." exit 1 fi - name: Get modified code sample paths id: files env: EVENT_NAME: ${{ github.event_name }} PR_BASE_REF: ${{ github.event.pull_request.base.ref }} BEFORE_SHA: ${{ github.event.before }} run: | if [[ "$EVENT_NAME" == "schedule" || "$EVENT_NAME" == "workflow_dispatch" ]]; then echo "run_all=true" >> "$GITHUB_OUTPUT" echo "Full run: testing all code samples in this leg" else echo "run_all=false" >> "$GITHUB_OUTPUT" if [[ "$EVENT_NAME" == "pull_request" ]]; then git fetch origin "$PR_BASE_REF" BASE=$(git merge-base HEAD "origin/$PR_BASE_REF") else BASE="$BEFORE_SHA" fi # Every leg receives the full modified list; the runner keeps only # the files under this leg's CODE_SAMPLE_PATHS. FILES=$(git diff --name-only "$BASE" HEAD -- src/code-samples/ \ | grep -E '\.(py|ts|java|kt|go|sh)$' \ | tr '\n' ' ' || true) { echo "files<> "$GITHUB_OUTPUT" if [[ -n "$FILES" ]]; then echo "Modified code samples: $FILES" else echo "No modified .py, .ts, .java, .kt, .go, or .sh files in src/code-samples/" fi fi - name: Set up Python and uv uses: ./.github/actions/uv_setup with: python-version: "3.13" - name: Install project dependencies run: uv sync - name: Install Node.js uses: actions/setup-node@v4 with: # Node 22+ required for samples that import node:sqlite (e.g. graph-api nodes retry). node-version: "22" - name: Set up Java (for JBang) uses: actions/setup-java@v4 with: distribution: "temurin" java-version: "21" - name: Install JBang uses: jbangdev/setup-jbang@v0.1.1 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: src/code-samples/go.mod - name: Wait for PostgreSQL run: | for i in $(seq 1 30); do if python3 -c " import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(2) try: s.connect(('127.0.0.1', 5432)) s.close() except OSError: exit(1) "; then echo "PostgreSQL is ready" exit 0 fi echo "Waiting for PostgreSQL... ($i/30)" sleep 2 done echo "PostgreSQL did not become ready" exit 1 - name: Test code samples id: test # Keep going so a small number of sample failures can still refresh # trace links; the final step fails the job when samples failed. continue-on-error: true env: # Anthropic calls go through the LLM Gateway's direct Anthropic path, which # takes native model names. ANTHROPIC_API_KEY: ${{ secrets.DOCS_CI_GATEWAY_KEY }} ANTHROPIC_BASE_URL: https://gateway.smith.langchain.com/anthropic LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }} # Separate projects so LangSmith-sample runs do not land among the # OSS traces that the monthly refresh shares publicly. LANGSMITH_PROJECT: ${{ matrix.product == 'langsmith' && 'docs-code-samples-langsmith' || 'docs-code-samples' }} # Enable tracing + public share collection on monthly/manual full runs # of the OSS leg only. Trace-link collection matches LangGraph agent # roots, which LangSmith SDK samples do not produce. CODE_SAMPLE_TRACING: ${{ matrix.product == 'oss' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && '1' || '' }} # Concurrent sample processes. Each traced sample stamps # LANGSMITH_DOCS_SNIPPET; collection matches that metadata under a lock. CODE_SAMPLE_JOBS: "4" CODE_SAMPLE_PATHS: ${{ steps.paths.outputs.paths }} # OpenAI calls go through the LLM Gateway's direct OpenAI path, which # takes native model names, so samples need no changes. OPENAI_API_KEY: ${{ secrets.DOCS_CI_GATEWAY_KEY }} OPENAI_BASE_URL: https://gateway.smith.langchain.com/openai/v1 POSTGRES_URI: postgresql://postgres:postgres@127.0.0.1:5432/postgres?sslmode=disable TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} # Gemini calls go through the LLM Gateway too. The google-genai SDKs and # Python langchain-google-genai read GOOGLE_GEMINI_BASE_URL; the TypeScript # @langchain/google package routes only through LANGSMITH_GATEWAY, which # also sends other LangChain chat models through the gateway with this key. GOOGLE_API_KEY: ${{ secrets.DOCS_CI_GATEWAY_KEY }} GOOGLE_GEMINI_BASE_URL: https://gateway.smith.langchain.com/gemini LANGSMITH_GATEWAY: "true" LANGSMITH_GATEWAY_API_KEY: ${{ secrets.DOCS_CI_GATEWAY_KEY }} DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }} DAYTONA_API_URL: ${{ secrets.DAYTONA_API_URL }} RUN_ALL: ${{ steps.files.outputs.run_all }} MODIFIED_FILES: ${{ steps.files.outputs.files }} run: | if [[ "$RUN_ALL" == "true" ]]; then echo "Running all code samples in this leg..." make test-code-samples else FILES="$MODIFIED_FILES" FILES=$(echo "$FILES" | tr -d '\n' | xargs) if [[ -z "$FILES" ]]; then echo "sample_failures=0" >> "$GITHUB_OUTPUT" echo "traces_updated=0" >> "$GITHUB_OUTPUT" echo "No code samples to test" exit 0 fi make test-code-samples FILES="$FILES" fi - name: Regenerate snippet MDX with trace links id: regen_snippets if: >- matrix.product == 'oss' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.test.outcome != 'cancelled' && fromJSON(steps.test.outputs.sample_failures || '999') < 20 && fromJSON(steps.test.outputs.traces_updated || '0') > 0 run: make code-snippets - name: Open or update code sample traces PR if: >- matrix.product == 'oss' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && steps.regen_snippets.conclusion == 'success' env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail BRANCH_NAME="chore/refresh-code-sample-traces" PATHS=( "src/code-samples/trace-links.json" "src/snippets/code-samples" ) git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" # Preserve refreshed artifacts, then restore a clean tree for branch checkout. mkdir -p "$RUNNER_TEMP/code-sample-traces" for path in "${PATHS[@]}"; do if [[ -e "$path" ]]; then mkdir -p "$RUNNER_TEMP/code-sample-traces/$(dirname "$path")" cp -a "$path" "$RUNNER_TEMP/code-sample-traces/$path" fi done git checkout -- . || true git clean -fd -- src/snippets/code-samples src/code-samples/trace-links.json || true PR_NUMBER="$(gh pr list --head "$BRANCH_NAME" --state open \ --json number --jq '.[0].number // empty')" if [ -n "$PR_NUMBER" ]; then echo "Found open PR #$PR_NUMBER on $BRANCH_NAME; appending to it" git fetch --depth=1 origin \ "$BRANCH_NAME:refs/remotes/origin/$BRANCH_NAME" git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME" else echo "No open PR on $BRANCH_NAME; starting a new one from main" git checkout -B "$BRANCH_NAME" fi for path in "${PATHS[@]}"; do if [[ -e "$RUNNER_TEMP/code-sample-traces/$path" ]]; then mkdir -p "$(dirname "$path")" rm -rf "$path" cp -a "$RUNNER_TEMP/code-sample-traces/$path" "$path" fi done if git diff --quiet -- "${PATHS[@]}"; then echo "No trace-link changes to commit" exit 0 fi git add -- "${PATHS[@]}" git commit -m "$(cat <> "$GITHUB_STEP_SUMMARY" exit 0 fi git push --force origin "$BRANCH_NAME" gh pr create \ --title "chore: refresh code sample example traces" \ --body "$(cat <<'EOF' ## Summary Automated refresh of public LangSmith example traces linked under docs code samples. ## Details - Monthly/full `make test-code-samples` with `CODE_SAMPLE_TRACING=1` - Updates `src/code-samples/trace-links.json` - Regenerates `src/snippets/code-samples/*.mdx` with a `View example trace` Card CTA - Multi-snippet source files are skipped until split into one file per snippet Later refreshes append a commit to this PR while it stays open. 🤖 This PR was created automatically by GitHub Actions EOF )" \ --base main \ --head "$BRANCH_NAME" echo "Opened a new code sample traces PR" >> "$GITHUB_STEP_SUMMARY" - name: Fail job when code samples failed if: always() && steps.test.outcome == 'failure' run: | echo "Code sample failures: ${{ steps.test.outputs.sample_failures }}" echo "Trace links updated: ${{ steps.test.outputs.traces_updated }}" exit 1