Skip to content
Open
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
64 changes: 32 additions & 32 deletions .github/workflows/run-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,44 @@ on:
types: [validate-examples]
merge_group:
jobs:
prepare:
runs-on: ubuntu-latest
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ github.ref }}
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- name: Parse repository_dispatch payload
if: github.event_name == 'repository_dispatch'
run: |
if [ ${{ github.event.client_payload.command }} = "ok-to-test" ]; then
echo "CHECKOUT_REPO=${{ github.event.client_payload.pull_head_repo }}" >> $GITHUB_ENV
echo "CHECKOUT_REF=${{ github.event.client_payload.pull_head_ref }}" >> $GITHUB_ENV
fi

- name: Check out code
uses: actions/checkout@v6
with:
repository: ${{ env.CHECKOUT_REPO }}
ref: ${{ env.CHECKOUT_REF }}

- name: Compute compatibility test matrix
id: set-matrix
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python tools/compute_compat_matrix.py

validate:
needs: prepare
runs-on: ubuntu-latest
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_REF: ${{ github.ref }}

strategy:
fail-fast: false
matrix:
python_ver: ["3.10", "3.11", "3.12", "3.13", "3.14"]
matrix: ${{ fromJson(needs.prepare.outputs.matrix) }}
steps:
- name: Parse repository_dispatch payload
if: github.event_name == 'repository_dispatch'
Expand All @@ -50,35 +78,6 @@ jobs:
with:
repository: ${{ env.CHECKOUT_REPO }}
ref: ${{ env.CHECKOUT_REF }}
- name: Determine latest Dapr Runtime version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MIN_RUNTIME_VERSION="1.18.0"
RUNTIME_VERSION=$(curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/dapr/dapr/releases?per_page=10" | \
jq -r 'map(select(.prerelease == false)) | sort_by(.created_at) | reverse | .[0].tag_name | ltrimstr("v")')
if [ -z "$RUNTIME_VERSION" ] || [ "$RUNTIME_VERSION" = "null" ]; then
echo "Failed to resolve Dapr Runtime version" && exit 1
fi
if [ "$(printf '%s\n' "$MIN_RUNTIME_VERSION" "$RUNTIME_VERSION" | sort -V | head -n1)" != "$MIN_RUNTIME_VERSION" ]; then
echo "Resolved runtime version $RUNTIME_VERSION is below minimum $MIN_RUNTIME_VERSION, using minimum instead"
RUNTIME_VERSION="$MIN_RUNTIME_VERSION"
fi
echo "DAPR_RUNTIME_VER=$RUNTIME_VERSION" >> $GITHUB_ENV
echo "Found $RUNTIME_VERSION"
- name: Determine latest Dapr CLI version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
CLI_VERSION=$(curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/dapr/cli/releases?per_page=10" | \
jq -r 'map(select(.prerelease == false)) | sort_by(.created_at) | reverse | .[0].tag_name | ltrimstr("v")')
if [ -z "$CLI_VERSION" ] || [ "$CLI_VERSION" = "null" ]; then
echo "Failed to resolve Dapr CLI version" && exit 1
fi
echo "DAPR_CLI_VER=$CLI_VERSION" >> $GITHUB_ENV
echo "Found $CLI_VERSION"
- name: Set up Python ${{ matrix.python_ver }}
uses: actions/setup-python@v6
with:
Expand All @@ -92,9 +91,10 @@ jobs:
with:
commit: ${{ github.event.inputs.daprcli_commit }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Dapr runtime
- name: Set up Dapr runtime ${{ matrix.runtime_version }}
uses: dapr/.github/.github/actions/setup-dapr-runtime@main
with:
version: ${{ matrix.runtime_version }}
commit: ${{ github.event.inputs.daprdapr_commit }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Llama
Expand Down
95 changes: 95 additions & 0 deletions tests/test_compute_compat_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import unittest
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path

_MODULE_PATH = Path(__file__).resolve().parent.parent / 'tools' / 'compute_compat_matrix.py'
_spec = spec_from_file_location('compute_compat_matrix', _MODULE_PATH)
if _spec is None or _spec.loader is None:
raise ImportError(f'Cannot load compute_compat_matrix from {_MODULE_PATH}')
compute_compat_matrix = module_from_spec(_spec)
_spec.loader.exec_module(compute_compat_matrix)

parse_sdk_version = compute_compat_matrix.parse_sdk_version
runtime_minors_from_sdk = compute_compat_matrix.runtime_minors_from_sdk
version_key = compute_compat_matrix.version_key
latest_patch_for_minor = compute_compat_matrix.latest_patch_for_minor
build_matrix = compute_compat_matrix.build_matrix

SAMPLE_RELEASES = [
{'tag_name': 'v1.18.0', 'prerelease': False},
{'tag_name': 'v1.18.0-rc.5', 'prerelease': True},
{'tag_name': 'v1.18.0-rc.1', 'prerelease': True},
{'tag_name': 'v1.17.9', 'prerelease': False},
{'tag_name': 'v1.17.0', 'prerelease': False},
{'tag_name': 'v1.16.14', 'prerelease': False},
]


class ParseSdkVersionTests(unittest.TestCase):
def test_strips_dev_suffix(self) -> None:
self.assertEqual(parse_sdk_version('1.19.0.dev'), (1, 19))

def test_stable_version(self) -> None:
self.assertEqual(parse_sdk_version('1.18.0'), (1, 18))


class RuntimeMinorsFromSdkTests(unittest.TestCase):
def test_three_minors_from_main(self) -> None:
self.assertEqual(runtime_minors_from_sdk(1, 19), ['1.19', '1.18', '1.17'])

def test_three_minors_from_release_branch(self) -> None:
self.assertEqual(runtime_minors_from_sdk(1, 16), ['1.16', '1.15', '1.14'])


class VersionKeyTests(unittest.TestCase):
def test_rc_ordering(self) -> None:
self.assertLess(version_key('1.18.0-rc.1'), version_key('1.18.0-rc.5'))

def test_stable_ordering(self) -> None:
self.assertLess(version_key('1.17.0'), version_key('1.17.9'))


class LatestPatchForMinorTests(unittest.TestCase):
def test_prefers_stable_over_rc(self) -> None:
patch = latest_patch_for_minor(SAMPLE_RELEASES, '1.18')
self.assertEqual(patch, '1.18.0')

def test_picks_latest_stable_patch(self) -> None:
patch = latest_patch_for_minor(SAMPLE_RELEASES, '1.17')
self.assertEqual(patch, '1.17.9')

def test_falls_back_to_rc_when_no_stable(self) -> None:
releases = [
{'tag_name': 'v1.19.0-rc.2', 'prerelease': True},
{'tag_name': 'v1.19.0-rc.5', 'prerelease': True},
]
patch = latest_patch_for_minor(releases, '1.19')
self.assertEqual(patch, '1.19.0-rc.5')

def test_returns_none_when_missing(self) -> None:
self.assertIsNone(latest_patch_for_minor(SAMPLE_RELEASES, '1.15'))


class BuildMatrixTests(unittest.TestCase):
def test_builds_python_by_runtime_jobs(self) -> None:
matrix = build_matrix('1.18.0', SAMPLE_RELEASES, python_versions=('3.10', '3.11'))

self.assertEqual(len(matrix['include']), 6)
self.assertEqual(
matrix['include'][0],
{'python_ver': '3.10', 'runtime_version': '1.18.0'},
)

def test_skips_unresolved_runtime_minor(self) -> None:
matrix = build_matrix('1.19.0.dev', SAMPLE_RELEASES, python_versions=('3.10',))

runtime_versions = {entry['runtime_version'] for entry in matrix['include']}
self.assertEqual(runtime_versions, {'1.18.0', '1.17.9'})

def test_raises_when_no_runtime_resolves(self) -> None:
with self.assertRaises(ValueError):
build_matrix('1.20.0.dev', [], python_versions=('3.10',))


if __name__ == '__main__':
unittest.main()
170 changes: 170 additions & 0 deletions tools/compute_compat_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Build a GitHub Actions matrix for Dapr runtime compatibility testing.

Derives runtime minors N, N-1, N-2 from the SDK VERSION file, resolves the
latest patch (or RC) per minor from dapr/dapr GitHub releases, and emits a
matrix of Python version × runtime version pairs for CI.
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

DAPR_RELEASES_URL = 'https://api.github.com/repos/dapr/dapr/releases?per_page=100'
DEFAULT_PYTHON_VERSIONS = ('3.10', '3.11', '3.12', '3.13', '3.14')
COMPAT_RUNTIME_COUNT = 3
DEFAULT_VERSION_FILE = Path('VERSION')
GITHUB_OUTPUT_ENV = 'GITHUB_OUTPUT'
GITHUB_TOKEN_ENV = 'GITHUB_TOKEN'
MATRIX_OUTPUT_KEY = 'matrix'


def parse_sdk_version(sdk_version: str) -> tuple[int, int]:
"""Return major and minor integers from an SDK version string."""
base_version = sdk_version.split('.dev', maxsplit=1)[0]
major_text, minor_text, *_ = base_version.split('.')
return int(major_text), int(minor_text)


def runtime_minors_from_sdk(major: int, minor: int, count: int = COMPAT_RUNTIME_COUNT) -> list[str]:
"""Return Dapr runtime minor strings for N .. N-(count-1)."""
return [f'{major}.{minor - offset}' for offset in range(count)]


def version_key(version: str) -> tuple[int, ...]:
"""Sort key for Dapr runtime version strings including RC suffixes."""
base_version, _, suffix = version.partition('-')
parts = tuple(int(part) for part in base_version.split('.'))
if suffix.startswith('rc.'):
return parts + (int(suffix.removeprefix('rc.')),)
return parts


def latest_patch_for_minor(releases: list[dict[str, Any]], runtime_minor: str) -> str | None:
"""Return the latest stable or RC patch version for a runtime minor."""
prefix = f'{runtime_minor}.'
for prerelease in (False, True):
versions = [
release['tag_name'].removeprefix('v')
for release in releases
if release.get('prerelease') == prerelease
and release['tag_name'].removeprefix('v').startswith(prefix)
]
if versions:
return sorted(versions, key=version_key)[-1]
return None


def build_matrix(
sdk_version: str,
releases: list[dict[str, Any]],
python_versions: tuple[str, ...] = DEFAULT_PYTHON_VERSIONS,
) -> dict[str, list[dict[str, str]]]:
"""Build the GitHub Actions strategy matrix for compatibility testing."""
major, minor = parse_sdk_version(sdk_version)
runtime_minors = runtime_minors_from_sdk(major, minor)
matrix_include: list[dict[str, str]] = []

for runtime_minor in runtime_minors:
runtime_version = latest_patch_for_minor(releases, runtime_minor)
if runtime_version is None:
print(f'Warning: no Dapr runtime release found for {runtime_minor}, skipping')
continue
for python_version in python_versions:
matrix_include.append(
{
'python_ver': python_version,
'runtime_version': runtime_version,
}
)

if not matrix_include:
raise ValueError('No Dapr runtime releases found for compatibility matrix')

return {'include': matrix_include}


def fetch_dapr_releases(
github_token: str | None, timeout_seconds: float = 30.0
) -> list[dict[str, Any]]:
"""Fetch release metadata from the dapr/dapr GitHub repository."""
headers: dict[str, str] = {}
if github_token:
headers['Authorization'] = f'Bearer {github_token}'

request = urllib.request.Request(DAPR_RELEASES_URL, headers=headers)
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
payload = json.load(response)

if not isinstance(payload, list):
raise ValueError('Unexpected GitHub API response for dapr/dapr releases')

return payload


def read_sdk_version(version_file: Path) -> str:
"""Read and strip the SDK version from VERSION."""
return version_file.read_text(encoding='utf-8').strip()


def write_github_output(
matrix: dict[str, list[dict[str, str]]], output_key: str = MATRIX_OUTPUT_KEY
) -> None:
"""Append matrix JSON to the GitHub Actions output file."""
output_path = os.environ.get(GITHUB_OUTPUT_ENV)
if not output_path:
return

with open(output_path, 'a', encoding='utf-8') as output_file:
output_file.write(f'{output_key}={json.dumps(matrix)}\n')


def main(argv: list[str] | None = None) -> int:
"""Compute the compatibility matrix and print or write CI outputs."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
'--version-file',
type=Path,
default=DEFAULT_VERSION_FILE,
help='Path to the SDK VERSION file (default: VERSION)',
)
parser.add_argument(
'--version',
help='SDK version string (overrides --version-file)',
)
args = parser.parse_args(argv)

sdk_version = args.version if args.version else read_sdk_version(args.version_file)
github_token = os.environ.get(GITHUB_TOKEN_ENV)

try:
releases = fetch_dapr_releases(github_token)
except urllib.error.URLError as err:
print(f'Failed to fetch Dapr releases: {err}', file=sys.stderr)
return 1

try:
matrix = build_matrix(sdk_version, releases)
except ValueError as err:
print(str(err), file=sys.stderr)
return 1

major, minor = parse_sdk_version(sdk_version)
runtime_minors = runtime_minors_from_sdk(major, minor)
print(f'SDK version: {sdk_version}')
print(f'Runtime minors: {runtime_minors}')
print(f'Matrix ({len(matrix["include"])} jobs): {json.dumps(matrix)}')

write_github_output(matrix)
return 0


if __name__ == '__main__':
raise SystemExit(main())