name: Per merge CI

on: # rebuild any PRs and main branch changes
  pull_request:
  push:
    branches:
      - main

permissions:
  contents: read
  actions: write

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  build-and-lint:
    name: "Format, docs, and lint"
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 15 || 10 }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc
      - run: cargo fmt --all --check
      - run: cargo doc --workspace --all-features --no-deps
        env:
          RUSTDOCFLAGS: "-D warnings"
      - run: cargo lint
      - run: cargo test-lint
      - run: cargo check
      - run: cargo check --features experimental
      - name: Check vendored proto compilation
        run: cargo check --features temporalio-client/vendored-protox
        env:
          PROTOC: /does/not/exist
          CARGO_TARGET_DIR: /tmp/vendored-protox-target
      - run: git diff --exit-code

  test:
    name: Unit Tests
    timeout-minutes: ${{ matrix.timeoutMinutes || 25 }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, ubuntu-arm, macos-intel, macos-arm, windows-latest]
        include:
          - os: windows-latest
            timeoutMinutes: 30
          - os: ubuntu-latest
          - os: ubuntu-arm
            runsOn: ubuntu-24.04-arm64-2-core
            timeoutMinutes: 30
          - os: macos-arm
            runsOn: macos-14
          - os: macos-intel
            runsOn: macos-26-intel
            timeoutMinutes: 40
    runs-on: ${{ matrix.runsOn || matrix.os }}
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc
      # Workaround for https://github.com/actions/runner-images/issues/12432
      # from https://github.com/rust-lang/rust/issues/141626#issuecomment-2919419236
      # Visual Studio bug tracker https://developercommunity.visualstudio.com/t/Regression-from-1943:-linkexe-crashes/10912960
      - name: Setup RUSTFLAGS (Windows)
        if: runner.os == 'Windows'
        uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a # v9
        with:
          script: |
            core.exportVariable('RUSTFLAGS', '-Csymbol-mangling-version=v0');
      - run: cargo test --features experimental -- --include-ignored --nocapture
      - name: Find test executable for cgroup tests
        id: find-cgroup-test
        if: runner.os == 'Linux' && runner.arch == 'X64'
        run: |
          test_executable=$(cargo build --tests --features experimental --message-format json | jq -r 'select(.profile?.test == true and .target?.name == "temporalio_sdk_core" and .executable) | .executable')
          cp $test_executable ./core-tests
      - name: Upload cgroup test executable
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        if: runner.os == 'Linux' && runner.arch == 'X64'
        with:
          name: cgroup-tests-linux-x86_64
          path: ./core-tests
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: coverage-report
          path: tarpaulin-report.html
      - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: state-machine-coverage
          path: machine_coverage/

  msrv:
    name: "Verify MSRV (${{ matrix.group }})"
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 20 || 15 }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - group: rust-1.88
            crates: client common common-wasm macros protos sdk-core workflow
          - group: rust-1.92
            crates: sdk
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc cargo:cargo-msrv
          rust-cache-key: msrv-${{ matrix.group }}
      - name: Verify crate MSRVs
        run: |
          read -ra crates <<< "${{ matrix.crates }}"
          for crate in "${crates[@]}"; do
            cargo msrv verify --all-features --path "crates/$crate"
          done

  cgroup-tests:
    name: cgroup tests
    needs: test
    runs-on: ubuntu-latest
    container:
      image: ubuntu
      options: --cpus=1 --memory=512m
    env:
      CGROUP_TESTS_ENABLED: true
    steps:
      - name: Download cgroup test executable
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: cgroup-tests-linux-x86_64
      - name: Run tests
        run: |
          chmod +x ./core-tests
          ./core-tests cgroup_ --nocapture

  integ-tests:
    name: Integ tests
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 30 || 25 }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, ubuntu-arm, macos-intel, macos-arm, windows-latest]
        include:
          - os: windows-latest
          - os: ubuntu-latest
          - os: ubuntu-arm
            runsOn: ubuntu-24.04-arm64-2-core
          - os: macos-arm
            runsOn: macos-14
          - os: macos-intel
            runsOn: macos-26-intel
            timeoutMinutes: 40
    runs-on: ${{ matrix.runsOn || matrix.os }}
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc
      # Workaround for https://github.com/actions/runner-images/issues/12432
      # from https://github.com/rust-lang/rust/issues/141626#issuecomment-2919419236
      # Visual Studio bug tracker https://developercommunity.visualstudio.com/t/Regression-from-1943:-linkexe-crashes/10912960
      - name: Setup RUSTFLAGS (Windows)
        if: runner.os == 'Windows'
        uses: actions/github-script@450193c5abd4cdb17ba9f3ffcfe8f635c4bb6c2a # v9
        with:
          script: |
            core.exportVariable('RUSTFLAGS', '-Csymbol-mangling-version=v0');
      - run: cargo integ-test

  wasm-workflow-tests:
    name: WASM workflow tests
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 30 || 25 }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc cargo:cargo-component
          rust-cache-key: wasm-workflow-tests
      - name: Install WASM Rust targets
        # Install after rust-toolchain so targets are added to the correct toolchain.
        run: rustup target add wasm32-unknown-unknown wasm32-wasip1
      - run: cargo integ-test -t wasm_workflow_tests -- --test-threads 1

  cloud-tests:
    if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == '' || github.event.pull_request.head.repo.full_name == 'temporalio/sdk-rust')
    name: Cloud tests
    env:
      TEMPORAL_CLIENT_CLOUD_API_VERSION: v0.19.1
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 25 || 20 }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc
      - env:
          TEMPORAL_CLOUD_ADDRESS: https://${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233
          TEMPORAL_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}
          TEMPORAL_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }}
          TEMPORAL_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }}
        run: cargo test --features=test-utilities,temporalio-sdk/experimental --test cloud_tests
      - name: Generate Cloud test certificates
        run: |
          umask 077
          cert_dir="$RUNNER_TEMP/cloud-test-certs"
          mkdir "$cert_dir"
          openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
            -keyout "$cert_dir/ca.key" -out "$cert_dir/ca.pem" \
            -subj '/CN=Temporal Rust SDK Cloud CI CA'
          openssl req -newkey rsa:2048 -nodes \
            -keyout "$cert_dir/client.key" -out "$cert_dir/client.csr" \
            -subj '/CN=Temporal Rust SDK Cloud CI'
          openssl x509 -req -days 1 -in "$cert_dir/client.csr" \
            -CA "$cert_dir/ca.pem" -CAkey "$cert_dir/ca.key" -CAcreateserial \
            -out "$cert_dir/client.pem" -extfile <(printf 'extendedKeyUsage=clientAuth')
          {
            echo "TEMPORAL_CLOUD_CLIENT_CA_PATH=$cert_dir/ca.pem"
            echo "TEMPORAL_TLS_CLIENT_CERT_PATH=$cert_dir/client.pem"
            echo "TEMPORAL_TLS_CLIENT_KEY_PATH=$cert_dir/client.key"
          } >> "$GITHUB_ENV"
      - name: Create Cloud namespace
        id: create-cloud-namespace
        env:
          TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
        run: cargo integ-test cloud-namespace create
      - name: Run Cloud-eligible integration tests
        timeout-minutes: 20
        env:
          TEMPORAL_ADDRESS: ${{ steps.create-cloud-namespace.outputs.namespace }}.tmprl.cloud:7233
          TEMPORAL_NAMESPACE: ${{ steps.create-cloud-namespace.outputs.namespace }}
        run: |
          set -o pipefail
          cargo integ-test -s envconfig --cloud 2>&1 | tee cloud-integration.log
      - name: Delete Cloud namespace
        id: delete-cloud-namespace
        if: ${{ always() && steps.create-cloud-namespace.outputs.namespace != '' }}
        continue-on-error: true
        env:
          TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
        run: cargo integ-test cloud-namespace delete "${{ steps.create-cloud-namespace.outputs.namespace }}"
      - name: Report Cloud namespace cleanup failure
        if: ${{ always() && steps.delete-cloud-namespace.outcome == 'failure' }}
        run: echo "::warning title=Cloud namespace cleanup failed::Failed to delete Cloud namespace ${{ steps.create-cloud-namespace.outputs.namespace }}"
      - name: Upload Cloud test output
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
        with:
          name: cloud-integration-tests-output
          path: cloud-integration.log
          if-no-files-found: ignore
          retention-days: 14

  docker-integ-tests:
    name: Docker integ tests
    env:
      TEMPORAL_CLOUD_ADDRESS: https://${{ vars.TEMPORAL_CLIENT_NAMESPACE }}.tmprl.cloud:7233
      TEMPORAL_CLOUD_NAMESPACE: ${{ vars.TEMPORAL_CLIENT_NAMESPACE }}
      TEMPORAL_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }}
      TEMPORAL_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }}
      DOCKER_PROMETHEUS_RUNNING: true
    timeout-minutes: ${{ github.ref == 'refs/heads/main' && 25 || 20 }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc
      - name: Start container for otel-collector and prometheus
        uses: hoverkraft-tech/compose-action@b659c744ee9bed25aea2307c017ad849bd104433 # v3.2.0
        with:
          compose-file: ./etc/docker/docker-compose-ci.yaml
      - run: cargo integ-test docker_
        env:
          TMPDIR: ${{ runner.temp }}
      - name: Verify worker environment detection inside Docker
        run: |
          integ_test_binary="$(
            find "$GITHUB_WORKSPACE/target/debug/deps" -maxdepth 1 -type f \
              -name 'integ_tests-*' -perm -111 \
              -printf '%T@ %p\n' | sort -nr | head -n 1 | cut -d' ' -f2-
          )"
          integ_runner_binary="$(
            find "$GITHUB_WORKSPACE/target/debug/deps" -maxdepth 1 -type f \
              -name 'integ_runner-*' -perm -111 \
              -printf '%T@ %p\n' | sort -nr | head -n 1 | cut -d' ' -f2-
          )"
          test -x "$integ_test_binary"
          test -x "$integ_runner_binary"
          # The CLI is cached under a version-derived name, so the container only finds the one
          # the previous step downloaded if it asks for the same version. Cargo's `[env]` does not
          # reach a directly invoked binary, hence passing the pin, if any, explicitly.
          cli_version_override="$(
            sed -n 's/^CLI_VERSION_OVERRIDE = "\(.*\)"$/\1/p' .cargo/config.toml
          )"
          cli_version_env=()
          if [ -n "$cli_version_override" ]; then
            cli_version_env=(--env "CLI_VERSION_OVERRIDE=$cli_version_override")
          fi
          docker run --rm \
            --env TMPDIR="$RUNNER_TEMP" \
            --env TEMPORAL_TEST_EXPECT_DOCKER=true \
            "${cli_version_env[@]}" \
            --volume "$RUNNER_TEMP:$RUNNER_TEMP" \
            --volume "$GITHUB_WORKSPACE:$GITHUB_WORKSPACE" \
            --volume /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro \
            --workdir "$GITHUB_WORKSPACE" \
            ubuntu:24.04 \
            "$integ_runner_binary" --test-executable "$integ_test_binary" \
              worker_heartbeat_multiple_workers

  examples:
    name: "Build and run examples"
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc github:temporalio/cli
      - name: Build examples
        run: cargo build --examples -p temporalio-sdk --features examples
      - name: Start Temporal dev server
        run: |
          temporal server start-dev --headless &
          sleep 5
          temporal operator search-attribute create --name CustomKeywordField --type Keyword
          temporal operator search-attribute create --name CustomIntField --type Int
      - name: Run examples
        run: bash ./etc/run-sdk-examples.sh

  c-bridge-static-link-test:
    name: "C bridge static link test"
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
      - uses: ./.github/actions/setup
        with:
          mise-install-args: protoc

      - name: Build crate as static library
        run: cargo rustc --package temporalio-sdk-core-c-bridge --features xz2-static -- --crate-type=staticlib

      - name: Build C test program
        run: gcc -I./crates/sdk-core-c-bridge/include crates/sdk-core/tests/c_bridge_smoke_test.c target/debug/deps/libtemporalio_sdk_core_c_bridge.a -lpthread -ldl -lm -o test

      - name: Run C test program
        run: ./test
