Skip to content

Commit c1fceb7

Browse files
sethmlarsonezio-melottiambv
authored
Build source and documentation in GitHub Actions (#71)
Co-authored-by: Ezio Melotti <ezio.melotti@gmail.com> Co-authored-by: Łukasz Langa <lukasz@langa.pl>
1 parent 58a7c91 commit c1fceb7

3 files changed

Lines changed: 211 additions & 54 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
on:
2+
workflow_dispatch:
3+
inputs:
4+
git_remote:
5+
type: choice
6+
description: "Git remote to checkout"
7+
options:
8+
- python
9+
- Yhg1s
10+
- pablogsal
11+
- ambv
12+
git_commit:
13+
type: string
14+
description: "Git commit to target for the release. Must use the full commit SHA, not the short ID"
15+
cpython_release:
16+
type: string
17+
description: "CPython release number (ie '3.11.5', note without the 'v' prefix)"
18+
19+
name: "Build Python source and docs artifacts"
20+
21+
jobs:
22+
source-and-docs:
23+
runs-on: ubuntu-22.04
24+
steps:
25+
- name: "Checkout python/release-tools"
26+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
27+
28+
- name: "Checkout ${{ inputs.git_remote }}/cpython"
29+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
30+
with:
31+
repository: "${{ inputs.git_remote }}/cpython"
32+
ref: "v${{ inputs.cpython_release }}"
33+
path: "cpython"
34+
35+
- name: "Verify CPython commit matches tag"
36+
run: |
37+
if [[ "${{ inputs.git_commit }}" != "$(cd cpython && git rev-parse HEAD)" ]]; then
38+
echo "expected git commit ('${{ inputs.git_commit }}') didn't match tagged commit ('$(git rev-parse HEAD)')"
39+
exit 1
40+
fi
41+
42+
- name: "Setup Python"
43+
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
44+
with:
45+
python-version: 3.11
46+
47+
- name: "Install source dependencies"
48+
run: |
49+
python -m pip install --no-deps \
50+
-r requirements.txt
51+
52+
- name: "Install docs dependencies"
53+
# Docs aren't built for alpha or beta releases.
54+
if: ${{ !(contains(inputs.cpython_release, 'a') || contains(inputs.cpython_release, 'b')) }}
55+
run: |
56+
python -m pip install \
57+
-r cpython/Doc/requirements.txt
58+
59+
sudo apt-get update
60+
sudo apt-get install --yes --no-install-recommends \
61+
latexmk texlive-xetex xindy texinfo texlive-latex-base \
62+
texlive-fonts-recommended texlive-fonts-extra \
63+
texlive-full
64+
65+
- name: "Build Python release artifacts"
66+
run: |
67+
cd cpython
68+
python ../release.py --export ${{ inputs.cpython_release }}
69+
70+
- name: "Test Python source tarballs"
71+
run: |
72+
mkdir -p ./tmp/installation/
73+
cp cpython/${{ inputs.cpython_release }}/src/Python-${{ inputs.cpython_release }}.tgz ./tmp/
74+
cd tmp/
75+
tar xvf Python-${{ inputs.cpython_release }}.tgz
76+
cd Python-${{ inputs.cpython_release }}
77+
78+
./configure --prefix=$(realpath '../installation/')
79+
make -j
80+
make install -j
81+
82+
cd ../installation
83+
./bin/python3 -m test -uall
84+
85+
- name: "Upload the source artifacts"
86+
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
87+
with:
88+
name: source
89+
path: |
90+
cpython/${{ inputs.cpython_release }}/src
91+
92+
- name: "Upload the docs artifacts"
93+
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
94+
# Conditionally run this step if there is a 'docs/' directory.
95+
# Docs aren't built for alpha or beta releases.
96+
if: ${{ hashFiles('cpython/${{ inputs.cpython_release }}/docs') != '' }}
97+
with:
98+
name: docs
99+
path: |
100+
cpython/${{ inputs.cpython_release }}/docs

release.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -277,19 +277,6 @@ def tarball(source, clamp_mtime):
277277
print(' %s %8s %s' % (
278278
checksum_xz.hexdigest(), int(os.path.getsize(xz)), xz))
279279

280-
print('Signing tarballs with GPG')
281-
uid = os.environ.get("GPG_KEY_FOR_RELEASE")
282-
if not uid:
283-
print('List of available private keys:')
284-
run_cmd(['gpg -K | grep -A 1 "^sec"'], shell=True)
285-
uid = input('Please enter key ID to use for signing: ')
286-
run_cmd(['gpg', '-bas', '-u', uid, tgz])
287-
run_cmd(['gpg', '-bas', '-u', uid, xz])
288-
289-
print('Signing tarballs with Sigstore')
290-
run_cmd(['python3', '-m', 'sigstore', 'sign',
291-
'--oidc-disable-ambient-providers', tgz, xz], shell=False)
292-
293280

294281
def export(tag, silent=False):
295282
make_dist(tag.text)

run_release.py

Lines changed: 111 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,6 @@ def check_tool(db: DbfilenameShelf, tool: str) -> None:
297297

298298

299299
check_git = functools.partial(check_tool, tool="git")
300-
check_latexmk = functools.partial(check_tool, tool="latexmk")
301300
check_make = functools.partial(check_tool, tool="make")
302301
check_blurb = functools.partial(check_tool, tool="blurb")
303302
check_autoconf = functools.partial(check_tool, tool="autoconf")
@@ -474,39 +473,63 @@ def create_tag(db: DbfilenameShelf) -> None:
474473
)
475474

