Repository navigation
ci: fix release notes format and tag order - #45
Conversation
Release notes now follow the organization's format: `scripts.release notes` raises the grouped changelog sections from `###` to `##` and refuses sections without an intro paragraph, with unknown names or in another order. `scripts.checks check` validates the section of each package's current version the same way, so the release PR fails rather than the tag. The new command `scripts.release pins` fails the meta distribution's build job if a pinned plugin version is not on PyPI yet, before anything is built or published. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Tag order: plugin tags first, the meta tag only once its pins are on PyPI (checked by the build job). - Trusted publishers: update every existing PyPI project before its next tag when the environment or workflow file is renamed; the 2026-10 rename from `pypi` to `release` was done. - Release notes: the org format with `##` sections; GitHub's generated notes list all repository PRs (accepted); why first releases list the package's own commits. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Headings: CommonMark rules for indentation (up to 3 spaces), ATX closing sequences (only after a space) and setext headings (refused as level 1 or 2); deeper headings before the first grouped section are refused. - Fences: backtick lines with backticks in the info string are inline code; closing fences may be indented by up to 3 spaces only. - PyPI check: quote names and versions without safe characters; tests pin the literal URLs, the operator hint and an outage in `main`. - Docs: list all refusals. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Setext headings only below paragraph text: a line of `-` or `=` after a heading, list item, block quote or code block is a thematic break or text, not a heading. - PyPI check: errors while reading the response (timeouts, dropped connections) also give the "Can't check" annotation. - Docs: rewrap the paragraph on refused sections. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideThe PR hardens the release process by enforcing the organization’s changelog format before tagging, checking meta-distribution pins against PyPI before builds, and documenting the resulting tag order, generated-note behavior, and trusted-publisher requirements. Sequence diagram for release validation and pinned plugin checkssequenceDiagram
participant Workflow as ReleaseWorkflow
participant Checks as scripts.checks
participant Release as scripts.release
participant PyPI as PyPI
participant Build as Build
Workflow->>Checks: check()
Checks->>Release: notes(tag)
Release-->>Checks: validated formatted notes
Checks-->>Workflow: pass or fail
Workflow->>Release: pins(tag)
Release->>PyPI: GET /pypi/{name}/{version}/json
PyPI-->>Release: 200 or 404
alt pinned version missing
Release-->>Workflow: fail and annotate release plugins first
else all pins available or plugin tag
Release-->>Workflow: pass
Workflow->>Build: build and publish
end
Flow diagram for changelog release-note formattingflowchart TD
A[Version changelog section] --> B{Intro paragraph present}
B -- No --> X[Reject release notes]
B -- Yes --> C{Headings valid and grouped sections ordered}
C -- No --> X
C -- Yes --> D{Code blocks closed}
D -- No --> X
D -- Yes --> E[Raise grouped headings from level 3 to level 2]
E --> F[Publish formatted notes followed by Commits]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #45 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 6 6
Lines 553 553
Branches 71 71
=========================================
Hits 553 553
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Hey - I've found 4 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="scripts/release.py" line_range="135" />
<code_context>
if not body:
raise ValueError(f"The section of {package['version']} is empty")
- return body + "\n"
+ return _format(body, package["version"]) + "\n"
+
+
</code_context>
<issue_to_address>
**Later notes are silently omitted**
When a current changelog section contains an unexpected level-two heading before its next version heading, `notes()` slices the changelog body at any `## ` line before calling `_format()`, so the validator accepts only the prefix and release notes omit the remaining changes; the release check passes despite the malformed section.
Use only valid version headings as section boundaries so an unexpected level-two heading remains in the body and is rejected by `_format()`.
</issue_to_address>
### Comment 2
<location path="scripts/release.py" line_range="179" />
<code_context>
+ raise ValueError(f"{prefix} has a heading {lines[-1].strip()!r}")
+ elif heading is not None:
+ level, title = heading["level"], heading["title"] or ""
+ if not lines:
+ raise ValueError(f"{prefix} has no intro paragraph")
+ if len(level) < len("###"):
</code_context>
<issue_to_address>
**Non-paragraph content passes as intro**
When the changelog section starts with a list, block quote, or code block before its first grouped heading, `_format` checks only whether `lines` is nonempty, so non-paragraph content passes the intro check and `notes()` publishes release notes without the required intro paragraph.
Validate that the content before the first grouped heading contains an actual paragraph.
</issue_to_address>
### Comment 3
<location path="scripts/release.py" line_range="68" />
<code_context>
+UNDERLINE = re.compile(r" {0,3}(=+|-+)[ \t]*")
+# Lines of top-level paragraph text: not blank or indented, and not of a
+# list item or block quote
+PARAGRAPH = re.compile(r"(?![-*+][ \t]|[0-9]+[.)][ \t]|>)\S")
+# Opening fences of code blocks, whose lines are no headings (no backticks
+# in the info string of a backtick fence, or it is inline code)
</code_context>
<issue_to_address>
**Setext headings pass validation**
When a setext underline follows a paragraph indented by one to three spaces or beginning with a ten-digit ordered-list-like prefix, `PARAGRAPH.match()` does not recognize these valid paragraph lines, so `_format()` misses the following setext underline and publishes a level-1 or level-2 heading that it should refuse.
Update `PARAGRAPH` to recognize paragraphs with up to three leading spaces and ten-digit ordered-list-like prefixes.
</issue_to_address>
### Comment 4
<location path="scripts/release.py" line_range="202" />
<code_context>
+ lines.append(line.replace("#", "", 1))
+ continue
+ else:
+ paragraph = PARAGRAPH.match(line) is not None
+ lines.append(line)
+ if closing is not None:
</code_context>
<issue_to_address>
**Adjacent thematic breaks are refused**
When a valid section contains two consecutive thematic-break lines, `PARAGRAPH` treats a thematic-break line such as `---` as paragraph text. A second thematic break immediately after it is then treated as a setext heading, so `_format` rejects otherwise valid release notes.
Exclude CommonMark thematic-break lines from the paragraph state used to detect setext headings.
</issue_to_address>Linked issues
#44 ci: fix the pre-release review findings · 6 of 6 met
| # | Requirement | Status | Notes |
|---|---|---|---|
| 1 | Document trusted-publisher updates after renaming the environment or workflow, and record the 2026-10 rename update. | ✅ Met | The docs require updating existing and pending publishers before the next tag, and record the projects and pending publisher covered by the rename. |
| 2 | Document that plugin tags must be pushed before the meta tag, which waits until pinned plugin versions are on PyPI. | ✅ Met | The release steps give the tag order and say to re-run failed jobs after the plugin releases are available. |
| 3 | Fail the meta distribution's build early when a pinned plugin version is not on PyPI. | ✅ Met | The workflow runs the pins check before setting up build tools; the check treats HTTP 404 as missing, fails on other errors, and returns successfully for plugins. |
| 4 | Format release notes with grouped sections at level 2, and refuse unknown or out-of-order grouped sections. | ✅ Met | scripts.release notes raises the changelog headings one level and validates grouped section names and order. |
| 5 | Document the release-notes format, including GitHub's generated notes and their Full Changelog line. | ✅ Met | The releasing guide describes the changelog section, ## Commits, and the generated notes' ending line. |
| 6 | Document that GitHub's generated notes ignore paths and include repository PRs since a plugin's previous tag. | ✅ Met | The guide states that plugin commit notes include PRs from across the repository. |
Sourcery assessment
Needs a human reviewer. 4 findings to address first, and if the changelog parser or heading transformation is wrong, a release could publish incorrectly formatted or incomplete public release notes; reverting the code would not change notes already published, though they can be corrected. A meta release blocked because a pinned plugin is not yet on PyPI can be rerun after that plugin is released.
Blocking findings: scripts/release.py:135, scripts/release.py:179, scripts/release.py:68, scripts/release.py:202
- Sections end at the next version heading only; other `##` headings stay in the section and are refused. - The intro must be paragraph text, not a list, quote or code block. - Paragraph text may be indented by up to three spaces; ordered list markers have at most nine digits; thematic breaks are no paragraph text, so consecutive breaks are no setext headings. Refs #44 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
This PR fixes the findings of the pre-release review that must be done before the tags
foca-plugin-generic-routing-random-v0.1.1,foca-plugin-tes-routing-distance-v0.1.1andfoca-plugins-v0.1.0are pushed.L5, release notes format:
python -m scripts.release notesnow raises the grouped changelog sections from###to##, matching the org format. Deeper headings move up one level as well; code blocks are left as they are. It refuses a section in any of these cases:python -m scripts.checks check(pre-commit and CI) runs the same validation on the section of each package's current version, so a bad changelog fails the release PR and not the immutable tag. The workflow still appends## Commitswith GitHub's generated notes copied verbatim, without their## What's Changedheader, and ending with their**Full Changelog**line.L1, tag order: a new command,
python -m scripts.release pins <tag>, runs as the step "Check pinned plugins" in the build job. For the meta distribution it fails before anything is built if one of its exact pins is not on PyPI yet. Only HTTP 404 means "missing"; any other error also fails, with a "Can't check" annotation. For plugins the step does nothing. The docs now say to push the plugin tags first and the meta tag only once its pins are on PyPI; after a failure, "Re-run failed jobs".M1, trusted publishers:
docs/releasing.mdnow says that a rename of the environment or the workflow file means updating the trusted publisher of every existing PyPI project before its next tag. It also records that the 2026-10 rename frompypitorelease(ci: publish in the environment release #25) was done for both plugin projects and the pendingfoca-pluginspublisher.L4 (accepted): documented that GitHub's generated notes ignore paths, so a plugin's
## Commitslists every repository PR since its previous tag.First releases: I kept the
git logcommit list of the package directory. I checked on 2026-11-01: withoutprevious_tag_name, GitHub's generate-notes API does not start at the beginning of the history. It starts at the newest release of any package in the repo. A dry call forfoca-plugins-v0.1.0returnedcompare/foca-plugin-tes-routing-distance-v0.1.0...foca-plugins-v0.1.0. After the plugin 0.1.1 tags on the same commit, that list would be empty. The docs explain this.PIN_PATTERNmoved toscripts/packages.py, soscripts/release.pycan use it without importingscripts/checks.py. Thepackagespre-commit hook now also runs on changes toscripts/checks.pyandscripts/release.py.Closes #44
Verification
coverage run --source scripts,docs -m pytest tests && coverage report: 101 passed, 100% branch coverage.pre-commit run --all-files: all hooks passed (ruff, mypy, semgrep, ast-grep, actionlint, gitleaks, package conventions).mkdocs build --strict: passed.release,previousandnotesgive the expected output, with##sections.pinspasses for the plugins and fails forfoca-plugins-v0.1.0as intended, because 0.1.1 is not on PyPI yet.CI passed, Lint, Docs, Test scripts, the full test matrix, CodeQL and Codecov.Review
Full lane, because the change touches the release workflow and scripts. There were two rounds of adversarial-reviewer and quality-reviewer.
Round 1 fixes: CommonMark rules for indentation, ATX closing sequences, fences with backticks in the info string, closing fences and setext headings; refusal of deeper headings outside a section;
quote(..., safe="")with tests on the literal URLs; tests for the operator hint and a PyPI outage; a typedFakePyPI.Round 2 fixes: setext underlines count only below paragraph text; timeouts and dropped connections while reading the response also give the annotation.
Sourcery (4 threads, all fixed and resolved). A section now ends only at the next version heading, and any other
##heading inside it is refused. The intro must be paragraph text. Paragraph text may be indented by up to 3 spaces. Ordered-list markers have at most 9 digits. Thematic breaks are not paragraph text. On the new head, CI and the Sourcery review check passed again. Copilot did not post a review.Declined review findings
_check_notesinscripts/checks.pyrepeats the private-classifier test ofrelease()(low). It is two lines and consistent with_check_meta, and a shared helper would change unrelated code.Open questions
None.
🤖 Generated with Claude Code