blob: 6e18650c5656422b85144d81c37a810138ce9535 [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.
"""Derive registry trigger inputs for publish-docs-to-s3.yml.
Implements the gating logic for astronomer/issues-airflow#1305.
Wave dispatches send INCLUDE_DOCS="all-providers apache-airflow-providers"
(see dev/breeze/src/airflow_breeze/commands/workflow_commands.py:188-195
and dev/README_RELEASE_PROVIDERS.md). When a meta-token is present and the
ref is a wave tag (providers/YYYY-MM-DD), this script derives the wave's
actual provider list from per-provider tags reachable from the wave ref
but not from the previous wave tag, so registry-build runs incrementally
on just those providers. Falls back to a full rebuild for non-wave refs
or when the derivation produces an empty list.
Tag-push ordering: README_RELEASE_PROVIDERS.md flows the wave-tag push
BEFORE provider RC tags. Today publish-docs-to-s3 is dispatched manually
AFTER all provider tags exist, so the derivation always has its inputs.
Any future auto-trigger on wave-tag push must address this race.
Inputs (env): INCLUDE_DOCS, REF, GITHUB_OUTPUT.
Outputs (written to GITHUB_OUTPUT):
registry-providers space-separated provider IDs, or "" for full build
registry-full-build "true" for full rebuild, "false" otherwise
"""
from __future__ import annotations
import os
import re
import subprocess
import sys
WAVE_TAG_RE = re.compile(r"^providers/[0-9]{4}-[0-9]{2}-[0-9]{2}$")
WAVE_TAG_GLOB = "providers/[0-9]*-*-*"
PROVIDER_TAG_GLOB = "providers-*/*"
PROVIDER_TAG_RE = re.compile(r"^providers-(?P<id>.+?)/(?P<ver>.+)$")
RC_SUFFIX_RE = re.compile(r"rc[0-9]+$")
META_TOKENS = frozenset({"all", "all-providers", "apache-airflow-providers"})
# Every doc distribution that is not a provider. Must stay equal to
# REGULAR_DOC_PACKAGES in dev/breeze/src/airflow_breeze/global_constants.py minus
# META_TOKENS -- that constant is the source of truth for what `include-docs`
# accepts, and a token missing here is treated as a provider ID, which fails the
# registry extraction. tests/test_derive_wave_providers.py pins the two together.
NON_PROVIDER_TOKENS = frozenset(
{
"apache-airflow",
"helm-chart",
"docker-stack",
"apache-airflow-ctl",
"apache-airflow-mypy",
"task-sdk",
"java-sdk",
"ts-sdk",
}
)
PROVIDER_PREFIX = "apache-airflow-providers-"
def _git(*args: str) -> list[str]:
result = subprocess.run(["git", *args], capture_output=True, text=True, check=True)
return [line for line in result.stdout.splitlines() if line]
def previous_wave_tag(ref: str, wave_tags: list[str]) -> str | None:
"""Return the wave tag immediately preceding ref, or None.
Filters input through WAVE_TAG_RE because the git glob `providers/[0-9]*-*-*`
is broader than the strict `providers/YYYY-MM-DD` shape and matches
unrelated tags (e.g. `providers/2.11/2026-02-16` for an Airflow 2.11
release with a date suffix).
"""
valid = [t for t in wave_tags if WAVE_TAG_RE.match(t)]
try:
idx = valid.index(ref)
except ValueError:
return None
return valid[idx + 1] if idx + 1 < len(valid) else None
def wave_provider_ids(tags: list[str]) -> list[str]:
"""Provider IDs from per-provider tags, dropping rc tags. Sorted."""
ids: set[str] = set()
for tag in tags:
if RC_SUFFIX_RE.search(tag):
continue
match = PROVIDER_TAG_RE.match(tag)
if match:
ids.add(match.group("id"))
return sorted(ids)
def parse_explicit_packages(include_docs: str) -> list[str]:
"""Per-package incremental path -- INCLUDE_DOCS without meta-tokens."""
providers: list[str] = []
seen: set[str] = set()
for token in include_docs.split():
if token in NON_PROVIDER_TOKENS or token in META_TOKENS:
continue
if token.startswith(PROVIDER_PREFIX):
provider_id = token[len(PROVIDER_PREFIX) :]
else:
provider_id = token.replace(".", "-")
if provider_id and provider_id not in seen:
seen.add(provider_id)
providers.append(provider_id)
return providers
def derive(include_docs: str, ref: str, *, git_runner=_git) -> tuple[str, bool, str]:
"""Return (registry_providers, full_build, log_message).
Pure function -- pass git_runner=fake for tests.
"""
tokens = include_docs.split()
has_meta = any(t in META_TOKENS for t in tokens)
if not has_meta:
providers = parse_explicit_packages(include_docs)
return " ".join(providers), False, ""
if not WAVE_TAG_RE.match(ref):
return "", True, f"REF={ref} is not a wave-tag pattern; using full rebuild for meta-token dispatch."
wave_tags = git_runner("tag", "--list", WAVE_TAG_GLOB, "--sort=-creatordate")
prev = previous_wave_tag(ref, wave_tags)
if not prev:
return (
"",
True,
f"::warning::No predecessor wave tag found before {ref}; falling back to full rebuild.",
)
raw_tags = git_runner("tag", "--merged", ref, "--no-merged", prev, "--list", PROVIDER_TAG_GLOB)
providers = wave_provider_ids(raw_tags)
if not providers:
return (
"",
True,
f"::warning::Wave ref {ref} has no new per-provider tags since {prev}; falling back to full rebuild.",
)
joined = " ".join(providers)
return joined, False, f"Derived wave providers ({prev} -> {ref}): {joined}"
def main() -> int:
include_docs = os.environ.get("INCLUDE_DOCS", "")
ref = os.environ.get("REF", "")
output_path = os.environ.get("GITHUB_OUTPUT")
if not output_path:
print("GITHUB_OUTPUT not set", file=sys.stderr)
return 1
registry_providers, full_build, log = derive(include_docs, ref)
if log:
print(log)
with open(output_path, "a", encoding="utf-8") as fh:
fh.write(f"registry-providers={registry_providers}\n")
fh.write(f"registry-full-build={'true' if full_build else 'false'}\n")
return 0
if __name__ == "__main__":
sys.exit(main())