476475

477-
def build_release_artifacts(db: DbfilenameShelf) -> None:
478-
with cd(db["git_repo"]):
479-
release_mod.export(db["release"])
480-
481-
482-
def test_release_artifacts(db: DbfilenameShelf) -> None:
483-
with tempfile.TemporaryDirectory() as tmppath:
484-
the_dir = pathlib.Path(tmppath)
485-
the_dir.mkdir(exist_ok=True)
486-
filename = f"Python-{db['release']}"
487-
tarball = f"Python-{db['release']}.tgz"
488-
shutil.copy2(
489-
db["git_repo"] / str(db["release"]) / "src" / tarball,
490-
the_dir / tarball,
491-
)
492-
subprocess.check_call(["tar", "xvf", tarball], cwd=the_dir)
493-
subprocess.check_call(
494-
["./configure", "--prefix", str(the_dir / "installation")],
495-
cwd=the_dir / filename,
496-
)
497-
subprocess.check_call(["make", "-j"], cwd=the_dir / filename)
498-
subprocess.check_call(["make", "install", "-j"], cwd=the_dir / filename)
499-
process = subprocess.run(
500-
["./bin/python3", "-m", "test", "-uall"],
501-
cwd=str(the_dir / "installation"),
502-
text=True,
503-
)
476+
def wait_for_source_and_docs_artifacts(db: DbfilenameShelf) -> None:
477+
# Determine if we need to wait for docs or only source artifacts.
478+
release_tag = db["release"]
479+
should_wait_for_docs = release_tag.is_final or release_tag.is_release_candiate
504480

505-
if process.returncode == 0:
506-
return
481+
# Create the directory so it's easier to place the artifacts there.
482+
release_path = pathlib.Path(db["git_repo"] / str(release_tag))
483+
release_path.mkdir(parents=True, exist_ok=True)
507484

