Fix docker image hash update script (#2922)

Simplifies and improves the automated hash update process. Move away
from distro + architecture-aware update to more generic token-based
find-and-replace. This fixes a bug where arm64 image digests were not
properly updated in TF files

This generates
https://github.com/bazelbuild/continuous-integration/pull/2923.
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py
index 37f70ff..72a4b5a 100755
--- a/buildkite/bazelci.py
+++ b/buildkite/bazelci.py
@@ -343,7 +343,6 @@
     ]
 )
 
-# DO_NOT_MODIFY_IMAGE_HASHES_SENTINEL_START
 IMAGE_HASHES = {
     "rockylinux8": {
         "amd64": "sha256:bf148717fe5349e498345dbf3efc1f7690b89e310a2ae970c3bec1ab4e9cd61e",
@@ -429,7 +428,6 @@
         "amd64": "sha256:e6ac038ad949be3e18573735e8a46f5ac74f2e59af2196ed50da0187ea87c259",
     },
 }
-# DO_NOT_MODIFY_IMAGE_HASHES_SENTINEL_END
 
 
 def get_docker_image(image_name, is_arm64=False):
diff --git a/buildkite/digests.json b/buildkite/digests.json
new file mode 100644
index 0000000..3f5e34f
--- /dev/null
+++ b/buildkite/digests.json
@@ -0,0 +1,85 @@
+{
+    "rockylinux8": {
+        "amd64": "sha256:bf148717fe5349e498345dbf3efc1f7690b89e310a2ae970c3bec1ab4e9cd61e",
+        "arm64": "sha256:1b7ac73ca16e71e148bc4bb68858d0a0e44129e7f4949b8aa90be2d169ef01b9"
+    },
+    "rockylinux8-releaser": {
+        "amd64": "sha256:164d20f7df3c44789799a6cd956c535adf4a3f15b8b5a0348fd781578b4808e3",
+        "arm64": "sha256:4635e2b5f0a265c45b462c318e81928f3c2b5e02c580eddd8f5ae0dc44254005"
+    },
+    "rockylinux8-java8": {
+        "amd64": "sha256:79b0528e2ce5d3212dbd4f6b8bc6e331038e92288d379043fd941c45dab5be44",
+        "arm64": "sha256:75d11e32035ba1b4a9709431bcb5140d14de4ff455f2d2aec9d659d075d7e75c"
+    },
+    "rockylinux8-java11": {
+        "amd64": "sha256:042605c8ea1ce7bc243cb5b363d3581876c849cc8d99104e3c20ab0b80d6dd87",
+        "arm64": "sha256:ba8450e5285baf2a148b6f5647b02d0781484aa44665b68ce18c3f39dca2830e"
+    },
+    "rockylinux8-java11-devtoolset10": {
+        "amd64": "sha256:9d271aed202917c7769844eb2e2737b109541457fe387ceffc72035483f1ba44",
+        "arm64": "sha256:52002e0cc6a66c96131b10898cd67d85bab3e320261d6e73637830bab0f51fdf"
+    },
+    "debian10-java11": {
+        "amd64": "sha256:4c54ce7c02f61b8cb24a7447eb14b00e4a62f34c8afd3bd27d0c1a760bdf2a10"
+    },
+    "debian11-java17": {
+        "amd64": "sha256:42da27bb2aa6101b3e03728c0f18e20772e70f708e84d708e4f3a28ea40560d8"
+    },
+    "debian12": {
+        "amd64": "sha256:22d6fb6518a579f2fd32f07c7641df663e3d9306d68417c6623aafd9f7813963",
+        "arm64": "sha256:46b1909a9e51f6784d07df101f182b62cb9281b1009ba67276307ab15be69bd7"
+    },
+    "debian13": {
+        "amd64": "sha256:23ea24eb7c394883a7e069918896d36e9090373f27634d9cfdd30b0c6f58796d",
+        "arm64": "sha256:8143489bf8a6f7c890838ce209c0ec940f5b69cbf78aa9158d5a45ce3cf16f4c"
+    },
+    "ubuntu1604-java8": {
+        "amd64": "sha256:9e5e7c5383c4a8e919d02fd81d7197bd76e912b465511dab7b45336ca3b193d0"
+    },
+    "ubuntu1804-java11": {
+        "amd64": "sha256:25cbb00de12a00485b215440d4ef58469ba305afa54283e40f5b7a8b8ca13c0f"
+    },
+    "ubuntu2004-java11": {
+        "amd64": "sha256:da0038022a8173e05549fcd003d0af97f7e89a1ee29c6f2e19db10ae7d8dc328",
+        "arm64": "sha256:51962774622467357982690143105843a387e6c99d465778c212eecdd047f039"
+    },
+    "ubuntu2004": {
+        "amd64": "sha256:ffa90e8bad9b3952a40a0f0a9a42e3438faf0002aaa3f20cba6dd2a258ec1a56",
+        "arm64": "sha256:91b7cac1ac7ccc22a7c814a37bb7e6c4c1cb8ccf6080d6a9136afd19f31d9a85"
+    },
+    "ubuntu2204": {
+        "amd64": "sha256:2a482c928ff7527594f2e528ac3724486f9cf0fd682394b8aa2f5fed4eb9676f",
+        "arm64": "sha256:5ca61bdbd4b0f3aea8947ed9816f29b9da41a8cef9b1b2217f63724edb8a256a"
+    },
+    "ubuntu2404": {
+        "amd64": "sha256:53dd4ae8e1a2b4c070bc15fb3b60e0d30b81ccc85c6c44e18b20de992be8e9a3",
+        "arm64": "sha256:9d4c2fb2962bd49cccb2929e9bd84a9104952caadedbe80e1a7717fb139f9970"
+    },
+    "ubuntu2004-kythe": {
+        "amd64": "sha256:3862183a7674064e7e36ab851166e5a20eaccb47c8dd7205354fc8de6a6be0e9"
+    },
+    "ubuntu2204-kythe": {
+        "amd64": "sha256:3b0fe8e438faacec522eb3cfbfce116fbcddf1c4cd9a32f847217cac154e5782",
+        "arm64": "sha256:bbbf6342d9eaf7846ccf4ea9557c94dcb1c6428eafa46526370d476d907801ef"
+    },
+    "ubuntu2404-kythe": {
+        "amd64": "sha256:e64cd16e729a251538c0776ef68cf2f88eb590a05b88ff4e66fe35e8495babdf",
+        "arm64": "sha256:a3b0bc5627c3480f2529fce16403c74a245a4e5ea60986317cf31abcc1a6b98d"
+    },
+    "ubuntu2204-java17": {
+        "amd64": "sha256:793035b3e53a0fcf341c5ca541886178b04838008b8ed2e67a4c6aa17325fd8c",
+        "arm64": "sha256:ed4059cb3493cbae25cf4c82d50e0f96486e267a0b960a9c1a168e741e5c21ae"
+    },
+    "fedora39-java17": {
+        "amd64": "sha256:ea39c59f30fe9892c58f12664aa12c6d67be7b01268416cb9e7a47167ab8631d"
+    },
+    "fedora40-java21": {
+        "amd64": "sha256:0ab39f88defa6427cbfc033a3480854fcb6ac3e9de78dc1dc6918b4547d46bd0"
+    },
+    "fedora43-java25": {
+        "amd64": "sha256:04ed68b97d884184fbcc89d09e48b5acc09d18b1e10c15021e0210223d1305ae"
+    },
+    "docgen": {
+        "amd64": "sha256:e6ac038ad949be3e18573735e8a46f5ac74f2e59af2196ed50da0187ea87c259"
+    }
+}
diff --git a/buildkite/update_docker_image_hashes.py b/buildkite/update_docker_image_hashes.py
index bfa8e4b..4c31e4a 100755
--- a/buildkite/update_docker_image_hashes.py
+++ b/buildkite/update_docker_image_hashes.py
@@ -133,83 +133,28 @@
     return "\n".join(lines)
 
 
-SENTINEL_START = "# DO_NOT_MODIFY_IMAGE_HASHES_SENTINEL_START"
-SENTINEL_END = "# DO_NOT_MODIFY_IMAGE_HASHES_SENTINEL_END"
+def find_replace_hashes(curr_hashes, next_hashes, files):
+    for ff in files:
+        for k in IMAGE_KEYS:
+            for arch in ["amd64", "arm64"]:
+                if arch not in curr_hashes[k] or arch not in next_hashes[k]:
+                    continue
+                with open(ff, "r") as f:
+                    content = f.read()
+                with open(ff, "w") as f:
+                    f.write(content.replace(curr_hashes[k][arch], next_hashes[k][arch]))
 
 
-def update_bazelci_file(bazelci_path, new_image_hashes_str):
-    output_lines = []
-    in_sentinel_block = False
-    found_start = False
-    found_end = False
-
-    with open(bazelci_path, "r", encoding="utf-8") as f:
-        for line in f:
-            if line.startswith(SENTINEL_START):
-                output_lines.append(line)
-                output_lines.append(f"{new_image_hashes_str}\n")
-                in_sentinel_block = True
-                found_start = True
-            elif line.startswith(SENTINEL_END):
-                in_sentinel_block = False
-                output_lines.append(line)
-                found_end = True
-            elif not in_sentinel_block:
-                output_lines.append(line)
-
-    if not found_start:
-        raise ValueError(f"Could not find start sentinel '{SENTINEL_START}' in {bazelci_path}")
-    if not found_end:
-        raise ValueError(f"Could not find end sentinel '{SENTINEL_END}' in {bazelci_path}")
-
-    with open(bazelci_path, "w", encoding="utf-8") as f:
-        f.writelines(output_lines)
-
-
-def update_pinned_hashes(files, image_hashes):
-    pattern = re.compile(r"gcr\.io/bazel-public/([a-zA-Z0-9_\-]+)(?:@sha256:[a-f0-9]+)?")
-
-    updated_files = []
-    for fpath in sorted(files):
-        with open(fpath, "r", encoding="utf-8") as f:
-            content = f.read()
-
-        def replacer(match):
-            img_name = match.group(1)
-            if img_name in image_hashes and "amd64" in image_hashes[img_name]:
-                digest = image_hashes[img_name]["amd64"]
-                return f"gcr.io/bazel-public/{img_name}@{digest}"
-            return match.group(0)
-
-        updated_content = pattern.sub(replacer, content)
-        if updated_content != content:
-            with open(fpath, "w", encoding="utf-8") as f:
-                f.write(updated_content)
-            updated_files.append(fpath)
-
-    return updated_files
-
-
-def update_terraform_configs(terraform_dir, image_hashes):
-    tf_files = list(terraform_dir.glob("**/*.tf")) + list(terraform_dir.glob("**/*.tpl"))
-    return update_pinned_hashes(tf_files, image_hashes)
-
-
-def update_pipeline_ymls(pipelines_dir, image_hashes):
-    pipeline_yml_files = list(pipelines_dir.glob("**/*.yml"))
-    return update_pinned_hashes(pipeline_yml_files, image_hashes)
-
-
-def update_rbe_presets(rbe_file, image_hashes):
-    return update_pinned_hashes([rbe_file], image_hashes)
-
-
-def update_setup_docker(setup_docker_file, image_hashes):
-    return update_pinned_hashes([setup_docker_file], image_hashes)
+def get_git_root() -> str:
+    p = subprocess.run(["git", "rev-parse", "--show-toplevel"],
+                       capture_output=True, text=True)
+    if p.returncode != 0:
+        raise RuntimeError("Could not determine the git root: " + p.stderr)
+    return p.stdout.strip()
 
 
 def main():
