Skip to content

chore: add Kokoro release config and script for OSS Exit Gate - #1102

Open
zarturod wants to merge 1 commit into
googleads:mainfrom
zarturod:chore/oss-exit-gate-kokoro-release
Open

zarturod wants to merge 1 commit into
googleads:mainfrom
zarturod:chore/oss-exit-gate-kokoro-release

Conversation

@zarturod

Copy link
Copy Markdown

Summary

Adds .kokoro/release.cfg and .kokoro/release.sh to migrate google-ads-python releases (PyPI (google-ads)) to OSS Exit Gate (google-ads-python/pypi) via the BAM DevRel Kokoro RBE release pool:

  • Builds release artifacts in the Kokoro release container (ubuntu2204/full:current).
  • Stages built artifacts to the internal OSS Exit Gate Artifact Registry staging repository (oss-exit-gate-prod).
  • Supports DRY_RUN=true (default) to stage and validate artifacts without publishing, and DRY_RUN=false to upload manifest.json to gs://oss-exit-gate-prod-projects-bucket/google-ads-python/pypi/manifests to trigger automated BCID verification and production release.

Related Internal CLs

  • Exit Gate Project Config (google-ads-python/pypi): http://cl/1009120863 (submitted) & http://cl/1009709848
  • Kokoro Release Job Config (ads-api-devrel/bam-devrel/aim/google-ads-python-release.cfg): http://cl/1009766585

@zarturod
zarturod requested review from a team as code owners October 29, 2026 16:59
Comment thread .kokoro/release.sh
#!/bin/bash
set -euo pipefail

# Default to dry-run for safety unless DRY_RUN=false is explicitly passed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth being explicit in the header that DRY_RUN=true is not side-effect free: it still deletes any existing google-ads package in the Exit Gate staging repo and re-uploads. Someone reading "dry run" will assume read-only.

Also consider normalizing case so a True from the Kokoro UI doesn't hard-fail:

Suggested change
# Default to dry-run for safety unless DRY_RUN=false is explicitly passed.
# Default to dry-run for safety unless DRY_RUN=false is explicitly passed.
# NOTE: DRY_RUN=true still builds, wipes the staging package in Artifact
# Registry, and re-uploads; it only skips the manifest upload that triggers
# the external publish.
DRY_RUN="${DRY_RUN:-true}"
DRY_RUN="${DRY_RUN,,}"

Comment thread .kokoro/release.sh
echo "=== Installing pyenv Python version (if needed): ${PYENV_VER} ==="
pyenv install -s "${PYENV_VER}"

echo "=== Activating pyenv Python version: ${PYENV_VER} ==="

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: pyenv global mutates container-wide state. export PYENV_VERSION="${PYENV_VER}" (or pyenv shell) scopes it to this process and is one less thing to reason about if the image is ever reused.

Also note pyenv install -s 3.12 only resolves the bare 3.12 prefix on pyenv ≥ 2.3.x; fine for ubuntu2204/full:current today, but if this ever fails with "definition not found" that's why.

Comment thread .kokoro/release.sh
echo "=== Activating pyenv Python version: ${PYENV_VER} ==="
pyenv global "${PYENV_VER}"

python3 -m venv .venv

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: creating the venv inside the repo root means it's sitting next to pyproject.toml when build runs. setuptools won't pick it up for the wheel, but keeping it out of the tree avoids any surprises with sdist/egg-info globbing:

Suggested change
python3 -m venv .venv
VENV_DIR="${KOKORO_ARTIFACTS_DIR:-/tmp}/release-venv"
python3 -m venv "${VENV_DIR}"
source "${VENV_DIR}/bin/activate"

Comment thread .kokoro/release.sh
rm -rf dist/ build/ *.egg-info

echo "=== Building sdist and wheel ==="
python3 -m build

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This undercuts --require-hashes above. python -m build defaults to an isolated build env and installs whatever pyproject.toml lists in [build-system].requires — which is an unpinned setuptools — fresh from PyPI with no hash check. So the one dependency that actually produces the wheel is the one that isn't pinned.

Suggest building in the already-verified venv instead, and adding setuptools (+ wheel) to requirements.in so they're hash-locked too:

