Merge branch 'main' into docs/response-cache-logical-keys #4431
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: Build pull request | |
| on: | |
| push: | |
| branches: | |
| - 'pull-request/**' | |
| - 'main' | |
| - 'release/**' | |
| tags: | |
| - '*' | |
| # This allows a subsequently queued workflow run to interrupt previous runs | |
| concurrency: | |
| group: '${{ github.workflow }} @ ${{ github.event_name }} @ ${{ github.head_ref || github.ref }}' | |
| cancel-in-progress: true | |
| jobs: | |
| pr-builder: | |
| needs: | |
| - prepare | |
| - ci_required | |
| if: >- | |
| ${{ | |
| always() | |
| && !cancelled() | |
| && needs.prepare.result == 'success' | |
| && needs.ci_required.result == 'success' | |
| }} | |
| permissions: | |
| contents: read | |
| uses: rapidsai/shared-workflows/.github/workflows/pr-builder.yaml@4866bb5437e913caf5bf775f57c91abd144ed391 # main | |
| with: | |
| needs: ${{ toJSON(needs) }} | |
| prepare: | |
| # Executes the get-pr-info action to determine if the PR has the skip-ci label, if the action fails we assume the | |
| # PR does not have the label | |
| name: Prepare | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: read | |
| steps: | |
| - name: Get PR Info | |
| id: get-pr-info | |
| uses: nv-gha-runners/get-pr-info@090577647b8ddc4e06e809e264f7881650ecdccf # main | |
| if: ${{ startsWith(github.ref_name, 'pull-request/') }} | |
| - name: Validate release tag format | |
| if: ${{ github.ref_type == 'tag' }} | |
| run: | | |
| set -e | |
| tag="${{ github.ref_name }}" | |
| if [[ "$tag" == v* ]]; then | |
| echo "Error: release tags must not start with 'v'; use raw SemVer such as 0.1.0 or 0.1.0-rc.1" >&2 | |
| exit 1 | |
| fi | |
| if [[ ! "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-((alpha|beta|rc)\.[0-9]+))?$ ]]; then | |
| echo "Error: unsupported release tag format '$tag'; use 0.1.0 or prereleases like 0.1.0-rc.1" >&2 | |
| exit 1 | |
| fi | |
| - name: Derive workflow policy | |
| id: policy | |
| env: | |
| DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF_NAME: ${{ github.ref_name }} | |
| REF_TYPE: ${{ github.ref_type }} | |
| run: | | |
| set -euo pipefail | |
| full_ci=false | |
| publish_packages=false | |
| if [[ "$REF_TYPE" == "tag" || "$REF_NAME" == "$DEFAULT_BRANCH" ]]; then | |
| full_ci=true | |
| fi | |
| if [[ "$REF_TYPE" == "tag" && ! "$REF_NAME" =~ -alpha\.[0-9]+$ ]]; then | |
| publish_packages=true | |
| fi | |
| { | |
| printf 'full_ci=%s\n' "$full_ci" | |
| printf 'publish_packages=%s\n' "$publish_packages" | |
| } >> "$GITHUB_OUTPUT" | |
| outputs: | |
| full_ci: ${{ steps.policy.outputs.full_ci }} | |
| is_pr: ${{ startsWith(github.ref_name, 'pull-request/') }} | |
| is_main_branch: ${{ github.ref_name == 'main' }} | |
| has_skip_ci_label: ${{ steps.get-pr-info.outcome == 'success' && contains(fromJSON(steps.get-pr-info.outputs.pr-info).labels.*.name, 'skip-ci') || false }} | |
| publish_packages: ${{ steps.policy.outputs.publish_packages }} | |
| pr_info: ${{ steps.get-pr-info.outcome == 'success' && steps.get-pr-info.outputs.pr-info || '' }} | |
| ci_changes: | |
| name: Changes | |
| needs: [prepare] | |
| uses: ./.github/workflows/ci_changes.yml | |
| if: ${{ ! fromJSON(needs.prepare.outputs.has_skip_ci_label) }} | |
| permissions: | |
| contents: read | |
| with: | |
| # Info about the PR. Empty for non PR branches. Useful for extracting PR number, title, etc. | |
| pr_info: ${{ needs.prepare.outputs.pr_info }} | |
| full_ci: ${{ needs.prepare.outputs.full_ci == 'true' }} | |
| ref_name: ${{ github.ref_name }} | |
| default_branch: ${{ github.event.repository.default_branch }} | |
| ci_check: | |
| name: Check | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_check.yml | |
| if: ${{ needs.ci_changes.result == 'success' }} | |
| permissions: | |
| contents: read | |
| with: | |
| full_ci: ${{ needs.prepare.outputs.full_ci == 'true' }} | |
| base: ${{ needs.ci_changes.outputs.base }} | |
| run_installer: ${{ needs.ci_changes.outputs.run_installer == 'true' }} | |
| run_python_integration_langchain: ${{ needs.ci_changes.outputs.run_python_integration_langchain == 'true' }} | |
| ci_license_diff: | |
| name: License Diff | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_license_diff.yml | |
| if: >- | |
| ${{ | |
| needs.prepare.outputs.is_pr == 'true' | |
| && needs.ci_changes.outputs.run_dependencies == 'true' | |
| }} | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| with: | |
| base: ${{ needs.ci_changes.outputs.base }} | |
| default_branch: ${{ github.event.repository.default_branch }} | |
| ci_rust: | |
| name: Rust | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_rust.yml | |
| if: ${{ needs.ci_changes.outputs.run_rust == 'true' }} | |
| permissions: | |
| contents: read | |
| secrets: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| with: | |
| run_package: ${{ needs.ci_changes.outputs.run_rust_package == 'true' }} | |
| ci_go: | |
| name: Go | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_go.yml | |
| if: ${{ needs.ci_changes.outputs.run_go == 'true' }} | |
| permissions: | |
| contents: read | |
| secrets: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| ci_node: | |
| name: Node.js | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_node.yml | |
| if: ${{ needs.ci_changes.outputs.run_node == 'true' }} | |
| permissions: | |
| contents: read | |
| secrets: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| with: | |
| ref_type: ${{ github.ref_type }} | |
| ref_name: ${{ github.ref_name }} | |
| run_package: ${{ needs.ci_changes.outputs.run_node_package == 'true' }} | |
| run_openclaw: ${{ needs.ci_changes.outputs.run_openclaw == 'true' }} | |
| ci_python: | |
| name: Python | |
| needs: [prepare, ci_changes] | |
| uses: ./.github/workflows/ci_python.yml | |
| if: ${{ needs.ci_changes.outputs.run_python == 'true' || needs.ci_changes.outputs.run_python_integration_langchain == 'true' }} | |
| permissions: | |
| contents: read | |
| secrets: | |
| CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} | |
| with: | |
| ref_type: ${{ github.ref_type }} | |
| ref_name: ${{ github.ref_name }} | |
| run_package: ${{ needs.ci_changes.outputs.run_python_package == 'true' }} | |
| run_integration_langchain: ${{ needs.ci_changes.outputs.run_python_integration_langchain == 'true' }} | |
| ci_required: | |
| name: CI Pipeline | |
| needs: | |
| - prepare | |
| - ci_changes | |
| - ci_check | |
| - ci_rust | |
| - ci_go | |
| - ci_node | |
| - ci_python | |
| if: ${{ always() && !cancelled() && needs.prepare.result == 'success' && ! fromJSON(needs.prepare.outputs.has_skip_ci_label) }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Verify required CI jobs | |
| env: | |
| CHANGES_RESULT: ${{ needs.ci_changes.result }} | |
| CHECK_RESULT: ${{ needs.ci_check.result }} | |
| RUST_RESULT: ${{ needs.ci_rust.result }} | |
| GO_RESULT: ${{ needs.ci_go.result }} | |
| NODE_RESULT: ${{ needs.ci_node.result }} | |
| PYTHON_RESULT: ${{ needs.ci_python.result }} | |
| publish_packages: ${{ needs.prepare.outputs.publish_packages }} | |
| run: | | |
| set -euo pipefail | |
| failed=false | |
| require_success() { | |
| local name="$1" | |
| local result="$2" | |
| if [[ "$result" != "success" ]]; then | |
| echo "Error: ${name} finished with result '${result}', expected success" >&2 | |
| failed=true | |
| fi | |
| } | |
| allow_success_or_skipped() { | |
| local name="$1" | |
| local result="$2" | |
| if [[ "$result" != "success" && "$result" != "skipped" ]]; then | |
| echo "Error: ${name} finished with result '${result}', expected success or skipped" >&2 | |
| failed=true | |
| fi | |
| } | |
| require_success "Changes" "$CHANGES_RESULT" | |
| require_success "Check" "$CHECK_RESULT" | |
| if [[ "$publish_packages" == "true" ]]; then | |
| require_success "Rust" "$RUST_RESULT" | |
| require_success "Node.js" "$NODE_RESULT" | |
| require_success "Python" "$PYTHON_RESULT" | |
| else | |
| allow_success_or_skipped "Rust" "$RUST_RESULT" | |
| allow_success_or_skipped "Node.js" "$NODE_RESULT" | |
| allow_success_or_skipped "Python" "$PYTHON_RESULT" | |
| fi | |
| allow_success_or_skipped "Go" "$GO_RESULT" | |
| if [[ "$failed" == "true" ]]; then | |
| exit 1 | |
| fi | |
| release-distribution-artifacts: | |
| name: Release Distribution Artifacts | |
| needs: [prepare, ci_required] | |
| if: ${{ needs.prepare.outputs.publish_packages == 'true' && needs.ci_required.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Download CLI distribution artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: cli-* | |
| merge-multiple: true | |
| path: release-assets/ | |
| - name: Download Node distribution artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: node-npm-package-* | |
| merge-multiple: true | |
| path: release-assets/ | |
| - name: Download Python API wheel artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: wheel-* | |
| merge-multiple: true | |
| path: release-assets/ | |
| - name: Generate distribution checksums | |
| run: | | |
| set -euo pipefail | |
| ( | |
| cd release-assets | |
| expect_count() { | |
| local expected="$1" | |
| local label="$2" | |
| shift 2 | |
| local actual="$#" | |
| if [ "$actual" -ne "$expected" ]; then | |
| echo "Error: expected ${expected} ${label} artifacts, found ${actual}" >&2 | |
| exit 1 | |
| fi | |
| } | |
| cli_binaries=(nemo-relay-cli-*) | |
| cli_wheels=(nemo_relay_cli_bin-*.whl) | |
| python_api_wheels=(nemo_relay-*.whl) | |
| node_npm=(nemo-relay-node-npm-*.tgz) | |
| expect_count 7 "CLI binary" "${cli_binaries[@]}" | |
| expect_count 7 "CLI wheel" "${cli_wheels[@]}" | |
| expect_count 7 "Python API wheel" "${python_api_wheels[@]}" | |
| expect_count 8 "Node npm" "${node_npm[@]}" | |
| mapfile -t distribution_assets < <( | |
| find . -maxdepth 1 -type f \ | |
| ! -name 'SHA256SUMS' \ | |
| ! -name '*.sha256' \ | |
| -printf '%f\n' | | |
| sort | |
| ) | |
| if [ "${#distribution_assets[@]}" -eq 0 ]; then | |
| echo "Error: no distribution artifacts were collected" >&2 | |
| exit 1 | |
| fi | |
| sha256sum "${distribution_assets[@]}" > SHA256SUMS | |
| sha256sum --check SHA256SUMS | |
| for filename in nemo-relay-cli-*; do | |
| if [[ "$filename" == *.tgz || "$filename" == *.whl || "$filename" == *.sha256 ]]; then | |
| continue | |
| fi | |
| sha256sum "$filename" > "${filename}.sha256" | |
| done | |
| printf 'Distribution release assets:\n' | |
| printf ' release-assets/%s\n' * | |
| ) | |
| - name: Upload CLI assets and checksums to draft GitHub Release | |
| uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 | |
| with: | |
| draft: true | |
| prerelease: ${{ contains(github.ref_name, '-alpha.') || contains(github.ref_name, '-beta.') || contains(github.ref_name, '-rc.') }} | |
| overwrite_files: true | |
| fail_on_unmatched_files: true | |
| files: release-assets/* | |
| publish-rust: | |
| name: Publish (crates.io) | |
| needs: [prepare, ci_required] | |
| if: ${{ needs.prepare.outputs.publish_packages == 'true' && needs.ci_required.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| environment: crates.io | |
| env: | |
| UV_PYTHON_DOWNLOADS: never | |
| UV_PYTHON_PREFERENCE: only-system | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Load CI tool versions | |
| id: ci-config | |
| uses: ./.github/actions/load-ci-tool-versions | |
| - uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4 | |
| with: | |
| cache: false | |
| toolchain: ${{ steps.ci-config.outputs.rust_version }} | |
| - uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8 | |
| with: | |
| version: ${{ steps.ci-config.outputs.uv_version }} | |
| enable-cache: true | |
| cache-dependency-glob: ${{ github.workspace }}/uv.lock | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ steps.ci-config.outputs.default_python_version }} | |
| - uses: taiki-e/install-action@c070f87102a1c75b3183910f391c1cb887fe13c8 # v2.77.6 | |
| with: | |
| tool: just@${{ steps.ci-config.outputs.just_version }} | |
| - name: Set project release version | |
| working-directory: ${{ github.workspace }} | |
| run: just set-version "${{ github.ref_name }}" | |
| - name: Authenticate to crates.io with trusted publishing | |
| id: crates-io-auth | |
| uses: rust-lang/crates-io-auth-action@bbd81622f20ce9e2dd9622e3218b975523e45bbe # v1.0.4 | |
| - name: Publish to crates.io with trusted publishing | |
| working-directory: ${{ github.workspace }} | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ steps.crates-io-auth.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| version="${{ github.ref_name }}" | |
| packages=( | |
| nemo-relay-types | |
| nemo-relay-plugin | |
| nemo-relay-worker-proto | |
| nemo-relay-worker | |
| nemo-relay | |
| nemo-relay-adaptive | |
| nemo-relay-pii-redaction | |
| nemo-relay-ffi | |
| nemo-relay-cli | |
| ) | |
| for package in "${packages[@]}"; do | |
| if cargo info "${package}@${version}" --registry crates-io >/dev/null 2>&1; then | |
| echo "${package} ${version} already exists on crates.io; skipping" | |
| continue | |
| fi | |
| cargo publish --package "$package" --no-verify --allow-dirty | |
| done | |
| publish-python: | |
| name: Publish (PyPI) | |
| needs: [prepare, ci_required] | |
| if: ${{ needs.prepare.outputs.publish_packages == 'true' && needs.ci_required.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| environment: pypi | |
| steps: | |
| - name: Download wheel artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: wheel-* | |
| merge-multiple: true | |
| path: dist/ | |
| - name: Download Python plugin SDK wheel artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: plugin-wheel | |
| path: dist/ | |
| - name: Download CLI wheel artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: cli-python-wheel-* | |
| merge-multiple: true | |
| path: dist/ | |
| - name: Download Python source distribution artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: python-sdist | |
| path: dist/ | |
| - name: Verify Python API wheel artifacts | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| wheels=(dist/nemo_relay-*.whl) | |
| if [ "${#wheels[@]}" -ne 7 ]; then | |
| echo "Error: expected 7 Python API wheel artifacts, found ${#wheels[@]}" >&2 | |
| exit 1 | |
| fi | |
| - name: Verify Python source distribution artifact | |
| run: | | |
| set -euo pipefail | |
| shopt -s nullglob | |
| sdists=(dist/nemo_relay-*.tar.gz) | |
| if [ "${#sdists[@]}" -ne 1 ]; then | |
| echo "Error: expected one Python API source distribution artifact, found ${#sdists[@]}" >&2 | |
| exit 1 | |
| fi | |
| - name: Publish to PyPI with trusted publishing | |
| uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 | |
| with: | |
| skip-existing: true | |
| publish-npm: | |
| name: Publish (npm) | |
| needs: [prepare, ci_required] | |
| if: ${{ needs.prepare.outputs.publish_packages == 'true' && needs.ci_required.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| environment: npm | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 | |
| - name: Load CI tool versions | |
| id: ci-config | |
| uses: ./.github/actions/load-ci-tool-versions | |
| - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 | |
| with: | |
| node-version: ${{ steps.ci-config.outputs.node_version }} | |
| registry-url: "https://registry.npmjs.org" | |
| - name: Download split Node package artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: node-npm-package-* | |
| merge-multiple: true | |
| path: node-packages/ | |
| - name: Download OpenClaw plugin artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: openclaw-npm | |
| path: openclaw-package/ | |
| - name: Select npm dist-tag | |
| run: | | |
| set -e | |
| npm_tag="latest" | |
| if [[ "${{ github.ref_name }}" =~ -(alpha|beta|rc)\.[0-9]+$ ]]; then | |
| npm_tag="next" | |
| fi | |
| printf 'NEMO_RELAY_NPM_DIST_TAG=%s\n' "$npm_tag" >> "$GITHUB_ENV" | |
| - name: Publish Node.js package to npm | |
| run: | | |
| set -euo pipefail | |
| version="${{ github.ref_name }}" | |
| for pkg in node-packages/nemo-relay-node-npm-{linux-x64,linux-arm64,linux-x64-musl,linux-arm64-musl,darwin-arm64,win32-x64,win32-arm64}-${version}.tgz; do | |
| name="$(tar xOf "$pkg" package/package.json | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).name')" | |
| if npm view "${name}@${version}" version --registry https://registry.npmjs.org >/dev/null 2>&1; then | |
| echo "${name} ${version} already exists on npm; skipping" | |
| continue | |
| fi | |
| npm publish "./$pkg" --access public --tag "${NEMO_RELAY_NPM_DIST_TAG}" | |
| done | |
| metapackage="node-packages/nemo-relay-node-npm-${version}.tgz" | |
| if npm view "nemo-relay-node@${version}" version --registry https://registry.npmjs.org >/dev/null 2>&1; then | |
| echo "nemo-relay-node ${version} already exists on npm; skipping" | |
| else | |
| npm publish "./$metapackage" --access public --tag "${NEMO_RELAY_NPM_DIST_TAG}" | |
| fi | |
| - name: Publish OpenClaw plugin package to npm | |
| run: | | |
| set -euo pipefail | |
| version="${{ github.ref_name }}" | |
| if npm view "nemo-relay-openclaw@${version}" version --registry https://registry.npmjs.org >/dev/null 2>&1; then | |
| echo "nemo-relay-openclaw ${version} already exists on npm; skipping" | |
| exit 0 | |
| fi | |
| for pkg in ./openclaw-package/*.tgz; do | |
| echo "Publishing ${pkg}..." | |
| npm publish "${pkg}" --access public --tag "${NEMO_RELAY_NPM_DIST_TAG}" | |
| done |