This is an automated email from the ASF dual-hosted git repository.

jedcunningham pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new b43256b0882 Remove significant newsfragment template (#60545)
b43256b0882 is described below

commit b43256b0882a63bc86494ab304767968c568baa9
Author: Jed Cunningham <[email protected]>
AuthorDate: Wed Jan 14 13:23:11 2026 -0700

    Remove significant newsfragment template (#60545)
    
    We added structure to these to keep track of the large number of
    breaking changes that were coming for Airflow 3. We no longer need any
    of this structure or enforcement - we can go back to letting 
authors/reviewers
    steer the content.
---
 .github/workflows/news-fragment.yml                |  63 -----
 airflow-core/.pre-commit-config.yaml               |  10 -
 airflow-core/newsfragments/config.toml             |   2 +-
 .../newsfragments/template.significant.rst         |  33 ---
 contributing-docs/18_contribution_workflow.rst     |  10 +-
 dev/breeze/src/airflow_breeze/utils/github.py      |   2 +-
 .../ci/prek/significant_newsfragments_checker.py   | 253 ---------------------
 7 files changed, 4 insertions(+), 369 deletions(-)

diff --git a/.github/workflows/news-fragment.yml 
b/.github/workflows/news-fragment.yml
deleted file mode 100644
index 45efd3364ea..00000000000
--- a/.github/workflows/news-fragment.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-# 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.
-#
----
-name: CI
-
-on:  # yamllint disable-line rule:truthy
-  pull_request:
-    types: [labeled, unlabeled, opened, reopened, synchronize]
-permissions:
-  contents: read
-jobs:
-  check-news-fragment:
-    name: Check News Fragment
-    runs-on: ubuntu-22.04
-    if: "contains(github.event.pull_request.labels.*.name, 
'airflow3.0:breaking')"
-
-    steps:
-      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # 
v4.2.2
-        with:
-          persist-credentials: false
-          # `towncrier check` runs `git diff --name-only origin/main...`, which
-          # needs a non-shallow clone.
-          fetch-depth: 0
-
-      - name: Check news fragment existence
-        env:
-          BASE_REF: ${{ github.base_ref }}
-        run: >
-          python -m pip install --upgrade uv &&
-          uv tool run towncrier check
-          --dir airflow-core
-          --config airflow-core/newsfragments/config.toml
-          --compare-with origin/${BASE_REF}
-          ||
-          {
-          printf "\033[1;33mMissing significant newsfragment for PR labeled 
with
-          'airflow3.0:breaking'.\nCheck
-          
https://github.com/apache/airflow/blob/main/contributing-docs/18_contribution_workflow.rst
-          for guidance.\033[m\n"
-          &&
-          false
-          ; }
-
-      - name: Check news fragment format
-        env:
-          BASE_REF: ${{ github.base_ref }}
-        run: >
-          uv run scripts/ci/prek/significant_newsfragments_checker.py
diff --git a/airflow-core/.pre-commit-config.yaml 
b/airflow-core/.pre-commit-config.yaml
index a89c2be8b29..f1bb1de4425 100644
--- a/airflow-core/.pre-commit-config.yaml
+++ b/airflow-core/.pre-commit-config.yaml
@@ -148,16 +148,6 @@ repos:
         language: python
         pass_filenames: true
         files: ^src/airflow/.*\.py$
-      - id: check-significant-newsfragments-are-valid
-        name: Check significant newsfragments are valid
-        # Significant newsfragments follows a special format so that we can 
group information easily.
-        language: python
-        files: ^newsfragments/.*\.rst$
-        entry: ../scripts/ci/prek/significant_newsfragments_checker.py
-        pass_filenames: false
-        # We sometimes won't have newsfragments in the repo, so always run it 
so `check-hooks-apply` passes
-        # This is fast, so not too much downside
-        always_run: true
       - id: create-missing-init-py-files-tests
         name: Create missing init.py files in tests
         entry: ../scripts/ci/prek/check_init_in_tests.py
diff --git a/airflow-core/newsfragments/config.toml 
b/airflow-core/newsfragments/config.toml
index cdff723d95c..55856f6ad66 100644
--- a/airflow-core/newsfragments/config.toml
+++ b/airflow-core/newsfragments/config.toml
@@ -18,7 +18,7 @@
 name = "Airflow"
 filename = "../RELEASE_NOTES.rst"
 underlines = ["-", '^']
-ignore = ["config.toml", "template.significant.rst"]
+ignore = ["config.toml"]
 
 [[tool.towncrier.type]]
 directory = "significant"
diff --git a/airflow-core/newsfragments/template.significant.rst 
b/airflow-core/newsfragments/template.significant.rst
deleted file mode 100644
index 464486489c6..00000000000
--- a/airflow-core/newsfragments/template.significant.rst
+++ /dev/null
@@ -1,33 +0,0 @@
-.. Write a short and imperative summary of this changes
-
-.. Provide additional contextual information
-
-.. Check the type of change that applies to this change
-.. Dag changes: requires users to change their Dag code
-.. Config changes: requires users to change their Airflow config
-.. API changes: requires users to change their Airflow REST API calls
-.. CLI changes: requires users to change their Airflow CLI usage
-.. Behaviour changes: the existing code won't break, but the behavior is 
different
-.. Plugin changes: requires users to change their Airflow plugin implementation
-.. Dependency changes: requires users to change their dependencies (e.g., 
Postgres 12)
-.. Code interface changes: requires users to change other implementations 
(e.g., auth manager)
-
-* Types of change
-
-  * [ ] Dag changes
-  * [ ] Config changes
-  * [ ] API changes
-  * [ ] CLI changes
-  * [ ] Behaviour changes
-  * [ ] Plugin changes
-  * [ ] Dependency changes
-  * [ ] Code interface changes
-
-.. List the migration rules needed for this change (see 
https://github.com/apache/airflow/issues/41641)
-
-* Migration rules needed
-
-.. e.g.,
-.. * Remove context key ``execution_date``
-.. * context key ``triggering_dataset_events`` → ``triggering_asset_events``
-.. * Remove method 
``airflow.providers_manager.ProvidersManager.initialize_providers_dataset_uri_resources``
 → 
``airflow.providers_manager.ProvidersManager.initialize_providers_asset_uri_resources``
diff --git a/contributing-docs/18_contribution_workflow.rst 
b/contributing-docs/18_contribution_workflow.rst
index 438284c063f..8cbc37c98cd 100644
--- a/contributing-docs/18_contribution_workflow.rst
+++ b/contributing-docs/18_contribution_workflow.rst
@@ -196,14 +196,8 @@ Step 4: Prepare PR
      and place in either `airflow-core/newsfragments 
</airflow-core/newsfragments>`__ for core newsfragments,
      or `chart/newsfragments </chart/newsfragments>`__ for helm chart 
newsfragments.
 
-     In general newsfragments must be one line.  For newsfragment type 
``significant``,
-     you should follow the template in 
``airflow-core/newsfragments/template.significant.rst`` to include summary, 
body, change type and migrations rules needed.
-     One thing to note here is that a ``significant`` newsfragment always 
doesn't have to be a breaking change, i.e. it can not have a change type and 
migration rules.
-     This can also be done by the following command.
-
-     .. code-block:: bash
-
-        uv tool run towncrier create --dir airflow-core --config 
newsfragments/config.toml --content "`cat 
airflow-core/newsfragments/template.significant.rst`"
+     In general newsfragments must be one line.  For newsfragment type 
``significant``, you may include summary and body separated by a blank line, 
similar to ``git`` commit messages.
+     One thing to note here is that a ``significant`` newsfragment doesn't 
have to be a breaking change, it can be something that is notable but not 
breaking.
 
 2. Rebase your fork, squash commits, and resolve all conflicts. See `How to 
rebase PR <10_working_with_git.rst#how-to-rebase-pr>`_
    if you need help with rebasing your change. Remember to rebase often if 
your PR takes a lot of time to
diff --git a/dev/breeze/src/airflow_breeze/utils/github.py 
b/dev/breeze/src/airflow_breeze/utils/github.py
index 4b4ea3f5022..40e24e58361 100644
--- a/dev/breeze/src/airflow_breeze/utils/github.py
+++ b/dev/breeze/src/airflow_breeze/utils/github.py
@@ -345,7 +345,7 @@ def download_artifact_from_pr(pr: str, output_file: Path, 
github_repository: str
     data = workflow_runs.json()["workflow_runs"]
     sorted_data = sorted(data, key=lambda x: 
datetime.fromisoformat(x["created_at"]), reverse=True)
     run_id = None
-    # Filter only workflow with ci.yml, we may get multiple workflows for a PR 
ex: codeql-analysis.yml, news-fragment.yml
+    # Filter only workflow with ci.yml, we may get multiple workflows for a PR 
ex: codeql-analysis.yml
 
     for run in sorted_data:
         if run.get("path").endswith("ci.yml"):
diff --git a/scripts/ci/prek/significant_newsfragments_checker.py 
b/scripts/ci/prek/significant_newsfragments_checker.py
deleted file mode 100755
index 3e0d12d3ee1..00000000000
--- a/scripts/ci/prek/significant_newsfragments_checker.py
+++ /dev/null
@@ -1,253 +0,0 @@
-#!/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.
-# /// script
-# requires-python = ">=3.10,<3.11"
-# dependencies = [
-#   "docutils>=0.21.2",
-#   "jinja2>=3.1.5",
-#   "pygments>=2.19.1",
-# ]
-# ///
-
-from __future__ import annotations
-
-import argparse
-import csv
-import glob
-import re
-
-import docutils.nodes
-from docutils.core import publish_doctree
-from jinja2 import BaseLoader, Environment
-
-UNDONE_LIST_TEMPLATE = """
----Undone rules found in "{{ filename }}"---
-{% if undone_ruff_rules -%}
-======Ruff rules======
-    {%- for ruld_id, rules in undone_ruff_rules.items() %}
-* Code: {{ ruld_id }}
-    {% for rule in rules %}* {{ rule }}
-    {% endfor %}
-    {%- endfor %}
-{%- endif -%}
-{%- if undone_config_rules %}
-======airflow config lint rules======
-{% for rule in undone_config_rules %}* {{ rule }}
-{% endfor %}
-{% endif %}
-"""
-
-
-class SignificantNewsFragmentVisitor(docutils.nodes.NodeVisitor):
-    """Visitor to collect significant newsfragement content."""
-
-    TYPES_OF_CHANGE_TITLE = "Types of change"
-    EXPECTED_TYPE_OF_CHANGES = {
-        "Dag changes",
-        "Config changes",
-        "API changes",
-        "CLI changes",
-        "Behaviour changes",
-        "Plugin changes",
-        "Dependency changes",
-        "Code interface changes",
-    }
-    MIGRATION_RULE_TITLE = "Migration rules needed"
-    CONFIG_RULE_TITLE = "airflow config lint"
-    RUFF_RULE_TITLE = "ruff"
-
-    def __init__(self, *args, **kwargs) -> None:
-        super().__init__(*args, **kwargs)
-
-        self.types_of_change: dict[str, bool] = {}
-        self.ruff_rules: dict[str, list[tuple[bool, str]]] = {}
-        self.config_rules: list[tuple[bool, str]] = []
-
-    def visit_list_item(self, node: docutils.nodes.list_item) -> None:
-        list_title = node[0].astext()
-
-        for title, extract_func in (
-            (self.TYPES_OF_CHANGE_TITLE, self._extract_type_of_changes),
-            (self.MIGRATION_RULE_TITLE, self._extract_migration_rules),
-        ):
-            if list_title == title:
-                list_content = node[1]
-                if not isinstance(list_content, docutils.nodes.bullet_list):
-                    raise ValueError(f"Incorrect format 
{list_content.astext()}")
-
-                extract_func(list_content)
-                break
-
-    def _extract_type_of_changes(self, node: docutils.nodes.bullet_list) -> 
None:
-        for change in node:
-            checked, change_type = self._extract_check_list_item(change)
-            self.types_of_change[change_type] = checked
-
-        change_types = set(self.types_of_change.keys())
-        missing_keys = self.EXPECTED_TYPE_OF_CHANGES - change_types
-        if missing_keys:
-            raise ValueError(f"Missing type of changes: {missing_keys}")
-
-        unexpected_keys = change_types - self.EXPECTED_TYPE_OF_CHANGES
-        if unexpected_keys:
-            raise ValueError(f"Unexpected type of changes: {unexpected_keys}")
-
-    def _extract_migration_rules(self, node: docutils.nodes.bullet_list) -> 
None:
-        for sub_node in node:
-            if not isinstance(sub_node, docutils.nodes.list_item):
-                raise ValueError(f"Incorrect format {sub_node.astext()}")
-
-            list_title = sub_node[0].astext()
-            list_content = sub_node[1]
-
-            if list_title == self.CONFIG_RULE_TITLE:
-                if not isinstance(list_content, docutils.nodes.bullet_list):
-                    raise ValueError(f"Incorrect format 
{list_content.astext()}")
-
-                self.config_rules = [self._extract_check_list_item(item) for 
item in list_content]
-            elif list_title == self.RUFF_RULE_TITLE:
-                if not isinstance(list_content, docutils.nodes.bullet_list):
-                    raise ValueError("Incorrect format")
-
-                self._extract_ruff_rules(list_content)
-
-    def _extract_ruff_rules(self, node: docutils.nodes.bullet_list) -> None:
-        for ruff_node in node:
-            if not isinstance(ruff_node, docutils.nodes.list_item):
-                raise ValueError(f"Incorrect format {ruff_node.astext()}")
-
-            ruff_rule_id = ruff_node[0].astext()
-            rules_node = ruff_node[1]
-
-            if not isinstance(rules_node, docutils.nodes.bullet_list):
-                raise ValueError(f"Incorrect format {rules_node.astext()}")
-
-            self.ruff_rules[ruff_rule_id] = 
[self._extract_check_list_item(rule) for rule in rules_node]
-
-    def unknown_visit(self, node: docutils.nodes.Node) -> None:
-        """Handle other nodes."""
-
-    @staticmethod
-    def _extract_check_list_item(node: docutils.nodes.Node) -> tuple[bool, 
str]:
-        if not isinstance(node, docutils.nodes.list_item):
-            raise ValueError(f"Incorrect format {node.astext()}")
-
-        text = node.astext()
-        if text[0] != "[" or text[2] != "]":
-            raise ValueError(
-                f"{text} should be a checklist (e.g., * [ ] 
``logging.dag_processor_manager_log_location``)"
-            )
-        return text[:3] == "[x]", text[4:]
-
-    @property
-    def formatted_ruff_rules(self) -> str:
-        str_repr = ""
-        for rule_id, rules in self.ruff_rules.items():
-            str_repr += f"**{rule_id}**\n" + "\n".join(f"* {content}" for _, 
content in rules)
-        return str_repr
-
-    @property
-    def formatted_config_rules(self) -> str:
-        return "\n".join(f"* {content}" for _, content in self.config_rules)
-
-    @property
-    def undone_ruff_rules(self) -> dict[str, list[str]]:
-        undone_ruff_rules = {}
-        for rule_id, rules in self.ruff_rules.items():
-            undone_rules = [rule for checked, rule in rules if checked is 
False]
-            if undone_rules:
-                undone_ruff_rules[rule_id] = undone_rules
-        return undone_ruff_rules
-
-    @property
-    def undone_config_rules(self) -> list[str]:
-        return [rule[1] for rule in self.config_rules if rule[0] is False]
-
-    @property
-    def has_undone_rules(self) -> bool:
-        return bool(self.undone_ruff_rules) or bool(self.undone_config_rules)
-
-
-def parse_significant_newsfragment(source: str) -> 
SignificantNewsFragmentVisitor:
-    document = publish_doctree(source)
-    visitor = SignificantNewsFragmentVisitor(document)
-    document.walk(visitor)
-    return visitor
-
-
-def parse_newsfragment_file(filename: str, *, export: bool, list_todo: bool) 
-> None:
-    content = newsfragment_file.read()
-    description = content.split("* Types of change")[0]
-    title = description.split("\n")[0]
-
-    visitor = parse_significant_newsfragment(content)
-    if not len(visitor.types_of_change):
-        raise ValueError("Missing type of changes")
-
-    if export:
-        newsfragment_details.append(
-            {
-                "AIP or PR name": aip_pr_name,
-                "Title": title,
-                "Description": description,
-            }
-            | visitor.types_of_change
-            | {
-                "Ruff rules": visitor.formatted_ruff_rules,
-                "Config rules": visitor.formatted_config_rules,
-            }
-        )
-
-    if list_todo and visitor.has_undone_rules:
-        jinja_loader = Environment(loader=BaseLoader(), autoescape=True)
-        undone_msg = jinja_loader.from_string(UNDONE_LIST_TEMPLATE).render(
-            filename=filename,
-            undone_ruff_rules=visitor.undone_ruff_rules,
-            undone_config_rules=visitor.undone_config_rules,
-        )
-        print(undone_msg)
-
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(description="Summarize Significant 
Newsfragments")
-    parser.add_argument("--list-todo", help="List undone migration rules", 
action="store_true")
-    parser.add_argument("--export", help="Export the summarization to provided 
output path in csv format")
-    args = parser.parse_args()
-
-    newsfragment_details: list[dict] = []
-    for filename in glob.glob("airflow-core/newsfragments/*.significant.rst"):
-        if filename == "airflow-core/newsfragments/template.significant.rst":
-            continue
-
-        match = 
re.search(r"airflow-core/newsfragments/(.*)\.significant\.rst", filename)
-        if not match:
-            raise ValueError()
-        aip_pr_name = match.group(1)
-
-        with open(filename) as newsfragment_file:
-            try:
-                parse_newsfragment_file(filename, export=args.export, 
list_todo=args.list_todo)
-            except ValueError as e:
-                print(f'Error found in "{filename}"')
-                raise e
-
-    if args.export:
-        with open(args.export, "w") as summary_file:
-            writer = csv.DictWriter(summary_file, 
fieldnames=newsfragment_details[0].keys())
-            writer.writeheader()
-            writer.writerows(newsfragment_details)

Reply via email to