Fix(ci): Detect breaking titles in note check

The autolabeler adds the breaking-change label with GITHUB_TOKEN,
which does not trigger a new workflow run, so a 'Type!:' pull request
could pass the release note check unlabelled. Apply the autolabeler's
title rule directly and re-run on 'edited' events.

Also drop scripts/__init__.py: a top-level 'scripts' package in the
workspace shadows the one the Grype audit action runs with
'python -m scripts.collect_bypasses', which broke that job. The
renderer tests now load the script by path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
This commit is contained in:
Andrew GrimbergandCopilot committed 2026-11-02 08:57:25 -08:00
1 parent 617bee6227
commit 887eedb2c1
5 files changed
+52 -21

No files matched your search

+23 -9
View File
@@ -6,13 +6,14 @@ name: 'Release Notes'
# yamllint disable-line rule:truthy
on:
# 'labeled' and 'unlabeled' re-evaluate the breaking-change rule when
# the autolabeler (or a maintainer) changes the labels.
# 'edited', 'labeled' and 'unlabeled' re-evaluate the breaking-change
# rule when the title or labels change.
pull_request:
types:
- opened
- reopened
- synchronize
- edited
- labeled
- unlabeled
branches:
@@ -64,14 +65,27 @@ jobs:
} >> "$GITHUB_OUTPUT"
- name: 'Require a release note for breaking changes'
if: >-
contains(github.event.pull_request.labels.*.name, 'breaking-change')
&& steps.changes.outputs.added != 'true'
env:
ADDED: ${{ steps.changes.outputs.added }}
# yamllint disable-line rule:line-length
LABELLED: ${{ contains(github.event.pull_request.labels.*.name, 'breaking-change') }}
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
echo "::error::Pull requests labelled 'breaking-change' must" \
"add a release note: run 'uv run reno new <slug>' and fill" \
"in the upgrade section."
exit 1
# The autolabeler adds 'breaking-change' with GITHUB_TOKEN,
# which does not start a new run, so also apply its title rule
# from .github/release-drafter.yml here.
breaking="$LABELLED"
shopt -s nocasematch
title_rule='^[a-z]+(\([^)]+\))?!:'
if [[ "$PR_TITLE" =~ $title_rule ]]; then
breaking=true
fi
if [ "$breaking" = true ] && [ "$ADDED" != true ]; then
echo "::error::Breaking changes must add a release note:" \
"run 'uv run reno new <slug>' and fill in the upgrade" \
"section."
exit 1
fi
# yamllint disable-line rule:line-length
- uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3
+3 -3
View File
@@ -265,9 +265,9 @@ from the repository root using `uv`:
Describe user-visible changes with a [reno](https://docs.openstack.org/reno/latest/)
note in the same pull request: run `uv run reno new <slug>`, fill in
the sections that apply in Markdown, delete the rest, and check it with
`uv run reno lint`. A note is **required** for a breaking change (the
`breaking-change` label); CI fails without one. Upgrade actions belong
in `upgrade`.
`uv run reno lint`. A note is **required** for a breaking change (a
`Type!:` title or the `breaking-change` label); CI fails without one.
Upgrade actions belong in `upgrade`.
## Development Workflow Summary
+4 -4
View File
@@ -936,10 +936,10 @@ uv run reno lint
```
Fill in the sections that apply (`upgrade`, `features`, `fixes`,
`deprecations` or `security`) and delete the rest. A pull request
labelled `breaking-change` must add a note. The draft GitHub release
shows every note added since the last release tag, above the list of
pull requests. To preview it:
`deprecations` or `security`) and delete the rest. A breaking change
(a `Type!:` title or the `breaking-change` label) must add a note. The
draft GitHub release shows every note added since the last release tag,
above the list of pull requests. To preview it:
```bash
uv run python scripts/render_release_notes.py
-3
View File
@@ -1,3 +0,0 @@
# SPDX-FileCopyrightText: 2026 Andrew Grimberg <tykeal@bardicgrove.org>
# SPDX-License-Identifier: Apache-2.0
"""Maintainer scripts for the repository."""
+22 -2
View File
@@ -4,13 +4,34 @@
from __future__ import annotations
import importlib.util
from pathlib import Path
import sys
from typing import Any
import pytest
import yaml
from scripts import render_release_notes as rrn
REPO_ROOT = Path(__file__).resolve().parents[2]
def _load_renderer() -> Any:
"""Import scripts/render_release_notes.py by path.
scripts/ is deliberately not a package: a top-level ``scripts``
package in the workspace shadows the ``scripts`` package that some
GitHub Actions run with ``python -m``.
"""
path = REPO_ROOT / "scripts" / "render_release_notes.py"
spec = importlib.util.spec_from_file_location("render_release_notes", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
rrn = _load_renderer()
SECTIONS = [
("upgrade", "Upgrade Notes", 1),
@@ -18,7 +39,6 @@ SECTIONS = [
("fixes", "Bug Fixes", 1),
]
SECTION_NAMES = [name for name, _, _ in SECTIONS]
REPO_ROOT = Path(__file__).resolve().parents[2]
class FakeLoader: