Skip to content

chore: migrate xmllint to Python xml.etree.ElementTree in CI scripts and workflows - #14181

Draft
blakeli0 wants to merge 2 commits into
googleapis:mainfrom
blakeli0:chore/migrate-xmllint-to-python
Draft

chore: migrate xmllint to Python xml.etree.ElementTree in CI scripts and workflows#14181
blakeli0 wants to merge 2 commits into
googleapis:mainfrom
blakeli0:chore/migrate-xmllint-to-python

Conversation

@blakeli0

Copy link
Copy Markdown
Contributor

Description

Migrate all xmllint (libxml2-utils) usages across Kokoro scripts and generation tools to Python 3's built-in xml.etree.ElementTree.

This eliminates the need for sudo apt-get install libxml2-utils steps in GitHub Actions workflows, resolving intermittent CI timeouts caused by stalled Ubuntu/Azure package mirrors.

Fixes #14132

Changes

  • .kokoro/common.sh & sdk-platform-java/.kokoro/presubmit/common.sh:
    • parse_pom_version: Extracts project version using Python xml.etree.ElementTree.
    • find_all_poms_with_versioned_dependency: Checks for versioned artifact dependencies using Python.
    • update_pom_dependency: Updates target dependency version nodes in pom.xml via Python.
  • .kokoro/common_test.sh:
    • Updated test verification to assert updated versions with Python instead of xmllint --shell.
  • generation/check_existing_release_versions.sh:
    • Replaced multiple xmllint --xpath calls with a single Python invocation to extract groupId, artifactId, and version.
  • .kokoro/client-library-check.sh & .kokoro/client-library-check-doclet.sh:
    • Replaced replace_java_shared_config_version, replace_java_shared_dependencies_version, and replace_sdk_platform_java_config_version with Python XML modification.
  • .kokoro/presubmit/downstream-build.sh & sdk-platform-java/scripts/create_native_image_test_env.sh:
    • Replaced modify_shared_config, modify_shared_dependencies, and XPath lookups with Python.
  • GitHub Actions Workflows:
    • Removed libxml2-utils / xmllint installation steps in sdk-platform-java-downstream.yaml, versions.yaml, java-shared-config-downstream-maven-plugins.yaml, and java-shared-config-downstream-dependencies.yaml.

…and workflows

Migrate xmllint (libxml2-utils) usages across Kokoro scripts and generation tools to Python 3's built-in xml.etree.ElementTree.

This eliminates the need for installing libxml2-utils in GitHub Actions workflows, resolving CI timeouts caused by transient mirror stalls (fixes googleapis#14132).
@blakeli0
blakeli0 requested review from a team as code owners August 24, 2026 21:52

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request replaces the dependency on xmllint with inline Python scripts using xml.etree.ElementTree to parse and modify Maven pom.xml files across various Kokoro and generation shell scripts. The review feedback highlights two main issues with the new Python implementations: first, ElementTree.parse() discards XML comments (such as license headers) when writing back to files, which can be resolved by using a custom parser with insert_comments=True; second, embedding shell variables like $1 directly inside Python script strings can cause syntax errors, so passing them as command-line arguments via sys.argv is recommended.

Comment thread .kokoro/common.sh Outdated
Comment on lines +495 to +510
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
tree = ET.parse(file)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"

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.

high

By default, xml.etree.ElementTree.parse() discards all XML comments (including crucial Apache license headers at the top of pom.xml files). When the modified tree is written back to the file, these comments are permanently lost.

To preserve comments in Python 3.8+, you can pass a custom parser configured with insert_comments=True to ET.parse().

Suggested change
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
tree = ET.parse(file)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(file, parser=parser)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"

Comment on lines +23 to +38
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
tree = ET.parse(file)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"

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.

high

By default, xml.etree.ElementTree.parse() discards all XML comments (including crucial Apache license headers at the top of pom.xml files). When the modified tree is written back to the file, these comments are permanently lost.

To preserve comments in Python 3.8+, you can pass a custom parser configured with insert_comments=True to ET.parse().

Suggested change
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
tree = ET.parse(file)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"
python3 -c "
import xml.etree.ElementTree as ET, sys
file, artifact_id, new_ver = 'pom.xml', sys.argv[1], sys.argv[2]
ET.register_namespace('', 'http://maven.apache.org/POM/4.0.0')
parser = ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
tree = ET.parse(file, parser=parser)
root = tree.getroot()
updated = False
for elem in root.iter():
art = elem.find('{*}artifactId')
ver = elem.find('{*}version')
if art is not None and art.text == artifact_id and ver is not None:
ver.text = new_ver
updated = True
if updated:
tree.write(file, encoding='utf-8', xml_declaration=True)
" "$2" "$3"

Comment thread .kokoro/common.sh Outdated
Comment on lines +558 to +571
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse('$1/pom.xml').getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(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.

medium

Using shell expansion ($1) directly inside the Python script string can lead to syntax errors or unexpected behavior if the directory path contains spaces, single quotes, or other special characters. It is much safer and more robust to pass the file path as a command-line argument to the Python script and access it via sys.argv.

Suggested change
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse('$1/pom.xml').getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(1)
")
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse(sys.argv[1]).getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(1)
" "$1/pom.xml")

Comment on lines +86 to +99
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse('$1/pom.xml').getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(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.

medium

Using shell expansion ($1) directly inside the Python script string can lead to syntax errors or unexpected behavior if the directory path contains spaces, single quotes, or other special characters. It is much safer and more robust to pass the file path as a command-line argument to the Python script and access it via sys.argv.

Suggested change
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse('$1/pom.xml').getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(1)
")
result=$(python3 -c "
import xml.etree.ElementTree as ET, sys
try:
root = ET.parse(sys.argv[1]).getroot()
v = root.find('{*}version')
if v is None:
v = root.find('{*}parent/{*}version')
if v is not None and v.text:
print(v.text.strip())
else:
sys.exit(1)
except Exception:
sys.exit(1)
" "$1/pom.xml")

… script

Extract XML parsing and manipulation logic into standalone pom_utils.py scripts with dedicated CLI commands (get-version, has-versioned-dep, get-dep-version, update-dep-version, update-property, get-coordinates) for cleaner readability and reusability.
@blakeli0
blakeli0 marked this pull request as draft August 24, 2026 22:53
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.

Github CI failing when trying to install xmllint

1 participant