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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 320fe23bc3 [Improve][CI] Add connector dist registration completeness 
check (#10895)
320fe23bc3 is described below

commit 320fe23bc3c501482c620a1bc0873346ae47c86d
Author: Daniel <[email protected]>
AuthorDate: Fri May 22 22:56:29 2026 +0800

    [Improve][CI] Add connector dist registration completeness check (#10895)
---
 .github/workflows/backend.yml         |   7 +
 tools/check_connector_registration.py | 403 ++++++++++++++++++++++++++++++++++
 2 files changed, 410 insertions(+)

diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml
index fc1d9cfbd7..7a178fa521 100644
--- a/.github/workflows/backend.yml
+++ b/.github/workflows/backend.yml
@@ -79,6 +79,13 @@ jobs:
           else
             echo "No .class files found in git repository."
           fi
+      - name: Check connector registration completeness
+        run: |
+          if [ -n "$GITHUB_BASE_REF" ]; then
+            python3 tools/check_connector_registration.py --base-ref 
"$GITHUB_BASE_REF"
+          else
+            echo "GITHUB_BASE_REF is not set (push event), skipping connector 
registration check."
+          fi
 
   helm-chart-check:
     name: Check Helm Chart Syntax
diff --git a/tools/check_connector_registration.py 
b/tools/check_connector_registration.py
new file mode 100644
index 0000000000..81df370957
--- /dev/null
+++ b/tools/check_connector_registration.py
@@ -0,0 +1,403 @@
+#  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.
+
+# !/usr/bin/python
+"""
+Comprehensive connector registration completeness check.
+
+Verifies that every new connector module is registered in ALL required 
configuration
+files. A connector that misses even one of these registrations will be 
silently broken:
+not distributed, not discoverable by name, and not installable via 
install-plugin.sh.
+
+Registration points checked (all are ERRORS):
+  1. seatunnel-connectors-v2/pom.xml              -- Maven module (required 
for compile/build)
+  2. seatunnel-dist/pom.xml  <scope>provided>     -- Dist packaging 
(connectors/ directory)
+  3. plugin-mapping.properties                    -- Runtime connector-name -> 
jar resolution
+  4. config/plugin_config                         -- install-plugin.sh 
downloadable list
+
+Modes of operation:
+  --base-ref <ref>   CI diff mode: check only connectors newly added against 
<ref>.
+                     Use this in CI via GITHUB_BASE_REF. Avoids false 
positives from
+                     pre-existing historical omissions in the repository.
+  (no arg)           Static mode: check all connector modules. Some checks may 
report
+                     pre-existing omissions that are known and intentional.
+
+Usage:
+    python tools/check_connector_registration.py
+    python tools/check_connector_registration.py --base-ref origin/dev
+    python tools/check_connector_registration.py --base-ref upstream/dev
+
+Exit codes:
+    0 - All checked connectors pass all critical registration checks
+    1 - One or more critical registrations are missing
+"""
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+# ---------------------------------------------------------------------------
+# Exclusion sets: modules that are intentionally NOT standalone connectors
+# and therefore do NOT need all registration entries.
+# ---------------------------------------------------------------------------
+
+# Base/utility modules that are never deployed as standalone connectors.
+# These lack factory implementations and are dependencies of real connectors.
+EXCLUDED_FROM_ALL = {
+    "connector-common",
+    "connector-file-base",
+    "connector-file-base-hadoop",
+    "connector-cdc-base",
+}
+
+# Connectors bundled into the seatunnel-dist core jar (always available,
+# no separate install-plugin.sh download needed).
+# These appear in seatunnel-dist/pom.xml but not in config/plugin_config.
+BUNDLED_IN_DIST = {
+    "connector-console",
+    "connector-fake",
+    "connector-assert",
+    "connector-socket",
+}
+
+
+# ---------------------------------------------------------------------------
+# Parsing helpers
+# ---------------------------------------------------------------------------
+
+
+def get_artifact_id(pom_path):
+    """
+    Extract the project-level artifactId from a pom.xml file.
+    Strips the <parent> block first to avoid matching the parent artifactId.
+    """
+    with open(pom_path, "r", encoding="utf-8") as f:
+        content = f.read()
+    # Remove <parent> block so we only match the project-level artifactId
+    content_no_parent = re.sub(r"<parent>.*?</parent>", "", content, 
flags=re.DOTALL)
+    m = re.search(r"<artifactId>\s*([^\s<]+)\s*</artifactId>", 
content_no_parent)
+    return m.group(1).strip() if m else None
+
+
+def find_all_connector_modules(connectors_dir):
+    """
+    Walk seatunnel-connectors-v2/ and return a dict of {artifactId -> 
abs_dir_path}
+    for every connector module that has both a pom.xml and a src/ directory.
+    Excludes modules in EXCLUDED_FROM_ALL.
+    """
+    modules = {}
+    for root, dirs, files in os.walk(connectors_dir):
+        # Avoid descending into build artifacts
+        dirs[:] = [d for d in dirs if d != "target"]
+        if "pom.xml" not in files:
+            continue
+        if not os.path.isdir(os.path.join(root, "src")):
+            continue
+        artifact_id = get_artifact_id(os.path.join(root, "pom.xml"))
+        if (
+            artifact_id
+            and artifact_id.startswith("connector-")
+            and artifact_id not in EXCLUDED_FROM_ALL
+        ):
+            modules[artifact_id] = root
+    return modules
+
+
+def get_new_connector_modules_from_diff(repo_root, base_ref, connectors_dir):
+    """
+    Return {artifactId -> abs_dir_path} for connector modules whose pom.xml was
+    ADDED (not modified) in the diff between base_ref and HEAD.
+    Only top-level connector directories (depth=1 under 
seatunnel-connectors-v2/)
+    are considered as "new" connectors; subdirectory additions within an 
existing
+    connector are not treated as new standalone connectors.
+    """
+    try:
+        result = subprocess.run(
+            ["git", "diff", "--name-only", "--diff-filter=A", 
f"{base_ref}...HEAD"],
+            capture_output=True,
+            text=True,
+            cwd=repo_root,
+            check=True,
+        )
+    except subprocess.CalledProcessError as e:
+        print(
+            f"WARNING: git diff failed (base_ref={base_ref}): {e}. "
+            "Falling back to static full scan."
+        )
+        return None
+
+    new_modules = {}
+    # Match exactly: seatunnel-connectors-v2/connector-<name>/pom.xml (no 
deeper path)
+    pattern = re.compile(
+        r"seatunnel-connectors-v2/(connector-[^/]+)/pom\.xml$"
+    )
+    for line in result.stdout.strip().splitlines():
+        m = pattern.match(line)
+        if not m:
+            continue
+        connector_dir = m.group(1)
+        abs_pom = os.path.join(repo_root, "seatunnel-connectors-v2", 
connector_dir, "pom.xml")
+        if not os.path.isfile(abs_pom):
+            continue
+        artifact_id = get_artifact_id(abs_pom)
+        if (
+            artifact_id
+            and artifact_id.startswith("connector-")
+            and artifact_id not in EXCLUDED_FROM_ALL
+        ):
+            new_modules[artifact_id] = os.path.join(
+                repo_root, "seatunnel-connectors-v2", connector_dir
+            )
+    return new_modules
+
+
+# ---------------------------------------------------------------------------
+# Per-registration-point checkers
+# ---------------------------------------------------------------------------
+
+
+def is_module_declared_in_parent_pom(connector_abs_dir):
+    """
+    Return True if the connector's parent directory has a pom.xml that declares
+    this connector as a <module>.  This handles both top-level connectors
+    (parent = seatunnel-connectors-v2/) and nested sub-connectors
+    (parent = seatunnel-connectors-v2/connector-cdc/, etc.).
+    """
+    parent_dir = os.path.dirname(connector_abs_dir)
+    parent_pom = os.path.join(parent_dir, "pom.xml")
+    if not os.path.isfile(parent_pom):
+        return False
+    connector_dir_name = os.path.basename(connector_abs_dir)
+    with open(parent_pom, "r", encoding="utf-8") as f:
+        content = f.read()
+    # Match <module>connector-dir-name</module> (exact name, may have 
whitespace)
+    return bool(
+        re.search(
+            r"<module>\s*" + re.escape(connector_dir_name) + r"\s*</module>",
+            content,
+        )
+    )
+
+
+def build_dist_registered(dist_pom_path):
+    """
+    Return set of artifactIds with scope=provided in seatunnel-dist/pom.xml.
+    The assembly-bin-ci.xml collects all scope=provided connector-* 
dependencies
+    from this pom into the connectors/ directory of the dist package.
+    """
+    with open(dist_pom_path, "r", encoding="utf-8") as f:
+        content = f.read()
+    # Find all <dependency> blocks that contain scope=provided and a 
connector- artifactId
+    registered = set()
+    for dep_block in re.findall(r"<dependency>.*?</dependency>", content, 
re.DOTALL):
+        if "<scope>provided</scope>" not in dep_block:
+            continue
+        m = re.search(r"<artifactId>\s*(connector-[^\s<]+)\s*</artifactId>", 
dep_block)
+        if m:
+            registered.add(m.group(1).strip())
+    return registered
+
+
+def build_plugin_mapping_jars(plugin_mapping_path):
+    """
+    Return set of jar names (connector-*) that appear as VALUES in 
plugin-mapping.properties.
+    e.g. 'seatunnel.source.Kafka = connector-kafka'  → 'connector-kafka'
+    """
+    with open(plugin_mapping_path, "r", encoding="utf-8") as f:
+        content = f.read()
+    return set(re.findall(r"=\s*(connector-[^\s#\n]+)", content))
+
+
+def build_plugin_config_set(plugin_config_path):
+    """Return set of connector names listed in config/plugin_config."""
+    names = set()
+    with open(plugin_config_path, "r", encoding="utf-8") as f:
+        for line in f:
+            line = line.strip()
+            if line.startswith("connector-"):
+                names.add(line)
+    return names
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+
+def parse_args():
+    parser = argparse.ArgumentParser(
+        description="Check connector registration completeness."
+    )
+    parser.add_argument(
+        "--base-ref",
+        metavar="REF",
+        default=os.environ.get("GITHUB_BASE_REF", ""),
+        help=(
+            "Git ref to diff against (e.g. 'origin/dev'). "
+            "When set, only newly added connectors are checked. "
+            "Defaults to GITHUB_BASE_REF env var."
+        ),
+    )
+    return parser.parse_args()
+
+
+def main():
+    args = parse_args()
+    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+    connectors_dir = os.path.join(repo_root, "seatunnel-connectors-v2")
+    dist_pom = os.path.join(repo_root, "seatunnel-dist", "pom.xml")
+    plugin_mapping = os.path.join(repo_root, "plugin-mapping.properties")
+    plugin_config = os.path.join(repo_root, "config", "plugin_config")
+
+    # --- Validate required files exist ---
+    for path, label in [
+        (connectors_dir, "seatunnel-connectors-v2/"),
+        (dist_pom, "seatunnel-dist/pom.xml"),
+        (plugin_mapping, "plugin-mapping.properties"),
+        (plugin_config, "config/plugin_config"),
+    ]:
+        if not os.path.exists(path):
+            print(f"ERROR: Required file not found: {path}")
+            sys.exit(1)
+
+    # --- Determine which connectors to check ---
+    base_ref = args.base_ref.strip()
+    diff_mode = False
+
+    if base_ref:
+        # CI diff mode: only check connectors newly added in this PR
+        # Prefix with 'origin/' if it looks like a plain branch name and not 
already prefixed
+        if "/" not in base_ref:
+            base_ref = "origin/" + base_ref
+        print(f"CI diff mode: checking connectors newly added against 
{base_ref}")
+        connectors_to_check = get_new_connector_modules_from_diff(
+            repo_root, base_ref, connectors_dir
+        )
+        if connectors_to_check is None:
+            # git diff failed; fall back to static mode
+            connectors_to_check = find_all_connector_modules(connectors_dir)
+        elif not connectors_to_check:
+            print("No new connector modules detected in this diff. Nothing to 
check.")
+            sys.exit(0)
+        else:
+            diff_mode = True
+            print(
+                f"Found {len(connectors_to_check)} new connector(s): "
+                + ", ".join(sorted(connectors_to_check))
+            )
+    else:
+        # Static mode: check all connectors with source code
+        print("Static mode: checking all connector modules with source code.")
+        connectors_to_check = find_all_connector_modules(connectors_dir)
+
+    print(f"Total connectors to check: {len(connectors_to_check)}")
+    print()
+
+    # --- Load registration data from all files ---
+    dist_registered = build_dist_registered(dist_pom)
+    pm_jars = build_plugin_mapping_jars(plugin_mapping)
+    pc_names = build_plugin_config_set(plugin_config)
+
+    # --- Check each connector ---
+    errors = {}   # {connector: [missing_locations]}
+
+    for artifact_id, connector_dir in sorted(connectors_to_check.items()):
+        connector_errors = []
+
+        # Check 1: parent pom.xml module declaration
+        # Each connector must be declared as <module> in its parent 
directory's pom.xml.
+        # Connectors discovered via os.walk but missing from the pom hierarchy
+        # will be silently ignored by Maven (not compiled, not packaged).
+        if not is_module_declared_in_parent_pom(connector_dir):
+            connector_errors.append(
+                "parent pom.xml (missing <module> entry; "
+                "Maven will not compile this connector)"
+            )
+
+        # Check 2: seatunnel-dist/pom.xml scope=provided dependency
+        # This controls whether the connector jar ends up in the dist 
connectors/ dir.
+        if artifact_id not in dist_registered:
+            connector_errors.append(
+                "seatunnel-dist/pom.xml (missing <scope>provided</scope> 
dependency; "
+                "connector jar will NOT be included in the distribution 
package)"
+            )
+
+        # Check 3: plugin-mapping.properties name->jar mapping
+        # Required for users to reference the connector by PluginName in job 
configs.
+        if artifact_id not in pm_jars:
+            connector_errors.append(
+                "plugin-mapping.properties (missing name→jar mapping; "
+                "users cannot reference this connector by name in job configs)"
+            )
+
+        # Check 4: config/plugin_config install list
+        # Required for install-plugin.sh to download the connector.
+        # Skip connectors that are bundled into the dist core (always 
available).
+        if artifact_id not in BUNDLED_IN_DIST and artifact_id not in pc_names:
+            connector_errors.append(
+                "config/plugin_config (missing entry; "
+                "install-plugin.sh cannot download this connector)"
+            )
+
+        if connector_errors:
+            errors[artifact_id] = connector_errors
+
+    # --- Print results ---
+    if not errors:
+        mode_desc = "newly added" if diff_mode else "all"
+        print(
+            f"OK: All {len(connectors_to_check)} {mode_desc} connector 
module(s) "
+            "pass all critical registration checks."
+        )
+        sys.exit(0)
+
+    print("ERRORS: The following connectors are missing critical 
registrations.")
+    print(
+        "Each missing registration silently breaks a specific aspect of the 
connector."
+    )
+    print()
+
+    for connector, locs in sorted(errors.items()):
+        print(f"  {connector}:")
+        for loc in locs:
+            print(f"    ERROR: missing from {loc}")
+        print()
+
+    print("-" * 70)
+    print(f"Total connectors with registration errors: {len(errors)}")
+    print()
+    print("Fix guide:")
+    print(
+        "  1. seatunnel-connectors-v2/pom.xml: add 
<module>CONNECTOR_NAME</module>"
+    )
+    print(
+        "  2. seatunnel-dist/pom.xml: add <dependency> with 
<scope>provided</scope>"
+    )
+    print(
+        "  3. plugin-mapping.properties: add 'seatunnel.source.NAME = 
CONNECTOR_NAME'"
+    )
+    print(
+        "     and/or 'seatunnel.sink.NAME = CONNECTOR_NAME'"
+    )
+    print(
+        "  4. config/plugin_config: add a line with CONNECTOR_NAME under 
--connectors-v2--"
+    )
+    sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()

Reply via email to