e768098d0e
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
365 lines
12 KiB
Python
365 lines
12 KiB
Python
# Enforce the check of pipelines.
|
|
# This script will get the diff of the current branch and main branch, calculate the pipelines that should be triggered.
|
|
# Then it will check if the triggered pipelines are successful. This script will loop for 30*loop-times seconds at most.
|
|
|
|
# How many checks are triggered:
|
|
# 1. sdk checks: sdk_cli_tests, sdk_cli_azure_test, sdk_cli_global_config_tests are triggered.
|
|
# 2. examples checks: this script calculate the path filters and decide what should be triggered.
|
|
|
|
# Trigger checks and return the status of the checks:
|
|
# 1. If examples are not correctly generated, fail.
|
|
# 2. If required pipelines are not triggered within 6 rounds of loops, fail.
|
|
# 2.1 (special_care global variable could help on some pipelines that need to bypass the check)
|
|
|
|
# Check pipelines succeed or not:
|
|
# 1. These pipelines should return status within loop-times rounds.
|
|
# 2. If there is failed pipeline in the triggered pipelines, fail.
|
|
|
|
# Import necessary libraries
|
|
import os
|
|
import fnmatch
|
|
import subprocess
|
|
import time
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
# Define variables
|
|
github_repository = "microsoft/promptflow"
|
|
snippet_debug = os.getenv("SNIPPET_DEBUG", 0)
|
|
merge_commit = ""
|
|
loop_times = 40 # 40 * 30 seconds = 20 minutes
|
|
github_workspace = os.path.expanduser("~/promptflow/")
|
|
|
|
# Special cases for pipelines that need to be triggered more or less than default value 1.
|
|
# If 0, the pipeline will not be ignored in check enforcer.
|
|
# Please notice that the key should be the Job Name in the pipeline.
|
|
special_care = {
|
|
"sdk_cli_tests": 4,
|
|
"sdk_cli_azure_test_replay": 4,
|
|
"sdk_cli_global_config_tests": 1,
|
|
"tracing-e2e-test": 12,
|
|
"tracing-unit-test": 12,
|
|
"core_test": 4,
|
|
"azureml_serving_test": 4,
|
|
# "samples_connections_connection": 0,
|
|
}
|
|
|
|
# Copy from original yaml pipelines
|
|
checks = {
|
|
"sdk_cli_tests": [
|
|
"src/promptflow-core/**",
|
|
"src/promptflow-devkit/**",
|
|
"src/promptflow/**",
|
|
"src/promptflow-tracing/**",
|
|
"scripts/building/**",
|
|
"src/promptflow-recording/**",
|
|
],
|
|
"sdk_cli_global_config_tests": [
|
|
"src/promptflow-core/**",
|
|
"src/promptflow-devkit/**",
|
|
"src/promptflow-tracing/**",
|
|
"src/promptflow-azure/**",
|
|
"src/promptflow/**",
|
|
"scripts/building/**",
|
|
],
|
|
"sdk_cli_azure_test_replay": [
|
|
"src/promptflow/**",
|
|
"scripts/building/**",
|
|
"src/promptflow-tracing/**",
|
|
"src/promptflow-core/**",
|
|
"src/promptflow-devkit/**",
|
|
"src/promptflow-azure/**",
|
|
],
|
|
"tracing-e2e-test": [
|
|
"src/promptflow-tracing/**",
|
|
],
|
|
"tracing-unit-test": [
|
|
"src/promptflow-tracing/**",
|
|
],
|
|
"core_test": [
|
|
"src/promptflow-tracing/**",
|
|
"src/promptflow-core/**",
|
|
],
|
|
"azureml_serving_test": [
|
|
"src/promptflow-tracing/**",
|
|
"src/promptflow-core/**",
|
|
],
|
|
}
|
|
|
|
reverse_checks = {}
|
|
pipelines = {}
|
|
pipelines_count = {}
|
|
failed_reason = ""
|
|
|
|
|
|
# Define functions
|
|
def trigger_checks(valid_status_array):
|
|
global failed_reason
|
|
global github_repository
|
|
global merge_commit
|
|
global snippet_debug
|
|
global pipelines
|
|
global pipelines_count
|
|
|
|
output = subprocess.check_output(
|
|
f"gh api /repos/{github_repository}/commits/{merge_commit}/check-suites?per_page=100",
|
|
shell=True,
|
|
)
|
|
check_suites = json.loads(output)["check_suites"]
|
|
for suite in check_suites:
|
|
if snippet_debug != 0:
|
|
print(f"check-suites id {suite['id']}")
|
|
suite_id = suite["id"]
|
|
output = subprocess.check_output(
|
|
f"gh api /repos/{github_repository}/check-suites/{suite_id}/check-runs?per_page=100",
|
|
shell=True,
|
|
)
|
|
check_runs = json.loads(output)["check_runs"]
|
|
for run in check_runs:
|
|
if snippet_debug != 0:
|
|
print(f"check runs name {run['name']}")
|
|
for key in pipelines.keys():
|
|
value = pipelines[key]
|
|
if value == 0:
|
|
continue
|
|
if key in run["name"]:
|
|
pipelines_count[key] += 1
|
|
valid_status_array.append(run)
|
|
|
|
for key in pipelines.keys():
|
|
if pipelines_count[key] < pipelines[key]:
|
|
print(
|
|
f"[Failure]Pipeline {key} is triggered {pipelines_count[key]} times, less than {pipelines[key]} times."
|
|
)
|
|
failed_reason = "Not all pipelines are triggered."
|
|
else:
|
|
print(
|
|
f"Pipeline {key} is triggered {pipelines_count[key]} times, more or equal to {pipelines[key]} times."
|
|
)
|
|
|
|
|
|
def status_checks(valid_status_array):
|
|
global failed_reason
|
|
global pipelines
|
|
global pipelines_count
|
|
# Basic fact of sdk cli checked pipelines.
|
|
failed_reason = ""
|
|
|
|
# Loop through each valid status array.
|
|
for status in valid_status_array:
|
|
# Check if the pipeline was successful.
|
|
if status["conclusion"] and status["conclusion"].lower() == "success":
|
|
# Add 1 to the count of successful pipelines.
|
|
pass
|
|
# Check if the pipeline failed.
|
|
elif status["conclusion"] and status["conclusion"].lower() == "failure":
|
|
failed_reason = "Required pipelines are not successful."
|
|
# Check if the pipeline is still running.
|
|
else:
|
|
if failed_reason == "":
|
|
failed_reason = "Required pipelines are not finished."
|
|
# Print the status of the pipeline to the console.
|
|
print(status["name"] + " is checking.")
|
|
|
|
|
|
def trigger_prepare(input_paths):
|
|
global github_workspace
|
|
global checks
|
|
global reverse_checks
|
|
global pipelines
|
|
global pipelines_count
|
|
global failed_reason
|
|
global special_care
|
|
|
|
for input_path in input_paths:
|
|
if "samples_connections_connection" in input_path:
|
|
continue
|
|
if input_path.startswith("migration-guide/"):
|
|
continue
|
|
if "-maf" in input_path:
|
|
continue
|
|
# Check if the input path contains "examples" or "samples".
|
|
if "examples" in input_path or "samples" in input_path:
|
|
sys.path.append(os.path.expanduser(github_workspace + "/scripts/readme"))
|
|
from readme import main as readme_main
|
|
|
|
os.chdir(os.path.expanduser(github_workspace))
|
|
|
|
# Get the list of pipelines from the readme file.
|
|
pipelines_samples = readme_main(check=True)
|
|
|
|
git_diff_files = [
|
|
item
|
|
for item in subprocess.check_output(
|
|
["git", "diff", "--name-only", "HEAD"]
|
|
)
|
|
.decode("utf-8")
|
|
.split("\n")
|
|
if item != ""
|
|
]
|
|
for _ in git_diff_files:
|
|
failed_reason = "Run readme generation before check in"
|
|
return
|
|
# Merge the pipelines from the readme file with the original list of pipelines.
|
|
for key in pipelines_samples.keys():
|
|
value = pipelines_samples[key]
|
|
checks[key] = value
|
|
|
|
# Reverse checks.
|
|
for key in checks.keys():
|
|
value = checks[key]
|
|
for path in value:
|
|
if path in reverse_checks:
|
|
reverse_checks[path].append(key)
|
|
else:
|
|
reverse_checks[path] = [key]
|
|
|
|
# Render pipelines and pipelines_count using input_paths.
|
|
for input_path in input_paths:
|
|
# Input pattern /**: input_path should match in the middle.
|
|
# Input pattern /*: input_path should match last but one.
|
|
# Other input pattern: input_path should match last.
|
|
|
|
# Skip if input path is a markdown file.
|
|
if input_path.endswith(".md"):
|
|
continue
|
|
|
|
keys = [
|
|
key for key in reverse_checks.keys() if fnmatch.fnmatch(input_path, key)
|
|
]
|
|
# Loop through each key in the list of keys.
|
|
for key_item in keys:
|
|
# Loop through each pipeline in the list of pipelines.
|
|
for key in reverse_checks[key_item]:
|
|
# Check if the pipeline is in the list of pipelines.
|
|
if key in special_care:
|
|
pipelines[key] = special_care[key]
|
|
else:
|
|
pipelines[key] = 1
|
|
# Set the pipeline count to 0.
|
|
pipelines_count[key] = 0
|
|
|
|
|
|
def run_checks():
|
|
global github_repository
|
|
global snippet_debug
|
|
global merge_commit
|
|
global loop_times
|
|
global github_workspace
|
|
global failed_reason
|
|
|
|
if merge_commit == "":
|
|
merge_commit = (
|
|
subprocess.check_output(["git", "log", "-1"]).decode("utf-8").split("\n")
|
|
)
|
|
if snippet_debug != 0:
|
|
print(merge_commit)
|
|
for line in merge_commit:
|
|
if "Merge" in line and "into" in line:
|
|
merge_commit = line.split(" ")[-3]
|
|
break
|
|
if snippet_debug != 0:
|
|
print("MergeCommit " + merge_commit)
|
|
|
|
not_started_counter = 5
|
|
|
|
os.chdir(github_workspace)
|
|
# Get diff of current branch and main branch.
|
|
try:
|
|
git_merge_base = (
|
|
subprocess.check_output(["git", "merge-base", "origin/main", "HEAD"])
|
|
.decode("utf-8")
|
|
.rstrip()
|
|
)
|
|
git_diff = (
|
|
subprocess.check_output(
|
|
["git", "diff", "--name-only", "--diff-filter=d", f"{git_merge_base}"],
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
.decode("utf-8")
|
|
.rstrip()
|
|
.split("\n")
|
|
)
|
|
for single_diff in git_diff:
|
|
print(single_diff)
|
|
except subprocess.CalledProcessError as e:
|
|
print("Exception on process, rc=", e.returncode, "output=", e.output)
|
|
raise e
|
|
|
|
# Prepare how many pipelines should be triggered.
|
|
trigger_prepare(git_diff)
|
|
if failed_reason != "":
|
|
raise Exception(failed_reason)
|
|
|
|
# Loop for 20 minutes at most.
|
|
for i in range(loop_times):
|
|
# Wait for 30 seconds.
|
|
time.sleep(30)
|
|
|
|
# Reset the failed reason.
|
|
failed_reason = ""
|
|
# Reset the valid status array.
|
|
valid_status_array = []
|
|
|
|
# Get all triggered pipelines.
|
|
# If not all pipelines are triggered, continue.
|
|
trigger_checks(valid_status_array)
|
|
if failed_reason != "":
|
|
if not_started_counter == 0:
|
|
raise Exception(failed_reason + " for 6 times.")
|
|
print(failed_reason)
|
|
not_started_counter -= 1
|
|
continue
|
|
|
|
# Get pipeline conclusion priority:
|
|
# 1. Not successful, Fail.
|
|
# 2. Not finished, Continue.
|
|
# 3. Successful, Break.
|
|
status_checks(valid_status_array)
|
|
|
|
# Check if the failed reason contains "not successful".
|
|
if "not successful" in failed_reason.lower():
|
|
raise Exception(failed_reason)
|
|
# Check if the failed reason contains "not finished".
|
|
elif "not finished" in failed_reason.lower():
|
|
print(failed_reason)
|
|
continue
|
|
# Otherwise, print that all required pipelines are successful.
|
|
else:
|
|
print("All required pipelines are successful.")
|
|
break
|
|
|
|
# Check if the failed reason is not empty.
|
|
if failed_reason != "":
|
|
raise Exception(failed_reason)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Run the checks.
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-m",
|
|
"--merge-commit",
|
|
help="merge commit sha",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--loop-times",
|
|
type=int,
|
|
help="Loop times",
|
|
)
|
|
parser.add_argument(
|
|
"-t",
|
|
"--github-workspace",
|
|
help="base path of github workspace",
|
|
)
|
|
args = parser.parse_args()
|
|
if args.merge_commit:
|
|
merge_commit = args.merge_commit
|
|
if args.loop_times:
|
|
loop_times = args.loop_times
|
|
if args.github_workspace:
|
|
github_workspace = args.github_workspace
|
|
run_checks()
|