# Builds and publishes LangChain packages to PyPI. # # Manually triggered, though can be used as a reusable workflow (workflow_call). # # Handles version bumping, building, and publishing to PyPI with authentication. name: "๐Ÿš€ Package Release" # Run title resolves dropdown values to the published package name (e.g. # `core` -> `langchain-core`, `openai` -> `langchain-openai`). Falls back to # the raw input for override and `workflow_call` cases, which already pass # a full path. Three dropdown values don't follow `langchain-{name}`: # `langchain` -> `langchain-classic`, `langchain_v1` -> `langchain`, # `standard-tests` -> `langchain-tests`. run-name: >- Release ${{ inputs.working-directory-override || (startsWith(inputs.working-directory, 'libs/') && inputs.working-directory) || (inputs.working-directory == 'langchain' && 'langchain-classic') || (inputs.working-directory == 'langchain_v1' && 'langchain') || (inputs.working-directory == 'standard-tests' && 'langchain-tests') || format('langchain-{0}', inputs.working-directory) }} ${{ inputs.release-version }} on: workflow_call: inputs: working-directory: required: true type: string description: "From which folder this pipeline executes" release-version: required: false type: string default: "" description: "Expected package version. If provided, must match pyproject.toml." allow-prereleases: required: false type: boolean default: false description: "Pass `--prerelease=allow` to wheel-install steps so transitive prerelease deps (e.g. langgraph-checkpoint>=4.1.0a3 pulled in by an alpha langgraph) resolve. Use only when the release itself is a prerelease and at least one dep is also a prerelease." # `workflow_call` callers must pass an exact lowercase value: `none` or a # partner name from the `test-prior-published-packages-against-new-core` # matrix (or `all`). Unrecognized values fail safe (the check still runs). # Keep this list in sync with that matrix and the `workflow_dispatch` # `options` below. skip-prior-published-package-checks: required: false type: string default: "none" description: "Prior published partner check to skip for core releases: none, anthropic, openai, or all." workflow_dispatch: inputs: working-directory: required: true type: choice description: "From which folder this pipeline executes" default: "langchain_v1" # Short names only โ€” `EFFECTIVE_WORKING_DIR` below re-adds the `libs/` # or `libs/partners/` prefix. When adding a new option, also update the # non-partner allowlist in `EFFECTIVE_WORKING_DIR` if it isn't a partner # package (partners are the default branch). options: - core - langchain - langchain_v1 - text-splitters - standard-tests - model-profiles - anthropic - chroma - deepseek - exa - fireworks - groq - huggingface - mistralai - nomic - ollama - openai - openrouter - perplexity - qdrant - xai working-directory-override: required: false type: string description: "Manual override โ€” takes precedence over dropdown (e.g. libs/partners/partner-xyz)" release-version: required: true type: string default: "0.1.0" description: "New version of package being released" dangerous-nonmaster-release: required: false type: boolean default: false description: "Release from a non-master branch (danger!) - Only use for hotfixes" allow-prereleases: required: false type: boolean default: false description: "Pass `--prerelease=allow` to wheel-install steps so transitive prerelease deps (e.g. langgraph-checkpoint>=4.1.0a3 pulled in by an alpha langgraph) resolve. Use only when the release itself is a prerelease and at least one dep is also a prerelease." skip-prior-published-package-checks: required: false type: choice default: none description: "Prior published partner check to skip for core releases" options: - none - anthropic - openai - all env: PYTHON_VERSION: "3.11" UV_FROZEN: "true" UV_NO_SYNC: "true" # Resolves to a full path. Accepts either: # - `working-directory-override` as a full path (e.g. `libs/partners/partner-xyz`) # - `working-directory` as a full path (from `workflow_call` callers) # - `working-directory` as a short dropdown name (from `workflow_dispatch`) EFFECTIVE_WORKING_DIR: >- ${{ inputs.working-directory-override || (startsWith(inputs.working-directory, 'libs/') && inputs.working-directory) || (contains(fromJSON('["core","langchain","langchain_v1","text-splitters","standard-tests","model-profiles"]'), inputs.working-directory) && format('libs/{0}', inputs.working-directory)) || format('libs/partners/{0}', inputs.working-directory) }} permissions: contents: read # Job-level overrides grant write only where needed (mark-release) jobs: # Build the distribution package and extract version info # Runs in isolated environment with minimal permissions for security build: name: ๐Ÿ“ฆ Build distribution if: github.ref == 'refs/heads/master' || inputs.dangerous-nonmaster-release environment: Release runs-on: ubuntu-latest permissions: contents: read outputs: pkg-name: ${{ steps.check-version.outputs.pkg-name }} version: ${{ steps.check-version.outputs.version }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python + uv uses: "./.github/actions/uv_setup" with: python-version: ${{ env.PYTHON_VERSION }} enable-cache: "false" - name: Summarize release bypasses if: >- inputs.dangerous-nonmaster-release || inputs.allow-prereleases || inputs.skip-prior-published-package-checks != 'none' env: ALLOW_PRERELEASES: ${{ inputs.allow-prereleases }} DANGEROUS_NONMASTER_RELEASE: ${{ inputs.dangerous-nonmaster-release }} SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS: ${{ inputs.skip-prior-published-package-checks }} run: | echo "::warning::Release bypass input(s) enabled. See job summary." { echo "## โš ๏ธ Release bypasses enabled" echo echo "One or more release safety bypasses were selected for this run:" echo if [ "$DANGEROUS_NONMASTER_RELEASE" = "true" ]; then echo "- \`dangerous-nonmaster-release\`: release jobs may run from a non-\`master\` ref." fi if [ "$ALLOW_PRERELEASES" = "true" ]; then echo "- \`allow-prereleases\`: install checks use \`--prerelease=allow\`." fi if [ -n "$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS" ] && [ "$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS" != "none" ]; then echo "- \`skip-prior-published-package-checks\`: \`$SKIP_PRIOR_PUBLISHED_PACKAGE_CHECKS\`." fi } >> "$GITHUB_STEP_SUMMARY" - name: Check version id: check-version shell: python working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} env: RELEASE_VERSION_INPUT: ${{ inputs.release-version }} run: | import os import re import sys import tomllib import urllib.error import urllib.request with open("pyproject.toml", "rb") as f: data = tomllib.load(f) pkg_name = data["project"]["name"] version = data["project"]["version"] requested_version = os.environ.get("RELEASE_VERSION_INPUT", "").strip() def normalize(v): # Lightweight PEP 440 comparison key: lowercase and drop the `-`, # `_`, or `.` separators that precede a pre/post/dev segment so that # e.g. `0.1.0-rc1` and `0.1.0rc1` compare equal. Full canonicalization # lives in `packaging`, which isn't installed in this bare release step. return re.sub(r"[-_.]+(?=[a-z])", "", v.lower()) if requested_version and normalize(requested_version) != normalize(version): print( f"::error::Requested release version {requested_version!r} does " f"not match {pkg_name} pyproject.toml version {version!r}." ) sys.exit(1) # Query the per-version endpoint so PyPI applies PEP 440 normalization # (e.g. `0.1.0-rc1` and `0.1.0rc1` resolve to the same release): HTTP 200 # means the version is already published, 404 means it's available # (including the first-ever release of a new package). Only the status # code is used, so a malicious or malformed response body can't mislead us. url = f"https://pypi.org/pypi/{pkg_name}/{version}/json" try: with urllib.request.urlopen(url, timeout=10): already_published = True except urllib.error.HTTPError as err: if err.code == 404: already_published = False else: # Fail closed: an unexpected status means we can't verify. print( f"::error::PyPI returned HTTP {err.code} checking whether " f"{pkg_name}=={version} exists; cannot verify, aborting." ) sys.exit(1) except urllib.error.URLError as err: # Fail closed: if PyPI is unreachable we must not assume the version # is free, or we risk re-publishing an existing release. print( f"::error::Could not reach PyPI to verify {pkg_name}=={version} " f"({err.reason}); cannot verify, aborting." ) sys.exit(1) if already_published: print(f"::error::{pkg_name}=={version} already exists on PyPI.") sys.exit(1) with open(os.environ["GITHUB_OUTPUT"], "a") as f: f.write(f"pkg-name={pkg_name}\n") f.write(f"version={version}\n") # We want to keep this build stage *separate* from the release stage, # so that there's no sharing of permissions between them. # (Release stage has trusted publishing and GitHub repo contents write access, # which the build stage must not have access to.) # # Otherwise, a malicious `build` step (e.g. via a compromised dependency) # could get access to our GitHub or PyPI credentials. # # Per the trusted publishing GitHub Action: # > 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 project for distribution run: uv build working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} - name: Upload build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ release-notes: name: ๐Ÿ“ Generate release notes # release-notes must run before publishing because its check-tags step # validates version/tag state โ€” do not remove this dependency. needs: - build runs-on: ubuntu-latest permissions: contents: read outputs: release-body: ${{ steps.generate-release-body.outputs.release-body }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: repository: langchain-ai/langchain path: langchain sparse-checkout: | # this only grabs files for relevant dir ${{ env.EFFECTIVE_WORKING_DIR }} ref: ${{ github.ref }} # this scopes to just ref'd branch fetch-depth: 0 # this fetches entire commit history - name: Check tags id: check-tags shell: bash working-directory: langchain/${{ env.EFFECTIVE_WORKING_DIR }} env: PKG_NAME: ${{ needs.build.outputs.pkg-name }} VERSION: ${{ needs.build.outputs.version }} run: | # Handle regular versions and pre-release versions differently if [[ "$VERSION" == *"-"* ]]; then # This is a pre-release version (contains a hyphen) # Extract the base version without the pre-release suffix BASE_VERSION=${VERSION%%-*} # Look for the latest release of the same base version REGEX="^$PKG_NAME==$BASE_VERSION\$" PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1) # If no exact base version match, look for the latest release of any kind if [ -z "$PREV_TAG" ]; then REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$" PREV_TAG=$(git tag --sort=-creatordate | (grep -P "$REGEX" || true) | head -1) fi else # Regular version handling PREV_TAG="$PKG_NAME==${VERSION%.*}.$(( ${VERSION##*.} - 1 ))"; [[ "${VERSION##*.}" -eq 0 ]] && PREV_TAG="" # backup case if releasing e.g. 0.3.0, looks up last release # note if last release (chronologically) was e.g. 0.1.47 it will get # that instead of the last 0.2 release if [ -z "$PREV_TAG" ]; then REGEX="^$PKG_NAME==\\d+\\.\\d+\\.\\d+\$" echo $REGEX PREV_TAG=$(git tag --sort=-creatordate | (grep -P $REGEX || true) | head -1) fi fi # if PREV_TAG is empty or came out to 0.0.0, let it be empty if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then echo "No previous tag found - first release" else # confirm prev-tag actually exists in git repo with git tag GIT_TAG_RESULT=$(git tag -l "$PREV_TAG") if [ -z "$GIT_TAG_RESULT" ]; then echo "Previous tag $PREV_TAG not found in git repo" exit 1 fi fi TAG="${PKG_NAME}==${VERSION}" if [ "$TAG" == "$PREV_TAG" ]; then echo "No new version to release" exit 1 fi echo tag="$TAG" >> $GITHUB_OUTPUT echo prev-tag="$PREV_TAG" >> $GITHUB_OUTPUT - name: Generate release body id: generate-release-body working-directory: langchain env: WORKING_DIR: ${{ env.EFFECTIVE_WORKING_DIR }} PKG_NAME: ${{ needs.build.outputs.pkg-name }} TAG: ${{ steps.check-tags.outputs.tag }} PREV_TAG: ${{ steps.check-tags.outputs.prev-tag }} run: | PREAMBLE="Changes since $PREV_TAG" # if PREV_TAG is empty or 0.0.0, then we are releasing the first version if [ -z "$PREV_TAG" ] || [ "$PREV_TAG" = "$PKG_NAME==0.0.0" ]; then PREAMBLE="Initial release" PREV_TAG=$(git rev-list --max-parents=0 HEAD) fi { echo 'release-body<> "$GITHUB_OUTPUT" pre-release-checks: name: โœ… Pre-release checks needs: - build - release-notes environment: Release runs-on: ubuntu-latest permissions: contents: read timeout-minutes: 20 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # We explicitly *don't* set up caching here. This ensures our tests are # maximally sensitive to catching breakage. # # For example, here's a way that caching can cause a falsely-passing test: # - Make the langchain package manifest no longer list a dependency package # as a requirement. This means it won't be installed by `pip install`, # and attempting to use it would cause a crash. # - That dependency used to be required, so it may have been cached. # When restoring the venv packages from cache, that dependency gets included. # - Tests pass, because the dependency is present even though it wasn't specified. # - The package is published, and it breaks on the missing dependency when # used in the real world. - name: Set up Python + uv uses: "./.github/actions/uv_setup" id: setup-python with: python-version: ${{ env.PYTHON_VERSION }} enable-cache: "false" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ - name: Import dist package shell: bash working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} env: PKG_NAME: ${{ needs.build.outputs.pkg-name }} VERSION: ${{ needs.build.outputs.version }} PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }} # Install directly from the locally-built wheel (no index resolution needed). # `PRERELEASE_FLAG` is empty by default; opt-in via the `allow-prereleases` # workflow input lets transitive prerelease deps resolve during alpha # release cycles. Stable-release safety is still enforced by the # `Check for prerelease versions` step below. run: | uv venv VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG dist/*.whl # Replace all dashes in the package name with underscores, # since that's how Python imports packages with dashes in the name. # also remove _official suffix IMPORT_NAME="$(echo "$PKG_NAME" | sed s/-/_/g | sed s/_official//g)" uv run python -c "import $IMPORT_NAME; print(dir($IMPORT_NAME))" - name: Import test dependencies run: uv sync --group test working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} # Overwrite the local version of the package with the built version - name: Import published package (again) working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} shell: bash env: PKG_NAME: ${{ needs.build.outputs.pkg-name }} VERSION: ${{ needs.build.outputs.version }} PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }} run: | VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG dist/*.whl - name: Check for prerelease versions # Block release if any dependencies allow prerelease versions # (unless this is itself a prerelease version) working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} run: | uv run python $GITHUB_WORKSPACE/.github/scripts/check_prerelease_dependencies.py pyproject.toml - name: Run unit tests run: make tests working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} - name: Get minimum versions # Find the minimum published versions that satisfies the given constraints working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} id: min-version run: | VIRTUAL_ENV=.venv uv pip install packaging requests python_version="$(uv run python --version | awk '{print $2}')" min_versions="$(uv run python $GITHUB_WORKSPACE/.github/scripts/get_min_versions.py pyproject.toml release $python_version)" echo "min-versions=$min_versions" >> "$GITHUB_OUTPUT" echo "min-versions=$min_versions" - name: Run unit tests with minimum dependency versions if: ${{ steps.min-version.outputs.min-versions != '' }} env: MIN_VERSIONS: ${{ steps.min-version.outputs.min-versions }} PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }} run: | VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG --force-reinstall --editable . VIRTUAL_ENV=.venv uv pip install $PRERELEASE_FLAG --force-reinstall $MIN_VERSIONS make tests PYTEST_EXTRA="-q -k 'not test_serdes'" working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} - name: Import integration test dependencies run: uv sync --group test --group test_integration working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} - name: Run integration tests # Uses the Makefile's `integration_tests` target for the specified package if: ${{ startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/partners/') }} env: AI21_API_KEY: ${{ secrets.AI21_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }} AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME }} AZURE_OPENAI_LLM_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LLM_DEPLOYMENT_NAME }} AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }} NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} GOOGLE_SEARCH_API_KEY: ${{ secrets.GOOGLE_SEARCH_API_KEY }} GOOGLE_CSE_ID: ${{ secrets.GOOGLE_CSE_ID }} GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} HUGGINGFACEHUB_API_TOKEN: ${{ secrets.HUGGINGFACEHUB_API_TOKEN }} EXA_API_KEY: ${{ secrets.EXA_API_KEY }} NOMIC_API_KEY: ${{ secrets.NOMIC_API_KEY }} WATSONX_APIKEY: ${{ secrets.WATSONX_APIKEY }} WATSONX_PROJECT_ID: ${{ secrets.WATSONX_PROJECT_ID }} ASTRA_DB_API_ENDPOINT: ${{ secrets.ASTRA_DB_API_ENDPOINT }} ASTRA_DB_APPLICATION_TOKEN: ${{ secrets.ASTRA_DB_APPLICATION_TOKEN }} ASTRA_DB_KEYSPACE: ${{ secrets.ASTRA_DB_KEYSPACE }} ES_URL: ${{ secrets.ES_URL }} ES_CLOUD_ID: ${{ secrets.ES_CLOUD_ID }} ES_API_KEY: ${{ secrets.ES_API_KEY }} MONGODB_ATLAS_URI: ${{ secrets.MONGODB_ATLAS_URI }} UPSTAGE_API_KEY: ${{ secrets.UPSTAGE_API_KEY }} FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }} XAI_API_KEY: ${{ secrets.XAI_API_KEY }} DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} PPLX_API_KEY: ${{ secrets.PPLX_API_KEY }} OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }} OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }} LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }} run: make integration_tests working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} test-pypi-publish: name: ๐Ÿงช Publish to TestPyPI # release-notes must run before publishing because its check-tags step # validates version/tag state โ€” do not remove this dependency. needs: - build - release-notes - pre-release-checks environment: Release runs-on: ubuntu-latest permissions: # This permission is used for trusted publishing: # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ # # Trusted publishing has to also be configured on PyPI for each package: # https://docs.pypi.org/trusted-publishers/adding-a-publisher/ id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ - name: Publish to test PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: packages-dir: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ verbose: true print-hash: true repository-url: https://test.pypi.org/legacy/ # We overwrite any existing distributions with the same name and version. # This is *only for CI use* and is *extremely dangerous* otherwise! # https://github.com/pypa/gh-action-pypi-publish#tolerating-release-package-file-duplicates skip-existing: true # Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0 attestations: false # Test select published packages against new core # Done when code changes are made to langchain-core test-prior-published-packages-against-new-core: name: ๐Ÿ”„ Test prior partners against new core # Installs the new core with old partners: Installs the new unreleased core # alongside the previously published partner packages and runs unit and integration tests needs: - build - release-notes - test-pypi-publish - pre-release-checks environment: Release runs-on: ubuntu-latest permissions: contents: read strategy: matrix: # When adding a partner, also update the `skip-prior-published-package-checks` # input (the `workflow_dispatch` `options` list and the `workflow_call` # description) so the per-partner skip remains selectable. partner: [ anthropic, openai ] fail-fast: false # Continue testing other partners if one fails env: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ANTHROPIC_FILES_API_IMAGE_ID: ${{ secrets.ANTHROPIC_FILES_API_IMAGE_ID }} ANTHROPIC_FILES_API_PDF_ID: ${{ secrets.ANTHROPIC_FILES_API_PDF_ID }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} AZURE_OPENAI_API_VERSION: ${{ secrets.AZURE_OPENAI_API_VERSION }} AZURE_OPENAI_API_BASE: ${{ secrets.AZURE_OPENAI_API_BASE }} AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_CHAT_DEPLOYMENT_NAME }} AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LEGACY_CHAT_DEPLOYMENT_NAME }} AZURE_OPENAI_LLM_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_LLM_DEPLOYMENT_NAME }} AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_EMBEDDINGS_DEPLOYMENT_NAME }} LANGCHAIN_TESTS_USER_AGENT: ${{ secrets.LANGCHAIN_TESTS_USER_AGENT }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 # We implement this conditional as Github Actions does not have good support # for conditionally needing steps. https://github.com/actions/runner/issues/491 # TODO: this seems to be resolved upstream, so we can probably remove this workaround - name: Check if libs/core run: | if [ "${{ startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') }}" != "true" ]; then echo "Not in libs/core. Exiting successfully." exit 0 fi - name: Set up Python + uv if: startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') uses: "./.github/actions/uv_setup" with: python-version: ${{ env.PYTHON_VERSION }} enable-cache: "false" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 if: startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ - name: Skip prior published ${{ matrix.partner }} check if: >- startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') && (inputs.skip-prior-published-package-checks == matrix.partner || inputs.skip-prior-published-package-checks == 'all') run: | echo "Skipping prior published ${{ matrix.partner }} check as requested." - name: Test against ${{ matrix.partner }} if: >- startsWith(env.EFFECTIVE_WORKING_DIR, 'libs/core') && inputs.skip-prior-published-package-checks != matrix.partner && inputs.skip-prior-published-package-checks != 'all' env: PARTNER: ${{ matrix.partner }} PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }} run: | PACKAGE_NAME="langchain-$PARTNER" # Identify the latest non-yanked published package release, excluding pre-releases. # Fail closed (matching the `Check version` step) so a PyPI outage or a # missing release aborts with a clear message rather than an empty version. LATEST_PACKAGE_VERSION="$(PACKAGE_NAME="$PACKAGE_NAME" python - <<'PY' import json import os import re import sys import urllib.error import urllib.request package_name = os.environ["PACKAGE_NAME"] url = f"https://pypi.org/pypi/{package_name}/json" try: with urllib.request.urlopen(url, timeout=10) as response: data = json.load(response) except urllib.error.HTTPError as err: print( f"::error::PyPI returned HTTP {err.code} listing {package_name} " f"releases; cannot determine latest version, aborting.", file=sys.stderr, ) sys.exit(1) except urllib.error.URLError as err: print( f"::error::Could not reach PyPI to list {package_name} releases " f"({err.reason}); cannot determine latest version, aborting.", file=sys.stderr, ) sys.exit(1) versions: list[tuple[int, int, int, str]] = [] for version, files in data["releases"].items(): if not re.fullmatch(r"\d+\.\d+\.\d+", version): continue if not files or all(file.get("yanked", False) for file in files): continue versions.append((*map(int, version.split(".")), version)) if not versions: print(f"::error::No non-yanked final releases found for {package_name}", file=sys.stderr) sys.exit(1) print(max(versions)[3]) PY )" # Belt-and-suspenders: a bare assignment masks the heredoc's exit status # in some shells, so guard explicitly rather than relying on `set -e`. if [ -z "$LATEST_PACKAGE_VERSION" ]; then echo "::error::Could not determine latest published $PACKAGE_NAME version; aborting." exit 1 fi LATEST_PACKAGE_TAG="$PACKAGE_NAME==$LATEST_PACKAGE_VERSION" echo "Latest non-yanked package tag: $LATEST_PACKAGE_TAG" # Ensure the PyPI release maps to a source tag before running tests. git ls-remote --exit-code --tags origin "refs/tags/$LATEST_PACKAGE_TAG" # Shallow-fetch just that single tag git fetch --depth=1 origin tag "$LATEST_PACKAGE_TAG" # Checkout the latest package files rm -rf "$GITHUB_WORKSPACE/libs/partners/$PARTNER"/* rm -rf $GITHUB_WORKSPACE/libs/standard-tests/* cd $GITHUB_WORKSPACE/libs/ git checkout "$LATEST_PACKAGE_TAG" -- standard-tests/ git checkout "$LATEST_PACKAGE_TAG" -- "partners/$PARTNER/" cd "partners/$PARTNER" # Print as a sanity check echo "Version number from pyproject.toml: " cat pyproject.toml | grep "version = " # Run tests uv sync --group test --group test_integration uv pip install $PRERELEASE_FLAG ../../core/dist/*.whl make test make integration_tests # Test external packages that depend on langchain-core/langchain against the new release # Only runs for core and langchain_v1 releases to catch breaking changes before publish test-dependents: name: "๐Ÿ Test dependent: ${{ matrix.package.path }} (Python ${{ matrix.python-version }})" needs: - build - release-notes - test-pypi-publish - pre-release-checks runs-on: ubuntu-latest permissions: contents: read # Only run for core or langchain_v1 releases. # Job-level 'if' does not support env context, so EFFECTIVE_WORKING_DIR is # unavailable; must use inputs directly and match both forms: short dropdown # names (workflow_dispatch, e.g. 'core') and full 'libs/' paths # (workflow_call / working-directory-override). if: >- contains(fromJSON('["core","langchain_v1"]'), inputs.working-directory-override || inputs.working-directory) || startsWith(inputs.working-directory-override || inputs.working-directory, 'libs/core') || startsWith(inputs.working-directory-override || inputs.working-directory, 'libs/langchain_v1') strategy: fail-fast: false matrix: python-version: [ "3.11", "3.13" ] package: - name: deepagents repo: langchain-ai/deepagents path: libs/deepagents # No API keys needed for now - deepagents `make test` only runs unit tests steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: path: langchain - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: repository: ${{ matrix.package.repo }} path: ${{ matrix.package.name }} - name: Set up Python + uv uses: "./langchain/.github/actions/uv_setup" with: python-version: ${{ matrix.python-version }} enable-cache: "false" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: dist/ - name: Install ${{ matrix.package.name }} with local packages # External dependents don't have [tool.uv.sources] pointing to this repo, # so we install the package normally then override with the built wheel. env: PRERELEASE_FLAG: ${{ inputs.allow-prereleases && '--prerelease=allow' || '' }} run: | cd ${{ matrix.package.name }}/${{ matrix.package.path }} # Install the package with test dependencies uv sync --group test # Override with the built wheel from this release uv pip install $PRERELEASE_FLAG $GITHUB_WORKSPACE/dist/*.whl - name: Run ${{ matrix.package.name }} tests run: | cd ${{ matrix.package.name }}/${{ matrix.package.path }} make test publish: name: ๐Ÿš€ Publish to PyPI # Publishes the package to PyPI needs: - build - release-notes - test-pypi-publish - pre-release-checks - test-dependents - test-prior-published-packages-against-new-core # Run if all needed jobs succeeded or were skipped (test-dependents and # test-prior-published-packages-against-new-core only run for core/langchain_v1) if: ${{ !cancelled() && !failure() }} environment: Release runs-on: ubuntu-latest permissions: # This permission is used for trusted publishing: # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/ # # Trusted publishing has to also be configured on PyPI for each package: # https://docs.pypi.org/trusted-publishers/adding-a-publisher/ id-token: write defaults: run: working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python + uv uses: "./.github/actions/uv_setup" with: python-version: ${{ env.PYTHON_VERSION }} enable-cache: "false" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 with: packages-dir: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ verbose: true print-hash: true # Temp workaround since attestations are on by default as of gh-action-pypi-publish v1.11.0 attestations: false mark-release: name: ๐Ÿท๏ธ Tag GitHub release # Marks the GitHub release with the new version tag needs: - build - release-notes - test-pypi-publish - pre-release-checks - publish # Run if all needed jobs succeeded or were skipped if: ${{ !cancelled() && !failure() }} environment: Release runs-on: ubuntu-latest permissions: # This permission is needed by `ncipollo/release-action` to # create the GitHub release/tag contents: write defaults: run: working-directory: ${{ env.EFFECTIVE_WORKING_DIR }} steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Python + uv uses: "./.github/actions/uv_setup" with: python-version: ${{ env.PYTHON_VERSION }} enable-cache: "false" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: dist path: ${{ env.EFFECTIVE_WORKING_DIR }}/dist/ - name: Create Tag uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1 with: # JS actions ignore `defaults.run.working-directory`, so this glob is # resolved from the repo root. Point it at the package's `dist/` # (where `download-artifact` placed the wheels) instead of a bare # `dist/*`, which never matched and attached no assets to releases. artifacts: "${{ env.EFFECTIVE_WORKING_DIR }}/dist/*" token: ${{ secrets.GITHUB_TOKEN }} generateReleaseNotes: false tag: ${{needs.build.outputs.pkg-name}}==${{ needs.build.outputs.version }} body: ${{ needs.release-notes.outputs.release-body }} commit: ${{ github.sha }} makeLatest: ${{ needs.build.outputs.pkg-name == 'langchain-core'}}