Skip to content

Continuous integration build & test #32

Continuous integration build & test

Continuous integration build & test #32

# Summary: TFQ continuous integration workflow for building & testing TFQ.
#
# This workflow compiles TFQ and runs test cases to verify everything works.
# It triggers on certain events such as pull requests and merge-queue merges,
# tries to be as efficient as possible by caching the Python environment and
# Bazel artifacts, and can be invoked manually via the "Run workflow" button at
# https://github.com/tensorflow/quantum/actions/workflows/ci-build-checks.yaml
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
name: CI build checks
run-name: Continuous integration build & test
on:
pull_request:
types: [opened, synchronize]
branches:
- master
merge_group:
types:
- checks_requested
push:
branches:
- master
# Allow manual invocation, with options that can be useful for debugging.
workflow_dispatch:
inputs:
sha:
description: 'SHA of commit to run against:'
type: string
required: true
python_ver:
description: 'Python version:'
type: string
default: '3.10.15'
extra_bazel_options:
description: 'Extra Bazel options:'
type: string
remake_python_cache:
description: 'Delete & remake the Python cache'
type: boolean
default: false
debug:
description: 'Print additional workflow info'
type: boolean
default: false
env:
# Default Python version to use. Important: give it a full x.y.z number.
python_ver: '3.10.15'
# Additional .bazelrc options to use.
bazelrc_additions: |
common -c opt
common --announce_rc
common --color=no
common --experimental_repo_remote_exec
common --remote_upload_local_results=false
common --cxxopt="-D_GLIBCXX_USE_CXX11_ABI=1"
common --cxxopt="-std=c++17"
common --cxxopt="-msse2"
common --cxxopt="-msse3"
common --cxxopt="-msse4"
build --verbose_failures
build --auto_output_filter=none
build --show_progress_rate_limit=1
test --test_output=errors
test --test_summary=detailed
# Note: these regexes are Bash regex syntax, NOT path glob syntax.
# Also, do not put dashes in front of the values.
ignore_patterns: |-
.*\.md$
.*\.jpg$
.*\.png$
^\.gitignore
^\.pylintrc
^\.yamllint.yaml
^\.github/problem-matchers/.*
^benchmarks/.*
concurrency:
# Cancel any previously-started but still active runs on the same branch.
cancel-in-progress: true
group: ${{github.workflow}}-${{github.event.pull_request.number||github.ref}}
jobs:
# Summary of basic strategy:
# 1. Job "Decision" quickly determines if the rest of the workflow needs
# to run. A run is needed if (a) this was triggered by a merge_queue
# event, (b) the event changed files that we don't ignore, or (c) it
# was invoked manually via workflow_dispatch.
#
# 2. If the workflow needs to proceed, job Setup installs Python
# dependencies and caches them.
#
# 3. Job Build then builds the Python wheel for TFQ. As a side-effect,
# Bazel caches some build artifacts, potentially saving time in
# the remaining jobs as well as in future runs of the workflow.
#
# 4. Jobs Wheel_tests, Bazel_tests, and Tutorial_tests can all run in
# parallel after Build finishes. Bazel_tests also uses and updates the
# Bazel cache artifacts, potentially saving time in future runs.
Decision:
runs-on: ubuntu-24.04
outputs:
need_run: >-
${{github.event_name == 'merge_queue' ||
steps.files.outputs.have_changes == 'true'}}
steps:
- if: github.event_name != 'merge_queue'
name: Determine files changed by this ${{github.event_name}} event
id: files
env:
GH_TOKEN: ${{github.token}}
# Note that this approach doesn't need to check out a copy of the repo.
run: |
set -x +e
# shellcheck disable=SC2207
# Get an array of paths changed in this workflow trigger event.
if [[ "${{github.event_name}}" == "pull_request" ]]; then
url=${{github.event.pull_request.url}}
paths=($(gh pr view $url --json files --jq '.files | .[].path'))
else
# There's no event sha for manual runs, so we rely on user input.
# Make sure the sha is valid.
if [[ "${{github.event_name}}" == "workflow_dispatch" ]]; then
url="repos/${{github.repository}}/commits/${{inputs.sha}}"
full_sha="$(gh api $url -q '.sha')"
exit_code=$?
if [[ "$exit_code" == "0" ]]; then
sha=$full_sha
else
{
echo "### :x: Workflow error"
echo "The SHA provided to _Run Workflow_ does not exist:"
echo "<code>${{inputs.sha}}</code>"
} >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
else
sha=${{github.sha}}
fi
url="repos/${{github.repository}}/commits/$sha"
# shellcheck disable=SC2086
paths=($(gh api $url --jq '.files[].filename'))
fi
# Test array of paths against the patterns of changes we can ignore.
# Default to no-changes if every path matches at least one pattern.
echo 'have_changes=false' >> "$GITHUB_OUTPUT"
ignorable=(${{env.ignore_patterns}})
for path in "${paths[@]}"; do
for pattern in "${ignorable[@]}"; do
# The path matched a pattern => can be ignored. Go to next path.
[[ $path =~ $pattern ]] && continue 2
done
# None of the patterns matched this path.
echo 'have_changes=true' >> "$GITHUB_OUTPUT"
break
done
Setup:
if: needs.Decision.outputs.need_run == 'true'
needs: Decision
runs-on: ubuntu-20.04
timeout-minutes: 15
outputs:
python_cache_key: ${{steps.parameters.outputs.python_cache_key}}
python_cache_paths: ${{steps.parameters.outputs.python_cache_paths}}
bazel_cache_key: ${{steps.parameters.outputs.bazel_cache_key}}
debug: ${{steps.parameters.outputs.debug}}
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
# Note: setup-python has a cache facility, but we don't use it here
# because we want to cache more Python things than setup-python does.
- name: Set up Python ${{inputs.python_ver || env.python_ver}}
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Set cache keys and other parameters
id: parameters
run: |
#
# Including __init__.py here lets us detect changes to __version__.
hash=${{hashFiles('WORKSPACE', '**/BUILD', '**/*.bzl', '**/.patch',
'requirements.txt', 'tensorflow_quantum/__init__.py')}}
key="${{github.workflow_ref}}-$hash"
# shellcheck disable=SC2129
echo "python_cache_key=$key" >> "$GITHUB_OUTPUT"
# shellcheck disable=SC2005
# The paths used for actions/cache need to be on separate lines.
{
echo 'python_cache_paths<<EOF'
echo "$(pip cache dir)"
echo "${{env.pythonLocation}}"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
# Make the Bazel disk cache specific to the version of this workflow.
echo "bazel_cache_key=${{github.workflow_ref}}" >> "$GITHUB_OUTPUT"
# If the user re-runs the workflow with debugging turned on via the
# GitHub GUI (instead of using workflow_dispatch), include the debug
# info they'd get if they did use workflow_dispatch.
if [[ "${{inputs.debug}}" == "true" ||
"${{runner.debug}}" == "1" ]]; then
echo "debug=true" >> "$GITHUB_OUTPUT"
else
echo "debug=false" >> "$GITHUB_OUTPUT"
fi
- name: Test if the cache already exists
uses: actions/cache@v4
id: check_cache
with:
lookup-only: true
key: ${{steps.parameters.outputs.python_cache_key}}
path: ${{steps.parameters.outputs.python_cache_paths}}
- if: >-
steps.check_cache.outputs.cache-hit == 'true' &&
inputs.remake_python_cache == 'true'
name: Clear the Python cache
continue-on-error: true
env:
GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
key="${{steps.parameters.outputs.python_cache_key}}"
gh extension install actions/gh-actions-cache
gh actions-cache delete "$key" --confirm
- if: >-
steps.check_cache.outputs.cache-hit != 'true' ||
inputs.remake_python_cache == 'true'
name: Set up the Python cache
uses: actions/cache@v4
id: restore_cache
with:
key: ${{steps.parameters.outputs.python_cache_key}}
path: ${{steps.parameters.outputs.python_cache_paths}}
- if: >-
steps.check_cache.outputs.cache-hit != 'true' ||
inputs.remake_python_cache == 'true'
name: Install TFQ Python dependencies and cache them
run: |
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
# The next ones are for validating tutorials
pip install jupyter
pip install nbclient==0.6.5 jupyter-client==6.1.12 ipython==7.22.0
pip install ipykernel==5.1.1
pip install gym==0.24.1
pip install seaborn==0.12.0
pip install -q git+https://github.com/tensorflow/docs
Build_wheel:
if: needs.Decision.outputs.need_run == 'true'
name: Build Python wheel
needs: [Decision, Setup]
runs-on: ubuntu-20.04
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Restore our Python cache
uses: actions/cache@v4
with:
key: ${{needs.Setup.outputs.python_cache_key}}
path: ${{needs.Setup.outputs.python_cache_paths}}
fail-on-cache-miss: true
- name: Set up Bazel
uses: bazel-contrib/[email protected]
# Note that we don't need to set the Bazel version to use, because it
# knows to use what's in the .bazel-version file.
with:
bazelrc: ${{env.bazelrc_additions}}
# The next 3 caches can be shared between all workflow runs.
bazelisk-cache: true
external-cache: true
repository-cache: true
disk-cache: ${{needs.Setup.outputs.bazel_cache_key}}
- name: Build Python wheel for TFQ
run: |
set -x -o pipefail
printf "Y\n" | ./configure.sh
bazel build ${{inputs.extra_bazel_options}} \
release:build_pip_package 2>&1 | tee bazel-build.log
mkdir -p ./wheel
./bazel-bin/release/build_pip_package \
"$(pwd)/wheel" 2>&1 | tee python-bdist.log
pip install -U ./wheel/*.whl
- name: Save the wheel for the tutorial tests
uses: actions/upload-artifact@v4
with:
name: wheel-${{github.run_id}}
path: ./wheel
compression-level: 0
overwrite: true
- if: failure() || needs.Setup.outputs.debug == 'true'
name: Make Bazel artifacts downloadable for analysis
uses: actions/upload-artifact@v4
with:
name: bazel-build-artifacts-${{github.run_id}}
retention-days: 14
compression-level: 9
include-hidden-files: true
path: |
bazel-build.log
python-bdist.log
/home/runner/.bazel/execroot/__main__/bazel-out/
!/home/runner/.bazel/execroot/__main__/bazel-out/**/*.so
!/home/runner/.bazel/execroot/__main__/bazel-out/**/*.o
!/home/runner/.bazel/execroot/__main__/bazel-out/**/_objs
!/home/runner/.bazel/execroot/__main__/bazel-out/**/_solib_k8
Wheel_tests:
if: needs.Decision.outputs.need_run == 'true'
name: Test the Python wheel
needs: [Decision, Setup, Build_wheel]
runs-on: ubuntu-20.04
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Restore our Python cache
uses: actions/cache@v4
with:
key: ${{needs.Setup.outputs.python_cache_key}}
path: ${{needs.Setup.outputs.python_cache_paths}}
fail-on-cache-miss: true
- name: Get the Python wheel we built
uses: actions/download-artifact@v4
with:
name: wheel-${{github.run_id}}
path: ./wheel
- name: Install the Python wheel
run: |
pip install ./wheel/*.whl
- name: Test the wheel
run: |
set -x +e
./scripts/run_example.sh
Bazel_tests:
if: needs.Decision.outputs.need_run == 'true'
name: Test the rest of TFQ
needs: [Decision, Setup, Build_wheel]
runs-on: ubuntu-20.04
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Restore our Python cache
uses: actions/cache@v4
with:
key: ${{needs.Setup.outputs.python_cache_key}}
path: ${{needs.Setup.outputs.python_cache_paths}}
fail-on-cache-miss: true
- name: Set up Bazel
uses: bazel-contrib/[email protected]
with:
bazelrc: ${{env.bazelrc_additions}}
bazelisk-cache: true
external-cache: true
repository-cache: true
disk-cache: ${{needs.Setup.outputs.bazel_cache_key}}
- name: Run all Bazel tests
id: test
run: |
set -x +e -o pipefail
printf "Y\n" | ./configure.sh
bazel test ${{inputs.extra_bazel_options}} \
//tensorflow_quantum/... 2>&1 | tee bazel-tests.log
- if: failure() || needs.Setup.outputs.debug == 'true'
name: Make Bazel artifacts downloadable for analysis
uses: actions/upload-artifact@v4
with:
name: bazel-tests-${{github.run_id}}
retention-days: 7
compression-level: 9
include-hidden-files: true
path: |
bazel-tests.log
/home/runner/.bazel/execroot/__main__/bazel-out/
!/home/runner/.bazel/execroot/__main__/bazel-out/**/*.so
!/home/runner/.bazel/execroot/__main__/bazel-out/**/*.o
!/home/runner/.bazel/execroot/__main__/bazel-out/**/_objs
!/home/runner/.bazel/execroot/__main__/bazel-out/**/_solib_k8
Tutorial_tests:
if: needs.Decision.outputs.need_run == 'true'
name: Test the tutorials
runs-on: ubuntu-20.04
needs: [Decision, Setup, Build_wheel]
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Restore our Python cache
uses: actions/cache@v4
with:
key: ${{needs.Setup.outputs.python_cache_key}}
path: ${{needs.Setup.outputs.python_cache_paths}}
fail-on-cache-miss: true
- name: Get the Python wheel we built
uses: actions/download-artifact@v4
with:
name: wheel-${{github.run_id}}
path: ./wheel
- name: Install the Python wheel
run: |
pip install ./wheel/*.whl
- name: Test the tutorials
run: |
cd ..
examples_output=$(python3 quantum/scripts/test_tutorials.py)
exit_code=$?
if [ "$exit_code" != "0" ]; then
echo 'Tutorials failed to run to completion:'
echo "{$examples_output}"
exit 64;
fi
# This debug part is purposefully both a separate job and conditioned to run
# after setup and build_wheel, in order that it can get the restored cache
# contents and show what those look like. That's most useful when debugging.
Debug:
if: failure() || needs.setup.outputs.debug == 'true'
name: Print debugging info
needs: [Decision, Setup, Build_wheel, Bazel_tests]
runs-on: ubuntu-20.04
steps:
- name: Check out a copy of the TFQ git repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{inputs.python_ver || env.python_ver}}
- name: Restore our Python cache
uses: actions/cache@v4
with:
key: ${{needs.Setup.outputs.python_cache_key}}
path: ${{needs.Setup.outputs.python_cache_paths}}
fail-on-cache-miss: true
- name: Set up Bazel
uses: bazel-contrib/[email protected]
with:
bazelrc: ${{env.bazelrc_additions}}
bazelisk-cache: true
external-cache: true
repository-cache: true
disk-cache: ${{needs.Setup.outputs.bazel_cache_key}}
- name: Print debugging info
run: |
echo ''
echo "::group::Contents of $(pwd)"
ls -la
echo '::endgroup::'
echo '::group::Pip info'
pip --version
pip list
echo '::endgroup::'
echo '::group::Python installation'
pyversion="$(python --version | awk '{print $2}')"
ls -l /opt/hostedtoolcache/{.,Python,Python/"$pyversion"/x64/bin}
echo '::endgroup::'
echo '::group::Bazel info'
bazel --version
ls -la /home/runner/.cache
if [[ -e /home/runner/.bazel ]]; then
ls -la /home/runner/.bazel
fi
echo '::endgroup::'
echo '::group::Contents of /home/runner/.bazelrc'
cat /home/runner/.bazelrc
echo '::endgroup::'
echo '::group::Environment variables'
env
echo '::endgroup::'