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

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


The following commit(s) were added to refs/heads/main by this push:
     new 634be2a1c13 Update release scripts (#4956)
634be2a1c13 is described below

commit 634be2a1c1358363fd6110b6502e094027a704e4
Author: Andrzej BiaƂecki <[email protected]>
AuthorDate: Mon Sep 28 23:26:28 2026 +0200

    Update release scripts (#4956)
    
    Co-authored-by: David Smiley <[email protected]>
    Co-authored-by: Claude Sonnet 5 <[email protected]>
---
 dev-tools/scripts/releaseWizard.py                 |  6 +-
 dev-tools/scripts/releaseWizard.yaml               | 67 ++++++++++++++++---
 .../documentation/changes-to-html/changes2html.py  | 77 +++++++++++++++-------
 3 files changed, 119 insertions(+), 31 deletions(-)

diff --git a/dev-tools/scripts/releaseWizard.py 
b/dev-tools/scripts/releaseWizard.py
index 6a4dd91c549..26f635651bd 100755
--- a/dev-tools/scripts/releaseWizard.py
+++ b/dev-tools/scripts/releaseWizard.py
@@ -135,6 +135,7 @@ def expand_jinja(text, vars=None):
         'docker_version_to_remove': state.get_docker_version_to_remove(),
         'editor': get_editor(),
         'rename_cmd': 'ren' if is_windows() else 'mv',
+        'open_cmd': 'start' if is_windows() else ('open' if is_mac() else 
'xdg-open'),
         'vote_close_72h': vote_close_72h_date().strftime("%Y-%m-%d %H:00 UTC"),
         'vote_close_72h_epoch': unix_time_millis(vote_close_72h_date()),
         'vote_close_72h_holidays': vote_close_72h_holidays(),
@@ -457,6 +458,8 @@ class ReleaseState:
         if v.is_major_release():
             return 'main'
         elif v.is_minor_release():
+            if self.is_lts_release():
+                return self.get_lts_stable_branch_name()
             return self.get_stable_branch_name()
         elif v.major == Version.parse(self.latest_version).major:
             return self.get_minor_branch_name()
@@ -1871,7 +1874,8 @@ class Command(SecretYamlObject):
         return self.jinjaify(self.redirect)
 
     def get_cmd(self):
-        return self.jinjaify(self.cmd, join=True)
+        # Run python scripts with this interpreter (e.g. a venv), not whatever 
python3 is on PATH
+        return re.sub(r'^python3(?=\s)', lambda _: 
shlex.quote(sys.executable), self.jinjaify(self.cmd, join=True))
 
     def get_vars(self):
         myvars = {}
diff --git a/dev-tools/scripts/releaseWizard.yaml 
b/dev-tools/scripts/releaseWizard.yaml
index a85e558775f..0a3b0201b52 100644
--- a/dev-tools/scripts/releaseWizard.yaml
+++ b/dev-tools/scripts/releaseWizard.yaml
@@ -409,7 +409,7 @@ groups:
       commands_text: Run these commands to create a release branch
       commands:
       - !Command
-        cmd: git checkout {{ stable_branch }}
+        cmd: git checkout {{ base_branch }}
         tee: true
       - !Command
         cmd: git pull --ff-only
@@ -524,7 +524,7 @@ groups:
       commands_text: Run these commands to add the new minor version {{ 
next_version }} to the stable and unstable branches
       commands:
       - !Command
-        cmd: git checkout {{ stable_branch }}
+        cmd: git checkout {{ base_branch }}
         tee: true
       - !Command
         cmd: python3 -u dev-tools/scripts/addVersion.py {{ next_version }}
@@ -755,26 +755,77 @@ groups:
     description: |
       This step first validates all changelogs in `changelog/unreleased/`.
       If there are any errors, they must be fixed before proceeding.
-      
+
       It then moves changelog entries into `changelog/v{{ release_version }}/` 
and
       regenerates `CHANGELOG.md` without a release date. The date is updated 
later,
       in the `publish` phase.
 
-      The result is committed to `{{ release_branch }}` only. Cherry-picking to
-      `{{ stable_branch }}` and `main` is done in `publish` phase.
+      This step is deliberately run without `--commit`, leaving the result
+      uncommitted in the working tree so it can be spot-checked in the next
+      step before anything is committed or pushed.
 
       Can be run standalone, see `dev-tools/scripts/logchange.py prepare 
--help`.
     depends: dependency_updates_changes
     commands: !Commands
       root_folder: '{{ git_checkout_folder }}'
-      commands_text: Prepare changelog folder and regenerate CHANGELOG.md for 
RC{{ rc_number }}
+      commands_text: Prepare changelog folder and regenerate CHANGELOG.md for 
RC{{ rc_number }} (uncommitted)
       confirm_each_command: false
       commands:
       - !Command
-        cmd: python3 -u dev-tools/scripts/logchange.py prepare --version {{ 
release_version }}  --release-branch {{ release_branch }} --gradle-cmd {{ 
gradle_cmd }} --rc-number {{ rc_number }} --commit
-        comment: Move unreleased entries to version folder and commit; then 
regenerate CHANGELOG.md and commit (two commits total)
+        cmd: python3 -u dev-tools/scripts/logchange.py prepare --version {{ 
release_version }}  --release-branch {{ release_branch }} --gradle-cmd {{ 
gradle_cmd }} --rc-number {{ rc_number }}
+        comment: Move unreleased entries to version folder and regenerate 
CHANGELOG.md, left uncommitted
         logfile: changelog-update-rc.log
         tee: true
+  - !Todo
+    id: review_changelog_rc
+    title: Spot-check the changelog for RC{{ rc_number }}
+    description: |
+      Before committing, spot-check the changelog that was just regenerated.
+      This is the last easy chance to catch mistakes -- once RC{{ rc_number }} 
is
+      built and staged, fixing the changelog means a new RC and restarting the 
vote.
+
+      Look at `git diff changelog/ CHANGELOG.md` and the rendered
+      `solr/documentation/build/site/changes/Changes.html` (opened below) for
+      things like: entries in the wrong section, an "Important notes" entry
+      that shouldn't be called out, duplicate/missing issues, and typos.
+
+      If something needs fixing, edit the relevant file(s) under
+      `changelog/v{{ release_version }}/` and re-run this step's commands.
+    depends: changelog_update_rc
+    commands: !Commands
+      root_folder: '{{ git_checkout_folder }}'
+      commands_text: Show the changelog diff and open the rendered 
Changes.html for review
+      confirm_each_command: false
+      commands:
+      - !Command
+        cmd: git diff --stat changelog CHANGELOG.md
+        stdout: true
+      - !Command
+        cmd: "{{ gradle_cmd }} :solr:documentation:changesToHtml"
+        logfile: changes-to-html.log
+        tee: true
+      - !Command
+        cmd: "{{ open_cmd }} 
solr/documentation/build/site/changes/Changes.html"
+        stdout: true
+    post_description: |
+      Once the changelog looks right, proceed to the next step to commit and 
push it.
+  - !Todo
+    id: commit_changelog_rc
+    title: Commit and push changelog for RC{{ rc_number }}
+    description: |
+      Commits the reviewed changelog to `{{ release_branch }}` only. 
Cherry-picking to
+      `{{ stable_branch }}` and `main` is done in the `publish` phase.
+    depends: review_changelog_rc
+    commands: !Commands
+      root_folder: '{{ git_checkout_folder }}'
+      commands_text: Commit the reviewed changelog and push it to {{ 
release_branch }}
+      confirm_each_command: false
+      commands:
+      - !Command
+        cmd: python3 -u dev-tools/scripts/logchange.py prepare --version {{ 
release_version }}  --release-branch {{ release_branch }} --gradle-cmd {{ 
gradle_cmd }} --rc-number {{ rc_number }} --commit
+        comment: Commit the (already reviewed) changelog entries and 
regenerated CHANGELOG.md
+        logfile: commit-changelog-update-rc.log
+        tee: true
       - !Command
         cmd: git push origin {{ release_branch }}
         comment: Push prepared changelog to {{ release_branch }}
diff --git a/gradle/documentation/changes-to-html/changes2html.py 
b/gradle/documentation/changes-to-html/changes2html.py
index 1d52618834e..287c3205145 100755
--- a/gradle/documentation/changes-to-html/changes2html.py
+++ b/gradle/documentation/changes-to-html/changes2html.py
@@ -142,21 +142,22 @@ class HTMLGenerator:
         """Format a single issue reference as an HTML anchor tag"""
         return f'<a href="{url_prefix}{issue_id}">{label}</a>'
 
-    def _extract_markdown_issue(self, text):
+    def _find_markdown_issue_matches(self, text):
         """
-        Extract markdown-formatted JIRA/GitHub issues like [SOLR-123](url) or 
[PR#123](url).
-        Returns (issue_link_html, text_without_issue) or (None, text) if not 
found.
+        Find every markdown-formatted JIRA/GitHub issue reference in text,
+        e.g. [SOLR-123](url) or [PR#123](url) -- an item can reference more
+        than one issue. Returns a list of (start, end, issue_link_html)
+        tuples in the order they appear.
         """
+        matches = []
         for pattern, url_prefix, label_fmt in self.issue_patterns:
-            match = re.search(pattern, text)
-            if match:
+            for match in re.finditer(pattern, text):
                 issue_id = match.group(1)
                 label = label_fmt.format(issue_id)
                 issue_html = self._format_issue_link(url_prefix, issue_id, 
label)
-                text_without = (text[:match.start()] + 
text[match.end():]).strip()
-                return issue_html, text_without
-
-        return None, text
+                matches.append((match.start(), match.end(), issue_html))
+        matches.sort(key=lambda m: m[0])
+        return matches
 
     def _extract_plain_pr_references(self, text):
         """
@@ -187,17 +188,39 @@ class HTMLGenerator:
 
     def extract_issue_from_text(self, text):
         """
-        Extract the first issue reference from text.
-        Tries in order: markdown JIRA/GitHub issues, plain GitHub PR 
references.
-        Returns (issue_link_html, text_without_issue) or (None, text) if not 
found.
+        Extract issue reference(s) from text.
+        Tries markdown-formatted JIRA/GitHub issues first -- there may be
+        more than one, e.g. two related JIRA issues on the same change --
+        falling back to a plain GitHub PR reference like "#123" if none
+        are found.
+
+        Returns (issue_link_html_or_None, description_or_None, tail).
+        When markdown issues were found, `description` is the text before
+        the first issue reference and `tail` is everything from there on
+        with the issue reference(s) removed (normally just the trailing
+        author "(...)" groups). Author extraction must be bounded to
+        `tail`, never `description`, so a parenthetical at the end of the
+        description (e.g. an acronym expansion) is never mistaken for an
+        author group. When `description` is None, no markdown issue was
+        found and `tail` is just `text` with any plain PR reference
+        removed, if present.
         """
-        # Try markdown-formatted issues first
-        issue_html, text_without = self._extract_markdown_issue(text)
-        if issue_html:
-            return issue_html, text_without
+        matches = self._find_markdown_issue_matches(text)
+        if matches:
+            first_start = matches[0][0]
+            parts = []
+            pieces = []
+            last_end = first_start
+            for start, end, issue_html in matches:
+                pieces.append(text[last_end:start])
+                parts.append(issue_html)
+                last_end = end
+            pieces.append(text[last_end:])
+            return ' '.join(parts), text[:first_start], ''.join(pieces)
 
         # Fall back to plain GitHub PR references
-        return self._extract_plain_pr_references(text)
+        issue_html, text_without = self._extract_plain_pr_references(text)
+        return issue_html, None, text_without
 
     def _format_single_author(self, author_text):
         """
@@ -372,11 +395,21 @@ class HTMLGenerator:
         Format: [ISSUE](url) description (author1) (author2)
         Output: <a href>ISSUE</a>: description<br><span 
class="attrib">(authors)</span>
         """
-        # Extract the issue
-        issue_html, text_after_issue = self.extract_issue_from_text(item_text)
-
-        # Always try to extract authors, whether or not we found an issue
-        authors_list, description = self.extract_authors(text_after_issue if 
issue_html else item_text)
+        # Extract the issue(s)
+        issue_html, description_head, tail = 
self.extract_issue_from_text(item_text)
+
+        if description_head is not None:
+            # Markdown issue(s) found: bound author extraction to the tail
+            # so it can't reach back into the description.
+            authors_list, tail_remainder = self.extract_authors(tail)
+            description = description_head.strip()
+            tail_remainder = tail_remainder.strip()
+            if tail_remainder:
+                description = f'{description} {tail_remainder}'.strip()
+        else:
+            # No markdown issue (plain PR reference, or no issue at all):
+            # extract authors from the whole remaining text, as before.
+            authors_list, description = self.extract_authors(tail if 
issue_html else item_text)
 
         if issue_html:
             # We have an issue link

Reply via email to