508-
if not ask_question("Some test_failed! Do you want to continue?"):
509-
raise ReleaseException("Test failed!")
485+
# Build the list of filepaths we're expecting.
486+
wait_for_paths = [
487+
release_path / "src" / f"Python-{release_tag}.tgz",
488+
release_path / "src" / f"Python-{release_tag}.tar.xz"
489+
]
490+
if should_wait_for_docs:
491+
docs_path = release_path / "docs"
492+
wait_for_paths.extend([
493+
docs_path / f"python-{release_tag}-docs.epub",
494+
docs_path / f"python-{release_tag}-docs-html.tar.bz2",
495+
docs_path / f"python-{release_tag}-docs-html.zip",
496+
docs_path / f"python-{release_tag}-docs-pdf-a4.tar.bz2",
497+
docs_path / f"python-{release_tag}-docs-pdf-a4.zip",
498+
docs_path / f"python-{release_tag}-docs-pdf-letter.tar.bz2",
499+
docs_path / f"python-{release_tag}-docs-pdf-letter.zip",
500+
docs_path / f"python-{release_tag}-docs-texinfo.tar.bz2",
501+
docs_path / f"python-{release_tag}-docs-texinfo.zip",
502+
docs_path / f"python-{release_tag}-docs-text.tar.bz2",
503+
docs_path / f"python-{release_tag}-docs-text.zip",
504+
])
505+
506+
print(f"Waiting for source{' and docs' if should_wait_for_docs else ''} artifacts to be built")
507+
print(f"Artifacts should be placed at '{release_path}':")
508+
for path in wait_for_paths:
509+
print(f"- '{os.path.relpath(path, release_path)}'")
510+
511+
while not all(path.exists() for path in wait_for_paths):
512+
time.sleep(1)
513+
514+
515+
def sign_source_artifacts(db: DbfilenameShelf) -> None:
516+
print('Signing tarballs with GPG')
517+
uid = os.environ.get("GPG_KEY_FOR_RELEASE")
518+
if not uid:
519+
print('List of available private keys:')
520+
subprocess.check_call('gpg -K | grep -A 1 "^sec"', shell=True)
521+
uid = input('Please enter key ID to use for signing: ')
522+
523+
tarballs_path = pathlib.Path(db["git_repo"] / str(db["release"]) / "src")
524+
tgz = str(tarballs_path / ("Python-%s.tgz" % db["release"]))
525+
xz = str(tarballs_path / ("Python-%s.tar.xz" % db["release"]))
526+
527+
subprocess.check_call(['gpg', '-bas', '-u', uid, tgz])
528+
subprocess.check_call(['gpg', '-bas', '-u', uid, xz])
529+
530+
print('Signing tarballs with Sigstore')
531+
subprocess.check_call(['python3', '-m', 'sigstore', 'sign',
532+
'--oidc-disable-ambient-providers', tgz, xz])
510533

511534

512535
def build_sbom_artifacts(db):
@@ -698,9 +721,55 @@ def execute_command(command):
698721
execute_command(f"find {destination} -type f -exec chmod 664 {{}} \\;")
699722

700723

