Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .kokoro/release.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Format: //devtools/kokoro/config/proto/build.proto

build_file: "github/google-ads-ruby/.kokoro/release.sh"

# Default to DRY_RUN=true for safety; can be overridden via API or UI
env_vars: {
key: "DRY_RUN"
value: "true"
}

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.

Consider retaining the release inputs as Kokoro artifacts so a published gem can be matched back to the exact build that produced it:

action {
  define_artifacts {
    regex: "github/google-ads-ruby/*.gem"
    regex: "github/google-ads-ruby/manifest*.json"
  }
}

Also worth an explicit timeout_mins so a hung upload fails fast rather than holding a pool slot.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please revert this PR, we are not ready to do this migration.
Pls reach out to anash@ to discuss the specifics.

93 changes: 93 additions & 0 deletions .kokoro/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash
set -euo pipefail
set +x

# Default to dry-run for safety unless DRY_RUN=false is explicitly passed.
DRY_RUN="${DRY_RUN:-true}"
if [[ "${DRY_RUN}" != "true" && "${DRY_RUN}" != "false" ]]; then
echo "ERROR: DRY_RUN must be 'true' or 'false' (got '${DRY_RUN}')." >&2
exit 1
fi

REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${REPO_DIR}"

echo "=== Building and Releasing from: ${REPO_DIR} (DRY_RUN=${DRY_RUN}) ==="

# -----------------------------------------------------------------------------
# 1. Build RubyGem Artifact
# -----------------------------------------------------------------------------
rm -f *.gem
if [[ -f "third_party/rspec/caller_filter.rb" ]]; then
cp third_party/rspec/caller_filter.rb lib/google/ads/google_ads/deprecation.rb
fi
gem build google-ads-googleads.gemspec

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.

