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
43 changes: 43 additions & 0 deletions .github/actions/release-smoke-package/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Release package smoke test
description: Install a published temporalio package and run a minimal SDK workflow.
inputs:
version:
description: "Package version to install and verify"
required: true
index-url:
description: "Primary package index URL"
required: true
dependency-index-url:
description: "Optional dependency package index URL"
required: false
default: ""
runs:
using: composite
steps:
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.10"
- name: Install package
shell: bash
env:
VERSION: ${{ inputs.version }}
INDEX_URL: ${{ inputs.index-url }}
DEPENDENCY_INDEX_URL: ${{ inputs.dependency-index-url }}
run: |
set -euo pipefail
python -m venv .venv
.venv/bin/python -m pip install --upgrade pip

install_args=(--version "$VERSION" --index-url "$INDEX_URL")
if [[ -n "$DEPENDENCY_INDEX_URL" ]]; then
install_args+=(--dependency-index-url "$DEPENDENCY_INDEX_URL")
fi

.venv/bin/python .github/scripts/install_release_package.py "${install_args[@]}"
- name: Run SDK smoke test
shell: bash
env:
VERSION: ${{ inputs.version }}
run: |
set -euo pipefail
.venv/bin/python .github/scripts/release_smoke_package.py
54 changes: 54 additions & 0 deletions .github/scripts/install_release_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Install a release package for smoke testing."""

from __future__ import annotations

import argparse
import importlib.metadata
import subprocess
import sys
from collections.abc import Sequence


def _pip_install(args: Sequence[str]) -> None:
subprocess.check_call([sys.executable, "-m", "pip", "install", *args])


def install_package(args: argparse.Namespace) -> None:
package = f"temporalio=={args.version}"
if args.dependency_index_url:
_pip_install(
[
"--prefer-binary",
"--index-url",
args.index_url,
"--no-deps",
package,
]
)

requirements = importlib.metadata.requires("temporalio") or []
if requirements:
_pip_install(
[
"--prefer-binary",
"--index-url",
args.dependency_index_url,
*requirements,
]
)
else:
_pip_install(["--prefer-binary", "--index-url", args.index_url, package])

subprocess.check_call([sys.executable, "-m", "pip", "check"])


def main(argv: Sequence[str] | None = None) -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--version", required=True)
parser.add_argument("--index-url", required=True)
parser.add_argument("--dependency-index-url")
install_package(parser.parse_args(argv))


if __name__ == "__main__":
main()
59 changes: 59 additions & 0 deletions .github/scripts/release_smoke_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Smoke test an installed temporalio release package."""

from __future__ import annotations

import asyncio
import os
import uuid
from datetime import timedelta

import temporalio
from temporalio import activity, workflow
from temporalio.testing import WorkflowEnvironment
from temporalio.worker import UnsandboxedWorkflowRunner, Worker


@activity.defn
async def say_hello(name: str) -> str:
return f"Hello, {name}!"


@workflow.defn
class SmokeWorkflow:
@workflow.run
async def run(self, name: str) -> str:
return await workflow.execute_activity(
say_hello,
name,
start_to_close_timeout=timedelta(seconds=10),
)


async def main() -> None:
expected_version = os.environ["VERSION"]
if temporalio.__version__ != expected_version:
raise RuntimeError(
f"Expected temporalio {expected_version}, got {temporalio.__version__}"
)

task_queue = f"release-smoke-{uuid.uuid4()}"
async with await WorkflowEnvironment.start_local() as env:
async with Worker(
env.client,
task_queue=task_queue,
workflows=[SmokeWorkflow],
activities=[say_hello],
workflow_runner=UnsandboxedWorkflowRunner(),
):
result = await env.client.execute_workflow(
SmokeWorkflow.run,
"trusted publishing",
id=task_queue,
task_queue=task_queue,
)
if result != "Hello, trusted publishing!":
raise RuntimeError(f"Unexpected workflow result: {result!r}")