724+
def start_build_of_source_and_docs(db: DbfilenameShelf) -> None:
725+
# Get the git commit SHA for the tag
726+
commit_sha = subprocess.check_output(
727+
["git", "rev-list", "-n", "1", db["release"].gitname],
728+
cwd=db["git_repo"]
729+
).decode().strip()
730+
731+
# Get the owner of the GitHub repo (first path segment in a 'github.com' remote URL)
732+
# This works for both 'https' and 'ssh' style remote URLs.
733+
origin_remote_url = subprocess.check_output(
734+
["git", "ls-remote", "--get-url", "origin"],
735+
cwd=db["git_repo"]
736+
).decode().strip()
737+
match = re.match(r"github\.com/([^/]+)/", origin_remote_url)
738+
if not match:
739+
raise ReleaseException(f"Could not parse GitHub owner from 'origin' remote URL: {origin_remote_url}")
740+
origin_remote_github_owner = match.group(1)
741+
742+
# We ask for human verification at this point since this commit SHA is 'locked in'
743+
print()
744+
print(f"Go to https://github.com/{origin_remote_github_owner}/cpython/commit/{commit_sha}")
745+
print("- Ensure that there is no warning that the commit does not belong to this repository.")
746+
print("- Ensure that the commit diff does not contain any unexpected changes.")
747+
print("- For the next step, ensure the commit SHA matches the one you verified on GitHub in this step.")
748+
print()
749+
if not ask_question(
750+
"Have you verified the release commit hasn't been tampered with on GitHub?"
751+
):
752+
raise ReleaseException("Commit must be visually reviewed before starting build")
753+
754+
# After visually confirming the release manager can start the build process
755+
# with the known good commit SHA.
756+
print()
757+
print("Go to https://github.com/python/release-tools/actions/workflows/source-and-docs-release.yml")
758+
print("Select 'Run workflow' and enter the following values:")
759+
print(f"- Git remote to checkout: {origin_remote_github_owner}")
760+
print(f"- Git commit to target for the release: {commit_sha}")
761+
print(f"- CPython release number: {db['release']}")
762+
print()
763+
764+
if not ask_question(
765+
"Have you started the source and docs build?"
766+
):
767+
raise ReleaseException("Source and docs build must be started")
768+
769+
701770
def send_email_to_platform_release_managers(db: DbfilenameShelf) -> None:
702771
if not ask_question(
703-
"Have you notified the platform release managers about the availability of artifacts?"
772+
"Have you notified the platform release managers about the availability of the commit SHA and tag?"
704773
):
705774
raise ReleaseException("Platform release managers muy be notified")
706775

@@ -1040,9 +1109,9 @@ def _api_key(api_key):
10401109
)
10411110
args = parser.parse_args()
10421111
auth_key = args.auth_key or os.getenv("AUTH_INFO")
1112+
assert isinstance(auth_key, str), "We need an AUTH_INFO env var or --auth-key"
10431113
tasks = [
10441114
Task(check_git, "Checking git is available"),
1045-
Task(check_latexmk, "Checking latexmk is available"),
10461115
Task(check_make, "Checking make is available"),
10471116
Task(check_blurb, "Checking blurb is available"),
10481117
Task(check_docker, "Checking docker is available"),
@@ -1061,18 +1130,19 @@ def _api_key(api_key):
10611130
Task(bump_version, "Bump version"),
10621131
Task(check_cpython_repo_is_clean, "Checking git repository is clean"),
10631132
Task(create_tag, "Create tag"),
1064-
Task(build_release_artifacts, "Building release artifacts"),
1065-
Task(test_release_artifacts, "Test release artifacts"),
1133+
Task(push_to_local_fork, "Push new tags and branches to private fork"),
1134+
Task(start_build_of_source_and_docs, "Start the builds for source and docs artifacts"),
1135+
Task(
1136+
send_email_to_platform_release_managers,
1137+
"Platform release managers have been notified of the commit SHA",
1138+
),
1139+
Task(wait_for_source_and_docs_artifacts, "Wait for source and docs artifacts to build"),
10661140
Task(build_sbom_artifacts, "Building SBOM artifacts"),
1141+
Task(sign_source_artifacts, "Sign source artifacts"),
10671142
Task(upload_files_to_server, "Upload files to the PSF server"),
10681143
Task(place_files_in_download_folder, "Place files in the download folder"),
10691144
Task(upload_docs_to_the_docs_server, "Upload docs to the PSF docs server"),
10701145
Task(unpack_docs_in_the_docs_server, "Place docs files in the docs folder"),
1071-
Task(push_to_local_fork, "Push new tags and branches to private fork"),
1072-
Task(
1073-
send_email_to_platform_release_managers,
1074-
"Platform release managers have been notified of the release artifacts",
1075-
),
10761146
Task(wait_util_all_files_are_in_folder, "Wait until all files are ready"),
10771147
Task(create_release_object_in_db, "The django release object has been created"),
10781148
Task(post_release_merge, "Merge the tag into the release branch"),

0 commit comments

Comments
 (0)