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.