Skip to content

Commit 05aef6b

Browse files
authored
Add trusted publishing release workflow (#1575)
* Add trusted publishing release workflow * Format release verification script * Harden release smoke package install
1 parent d211027 commit 05aef6b

6 files changed

Lines changed: 550 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+
dependency-index-url:
11+
description: "Optional dependency 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+
DEPENDENCY_INDEX_URL: ${{ inputs.dependency-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=(--version "$VERSION" --index-url "$INDEX_URL")
32+
if [[ -n "$DEPENDENCY_INDEX_URL" ]]; then
33+
install_args+=(--dependency-index-url "$DEPENDENCY_INDEX_URL")
34+
fi
35+
36+
.venv/bin/python .github/scripts/install_release_package.py "${install_args[@]}"
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Install a release package for smoke testing."""
2+
3+
from __future__ import annotations
4+
5+
import argparse
6+
import importlib.metadata
7+
import subprocess
8+
import sys
9+
from collections.abc import Sequence
10+
11+
12+
def _pip_install(args: Sequence[str]) -> None:
13+
subprocess.check_call([sys.executable, "-m", "pip", "install", *args])
14+
15+
16+
def install_package(args: argparse.Namespace) -> None:
17+
package = f"temporalio=={args.version}"
18+
if args.dependency_index_url:
19+
_pip_install(
20+
[
21+
"--prefer-binary",
22+
"--index-url",
23+
args.index_url,
24+
"--no-deps",
25+
package,
26+
]
27+
)
28+
29+
requirements = importlib.metadata.requires("temporalio") or []
30+
if requirements:
31+
_pip_install(
32+
[
33+
"--prefer-binary",
34+
"--index-url",
35+
args.dependency_index_url,
36+
*requirements,
37+
]
38+
)
39+
else:
40+
_pip_install(["--prefer-binary", "--index-url", args.index_url, package])
41+
42+
subprocess.check_call([sys.executable, "-m", "pip", "check"])
43+
44+
45+
def main(argv: Sequence[str] | None = None) -> None:
46+
parser = argparse.ArgumentParser()
47+
parser.add_argument("--version", required=True)
48+
parser.add_argument("--index-url", required=True)
49+
parser.add_argument("--dependency-index-url")
50+
install_package(parser.parse_args(argv))
51+
52+
53+
if __name__ == "__main__":
54+
main()
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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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(r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?", pyproject_version):
44+
raise RuntimeError(f"Invalid checked-in version: {pyproject_version!r}")
45+
return pyproject_version
46+
47+
48+
def _write_github_output(path: pathlib.Path, *, version: str, sha: str) -> None:
49+
with path.open("a", encoding="utf-8") as output:
50+
print(f"version={version}", file=output)
51+
print(f"sha={sha}", file=output)
52+
53+
54+
def validate_version(args: argparse.Namespace) -> None:
55+
version = _checked_in_version()
56+
if args.github_output:
57+
_write_github_output(
58+
pathlib.Path(args.github_output),
59+
version=version,
60+
sha=args.sha,
61+
)
62+
else:
63+
print(version)
64+
65+
66+
def verify_dist(args: argparse.Namespace) -> None:
67+
dist_dir = pathlib.Path(args.dist_dir)
68+
files = sorted(path.name for path in dist_dir.iterdir() if path.is_file())
69+
wheels = [name for name in files if name.endswith(".whl")]
70+
sdists = [name for name in files if name.endswith(".tar.gz")]
71+
72+
if len(files) != len(set(files)):
73+
raise RuntimeError("Duplicate distribution filenames found")
74+
expected_sdist = f"temporalio-{args.version}.tar.gz"
75+
if sdists != [expected_sdist]:
76+
raise RuntimeError(f"Expected only sdist {expected_sdist!r}, found {sdists!r}")
77+
if len(wheels) != 5:
78+
raise RuntimeError(
79+
f"Expected 5 platform wheels, found {len(wheels)}: {wheels!r}"
80+
)
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(
103+
f"Missing expected platform wheels: {missing!r}; found {wheels!r}"
104+
)
105+
106+
print("Verified release artifacts:")
107+
for name in files:
108+
print(f" {name}")
109+
110+
111+
def main(argv: Sequence[str] | None = None) -> None:
112+
parser = argparse.ArgumentParser()
113+
subparsers = parser.add_subparsers(required=True)
114+
115+
validate_parser = subparsers.add_parser("validate-version")
116+
validate_parser.add_argument("--sha", required=True)
117+
validate_parser.add_argument("--github-output")
118+
validate_parser.set_defaults(func=validate_version)
119+
120+
verify_parser = subparsers.add_parser("verify-dist")
121+
verify_parser.add_argument("--version", required=True)
122+
verify_parser.add_argument("--dist-dir", default="dist")
123+
verify_parser.set_defaults(func=verify_dist)
124+
125+
args = parser.parse_args(argv)
126+
args.func(args)
127+
128+
129+
if __name__ == "__main__":
130+
main()

.github/workflows/build-binaries.yml

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

0 commit comments

Comments
 (0)