|
5 | 5 | paths-ignore: |
6 | 6 | - '**.md' |
7 | 7 | pull_request: |
8 | | - types: [opened, synchronize, reopened, ready_for_review] |
9 | 8 | paths-ignore: |
10 | 9 | - '**.md' |
11 | 10 |
|
12 | | -permissions: |
13 | | - contents: read |
14 | | - packages: read |
15 | | - id-token: write |
| 11 | +concurrency: |
| 12 | + # Cancels runs from previous pushes in a PR. |
| 13 | + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} |
| 14 | + cancel-in-progress: true |
| 15 | + |
| 16 | +permissions: {} |
16 | 17 |
|
17 | 18 | jobs: |
18 | | - ci-complete: |
19 | | - if: github.event_name != 'pull_request' || github.event.pull_request.draft == false |
20 | | - timeout-minutes: 60 |
21 | | - runs-on: [self-hosted, Linux] |
22 | | - container: |
23 | | - image: ghcr.io/dimensionalos/ros-dev:dev |
24 | | - env: |
25 | | - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
26 | | - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} |
27 | | - ALIBABA_API_KEY: ${{ secrets.ALIBABA_API_KEY }} |
| 19 | + lint: |
| 20 | + timeout-minutes: 10 |
| 21 | + runs-on: ubuntu-latest |
| 22 | + permissions: |
| 23 | + contents: read # For checkout |
| 24 | + env: |
| 25 | + UV_NO_SYNC: "1" # Disable installing default packages on `uv run` |
28 | 26 |
|
29 | 27 | steps: |
30 | | - - uses: actions/checkout@v5 |
| 28 | + - name: Checkout |
| 29 | + uses: actions/checkout@v6 |
| 30 | + - name: Install uv |
| 31 | + uses: astral-sh/setup-uv@v6 |
31 | 32 | with: |
32 | | - clean: false |
| 33 | + enable-cache: true |
| 34 | + - name: Install lint dependencies |
| 35 | + run: uv sync --only-group lint --frozen |
| 36 | + - name: Mypy |
| 37 | + run: uv run mypy |
| 38 | + - name: Run pre-commit |
| 39 | + uses: pre-commit/action@v3.0.1 |
33 | 40 |
|
34 | | - - name: Fix permissions |
35 | | - run: | |
36 | | - git config --global --add safe.directory '*' |
37 | | - git clean -ffdx -e .venv |
| 41 | + md-babel: |
| 42 | + timeout-minutes: 10 |
| 43 | + runs-on: ubuntu-latest |
| 44 | + permissions: |
| 45 | + contents: read # For checkout |
38 | 46 |
|
| 47 | + steps: |
| 48 | + - uses: actions/checkout@v6 |
| 49 | + with: |
| 50 | + lfs: true |
| 51 | + # Docs decode JPEG from SQLite via PyTurboJPEG; pyaudio needs portaudio. |
| 52 | + - name: Install system dependencies |
| 53 | + run: | |
| 54 | + sudo apt-get update |
| 55 | + sudo apt-get install -y libturbojpeg portaudio19-dev |
| 56 | + - name: Install uv |
| 57 | + uses: astral-sh/setup-uv@v6 |
| 58 | + with: |
| 59 | + enable-cache: true |
| 60 | + - uses: actions/setup-node@v4 |
| 61 | + with: |
| 62 | + node-version: 'lts/*' |
39 | 63 | - name: Install Python dependencies |
40 | | - run: uv sync --extra all --frozen |
| 64 | + run: uv sync --group tests --frozen |
| 65 | + - name: Execute documentation code blocks |
| 66 | + run: ./bin/run-doc-codeblocks --ci --no-cache |
41 | 67 |
|
42 | | - - name: Remove pydrake stubs |
43 | | - run: | |
44 | | - find .venv/lib/*/site-packages/pydrake -name '*.pyi' -delete 2>/dev/null || true |
| 68 | + tests: |
| 69 | + timeout-minutes: 20 |
| 70 | + strategy: |
| 71 | + matrix: |
| 72 | + pyver: ['3.10', '3.11', '3.12', '3.13', '3.14'] |
| 73 | + os: [ubuntu] |
| 74 | + experimental: [false] |
| 75 | + include: |
| 76 | + - os: ubuntu |
| 77 | + pyver: "3.14t" |
| 78 | + experimental: false |
| 79 | + - os: ubuntu |
| 80 | + pyver: "3.15" |
| 81 | + experimental: true |
| 82 | + fail-fast: true |
| 83 | + runs-on: ${{ matrix.os }}-latest |
| 84 | + continue-on-error: ${{ matrix.experimental }} |
| 85 | + permissions: |
| 86 | + contents: read # For checkout |
| 87 | + id-token: write # For codecov-action's OIDC upload |
45 | 88 |
|
46 | | - - name: Run tests |
| 89 | + steps: |
| 90 | + - name: Checkout |
| 91 | + uses: actions/checkout@v6 |
| 92 | + - name: Install uv |
| 93 | + uses: astral-sh/setup-uv@v6 |
| 94 | + with: |
| 95 | + enable-cache: true |
| 96 | + - name: Setup Python |
| 97 | + uses: actions/setup-python@v6 |
| 98 | + with: |
| 99 | + allow-prereleases: true |
| 100 | + python-version: ${{ matrix.pyver }} |
| 101 | + - name: Install dependency for pyaudio (Ubuntu) |
| 102 | + if: matrix.os == 'ubuntu' |
47 | 103 | run: | |
48 | | - /entrypoint.sh bash -c "source .venv/bin/activate && _DIMOS_COV=1 coverage run -m pytest --junitxml=junit.xml --durations=0 -m 'not (tool or mujoco)' && coverage combine && coverage xml" |
49 | | -
|
50 | | - - name: Run mypy |
| 104 | + sudo apt-get update |
| 105 | + sudo apt-get install -y portaudio19-dev |
| 106 | + - name: Install dependency for pyaudio (macOS) |
| 107 | + if: matrix.os == 'macos' |
| 108 | + run: brew install portaudio |
| 109 | + - name: Remove git LFS to avoid accidental large downloads |
| 110 | + run: sudo rm -f /usr/bin/git-lfs |
| 111 | + - name: Set PYTHON_GIL=0 for free-threading builds |
| 112 | + if: ${{ endsWith(matrix.pyver, 't') }} |
| 113 | + run: echo "PYTHON_GIL=0" >> $GITHUB_ENV |
| 114 | + - name: Run tests |
| 115 | + run: uv run pytest --numprocesses=3 --cov=dimos/ --junitxml=junit.xml -m 'not (tool or self_hosted or mujoco)' |
| 116 | + - name: Re-run the failing tests with maximum verbosity |
| 117 | + if: failure() |
| 118 | + env: |
| 119 | + COLOR: yes |
| 120 | + run: >- # `exit 1` makes sure that the job remains red with flaky runs |
| 121 | + uv run pytest --no-cov -vvvvv --lf -m 'not (tool or self_hosted or mujoco)' && exit 1 |
| 122 | + shell: bash |
| 123 | + - name: Turn coverage into xml |
| 124 | + run: uv run python -m coverage xml |
| 125 | + - name: Upload coverage |
| 126 | + uses: codecov/codecov-action@v6 |
| 127 | + with: |
| 128 | + disable_search: true |
| 129 | + fail_ci_if_error: true |
| 130 | + files: ./coverage.xml |
| 131 | + flags: OS-${{ matrix.os }},Py-${{ matrix.pyver }} |
| 132 | + use_oidc: true |
| 133 | + - name: Upload test results to Codecov |
51 | 134 | if: ${{ !cancelled() }} |
52 | | - run: | |
53 | | - /entrypoint.sh bash -c "source .venv/bin/activate && MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy dimos" |
| 135 | + uses: codecov/codecov-action@v6 |
| 136 | + with: |
| 137 | + report_type: test_results |
| 138 | + use_oidc: true |
| 139 | + |
| 140 | + self-hosted-tests: |
| 141 | + # Skip on draft PRs and on PRs from forks — the latter would expose the |
| 142 | + # self-hosted runner to untrusted code from external contributors. |
| 143 | + if: | |
| 144 | + github.event_name == 'push' || ( |
| 145 | + github.event.pull_request.draft == false && |
| 146 | + github.event.pull_request.head.repo.full_name == github.repository |
| 147 | + ) |
| 148 | + env: |
| 149 | + UV_NO_SYNC: "1" # Disable installing default packages on `uv run` |
| 150 | + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} |
| 151 | + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} |
| 152 | + ALIBABA_API_KEY: ${{ secrets.ALIBABA_API_KEY }} |
| 153 | + timeout-minutes: 30 |
| 154 | + strategy: |
| 155 | + fail-fast: false |
| 156 | + matrix: |
| 157 | + include: |
| 158 | + - runner: [self-hosted, Linux] |
| 159 | + # GitHub Actions only honours `container:` on Linux runners. |
| 160 | + container: |
| 161 | + image: ghcr.io/dimensionalos/ros-dev:dev |
| 162 | + markers: "self_hosted or skipif_no_ros" |
| 163 | + experimental: false |
| 164 | + - runner: [self-hosted, macos, arm64] |
| 165 | + container: null # run on host — `container:` is Linux-only |
| 166 | + markers: "self_hosted" |
| 167 | + experimental: true |
| 168 | + runs-on: ${{ matrix.runner }} |
| 169 | + continue-on-error: ${{ matrix.experimental }} |
| 170 | + permissions: |
| 171 | + contents: read # For checkout |
| 172 | + packages: read # For pulling the ros-dev container from ghcr.io |
| 173 | + id-token: write # For codecov-action's OIDC upload |
| 174 | + container: ${{ matrix.container }} |
54 | 175 |
|
| 176 | + steps: |
| 177 | + - uses: actions/checkout@v5 |
| 178 | + with: |
| 179 | + clean: false |
| 180 | + # If we ever allow external PRs on custom runner, persisting credentials |
| 181 | + # could be abused by attackers. |
| 182 | + persist-credentials: false |
| 183 | + - name: Fix permissions |
| 184 | + run: | |
| 185 | + git config --global --add safe.directory '*' |
| 186 | + git clean -ffdx |
| 187 | + - name: Install uv |
| 188 | + uses: astral-sh/setup-uv@v6 |
| 189 | + with: |
| 190 | + enable-cache: true |
| 191 | + - name: Install dependencies |
| 192 | + run: uv sync --group tests-self-hosted --frozen |
| 193 | + - name: Build C++ extensions in-place |
| 194 | + run: uv run python setup.py build_ext --inplace |
| 195 | + - name: Source ROS environment |
| 196 | + # The uv venv is sealed (include-system-site-packages = false), so |
| 197 | + # `import rclpy` / `ament_index_python` would fail. Sourcing the ROS |
| 198 | + # setup script and exporting PYTHONPATH/AMENT_PREFIX_PATH/etc into |
| 199 | + # GITHUB_ENV makes them importable from `uv run`. |
| 200 | + if: contains(toJSON(matrix.runner), 'Linux') |
| 201 | + shell: bash |
| 202 | + run: | |
| 203 | + source /opt/ros/humble/setup.bash |
| 204 | + { |
| 205 | + echo "PYTHONPATH=$PYTHONPATH" |
| 206 | + echo "AMENT_PREFIX_PATH=$AMENT_PREFIX_PATH" |
| 207 | + echo "CMAKE_PREFIX_PATH=$CMAKE_PREFIX_PATH" |
| 208 | + echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" |
| 209 | + echo "ROS_DISTRO=$ROS_DISTRO" |
| 210 | + echo "ROS_VERSION=$ROS_VERSION" |
| 211 | + echo "ROS_PYTHON_VERSION=$ROS_PYTHON_VERSION" |
| 212 | + } >> "$GITHUB_ENV" |
| 213 | + - name: Run tests |
| 214 | + run: uv run pytest --cov=dimos/ --junitxml=junit.xml -m '(${{ matrix.markers }}) and not (tool or mujoco)' |
| 215 | + - name: Re-run the failing tests with maximum verbosity |
| 216 | + if: failure() |
| 217 | + env: |
| 218 | + COLOR: yes |
| 219 | + run: >- # `exit 1` makes sure that the job remains red with flaky runs |
| 220 | + uv run pytest --no-cov -vvvvv --lf -m '(${{ matrix.markers }}) and not (tool or mujoco)' && exit 1 |
| 221 | + shell: bash |
| 222 | + - name: Turn coverage into xml |
| 223 | + run: uv run python -m coverage xml |
55 | 224 | - name: Upload coverage |
56 | 225 | uses: codecov/codecov-action@v6 |
57 | 226 | with: |
58 | 227 | disable_search: true |
59 | 228 | fail_ci_if_error: true |
60 | 229 | files: ./coverage.xml |
| 230 | + flags: SelfHosted-${{ matrix.runner[1] }} |
61 | 231 | use_oidc: true |
62 | 232 | - name: Upload test results to Codecov |
63 | 233 | if: ${{ !cancelled() }} |
64 | 234 | uses: codecov/codecov-action@v6 |
65 | 235 | with: |
66 | 236 | report_type: test_results |
67 | 237 | use_oidc: true |
68 | | - |
69 | 238 | - name: Check disk space |
70 | 239 | if: failure() |
71 | 240 | run: | |
72 | 241 | df -h |
| 242 | +
|
| 243 | + # Cross-job fail-fast: GitHub Actions only fail-fasts within a matrix, |
| 244 | + # not across sibling jobs. This watcher fires the moment `tests` fails |
| 245 | + # and cancels the whole workflow run. |
| 246 | + fail-fast: |
| 247 | + if: failure() |
| 248 | + needs: [tests] |
| 249 | + runs-on: ubuntu-latest |
| 250 | + permissions: |
| 251 | + actions: write # For `gh run cancel` |
| 252 | + steps: |
| 253 | + - name: Cancel workflow run |
| 254 | + env: |
| 255 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 256 | + run: gh run cancel ${{ github.run_id }} --repo ${{ github.repository }} |
| 257 | + |
| 258 | + ci-complete: # This is used for branch protection. |
| 259 | + if: always() |
| 260 | + |
| 261 | + needs: |
| 262 | + - lint |
| 263 | + - md-babel |
| 264 | + - tests |
| 265 | + - self-hosted-tests |
| 266 | + |
| 267 | + runs-on: ubuntu-latest |
| 268 | + permissions: |
| 269 | + id-token: write # For codecov-action's OIDC notify |
| 270 | + |
| 271 | + steps: |
| 272 | + - name: Decide whether the needed jobs succeeded or failed |
| 273 | + uses: re-actors/alls-green@release/v1 |
| 274 | + with: |
| 275 | + allowed-skips: self-hosted-tests |
| 276 | + jobs: ${{ toJSON(needs) }} |
| 277 | + - name: Trigger Codecov notifications |
| 278 | + uses: codecov/codecov-action@v6 |
| 279 | + with: |
| 280 | + run_command: send-notifications |
| 281 | + use_oidc: true |
| 282 | + fail_ci_if_error: true |
0 commit comments