Files
airflow/dev/registry/patch_providers_json.py
Kaxil Naik 7e49884005 Make registry-backfill workflow actually publish backfilled pages (#66027)
Three related bugs in `.github/workflows/registry-backfill.yml` that
combine to make backfill jobs report success while uploading nothing:

1. **`Download data files from S3 for build` clobbers the in-flight
   `providers.json`.** The workflow downloads the cached S3 copy of
   `providers.json` to `registry/src/_data/providers.json` AFTER
   extraction has run. The cached copy predates the backfilled version,
   so `provider.versions[]` doesn't list it.
   `registry/src/_data/providerVersions.js:48-50` then intersects
   `provider.versions ∩ availableSet` and drops the on-disk
   `metadata.json` for the new version. Eleventy emits no per-version
   page for the backfilled version.

2. **`aws s3 sync` exits 0 on missing source.** Combined with (1), the
   workflow's "Sync backfilled version pages to S3" step silently
   no-ops when the build didn't emit a page, and the job goes green.

3. **A redundant `Extract version metadata from git tags` step
   duplicated `breeze registry backfill`'s internal call AND was broken
   for multi-version matrix entries.** `extract_versions.py:446`
   declared `--version` as single-valued (no `action="append"`), so
   when the workflow looped `--version A --version B` argparse only
   kept the last one. The trailing `|| true` masked the failure.

Fixes:

- New script `dev/registry/patch_providers_json.py` runs after the S3
  download and appends backfilled version(s) into the targeted
  `provider.versions` array. It also defensively keeps `provider.version`
  (the latest) in the array -- otherwise patching into a previously-empty
  list could leave the latest field excluded once the array becomes
  the authoritative filter for `providerVersions.js`.

- Sync step now asserts (a) the per-version `index.html` was emitted,
  (b) the API directory exists, (c) `parameters.json` is non-empty,
  before running each `aws s3 sync`. Silent no-ops are now hard
  failures with `::error::` annotations.

- The redundant standalone `Extract version metadata` step is deleted.
  `breeze registry backfill` already runs `_run_extract_versions`
  internally and propagates exit codes correctly.

`packaging` is now declared as a runtime dep of `dev/registry/`
(previously only available transitively via pydantic) since
`patch_providers_json.py` uses `packaging.version` for newest-first
sorting.

Tests: 11 unit tests in `dev/registry/tests/test_patch_providers_json.py`
covering append, dedup, unknown-provider error, multi-version,
invalid-version sort fallback, empty-list edge (the latest-included
guarantee), latest-field-untouched, other-providers-untouched, and
three CLI invocation paths.
2026-04-28 21:55:09 +01:00

111 lines
4.2 KiB
Python

#!/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.
"""Patch providers.json to include backfilled versions in `provider.versions`.
Backfill writes per-version `metadata.json` to disk for the targeted version,
but the `Download data files from S3 for build` workflow step then overwrites
`registry/src/_data/providers.json` with the cached S3 copy. That cached copy
predates the backfilled version (that's why we're backfilling). Without
patching, `providerVersions.js`'s ``provider.versions ∩ availableSet`` filter
drops the on-disk metadata and the Eleventy build emits no page.
This script runs after the S3 download, in the per-matrix backfill job, and
appends the backfilled version(s) into the targeted ``provider.versions``
array. It also defensively re-includes ``provider.version`` (the "latest"
field) -- otherwise patching into a previously-empty ``versions: []`` could
leave the latest field excluded once the array becomes the authoritative
filter.
Backfill-only tool. ``registry-build.yml`` (the full-build workflow) doesn't
need this -- ``extract_metadata.py`` regenerates ``providers.json`` from
scratch there.
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from packaging.version import InvalidVersion, Version
def patch(providers_json_path: Path, provider_id: str, versions: list[str]) -> int:
"""Append `versions` into the targeted provider's `versions[]`. Returns exit code."""
data = json.loads(providers_json_path.read_text())
target = next((p for p in data["providers"] if p["id"] == provider_id), None)
if target is None:
print(
f"ERROR: provider '{provider_id}' not found in {providers_json_path}. "
f"This script is for backfilling versions of providers already in the "
f"registry; for new providers run a full registry-build.yml dispatch."
)
return 1
existing = list(target.get("versions") or [])
# Always keep `provider.version` (latest) in `versions[]`. Without this,
# patching into a previously-empty list could leave the latest field out
# of providerVersions.js's authoritative filter.
latest = target.get("version")
if latest and latest not in existing:
existing.append(latest)
added: list[str] = []
for v in versions:
if v not in existing:
existing.append(v)
added.append(v)
try:
existing.sort(key=Version, reverse=True)
except InvalidVersion:
existing.sort(reverse=True)
target["versions"] = existing
providers_json_path.write_text(json.dumps(data, indent=2) + "\n")
print(f"Patched {provider_id}: added {added or []}; versions list now has {len(existing)} entries")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0] if __doc__ else "")
parser.add_argument(
"--providers-json",
required=True,
type=Path,
help="Path to providers.json to modify in place.",
)
parser.add_argument(
"--provider",
required=True,
help="Provider ID (e.g. 'amazon', 'common-compat').",
)
parser.add_argument(
"--version",
required=True,
action="append",
help="Version(s) to add (repeat the flag for multiple).",
)
args = parser.parse_args()
return patch(args.providers_json, args.provider, args.version)
if __name__ == "__main__":
sys.exit(main())