.github/workflows/_release.yml YAML 915 lines View on github.com → Search inside
1# Builds and publishes LangChain packages to PyPI.2#3# Manually triggered, though can be used as a reusable workflow (workflow_call).4#5# Handles version bumping, building, and publishing to PyPI with authentication.67name: "🚀 Package Release"8# Run title resolves dropdown values to the published package name (e.g.9# `core` -> `langchain-core`, `openai` -> `langchain-openai`). Falls back to10# the raw input for override and `workflow_call` cases, which already pass11# a full path. Three dropdown values don't follow `langchain-{name}`:12# `langchain` -> `langchain-classic`, `langchain_v1` -> `langchain`,13# `standard-tests` -> `langchain-tests`.14run-name: >-15  Release ${{ inputs.working-directory-override ||16  (startsWith(inputs.working-directory, 'libs/') && inputs.working-directory) ||17  (inputs.working-directory == 'langchain' && 'langchain-classic') ||18  (inputs.working-directory == 'langchain_v1' && 'langchain') ||19  (inputs.working-directory == 'standard-tests' && 'langchain-tests') ||20  format('langchain-{0}', inputs.working-directory) }} ${{21  inputs.release-version }}22on:23  workflow_call:24    inputs:25      working-directory:26        required: true27        type: string28        description: "From which folder this pipeline executes"29      release-version:30        required: false31        type: string32        default: ""33        description: "Expected package version. If provided, must match pyproject.toml."34      allow-prereleases:35        required: false36        type: boolean37        default: false38        description: "Pass `--prerelease=allow` to wheel-install steps so39          transitive prerelease deps (e.g. langgraph-checkpoint>=4.1.0a3 pulled40          in by an alpha langgraph) resolve. Use only when the release itself41          is a prerelease and at least one dep is also a prerelease."42      # `workflow_call` callers must pass an exact lowercase value: `none` or a43      # partner name from the `test-prior-published-packages-against-new-core`44      # matrix (or `all`). Unrecognized values fail safe (the check still runs).45      # Keep this list in sync with that matrix and the `workflow_dispatch`46      # `options` below.47      skip-prior-published-package-checks:48        required: false49        type: string50        default: "none"51        description: "Prior published partner check to skip for core releases:52          none, anthropic, openai, or all."53  workflow_dispatch:54    inputs:55      working-directory:56        required: true57        type: choice58        description: "From which folder this pipeline executes"59        default: "langchain_v1"60        # Short names only  `EFFECTIVE_WORKING_DIR` below re-adds the `libs/`61        # or `libs/partners/` prefix. When adding a new option, also update the62        # non-partner allowlist in `EFFECTIVE_WORKING_DIR` if it isn't a partner63        # package (partners are the default branch).64        options:65          - core66          - langchain67          - langchain_v168          - text-splitters69          - standard-tests70          - model-profiles71          - anthropic72          - chroma73          - deepseek74          - exa75          - fireworks76          - groq77          - huggingface78          - mistralai79          - nomic80          - ollama81          - openai82          - openrouter83          - perplexity84          - qdrant85          - xai86      working-directory-override:87        required: false88        type: string89        description: "Manual override — takes precedence over dropdown (e.g.90          libs/partners/partner-xyz)"91      release-version:92        required: true93        type: string94        default: "0.1.0"95        description: "New version of package being released"96      dangerous-nonmaster-release:97        required: false98        type: boolean99        default: false100        description: "Release from a non-master branch (danger!) - Only use for hotfixes"101      allow-prereleases:102        required: false103        type: boolean104        default: false105        description: "Pass `--prerelease=allow` to wheel-install steps so106          transitive prerelease deps (e.g. langgraph-checkpoint>=4.1.0a3 pulled107          in by an alpha langgraph) resolve. Use only when the release itself108          is a prerelease and at least one dep is also a prerelease."109      skip-prior-published-package-checks:110        required: false111        type: choice112        default: none113        description: "Prior published partner check to skip for core releases"114        options:115          - none116          - anthropic117          - openai118          - all119120env:121  PYTHON_VERSION: "3.11"122  UV_FROZEN: "true"123  UV_NO_SYNC: "true"124  # Resolves to a full path. Accepts either:125  #   - `working-directory-override` as a full path (e.g. `libs/partners/partner-xyz`)126  #   - `working-directory` as a full path (from `workflow_call` callers)127  #   - `working-directory` as a short dropdown name (from `workflow_dispatch`)128  EFFECTIVE_WORKING_DIR: >-129    ${{130      inputs.working-directory-override131      || (startsWith(inputs.working-directory, 'libs/') && inputs.working-directory)132      || (contains(fromJSON('["core","langchain","langchain_v1","text-splitters","standard-tests","model-profiles"]'), inputs.working-directory) && format('libs/{0}', inputs.working-directory))133      || format('libs/partners/{0}', inputs.working-directory)134    }}135136permissions:137  contents: read # Job-level overrides grant write only where needed (mark-release)138139jobs:140  # Build the distribution package and extract version info141  # Runs in isolated environment with minimal permissions for security142  build:143    name: 📦 Build distribution144    if: github.repository_owner == 'langchain-ai' && (github.ref == 'refs/heads/master' || inputs.dangerous-nonmaster-release)145    environment: Release146    runs-on: ubuntu-latest147    permissions:148      contents: read149150    outputs:151      pkg-name: ${{ steps.check-version.outputs.pkg-name }}152      version: ${{ steps.check-version.outputs.version }}153154    steps:155      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6156157      - name: Set up Python + uv158        uses: "./.github/actions/uv_setup"159        with:160          python-version: ${{ env.PYTHON_VERSION }}161          enable-cache: "false"162163      - name: Summarize release bypasses164        if: >-165          inputs.dangerous-nonmaster-release || inputs.allow-prereleases ||166          inputs.skip-prior-published-package-checks != 'none'167        env:168          ALLOW_PRERELEASES: ${{ inputs.allow-prereleases }}169          DANGEROUS_NONMASTER_RELEASE: ${{ inputs.dangerous-nonmaster-release }}170          SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS: ${{ inputs.skip-prior-published-package-checks }}171        run: |172          echo "::warning::Release bypass input(s) enabled. See job summary."173          {174            echo "## ⚠️ Release bypasses enabled"175            echo176            echo "One or more release safety bypasses were selected for this run:"177            echo178            if [ "$DANGEROUS_NONMASTER_RELEASE" = "true" ]; then179              echo "- \`dangerous-nonmaster-release\`: release jobs may run from a non-\`master\` ref."180            fi181            if [ "$ALLOW_PRERELEASES" = "true" ]; then182              echo "- \`allow-prereleases\`: install checks use \`--prerelease=allow\`."183            fi184            if [ -n "$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS" ] && [ "$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS" != "none" ]; then185              echo "- \`skip-prior-published-package-checks\`: \`$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS\`."186            fi187          } >> "$GITHUB_STEP_SUMMARY"188189      - name: Check version190        id: check-version191        shell: python192        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}193        env:194          RELEASE_VERSION_INPUT: ${{ inputs.release-version }}195        run: |196          import os197          import re198          import sys199          import tomllib200          import urllib.error201          import urllib.request202203          with open("pyproject.toml", "rb") as f:204              data = tomllib.load(f)205206          pkg_name = data["project"]["name"]207          version = data["project"]["version"]208          requested_version = os.environ.get("RELEASE_VERSION_INPUT", "").strip()209210211          def normalize(v):212              # Lightweight PEP 440 comparison key: lowercase and drop the `-`,213              # `_`, or `.` separators that precede a pre/post/dev segment so that214              # e.g. `0.1.0-rc1` and `0.1.0rc1` compare equal. Full canonicalization215              # lives in `packaging`, which isn't installed in this bare release step.216              return re.sub(r"[-_.]+(?=[a-z])", "", v.lower())217218219          if requested_version and normalize(requested_version) != normalize(version):220              print(221                  f"::error::Requested release version {requested_version!r} does "222                  f"not match {pkg_name} pyproject.toml version {version!r}."223              )224              sys.exit(1)225226          # Query the per-version endpoint so PyPI applies PEP 440 normalization227          # (e.g. `0.1.0-rc1` and `0.1.0rc1` resolve to the same release): HTTP 200228          # means the version is already published, 404 means it's available229          # (including the first-ever release of a new package). Only the status230          # code is used, so a malicious or malformed response body can't mislead us.231          url = f"https://pypi.org/pypi/{pkg_name}/{version}/json"232          try:233              with urllib.request.urlopen(url, timeout=10):234                  already_published = True235          except urllib.error.HTTPError as err:236              if err.code == 404:237                  already_published = False238              else:239                  # Fail closed: an unexpected status means we can't verify.240                  print(241                      f"::error::PyPI returned HTTP {err.code} checking whether "242                      f"{pkg_name}=={version} exists; cannot verify, aborting."243                  )244                  sys.exit(1)245          except urllib.error.URLError as err:246              # Fail closed: if PyPI is unreachable we must not assume the version247              # is free, or we risk re-publishing an existing release.248              print(249                  f"::error::Could not reach PyPI to verify {pkg_name}=={version} "250                  f"({err.reason}); cannot verify, aborting."251              )252              sys.exit(1)253254          if already_published:255              print(f"::error::{pkg_name}=={version} already exists on PyPI.")256              sys.exit(1)257258259          with open(os.environ["GITHUB_OUTPUT"], "a") as f:260              f.write(f"pkg-name={pkg_name}\n")261              f.write(f"version={version}\n")262263      # We want to keep this build stage *separate* from the release stage,264      # so that there's no sharing of permissions between them.265      # (Release stage has trusted publishing and GitHub repo contents write access,266      # which the build stage must not have access to.)267      #268      # Otherwise, a malicious `build` step (e.g. via a compromised dependency)269      # could get access to our GitHub or PyPI credentials.270      #271      # Per the trusted publishing GitHub Action:272      # > It is strongly advised to separate jobs for building [...]273      # > from the publish job.274      # https://github.com/pypa/gh-action-pypi-publish#non-goals275      - name: Build project for distribution276        run: uv build277        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}278279      - name: Upload build280        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7281        with:282          name: dist283          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/284  release-notes:285    name: 📝 Generate release notes286    # release-notes must run before publishing because its check-tags step287    # validates version/tag state  do not remove this dependency.288    needs:289      - build290    runs-on: ubuntu-latest291    permissions:292      contents: read293    outputs:294      release-body: ${{ steps.generate-release-body.outputs.release-body }}295    steps:296      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6297        with:298          repository: langchain-ai/langchain299          path: langchain300          sparse-checkout: | # this only grabs files for relevant dir301            ${{ env.EFFECTIVE_WORKING_DIR }}302          ref: ${{ github.ref }} # this scopes to just ref'd branch303          fetch-depth: 0 # this fetches entire commit history304      - name: Check tags305        id: check-tags306        shell: bash307        working-directory: langchain/${{ env.EFFECTIVE_WORKING_DIR }}308        env:309          PKG_NAME: ${{ needs.build.outputs.pkg-name }}310          VERSION: ${{ needs.build.outputs.version }}311        run: |312          # Handle regular versions and pre-release versions differently313          if [[ "$VERSION" == *"-"* ]]; then314            # This is a pre-release version (contains a hyphen)315            # Extract the base version without the pre-release suffix316            BASE_VERSION=${VERSION%%-*}317            # Look for the latest release of the same base version318            REGEX="^$PKG_NAME==$BASE_VERSION\$"319            PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1)320321            # If no exact base version match, look for the latest release of any kind322            if [ -z "$PREV_TAG" ]; then323              REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$"324              PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1)325            fi326          else327            # Regular version handling328            PREV_TAG="$PKG_NAME==${VERSION%.*}.$(( ${VERSION##*.} - 1 ))"; [[ "${VERSION##*.}" -eq 0 ]] && PREV_TAG=""329330            # backup case if releasing e.g. 0.3.0, looks up last release331            # note if last release (chronologically) was e.g. 0.1.47 it will get332            # that instead of the last 0.2 release333            if [ -z "$PREV_TAG" ]; then334              REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$"335              echo $REGEX336              PREV_TAG=$(git tag --sort=-creatordate | (grep -P $REGEX || true) | head -1)337            fi338          fi339340          # if PREV_TAG is empty or came out to 0.0.0, let it be empty341          if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then342            echo "No previous tag found - first release"343          else344            # confirm prev-tag actually exists in git repo with git tag345            GIT_TAG_RESULT=$(git tag -l "$PREV_TAG")346            if [ -z "$GIT_TAG_RESULT" ]; then347              echo "Previous tag $PREV_TAG not found in git repo"348              exit 1349            fi350          fi351352353          TAG="${PKG_NAME}==${VERSION}"354          if [ "$TAG" == "$PREV_TAG" ]; then355            echo "No new version to release"356            exit 1357          fi358          echo tag="$TAG" >> $GITHUB_OUTPUT359          echo prev-tag="$PREV_TAG" >> $GITHUB_OUTPUT360      - name: Generate release body361        id: generate-release-body362        working-directory: langchain363        env:364          WORKING_DIR: ${{ env.EFFECTIVE_WORKING_DIR }}365          PKG_NAME: ${{ needs.build.outputs.pkg-name }}366          TAG: ${{ steps.check-tags.outputs.tag }}367          PREV_TAG: ${{ steps.check-tags.outputs.prev-tag }}368        run: |369          PREAMBLE="Changes since $PREV_TAG"370          # if PREV_TAG is empty or 0.0.0, then we are releasing the first version371          if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then372            PREAMBLE="Initial release"373            PREV_TAG=$(git rev-list --max-parents=0 HEAD)374          fi375          {376            echo 'release-body<<EOF'377            echo $PREAMBLE378            echo379            git log --format="%s" "$PREV_TAG"..HEAD -- $WORKING_DIR380            echo EOF381          } >> "$GITHUB_OUTPUT"382383  pre-release-checks:384    name:  Pre-release checks385    needs:386      - build387      - release-notes388    environment: Release389    runs-on: ubuntu-latest390    permissions:391      contents: read392    timeout-minutes: 20393    steps:394      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6395396      # We explicitly *don't* set up caching here. This ensures our tests are397      # maximally sensitive to catching breakage.398      #399      # For example, here's a way that caching can cause a falsely-passing test:400      # - Make the langchain package manifest no longer list a dependency package401      #   as a requirement. This means it won't be installed by `pip install`,402      #   and attempting to use it would cause a crash.403      # - That dependency used to be required, so it may have been cached.404      #   When restoring the venv packages from cache, that dependency gets included.405      # - Tests pass, because the dependency is present even though it wasn't specified.406      # - The package is published, and it breaks on the missing dependency when407      #   used in the real world.408409      - name: Set up Python + uv410        uses: "./.github/actions/uv_setup"411        id: setup-python412        with:413          python-version: ${{ env.PYTHON_VERSION }}414          enable-cache: "false"415416      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8417        with:418          name: dist419          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/420421      - name: Import dist package422        shell: bash423        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}424        env:425          PKG_NAME: ${{ needs.build.outputs.pkg-name }}426          VERSION: ${{ needs.build.outputs.version }}427          PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }}428        # Install directly from the locally-built wheel (no index resolution needed).429        # `PRERELEASE_FLAG` is empty by default; opt-in via the `allow-prereleases`430        # workflow input lets transitive prerelease deps resolve during alpha431        # release cycles. Stable-release safety is still enforced by the432        # `Check for prerelease versions` step below.433        run: |434          uv venv435          VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG dist/*.whl436437          # Replace all dashes in the package name with underscores,438          # since that's how Python imports packages with dashes in the name.439          # also remove _official suffix440          IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/_/g | sed s/_official//g)"441442          uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))"443444      - name: Import test dependencies445        run: uv sync --group test446        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}447448      # Overwrite the local version of the package with the built version449      - name: Import published package (again)450        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}451        shell: bash452        env:453          PKG_NAME: ${{ needs.build.outputs.pkg-name }}454          VERSION: ${{ needs.build.outputs.version }}455          PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }}456        run: |457          VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG dist/*.whl458459      - name: Check for prerelease versions460        # Block release if any dependencies allow prerelease versions461        # (unless this is itself a prerelease version)462        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}463        run: |464          uv run python $GITHUB_WORKSPACE/.github/scripts/check_prerelease_dependencies.py pyproject.toml465466      - name: Run unit tests467        run: make tests468        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}469470      - name: Get minimum versions471        # Find the minimum published versions that satisfies the given constraints472        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}473        id: min-version474        run: |475          VIRTUAL_ENV=.venv uv pip install packaging requests476          python_version="$(uv run python --version | awk '{print $2}')"477          min_versions="$(uv run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml release $python_version)"478          echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT"479          echo "min-versions=$min_versions"480481      - name: Run unit tests with minimum dependency versions482        if: ${{ steps.min-version.outputs.min-versions != '' }}483        env:484          MIN_VERSIONS: ${{ steps.min-version.outputs.min-versions }}485          PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }}486        run: |487          VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG --force-reinstall --editable .488          VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG --force-reinstall $MIN_VERSIONS489          make tests PYTEST_EXTRA="-q -k 'not test_serdes'"490        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}491492      - name: Import integration test dependencies493        run: uv sync --group test --group test_integration494        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}495496      - name: Run integration tests497        # Uses the Makefile's `integration_tests` target for the specified package498        if: ${{ startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/partners/') }}499        env:500          AI21_API_KEY: ${{ secrets.AI21_API_KEY }}501          GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}502          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}503          MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}504          TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}505          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}506          AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}507          AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}508          AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}509          AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }}510          AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME }}511          AZURE_OPENAI_LLM_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LLM_DEPLOYMENT_NAME }}512          AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }}513          NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}514          GOOGLE_SEARCH_API_KEY: ${{ secrets.GOOGLE_SEARCH_API_KEY }}515          GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }}516          GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}517          HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }}518          EXA_API_KEY: ${{ secrets.EXA_API_KEY }}519          NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }}520          WATSONX_APIKEY: ${{ secrets.WATSONX_APIKEY }}521          WATSONX_PROJECT_ID: ${{ secrets.WATSONX_PROJECT_ID }}522          ASTRA_DB_API_ENDPOINT: ${{ secrets.ASTRA_DB_API_ENDPOINT }}523          ASTRA_DB_APPLICATION_TOKEN: ${{ secrets.ASTRA_DB_APPLICATION_TOKEN }}524          ASTRA_DB_KEYSPACE: ${{ secrets.ASTRA_DB_KEYSPACE }}525          ES_URL: ${{ secrets.ES_URL }}526          ES_CLOUD_ID: ${{ secrets.ES_CLOUD_ID }}527          ES_API_KEY: ${{ secrets.ES_API_KEY }}528          MONGODB_ATLAS_URI: ${{ secrets.MONGODB_ATLAS_URI }}529          UPSTAGE_API_KEY: ${{ secrets.UPSTAGE_API_KEY }}530          FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}531          XAI_API_KEY: ${{ secrets.XAI_API_KEY }}532          DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}533          PPLX_API_KEY: ${{ secrets.PPLX_API_KEY }}534          OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }}535          OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}536          LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}537        run: make integration_tests538        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}539540  test-pypi-publish:541    name: 🧪 Publish to TestPyPI542    # release-notes must run before publishing because its check-tags step543    # validates version/tag state  do not remove this dependency.544    needs:545      - build546      - release-notes547      - pre-release-checks548    environment: Release549    runs-on: ubuntu-latest550    permissions:551      # This permission is used for trusted publishing:552      # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/553      #554      # Trusted publishing has to also be configured on PyPI for each package:555      # https://docs.pypi.org/trusted-publishers/adding-a-publisher/556      id-token: write557558    steps:559      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6560561      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8562        with:563          name: dist564          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/565566      - name: Publish to test PyPI567        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1568        with:569          packages-dir: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/570          verbose: true571          print-hash: true572          repository-url: https://test.pypi.org/legacy/573          # We overwrite any existing distributions with the same name and version.574          # This is *only for CI use* and is *extremely dangerous* otherwise!575          # https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates576          skip-existing: true577          # Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0578          attestations: false579580  # Test select published packages against new core581  # Done when code changes are made to langchain-core582  test-prior-published-packages-against-new-core:583    name: 🔄 Test prior partners against new core584    # Installs the new core with old partners: Installs the new unreleased core585    # alongside the previously published partner packages and runs unit and integration tests586    needs:587      - build588      - release-notes589      - test-pypi-publish590      - pre-release-checks591    environment: Release592    runs-on: ubuntu-latest593    permissions:594      contents: read595    strategy:596      matrix:597        # When adding a partner, also update the `skip-prior-published-package-checks`598        # input (the `workflow_dispatch` `options` list and the `workflow_call`599        # description) so the per-partner skip remains selectable.600        partner: [ anthropic, openai ]601      fail-fast: false # Continue testing other partners if one fails602    env:603      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}604      ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }}605      ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }}606      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}607      AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }}608      AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }}609      AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}610      AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }}611      AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME }}612      AZURE_OPENAI_LLM_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LLM_DEPLOYMENT_NAME }}613      AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }}614      LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }}615    steps:616      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6617618      # We implement this conditional as Github Actions does not have good support619      # for conditionally needing steps. https://github.com/actions/runner/issues/491620      # TODO: this seems to be resolved upstream, so we can probably remove this workaround621      - name: Check if libs/core622        run: |623          if [ "${{ startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') }}" != "true" ]; then624            echo "Not in libs/core. Exiting successfully."625            exit 0626          fi627628      - name: Set up Python + uv629        if: startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core')630        uses: "./.github/actions/uv_setup"631        with:632          python-version: ${{ env.PYTHON_VERSION }}633          enable-cache: "false"634635      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8636        if: startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core')637        with:638          name: dist639          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/640641      - name: Skip prior published ${{ matrix.partner }} check642        if: >-643          startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') &&644          (inputs.skip-prior-published-package-checks == matrix.partner ||645          inputs.skip-prior-published-package-checks == 'all')646        run: |647          echo "Skipping prior published ${{ matrix.partner }} check as requested."648649      - name: Test against ${{ matrix.partner }}650        if: >-651          startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') &&652          inputs.skip-prior-published-package-checks != matrix.partner &&653          inputs.skip-prior-published-package-checks != 'all'654        env:655          PARTNER: ${{ matrix.partner }}656          PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }}657        run: |658          PACKAGE_NAME="langchain-$PARTNER"659660          # Identify the latest non-yanked published package release, excluding pre-releases.661          # Fail closed (matching the `Check version` step) so a PyPI outage or a662          # missing release aborts with a clear message rather than an empty version.663          LATEST_PACKAGE_VERSION="$(PACKAGE_NAME="$PACKAGE_NAME" python - <<'PY'664          import json665          import os666          import re667          import sys668          import urllib.error669          import urllib.request670671          package_name = os.environ["PACKAGE_NAME"]672          url = f"https://pypi.org/pypi/{package_name}/json"673          try:674              with urllib.request.urlopen(url, timeout=10) as response:675                  data = json.load(response)676          except urllib.error.HTTPError as err:677              print(678                  f"::error::PyPI returned HTTP {err.code} listing {package_name} "679                  f"releases; cannot determine latest version, aborting.",680                  file=sys.stderr,681              )682              sys.exit(1)683          except urllib.error.URLError as err:684              print(685                  f"::error::Could not reach PyPI to list {package_name} releases "686                  f"({err.reason}); cannot determine latest version, aborting.",687                  file=sys.stderr,688              )689              sys.exit(1)690691          versions: list[tuple[int, int, int, str]] = []692          for version, files in data["releases"].items():693              if not re.fullmatch(r"\d+\.\d+\.\d+", version):694                  continue695              if not files or all(file.get("yanked", False) for file in files):696                  continue697              versions.append((*map(int, version.split(".")), version))698699          if not versions:700              print(f"::error::No non-yanked final releases found for {package_name}", file=sys.stderr)701              sys.exit(1)702703          print(max(versions)[3])704          PY705          )"706707          # Belt-and-suspenders: a bare assignment masks the heredoc's exit status708          # in some shells, so guard explicitly rather than relying on `set -e`.709          if [ -z "$LATEST_PACKAGE_VERSION" ]; then710            echo "::error::Could not determine latest published $PACKAGE_NAME version; aborting."711            exit 1712          fi713714          LATEST_PACKAGE_TAG="$PACKAGE_NAME==$LATEST_PACKAGE_VERSION"715          echo "Latest non-yanked package tag: $LATEST_PACKAGE_TAG"716717          # Ensure the PyPI release maps to a source tag before running tests.718          git ls-remote --exit-code --tags origin "refs/tags/$LATEST_PACKAGE_TAG"719720          # Shallow-fetch just that single tag721          git fetch --depth=1 origin tag "$LATEST_PACKAGE_TAG"722723          # Checkout the latest package files724          rm -rf "$GITHUB_WORKSPACE/libs/partners/$PARTNER"/*725          rm -rf $GITHUB_WORKSPACE/libs/standard-tests/*726          cd $GITHUB_WORKSPACE/libs/727          git checkout "$LATEST_PACKAGE_TAG" -- standard-tests/728          git checkout "$LATEST_PACKAGE_TAG" -- "partners/$PARTNER/"729          cd "partners/$PARTNER"730731          # Print as a sanity check732          echo "Version number from pyproject.toml: "733          cat pyproject.toml | grep "version = "734735          # Run tests736          uv sync --group test --group test_integration737          uv pip install $PRERELEASE_FLAG ../../core/dist/*.whl738          make test739          make integration_tests740741  # Test external packages that depend on langchain-core/langchain against the new release742  # Only runs for core and langchain_v1 releases to catch breaking changes before publish743  test-dependents:744    name: "🐍 Test dependent: ${{ matrix.package.path }} (Python ${{745      matrix.python-version }})"746    needs:747      - build748      - release-notes749      - test-pypi-publish750      - pre-release-checks751    runs-on: ubuntu-latest752    permissions:753      contents: read754    # Only run for core or langchain_v1 releases.755    # Job-level 'if' does not support env context, so EFFECTIVE_WORKING_DIR is756    # unavailable; must use inputs directly and match both forms: short dropdown757    # names (workflow_dispatch, e.g. 'core') and full 'libs/' paths758    # (workflow_call / working-directory-override).759    if: >-760      contains(fromJSON('["core","langchain_v1"]'),761      inputs.working-directory-override || inputs.working-directory) ||762      startsWith(inputs.working-directory-override || inputs.working-directory,763      'libs/core') || startsWith(inputs.working-directory-override ||764      inputs.working-directory, 'libs/langchain_v1')765    strategy:766      fail-fast: false767      matrix:768        python-version: [ "3.11", "3.13" ]769        package:770          - name: deepagents771            repo: langchain-ai/deepagents772            path: libs/deepagents773    # No API keys needed for now - deepagents `make test` only runs unit tests774775    steps:776      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6777        with:778          path: langchain779780      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6781        with:782          repository: ${{ matrix.package.repo }}783          path: ${{ matrix.package.name }}784785      - name: Set up Python + uv786        uses: "./langchain/.github/actions/uv_setup"787        with:788          python-version: ${{ matrix.python-version }}789          enable-cache: "false"790791      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8792        with:793          name: dist794          path: dist/795796      - name: Install ${{ matrix.package.name }} with local packages797        # External dependents don't have [tool.uv.sources] pointing to this repo,798        # so we install the package normally then override with the built wheel.799        env:800          PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }}801        run: |802          cd ${{ matrix.package.name }}/${{ matrix.package.path }}803804          # Install the package with test dependencies805          uv sync --group test806807          # Override with the built wheel from this release808          uv pip install $PRERELEASE_FLAG $GITHUB_WORKSPACE/dist/*.whl809810      - name: Run ${{ matrix.package.name }} tests811        run: |812          cd ${{ matrix.package.name }}/${{ matrix.package.path }}813          make test814815  publish:816    name: 🚀 Publish to PyPI817    # Publishes the package to PyPI818    needs:819      - build820      - release-notes821      - test-pypi-publish822      - pre-release-checks823      - test-dependents824      - test-prior-published-packages-against-new-core825      # Run if all needed jobs succeeded or were skipped (test-dependents and826      # test-prior-published-packages-against-new-core only run for core/langchain_v1)827    if: ${{ !cancelled() && !failure() }}828    environment: Release829    runs-on: ubuntu-latest830    permissions:831      # This permission is used for trusted publishing:832      # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/833      #834      # Trusted publishing has to also be configured on PyPI for each package:835      # https://docs.pypi.org/trusted-publishers/adding-a-publisher/836      id-token: write837838    defaults:839      run:840        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}841842    steps:843      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6844845      - name: Set up Python + uv846        uses: "./.github/actions/uv_setup"847        with:848          python-version: ${{ env.PYTHON_VERSION }}849          enable-cache: "false"850851      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8852        with:853          name: dist854          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/855856      - name: Publish package distributions to PyPI857        uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1858        with:859          packages-dir: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/860          verbose: true861          print-hash: true862          # Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0863          attestations: false864865  mark-release:866    name: 🏷️ Tag GitHub release867    # Marks the GitHub release with the new version tag868    needs:869      - build870      - release-notes871      - test-pypi-publish872      - pre-release-checks873      - publish874    # Run if all needed jobs succeeded or were skipped875    if: ${{ !cancelled() && !failure() }}876    environment: Release877    runs-on: ubuntu-latest878    permissions:879      # This permission is needed by `ncipollo/release-action` to880      # create the GitHub release/tag881      contents: write882883    defaults:884      run:885        working-directory: ${{ env.EFFECTIVE_WORKING_DIR }}886887    steps:888      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v6889890      - name: Set up Python + uv891        uses: "./.github/actions/uv_setup"892        with:893          python-version: ${{ env.PYTHON_VERSION }}894          enable-cache: "false"895896      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8897        with:898          name: dist899          path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/900901      - name: Create Tag902        uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1903        with:904          # JS actions ignore `defaults.run.working-directory`, so this glob is905          # resolved from the repo root. Point it at the package's `dist/`906          # (where `download-artifact` placed the wheels) instead of a bare907          # `dist/*`, which never matched and attached no assets to releases.908          artifacts: "${{ env.EFFECTIVE_WORKING_DIR }}/dist/*"909          token: ${{ secrets.GITHUB_TOKEN }}910          generateReleaseNotes: false911          tag: ${{needs.build.outputs.pkg-name}}==${{ needs.build.outputs.version }}912          body: ${{ needs.release-notes.outputs.release-body }}913          commit: ${{ github.sha }}914          makeLatest: ${{ needs.build.outputs.pkg-name == 'langchain-core'}}

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.