Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 46 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,54 @@ permissions:
pull-requests: read

jobs:
# Skipped on a Release PR: it only bumps the version and rewrites the changelog. Not
# keyed on github.actor, which is the pusher: clicking Update branch reattributes the
# merge commit to whoever clicked, and the skip would stop firing on the normal release
# path. A human commit pushed onto a Release PR is therefore untested until release.yml
# runs the lane on the merge commit.
unit:
if: >-
${{ !(github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
uses: ./.github/workflows/run_tests.yml
with:
marker: 'not integration'
secrets: inherit

# The one required status check on main, and it carries no matrix on purpose: a matrix job
# skipped by `if:` publishes a single check run with the template unexpanded, so per-leg
# contexts could never be satisfied on a Release PR. `skipped` is accepted only when the
# condition above holds, repeated here because Actions cannot share an expression; if the
# two drift this fails, which is the safe direction.
tests-passed:
name: 🧪 Tests
needs: unit
if: ${{ !cancelled() }}
runs-on: ubuntu-latest
steps:
- name: Check the unit lane
env:
RESULT: ${{ needs.unit.result }}
RELEASE_PR: >-
${{ github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--') }}
run: |
case "$RESULT" in
success)
echo "unit lane passed"
;;
skipped)
if [ "$RELEASE_PR" = "true" ]; then
echo "release pr: unit lane skipped by design"
else
echo "::error::the unit lane was skipped on a PR that is not a Release PR"
exit 1
fi
;;
*)
echo "::error::unit lane reported $RESULT"
exit 1
;;
esac

# Cancel in-flight runs for the same branch/PR when new commits arrive
concurrency:
Expand Down
12 changes: 5 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -164,23 +164,21 @@ jobs:
needs: detect
if: needs.detect.outputs.ready == 'true'
uses: ./.github/workflows/run_tests.yml
with:
marker: 'not integration'
secrets: inherit

# Deliberately absent from `release`'s `needs`. A failure here would skip tagging while
# the Release PR is already merged carrying `autorelease: pending`, so every later push
# would stand down and releases would stay wedged until someone cleared it by hand.
test-integration:
name: Test (integration)
needs: detect
if: needs.detect.outputs.ready == 'true'
uses: ./.github/workflows/run_tests.yml
with:
marker: 'integration'
uses: ./.github/workflows/run_integration.yml
secrets: inherit

# Irreversible half.
release:
name: 🚀 Tag and release
needs: [detect, test-unit, test-integration]
needs: [detect, test-unit]
if: needs.detect.outputs.ready == 'true'
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
68 changes: 68 additions & 0 deletions .github/workflows/run_integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: _run-integration
on:
workflow_call:
secrets: { }

# A constant, not `github.workflow`, which inside a reusable workflow resolves to the
# caller's name: the daily run and the pre-tag run would land in separate lanes and hit the
# shared app at once. Queue rather than cancel, so the schedule cannot kill a release gate.
concurrency:
group: run-integration-${{ github.ref }}
cancel-in-progress: false

env:
UV_FROZEN: "1"

permissions:
contents: read

# The only place `-m integration` runs: daily and before a tag, never as a status check on a
# pull request. See DEVELOPMENT.md for why it gates nothing.
jobs:
integration-non-video:
name: 🧪 Non-video integration (${{ matrix.python-version }})
environment:
name: ci
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
python-version: ${{ matrix.python-version }}
- name: Run integration tests
env:
STREAM_API_KEY: ${{ vars.STREAM_CHAT_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_CHAT_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_CHAT_BASE_URL }}
run: make test MARKER="integration"

integration-video:
name: 🧪 Video integration (${{ matrix.python-version }})
environment:
name: ci
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Install dependencies
uses: ./.github/actions/python-uv-setup
with:
python-version: ${{ matrix.python-version }}
- name: Run integration tests
env:
STREAM_API_KEY: ${{ vars.STREAM_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }}
run: make test-video MARKER="integration"
64 changes: 10 additions & 54 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
name: _run-tests
on:
workflow_call:
inputs:
marker:
description: 'pytest -m expression (e.g., `not integration` or `integration`)'
required: true
type: string
secrets: { }
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.marker }}
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
Expand All @@ -18,24 +13,12 @@ env:
permissions:
contents: read

