From c31a762b985894f64d3a80407b75fadb60240862 Mon Sep 17 00:00:00 2001 From: driazati <9407960+driazati@users.noreply.github.com> Date: Mon, 29 Aug 2022 11:00:54 -0700 Subject: [PATCH] [ci] Don't update Jenkinsfile timestamp on image updates (#12621) The timestamp in the Jenkinsfile is there to prevent post-merge conflicts from different PRs that edit the templates merging non-sequentially. This is not an issue when a line is edited in place though, which is often the case when Docker image tags are updated. This PR makes it so the timestamp is not updated in these cases which should reduce merge conflicts on these types of PRs. --- ci/jenkins/generate.py | 62 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/ci/jenkins/generate.py b/ci/jenkins/generate.py index 901d413364b3..3ccdedc6d924 100644 --- a/ci/jenkins/generate.py +++ b/ci/jenkins/generate.py @@ -18,11 +18,12 @@ import jinja2 import argparse import difflib -import re import datetime +import re import textwrap from pathlib import Path +from typing import List REPO_ROOT = Path(__file__).resolve().parent.parent.parent @@ -82,9 +83,51 @@ def lines_without_generated_tag(content): ] +def is_changed_images_only(lines: List[str]) -> bool: + """ + Return True if 'line' only edits an image tag or if 'line' is not a changed + line in a diff + """ + added_images = [] + removed_images = [] + diff_lines = [] + + for line in lines[2:]: + if not line.startswith("-") and not line.startswith("+"): + # not a diff line, ignore it + continue + + diff_lines.append(line) + + if len(diff_lines) == 0: + # no changes made + return True + + for line in diff_lines: + is_add = line.startswith("+") + line = line.strip().lstrip("+").lstrip("-") + match = re.search( + r"^(ci_[a-zA-Z0-9]+) = \'.*\'$", + line.strip().lstrip("+").lstrip("-"), + flags=re.MULTILINE, + ) + if match is None: + # matched a non-image line, quit early + return False + + if is_add: + added_images.append(match.groups()[0]) + else: + removed_images.append(match.groups()[0]) + + # make sure that the added image lines match the removed image lines + return len(added_images) > 0 and added_images == removed_images + + if __name__ == "__main__": help = "Regenerate Jenkinsfile from template" parser = argparse.ArgumentParser(description=help) + parser.add_argument("--force", action="store_true", help="always overwrite timestamp") parser.add_argument("--check", action="store_true", help="just verify the output didn't change") args = parser.parse_args() @@ -92,6 +135,10 @@ def lines_without_generated_tag(content): content = f.read() data["generated_time"] = datetime.datetime.now().isoformat() + timestamp_match = re.search(r"^// Generated at (.*)$", content, flags=re.MULTILINE) + if not timestamp_match: + raise RuntimeError("Could not find timestamp in Jenkinsfile") + original_timestamp = timestamp_match.groups()[0] environment = jinja2.Environment( loader=jinja2.FileSystemLoader(REPO_ROOT), @@ -103,11 +150,18 @@ def lines_without_generated_tag(content): template = environment.get_template(str(JENKINSFILE_TEMPLATE.relative_to(REPO_ROOT))) new_content = template.render(**data) - diff = "".join( - difflib.unified_diff( + diff = [ + line + for line in difflib.unified_diff( lines_without_generated_tag(content), lines_without_generated_tag(new_content) ) - ) + ] + if not args.force and is_changed_images_only(diff): + new_content = new_content.replace(data["generated_time"], original_timestamp) + print("Detected only Docker-image name changed, skipping timestamp update") + + diff = "".join(diff) + if args.check: if not diff: print("Success, the newly generated Jenkinsfile matched the one on disk")