| #!/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. |
| # |
| |
| """Validate .asf.yaml structural correctness. |
| |
| Checks YAML syntax, root-level structure, and type constraints for |
| known fields. Does NOT reject unknown keys — the ASF infra schema |
| evolves independently and unrecognized keys are silently ignored by |
| the ASF gitbox service. |
| |
| Reference: https://github.com/apache/infrastructure-asfyaml |
| """ |
| |
| import sys |
| |
| try: |
| import yaml |
| except ImportError: |
| sys.exit( |
| "PyYAML is required: pip install pyyaml\n" |
| "Or use: python3 -c \"import pip; pip.main(['install', 'pyyaml'])\"" |
| ) |
| |
| ASF_YAML_PATH = ".asf.yaml" |
| |
| |
| def validate(): |
| errors = [] |
| |
| try: |
| with open(ASF_YAML_PATH, "r") as f: |
| data = yaml.safe_load(f) |
| except FileNotFoundError: |
| print(f"SKIP: {ASF_YAML_PATH} not found") |
| return 0 |
| except yaml.YAMLError as e: |
| print(f"ERROR: Invalid YAML syntax in {ASF_YAML_PATH}: {e}") |
| return 1 |
| |
| if not isinstance(data, dict): |
| print(f"ERROR: {ASF_YAML_PATH} root must be a mapping") |
| return 1 |
| |
| # Validate types of known sections |
| for key in ("github", "notifications", "staging", "publish"): |
| if key in data and not isinstance(data[key], dict): |
| errors.append(f"'{key}' must be a mapping, got {type(data[key]).__name__}") |
| |
| github = data.get("github") |
| if isinstance(github, dict): |
| # features values must be booleans |
| features = github.get("features") |
| if isinstance(features, dict): |
| for key, val in features.items(): |
| if not isinstance(val, bool): |
| errors.append( |
| f"'github.features.{key}' must be a boolean, " |
| f"got {type(val).__name__}" |
| ) |
| |
| # enabled_merge_buttons values must be booleans or strings |
| merge_buttons = github.get("enabled_merge_buttons") |
| if isinstance(merge_buttons, dict): |
| for key, val in merge_buttons.items(): |
| if not isinstance(val, (bool, str)): |
| errors.append( |
| f"'github.enabled_merge_buttons.{key}' must be bool or string, " |
| f"got {type(val).__name__}" |
| ) |
| |
| # labels must be a list of strings |
| labels = github.get("labels") |
| if labels is not None and not isinstance(labels, list): |
| errors.append(f"'github.labels' must be a list, got {type(labels).__name__}") |
| |
| if errors: |
| print(f"ERROR: {ASF_YAML_PATH} validation failed:") |
| for err in errors: |
| print(f" - {err}") |
| return 1 |
| |
| print(f"OK: {ASF_YAML_PATH} is valid") |
| return 0 |
| |
| |
| if __name__ == "__main__": |
| sys.exit(validate()) |