# Every job below skips on a Release PR. release-please only bumps the version and rewrites
# the changelog, and every source commit in one already passed this suite on the PR it came
# from. The guard sits on each job rather than on the calling job in ci.yml, because a job
# skipped by `if:` reports success and satisfies a required status check, while a reusable
# workflow that is never called produces no check at all. It tests the author as well as the
# branch name: on its own, the name would let any PR, a fork's included, call its branch
# release-please--x and skip every required check, which branch protection counts as met.
# github.actor is the third clause, and it is the pusher rather than the PR author, so a
# human commit pushed onto the Release PR to fix a conflict or a changelog entry is tested
# like any other commit instead of riding the skip into main untested.
# The unit lane runs `-m "not integration"`, so it declares no environment and receives no
# credentials. Keep it that way: a fork PR gets no secrets and still goes green, and a live
# test added without the marker fails here instead of passing on someone else's.
jobs:
ruff:
if: >-
${{ !(github.actor == 'github-actions[bot]'
&& github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
name: Ruff
name: 🧪 Ruff
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -46,12 +29,7 @@ jobs:
run: make lint

typecheck:
if: >-
${{ !(github.actor == 'github-actions[bot]'
&& github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
name: Type Check (ty)
name: 🧪 Type Check (ty)
runs-on: ubuntu-latest
steps:
- name: Checkout
Expand All @@ -65,14 +43,7 @@ jobs:
# Uses STREAM_CHAT_* credentials which do NOT have video enabled.
# Video paths and manual test paths are defined in the Makefile.
test-non-video:
if: >-
${{ !(github.actor == 'github-actions[bot]'
&& github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
name: Non-video tests (${{ matrix.python-version }})
environment:
name: ci
name: 🧪 Non-video tests (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -87,23 +58,12 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Run tests
env:
STREAM_API_KEY: ${{ vars.STREAM_CHAT_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_CHAT_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_CHAT_BASE_URL }}
run: make test MARKER="${{ inputs.marker }}"
run: make test

# ── Video tests (video-enabled credentials) ─────────────────────
# Uses STREAM_* credentials which have video enabled.
test-video:
if: >-
${{ !(github.actor == 'github-actions[bot]'
&& github.event.pull_request.user.login == 'github-actions[bot]'
&& github.event.pull_request.head.repo.full_name == github.repository
&& startsWith(github.head_ref, 'release-please--')) }}
name: Video tests (${{ matrix.python-version }})
environment:
name: ci
name: 🧪 Video tests (${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
Expand All @@ -118,8 +78,4 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- name: Run tests
env:
STREAM_API_KEY: ${{ vars.STREAM_API_KEY }}
STREAM_API_SECRET: ${{ secrets.STREAM_API_SECRET }}
STREAM_BASE_URL: ${{ vars.STREAM_BASE_URL }}
run: make test-video MARKER="${{ inputs.marker }}"
run: make test-video
42 changes: 42 additions & 0 deletions .github/workflows/scheduled_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Scheduled tests
# The integration lane's home. Advisory: see DEVELOPMENT.md.
on:
schedule:
- cron: "0 9 * * *"
Comment thread
mogita marked this conversation as resolved.
workflow_dispatch:

permissions:
contents: read

jobs:
integration:
Comment thread
mogita marked this conversation as resolved.
uses: ./.github/workflows/run_integration.yml
secrets: inherit

# A failure here lands on no PR, and GitHub emails it only to whoever last edited the cron.
report:
name: Report a red run
needs: integration
if: failure()
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Open or update the tracking issue
env:
GH_TOKEN: ${{ github.token }}
TITLE: Daily integration run is red
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
num="$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "in:title \"$TITLE\"" \
--json number,title --jq "map(select(.title == \"$TITLE\")) | .[0].number // empty")"
if [ -n "$num" ]; then
gh issue comment "$num" --repo "$GITHUB_REPOSITORY" --body "Still red: $RUN_URL"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body \
"The daily \`-m integration\` run failed: $RUN_URL

Integration is advisory, so nothing is blocked by this. It still has to be read: the suite
runs against a live Stream app shared by five SDK repos, so decide whether this is the app,
the backend, or this SDK, and close the issue once a daily run is green again."
fi
25 changes: 24 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ make test-all # both of the above
```

Non-video and video tests are split because they require different Stream credentials.
The `MARKER` variable defaults to `"not integration"`. Override it for integration tests:
The `MARKER` variable defaults to `"not integration"`, which is every test that does not
touch a live Stream app, so the default needs no credentials. Override it to run the ones
that do:

```
make test-integration # runs both groups with -m "integration"
Expand All @@ -36,6 +38,27 @@ make test-jaeger # requires local Jaeger (docker run ... jaegertracing/all
make test-prometheus # requires getstream[telemetry] deps
```

### What CI runs

| Trigger | What runs | Gates anything? |
| --- | --- | --- |
| Pull request | `run_tests.yml`: ruff, ty, and `-m "not integration"` on five Python versions | yes, `🧪 Tests` is the required check |
| Daily at 09:00 UTC | `run_integration.yml`: `-m integration`, both credential sets | no |
| Push to `main` with a release pending | both; only the unit lane gates the tag | unit yes, integration no |

`@pytest.mark.integration` means one thing: the test talks to a live Stream app. The unit
lane therefore runs with no credentials, no `environment:` and no `STREAM_*`. Keep it that
way. A fork PR gets no secrets and still goes green, and a live test added without the
marker fails in CI instead of quietly passing on someone else's credentials.

Integration gates nothing, anywhere. It runs against an app five SDK repos share, so
another repo's run or a backend regression can redden it with nothing wrong here, and a red
pre-tag run used to wedge every later release behind `autorelease: pending`. A red daily run
opens an issue titled "Daily integration run is red". Fix it, do not route around it.

A Release PR skips the lane and `🧪 Tests` passes in seconds on a `skipped` result. The
merge commit still runs it before the tag.

### Linting and type checking

```
Expand Down
3 changes: 3 additions & 0 deletions tests/rtc/coordinator/test_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from tests.conftest import skip_on_rate_limit


pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_simple_connection_debug():
"""Simple test to debug websocket server handler signature."""
Expand Down
3 changes: 3 additions & 0 deletions tests/rtc/coordinator/test_heartbeat.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
from getstream import Stream


pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_heartbeat_sent_periodically(client: Stream):
"""Test that heartbeat messages are sent at regular intervals."""
Expand Down
4 changes: 4 additions & 0 deletions tests/rtc/test_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@


# Shared function for process setup and error handling

pytestmark = pytest.mark.integration


def run_process_with_stream_client(
process_type: str,
call_id: str,
Expand Down
3 changes: 3 additions & 0 deletions tests/rtc/test_video_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from getstream.video.rtc.pb.stream.video.sfu.models.models_pb2 import TRACK_TYPE_VIDEO


pytestmark = pytest.mark.integration


@pytest.mark.asyncio
async def test_detect_video_properties():
"""Test that video properties are correctly detected from a video file."""
Expand Down
Loading
Loading