Suggested change
python3 -m build
# --no-isolation: build with the hash-verified setuptools from .kokoro/requirements.txt
# rather than letting `build` fetch an unpinned copy from PyPI.
python3 -m build --no-isolation

While here, it'd be useful to capture the version once so the later steps can use it (see manifest comment below). Python 3.12 has tomllib in the stdlib:

VERSION="$(python3 -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
echo "=== Building google-ads ${VERSION} ==="

Comment thread .kokoro/release.sh
EXIT_GATE_REPOSITORY="google-ads-python--pypi"
PACKAGE_NAME="google-ads"

if gcloud artifacts packages describe "${PACKAGE_NAME}" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I follow why this is here (AR rejects re-uploading an existing version, and Exit Gate normally deletes the package after a successful publish, so anything present is a leftover from a failed/unpublished run). But a short comment saying exactly that would save the next person from thinking it's gratuitously destructive.

Two smaller points:

  • If twine upload fails after this delete, the staging repo is now empty and a previously-staged-but-unpublished version is gone. Acceptable given it's staging, but worth noting.
  • Once the manifest pins version (below), you could narrow this to gcloud artifacts versions delete "${VERSION}" --package=... and leave other versions alone. Not required — whole-package delete is simpler and matches the Java script — just flagging the option.

Comment thread .kokoro/release.sh
{
"packages": [
{
"name": "google-ads"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please pin the version here. Without "version", Exit Gate publishes the newest version of google-ads in the staging repo. Today that's correct only because step 4 wiped the package first — the publish is implicitly relying on a delete succeeding, and if someone later removes/relaxes that step (or two jobs overlap) you can push the wrong artifact to PyPI, which can't be un-published.

Pinning makes the manifest self-describing and the delete step a cleanliness measure rather than a correctness one (this is also what the gVisor Exit Gate release does):

Suggested change
"name": "google-ads"
"name": "google-ads",
"version": "${VERSION}"

Comment thread .kokoro/release.sh
fi

echo "=== Creating targeted release manifest for google-ads ==="
cat <<EOF > manifest.json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: write this somewhere other than the repo root so it can't be left behind in the checkout (and so it's retained as a Kokoro artifact for audit):

MANIFEST_PATH="${KOKORO_ARTIFACTS_DIR:-/tmp}/manifest.json"
cat <<EOF > "${MANIFEST_PATH}"

Comment thread .kokoro/release.sh
EOF

EXIT_GATE_BUCKET="gs://oss-exit-gate-prod-projects-bucket/google-ads-python/pypi/manifests"
MANIFEST_NAME="manifest-$(date +%Y%m%d%H%M%S).json"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: include the version (and the Kokoro build ID when present) in the object name so manifests in the bucket are self-identifying and trivially correlated back to a job. Each upload must still be a unique name since every object triggers a new publish:

Suggested change
MANIFEST_NAME="manifest-$(date +%Y%m%d%H%M%S).json"
MANIFEST_NAME="manifest-${VERSION}-${KOKORO_BUILD_ID:-$(date --utc +%Y%m%d%H%M%S)}.json"

Comment thread .kokoro/release.cfg

container_properties {
docker_image: "us-central1-docker.pkg.dev/kokoro-container-bakery/kokoro/ubuntu/ubuntu2204/full:current"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: consider retaining the built distributions (and the manifest, if moved to KOKORO_ARTIFACTS_DIR) as Kokoro artifacts so there's an auditable copy of exactly what was staged, plus an explicit timeout so a hung auth prompt can't run the default:

Suggested change
}
}
timeout_mins: 30
action {
define_artifacts {
regex: "github/google-ads-python/dist/*"
}
}

Comment thread .kokoro/requirements.in
build
twine
keyrings.google-artifactregistry-auth
requests >= 2.33.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: a one-line comment on why requests has a floor here (CVE? twine compat?) would help whoever next runs pip-compile. And per the --no-isolation suggestion in release.sh, add the build backend so it's hash-locked too:

Suggested change
requests >= 2.33.0
# Floor for <reason>; everything else is unpinned and resolved by pip-compile.
requests >= 2.33.0
# Build backend from pyproject.toml, pinned here so `build --no-isolation`
# uses a hash-verified copy instead of fetching the latest from PyPI.
setuptools
wheel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants