Skip to content

Major Refactoring of Azure DevOps Pipelines - #26008

Merged
Changming Sun (snnn) merged 4 commits into
mainfrom
users/snnn/rel1232-main
Sep 15, 2025
Merged

Changming Sun (snnn) merged 4 commits into
mainfrom
users/snnn/rel1232-main

Conversation

@snnn

@snnn Changming Sun (snnn) commented Sep 10, 2025 •

Copy link
Copy Markdown
Contributor

This pull request introduces a significant refactoring of the Azure Pipelines CI/CD infrastructure. The primary goals of these changes are to:

  1. Solve the problem that vcpkg/cmake version can get changed when the CI build machine image changes, which can make pipeline suddenly broken and interrupt our release process.
  2. Reduce the Zip-Nuget-Java-Nodejs Packaging Pipeline's running time by changing how macOS universal2 binaries are built.
  3. Add the support for RC releases for Java packages.

Key Changes:

1. Standardized Build Tool Setup (setup-build-tools.yml)

A new reusable template, setup-build-tools.yml, has been created to centralize the setup of essential build tools.

  • Pinned Dependencies: This new template allows us to pin specific versions of cmake and vcpkg, which was not previously possible. This ensures a more stable and predictable build environment across all pipelines.
  • Reduced Redundancy: By consolidating the setup logic into a single template, we have significantly reduced code duplication across numerous pipeline files, making them cleaner and easier to maintain.

Currently this file is only used in macOS and Windows pipelines since most Linux pipelines use docker to manage their environment.

2. Reworked macOS Universal Binary Build Process

The methodology for building macOS universal2 binaries has been fundamentally changed to improve reliability and flexibility.

  • Python Packaging: The Python packaging pipeline will no longer produce universal2 wheels. Instead, it will generate separate wheels for x86_64 and arm64 architectures.
  • NuGet C-API Packaging: The NuGet C-API pipeline has been updated to first build the x86_64 and arm64 binaries independently. These single-architecture binaries are then combined to create the final universal package, rather than building a universal2 package in a single pass.

The change is made mainly because:

  • Building for both ARCHs in a single pass is too slow, which may take about 5 hours in the ADO machine pool.
  • A lot of MLAS features are disabled when ORT is built in such a way.

3. Java Packaging and Testing Overhaul

The Download_Java_Tools stage in "Zip-Nuget-Java-Nodejs Packaging Pipeline" is deleted because it is no longer used. Previously it was added to reduce the times of downloading the same java packages again and again , which was to reduce download errors. Now we have setup a private ADO feed for this.

Besides, there are some major changes to the pipeline that:

  1. MD5 and SHA1 checksum files are provided along with the java package files instead of SHA256. This is because Sonatype's official docs says MD5/SHA1 checkcums are required while the others are optional. See: https://central.sonatype.org/publish/requirements/#supply-javadoc-and-sources . Now the publishing would fail if we don't have the MD5/SHA1 checksum files.
  2. The format of the checksum files is changed. Previously we used Linux's sha256sum command to generate such files, so each checksum file contains a hash value and a filename in the file content. However, it was not the expected format. Sonatype expects that the file only contains a hash value. This PR fixes the issue.
  3. A few powershell scripts were rewritten in python to improve error check and robustness
  4. Added the support for generating RC packages. Previously we had to manually modify the version numbers and manually do GPG sign.
  5. Two new files jar-packaging.yml and setup-maven.yml were added. We will use maven to fetch dependent packages(instead of directly HTTP fetching) to improve supply chain security, because maven allows us using a private feed to do so.

4. Dockerfile Enhancements

The Dockerfiles used in the CI have been updated to use a BASEIMAGE argument. This makes them more flexible, allowing the base image to be specified at build time, which simplifies maintenance and updates. It will allow us to using different base image repos in different CI environments. In the future we will change the Github Actions to only fetch base images from public docker repos. Meanwhile, ADO packaging pipelines will continue to use private repos.

5. Improved Release Management

The run_packaging_pipelines.py script has been updated to provide more robust and explicit control over
the package versioning for different build scenarios. This clarifies the process for generating nightly,
release candidate (RC), and final release packages.

The script now handles three distinct cases for package versioning:

  • Nightly Packages: For regular CI builds (e.g., on the main branch), the script triggers the packaging
    pipelines in "nightly" mode. This sets the IsReleaseBuild parameter to false and results in packages with
    a development version suffix (e.g., 1.2.3-dev-20250909-abcdef).

  • Release Candidate (RC) Builds: To create a pre-release or RC build, the script is run with the
    --build-mode release flag, along with the --pre-release-suffix-string (e.g., rc) and
    --pre-release-suffix-number (e.g., 1) arguments. This sets the IsReleaseBuild parameter to true and
    passes the suffix information to the pipelines, resulting in a semantically versioned pre-release package
    (e.g., 1.2.3-rc.1).

  • Final Release Builds: For a final release, the script is run with --build-mode release without any
    pre-release suffix arguments. This sets IsReleaseBuild to true, and the resulting package will have a
    clean, final version number (e.g., 1.2.3) based on the VERSION_NUMBER file.

Please note:

  • Java packages still only support the second and third mode.
  • Python packages only support the first and the last mode.

@snnn Changming Sun (snnn) changed the title Major Refactoring of CI/CD Pipelines Major Refactoring of Azure DevOps Pipelines Sep 10, 2025
for tgz in tgz_files:
print(f"Extracting {tgz.name}...")
with tarfile.open(tgz) as tar:
tar.extractall(path=".")

Check failure

Code scanning / CodeQL

Arbitrary file write during tarfile extraction

This file extraction depends on a [potentially untrusted source](1).

Copilot Autofix

AI about 1 year ago

To fix the vulnerability, we should sanitize the paths of all tarfile members before extraction. Specifically, before extracting each file, check that its member path does not contain "..", is not absolute, and does not escape the intended extraction directory. This requires iterating through tarfile members, validating each one, and then extracting them individually.

The recommended fix is:

  • In the block starting with with tarfile.open(tgz) as tar:, replace tar.extractall(path=".") with a loop that iterates over all members, validates each member's path, and extracts only safe entries using tar.extract.
  • To validate, use os.path.isabs(member.name) or ".." in pathlib.Path(member.name).parts (to check for path traversal), and optionally check that the resolved final path remains inside the extraction directory.
  • Make minimal changes to the function in question; don't alter current logic elsewhere.
  • Use only imports already present (i.e., use os, pathlib).

This change will only affect the extraction logic in the shown file tools/ci_build/prepare_macos_package.py, specifically in the main() function.


Suggested changeset 1
tools/ci_build/prepare_macos_package.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/ci_build/prepare_macos_package.py b/tools/ci_build/prepare_macos_package.py
--- a/tools/ci_build/prepare_macos_package.py
+++ b/tools/ci_build/prepare_macos_package.py
@@ -80,7 +80,15 @@
     for tgz in tgz_files:
         print(f"Extracting {tgz.name}...")
         with tarfile.open(tgz) as tar:
-            tar.extractall(path=".")
+            for member in tar.getmembers():
+                # Only extract safe paths: no absolute paths, no '..'
+                member_path = pathlib.Path(member.name)
+                if member_path.is_absolute() or ".." in member_path.parts:
+                    raise ValueError(f"Unsafe path in tar archive: {member.name}")
+                target_path = (staging_dir / member_path).resolve()
+                if not str(target_path).startswith(str(staging_dir)):
+                    raise ValueError(f"Tar extraction escapes staging dir: {member.name}")
+                tar.extract(member, path=staging_dir)
         tgz.unlink()  # Delete the archive
     print("##[endgroup]")
 
EOF
@@ -80,7 +80,15 @@
for tgz in tgz_files:
print(f"Extracting {tgz.name}...")
with tarfile.open(tgz) as tar:
tar.extractall(path=".")
for member in tar.getmembers():
# Only extract safe paths: no absolute paths, no '..'
member_path = pathlib.Path(member.name)
if member_path.is_absolute() or ".." in member_path.parts:
raise ValueError(f"Unsafe path in tar archive: {member.name}")
target_path = (staging_dir / member_path).resolve()
if not str(target_path).startswith(str(staging_dir)):
raise ValueError(f"Tar extraction escapes staging dir: {member.name}")
tar.extract(member, path=staging_dir)
tgz.unlink() # Delete the archive
print("##[endgroup]")

Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread tools/ci_build/run_gh_action.py Fixed
Comment thread tools/ci_build/run_gh_action.py Fixed
Comment thread tools/ci_build/run_gh_action.py Fixed

@github-advanced-security github-advanced-security AI 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.

lintrunner found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

Comment thread tools/ci_build/run_gh_action.py Fixed

print("Writing GnuPG key and passphrase to temporary files.")
private_key_file.write_text(gpg_private_key, encoding="utf-8")
passphrase_file.write_text(gpg_passphrase, encoding="utf-8")

Check failure

Code scanning / CodeQL

Clear-text storage of sensitive information

This expression stores [sensitive data (password)](1) as clear text.

Copilot Autofix

AI about 1 year ago

To address the clear-text storage of sensitive information, we should ensure that the temporary file passphrase.txt containing the GPG passphrase is created with restrictive permissions (owner read/write only, mode 0o600). This reduces the risk of exposure from other local users or processes. Instead of using Path.write_text, which does not set file permissions, we should use os.open with the appropriate mode, then write the passphrase, and close the file. This change should be made in the main function where the temporary file is created and written. No change is needed for the key file (private.key), as it is not flagged as a password. The GPG command should continue to read from the secure file.

This may require importing os (already present). The code block to replace concerns the writing of the passphrase file.

Suggested changeset 1
tools/ci_build/github/windows/sign_java_artifacts.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/ci_build/github/windows/sign_java_artifacts.py b/tools/ci_build/github/windows/sign_java_artifacts.py
--- a/tools/ci_build/github/windows/sign_java_artifacts.py
+++ b/tools/ci_build/github/windows/sign_java_artifacts.py
@@ -101,7 +101,12 @@
 
         print("Writing GnuPG key and passphrase to temporary files.")
         private_key_file.write_text(gpg_private_key, encoding="utf-8")
-        passphrase_file.write_text(gpg_passphrase, encoding="utf-8")
+        # Write the passphrase to the file with restrictive permissions (owner read/write only)
+        passphrase_fd = os.open(passphrase_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+        try:
+            os.write(passphrase_fd, gpg_passphrase.encode("utf-8"))
+        finally:
+            os.close(passphrase_fd)
 
         print("Importing GnuPG private key.")
         run_command([str(gpg_exe_path), "--batch", "--import", str(private_key_file)])
EOF
@@ -101,7 +101,12 @@

print("Writing GnuPG key and passphrase to temporary files.")
private_key_file.write_text(gpg_private_key, encoding="utf-8")
passphrase_file.write_text(gpg_passphrase, encoding="utf-8")
# Write the passphrase to the file with restrictive permissions (owner read/write only)
passphrase_fd = os.open(passphrase_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.write(passphrase_fd, gpg_passphrase.encode("utf-8"))
finally:
os.close(passphrase_fd)

print("Importing GnuPG private key.")
run_command([str(gpg_exe_path), "--batch", "--import", str(private_key_file)])
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread tools/ci_build/run_gh_action.py Fixed
Comment thread tools/ci_build/run_gh_action.py Fixed
Comment thread tools/ci_build/run_gh_action.py Fixed
try:
import semver
except ImportError:
print("Error: The 'semver' package is not installed. Please add it to your requirements.txt.", file=sys.stderr)

Check notice

Code scanning / CodeQL

Use of a print statement at module level

Print statement may execute during import.

Copilot Autofix

AI about 1 year ago

To fix this, the module-level print should be replaced by a logging call, specifically using the logging module, as recommended. We should add an import for logging (if not already present) at the top of the file, and replace line 23 with logging.error(...). This will avoid writing directly to standard out/consoles during import, and allow users to configure log handling. All other print statements in this file (lines 51, 64, 70, 71, 77, 92, 97, 98) are inside functions—not at module level—so only module-level code needs to be changed. Only change file tools/ci_build/github/windows/jar_packaging.py, specifically lines near 23; add import logging after standard imports if necessary, and replace the print.

Suggested changeset 1
tools/ci_build/github/windows/jar_packaging.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/ci_build/github/windows/jar_packaging.py b/tools/ci_build/github/windows/jar_packaging.py
--- a/tools/ci_build/github/windows/jar_packaging.py
+++ b/tools/ci_build/github/windows/jar_packaging.py
@@ -15,12 +15,12 @@
 import sys
 from pathlib import Path
 from typing import Any
-
+import logging
 # Add semver as a dependency
 try:
     import semver
 except ImportError:
-    print("Error: The 'semver' package is not installed. Please add it to your requirements.txt.", file=sys.stderr)
+    logging.error("The 'semver' package is not installed. Please add it to your requirements.txt.")
     sys.exit(1)
 
 # --- Helper Functions for Archiving ---
EOF
@@ -15,12 +15,12 @@
import sys
from pathlib import Path
from typing import Any

import logging
# Add semver as a dependency
try:
import semver
except ImportError:
print("Error: The 'semver' package is not installed. Please add it to your requirements.txt.", file=sys.stderr)
logging.error("The 'semver' package is not installed. Please add it to your requirements.txt.")
sys.exit(1)

# --- Helper Functions for Archiving ---
Copilot is powered by AI and may make mistakes. Always verify output.
@edgchen1

Copy link
Copy Markdown
Contributor

should we also update this react-native-ci.yml job to use an older CMake for the MacOS build?

@snnn

Copy link
Copy Markdown
Contributor Author

should we also update this react-native-ci.yml job to use an older CMake for the MacOS build?

Will address it after this PR.

@snnn

Copy link
Copy Markdown
Contributor Author

Should be fairly easy to migrate the pipeline to GH by employing an AI agent.

@snnn
Changming Sun (snnn) merged commit 323c87a into main Sep 15, 2025
@snnn
Changming Sun (snnn) deleted the users/snnn/rel1232-main branch September 15, 2025 21:06
Changming Sun (snnn) pushed a commit that referenced this pull request Sep 16, 2025
This PR cherry-picks several commits from the main branch to the
rel-1.23.0 release branch as part of the release process.

### Changes included:

*   **Major Refactoring of Azure DevOps Pipelines (#26008)**
    *   Commit: `2e6d7ccfdff55aaf7b0799d7e28b041e607dce2b`
*   **Disables failing test to unblock Python DML Pipeline (#26043)**
    *   Commit: `64c8f40d01bf14b3cf7ac4cf8606ad9e0e56feb0`
*   **Pin cmake version in macOS github Actions (#25998)**
    *   Commit: `148f13cc6b44cae156226cd4e0dcfc154691c5b4`
*   **Bump actions/setup-python from 5 to 6 (#25979)**
    *   Commit: `97a8d332595c974ad24be133df216565493ffb95`
*   **Remove CACHE_URL settings from Github Actions (#25989)**
    *   Commit: `e2a0999ba4b224ab90ef7a8768dd4941fcc19b17`
*   **Bump actions/checkout from 4 to 5 (#25771)**
    *   Commit: `f19215db21f8e1a8fc93090748e455f41076f456`
*   **Bump ruff from 0.12.8 to 0.12.9 (#25772)**
    *   Commit: `78df404871fa2f3fbbb7f1902f9623787ba8dc86`
*   **Bump ruff from 0.12.4 to 0.12.8 (#25713)**
    *   Commit: `7204746e709005d2c7294e7a24d63a2df4a1aee8`
*   **Update macOS target version from 13.3 to 13.4 (#25616)**
    *   Commit: `65bd82564cd31e0acf9139cdd826d08193212c6e`

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Prathik Rao <prathik.rao@gmail.com>
Changming Sun (snnn) pushed a commit that referenced this pull request Sep 16, 2025
This pull request introduces a significant refactoring of the Azure
Pipelines CI/CD infrastructure. The primary goals of these changes are
to:
1. Solve the problem that vcpkg/cmake version can get changed when the
CI build machine image changes, which can make pipeline suddenly broken
and interrupt our release process.
2. Reduce the `Zip-Nuget-Java-Nodejs Packaging Pipeline`'s running time
by changing how macOS universal2 binaries are built.
3. Add the support for RC releases for Java packages.

**1. Standardized Build Tool Setup (`setup-build-tools.yml`)**

A new reusable template, `setup-build-tools.yml`, has been created to
centralize the setup of essential build tools.

* **Pinned Dependencies:** This new template allows us to pin specific
versions of `cmake` and `vcpkg`, which was not previously possible. This
ensures a more stable and predictable build environment across all
pipelines.
* **Reduced Redundancy:** By consolidating the setup logic into a single
template, we have significantly reduced code duplication across numerous
pipeline files, making them cleaner and easier to maintain.

Currently this file is only used in macOS and Windows pipelines since
most Linux pipelines use docker to manage their environment.

**2. Reworked macOS Universal Binary Build Process**

The methodology for building macOS `universal2` binaries has been
fundamentally changed to improve reliability and flexibility.

* **Python Packaging:** The Python packaging pipeline will no longer
produce `universal2` wheels. Instead, it will generate separate wheels
for `x86_64` and `arm64` architectures.
* **NuGet C-API Packaging:** The NuGet C-API pipeline has been updated
to first build the `x86_64` and `arm64` binaries independently. These
single-architecture binaries are then combined to create the final
universal package, rather than building a `universal2` package in a
single pass.

The change is made mainly because:
- Building for both ARCHs in a single pass is too slow, which may take
about 5 hours in the ADO machine pool.
- A lot of MLAS features are disabled when ORT is built in such a way.

**3. Java Packaging and Testing Overhaul**

The Download_Java_Tools stage in "Zip-Nuget-Java-Nodejs Packaging
Pipeline" is deleted because it is no longer used. Previously it was
added to reduce the times of downloading the same java packages again
and again , which was to reduce download errors. Now we have setup a
private ADO feed for this.

Besides, there are some major changes to the pipeline that:

1. MD5 and SHA1 checksum files are provided along with the java package
files instead of SHA256. This is because Sonatype's official docs says
MD5/SHA1 checkcums are required while the others are optional. See:
https://central.sonatype.org/publish/requirements/#supply-javadoc-and-sources
. Now the publishing would fail if we don't have the MD5/SHA1 checksum
files.
2. The format of the checksum files is changed. Previously we used
Linux's sha256sum command to generate such files, so each checksum file
contains a hash value and a filename in the file content. However, it
was not the expected format. Sonatype expects that the file only
contains a hash value. This PR fixes the issue.
3. A few powershell scripts were rewritten in python to improve error
check and robustness
4. Added the support for generating RC packages. Previously we had to
manually modify the version numbers and manually do GPG sign.
5. Two new files `jar-packaging.yml` and `setup-maven.yml` were added.
We will use maven to fetch dependent packages(instead of directly HTTP
fetching) to improve supply chain security, because maven allows us
using a private feed to do so.

**4. Dockerfile Enhancements**

The Dockerfiles used in the CI have been updated to use a `BASEIMAGE`
argument. This makes them more flexible, allowing the base image to be
specified at build time, which simplifies maintenance and updates. It
will allow us to using different base image repos in different CI
environments. In the future we will change the Github Actions to only
fetch base images from public docker repos. Meanwhile, ADO packaging
pipelines will continue to use private repos.

**5. Improved Release Management**

The run_packaging_pipelines.py script has been updated to provide more
robust and explicit control over
the package versioning for different build scenarios. This clarifies the
process for generating nightly,
 release candidate (RC), and final release packages.

 The script now handles three distinct cases for package versioning:

* Nightly Packages: For regular CI builds (e.g., on the main branch),
the script triggers the packaging
pipelines in "nightly" mode. This sets the IsReleaseBuild parameter to
false and results in packages with
      a development version suffix (e.g., 1.2.3-dev-20250909-abcdef).

* Release Candidate (RC) Builds: To create a pre-release or RC build,
the script is run with the
--build-mode release flag, along with the --pre-release-suffix-string
(e.g., rc) and
--pre-release-suffix-number (e.g., 1) arguments. This sets the
IsReleaseBuild parameter to true and
passes the suffix information to the pipelines, resulting in a
semantically versioned pre-release package
      (e.g., 1.2.3-rc.1).

* Final Release Builds: For a final release, the script is run with
--build-mode release without any
pre-release suffix arguments. This sets IsReleaseBuild to true, and the
resulting package will have a
clean, final version number (e.g., 1.2.3) based on the VERSION_NUMBER
file.

Please note:
 - Java packages still only support the second and third mode.
 - Python packages only support the first and the last mode.
This was referenced Sep 16, 2025
Changming Sun (snnn) pushed a commit that referenced this pull request Sep 18, 2025
- **Major Refactoring of Azure DevOps Pipelines (#26008)**
- **Convert QNN x64 pipeline to GH (#26047)**
- **Upgrade com.diffplug.spotless to 7.2.1 in Java build (#26051)**
- **Fix Mac Catalyst build options. (#25970)**
- **Update Podfile.template: update macOS target version from 13.3 to 13.4 (#25699) **
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.

4 participants