This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new a84d2def196 [v3-3-test] Add prek hook to keep the Go toolchain version 
in sync (#69338) (#69498)
a84d2def196 is described below

commit a84d2def1967e075f655d36d840cdb2a17c72b1d
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Tue Jul 7 00:28:46 2026 +0200

    [v3-3-test] Add prek hook to keep the Go toolchain version in sync (#69338) 
(#69498)
    
    The Go toolchain version is pinned in eight places (go-sdk/go.mod, the
    go_example module, the setup-go step in both CI workflow copies, the
    golang alpine builder image used by the e2e conftest and by breeze, and
    the default_language_version.golang in the top-level and go-sdk prek
    configs). They have no cross-file include, so a dependency bump that
    raises the go directive in go-sdk/go.mod leaves the rest behind and the
    drift only surfaces as an opaque "go.mod requires go >= <new>" failure
    deep in CI — as happened in PR #69214.
    
    Add a prek hook that treats go-sdk/go.mod as the single source of truth
    and fails when any other site disagrees at major.minor granularity,
    pointing directly at the drifting file.
    (cherry picked from commit 0a48feeb1d58f2cb608e9b2258f83c2df1e8b06a)
    
    Co-authored-by: Jarek Potiuk <[email protected]>
---
 .pre-commit-config.yaml                            |  15 ++
 scripts/ci/prek/check_go_version_in_sync.py        | 197 +++++++++++++++++++++
 .../tests/ci/prek/test_check_go_version_in_sync.py | 130 ++++++++++++++
 3 files changed, 342 insertions(+)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 95bb77f184c..3998ee80bb5 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -269,6 +269,21 @@ repos:
           ^Dockerfile\.ci$
         pass_filenames: false
         require_serial: true
+      - id: check-go-version-in-sync
+        name: Check Go toolchain version is consistent across build files
+        entry: ./scripts/ci/prek/check_go_version_in_sync.py
+        language: python
+        files: >
+          (?x)
+          ^go-sdk/go\.mod$|
+          ^kubernetes-tests/lang_sdk/go_example/go\.mod$|
+          ^\.github/workflows/ci-(arm|amd)\.yml$|
+          ^airflow-e2e-tests/tests/airflow_e2e_tests/constants\.py$|
+          ^dev/breeze/src/airflow_breeze/commands/kubernetes_commands\.py$|
+          ^go-sdk/\.pre-commit-config\.yaml$|
+          ^\.pre-commit-config\.yaml$
+        pass_filenames: false
+        require_serial: true
       - id: check-partition-mapper-defaults-in-sync
         name: Check partition-mapper core/SDK sync (FanOutMapper table + 
SegmentWindow/FixedKeyMapper)
         entry: ./scripts/ci/prek/check_partition_mapper_defaults_in_sync.py
diff --git a/scripts/ci/prek/check_go_version_in_sync.py 
b/scripts/ci/prek/check_go_version_in_sync.py
new file mode 100755
index 00000000000..250bb2246b2
--- /dev/null
+++ b/scripts/ci/prek/check_go_version_in_sync.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Keep the Go toolchain version in sync across every file that pins it.
+
+``golang.org/x/net`` (and other ``golang.org/x`` modules) periodically raise
+their minimum Go version. When that happens a dependency bump changes the
+``go`` directive in ``go-sdk/go.mod`` but leaves the *other* places that pin
+the toolchain untouched — and CI then fails deep inside static checks and the
+Go SDK e2e bundle build with ``go.mod requires go >= <new>`` (this is exactly
+what happened in PR #69214). Those pins have no cross-file include, so nothing
+catches the drift until CI does.
+
+The single source of truth is the ``go`` directive in ``go-sdk/go.mod``. Every
+other site below must agree with it at ``major.minor`` granularity (the minor
+version is what selects the toolchain and the builder image; the go.mod patch
+suffix is not meaningful for those):
+
+- ``go-sdk/go.mod``                             -> ``go <maj.min[.patch]>``  
(SOURCE OF TRUTH)
+- ``kubernetes-tests/lang_sdk/go_example/go.mod`` -> ``go <maj.min[.patch]>``  
(resolves the SDK via ``replace``)
+- ``.github/workflows/ci-amd.yml``              -> Setup Go ``go-version: 
<maj.min>``
+- ``.github/workflows/ci-arm.yml``              -> Setup Go ``go-version: 
<maj.min>``
+- ``airflow-e2e-tests/tests/airflow_e2e_tests/constants.py`` -> 
``GO_BUILDER_IMAGE`` default ``golang:<maj.min>-alpine``
+- ``dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py`` -> 
``LANG_SDK_GO_BUILDER_IMAGE`` default ``golang:<maj.min>-alpine``
+- ``go-sdk/.pre-commit-config.yaml``            -> 
``default_language_version.golang`` (the toolchain prek uses for the SDK's 
``language: golang`` hooks — this is the pin that broke static checks in 
#69214, not the setup-go one)
+- ``.pre-commit-config.yaml``                   -> 
``default_language_version.golang`` (top-level default for any ``language: 
golang`` hook)
+
+When they disagree, bump the drifting sites to the source-of-truth minor
+version. If you add a new place that pins the Go version, register it here too.
+
+Run from the repo root:
+
+    uv run --project scripts python scripts/ci/prek/check_go_version_in_sync.py
+
+Exits 0 if every site agrees with the source of truth, 1 otherwise.
+"""
+
+from __future__ import annotations
+
+import dataclasses
+import pathlib
+import re
+import sys
+
+REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
+
+# The ``go`` directive: ``go 1.25`` or ``go 1.25.0``.
+GO_MOD_DIRECTIVE = re.compile(r"^go\s+(\d+\.\d+(?:\.\d+)?)\s*$", re.MULTILINE)
+# Setup Go step: ``go-version: 1.25`` (optionally quoted).
+WORKFLOW_GO_VERSION = 
re.compile(r"^\s*go-version:\s*[\"']?(\d+\.\d+(?:\.\d+)?)[\"']?\s*$", 
re.MULTILINE)
+# Builder image default: ``golang:1.25-alpine``.
+GOLANG_BUILDER_IMAGE = re.compile(r"golang:(\d+\.\d+(?:\.\d+)?)-alpine")
+# prek ``default_language_version`` entry: ``  golang: 1.25.0``.
+PREK_GOLANG_VERSION = re.compile(r"^\s*golang:\s*(\d+\.\d+(?:\.\d+)?)\s*$", 
re.MULTILINE)
+
+
[email protected]
+class VersionSite:
+    """A single location that pins the Go toolchain version."""
+
+    label: str
+    path: pathlib.Path
+    pattern: re.Pattern[str]
+    is_source_of_truth: bool = False
+
+    def extract(self) -> str | None:
+        """Return the captured version string, or ``None`` if the pattern is 
missing."""
+        if not self.path.exists():
+            return None
+        if m := self.pattern.search(self.path.read_text()):
+            return m.group(1)
+        return None
+
+
+def major_minor(version: str) -> str:
+    """Reduce ``1.25`` / ``1.25.0`` to its ``major.minor`` (``1.25``)."""
+    parts = version.split(".")
+    return ".".join(parts[:2])
+
+
+def build_sites(repo_root: pathlib.Path) -> list[VersionSite]:
+    """Build the list of Go-version pin sites, resolved against 
``repo_root``."""
+    workflows = repo_root / ".github" / "workflows"
+    return [
+        VersionSite(
+            label="go-sdk/go.mod  (go directive)",
+            path=repo_root / "go-sdk" / "go.mod",
+            pattern=GO_MOD_DIRECTIVE,
+            is_source_of_truth=True,
+        ),
+        VersionSite(
+            label="kubernetes-tests/lang_sdk/go_example/go.mod  (go 
directive)",
+            path=repo_root / "kubernetes-tests" / "lang_sdk" / "go_example" / 
"go.mod",
+            pattern=GO_MOD_DIRECTIVE,
+        ),
+        VersionSite(
+            label=".github/workflows/ci-amd.yml  (Setup Go go-version)",
+            path=workflows / "ci-amd.yml",
+            pattern=WORKFLOW_GO_VERSION,
+        ),
+        VersionSite(
+            label=".github/workflows/ci-arm.yml  (Setup Go go-version)",
+            path=workflows / "ci-arm.yml",
+            pattern=WORKFLOW_GO_VERSION,
+        ),
+        VersionSite(
+            label="airflow-e2e-tests/.../constants.py  (GO_BUILDER_IMAGE)",
+            path=repo_root / "airflow-e2e-tests" / "tests" / 
"airflow_e2e_tests" / "constants.py",
+            pattern=GOLANG_BUILDER_IMAGE,
+        ),
+        VersionSite(
+            label="dev/breeze/.../kubernetes_commands.py  
(LANG_SDK_GO_BUILDER_IMAGE)",
+            path=repo_root
+            / "dev"
+            / "breeze"
+            / "src"
+            / "airflow_breeze"
+            / "commands"
+            / "kubernetes_commands.py",
+            pattern=GOLANG_BUILDER_IMAGE,
+        ),
+        VersionSite(
+            label="go-sdk/.pre-commit-config.yaml  
(default_language_version.golang)",
+            path=repo_root / "go-sdk" / ".pre-commit-config.yaml",
+            pattern=PREK_GOLANG_VERSION,
+        ),
+        VersionSite(
+            label=".pre-commit-config.yaml  (default_language_version.golang)",
+            path=repo_root / ".pre-commit-config.yaml",
+            pattern=PREK_GOLANG_VERSION,
+        ),
+    ]
+
+
+def check_sync(sites: list[VersionSite], repo_root: pathlib.Path) -> 
tuple[int, str]:
+    """Compare every site against the source of truth. Returns ``(exit_code, 
report)``."""
+    results = [(site, site.extract()) for site in sites]
+
+    if missing := [site for site, version in results if version is None]:
+        lines = [f"ERROR: Go version pin not found in 
{site.path.relative_to(repo_root)}" for site in missing]
+        lines += [f"       (expected pattern {site.pattern.pattern!r})" for 
site in missing]
+        return 1, "\n".join(lines)
+
+    source = next(site for site, _ in results if site.is_source_of_truth)
+    expected = major_minor(source.extract())  # type: ignore[arg-type]
+
+    drifted = [(site, version) for site, version in results if 
major_minor(version) != expected]  # type: ignore[arg-type]
+    if not drifted:
+        return 0, f"OK: Go version is consistently {expected} across all 
{len(sites)} pin sites."
+
+    col = max(len(site.label) for site, _ in results)
+    lines = [
+        f"ERROR: Go version drifted from the source of truth ({source.label} = 
{expected}).",
+        "",
+        f"  {'PIN SITE':<{col}}  VERSION",
+    ]
+    for site, version in results:
+        marker = (
+            "  <- source of truth"
+            if site.is_source_of_truth
+            else (
+                "  <- DRIFT" if major_minor(version) != expected else ""  # 
type: ignore[arg-type]
+            )
+        )
+        lines.append(f"  {site.label:<{col}}  {version}{marker}")
+    lines += [
+        "",
+        f"Bump the drifting site(s) to {expected} to match {source.label},",
+        "or, if you intentionally changed the source of truth, update the 
other pins to match.",
+        "After changing a go.mod, run its `go mod tidy` so go.sum stays 
consistent.",
+    ]
+    return 1, "\n".join(lines)
+
+
+def main() -> int:
+    exit_code, report = check_sync(build_sites(REPO_ROOT), REPO_ROOT)
+    print(report)
+    return exit_code
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/scripts/tests/ci/prek/test_check_go_version_in_sync.py 
b/scripts/tests/ci/prek/test_check_go_version_in_sync.py
new file mode 100644
index 00000000000..8092f17e034
--- /dev/null
+++ b/scripts/tests/ci/prek/test_check_go_version_in_sync.py
@@ -0,0 +1,130 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+from __future__ import annotations
+
+from pathlib import Path
+
+import pytest
+from check_go_version_in_sync import build_sites, check_sync, major_minor
+
+# Default (all-consistent) version at every pin site. Individual tests override
+# one entry to simulate drift. go.mod carries the patch suffix; the workflow 
and
+# image sites carry only major.minor — the check must treat these as equal.
+DEFAULT_VERSIONS = {
+    "go_sdk_mod": "1.25.0",
+    "go_example_mod": "1.25.0",
+    "ci_amd": "1.25",
+    "ci_arm": "1.25",
+    "constants": "1.25",
+    "kubernetes_commands": "1.25",
+    "go_sdk_prek": "1.25.0",
+    "root_prek": "1.25.0",
+}
+
+
+def _write_tree(root: Path, versions: dict[str, str]) -> None:
+    """Materialise a minimal repo tree with a Go version pin at each of the 
eight sites."""
+
+    def write(rel: str, content: str) -> None:
+        path = root / rel
+        path.parent.mkdir(parents=True, exist_ok=True)
+        path.write_text(content)
+
+    write("go-sdk/go.mod", f"module github.com/apache/airflow/go-sdk\n\ngo 
{versions['go_sdk_mod']}\n")
+    write(
+        "kubernetes-tests/lang_sdk/go_example/go.mod",
+        f"module 
github.com/apache/airflow/kubernetes-tests/lang_sdk/go_example\n\n"
+        f"go {versions['go_example_mod']}\n",
+    )
+    for site, rel in (("ci_amd", "ci-amd.yml"), ("ci_arm", "ci-arm.yml")):
+        write(
+            f".github/workflows/{rel}",
+            f"      - name: Setup Go\n        with:\n          go-version: 
{versions[site]}\n",
+        )
+    write(
+        "airflow-e2e-tests/tests/airflow_e2e_tests/constants.py",
+        f'GO_BUILDER_IMAGE = os.environ.get("GO_BUILDER_IMAGE", 
"golang:{versions["constants"]}-alpine")\n',
+    )
+    write(
+        "dev/breeze/src/airflow_breeze/commands/kubernetes_commands.py",
+        f'LANG_SDK_GO_BUILDER_IMAGE = os.environ.get("GO_BUILDER_IMAGE", '
+        f'"golang:{versions["kubernetes_commands"]}-alpine")\n',
+    )
+    for site, rel in (
+        ("go_sdk_prek", "go-sdk/.pre-commit-config.yaml"),
+        ("root_prek", ".pre-commit-config.yaml"),
+    ):
+        write(
+            rel,
+            f"default_language_version:\n  python: python3\n  node: 22.19.0\n  
golang: {versions[site]}\n",
+        )
+
+
[email protected](
+    ("version", "expected"),
+    [("1.25", "1.25"), ("1.25.0", "1.25"), ("1.24.6", "1.24")],
+)
+def test_major_minor(version: str, expected: str):
+    assert major_minor(version) == expected
+
+
+def test_all_sites_consistent(tmp_path: Path):
+    _write_tree(tmp_path, DEFAULT_VERSIONS)
+    exit_code, report = check_sync(build_sites(tmp_path), tmp_path)
+    assert exit_code == 0
+    assert "consistently 1.25" in report
+
+
+def test_go_mod_patch_suffix_matches_major_minor_pins(tmp_path: Path):
+    """go.mod may pin 1.25.0 while the workflow/image sites pin 1.25 — that is 
in sync."""
+    _write_tree(tmp_path, {**DEFAULT_VERSIONS, "go_sdk_mod": "1.25.0", 
"ci_amd": "1.25"})
+    exit_code, _ = check_sync(build_sites(tmp_path), tmp_path)
+    assert exit_code == 0
+
+
[email protected](
+    "drifted_site",
+    ["go_example_mod", "ci_amd", "ci_arm", "constants", "kubernetes_commands", 
"go_sdk_prek", "root_prek"],
+)
+def test_drift_in_any_derived_site_is_flagged(tmp_path: Path, drifted_site: 
str):
+    """A single derived site left on the old minor version must fail the 
check."""
+    _write_tree(
+        tmp_path, {**DEFAULT_VERSIONS, drifted_site: "1.24" if "mod" not in 
drifted_site else "1.24.6"}
+    )
+    exit_code, report = check_sync(build_sites(tmp_path), tmp_path)
+    assert exit_code == 1
+    assert "drifted from the source of truth" in report
+    assert "<- DRIFT" in report
+
+
+def test_source_of_truth_bump_flags_all_stale_sites(tmp_path: Path):
+    """Bumping only go-sdk/go.mod (the source of truth) flags every other site 
— the #69214 scenario."""
+    _write_tree(tmp_path, {**DEFAULT_VERSIONS, "go_sdk_mod": "1.26.0"})
+    exit_code, report = check_sync(build_sites(tmp_path), tmp_path)
+    assert exit_code == 1
+    # All seven non-source sites are still on the old version -> all flagged.
+    assert report.count("<- DRIFT") == 7
+    assert "<- source of truth" in report
+
+
+def test_missing_pin_is_reported(tmp_path: Path):
+    _write_tree(tmp_path, DEFAULT_VERSIONS)
+    (tmp_path / "go-sdk" / "go.mod").write_text("module 
github.com/apache/airflow/go-sdk\n")
+    exit_code, report = check_sync(build_sites(tmp_path), tmp_path)
+    assert exit_code == 1
+    assert "Go version pin not found" in report
+    assert "go-sdk/go.mod" in report

Reply via email to