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
100 changes: 100 additions & 0 deletions .github/workflows/source-and-docs-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
on:
workflow_dispatch:
inputs:
git_remote:
type: choice
description: "Git remote to checkout"
options:
- python
- Yhg1s
- pablogsal
- ambv
git_commit:
type: string
description: "Git commit to target for the release. Must use the full commit SHA, not the short ID"
cpython_release:
type: string
description: "CPython release number (ie '3.11.5', note without the 'v' prefix)"

name: "Build Python source and docs artifacts"

jobs:
source-and-docs:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would like to add a GitHub Environment to allow staging and reviewing an execution w/ approvals. I can create a separate GitHub issue for this if it's desirable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this for Windows installers so yeah, that would be nice!

runs-on: ubuntu-22.04
steps:
- name: "Checkout python/release-tools"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

- name: "Checkout ${{ inputs.git_remote }}/cpython"
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with:
repository: "${{ inputs.git_remote }}/cpython"
ref: "v${{ inputs.cpython_release }}"
path: "cpython"

- name: "Verify CPython commit matches tag"
run: |
if [[ "${{ inputs.git_commit }}" != "$(cd cpython && git rev-parse HEAD)" ]]; then
echo "expected git commit ('${{ inputs.git_commit }}') didn't match tagged commit ('$(git rev-parse HEAD)')"
exit 1
fi
- name: "Setup Python"
uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4.7.1
with:
python-version: 3.11
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should update this to 3.x? Wish there was a way to track this with Dependabot.


- name: "Install source dependencies"
run: |
python -m pip install --no-deps \
-r requirements.txt
- name: "Install docs dependencies"
# Docs aren't built for alpha or beta releases.
if: ${{ !(contains(inputs.cpython_release, 'a') || contains(inputs.cpython_release, 'b')) }}
run: |
python -m pip install \
-r cpython/Doc/requirements.txt
sudo apt-get update
sudo apt-get install --yes --no-install-recommends \
latexmk texlive-xetex xindy texinfo texlive-latex-base \
texlive-fonts-recommended texlive-fonts-extra \
texlive-full
- name: "Build Python release artifacts"
run: |
cd cpython
python ../release.py --export ${{ inputs.cpython_release }}
- name: "Test Python source tarballs"
run: |
mkdir -p ./tmp/installation/
cp cpython/${{ inputs.cpython_release }}/src/Python-${{ inputs.cpython_release }}.tgz ./tmp/
cd tmp/
tar xvf Python-${{ inputs.cpython_release }}.tgz
cd Python-${{ inputs.cpython_release }}
./configure --prefix=$(realpath '../installation/')
make -j
make install -j
cd ../installation
./bin/python3 -m test -uall
- name: "Upload the source artifacts"
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
with:
name: source
path: |
cpython/${{ inputs.cpython_release }}/src
- name: "Upload the docs artifacts"
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
# Conditionally run this step if there is a 'docs/' directory.
# Docs aren't built for alpha or beta releases.
if: ${{ hashFiles('cpython/${{ inputs.cpython_release }}/docs') != '' }}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This conditional doesn't match the above, but I figure if docs got built then we'd want to upload them as artifacts?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this run:
https://github.com/python/release-tools/actions/runs/8350750234/job/22857827913

the docs upload was skipped for whatever reason. They were built though. The source artifacts uploaded just fine. I don't immediately see why that would be so I just built the docs locally after for this release. I compared the logs from my local run with the docs build on GHA, they look the same. They both have the line:

2024-03-19T22:28:44.9625823Z chdir'ing to Python-3.10.14/Doc
2024-03-19T22:28:44.9626238Z Removing doc build artifacts

This comes from here:

release-tools/release.py

Lines 342 to 350 in c1fceb7

with pushd(os.path.join(archivename, 'Doc')):
print('Removing doc build artifacts')
shutil.rmtree('venv', ignore_errors=True)
shutil.rmtree('build', ignore_errors=True)
shutil.rmtree('dist', ignore_errors=True)
shutil.rmtree('tools/docutils', ignore_errors=True)
shutil.rmtree('tools/jinja2', ignore_errors=True)
shutil.rmtree('tools/pygments', ignore_errors=True)
shutil.rmtree('tools/sphinx', ignore_errors=True)

which you can see is immediately preceded by a shutil.copytree, which in my case at least copied everything where expected. So maybe hashFiles does something weird. IDK.

with:
name: docs
path: |
cpython/${{ inputs.cpython_release }}/docs
13 changes: 0 additions & 13 deletions release.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,19 +277,6 @@ def tarball(source, clamp_mtime):
print(' %s %8s %s' % (
checksum_xz.hexdigest(), int(os.path.getsize(xz)), xz))

