refactor: derive lifecycle labels with strum #1421
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_python_integration_langchain: ${{ needs.ci_changes.outputs.run_python_integration_langchain == 'true' }} | |
| ci_license_diff: | |
| name: License Diff | |
| needs: [prepare, ci_changes, ci_check] | |
| uses: ./.github/workflows/ci_license_diff.yml | |
| if: >- | |
| ${{ | |
| needs.prepare.outputs.is_pr == 'true' | |
| && needs.ci_check.result == 'success' | |
| && 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, ci_check] | |
| uses: ./.github/workflows/ci_rust.yml | |
| if: ${{ needs.ci_check.result == 'success' && 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, ci_check] | |
| uses: ./.github/workflows/ci_go.yml | |
| if: ${{ needs.ci_check.result == 'success' && 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, ci_check] | |
| uses: ./.github/workflows/ci_node.yml | |
| if: ${{ needs.ci_check.result == 'success' && 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, ci_check] | |
| uses: ./.github/workflows/ci_python.yml | |
| if: ${{ needs.ci_check.result == 'success' && ( 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 == 'true' || needs.ci_changes.outputs.run_python_integration_langchain == 'true' ) }} | |
| run_integration_langchain: ${{ needs.ci_changes.outputs.run_python_integration_langchain == 'true' }} | |
| ci_wasm: | |
| name: WebAssembly | |
| needs: [prepare, ci_changes, ci_check] | |
| uses: ./.github/workflows/ci_wasm.yml | |
| if: ${{ needs.ci_check.result == 'success' && needs.ci_changes.outputs.run_wasm == '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_wasm_package == 'true' }} | |
| ci_required: | |
| name: CI Pipeline | |
| needs: | |
| - prepare | |
| - ci_changes | |
| - ci_check | |
| - ci_rust | |
| - ci_go | |
| - ci_node | |
| - ci_python | |
| - ci_wasm | |
| 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 }} | |
| WEBASSEMBLY_RESULT: ${{ needs.ci_wasm.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" | |
| require_success "WebAssembly" "$WEBASSEMBLY_RESULT" | |
| else | |
| allow_success_or_skipped "Rust" "$RUST_RESULT" | |
| allow_success_or_skipped "Node.js" "$NODE_RESULT" | |
| allow_success_or_skipped "Python" "$PYTHON_RESULT" | |
| allow_success_or_skipped "WebAssembly" "$WEBASSEMBLY_RESULT" | |
| fi | |
| allow_success_or_skipped "Go" "$GO_RESULT" | |
| if [[ "$failed" == "true" ]]; then | |
| exit 1 | |
| fi | |
| release-cli-artifacts: | |
| name: Release CLI 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 binary artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: cli-* | |
| merge-multiple: true | |
| path: release-assets/ | |
| - name: Generate CLI release checksums | |
| run: | | |
| set -euo pipefail | |
| ( | |
| cd release-assets | |
| cli_assets=(nemo-relay-cli-*) | |
| sha256sum "${cli_assets[@]}" > SHA256SUMS | |
| sha256sum --check SHA256SUMS | |
| while read -r digest filename; do | |
| printf '%s %s\n' "$digest" "$filename" > "${filename}.sha256" | |
| done < SHA256SUMS | |
| printf 'CLI 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, '-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 | |
| 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: Install managed Python | |
| run: | | |
| set -e | |
| UV_PYTHON_DOWNLOADS=manual uv python install --managed-python ${{ 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 | |
| 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: 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 consolidated Node package artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: npm-consolidated | |
| path: . | |
| - name: Download WebAssembly artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: wasm-bundler | |
| path: wasm-package/ | |
| - 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 }}" | |
| 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" | |
| exit 0 | |
| fi | |
| unzip -q ./consolidated.zip -d combined | |
| echo "Platform binaries included:" | |
| ls -la combined/package/*.node | |
| npm publish ./combined/package --access public --tag "${NEMO_RELAY_NPM_DIST_TAG}" | |
| - 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 | |
| - name: Publish WebAssembly package to npm | |
| run: | | |
| set -euo pipefail | |
| version="${{ github.ref_name }}" | |
| if npm view "nemo-relay-wasm@${version}" version --registry https://registry.npmjs.org >/dev/null 2>&1; then | |
| echo "nemo-relay-wasm ${version} already exists on npm; skipping" | |
| exit 0 | |
| fi | |
| for pkg in ./wasm-package/*.tgz; do | |
| echo "Publishing ${pkg}..." | |
| npm publish "${pkg}" --access public --tag "${NEMO_RELAY_NPM_DIST_TAG}" | |
| done |