gem build here bypasses the repo's own release build, rake build (task :build => [:copy_code, :codegen, :validate_protos, :file_permissions]). The script re-implements copy_third_party_code by hand (lines 21-23) but drops the other two steps that matter for a published gem:

  • file_permissions (chmod -R ugo+r .): the gemspec globs lib/** and gem build preserves file modes, so a restrictive umask on the Kokoro worker would ship a gem whose files aren't world-readable (the historical reason this task exists).
  • validate_protos: the only check that every generated v* module actually loads before we publish.

Suggest either calling the Rake tasks (bundle exec rake copy_code validate_protos file_permissions && gem build ..., or rake build and pick the gem up from pkg/), or at minimum adding chmod -R ugo+r lib and a ruby -Ilib -e 'require "google/ads/google_ads"' smoke test before building. Either way, please log ruby --version / gem --version at the top so a container image bump that changes the Ruby version is visible in the release log.


GEM_FILE="$(ls google-ads-googleads-*.gem | head -n 1)"

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.

The -z check on line 27 is unreachable: with set -e -o pipefail, a no-match ls fails the pipeline, the failed command substitution fails the assignment, and the script exits before the if. The version is also already known from the source of truth, so the glob can go entirely:

Suggested change
GEM_FILE="$(ls google-ads-googleads-*.gem | head -n 1)"
PACKAGE_VERSION="$(ruby -Ilib -e 'require "google/ads/google_ads/version"; puts Google::Ads::GoogleAds::CLIENT_LIB_VERSION')"
PACKAGE_NAME="google-ads-googleads"
GEM_FILE="${PACKAGE_NAME}-${PACKAGE_VERSION}.gem"
if [[ ! -f "${GEM_FILE}" ]]; then
echo "ERROR: expected ${GEM_FILE} after gem build." >&2
exit 1
fi

(and drop lines 31-33). This also guarantees the manifest version matches what the gemspec stamped into the artifact rather than whatever .gem happened to sort first.

if [[ -z "${GEM_FILE}" ]]; then
echo "ERROR: Built .gem file not found." >&2
exit 1
fi
PACKAGE_NAME="google-ads-googleads"
PACKAGE_VERSION="${GEM_FILE#google-ads-googleads-}"
PACKAGE_VERSION="${PACKAGE_VERSION%.gem}"
echo "Built ${GEM_FILE} (package=${PACKAGE_NAME}, version=${PACKAGE_VERSION})"

# -----------------------------------------------------------------------------
# 2. Stage to OSS Exit Gate Ruby Artifact Registry
# -----------------------------------------------------------------------------
EXIT_GATE_PROJECT="oss-exit-gate-prod"
EXIT_GATE_LOCATION="us"
EXIT_GATE_REPOSITORY="google-ads-ruby--rubygems"

if gcloud artifacts packages describe "${PACKAGE_NAME}" \
--project="${EXIT_GATE_PROJECT}" \
--location="${EXIT_GATE_LOCATION}" \
--repository="${EXIT_GATE_REPOSITORY}" &>/dev/null; then
echo "=== Deleting existing package '${PACKAGE_NAME}' from Exit Gate staging repository ==="
gcloud artifacts packages delete "${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.

This deletes every staged version of google-ads-googleads from the prod Exit Gate staging repo, and it runs on every invocation including the default DRY_RUN=true. go/oss-exit-gate-release-rubygems does suggest cleaning AR before a build, but combined with a dry-run default it means: someone kicks a dry run to test the pipeline while a real release is staged-but-not-yet-manifested, and the real artifact is gone. The Exit Gate only needs the version being published to be absent, so I'd scope this to the version being re-staged:

if gcloud artifacts versions describe "${PACKAGE_VERSION}" --package="${PACKAGE_NAME}" ... &>/dev/null; then
  gcloud artifacts versions delete "${PACKAGE_VERSION}" --package="${PACKAGE_NAME}" ... --quiet
fi

and keep the manifest at publish_all: false (as it is) so stray versions can never be published by accident. If you want the full-package wipe as a maintenance step, gate it behind an explicit CLEAN_STAGING=true rather than running it unconditionally.

--project="${EXIT_GATE_PROJECT}" \
--location="${EXIT_GATE_LOCATION}" \
--repository="${EXIT_GATE_REPOSITORY}" \
--quiet
fi

echo "=== Uploading ${GEM_FILE} to Exit Gate Ruby Artifact Registry ==="
set +x
ACCESS_TOKEN="$(gcloud auth print-access-token)"
curl -fsSL -X POST \

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.

Two things here:

  1. The bearer token is passed as a curl argument, so it is visible in /proc/<pid>/cmdline / ps on the worker for the duration of the upload; set +x only hides it from the shell trace (and the set +x on line 56 is a no-op since -x was never enabled). The documented path for this registry is gem push, which reads the token from the environment:
GEM_HOST_API_KEY="Bearer $(gcloud auth print-access-token)" \
  gem push "${GEM_FILE}" --host "https://us-ruby.pkg.dev/${EXIT_GATE_PROJECT}/${EXIT_GATE_REPOSITORY}"

(see go/oss-exit-gate-release-rubygems "Update Your Build Script to Publish to AR"). That also removes the hand-built /api/v1/gems URL and gives you RubyGems' own error reporting.

  1. If you keep curl, replace -f with --fail-with-body (curl 7.76+, present on 22.04): -f discards the response body on 4xx/5xx, which is exactly the AR error message you'll need when a release fails at 2am.

-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary @"${GEM_FILE}" \
"https://us-ruby.pkg.dev/${EXIT_GATE_PROJECT}/${EXIT_GATE_REPOSITORY}/api/v1/gems"
unset ACCESS_TOKEN

# -----------------------------------------------------------------------------
# 3. Trigger Exit Gate Release via GCS Manifest
# -----------------------------------------------------------------------------
if [[ "${DRY_RUN}" == "true" ]]; then
echo "=== DRY_RUN is enabled. Skipping manifest upload to Exit Gate. ==="
exit 0
fi

cat <<EOF > manifest.json
{
"publish_all": false,
"publishing_groups": [
{
"packages": [
{
"name": "${PACKAGE_NAME}",
"version": "${PACKAGE_VERSION}"
}
]
}
]
}
EOF

EXIT_GATE_BUCKET="gs://oss-exit-gate-prod-projects-bucket/google-ads-ruby/rubygems/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 in the manifest object name (manifest-${PACKAGE_VERSION}-$(date -u +%Y%m%dT%H%M%SZ).json) so the GCS listing is self-describing when auditing which manifest triggered which release, and use UTC so timestamps don't depend on the worker's TZ.


echo "=== Uploading manifest to ${EXIT_GATE_BUCKET}/${MANIFEST_NAME} ==="
gcloud storage cp manifest.json "${EXIT_GATE_BUCKET}/${MANIFEST_NAME}"
Loading