print('Signing tarballs with GPG')
uid = os.environ.get("GPG_KEY_FOR_RELEASE")
if not uid:
print('List of available private keys:')
run_cmd(['gpg -K | grep -A 1 "^sec"'], shell=True)
uid = input('Please enter key ID to use for signing: ')
run_cmd(['gpg', '-bas', '-u', uid, tgz])
run_cmd(['gpg', '-bas', '-u', uid, xz])

print('Signing tarballs with Sigstore')
run_cmd(['python3', '-m', 'sigstore', 'sign',
'--oidc-disable-ambient-providers', tgz, xz], shell=False)


def export(tag, silent=False):
make_dist(tag.text)
Expand Down
152 changes: 111 additions & 41 deletions run_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,6 @@ def check_tool(db: DbfilenameShelf, tool: str) -> None:


check_git = functools.partial(check_tool, tool="git")
check_latexmk = functools.partial(check_tool, tool="latexmk")
check_make = functools.partial(check_tool, tool="make")
check_blurb = functools.partial(check_tool, tool="blurb")
check_autoconf = functools.partial(check_tool, tool="autoconf")
Expand Down Expand Up @@ -474,39 +473,63 @@ def create_tag(db: DbfilenameShelf) -> None:
)


def build_release_artifacts(db: DbfilenameShelf) -> None:
with cd(db["git_repo"]):
release_mod.export(db["release"])


def test_release_artifacts(db: DbfilenameShelf) -> None:
with tempfile.TemporaryDirectory() as tmppath:
the_dir = pathlib.Path(tmppath)
the_dir.mkdir(exist_ok=True)
filename = f"Python-{db['release']}"
tarball = f"Python-{db['release']}.tgz"
shutil.copy2(
db["git_repo"] / str(db["release"]) / "src" / tarball,
the_dir / tarball,
)
subprocess.check_call(["tar", "xvf", tarball], cwd=the_dir)
subprocess.check_call(
["./configure", "--prefix", str(the_dir / "installation")],
cwd=the_dir / filename,
)
subprocess.check_call(["make", "-j"], cwd=the_dir / filename)
subprocess.check_call(["make", "install", "-j"], cwd=the_dir / filename)
process = subprocess.run(
["./bin/python3", "-m", "test", "-uall"],
cwd=str(the_dir / "installation"),
text=True,
)
def wait_for_source_and_docs_artifacts(db: DbfilenameShelf) -> None:
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This task I'd like feedback if this matches expectations of release managers, I saw there was another section that has "waiting" similar to this.

# Determine if we need to wait for docs or only source artifacts.
release_tag = db["release"]
should_wait_for_docs = release_tag.is_final or release_tag.is_release_candiate

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

if not ask_question("Some test_failed! Do you want to continue?"):
raise ReleaseException("Test failed!")
# Build the list of filepaths we're expecting.
wait_for_paths = [
release_path / "src" / f"Python-{release_tag}.tgz",
release_path / "src" / f"Python-{release_tag}.tar.xz"
]
if should_wait_for_docs:
docs_path = release_path / "docs"
wait_for_paths.extend([
docs_path / f"python-{release_tag}-docs.epub",
docs_path / f"python-{release_tag}-docs-html.tar.bz2",
docs_path / f"python-{release_tag}-docs-html.zip",
docs_path / f"python-{release_tag}-docs-pdf-a4.tar.bz2",
docs_path / f"python-{release_tag}-docs-pdf-a4.zip",
docs_path / f"python-{release_tag}-docs-pdf-letter.tar.bz2",
docs_path / f"python-{release_tag}-docs-pdf-letter.zip",
docs_path / f"python-{release_tag}-docs-texinfo.tar.bz2",
docs_path / f"python-{release_tag}-docs-texinfo.zip",
docs_path / f"python-{release_tag}-docs-text.tar.bz2",
docs_path / f"python-{release_tag}-docs-text.zip",
])

print(f"Waiting for source{' and docs' if should_wait_for_docs else ''} artifacts to be built")
print(f"Artifacts should be placed at '{release_path}':")
for path in wait_for_paths:
print(f"- '{os.path.relpath(path, release_path)}'")

