Skip to content

Commit f8c2566

Browse files
committed
Add trusted publishing release workflow
1 parent 9a95fef commit f8c2566

5 files changed

Lines changed: 500 additions & 81 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Release package smoke test
2+
description: Install a published temporalio package and run a minimal SDK workflow.
3+
inputs:
4+
version:
5+
description: "Package version to install and verify"
6+
required: true
7+
index-url:
8+
description: "Primary package index URL"
9+
required: true
10+
extra-index-url:
11+
description: "Optional fallback package index URL"
12+
required: false
13+
default: ""
14+
runs:
15+
using: composite
16+
steps:
17+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
18+
with:
19+
python-version: "3.10"
20+
- name: Install package
21+
shell: bash
22+
env:
23+
VERSION: ${{ inputs.version }}
24+
INDEX_URL: ${{ inputs.index-url }}
25+
EXTRA_INDEX_URL: ${{ inputs.extra-index-url }}
26+
run: |
27+
set -euo pipefail
28+
python -m venv .venv
29+
.venv/bin/python -m pip install --upgrade pip
30+
31+
install_args=(--prefer-binary --index-url "$INDEX_URL")
32+
if [[ -n "$EXTRA_INDEX_URL" ]]; then
33+
install_args+=(--extra-index-url "$EXTRA_INDEX_URL")
34+
fi
35+
36+
.venv/bin/python -m pip install "${install_args[@]}" "temporalio==$VERSION"
37+
- name: Run SDK smoke test
38+
shell: bash
39+
env:
40+
VERSION: ${{ inputs.version }}
41+
run: |
42+
set -euo pipefail
43+
.venv/bin/python .github/scripts/release_smoke_package.py
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""Smoke test an installed temporalio release package."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
import os
7+
import uuid
8+
from datetime import timedelta
9+
10+
import temporalio
11+
from temporalio import activity, workflow
12+
from temporalio.testing import WorkflowEnvironment
13+
from temporalio.worker import UnsandboxedWorkflowRunner, Worker
14+
15+
16+
@activity.defn
17+
async def say_hello(name: str) -> str:
18+
return f"Hello, {name}!"
19+
20+
21+
@workflow.defn
22+
class SmokeWorkflow:
23+
@workflow.run
24+
async def run(self, name: str) -> str:
25+
return await workflow.execute_activity(
26+
say_hello,
27+
name,
28+
start_to_close_timeout=timedelta(seconds=10),
29+
)
30+
31+
32+
async def main() -> None:
33+
expected_version = os.environ["VERSION"]
34+
if temporalio.__version__ != expected_version:
35+
raise RuntimeError(
36+
f"Expected temporalio {expected_version}, got {temporalio.__version__}"
37+
)
38+
39+
task_queue = f"release-smoke-{uuid.uuid4()}"
40+
async with await WorkflowEnvironment.start_local() as env:
41+
async with Worker(
42+
env.client,
43+
task_queue=task_queue,
44+
workflows=[SmokeWorkflow],
45+
activities=[say_hello],
46+
workflow_runner=UnsandboxedWorkflowRunner(),
47+
):
48+
result = await env.client.execute_workflow(
49+
SmokeWorkflow.run,
50+
"trusted publishing",
51+
id=task_queue,
52+
task_queue=task_queue,
53+
)
54+
if result != "Hello, trusted publishing!":
55+
raise RuntimeError(f"Unexpected workflow result: {result!r}")
56+
57+
58+
if __name__ == "__main__":
59+
asyncio.run(main())

.github/scripts/release_verify.py

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Release workflow validation helpers."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import ast
7+
import pathlib
8+
import re
9+
from collections.abc import Sequence
10+
11+
try:
12+
import tomllib
13+
except ModuleNotFoundError:
14+
import toml as tomllib # type: ignore[no-redef]
15+
16+
17+
def _checked_in_version() -> str:
18+
pyproject_version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())[
19+
"project"
20+
]["version"]
21+
service_tree = ast.parse(pathlib.Path("temporalio/service.py").read_text())
22+
service_version = None
23+
for stmt in service_tree.body:
24+
if (
25+
isinstance(stmt, ast.Assign)
26+
and any(
27+
isinstance(target, ast.Name) and target.id == "__version__"
28+
for target in stmt.targets
29+
)
30+
and isinstance(stmt.value, ast.Constant)
31+
and isinstance(stmt.value.value, str)
32+
):
33+
service_version = stmt.value.value
34+
break
35+
36+
if pyproject_version != service_version:
37+
raise RuntimeError(
38+
f"pyproject.toml version {pyproject_version!r} does not match "
39+
f"temporalio/service.py version {service_version!r}"
40+
)
41+
if pyproject_version.startswith("v"):
42+
raise RuntimeError("Checked-in version must not start with 'v'")
43+
if not re.fullmatch(
44+
r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?", pyproject_version
45+
):
46+
raise RuntimeError(f"Invalid checked-in version: {pyproject_version!r}")
47+
return pyproject_version
48+
49+
50+
def _write_github_output(path: pathlib.Path, *, version: str, sha: str) -> None:
51+
with path.open("a", encoding="utf-8") as output:
52+
print(f"version={version}", file=output)
53+
print(f"sha={sha}", file=output)
54+
55+
56+
def validate_version(args: argparse.Namespace) -> None:
57+
version = _checked_in_version()
58+
if args.github_output:
59+
_write_github_output(
60+
pathlib.Path(args.github_output),
61+
version=version,
62+
sha=args.sha,
63+
)
64+
else:
65+
print(version)
66+
67+
68+
def verify_dist(args: argparse.Namespace) -> None:
69+
dist_dir = pathlib.Path(args.dist_dir)
70+
files = sorted(path.name for path in dist_dir.iterdir() if path.is_file())
71+
wheels = [name for name in files if name.endswith(".whl")]
72+
sdists = [name for name in files if name.endswith(".tar.gz")]
73+
74+
if len(files) != len(set(files)):
75+
raise RuntimeError("Duplicate distribution filenames found")
76+
expected_sdist = f"temporalio-{args.version}.tar.gz"
77+
if sdists != [expected_sdist]:
78+
raise RuntimeError(f"Expected only sdist {expected_sdist!r}, found {sdists!r}")
79+
if len(wheels) != 5:
80+
raise RuntimeError(f"Expected 5 platform wheels, found {len(wheels)}: {wheels!r}")
81+
82+
for name in files:
83+
if not name.startswith(f"temporalio-{args.version}"):
84+
raise RuntimeError(
85+
f"Distribution filename does not match requested version "
86+
f"{args.version!r}: {name}"
87+
)
88+
89+
expected_platforms = {
90+
"linux-x86_64": lambda name: "manylinux" in name and "x86_64" in name,
91+
"linux-aarch64": lambda name: "manylinux" in name and "aarch64" in name,
92+
"macos-x86_64": lambda name: "macosx" in name and "x86_64" in name,
93+
"macos-arm64": lambda name: "macosx" in name and "arm64" in name,
94+
"windows-amd64": lambda name: "win_amd64" in name,
95+
}
96+
missing = [
97+
platform
98+
for platform, predicate in expected_platforms.items()
99+
if not any(predicate(name) for name in wheels)
100+
]
101+
if missing:
102+
raise RuntimeError(f"Missing expected platform wheels: {missing!r}; found {wheels!r}")
103+
104+
print("Verified release artifacts:")
105+
for name in files:
106+
print(f" {name}")
107+
108+
109+
def main(argv: Sequence[str] | None = None) -> None:
110+
parser = argparse.ArgumentParser()
111+
subparsers = parser.add_subparsers(required=True)
112+
113+
validate_parser = subparsers.add_parser("validate-version")
114+
validate_parser.add_argument("--sha", required=True)
115+
validate_parser.add_argument("--github-output")
116+
validate_parser.set_defaults(func=validate_version)
117+
118+
verify_parser = subparsers.add_parser("verify-dist")
119+
verify_parser.add_argument("--version", required=True)
120+
verify_parser.add_argument("--dist-dir", default="dist")
121+
verify_parser.set_defaults(func=verify_dist)
122+
123+
args = parser.parse_args(argv)
124+
args.func(args)
125+
126+
127+
if __name__ == "__main__":
128+
main()

.github/workflows/build-binaries.yml

Lines changed: 0 additions & 81 deletions
This file was deleted.

0 commit comments

Comments
 (0)