Skip to content

ci: fix release notes format and tag order - #45

Merged
uniqueg merged 5 commits into
devfrom
fix/prerelease-review-findings
Nov 1, 2026
Merged

uniqueg merged 5 commits into
devfrom
fix/prerelease-review-findings

Conversation

@uniqueg

@uniqueg uniqueg commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

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.1 and foca-plugins-v0.1.0 are pushed.

  • L5, release notes format: python -m scripts.release notes now 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:

    • no intro paragraph;
    • an unknown section name, or sections out of order;
    • level 1 or 2 headings, including setext headings;
    • a deeper heading before the first grouped section;
    • an unclosed code block.

    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 ## Commits with GitHub's generated notes copied verbatim, without their ## What's Changed header, 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.md now 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 from pypi to release (ci: publish in the environment release #25) was done for both plugin projects and the pending foca-plugins publisher.

  • L4 (accepted): documented that GitHub's generated notes ignore paths, so a plugin's ## Commits lists every repository PR since its previous tag.

  • First releases: I kept the git log commit list of the package directory. I checked on 2026-11-01: without previous_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 for foca-plugins-v0.1.0 returned compare/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_PATTERN moved to scripts/packages.py, so scripts/release.py can use it without importing scripts/checks.py. The packages pre-commit hook now also runs on changes to scripts/checks.py and scripts/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.
  • Local release scripts for all three tags: release, previous and notes give the expected output, with ## sections. pins passes for the plugins and fails for foca-plugins-v0.1.0 as intended, because 0.1.1 is not on PyPI yet.
  • CI: all checks passed on the first run, including 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 typed FakePyPI.

  • 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_notes in scripts/checks.py repeats the private-classifier test of release() (low). It is two lines and consistent with _check_meta, and a shared helper would change unrelated code.
  • HTML comments are not skipped, and headings inside list items or block quotes are not recognised (low). These are unrealistic in changelogs. A heading inside an HTML comment fails safe (it is refused as an unknown section), and the docs say to keep headings out of comments.

Open questions

None.

🤖 Generated with Claude Code

uniqueg and others added 4 commits November 1, 2026 10:20
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>
@sourcery-ai

sourcery-ai Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The 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 checks

sequenceDiagram
    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
Loading

Flow diagram for changelog release-note formatting

flowchart 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]
Loading

File-Level Changes

Change Details Files
Implemented strict release-note formatting and validation for changelog sections.
  • Promote grouped headings from level 3 to level 2 while preserving code blocks and deeper heading structure.
  • Reject missing intros, invalid or unordered sections, unsupported heading levels, headings outside grouped sections, and unclosed fences.
  • Validate current release sections during package convention checks and update pre-commit triggers.
scripts/release.py
scripts/checks.py
scripts/packages.py
.pre-commit-config.yaml
tests/test_release.py
tests/test_checks.py
Added release-time verification that meta-distribution plugin pins are available on PyPI.
  • Add the pins command and invoke it before building releases in the workflow.
  • Treat only HTTP 404 as unpublished; surface outages, timeouts, and other failures as annotated errors.
  • Skip the check for plugin tags and deduplicate exact pins.
scripts/release.py
.github/workflows/release.yml
scripts/packages.py
tests/test_release.py
Updated release documentation and workflow guidance for generated notes, tag sequencing, and trusted publishers.
  • Document organization-formatted notes, GitHub path-insensitive commit lists, and first-release fallback behavior.
  • Require plugin tags to be released before the meta tag and recommend rerunning failed jobs.
  • Document trusted-publisher updates required after workflow or environment renames.
docs/releasing.md
.github/workflows/release.yml

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9eda532) to head (0a086cb).
⚠️ Report is 3 commits behind head on dev.

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           
Flag Coverage Δ
foca-plugin-generic-example-noop 100.00% <ø> (ø)
foca-plugin-generic-routing-random 100.00% <ø> (ø)
foca-plugin-tes-routing-distance 100.00% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@uniqueg
uniqueg marked this pull request as ready for review November 1, 2026 10:24

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread scripts/release.py Resolved
Comment thread scripts/release.py Resolved
Comment thread scripts/release.py Resolved
Comment thread scripts/release.py Resolved
- 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>
@uniqueg
uniqueg merged commit f6c15c0 into dev Nov 1, 2026
78 checks passed
@uniqueg
uniqueg deleted the fix/prerelease-review-findings branch November 1, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: fix the pre-release review findings

1 participant