From e8d6ada520188b63663f2a859ea62d5ab03b7140 Mon Sep 17 00:00:00 2001 From: Mateus Reis <53548031+Mateus-Reis@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:37:08 +0100 Subject: [PATCH] feat: add Knative Operator compatibility scraper --- static/compatibilities/knative-operator.yaml | 105 +++ static/compatibilities/manifest.yaml | 1 + utils/compatibility/knative-operator.md | 66 ++ .../scrapers/knative-operator.py | 218 +++++ .../fixtures/knative_operator/LICENSE.knative | 206 +++++ .../tests/fixtures/knative_operator/README.md | 15 + .../kubernetes-ga-releases.json | 80 ++ .../kubernetes-provenance.json | 57 ++ .../knative_operator/operator-index.yaml | 750 ++++++++++++++++++ .../fixtures/knative_operator/provenance.json | 14 + .../knative_operator/release-schedule.md | 80 ++ .../tests/test_knative_operator.py | 315 ++++++++ .../tests/test_knative_recorded_sources.py | 69 ++ .../tests/test_knative_scrape.py | 86 ++ 14 files changed, 2062 insertions(+) create mode 100644 static/compatibilities/knative-operator.yaml create mode 100644 utils/compatibility/knative-operator.md create mode 100644 utils/compatibility/scrapers/knative-operator.py create mode 100644 utils/compatibility/tests/fixtures/knative_operator/LICENSE.knative create mode 100644 utils/compatibility/tests/fixtures/knative_operator/README.md create mode 100644 utils/compatibility/tests/fixtures/knative_operator/kubernetes-ga-releases.json create mode 100644 utils/compatibility/tests/fixtures/knative_operator/kubernetes-provenance.json create mode 100644 utils/compatibility/tests/fixtures/knative_operator/operator-index.yaml create mode 100644 utils/compatibility/tests/fixtures/knative_operator/provenance.json create mode 100644 utils/compatibility/tests/fixtures/knative_operator/release-schedule.md create mode 100644 utils/compatibility/tests/test_knative_operator.py create mode 100644 utils/compatibility/tests/test_knative_recorded_sources.py create mode 100644 utils/compatibility/tests/test_knative_scrape.py diff --git a/static/compatibilities/knative-operator.yaml b/static/compatibilities/knative-operator.yaml new file mode 100644 index 0000000000..916aee3ac5 --- /dev/null +++ b/static/compatibilities/knative-operator.yaml @@ -0,0 +1,105 @@ +icon: https://raw.githubusercontent.com/cncf/artwork/main/projects/knative/icon/color/knative-icon-color.svg +git_url: https://github.com/knative/operator +release_url: https://github.com/knative/operator/releases/tag/knative-v{vsn} +readme_url: https://github.com/knative/community/blob/main/mechanics/RELEASE-SCHEDULE.md +helm_repository_url: https://knative.github.io/operator +chart_name: knative-operator +versions: +- version: 1.23.1 + kube: ['1.36', '1.35', '1.34'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.23.1 + images: [] +- version: 1.23.0 + kube: ['1.36', '1.35', '1.34'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.23.0 + images: [] +- version: 1.22.0 + kube: ['1.36', '1.35', '1.34'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.22.0 + images: [] +- version: 1.21.0 + kube: ['1.35', '1.34', '1.33'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.21.0 + images: [] +- version: 1.20.0 + kube: ['1.34', '1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.20.0 + images: [] +- version: 1.19.0 + kube: ['1.33', '1.32'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.19.0 + images: [] +- version: 1.18.0 + kube: ['1.32', '1.31'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.18.0 + images: [] +- version: 1.17.0 + kube: ['1.32', '1.31', '1.30'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.17.0 + images: [] +- version: 1.16.0 + kube: ['1.31', '1.30', '1.29'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.16.0 + images: [] +- version: 1.15.0 + kube: ['1.30', '1.29', '1.28'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.15.0 + images: [] +- version: 1.14.0 + kube: ['1.30', '1.29', '1.28'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.14.0 + images: [] +- version: 1.13.0 + kube: ['1.29', '1.28', '1.27'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.13.0 + images: [] +- version: 1.12.0 + kube: ['1.28', '1.27', '1.26'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.12.0 + images: [] +- version: 1.11.2 + kube: ['1.27', '1.26', '1.25'] + requirements: [] + incompatibilities: [] + summary: null + chart_version: v1.11.2 + images: [] diff --git a/static/compatibilities/manifest.yaml b/static/compatibilities/manifest.yaml index 6d325d796f..6c79a230e9 100644 --- a/static/compatibilities/manifest.yaml +++ b/static/compatibilities/manifest.yaml @@ -22,6 +22,7 @@ names: - jaeger - karpenter - keda +- knative-operator - kube-prometheus-stack - kyverno - linkerd diff --git a/utils/compatibility/knative-operator.md b/utils/compatibility/knative-operator.md new file mode 100644 index 0000000000..b2a6850879 --- /dev/null +++ b/utils/compatibility/knative-operator.md @@ -0,0 +1,66 @@ +# Knative Operator compatibility sources + +The scraper describes the default Knative release associated with each Operator +application minor. Independently selected Serving/Eventing versions and custom +manifests are outside this table's scope. + +## Reproducible support rule + +The [Knative release schedule](https://github.com/knative/community/blob/main/mechanics/RELEASE-SCHEDULE.md) +provides a release date and Kubernetes minimum for each Knative minor. The +[release principles](https://github.com/knative/community/blob/main/mechanics/RELEASE-VERSIONING-PRINCIPLES.md#k8s-minimum-version-principle) +tie the supported Kubernetes range to the Knative release cut and qualify the +latest patch of each Kubernetes minor. The schedule's explicit minimum is the +component-specific lower bound. + +We derive a finite list: include each Kubernetes minor at or above that minimum +whose stable `.0` release was published by the Knative minor's scheduled cut date. +Every Operator patch in that minor uses the same list. This is an inference from +the documented policy and dated releases, not a claim that all later Kubernetes +versions are qualified. Older Kubernetes patch versions are not independently +qualified by this minor-level table. + +The [official Helm index](https://knative.github.io/operator/index.yaml) identifies +released Operator application and chart versions. Exact chart versions retain +their `v` prefix. Prereleases are excluded; build metadata is rejected because the +shared updater does not support it. A scheduled release without a stable +chart is not emitted. If multiple charts target one application version, the +newest stable chart is selected deterministically. + +The [Kubernetes stable marker](https://dl.k8s.io/release/stable.txt) bounds the +history lookup. Each minor's GA date comes from its exact GitHub release, for +example [v1.33.0](https://github.com/kubernetes/kubernetes/releases/tag/v1.33.0), +using `published_at` from `/repos/kubernetes/kubernetes/releases/tags/v1.33.0`. +The lookup requires a complete sequence with increasing dates. It does not use +patch-release timestamps or assume that a fixed page count covers history. + +Examples verified on 2026-09-08: + +- Knative 1.18 was cut on 2025-04-22. Kubernetes 1.33 GA followed on 2025-04-23, + so the list is 1.31 and 1.32, even though the Operator chart appeared later. +- Knative 1.23 was cut on 2026-07-28 with minimum 1.34. The list is 1.34, 1.35, + and 1.36. Kubernetes 1.37 is excluded even from the September Operator patch. +- The schedule states minimum 1.29 for Knative 1.16, while the + [Operator 1.16.6 embedded helper](https://github.com/knative/operator/blob/knative-v1.16.6/vendor/knative.dev/pkg/version/version.go) + contains a lower enforcement constant. The table follows the documented + support minimum, not merely the runtime admission check. + +## Refresh and tests + +Run the scraper through the existing compatibility runner with +`SCRAPER=knative-operator`. Retrieval, parsing, and range validation finish before +calling the existing update helper. Failure while fetching or validating the +compatibility sources prevents the updater from being called. The scraper itself +requires no additional Python dependencies. The committed table uses the shared +updater's existing reduction of repeated compatibility ranges; the parser still +processes every stable chart in the index. + +Run the isolated tests from the repository root: + +```sh +python -m unittest discover -s utils/compatibility/tests -p 'test_knative*.py' -v +``` + +Tests use recorded and synthetic data, without network requests, Helm execution, +or credentials. Helm, image enrichment, and optional summary generation are +handled by the existing shared updater, not by the parser tests. diff --git a/utils/compatibility/scrapers/knative-operator.py b/utils/compatibility/scrapers/knative-operator.py new file mode 100644 index 0000000000..881d21cf82 --- /dev/null +++ b/utils/compatibility/scrapers/knative-operator.py @@ -0,0 +1,218 @@ +"""Knative minor-cut compatibility policy; see ../knative-operator.md.""" + +from __future__ import annotations + +from datetime import date, datetime, timezone +import json +import re +from urllib.request import Request, urlopen + +import yaml + +SCHEDULE_URL = "https://raw.githubusercontent.com/knative/community/main/mechanics/RELEASE-SCHEDULE.md" +INDEX_URL = "https://knative.github.io/operator/index.yaml" +KUBERNETES_STABLE_URL = "https://dl.k8s.io/release/stable.txt" +TARGET_FILE = "../../static/compatibilities/knative-operator.yaml" +REQUEST_TIMEOUT = 30 + +NUMBER = r"(?:0|[1-9][0-9]*)" +MINOR = re.compile(rf"v?({NUMBER}\.{NUMBER})") +SEMVER = re.compile( + rf"v?({NUMBER}\.{NUMBER}\.{NUMBER})" + r"(?P-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" + r"(?P\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?" +) + + +class UniqueKeyLoader(yaml.SafeLoader): + def construct_mapping(self, node, deep=False): + keys = [self.construct_object(key, deep=deep) for key, _ in node.value] + if len(keys) != len(set(keys)): + raise ValueError("Duplicate Helm index YAML keys") + return super().construct_mapping(node, deep=deep) + + +def _version(value): + match = SEMVER.fullmatch(value) if isinstance(value, str) else None + if match is None: + raise ValueError(f"Invalid stable version: {value!r}") + return match + + +def _version_key(value): + return tuple(int(part) for part in _version(value)[1].split(".")) + + +def _minor_key(value): + match = MINOR.fullmatch(value) if isinstance(value, str) else None + if match is None: + raise ValueError(f"Invalid Kubernetes minor: {value!r}") + return tuple(int(part) for part in match[1].split(".")) + + +def parse_schedule(markdown): + releases = {} + columns = None + width = 0 + required = ("Release", "Min K8s Version", "Date") + for line in markdown.splitlines(): + if not line.strip().startswith("|"): + columns = None + continue + cells = [" ".join(cell.split()) for cell in line.strip().strip("|").split("|")] + if "Release" in cells or "Min K8s Version" in cells: + if any(cells.count(header) != 1 for header in required): + raise ValueError("Missing or duplicate required schedule headers") + columns = tuple(cells.index(header) for header in required) + width = len(cells) + continue + if columns is None: + continue + if all(re.fullmatch(r":?-{3,}:?", cell) for cell in cells): + continue + if len(cells) != width: + raise ValueError(f"Invalid schedule row: {line.strip()}") + release = MINOR.fullmatch(cells[columns[0]]) + minimum = MINOR.fullmatch(cells[columns[1]]) + if release is None or minimum is None: + raise ValueError(f"Invalid schedule version: {line.strip()}") + info = {"minimum": minimum[1], "date": date.fromisoformat(cells[columns[2]])} + key = release[1] + if key in releases and releases[key] != info: + raise ValueError(f"Conflicting schedule entries for Knative {key}") + releases[key] = info + if not releases: + raise ValueError("No Knative release schedule rows found") + return releases + + +def parse_charts(index_yaml): + try: + index = yaml.load(index_yaml, Loader=UniqueKeyLoader) + except (yaml.YAMLError, TypeError) as error: + raise ValueError(f"Invalid Helm index YAML: {error}") from error + entries = index.get("entries") if isinstance(index, dict) else None + charts = entries.get("knative-operator") if isinstance(entries, dict) else None + if not isinstance(charts, list) or not charts: + raise ValueError("No knative-operator chart entries found") + by_chart = {} + by_app = {} + for entry in charts: + if not isinstance(entry, dict): + raise ValueError("Invalid Helm chart entry") + chart = _version(entry.get("version")) + app = _version(entry.get("appVersion")) + if chart["prerelease"] or app["prerelease"]: + continue + if app["build"] or chart["build"]: + raise ValueError("Build metadata is unsupported by the compatibility updater") + chart_version = entry["version"] + app_version = entry["appVersion"].removeprefix("v") + chart_identity = chart_version.removeprefix("v") + if chart_identity in by_chart and by_chart[chart_identity] != app_version: + raise ValueError(f"Conflicting application versions for chart {chart_version}") + by_chart[chart_identity] = app_version + current = by_app.get(app_version) + if current is None or (_version_key(chart_version), chart_version) > ( + _version_key(current), current, + ): + by_app[app_version] = chart_version + if not by_app: + raise ValueError("No stable Knative Operator charts found") + return [ + {"version": app, "chart_version": by_app[app]} + for app in sorted(by_app, key=lambda app: (_version_key(app), app), reverse=True) + ] + + +def parse_kubernetes_release(payload, expected_tag): + if not isinstance(expected_tag, str) or not re.fullmatch(rf"v1\.{NUMBER}\.0", expected_tag): + raise ValueError(f"Expected a Kubernetes v1 minor GA tag, got {expected_tag}") + expected = _version(expected_tag) + if expected["prerelease"] or expected["build"] or _version_key(expected_tag)[2] != 0: + raise ValueError(f"Expected a Kubernetes GA tag, got {expected_tag}") + if not isinstance(payload, dict) or payload.get("tag_name") != expected_tag: + raise ValueError(f"Unexpected Kubernetes release record for {expected_tag}") + if payload.get("draft") is not False or payload.get("prerelease") is not False: + raise ValueError(f"Kubernetes {expected_tag} is not a published stable release") + published = payload.get("published_at") + if not isinstance(published, str): + raise ValueError(f"Missing publication date for Kubernetes {expected_tag}") + timestamp = datetime.fromisoformat(published.replace("Z", "+00:00")) + if timestamp.tzinfo is None: + raise ValueError(f"Missing publication timezone for Kubernetes {expected_tag}") + minor = ".".join(expected[1].split(".")[:2]) + return minor, timestamp.astimezone(timezone.utc).date() + + +def _required_releases(schedule, charts): + releases = [] + for chart in charts: + minor = ".".join(_version(chart["version"])[1].split(".")[:2]) + if minor not in schedule: + raise ValueError(f"No documented release schedule for Knative {minor}") + releases.append(schedule[minor]) + return releases + + +def build_versions(schedule_text, index_text, kubernetes_releases): + schedule = parse_schedule(schedule_text) + charts = parse_charts(index_text) + required = _required_releases(schedule, charts) + if not isinstance(kubernetes_releases, dict) or not kubernetes_releases: + raise ValueError("No Kubernetes GA history supplied") + history = sorted(kubernetes_releases, key=_minor_key) + previous = None + for minor in history: + key = _minor_key(minor) + published = kubernetes_releases[minor] + if type(published) is not date: + raise ValueError(f"Invalid Kubernetes publication date for {minor}") + if previous is not None: + if key != (previous[0][0], previous[0][1] + 1) or published <= previous[1]: + raise ValueError("Incomplete or nonmonotonic Kubernetes GA history") + previous = key, published + versions = [] + for chart, release in zip(charts, required): + floor = release["minimum"] + if floor not in kubernetes_releases or kubernetes_releases[floor] > release["date"]: + raise ValueError(f"Kubernetes minimum {floor} was not released by the Knative cut") + supported = [minor for minor in reversed(history) + if _minor_key(minor) >= _minor_key(floor) + and kubernetes_releases[minor] <= release["date"]] + versions.append({**chart, "kube": supported, "requirements": [], "incompatibilities": []}) + return versions + + +def fetch_text(url): + request = Request(url, headers={"User-Agent": "plural-knative-compatibility-scraper"}) + with urlopen(request, timeout=REQUEST_TIMEOUT) as response: + return response.read().decode("utf-8") + + +def scrape(): + from utils import print_error, update_compatibility_info + + try: + schedule_text = fetch_text(SCHEDULE_URL) + index_text = fetch_text(INDEX_URL) + releases = _required_releases(parse_schedule(schedule_text), parse_charts(index_text)) + floor = min((_minor_key(release["minimum"]) for release in releases)) + marker = fetch_text(KUBERNETES_STABLE_URL).strip() + stable = _version(marker) + if stable["prerelease"] or stable["build"]: + raise ValueError("Kubernetes stable marker must identify a stable release") + latest = _version_key(marker)[:2] + if latest[0] != floor[0] or not 0 <= latest[1] - floor[1] <= 100: + raise ValueError("Unsupported Kubernetes GA history bounds") + history = {} + for minor in range(floor[1], latest[1] + 1): + tag = f"v{floor[0]}.{minor}.0" + url = f"https://api.github.com/repos/kubernetes/kubernetes/releases/tags/{tag}" + key, published = parse_kubernetes_release(json.loads(fetch_text(url)), tag) + history[key] = published + versions = build_versions(schedule_text, index_text, history) + except (OSError, UnicodeError, ValueError) as error: + print_error(f"Unable to refresh Knative Operator compatibility: {error}") + return + update_compatibility_info(TARGET_FILE, versions) diff --git a/utils/compatibility/tests/fixtures/knative_operator/LICENSE.knative b/utils/compatibility/tests/fixtures/knative_operator/LICENSE.knative new file mode 100644 index 0000000000..6fa7daacaf --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/LICENSE.knative @@ -0,0 +1,206 @@ +Copyright 2022 Knative Authors +SPDX-License-Identifier: Apache-2.0 + +Code samples in this repository are provided with the following license: + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. diff --git a/utils/compatibility/tests/fixtures/knative_operator/README.md b/utils/compatibility/tests/fixtures/knative_operator/README.md new file mode 100644 index 0000000000..a99d0d5483 --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/README.md @@ -0,0 +1,15 @@ +# Recorded upstream fixtures + +Captured on 2026-09-08 for deterministic offline tests. + +- `release-schedule.md` and `operator-index.yaml` are unmodified upstream source + snapshots. Their retrieval URLs and SHA-256 file hashes are in `provenance.json`. +- `kubernetes-ga-releases.json` keeps the four relevant fields from each official + Kubernetes GA release API response. `kubernetes-provenance.json` records the + URLs and hashes of the original full responses, before this field projection. +- The Knative source snapshots are covered by the upstream Apache License 2.0, + included as `LICENSE.knative`. + +The tests also use small synthetic inputs for malformed data and release-date +boundaries. Refreshing the recorded snapshots requires updating the explicit +release-count and cutoff expectations. diff --git a/utils/compatibility/tests/fixtures/knative_operator/kubernetes-ga-releases.json b/utils/compatibility/tests/fixtures/knative_operator/kubernetes-ga-releases.json new file mode 100644 index 0000000000..3043d6096c --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/kubernetes-ga-releases.json @@ -0,0 +1,80 @@ +[ + { + "tag_name": "v1.25.0", + "draft": false, + "prerelease": false, + "published_at": "2022-08-23T21:27:47Z" + }, + { + "tag_name": "v1.26.0", + "draft": false, + "prerelease": false, + "published_at": "2022-12-09T04:00:59Z" + }, + { + "tag_name": "v1.27.0", + "draft": false, + "prerelease": false, + "published_at": "2023-04-11T19:27:46Z" + }, + { + "tag_name": "v1.28.0", + "draft": false, + "prerelease": false, + "published_at": "2023-08-15T17:00:03Z" + }, + { + "tag_name": "v1.29.0", + "draft": false, + "prerelease": false, + "published_at": "2023-12-13T16:34:20Z" + }, + { + "tag_name": "v1.30.0", + "draft": false, + "prerelease": false, + "published_at": "2024-04-17T20:45:16Z" + }, + { + "tag_name": "v1.31.0", + "draft": false, + "prerelease": false, + "published_at": "2024-08-13T13:47:21Z" + }, + { + "tag_name": "v1.32.0", + "draft": false, + "prerelease": false, + "published_at": "2024-12-11T21:13:11Z" + }, + { + "tag_name": "v1.33.0", + "draft": false, + "prerelease": false, + "published_at": "2025-04-23T17:44:49Z" + }, + { + "tag_name": "v1.34.0", + "draft": false, + "prerelease": false, + "published_at": "2025-08-27T17:19:17Z" + }, + { + "tag_name": "v1.35.0", + "draft": false, + "prerelease": false, + "published_at": "2025-12-17T20:01:55Z" + }, + { + "tag_name": "v1.36.0", + "draft": false, + "prerelease": false, + "published_at": "2026-04-22T17:57:14Z" + }, + { + "tag_name": "v1.37.0", + "draft": false, + "prerelease": false, + "published_at": "2026-08-26T16:29:12Z" + } +] diff --git a/utils/compatibility/tests/fixtures/knative_operator/kubernetes-provenance.json b/utils/compatibility/tests/fixtures/knative_operator/kubernetes-provenance.json new file mode 100644 index 0000000000..6e51416981 --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/kubernetes-provenance.json @@ -0,0 +1,57 @@ +{ + "retrieved_at": "2026-09-08T11:20:06.577404+00:00", + "records": [ + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.25.0", + "sha256": "e82c6a8cdc4917f7c949dfd2f7337cabecd861bb6636dfa900f0a0a68d34f5d9" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.26.0", + "sha256": "e9cc783f779be36631a43ad3e2eb5a165fb830bf5e00b5ef1dfcb810fe0b58f4" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.27.0", + "sha256": "baaff37d8a9ac39c316c93c55447805ab1bbbe7079fa07461166d7742317e2c3" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.28.0", + "sha256": "0baba30c058319d4724be3ea5c61ab4165cf36659460deda6b7a3a80c49bbf94" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.29.0", + "sha256": "e566f9237e69481fca1595f0b0b32cf9d7887b0721f970972c2353d5cb176e8e" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.30.0", + "sha256": "0d93b55c12dc15705300569f9ee906baae2b4503978206a3a28e31dc03347073" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.31.0", + "sha256": "da9cebaa41a16835056195e4a6cf423a4acc665900bbd3b0b33562ee87111271" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.32.0", + "sha256": "484e5cbd70e00918ccace30cbe7443aeef913472663673ac8ac9348f223dc888" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.33.0", + "sha256": "5b102c0084e8910873c3b5bf1196d8ce89f593ebbd12f2ebd99c488525e536ea" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.34.0", + "sha256": "10e12473c736c80f3f466aeebc0ab533b879400d714740fd60c4d7449ab6e789" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.35.0", + "sha256": "f67a24b2489a788e3015d4f2fc250335c89a5b24b1b2628cbb4b7ab18df7fc69" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.36.0", + "sha256": "4c3c66d2983a36b76d02197b9f5ba89a7a3ec8d4ab238d035394e3c1f17dc179" + }, + { + "url": "https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.37.0", + "sha256": "e8ca54c9a679f76541ea92de3071d38be2f8c6699561e9efe0e5b80cf6679550" + } + ] +} diff --git a/utils/compatibility/tests/fixtures/knative_operator/operator-index.yaml b/utils/compatibility/tests/fixtures/knative_operator/operator-index.yaml new file mode 100644 index 0000000000..5cbee3847b --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/operator-index.yaml @@ -0,0 +1,750 @@ +apiVersion: v1 +entries: + knative-operator: + - name: "knative-operator" + apiVersion: v2 + version: "v1.23.1" + appVersion: "1.23.1" + description: "Helm chart for deploying Knative Operator" + created: "2026-09-01T10:17:05Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.23.1/knative-operator-v1.23.1.tgz" + digest: "fbdab558474a547cb244904fb947f4fe028dcbc8049b8f5dda029d7ac07ac889" + - name: "knative-operator" + apiVersion: v2 + version: "v1.23.0" + appVersion: "1.23.0" + description: "Helm chart for deploying Knative Operator" + created: "2026-08-04T19:20:32Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.23.0/knative-operator-v1.23.0.tgz" + digest: "8722be0a739f1841f143a5c7e5dbf64b06dad24847b4743d286bdf8f9eeef672" + - name: "knative-operator" + apiVersion: v2 + version: "v1.22.3" + appVersion: "1.22.3" + description: "Helm chart for deploying Knative Operator" + created: "2026-07-07T09:58:56Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.22.3/knative-operator-v1.22.3.tgz" + digest: "b5a39fc679407d82e03e13c3e369d4e4f12e701b4f7343c3287eb1a5ab144bfa" + - name: "knative-operator" + apiVersion: v2 + version: "v1.21.4" + appVersion: "1.21.4" + description: "Helm chart for deploying Knative Operator" + created: "2026-07-07T09:50:00Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.21.4/knative-operator-v1.21.4.tgz" + digest: "4d339c1388eac924e08ddf2dfe940a11f9303251350e484ed1c622c301355195" + - name: "knative-operator" + apiVersion: v2 + version: "v1.22.2" + appVersion: "1.22.2" + description: "Helm chart for deploying Knative Operator" + created: "2026-06-02T10:00:49Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.22.2/knative-operator-v1.22.2.tgz" + digest: "2171aa2da8f1ebea43b3ac8a2f1cd9adadbee6aa0ea87bd80caf2e03a29f0f3f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.21.3" + appVersion: "1.21.3" + description: "Helm chart for deploying Knative Operator" + created: "2026-06-02T09:51:19Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.21.3/knative-operator-v1.21.3.tgz" + digest: "cc721fd1d458af0b0acff2c3e70aaea89e2b3d81872c1bdebaad3ebd13cb18ed" + - name: "knative-operator" + apiVersion: v2 + version: "v1.22.1" + appVersion: "1.22.1" + description: "Helm chart for deploying Knative Operator" + created: "2026-05-05T09:56:46Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.22.1/knative-operator-v1.22.1.tgz" + digest: "9d135c2215c5b5c9de3662113757c7863552903b8f1bb37eabe105796e720b98" + - name: "knative-operator" + apiVersion: v2 + version: "v1.22.0" + appVersion: "1.22.0" + description: "Helm chart for deploying Knative Operator" + created: "2026-05-01T16:02:58Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.22.0/knative-operator-v1.22.0.tgz" + digest: "7d6f1dfa4b70a9456253b090e5183b5e2a8035491fd974dbe6ea9705f3264554" + - name: "knative-operator" + apiVersion: v2 + version: "v1.21.2" + appVersion: "1.21.2" + description: "Helm chart for deploying Knative Operator" + created: "2026-05-01T17:27:33Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.21.2/knative-operator-v1.21.2.tgz" + digest: "ea89009a8d0da2025a507b8cd73ba057c46793b08a97a53e80f57930e610d53f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.21.1" + appVersion: "1.21.1" + description: "Helm chart for deploying Knative Operator" + created: "2026-02-25T21:51:57Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.21.1/knative-operator-v1.21.1.tgz" + digest: "84990d37bd929150e38cc941f09c33f2397aef51328f6ec9162212667deae77f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.20.2" + appVersion: "1.20.2" + description: "Helm chart for deploying Knative Operator" + created: "2026-02-25T21:52:52Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.20.2/knative-operator-v1.20.2.tgz" + digest: "61dc27c63908b206dc903182d74b1b02f73b81958b54101b82de0e172e0ef343" + - name: "knative-operator" + apiVersion: v2 + version: "v1.21.0" + appVersion: "1.21.0" + description: "Helm chart for deploying Knative Operator" + created: "2026-01-29T19:21:36Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.21.0/knative-operator-v1.21.0.tgz" + digest: "7d697894e977cfbbd4c68c9cd49b3a4a3cee4c2d9e3f65b63d2d11a4203b0422" + - name: "knative-operator" + apiVersion: v2 + version: "v1.20.1" + appVersion: "1.20.1" + description: "Helm chart for deploying Knative Operator" + created: "2025-12-30T10:31:22Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.20.1/knative-operator-v1.20.1.tgz" + digest: "3b00a506a1af7793fc84577c87761ebc1c45da1aba2ff83ab1cf9de88aa212fe" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.6" + appVersion: "1.19.6" + description: "Helm chart for deploying Knative Operator" + created: "2025-12-30T09:37:35Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.6/knative-operator-v1.19.6.tgz" + digest: "867094cd06d9460327627c9206f6f1fadf8cacf0dfbfbca1a86e41f18a0bf334" + - name: "knative-operator" + apiVersion: v2 + version: "v1.20.0" + appVersion: "1.20.0" + description: "Helm chart for deploying Knative Operator" + created: "2025-11-06T16:56:22Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.20.0/knative-operator-v1.20.0.tgz" + digest: "d44e06ba3ea74ceaf23fd19b521740102dad90ce08ff66badabf638833b50eed" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.5" + appVersion: "1.19.5" + description: "Helm chart for deploying Knative Operator" + created: "2025-10-28T09:39:45Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.5/knative-operator-v1.19.5.tgz" + digest: "2aea2f85ad0aff4cdb03ed9296e9009101ed9ea3efb6e882d94cf57ac97c4ec5" + - name: "knative-operator" + apiVersion: v2 + version: "v1.18.3" + appVersion: "1.18.3" + description: "Helm chart for deploying Knative Operator" + created: "2025-10-28T10:02:05Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.18.3/knative-operator-v1.18.3.tgz" + digest: "fae6084b17b423a305a9c74f9fcf9215f4764af87a5b4b09c49b3eee9c13a155" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.4" + appVersion: "1.19.4" + description: "Helm chart for deploying Knative Operator" + created: "2025-10-23T15:55:35Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.4/knative-operator-v1.19.4.tgz" + digest: "71b22d0c648df2956538b51fadf8983f217bce514c14f559a34eae8600fc4d05" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.3" + appVersion: "1.19.3" + description: "Helm chart for deploying Knative Operator" + created: "2025-09-17T14:21:15Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.3/knative-operator-v1.19.3.tgz" + digest: "d35b200f2c5229fb1f74051482b62aea1b90e0269c9e35b9c3159a8e4621b890" + - name: "knative-operator" + apiVersion: v2 + version: "v1.18.2" + appVersion: "1.18.2" + description: "Helm chart for deploying Knative Operator" + created: "2025-09-16T21:17:20Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.18.2/knative-operator-v1.18.2.tgz" + digest: "b54835a8077ef5d77abcf0a259f6ecfcc2e5e6259b67fdbdf6e966e433889886" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.2" + appVersion: "1.19.2" + description: "Helm chart for deploying Knative Operator" + created: "2025-09-09T09:38:44Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.2/knative-operator-v1.19.2.tgz" + digest: "5d28984fa569fd2c700650a1645612aeb0edf79eb846fc43cc751b17f7d65f72" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.1" + appVersion: "1.19.1" + description: "Helm chart for deploying Knative Operator" + created: "2025-09-02T09:38:35Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.1/knative-operator-v1.19.1.tgz" + digest: "ee0e3de67b0eeb59bdecbb4bc4d9c969b8141e844d3daec3d91ec25c84c09efa" + - name: "knative-operator" + apiVersion: v2 + version: "v1.19.0" + appVersion: "1.19.0" + description: "Helm chart for deploying Knative Operator" + created: "2025-07-26T19:22:00Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.19.0/knative-operator-v1.19.0.tgz" + digest: "06901fae2b81e4cb4cd68e5fcba1e8360b72cd3da0035914e6d735cd0e632450" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.8" + appVersion: "1.17.8" + description: "Helm chart for deploying Knative Operator" + created: "2025-05-06T19:44:18Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.8/knative-operator-v1.17.8.tgz" + digest: "9f1885995db5c33f5986141c3c39b2623408f526f87339f31ccb45a517fc7b8a" + - name: "knative-operator" + apiVersion: v2 + version: "v1.18.1" + appVersion: "1.18.1" + description: "Helm chart for deploying Knative Operator" + created: "2025-05-06T19:43:31Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.18.1/knative-operator-v1.18.1.tgz" + digest: "161c781e16622389cbcb7ef886188fa25935465331ec7fd0867a32a4be5679ff" + - name: "knative-operator" + apiVersion: v2 + version: "v1.18.0" + appVersion: "1.18.0" + description: "Helm chart for deploying Knative Operator" + created: "2025-04-24T19:25:22Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.18.0/knative-operator-v1.18.0.tgz" + digest: "019b83ef9577e6546ef14f3cce10ba2a1a2deae8c46441ec10be49413c40baa1" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.7" + appVersion: "1.17.7" + description: "Helm chart for deploying Knative Operator" + created: "2025-04-22T10:29:14Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.7/knative-operator-v1.17.7.tgz" + digest: "37a292b368405748ecb8cd35a6aa9900d5b04fef26a01c6525fa1e8417710fac" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.6" + appVersion: "1.16.6" + description: "Helm chart for deploying Knative Operator" + created: "2025-04-22T10:25:58Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.6/knative-operator-v1.16.6.tgz" + digest: "0e54180a724f57a61c5168f9c34949f652bca878b7acc8f8ed36674f82402986" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.6" + appVersion: "1.17.6" + description: "Helm chart for deploying Knative Operator" + created: "2025-04-15T10:29:57Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.6/knative-operator-v1.17.6.tgz" + digest: "0061c35ffde66bebeaa9b2edec19b4714613bd867ab2c176598c4ad1c796ccca" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.5" + appVersion: "1.16.5" + description: "Helm chart for deploying Knative Operator" + created: "2025-04-15T10:25:06Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.5/knative-operator-v1.16.5.tgz" + digest: "5f29b56dc93490c5f39e8905bbc6f65a1b60e993b19cfc57a13c7a57e87deafe" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.5" + appVersion: "1.17.5" + description: "Helm chart for deploying Knative Operator" + created: "2025-03-25T10:26:16Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.5/knative-operator-v1.17.5.tgz" + digest: "490cfa483cef2eb59248314c3767401f69f6c38b9683c013f8730f55979b024d" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.4" + appVersion: "1.16.4" + description: "Helm chart for deploying Knative Operator" + created: "2025-03-25T10:23:01Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.4/knative-operator-v1.16.4.tgz" + digest: "8d79e223980a5f6c4067997c461efd4f0d3213f7ceb1dc5546ba77e7857f5266" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.4" + appVersion: "1.17.4" + description: "Helm chart for deploying Knative Operator" + created: "2025-03-18T10:28:53Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.4/knative-operator-v1.17.4.tgz" + digest: "ca9940e6c151807d788df479c34921f48df9e6236048d68609ea93b2e46bab39" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.3" + appVersion: "1.16.3" + description: "Helm chart for deploying Knative Operator" + created: "2025-03-18T10:24:02Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.3/knative-operator-v1.16.3.tgz" + digest: "fc16caf3320690e1c516455f0df9fc90cd811c6b5f8d51968670873d92fb8dcd" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.2" + appVersion: "1.16.2" + description: "Helm chart for deploying Knative Operator" + created: "2025-02-18T10:23:56Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.2/knative-operator-v1.16.2.tgz" + digest: "5cdbf03f5e71a3e253afb05e28715e535e157e99557f8a6ad8c2fca98c639ddf" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.3" + appVersion: "1.17.3" + description: "Helm chart for deploying Knative Operator" + created: "2025-02-11T10:26:27Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.3/knative-operator-v1.17.3.tgz" + digest: "9f8ab15ae84b751a08b960f70f2398a46a872903958e3947f372cca8c2cf1aaa" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.2" + appVersion: "1.17.2" + description: "Helm chart for deploying Knative Operator" + created: "2025-02-04T10:30:28Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.2/knative-operator-v1.17.2.tgz" + digest: "15ab7780e6c6527b16fb5c74f10f83aebe9c9afade511f246f2be53f8cde8842" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.1" + appVersion: "1.17.1" + description: "Helm chart for deploying Knative Operator" + created: "2025-01-31T22:30:49Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.1/knative-operator-v1.17.1.tgz" + digest: "d79c8c5fefbb17a4b95ddc766fda52b1dc1f391399141f190244afca6294c24f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.17.0" + appVersion: "1.17.0" + description: "Helm chart for deploying Knative Operator" + created: "2025-01-27T19:21:24Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.17.0/knative-operator-v1.17.0.tgz" + digest: "bd2610710d0681a5391477fadb943313c0256853678aebf3616420d5df2bb446" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.1" + appVersion: "1.16.1" + description: "Helm chart for deploying Knative Operator" + created: "2025-01-22T16:24:04Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.1/knative-operator-v1.16.1.tgz" + digest: "3e971a58210912f6cd874581d5db55ad022453783c16f22468d2725dd67476be" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.10" + appVersion: "1.15.10" + description: "Helm chart for deploying Knative Operator" + created: "2025-01-22T21:13:10Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.10/knative-operator-v1.15.10.tgz" + digest: "da5c4383dbaa7f87c7e7574d8d35d0be7c5ae7c1c478fbe9745e32a11dd6995e" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.9" + appVersion: "1.15.9" + description: "Helm chart for deploying Knative Operator" + created: "2024-10-29T10:05:27Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.9/knative-operator-v1.15.9.tgz" + digest: "724a75e376a99f59a22659446bacff6fb1265bab9fdd6b96d56caef7b359e946" + - name: "knative-operator" + apiVersion: v2 + version: "v1.16.0" + appVersion: "1.16.0" + description: "Helm chart for deploying Knative Operator" + created: "2024-10-23T17:23:17Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.16.0/knative-operator-v1.16.0.tgz" + digest: "b9038b86551107c80cdb0fa0fe53e36cf24ca7ccb3acbea233bb7cca8036fd29" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.8" + appVersion: "1.15.8" + description: "Helm chart for deploying Knative Operator" + created: "2024-10-22T10:04:24Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.8/knative-operator-v1.15.8.tgz" + digest: "71c77dd9fbf85222016d37887af06940dfa8a756f3139146d6a17571f333b197" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.7" + appVersion: "1.15.7" + description: "Helm chart for deploying Knative Operator" + created: "2024-10-15T10:01:41Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.7/knative-operator-v1.15.7.tgz" + digest: "75fe99a03a89508159da0f72631a2ade3b63a865ae161a94c791f82ff6448644" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.6" + appVersion: "1.15.6" + description: "Helm chart for deploying Knative Operator" + created: "2024-10-08T10:06:53Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.6/knative-operator-v1.15.6.tgz" + digest: "3acdeef096f2f32f31a02078eb061322c3a724bbe8a1cde597d18c7c2732ca57" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.5" + appVersion: "1.15.5" + description: "Helm chart for deploying Knative Operator" + created: "2024-09-24T10:00:24Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.5/knative-operator-v1.15.5.tgz" + digest: "95f6f1b5e4a2ae1b01ee9d74afc0741468f86931441822456caba050abd90548" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.9" + appVersion: "1.14.9" + description: "Helm chart for deploying Knative Operator" + created: "2024-09-24T10:26:46Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.9/knative-operator-v1.14.9.tgz" + digest: "de060a3dab38f6acb953dc0978f7295ac482356f10f6b5d9b4dcb37b2f6912da" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.4" + appVersion: "1.15.4" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-27T10:04:48Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.4/knative-operator-v1.15.4.tgz" + digest: "470797df2e8994005172f5ccee60fa1e20314274848cb82a1208bc646fb1d7d4" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.8" + appVersion: "1.14.8" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-27T10:26:11Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.8/knative-operator-v1.14.8.tgz" + digest: "71e14ae92f8ffc5b69ef00dae56ee9fb962372ec4e330c9bf003bbb980534a57" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.3" + appVersion: "1.15.3" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-20T10:07:31Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.3/knative-operator-v1.15.3.tgz" + digest: "f5735b70ad4673df585f62623d038b1b9e51bffc271ddc722a74f209ee8f4c3b" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.2" + appVersion: "1.15.2" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-13T10:01:55Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.2/knative-operator-v1.15.2.tgz" + digest: "5007d0ca0502367c86f70e8490bd6f757c0176e953763d6d38d61b15f756c6e5" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.1" + appVersion: "1.15.1" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-06T10:01:02Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.1/knative-operator-v1.15.1.tgz" + digest: "afb8278600c24484098758750643de82869bcfba56089e2fd574b70fc7c5d5b1" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.7" + appVersion: "1.14.7" + description: "Helm chart for deploying Knative Operator" + created: "2024-08-06T10:22:04Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.7/knative-operator-v1.14.7.tgz" + digest: "02d1e98c342f0cc9a7ff93ecb8e692cad5a81336e5f69b479a115330bbe5c38f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.6" + appVersion: "1.14.6" + description: "Helm chart for deploying Knative Operator" + created: "2024-07-30T10:23:02Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.6/knative-operator-v1.14.6.tgz" + digest: "5038d1e799d46c48d72bdad616506cb30191859b0e7c098c56145f2dd10ecd6f" + - name: "knative-operator" + apiVersion: v2 + version: "v1.15.0" + appVersion: "1.15.0" + description: "Helm chart for deploying Knative Operator" + created: "2024-07-25T10:22:44Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.15.0/knative-operator-v1.15.0.tgz" + digest: "d45b9e8b1dbd3fee05e107e63193e38bf9c35cfe3e06a8c4006879a4e24c2222" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.5" + appVersion: "1.14.5" + description: "Helm chart for deploying Knative Operator" + created: "2024-07-16T10:27:04Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.5/knative-operator-v1.14.5.tgz" + digest: "c4a9a93196835574980439f60b25a584dbf22586907c66d2b56200631fd30cd5" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.4" + appVersion: "1.14.4" + description: "Helm chart for deploying Knative Operator" + created: "2024-07-02T10:21:52Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.4/knative-operator-v1.14.4.tgz" + digest: "adaa58a39e9a61c8e614af03b1156d10e9dec3a451e41c15faa8e51513b334ab" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.3" + appVersion: "1.14.3" + description: "Helm chart for deploying Knative Operator" + created: "2024-06-11T10:17:10Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.3/knative-operator-v1.14.3.tgz" + digest: "f5168081e596bdcb9aadb06ab1ace8fb910306bea516c33374651714fcdd041e" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.2" + appVersion: "1.14.2" + description: "Helm chart for deploying Knative Operator" + created: "2024-05-22T02:59:15Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.2/knative-operator-v1.14.2.tgz" + digest: "0c89939cbaf7b9f47c3fac0318858becc8753d6951168d6f29d986659e968825" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.1" + appVersion: "1.14.1" + description: "Helm chart for deploying Knative Operator" + created: "2024-05-14T10:21:31Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.1/knative-operator-1.14.1.tgz" + digest: "1b00c0062073a5dd926235877f7c0208e9b35651869f09389dc8133b79ab2f99" + - name: "knative-operator" + apiVersion: v2 + version: "v1.14.0" + appVersion: "1.14.0" + description: "Helm chart for deploying Knative Operator" + created: "2024-05-01T10:21:50Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.14.0/knative-operator-1.14.0.tgz" + digest: "4da41db0383199fe71b55a3fc371251229e001d68b7dfb7556f287da2bc30e01" + - name: "knative-operator" + apiVersion: v2 + version: "v1.13.3" + appVersion: "1.13.3" + description: "Helm chart for deploying Knative Operator" + created: "2024-03-19T10:18:18Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.13.3/knative-operator-1.13.3.tgz" + digest: "92aa651967926c6bf869a74649c9178ac99a67596a9a09ba0507b4a27d1b6174" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.5" + appVersion: "1.12.5" + description: "Helm chart for deploying Knative Operator" + created: "2024-03-19T10:11:06Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.5/knative-operator-1.12.5.tgz" + digest: "b2db79ad30d2cad7941cf6a284765d7f4e95d2746d5d730516f3fea2156931ef" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.4" + appVersion: "1.12.4" + description: "Helm chart for deploying Knative Operator" + created: "2024-03-05T10:12:51Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.4/knative-operator-1.12.4.tgz" + digest: "eb8b4ac59d3e79fc728dabf0a65c500c015437b9a3cea67e23cf16c56d2b76b7" + - name: "knative-operator" + apiVersion: v2 + version: "v1.13.2" + appVersion: "1.13.2" + description: "Helm chart for deploying Knative Operator" + created: "2024-02-27T10:20:02Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.13.2/knative-operator-1.13.2.tgz" + digest: "e8f9a30467663e82eb10b495f63501c302fbd1d38821b7e56ea3b040a889634e" + - name: "knative-operator" + apiVersion: v2 + version: "v1.13.1" + appVersion: "1.13.1" + description: "Helm chart for deploying Knative Operator" + created: "2024-02-06T10:17:44Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.13.1/knative-operator-1.13.1.tgz" + digest: "2e0c908d4a8b1ab48c2fdaf197c0eabf6c81904ba2d54639d88a853db5d8ae0a" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.3" + appVersion: "1.12.3" + description: "Helm chart for deploying Knative Operator" + created: "2024-02-06T10:08:48Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.3/knative-operator-1.12.3.tgz" + digest: "4a8a3c1383d371112daf13f9c7e8a41f69eb3e06a207b5f1aef84adc7fe4299a" + - name: "knative-operator" + apiVersion: v2 + version: "v1.13.0" + appVersion: "1.13.0" + description: "Helm chart for deploying Knative Operator" + created: "2024-01-25T01:21:34Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.13.0/knative-operator-1.13.0.tgz" + digest: "49e5af504bcdb78c3a8cf5c96150503d8dc6ab027053a722ee15ebc07ee32c33" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.2" + appVersion: "1.12.2" + description: "Helm chart for deploying Knative Operator" + created: "2024-01-09T10:09:07Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.2/knative-operator-1.12.2.tgz" + digest: "03424319cff9333b40211eed0192dad13af74727aa9d8662a8a26f6b9087a9a6" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.12" + appVersion: "1.11.12" + description: "Helm chart for deploying Knative Operator" + created: "2023-12-12T10:03:34Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.12/knative-operator-1.11.12.tgz" + digest: "8885505152d679b59b6f8cb12d7b319d733d7ccec0d0aa076ef48bcf40aedc99" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.1" + appVersion: "1.12.1" + description: "Helm chart for deploying Knative Operator" + created: "2023-12-05T10:11:14Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.1/knative-operator-1.12.1.tgz" + digest: "1aa6ffb6bfec0676a2c6106feddb51c2bb77c66d8c544bacdfeda9ce4a4645d5" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.11" + appVersion: "1.11.11" + description: "Helm chart for deploying Knative Operator" + created: "2023-11-30T15:56:01Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.11/knative-operator-1.11.11.tgz" + digest: "b0a8bca9b2bf198eaad5fde5c5aa0656e2ff75a459e324a4f130f0c464522b5b" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.10" + appVersion: "1.11.10" + description: "Helm chart for deploying Knative Operator" + created: "2023-10-31T09:58:29Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.10/knative-operator-1.11.10.tgz" + digest: "4de5e2dd780d902ae5f9e0391ff2373d758a712fa37233bd8cac484397aaa063" + - name: "knative-operator" + apiVersion: v2 + version: "v1.12.0" + appVersion: "1.12.0" + description: "Helm chart for deploying Knative Operator" + created: "2023-10-27T01:23:10Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.12.0/knative-operator-1.12.0.tgz" + digest: "d33a93b2dd0ade2ac70fc08da96e8b60598a584dd5ae2a1dc4acf9f7d04b7f50" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.9" + appVersion: "1.11.9" + description: "Helm chart for deploying Knative Operator" + created: "2023-10-24T09:57:58Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.9/knative-operator-1.11.9.tgz" + digest: "553dd64e8e5e8feea0a7d543f00d25205ce65a8c846618ec02c4cbf740f596dd" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.8" + appVersion: "1.11.8" + description: "Helm chart for deploying Knative Operator" + created: "2023-10-17T09:57:42Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.8/knative-operator-1.11.8.tgz" + digest: "62ab150473602486422e827da8daa90bdd93602c1ec086fccfe63d4ffd77ebd9" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.7" + appVersion: "1.11.7" + description: "Helm chart for deploying Knative Operator" + created: "2023-10-03T09:55:30Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.7/knative-operator-1.11.7.tgz" + digest: "374f6f47d51f50069d5e3561ae7f1076335be5d8c02f5901a8d6aee907062033" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.6" + appVersion: "1.11.6" + description: "Helm chart for deploying Knative Operator" + created: "2023-09-19T09:57:46Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.6/knative-operator-1.11.6.tgz" + digest: "33db13cd0872b3d60c1c949704b2aa9f998868ffa5f81730902b8140a617f6f9" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.5" + appVersion: "1.11.5" + description: "Helm chart for deploying Knative Operator" + created: "2023-09-05T09:55:23Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.5/knative-operator-1.11.5.tgz" + digest: "b1446f07d7f9737411720fa01b393fe0c6bc5e6d7270f14fedd08e1c44dc5b51" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.4" + appVersion: "1.11.4" + description: "Helm chart for deploying Knative Operator" + created: "2023-08-22T09:59:04Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.4/knative-operator-1.11.4.tgz" + digest: "3240764a6b1a87f996e1c75e8cc6c4302eeba3fc0f08fd6cb174e73d1fa3f382" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.3" + appVersion: "1.11.3" + description: "Helm chart for deploying Knative Operator" + created: "2023-08-15T09:57:03Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.3/knative-operator-1.11.3.tgz" + digest: "7a45bb6d14547ccb5248b376b7abd7a749a0a043732a0458e767506f96a13842" + - name: "knative-operator" + apiVersion: v2 + version: "v1.11.2" + appVersion: "1.11.2" + description: "Helm chart for deploying Knative Operator" + created: "2023-08-08T09:54:50Z" + urls: + - "https://github.com/knative/operator/releases/download/knative-v1.11.2/knative-operator-1.11.2.tgz" + digest: "1da435468456875e8627169b0bace33d5fbd8e6fcde8532d0efe92717332e9a2" diff --git a/utils/compatibility/tests/fixtures/knative_operator/provenance.json b/utils/compatibility/tests/fixtures/knative_operator/provenance.json new file mode 100644 index 0000000000..6d145b5944 --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/provenance.json @@ -0,0 +1,14 @@ +[ + { + "file": "release-schedule.md", + "url": "https://raw.githubusercontent.com/knative/community/main/mechanics/RELEASE-SCHEDULE.md", + "retrieved_at": "2026-09-08T11:12:14.264553+00:00", + "sha256": "b0d6b6c33ea0dfee10cbbfb7658299bb9397e39b6dc6936b464f58a9f03e76e9" + }, + { + "file": "operator-index.yaml", + "url": "https://knative.github.io/operator/index.yaml", + "retrieved_at": "2026-09-08T11:12:14.315655+00:00", + "sha256": "1be4fac668b144593a167a36c55c6072fa220417f3af554c1a2080119d9d87d6" + } +] diff --git a/utils/compatibility/tests/fixtures/knative_operator/release-schedule.md b/utils/compatibility/tests/fixtures/knative_operator/release-schedule.md new file mode 100644 index 0000000000..2923b79d89 --- /dev/null +++ b/utils/compatibility/tests/fixtures/knative_operator/release-schedule.md @@ -0,0 +1,80 @@ +--- +title: "Knative Release Schedule" +linkTitle: "Release schedule" +weight: 50 +type: "docs" +--- + +The Knative project releases quarterly, on Tuesday of the 4th week of January, April, July, October, represented by the following Crontab expression: + +``` +# minute hour day month weekday +0 0 22-28 1,4,7,10 2 +``` + +See the [Knative Community Calendar](../CALENDAR.MD) for upcoming release dates and events. + +The end of life (EOL) date for given release is calculated, by taking release date of a `n+2` release, and adding one week to it. + +The minimum Kubernetes version is the oldest K8s version in active community support (not in maintenance mode) at release time. See the [K8s minimum version principle](RELEASE-VERSIONING-PRINCIPLES.md#k8s-minimum-version-principle) for details. + +As much as possible, releases should be driven by automation, and repos should be ready to release at any point. It should also be possible to produce and consume nightly artifacts. + +With that said, it can be useful to have a list of when future releases will happen, so this document provides a schedule for the next 12 months of releases and a list of supported and historical releases. + +## Upcoming releases + +Dates shown are when the core Knative repositories (serving, eventing, etc.) are released. Supporting repositories release approximately one week earlier per the [release timeline](https://github.com/knative/release/blob/main/TIMELINE.md). + +| Release | Date | EOL | Min K8s Version | Notes | +|---------|------------|------------|-----------------| ----- | +| 1.24 | 2026-10-27 | 2027-05-04 | 1.35 | | +| 1.25 | 2027-01-26 | 2027-08-03 | 1.36 | | + +## Releases supported by community + +| Release | Date | EOL | Min K8s Version | Notes | +| ------- | ---------- | ---------- | --------------- | ----- | +| 1.23 | 2026-07-28 | 2027-02-02 | 1.34 | | +| 1.22 | 2026-04-28 | 2026-11-03 | 1.34 | | + +## No longer supported releases + +| Release | Date | EOL | Min K8s Version | Notes | +| ------- | ---------- | ---------- | --------------- |------------------------------------------------| +| 1.21 | 2026-01-27 | 2026-08-04 | 1.33 | | +| 1.20 | 2025-10-21 | 2026-05-05 | 1.32 | | +| 1.19 | 2025-07-22 | 2026-02-03 | 1.32 | | +| 1.18 | 2025-04-22 | 2025-10-28 | 1.31 | | +| 1.17 | 2025-01-28 | 2025-07-28 | 1.30 | | +| 1.16 | 2024-10-22 | 2025-04-22 | 1.29 | | +| 1.15 | 2024-07-23 | 2025-01-21 | 1.28 | | +| 1.14 | 2024-04-23 | 2024-10-22 | 1.28 | | +| 1.13 | 2024-01-23 | 2024-07-22 | 1.27 | | +| 1.12 | 2023-10-24 | 2024-04-30 | 1.26 | | +| 1.11 | 2023-07-25 | 2024-01-30 | 1.25 | | +| 1.10 | 2023-04-25 | 2023-10-31 | 1.24 | | +| 1.9 | 2023-01-24 | 2023-08-01 | 1.24 | | +| 1.8 | 2022-10-18 | 2023-05-02 | 1.23 | | +| 1.7 | 2022-08-23 | 2023-02-21 | 1.22 | | +| 1.6 | 2022-07-12 | 2023-01-10 | 1.22 | | +| 1.5 | 2022-05-31 | 2022-11-29 | 1.22 | | +| 1.4 | 2022-04-19 | 2022-10-11 | 1.22 | | +| 1.3 | 2022-03-08 | 2022-08-30 | 1.21 | | +| 1.2 | 2022-01-25 | 2022-07-12 | 1.21 | | +| 1.1 | 2021-12-14 | 2022-05-31 | 1.20 | | +| 1.0 | 2021-11-02 | 2022-04-19 | 1.20 | | +| 0.26 | 2021-09-21 | 2022-03-08 | 1.19 | | +| 0.25 | 2021-08-10 | 2022-01-25 | 1.19 | | +| 0.24 | 2021-06-29 | 2021-12-14 | 1.19 | | +| 0.23 | 2021-05-18 | 2021-11-02 | 1.18 | Removes v1beta1 Eventing APIs | +| 0.22 | 2021-04-06 | 2021-09-21 | 1.18 | | +| 0.21 | 2021-02-23 | 2021-08-10 | 1.18 | | +| 0.20 | 2021-01-12 | 2021-06-29 | 1.17 | **Moved by 3 weeks for end of year holidays** | +| 0.19 | 2020-11-10 | 2021-05-18 | 1.17 | Removes v1alpha1 and v1beta1 Serving APIs | +| 0.18 | 2020-09-29 | 2021-03-16 | 1.17 | | +| 0.17 | 2020-08-18 | 2021-02-02 | 1.16 | Introduces v1 Eventing APIs, removes v1alpha1 | +| 0.16 | 2020-07-07 | 2020-12-22 | 1.16 | | +| 0.15 | 2020-05-26 | 2020-11-10 | 1.16 | | +| 0.14 | 2020-04-14 | 2020-09-29 | 1.15 | Deprecates v1alpha1 Eventing APIs | +| 0.13 | 2020-03-03 | 2020-08-18 | 1.15 | | diff --git a/utils/compatibility/tests/test_knative_operator.py b/utils/compatibility/tests/test_knative_operator.py new file mode 100644 index 0000000000..a15b0a5556 --- /dev/null +++ b/utils/compatibility/tests/test_knative_operator.py @@ -0,0 +1,315 @@ +"""Offline tests for the upstream Knative schedule, Helm index, and GA history.""" + +from datetime import date +import importlib.util +from pathlib import Path +import unittest + + +SCRAPER_PATH = Path(__file__).resolve().parents[1] / "scrapers" / "knative-operator.py" +SPEC = importlib.util.spec_from_file_location("knative_operator", SCRAPER_PATH) +knative = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(knative) + + +SCHEDULE = """ +## Upcoming releases +| Release | Date | EOL | Min K8s Version | Notes | +| --- | --- | --- | --- | --- | +| 1.24 | 2026-10-27 | 2027-05-04 | 1.35 | scheduled | + +## Releases supported by community +| Release | Date | EOL | Min K8s Version | Notes | +| --- | --- | --- | --- | --- | +| 1.23 | 2026-07-28 | 2027-02-02 | 1.34 | | + +## No longer supported releases +| Release | Date | EOL | Min K8s Version | Notes | +| --- | --- | --- | --- | --- | +| 1.18 | 2025-04-22 | 2025-10-28 | 1.31 | | +""" + +KUBERNETES_RELEASES = { + "1.31": date(2024, 8, 13), + "1.32": date(2024, 12, 11), + "1.33": date(2025, 4, 23), + "1.34": date(2025, 8, 27), + "1.35": date(2025, 12, 17), + "1.36": date(2026, 4, 22), + "1.37": date(2026, 8, 26), +} + + +def helm_index(*pairs): + rows = ["apiVersion: v1", "entries:", " knative-operator:"] + for chart, app in pairs: + rows.extend([f' - version: "{chart}"', f' appVersion: "{app}"']) + return "\n".join(rows) + + +def kubernetes_release(**overrides): + payload = { + "tag_name": "v1.33.0", + "draft": False, + "prerelease": False, + "published_at": "2025-04-23T15:00:00Z", + } + payload.update(overrides) + return payload + + +class ScheduleTests(unittest.TestCase): + def test_reads_release_date_and_floor_from_every_schedule_section(self): + self.assertEqual(knative.parse_schedule(SCHEDULE), { + "1.24": {"minimum": "1.35", "date": date(2026, 10, 27)}, + "1.23": {"minimum": "1.34", "date": date(2026, 7, 28)}, + "1.18": {"minimum": "1.31", "date": date(2025, 4, 22)}, + }) + + def test_matches_reordered_headers_and_normalizes_version_prefixes(self): + source = """ +| Notes | Min K8s Version | Date | Release | +| :--- | ---: | :---: | --- | +| example | v1.34 | 2026-07-28 | v1.23 | +""" + self.assertEqual(knative.parse_schedule(source), { + "1.23": {"minimum": "1.34", "date": date(2026, 7, 28)}, + }) + + def test_ignores_unrelated_tables(self): + unrelated = "| Name | Count |\n| --- | --- |\n| widgets | 4 |\n\n" + self.assertEqual(knative.parse_schedule(unrelated + SCHEDULE), + knative.parse_schedule(SCHEDULE)) + + def test_rejects_missing_or_empty_schedule(self): + for source in ["", "Unavailable", SCHEDULE.split("| 1.24")[0]]: + with self.subTest(source=source), self.assertRaises(ValueError): + knative.parse_schedule(source) + + def test_rejects_invalid_release_or_minimum_version(self): + for value in ["1.x", "1.34 or 1.35", "", "01.34", "1.34.0"]: + for original in ["1.23", "1.34"]: + with self.subTest(value=value, original=original), self.assertRaises(ValueError): + knative.parse_schedule(SCHEDULE.replace(original, value)) + + def test_rejects_invalid_or_missing_date(self): + for value in ["", "2026-02-30", "28 July 2026", "TBD"]: + with self.subTest(value=value), self.assertRaises(ValueError): + knative.parse_schedule(SCHEDULE.replace("2026-07-28", value)) + + def test_rejects_truncated_rows(self): + source = SCHEDULE.replace("| 1.23 | 2026-07-28 | 2027-02-02 | 1.34 | |", "| 1.23 |") + with self.assertRaises(ValueError): + knative.parse_schedule(source) + + def test_rejects_duplicate_required_header(self): + source = "| Release | Release | Date | Min K8s Version |\n|---|---|---|---|\n|1.23|1.22|2026-07-28|1.34|" + with self.assertRaises(ValueError): + knative.parse_schedule(source) + + def test_rejects_missing_date_header(self): + with self.assertRaises(ValueError): + knative.parse_schedule(SCHEDULE.replace("| Date |", "| Published |")) + + def test_collapses_identical_duplicate_rows(self): + self.assertEqual(knative.parse_schedule(SCHEDULE + SCHEDULE), + knative.parse_schedule(SCHEDULE)) + + def test_rejects_duplicate_release_with_conflicting_floor_or_date(self): + for duplicate in [SCHEDULE.replace("1.34", "1.33"), + SCHEDULE.replace("2026-07-28", "2026-07-29")]: + with self.subTest(duplicate=duplicate), self.assertRaises(ValueError): + knative.parse_schedule(SCHEDULE + duplicate) + + +class ChartTests(unittest.TestCase): + def test_preserves_exact_chart_tag_and_normalizes_app_version(self): + self.assertEqual(knative.parse_charts(helm_index(("v1.23.1", "v1.23.1"))), [ + {"version": "1.23.1", "chart_version": "v1.23.1"}, + ]) + + def test_excludes_prereleases_of_either_chart_or_app(self): + source = helm_index( + ("v1.24.0-rc.1", "1.24.0"), + ("v1.24.0", "1.24.0-dev.20260908"), + ("v1.23.1", "1.23.1"), + ) + self.assertEqual(knative.parse_charts(source), [ + {"version": "1.23.1", "chart_version": "v1.23.1"}, + ]) + + def test_rejects_app_build_metadata_unsupported_by_shared_updater(self): + source = helm_index(("v1.23.1+chart.2", "1.23.1+build.7")) + with self.assertRaises(ValueError): + knative.parse_charts(source) + + def test_sorts_app_versions_numerically_in_descending_order(self): + pairs = [("v1.9.10", "1.9.10"), ("v1.23.2", "1.23.2"), ("v1.23.10", "1.23.10")] + result = knative.parse_charts(helm_index(*pairs)) + self.assertEqual([row["version"] for row in result], ["1.23.10", "1.23.2", "1.9.10"]) + self.assertEqual(result, knative.parse_charts(helm_index(*reversed(pairs)))) + + def test_keeps_numerically_latest_chart_for_the_same_app(self): + pairs = [("v1.23.2", "1.23.1"), ("v1.23.10", "1.23.1"), ("v1.23.1", "1.23.1")] + expected = [{"version": "1.23.1", "chart_version": "v1.23.10"}] + self.assertEqual(knative.parse_charts(helm_index(*pairs)), expected) + self.assertEqual(knative.parse_charts(helm_index(*reversed(pairs))), expected) + + def test_deduplicates_identical_entries(self): + source = helm_index(("v1.23.1", "1.23.1"), ("v1.23.1", "1.23.1")) + self.assertEqual(knative.parse_charts(source), [ + {"version": "1.23.1", "chart_version": "v1.23.1"}, + ]) + + def test_rejects_chart_mapped_to_conflicting_app_versions(self): + with self.assertRaises(ValueError): + knative.parse_charts(helm_index(("v1.23.1", "1.23.1"), ("v1.23.1", "1.23.0"))) + + def test_rejects_malformed_yaml_or_empty_chart_data(self): + sources = ["[", "null", "[]", "entries: {}", "entries:\n knative-operator: []", + "entries:\n knative-operator: [null]", + "entries:\n knative-operator: {version: '1.23.1'}"] + for source in sources: + with self.subTest(source=source), self.assertRaises(ValueError): + knative.parse_charts(source) + + def test_rejects_malformed_chart_or_app_versions(self): + for version in ["latest", "1.23", "01.23.1", "1.23.1-", ""]: + for pair in [(version, "1.23.1"), ("1.23.1", version)]: + with self.subTest(pair=pair), self.assertRaises(ValueError): + knative.parse_charts(helm_index(pair)) + + def test_rejects_missing_chart_or_app_version_field(self): + for field in ["version", "appVersion"]: + source = f'entries:\n knative-operator:\n - {field}: "1.23.1"' + with self.subTest(field=field), self.assertRaises(ValueError): + knative.parse_charts(source) + + def test_rejects_index_containing_only_prereleases(self): + with self.assertRaises(ValueError): + knative.parse_charts(helm_index(("v1.24.0-rc.1", "1.24.0-rc.1"))) + + +class KubernetesReleaseTests(unittest.TestCase): + def test_reads_minor_and_ga_publication_date(self): + self.assertEqual(knative.parse_kubernetes_release(kubernetes_release(), "v1.33.0"), + ("1.33", date(2025, 4, 23))) + + def test_accepts_explicit_timezone_offset(self): + payload = kubernetes_release(published_at="2025-04-23T15:00:00+00:00") + self.assertEqual(knative.parse_kubernetes_release(payload, "v1.33.0"), + ("1.33", date(2025, 4, 23))) + + def test_rejects_a_release_for_a_different_tag(self): + with self.assertRaises(ValueError): + knative.parse_kubernetes_release(kubernetes_release(tag_name="v1.32.0"), "v1.33.0") + + def test_rejects_non_ga_expected_tags(self): + for tag in ["1.33.0", "v1.33.1", "v1.33.0-rc.1", "v1.33.0+build.1", "v2.33.0"]: + with self.subTest(tag=tag), self.assertRaises(ValueError): + knative.parse_kubernetes_release(kubernetes_release(tag_name=tag), tag) + + def test_rejects_drafts_and_prereleases(self): + for field in ["draft", "prerelease"]: + for value in [True, "false", None]: + with self.subTest(field=field, value=value), self.assertRaises(ValueError): + knative.parse_kubernetes_release(kubernetes_release(**{field: value}), "v1.33.0") + + def test_rejects_missing_required_release_fields(self): + for field in ["tag_name", "draft", "prerelease", "published_at"]: + payload = kubernetes_release() + del payload[field] + with self.subTest(field=field), self.assertRaises(ValueError): + knative.parse_kubernetes_release(payload, "v1.33.0") + + def test_rejects_missing_timezone_or_invalid_timestamp(self): + for timestamp in [None, "", "2025-04-23", "2025-04-23T15:00:00", "2025-02-30T15:00:00Z"]: + with self.subTest(timestamp=timestamp), self.assertRaises(ValueError): + knative.parse_kubernetes_release(kubernetes_release(published_at=timestamp), "v1.33.0") + + def test_rejects_non_object_payloads(self): + for payload in [None, [], "error"]: + with self.subTest(payload=payload), self.assertRaises(ValueError): + knative.parse_kubernetes_release(payload, "v1.33.0") + + +class CompatibilityTests(unittest.TestCase): + def test_knative_118_excludes_kubernetes_133_released_the_next_day(self): + rows = knative.build_versions(SCHEDULE, helm_index(("v1.18.0", "1.18.0")), KUBERNETES_RELEASES) + self.assertEqual(rows, [{ + "version": "1.18.0", + "chart_version": "v1.18.0", + "kube": ["1.32", "1.31"], + "requirements": [], + "incompatibilities": [], + }]) + + def test_patch_release_keeps_the_minor_cutoff_after_a_new_kubernetes_ga(self): + index = helm_index(("v1.23.0", "1.23.0"), ("v1.23.1", "1.23.1")) + index += '\n created: "2026-09-01T10:17:05Z"\n' + rows = knative.build_versions(SCHEDULE, index, KUBERNETES_RELEASES) + self.assertEqual([row["version"] for row in rows], ["1.23.1", "1.23.0"]) + for row in rows: + self.assertEqual(row["kube"], ["1.36", "1.35", "1.34"]) + + def test_includes_a_kubernetes_ga_on_the_exact_minor_release_date(self): + history = dict(KUBERNETES_RELEASES, **{"1.33": date(2025, 4, 22)}) + rows = knative.build_versions(SCHEDULE, helm_index(("v1.18.0", "1.18.0")), history) + self.assertEqual(rows[0]["kube"], ["1.33", "1.32", "1.31"]) + + def test_emits_only_chart_versions_even_when_schedule_has_future_releases(self): + rows = knative.build_versions(SCHEDULE, helm_index(("v1.23.1", "1.23.1")), KUBERNETES_RELEASES) + self.assertEqual([row["version"] for row in rows], ["1.23.1"]) + + def test_unknown_released_minor_fails_the_whole_join(self): + index = helm_index(("v1.23.1", "1.23.1"), ("v1.99.0", "1.99.0")) + with self.assertRaises(ValueError): + knative.build_versions(SCHEDULE, index, KUBERNETES_RELEASES) + + def test_rejects_floor_that_was_not_ga_at_the_minor_cutoff(self): + source = SCHEDULE.replace("| 1.31 |", "| 1.33 |") + with self.assertRaises(ValueError): + knative.build_versions(source, helm_index(("v1.18.0", "1.18.0")), KUBERNETES_RELEASES) + + def test_rejects_missing_kubernetes_floor(self): + history = {minor: day for minor, day in KUBERNETES_RELEASES.items() if minor != "1.34"} + with self.assertRaises(ValueError): + knative.build_versions(SCHEDULE, helm_index(("v1.23.1", "1.23.1")), history) + + def test_rejects_gaps_in_kubernetes_history(self): + for missing_minor in ["1.32", "1.36"]: + history = {minor: day for minor, day in KUBERNETES_RELEASES.items() if minor != missing_minor} + with self.subTest(missing_minor=missing_minor), self.assertRaises(ValueError): + knative.build_versions(SCHEDULE, helm_index(("v1.18.0", "1.18.0")), history) + + def test_rejects_non_increasing_kubernetes_ga_dates(self): + for invalid_date in [KUBERNETES_RELEASES["1.35"], date(2025, 12, 16)]: + history = dict(KUBERNETES_RELEASES, **{"1.36": invalid_date}) + with self.subTest(invalid_date=invalid_date), self.assertRaises(ValueError): + knative.build_versions(SCHEDULE, helm_index(("v1.23.1", "1.23.1")), history) + + def test_does_not_require_history_older_than_the_lowest_needed_floor(self): + history = {minor: day for minor, day in KUBERNETES_RELEASES.items() + if int(minor.split(".")[1]) >= 34} + rows = knative.build_versions(SCHEDULE, helm_index(("v1.23.1", "1.23.1")), history) + self.assertEqual(rows[0]["kube"], ["1.36", "1.35", "1.34"]) + + def test_output_is_independent_of_source_order(self): + pairs = [("v1.18.0", "1.18.0"), ("v1.23.2", "1.23.2"), ("v1.23.10", "1.23.10")] + result = knative.build_versions(SCHEDULE, helm_index(*pairs), KUBERNETES_RELEASES) + reversed_history = dict(reversed(list(KUBERNETES_RELEASES.items()))) + self.assertEqual(result, knative.build_versions(SCHEDULE, helm_index(*reversed(pairs)), reversed_history)) + self.assertEqual([row["version"] for row in result], ["1.23.10", "1.23.2", "1.18.0"]) + + def test_rejects_chart_build_metadata_with_no_ordered_precedence(self): + with self.assertRaises(ValueError): + knative.build_versions(SCHEDULE, helm_index(("v1.23.1+chart.2", "1.23.1")), + KUBERNETES_RELEASES) + + def test_rejects_conflicting_chart_aliases(self): + with self.assertRaises(ValueError): + knative.parse_charts(helm_index(("v1.23.1", "1.23.1"), ("1.23.1", "1.23.0"))) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/compatibility/tests/test_knative_recorded_sources.py b/utils/compatibility/tests/test_knative_recorded_sources.py new file mode 100644 index 0000000000..fe829ff210 --- /dev/null +++ b/utils/compatibility/tests/test_knative_recorded_sources.py @@ -0,0 +1,69 @@ +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +import yaml + + +COMPATIBILITY = Path(__file__).resolve().parents[1] +FIXTURES = Path(__file__).parent / "fixtures/knative_operator" +ROOT = COMPATIBILITY.parents[1] + + +def load_module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +knative = load_module("knative_recorded", COMPATIBILITY / "scrapers/knative-operator.py") + + +class RecordedSourceTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + history = dict(knative.parse_kubernetes_release(record, record["tag_name"]) + for record in json.loads((FIXTURES / "kubernetes-ga-releases.json").read_text())) + cls.versions = knative.build_versions( + (FIXTURES / "release-schedule.md").read_text(), + (FIXTURES / "operator-index.yaml").read_text(), history, + ) + + def test_covers_all_recorded_stable_charts_and_historical_cutoffs(self): + self.assertEqual(len(self.versions), 83) + self.assertEqual(self.versions[0]["version"], "1.23.1") + self.assertEqual(self.versions[-1]["version"], "1.11.2") + by_version = {row["version"]: row for row in self.versions} + self.assertEqual(by_version["1.18.0"]["kube"], ["1.32", "1.31"]) + self.assertEqual(by_version["1.16.6"]["kube"], ["1.31", "1.30", "1.29"]) + self.assertFalse(any("1.37" in row["kube"] for row in self.versions)) + + def test_shared_updater_preserves_metadata_and_is_idempotent(self): + # Isolate optional summaries and Helm; exercise the actual merge/reducer/writer. + summarizer = SimpleNamespace(helm_summary=lambda *args: None, summarization_enabled=lambda: False) + with patch.dict(sys.modules, {"summarizer": summarizer}): + shared = load_module("knative_shared_utils", COMPATIBILITY / "utils.py") + table = yaml.safe_load((ROOT / "static/compatibilities/knative-operator.yaml").read_text()) + self.assertEqual(table["versions"], shared.reduce_versions(self.versions)) + metadata = {key: value for key, value in table.items() if key != "versions"} + with tempfile.TemporaryDirectory() as directory: + target = Path(directory) / "knative-operator.yaml" + shared.write_yaml(str(target), table) + with patch.object(shared, "get_chart_images", return_value=[]), patch("builtins.print"): + shared.update_compatibility_info(str(target), self.versions) + first = target.read_bytes() + shared.update_compatibility_info(str(target), self.versions) + updated = yaml.safe_load(first) + self.assertEqual(target.read_bytes(), first) + self.assertEqual({key: value for key, value in updated.items() if key != "versions"}, metadata) + self.assertEqual(updated["versions"], shared.reduce_versions(self.versions)) + + +if __name__ == "__main__": + unittest.main() diff --git a/utils/compatibility/tests/test_knative_scrape.py b/utils/compatibility/tests/test_knative_scrape.py new file mode 100644 index 0000000000..d32c511b21 --- /dev/null +++ b/utils/compatibility/tests/test_knative_scrape.py @@ -0,0 +1,86 @@ +import importlib.util +import json +from pathlib import Path +import sys +from types import SimpleNamespace +import unittest +from unittest.mock import Mock, patch +from urllib.error import URLError + + +MODULE_PATH = Path(__file__).resolve().parents[1] / "scrapers/knative-operator.py" +spec = importlib.util.spec_from_file_location("knative_operator_adapter", MODULE_PATH) +scraper = importlib.util.module_from_spec(spec) +spec.loader.exec_module(scraper) + +SCHEDULE = """ +| Release | Date | Min K8s Version | +| --- | --- | --- | +| 1.23 | 2026-07-28 | 1.34 | +""" +INDEX = """ +entries: + knative-operator: + - version: v1.23.1 + appVersion: 1.23.1 +""" + + +class ScrapeTests(unittest.TestCase): + def setUp(self): + self.helper = SimpleNamespace(update_compatibility_info=Mock(), print_error=Mock()) + self.responses = { + scraper.SCHEDULE_URL: SCHEDULE, + scraper.INDEX_URL: INDEX, + scraper.KUBERNETES_STABLE_URL: "v1.37.0\n", + } + for minor, published in [ + (34, "2025-08-27"), (35, "2025-12-17"), + (36, "2026-04-22"), (37, "2026-08-26"), + ]: + tag = f"v1.{minor}.0" + self.responses[f"https://api.github.com/repos/kubernetes/kubernetes/releases/tags/{tag}"] = json.dumps({ + "tag_name": tag, "draft": False, "prerelease": False, + "published_at": published + "T12:00:00Z", + }) + + def test_updates_only_after_complete_source_validation(self): + with patch.dict(sys.modules, {"utils": self.helper}), patch.object( + scraper, "fetch_text", side_effect=self.responses.__getitem__, + ): + scraper.scrape() + self.helper.print_error.assert_not_called() + self.helper.update_compatibility_info.assert_called_once_with(scraper.TARGET_FILE, [{ + "version": "1.23.1", "chart_version": "v1.23.1", + "kube": ["1.36", "1.35", "1.34"], "requirements": [], "incompatibilities": [], + }]) + + def test_fetch_failure_preserves_existing_table(self): + with patch.dict(sys.modules, {"utils": self.helper}), patch.object( + scraper, "fetch_text", side_effect=URLError("upstream unavailable"), + ): + scraper.scrape() + self.helper.update_compatibility_info.assert_not_called() + self.helper.print_error.assert_called_once() + + def test_incomplete_ga_source_preserves_existing_table(self): + self.responses["https://api.github.com/repos/kubernetes/kubernetes/releases/tags/v1.35.0"] = "{}" + with patch.dict(sys.modules, {"utils": self.helper}), patch.object( + scraper, "fetch_text", side_effect=self.responses.__getitem__, + ): + scraper.scrape() + self.helper.update_compatibility_info.assert_not_called() + self.helper.print_error.assert_called_once() + + def test_malformed_stable_marker_preserves_existing_table(self): + self.responses[scraper.KUBERNETES_STABLE_URL] = "v1.99.0-alpha.1" + with patch.dict(sys.modules, {"utils": self.helper}), patch.object( + scraper, "fetch_text", side_effect=self.responses.__getitem__, + ): + scraper.scrape() + self.helper.update_compatibility_info.assert_not_called() + self.helper.print_error.assert_called_once() + + +if __name__ == "__main__": + unittest.main()