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)