-    repo_root = Path(__file__).resolve().parent.parent
+    repo_root = Path(get_git_root())
     bazelci_path = repo_root / "buildkite" / "bazelci.py"
     terraform_dir = repo_root / "buildkite" / "terraform"
     pipelines_dir = repo_root / "pipelines"
@@ -240,27 +185,21 @@
 
     digests = fetch_all_image_digests(IMAGE_KEYS)
 
-    new_dict_str = format_image_hashes_dict(digests)
-    update_bazelci_file(bazelci_path, new_dict_str)
-    print(f"Successfully updated IMAGE_HASHES in {bazelci_path}!")
+    # Load the current digests
+    digests_json_file = repo_root / "buildkite" / "digests.json"
+    with open(digests_json_file, "r") as f:
+        curr_digests = json.loads(f.read())
 
-    updated_tf = update_terraform_configs(terraform_dir, digests)
-    print(f"Successfully updated {len(updated_tf)} terraform configuration files:")
-    for tf_path in updated_tf:
-        print(f"  - {tf_path}")
+    files_to_update = [
+        Path(repo_root / "buildkite" / "bazelci.py"),
+        rbe_presets_file,
+        setup_docker_file,
+    ] + list(terraform_dir.glob("**/*.tf")) + \
+            list(pipelines_dir.glob("**/*.yml")) + \
+            list(pipelines_dir.glob("**/*.yml.tpl"))
 
-    updated_rbe = update_rbe_presets(rbe_presets_file, digests)
-    if updated_rbe:
-        print(f"Successfully updated RBE presets in {rbe_presets_file}!")
-
-    updated_pipelines = update_pipeline_ymls(pipelines_dir, digests)
-    print(f"Successfully updated {len(updated_pipelines)} pipeline ymls:")
-    for p in updated_pipelines:
-        print(f"  - {p}")
-
-    updated_setup_docker = update_setup_docker(setup_docker_file, digests)
-    if updated_setup_docker:
-        print(f"Successfully updated setup-docker.sh in {setup_docker_file}!")
+    print("Updating image digests...")
+    find_replace_hashes(curr_digests, digests, files_to_update)
 
 
 if __name__ == "__main__":