if __name__ == "__main__":
asyncio.run(main())
130 changes: 130 additions & 0 deletions .github/scripts/release_verify.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
"""Release workflow validation helpers."""

from __future__ import annotations

import argparse
import ast
import pathlib
import re
from collections.abc import Sequence

try:
import tomllib
except ModuleNotFoundError:
import toml as tomllib # type: ignore[no-redef]


def _checked_in_version() -> str:
pyproject_version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())[
"project"
]["version"]
service_tree = ast.parse(pathlib.Path("temporalio/service.py").read_text())
service_version = None
for stmt in service_tree.body:
if (
isinstance(stmt, ast.Assign)
and any(
isinstance(target, ast.Name) and target.id == "__version__"
for target in stmt.targets
)
and isinstance(stmt.value, ast.Constant)
and isinstance(stmt.value.value, str)
):
service_version = stmt.value.value
break

if pyproject_version != service_version:
raise RuntimeError(
f"pyproject.toml version {pyproject_version!r} does not match "
f"temporalio/service.py version {service_version!r}"
)
if pyproject_version.startswith("v"):
raise RuntimeError("Checked-in version must not start with 'v'")
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?", pyproject_version):
raise RuntimeError(f"Invalid checked-in version: {pyproject_version!r}")
return pyproject_version


def _write_github_output(path: pathlib.Path, *, version: str, sha: str) -> None:
with path.open("a", encoding="utf-8") as output:
print(f"version={version}", file=output)
print(f"sha={sha}", file=output)


def validate_version(args: argparse.Namespace) -> None:
version = _checked_in_version()
if args.github_output:
_write_github_output(
pathlib.Path(args.github_output),
version=version,
sha=args.sha,
)
else:
print(version)


def verify_dist(args: argparse.Namespace) -> None:
dist_dir = pathlib.Path(args.dist_dir)
files = sorted(path.name for path in dist_dir.iterdir() if path.is_file())
wheels = [name for name in files if name.endswith(".whl")]
sdists = [name for name in files if name.endswith(".tar.gz")]

if len(files) != len(set(files)):
raise RuntimeError("Duplicate distribution filenames found")
expected_sdist = f"temporalio-{args.version}.tar.gz"
if sdists != [expected_sdist]:
raise RuntimeError(f"Expected only sdist {expected_sdist!r}, found {sdists!r}")
if len(wheels) != 5:
raise RuntimeError(
f"Expected 5 platform wheels, found {len(wheels)}: {wheels!r}"
)

for name in files:
if not name.startswith(f"temporalio-{args.version}"):
raise RuntimeError(
f"Distribution filename does not match requested version "
f"{args.version!r}: {name}"
)

expected_platforms = {
"linux-x86_64": lambda name: "manylinux" in name and "x86_64" in name,
"linux-aarch64": lambda name: "manylinux" in name and "aarch64" in name,
"macos-x86_64": lambda name: "macosx" in name and "x86_64" in name,
"macos-arm64": lambda name: "macosx" in name and "arm64" in name,
"windows-amd64": lambda name: "win_amd64" in name,
}
missing = [
platform
for platform, predicate in expected_platforms.items()
if not any(predicate(name) for name in wheels)
]
if missing:
raise RuntimeError(
f"Missing expected platform wheels: {missing!r}; found {wheels!r}"
)

print("Verified release artifacts:")
for name in files:
print(f" {name}")


def main(argv: Sequence[str] | None = None) -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(required=True)

validate_parser = subparsers.add_parser("validate-version")
validate_parser.add_argument("--sha", required=True)
validate_parser.add_argument("--github-output")
validate_parser.set_defaults(func=validate_version)

verify_parser = subparsers.add_parser("verify-dist")
verify_parser.add_argument("--version", required=True)
verify_parser.add_argument("--dist-dir", default="dist")
verify_parser.set_defaults(func=verify_dist)

args = parser.parse_args(argv)
args.func(args)


if __name__ == "__main__":
main()
81 changes: 0 additions & 81 deletions .github/workflows/build-binaries.yml

This file was deleted.

Loading
Loading