while not all(path.exists() for path in wait_for_paths):
time.sleep(1)
Comment on lines +511 to +512
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should exit if after a certain amount of seconds some files are still missing (e.g. because they haven't been created due to some error)?

Copy link
Collaborator Author

@sethmlarson sethmlarson Dec 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This follows the same model as the "waiting for binary installers" routine which waited forever. I'm not sure how release managers would like to use this tool so would need feedback if there's a preference.



def sign_source_artifacts(db: DbfilenameShelf) -> None:
print('Signing tarballs with GPG')
uid = os.environ.get("GPG_KEY_FOR_RELEASE")
if not uid:
print('List of available private keys:')
subprocess.check_call('gpg -K | grep -A 1 "^sec"', shell=True)
uid = input('Please enter key ID to use for signing: ')

tarballs_path = pathlib.Path(db["git_repo"] / str(db["release"]) / "src")
tgz = str(tarballs_path / ("Python-%s.tgz" % db["release"]))
xz = str(tarballs_path / ("Python-%s.tar.xz" % db["release"]))

subprocess.check_call(['gpg', '-bas', '-u', uid, tgz])
subprocess.check_call(['gpg', '-bas', '-u', uid, xz])

print('Signing tarballs with Sigstore')
subprocess.check_call(['python3', '-m', 'sigstore', 'sign',
'--oidc-disable-ambient-providers', tgz, xz])


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


def start_build_of_source_and_docs(db: DbfilenameShelf) -> None:
# Get the git commit SHA for the tag
commit_sha = subprocess.check_output(
["git", "rev-list", "-n", "1", db["release"].gitname],
cwd=db["git_repo"]
).decode().strip()

# Get the owner of the GitHub repo (first path segment in a 'github.com' remote URL)
# This works for both 'https' and 'ssh' style remote URLs.
origin_remote_url = subprocess.check_output(
["git", "ls-remote", "--get-url", "origin"],
cwd=db["git_repo"]
).decode().strip()
match = re.match(r"github\.com/([^/]+)/", origin_remote_url)
if not match:
raise ReleaseException(f"Could not parse GitHub owner from 'origin' remote URL: {origin_remote_url}")
origin_remote_github_owner = match.group(1)

# We ask for human verification at this point since this commit SHA is 'locked in'
print()
print(f"Go to https://github.com/{origin_remote_github_owner}/cpython/commit/{commit_sha}")
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are extra steps for ensuring that the commit SHA isn't on a fork and that there haven't been any unexpected upstream changes to the fork since the local commit.

print("- Ensure that there is no warning that the commit does not belong to this repository.")
print("- Ensure that the commit diff does not contain any unexpected changes.")
print("- For the next step, ensure the commit SHA matches the one you verified on GitHub in this step.")
print()
if not ask_question(
"Have you verified the release commit hasn't been tampered with on GitHub?"
):
raise ReleaseException("Commit must be visually reviewed before starting build")

# After visually confirming the release manager can start the build process
# with the known good commit SHA.
print()
print("Go to https://github.com/python/release-tools/actions/workflows/source-and-docs-release.yml")
print("Select 'Run workflow' and enter the following values:")
print(f"- Git remote to checkout: {origin_remote_github_owner}")
print(f"- Git commit to target for the release: {commit_sha}")
print(f"- CPython release number: {db['release']}")
print()

if not ask_question(
"Have you started the source and docs build?"
):
raise ReleaseException("Source and docs build must be started")


def send_email_to_platform_release_managers(db: DbfilenameShelf) -> None:
if not ask_question(
"Have you notified the platform release managers about the availability of artifacts?"
"Have you notified the platform release managers about the availability of the commit SHA and tag?"
):
raise ReleaseException("Platform release managers muy be notified")

Expand Down Expand Up @@ -1040,9 +1109,9 @@ def _api_key(api_key):
)
args = parser.parse_args()
auth_key = args.auth_key or os.getenv("AUTH_INFO")
assert isinstance(auth_key, str), "We need an AUTH_INFO env var or --auth-key"
tasks = [
Task(check_git, "Checking git is available"),
Task(check_latexmk, "Checking latexmk is available"),
Task(check_make, "Checking make is available"),
Task(check_blurb, "Checking blurb is available"),
Task(check_docker, "Checking docker is available"),
Expand All @@ -1061,18 +1130,19 @@ def _api_key(api_key):
Task(bump_version, "Bump version"),
Task(check_cpython_repo_is_clean, "Checking git repository is clean"),
Task(create_tag, "Create tag"),
Task(build_release_artifacts, "Building release artifacts"),
Task(test_release_artifacts, "Test release artifacts"),
Task(push_to_local_fork, "Push new tags and branches to private fork"),
Task(start_build_of_source_and_docs, "Start the builds for source and docs artifacts"),
Task(
send_email_to_platform_release_managers,
"Platform release managers have been notified of the commit SHA",
),
Task(wait_for_source_and_docs_artifacts, "Wait for source and docs artifacts to build"),
Task(build_sbom_artifacts, "Building SBOM artifacts"),
Task(sign_source_artifacts, "Sign source artifacts"),
Task(upload_files_to_server, "Upload files to the PSF server"),
Task(place_files_in_download_folder, "Place files in the download folder"),
Task(upload_docs_to_the_docs_server, "Upload docs to the PSF docs server"),
Task(unpack_docs_in_the_docs_server, "Place docs files in the docs folder"),
Task(push_to_local_fork, "Push new tags and branches to private fork"),
Task(
send_email_to_platform_release_managers,
"Platform release managers have been notified of the release artifacts",
),
Task(wait_util_all_files_are_in_folder, "Wait until all files are ready"),
Task(create_release_object_in_db, "The django release object has been created"),
Task(post_release_merge, "Merge the tag into the release branch"),
Expand Down