Skip to content

Release from main (dry run) #2

Release from main (dry run)

Release from main (dry run) #2

Workflow file for this run

name: Release Publish
run-name: Release from main${{ (github.event_name == 'pull_request' || inputs.dry_run) && ' (dry run)' || '' }}
on:
pull_request:
paths:
- ".github/actions/release-smoke-package/action.yml"
- ".github/workflows/release-publish.yml"
workflow_dispatch:
inputs:
dry_run:
description: "Verify release inputs and artifacts without publishing to package indexes or creating a release"
required: true
type: boolean
default: false
permissions:
contents: read
concurrency:
group: release-publish-main-${{ (github.event_name == 'pull_request' || inputs.dry_run) && 'dry-run' || 'publish' }}
cancel-in-progress: false
jobs:
collect_artifacts:
name: Collect release artifacts
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: read
contents: read
outputs:
build_run_id: ${{ steps.resolve_build.outputs.run_id }}
release_sha: ${{ steps.resolve_build.outputs.sha }}
version: ${{ steps.validate_versions.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: main
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: "3.11"
- name: Validate checked-in versions
id: validate_versions
run: |
set -euo pipefail
python - <<'PY'
import ast
import os
import pathlib
import re
import tomllib
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 SystemExit(
f"pyproject.toml version {pyproject_version!r} does not match "
f"temporalio/service.py version {service_version!r}"
)
if pyproject_version.startswith("v"):
raise SystemExit("Checked-in version must not start with 'v'")
if not re.fullmatch(r"[0-9]+(?:\.[0-9]+)+(?:[a-zA-Z0-9_.+-]+)?", pyproject_version):
raise SystemExit(f"Invalid checked-in version: {pyproject_version!r}")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
print(f"version={pyproject_version}", file=output)
PY
- name: Resolve latest successful Build Binaries run on main
id: resolve_build
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
main_sha="$(git rev-parse HEAD)"
run_json="$(gh run list \
--repo "$GITHUB_REPOSITORY" \
--workflow build-binaries.yml \
--branch main \
--status success \
--json databaseId,headSha,url \
--limit 1)"
run_id="$(jq -r '.[0].databaseId // empty' <<<"$run_json")"
run_sha="$(jq -r '.[0].headSha // empty' <<<"$run_json")"
run_url="$(jq -r '.[0].url // empty' <<<"$run_json")"
if [[ -z "$run_id" ]]; then
echo "No successful Build Binaries run found on main" >&2
exit 1
fi
if [[ "$run_sha" != "$main_sha" ]]; then
echo "Latest successful Build Binaries run is not for current main" >&2
echo "main SHA: $main_sha" >&2
echo "run SHA: $run_sha" >&2
echo "run URL: $run_url" >&2
exit 1
fi
echo "Using Build Binaries run $run_id at $run_sha"
echo "run_id=$run_id" >>"$GITHUB_OUTPUT"
echo "sha=$main_sha" >>"$GITHUB_OUTPUT"
- name: Download and flatten artifacts
env:
GH_TOKEN: ${{ github.token }}
RUN_ID: ${{ steps.resolve_build.outputs.run_id }}
run: |
set -euo pipefail
mkdir artifacts dist
gh run download "$RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--dir artifacts \
--pattern 'packages-*'
while IFS= read -r -d '' file; do
dest="dist/$(basename "$file")"
if [[ -e "$dest" ]]; then
echo "Duplicate distribution filename: $(basename "$file")" >&2
exit 1
fi
cp "$file" "$dest"
done < <(find artifacts -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0)
- name: Verify release artifacts
env:
VERSION: ${{ steps.validate_versions.outputs.version }}
run: |
set -euo pipefail
python - <<'PY'
import os
import pathlib
version = os.environ["VERSION"]
dist_dir = pathlib.Path("dist")
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 SystemExit("Duplicate distribution filenames found")
expected_sdist = f"temporalio-{version}.tar.gz"
if sdists != [expected_sdist]:
raise SystemExit(f"Expected only sdist {expected_sdist!r}, found {sdists!r}")
if len(wheels) != 5:
raise SystemExit(f"Expected 5 platform wheels, found {len(wheels)}: {wheels!r}")
for name in files:
if not name.startswith(f"temporalio-{version}"):
raise SystemExit(f"Distribution filename does not match requested version {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 SystemExit(f"Missing expected platform wheels: {missing!r}; found {wheels!r}")
print("Verified release artifacts:")
for name in files:
print(f" {name}")
PY
- name: Upload verified release artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-dist-${{ steps.validate_versions.outputs.version }}
path: dist
if-no-files-found: error
retention-days: 14
publish_testpypi:
name: Publish to TestPyPI
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
needs: collect_artifacts
runs-on: ubuntu-latest
timeout-minutes: 10
environment: testpypi
env:
VERSION: ${{ needs.collect_artifacts.outputs.version }}
permissions:
actions: read
contents: read
id-token: write
steps:
- name: Download verified release artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir downloaded dist
gh run download "$GITHUB_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--name "release-dist-$VERSION" \
--dir downloaded
while IFS= read -r -d '' file; do
cp "$file" "dist/$(basename "$file")"
done < <(find downloaded -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0)
test "$(find dist -type f | wc -l)" -gt 0
- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
repository-url: https://test.pypi.org/legacy/
skip-existing: true
smoke_testpypi:
name: Smoke test TestPyPI package
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
needs:
- collect_artifacts
- publish_testpypi
runs-on: ubuntu-latest
timeout-minutes: 10
env:
VERSION: ${{ needs.collect_artifacts.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: ./.github/actions/release-smoke-package
with:
version: ${{ env.VERSION }}
index-url: https://test.pypi.org/simple/
extra-index-url: https://pypi.org/simple/
publish_pypi:
name: Publish to PyPI
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
needs:
- collect_artifacts
- smoke_testpypi
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
env:
VERSION: ${{ needs.collect_artifacts.outputs.version }}
permissions:
actions: read
contents: read
id-token: write
steps:
- name: Download verified release artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir downloaded dist
gh run download "$GITHUB_RUN_ID" \
--repo "$GITHUB_REPOSITORY" \
--name "release-dist-$VERSION" \
--dir downloaded
while IFS= read -r -d '' file; do
cp "$file" "dist/$(basename "$file")"
done < <(find downloaded -type f \( -name '*.whl' -o -name '*.tar.gz' \) -print0)
test "$(find dist -type f | wc -l)" -gt 0
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: dist/
smoke_pypi:
name: Smoke test PyPI package
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
needs:
- collect_artifacts
- publish_pypi
runs-on: ubuntu-latest
timeout-minutes: 10
env:
VERSION: ${{ needs.collect_artifacts.outputs.version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- uses: ./.github/actions/release-smoke-package
with:
version: ${{ env.VERSION }}
index-url: https://pypi.org/simple/
create_draft_release:
name: Create draft GitHub Release
if: ${{ github.event_name != 'pull_request' && !inputs.dry_run }}
needs:
- collect_artifacts
- smoke_pypi
runs-on: ubuntu-latest
timeout-minutes: 5
env:
VERSION: ${{ needs.collect_artifacts.outputs.version }}
permissions:
contents: write
steps:
- name: Create draft release with generated notes
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ needs.collect_artifacts.outputs.release_sha }}
run: |
set -euo pipefail
gh release create "$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--target "$RELEASE_SHA" \
--title "$VERSION" \
--draft \
--generate-notes