Files
airflow/scripts/ci/verify_helm_chart_package.py
Shahar Epstein e853537c04 Bump inline script metadata to Python 3.11 (#74152)
* Bump inline script metadata to Python 3.11

The prek hooks and dev scripts declare the interpreter they run under
in PEP 723 metadata, and prek picks the hook interpreter from it, so
this has to land before any hook starts using 3.11-only APIs. With
Python 3.10 gone from the project they can also drop the tomli backport from their dependency lists, because
tomllib is in the standard library from 3.11.

* Refresh diagram checksums for the Python 3.11 script headers
2026-10-06 23:20:46 +02:00

182 lines
6.0 KiB
Python
Executable File

#!/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.
#
# Verify a packaged Apache Airflow Helm chart `.tgz`:
#
# * Top-level entries match an explicit expected list (no IDE files,
# workspace metadata, dev hook configs, or test sources sneak into the
# published package).
# * `helm lint` passes against the packaged tarball.
#
# Used by the `tests-helm-release` job in `.github/workflows/helm-tests.yml`
# right after `breeze release-management prepare-helm-chart-package` has
# produced the chart `.tgz`. Can also be run locally:
#
# uv run scripts/ci/verify_helm_chart_package.py dist/airflow-1.22.0.tgz
#
# The expected top-level set is defined inline below; update it deliberately
# when a chart-level file is genuinely added or removed. Anything ignored via
# `chart/.helmignore` should not appear here.
#
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tarfile
from pathlib import Path
# Files / directories expected at the top level of the packaged chart, i.e.
# inside the `airflow/` directory of `airflow-<version>.tgz`. Sorted for
# deterministic diffs. If you add or remove a file at the chart root, update
# this list along with `chart/.helmignore` (which controls what `helm package`
# bundles) so the two stay in sync.
EXPECTED_TOP_LEVEL: frozenset[str] = frozenset(
{
".helmignore",
"Chart.lock",
"Chart.yaml",
"INSTALL",
"LICENSE",
"NOTICE",
"README.md",
"RELEASE_NOTES.rst",
"charts",
"dockerfiles",
"files",
"reproducible_build.yaml",
"templates",
"values.schema.json",
"values.yaml",
"values_schema.schema.json",
}
)
def _tarball_top_level(tarball: Path) -> tuple[str, set[str]]:
"""
Read the chart `.tgz` and return (chart_dir_name, set_of_top_level_entries).
`helm package` produces an archive whose entries are all rooted under a
single directory named after the chart (e.g. `airflow/`). We strip that
prefix to compare against EXPECTED_TOP_LEVEL.
"""
chart_dirs: set[str] = set()
top_level: set[str] = set()
with tarfile.open(tarball, mode="r:gz") as tar:
for member in tar.getmembers():
parts = Path(member.name).parts
if not parts:
continue
chart_dirs.add(parts[0])
if len(parts) == 1:
# The root chart dir entry itself; ignore it for the
# top-level comparison.
continue
top_level.add(parts[1])
if len(chart_dirs) != 1:
raise SystemExit(
f"FAIL: expected the tarball to contain a single top-level chart "
f"directory, found {sorted(chart_dirs)} in {tarball}"
)
return next(iter(chart_dirs)), top_level
def _check_top_level(tarball: Path) -> tuple[str, bool]:
chart_dir, actual = _tarball_top_level(tarball)
extra = sorted(actual - EXPECTED_TOP_LEVEL)
missing = sorted(EXPECTED_TOP_LEVEL - actual)
ok = not extra and not missing
print(f"chart-dir: {chart_dir}")
print(f"top-level entries observed ({len(actual)}):")
for name in sorted(actual):
marker = "+" if name not in EXPECTED_TOP_LEVEL else " "
print(f" {marker} {name}")
if extra:
print()
print(f"FAIL: unexpected top-level entries shipped in {tarball.name}:")
for name in extra:
print(f" + {name}")
print()
print(
"Either add the file to chart/.helmignore so it is not bundled, "
"or — if it is genuinely a chart-consumer-relevant addition — "
"extend EXPECTED_TOP_LEVEL in this script."
)
if missing:
print()
print(f"FAIL: expected top-level entries missing from {tarball.name}:")
for name in missing:
print(f" - {name}")
print()
print(
"If the file was genuinely removed from the chart on purpose, "
"drop it from EXPECTED_TOP_LEVEL in this script."
)
return chart_dir, ok
def _run_helm_lint(tarball: Path) -> bool:
helm = shutil.which("helm")
if helm is None:
print("FAIL: `helm` not found on PATH; cannot lint the chart.")
return False
print()
print(f"$ helm lint {tarball}")
proc = subprocess.run([helm, "lint", str(tarball)], check=False)
return proc.returncode == 0
def main() -> int:
parser = argparse.ArgumentParser(
description="Verify a packaged Apache Airflow Helm chart.",
)
parser.add_argument(
"tarball",
type=Path,
help="Path to the packaged chart, e.g. dist/airflow-1.22.0.tgz",
)
args = parser.parse_args()
tarball: Path = args.tarball
if not tarball.is_file():
print(f"FAIL: not a file: {tarball}")
return 1
print(f"Verifying packaged chart: {tarball}")
print()
_, top_level_ok = _check_top_level(tarball)
lint_ok = _run_helm_lint(tarball)
print()
print("=== Summary ===")
print(f" top-level contents: {'OK' if top_level_ok else 'FAIL'}")
print(f" helm lint: {'OK' if lint_ok else 'FAIL'}")
return 0 if top_level_ok and lint_ok else 1
if __name__ == "__main__":
sys.exit(main())