This is an automated email from the ASF dual-hosted git repository.
lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git
The following commit(s) were added to refs/heads/main by this push:
new b1a7c2296 docs: refine creating-version-notes from the 7.3.0 release
run (#1828)
b1a7c2296 is described below
commit b1a7c2296985c22875cb5ecaf690cc5de240a09e
Author: Lukasz Lenart <[email protected]>
AuthorDate: Sat Aug 1 17:20:17 2026 +0200
docs: refine creating-version-notes from the 7.3.0 release run (#1828)
* docs: refine creating-version-notes from the 7.3.0 release run
Seven changes, each from something the 7.3.0 run got wrong or had to invent.
Breaking changes were far too long: the old guidance held up a
paragraph-length
example and asked for what replaces the old behaviour, which produces prose
the
ticket already carries. It is now one sentence plus the ticket link, whose
job is
to tell a reader whether to open the ticket.
Reconciliation knew only "listed" or "dropped", so a ticket resolved Won't
Do got
silently dropped. Such tickets now go under a Rejected requests section -
not into
a type section, where they would read as delivered. Deprecations get their
own
section for the same reason: nothing stops working yet.
Security summaries are no longer reproduced verbatim when the ticket itself
names
the bypass or the leak; they are truncated at a clause boundary and the cut
is
reported to the release manager.
The GitHub release range is now fixed at source by passing previous_tag_name
rather than generating wrong notes and pruning them, and creating a release
that
does not exist yet is covered alongside editing one that does.
Also adds the test-build announcement mail as the closing step, the full
page
section order, split verification by set-diff, and the note that
content_file is
rejected for paths outside the repository.
Co-Authored-By: Claude Opus 5 <[email protected]>
* docs: address the test-build announcement to both dev@ and user@
The recipient list was left as the release manager's call because 6.11.0
went to
dev@ and 7.3.0 to user@. Both were mistakes: the announcement goes to both
lists,
Bcc private@. A build announced to one list reaches half the people who
could
find a problem while the vote is open.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
---
.claude/skills/creating-version-notes/SKILL.md | 157 +++++++++++++++++++--
.../version-notes-template.md | 45 ++++--
2 files changed, 177 insertions(+), 25 deletions(-)
diff --git a/.claude/skills/creating-version-notes/SKILL.md
b/.claude/skills/creating-version-notes/SKILL.md
index d7cef5134..b15aa5cb3 100644
--- a/.claude/skills/creating-version-notes/SKILL.md
+++ b/.claude/skills/creating-version-notes/SKILL.md
@@ -1,6 +1,6 @@
---
name: creating-version-notes
-description: Use when preparing, updating, or reviewing the release
documentation for a Struts release or release candidate on any maintenance line
(6.x, 7.x) - the Version Notes page on the cwiki, its Migration Guide entry,
and the GitHub release notes.
+description: Use when preparing, updating, or reviewing the release
documentation for a Struts release or release candidate on any maintenance line
(6.x, 7.x) - the Version Notes page on the cwiki, its Migration Guide entry,
the GitHub release notes, and the test-build announcement mail.
---
# Creating Version Notes
@@ -54,11 +54,14 @@ Group under `<h2>` per issue type, in this order, omitting
any type with no issu
Within a section, order by issue key ascending. Each entry is `[WW-XXXX] -
<the JIRA summary verbatim>`.
-**Reconcile against what actually merged.** The JIRA query is the starting
point, not the answer. Two mismatches to check:
+**Reconcile against what actually merged.** The JIRA query is the starting
point, not the answer. Three mismatches to check:
-- A ticket marked fixed whose change did not make the release branch — it must
not be listed.
+- A ticket resolved `Fixed` whose change did not make the release branch — it
must not be listed as delivered.
+- A ticket resolved **`Won't Do`** or otherwise not `Fixed` — it belongs under
`Rejected requests`, not in a type section and not dropped. Check the
resolution, not just the status: both `Closed` and `Resolved` sit in the Done
category.
- Work that shipped under a ticket assigned to a different fix version — the
notes under-report the release.
+A ticket with no commit in the range is not automatically wrong. Check its
**component** first: `IDEA Plugin`, `Example Applications` and similar live in
other repositories and are still legitimately part of the release.
+
**Reconcile through the ticket's linked PR, reading the files it changed.** Do
not grep commit subjects, and do not go looking for the class named in the
ticket title: a title often names the *symptom* while the fix lives elsewhere.
WW-5630 reads "Performance Issue SecurityMemberAccess" and was fixed in
`ConfigParseUtil`; searching for the former concludes, wrongly, that the
backport is missing. Squash-merges also rewrite hashes, so the merge commit id
from the PR need not appear on the branch.
**Untick eted patch-level dependency bumps are not a gap.** Dependabot PRs for
patch updates are merged directly and deliberately get no ticket, so they get
no entry — there is nothing to link. Expect the pom to show a higher patch
version than the ticket text says: 6.11.0 shipped jackson 2.22.1 while WW-5648
reads "2.21.4 to 2.22.0". That is correct, not an omission. Minor and major
bumps do get a ticket and do get listed.
@@ -73,15 +76,49 @@ When a release is superseded before it ships, its content
does not disappear —
This is the same discipline `creating-security-bulletins` applies to Affected
Software, for the same reason: naming a version that never reached users
misdirects everyone downstream.
+## Page section order
+
+Beyond the boilerplate, sections appear in this order, each omitted when empty:
+
+**Breaking changes → Deprecations → Rejected requests → Bug → New Feature →
Improvement → Task → Dependency → Issue Detail → Issue List → Other resources**
+
+The first three are authored; the issue-type sections are derived from JIRA.
+
## Breaking changes
-Present only when the release has them — a maintenance release usually does
not. This section is **authored prose, not a ticket dump**: one item per
change, each stating what an application must now do differently, with its
ticket(s) linked at the end.
+Present only when the release has them — a maintenance release usually does
not.
+
+Each item is **one sentence plus its ticket link**:
+
+```
+<what changed, in terms of what an application sees> [WW-XXXX].
+```
+
+> Annotated wildcard actions are matched most-specific-first, so action
selection can differ [WW-3784].
+
+> `JSONInterceptor` uses a fresh reader and writer per request, so custom ones
must not hold state between requests [WW-5650].
+
+The sentence exists so a reader can judge **whether to open the ticket**, not
so they can avoid opening it. The ticket carries the detail — API signatures,
migration steps, the config that changes. Naming the affected type or setting
is enough; enumerating what replaces it is the ticket's job.
+
+Derive each item from the fix diff rather than the ticket title, and write
only what you confirmed. A change you suspect is breaking but could not pin
down is one to raise with the release manager, not to describe vaguely.
+
+## Deprecations
-The register is the upgrade decision, not the implementation. From 7.2.1:
+Where a release deprecates public API, list it separately from Breaking
changes — nothing stops working yet, so mixing the two overstates the upgrade
cost. Same one-line shape, naming the replacement where there is one:
-> `CookieInterceptor` now applies `@StrutsParameter` authorization to cookie
values and deprecates the 4-arg `populateCookieValueIntoStack(...)` in favor of
a new 5-arg overload taking the action, so un-annotated setters stop receiving
cookies and subclass overrides must migrate.
+> `ConversionRule.COLLECTION` and the `Collection_` key prefix are deprecated;
use `ConversionRule.ELEMENT` and `Element_` instead [WW-5656].
-Name the type or setting a user must act on, say what stops working, and say
what replaces it.
+## Rejected requests
+
+A ticket resolved **`Won't Do`** (or otherwise not `Fixed`) against this fix
version is still news: someone asked for it and the project decided against it.
+
+- **Do not put it in a type section.** Under Improvement or New Feature it
reads as delivered.
+- **Do not silently drop it either.** The decision is the value.
+- List it under `Rejected requests`, saying it will not be implemented and,
where the release manager gave one, the reason.
+
+> [WW-2635] - Flash scope - will not be implemented; the proposed mechanism
could introduce a security risk.
+
+Note the JIRA-generated release notes linked from the page *will* still
include these tickets under their type. Clearing the fix version in JIRA is the
only way to change that, and is the release manager's call.
## Security fixes in a release
@@ -91,6 +128,10 @@ A release usually ships before its bulletin publishes and
before a CVE exists. T
- **Do not add security framing the bulletin has not published yet** — no
severity, no attack description, no S2-XXX or CVE number that has not been
assigned and published.
- Once the bulletin is public, the notes may link it.
+**Where the ticket's own summary describes the defect, list the neutral part
of it.** "List the summary verbatim" assumes a neutrally-worded ticket, and
security tickets often are not. WW-5643 reads *"StrutsJSONReader parse state
shared across concurrent requests — maxDepth bypass and cross-request data
leak"*; the page carried it up to "concurrent requests" and stopped. The
trailing clause is the bulletin's job.
+
+Truncate at the clause boundary — never paraphrase into something the ticket
does not say, and never alter the ticket link. Then **tell the release manager
which summaries you cut and why**: whether an already-public JIRA summary
should be reproduced in full is their call, not yours, and it has to be made
before the page goes up rather than edited afterwards.
+
**REQUIRED BACKGROUND:** where the wording of a security-relevant entry is in
question, `creating-security-bulletins` governs what may be said and when.
## The Staging Repository block
@@ -113,23 +154,36 @@ Add an entry at the **top** of the list under the `<h2>`
for the matching line
**Verify against raw storage, not the diff.** A version diff of this page
renders empty even for a real change, because the markdown view discards
`ac:link` bodies. Fetch the new version with `convert_to_markdown=false` and
confirm the new entry is present, the prior entries survive in order, and the
trailing `<h3>` appears exactly once.
+This applies to **every** section update, including ones on the Version Notes
page itself — shortening `Breaking changes` carries the same risk of swallowing
the `Deprecations` heading that follows it. After any section write, confirm
the sections below it are still present exactly once. Where the page has no
`ac:link` in it, the cheaper markdown fetch is enough to see the headings.
+
+## Writing pages through the API
+
+`content_file` is rejected for any path outside the repository — a scratchpad
path fails as path traversal. Draft wherever you like, but **pass the body as
inline `content`** when creating or updating a page.
+
+The response carries the new version number. On a page you have just written,
that number is its own check: a create followed by one update should report
version 2, so anything higher means someone else wrote in between.
+
## The GitHub release notes
A release also has a GitHub release at the `STRUTS_X_Y_Z` tag, kept as a
**pre-release** while the vote runs. GitHub's generated body is a starting
point that needs two corrections before it is fit to publish.
-### Check the range before anything else
+### Name the previous tag yourself
-The generated body ends with `**Full Changelog**:
.../compare/<PREVIOUS>...<THIS>`. **Confirm `<PREVIOUS>` is the immediately
preceding release on this line.** GitHub picks it by tag reachability, and
Struts release branches get renamed and re-imported, so older tags are
frequently *not* ancestors of the new one and the heuristic reaches too far
back.
+**Never let GitHub choose the range.** It picks the previous tag by
reachability, and Struts release branches get renamed and re-imported, so older
tags are frequently *not* ancestors of the new one and the heuristic reaches
too far back. For 6.11.0 it chose `STRUTS_6_8_0` and produced ~101 entries, 88
of which had already shipped in 6.9.0 and 6.10.0.
-For 6.11.0 it chose `STRUTS_6_8_0` and produced ~101 entries, 88 of which had
already shipped in 6.9.0 and 6.10.0.
+Generate the body with the previous release named explicitly, and it comes out
right the first time:
-Get the real change set from git, which works even across unrelated histories:
+```bash
+gh api -X POST repos/apache/struts/releases/generate-notes \
+ -f tag_name=STRUTS_7_3_0 -f previous_tag_name=STRUTS_7_2_1 -q .body >
generated.md
+```
+
+Confirm the entry count is plausible against the real change set, which `git
log` gives even across unrelated histories:
```bash
-git log --format='%h %s' STRUTS_6_10_0..STRUTS_6_11_0
+git log --format='%h %s' STRUTS_7_2_1..STRUTS_7_3_0
```
-Drop every generated entry outside that range and correct the Full Changelog
link to the right previous tag. Drop `## New Contributors` too when the
contribution it cites falls outside the range.
+**If you inherit a body GitHub generated on its own**, check the `**Full
Changelog**: .../compare/<PREVIOUS>...<THIS>` line first, and regenerate as
above rather than pruning by hand. When pruning is unavoidable, drop `## New
Contributors` too if the contribution it cites falls outside the range — but
keep it when the contributors are genuinely new in this range.
### Split the entries
@@ -143,16 +197,80 @@ Two sections, `### Dependencies` nested under `## What's
Changed`, before any `#
**The discriminator is the ticket, not the author.** A Dependabot PR carrying
a ticket stays in What's Changed, because a ticketed bump is release content
and appears in the Version Notes Dependency section. A human PR that is purely
a dependency change (`Removes unused jaxb-core dependency`) belongs under
Dependencies. Both cases occur in the 6.9.0 release.
-Preserve the generated relative order within each section, and keep the entry
lines byte-identical — they carry the author and PR links GitHub rendered.
+A PR that mixes a dependency change with something else — CVE-driven library
updates *plus* a CI tweak — stays in What's Changed. Dependencies is for
entries that are nothing but a bump.
+
+Preserve the generated relative order within each section, and keep the entry
lines byte-identical — they carry the author and PR links GitHub rendered.
Split with a script rather than by retyping, then **prove nothing was lost**:
+
+```bash
+diff <(grep '^\* ' generated.md | sort) <(grep '^\* ' new.md | sort)
+```
+
+Empty output means the entry set is unchanged and only the grouping moved.
### Applying it
+The release may or may not exist yet — check before assuming which command you
need.
+
```bash
+# it exists (release cut earlier, or notes already generated):
gh release view STRUTS_X_Y_Z --json body -q .body > original.md # keep, so
it can be restored
gh release edit STRUTS_X_Y_Z --prerelease --notes-file new.md
+
+# it does not exist yet:
+gh release create STRUTS_X_Y_Z --title "Struts X.Y.Z" --prerelease
--verify-tag --notes-file new.md
+```
+
+Pass `--prerelease` either way, so a release still under vote is not silently
promoted, and `--verify-tag` on create so a typo in the tag fails instead of
creating one.
+
+## The test-build announcement
+
+Once the Version Notes page and the GitHub release are both up, the release
manager announces the test build so people can exercise the staged artifacts
during the vote. **Draft it last** — every link in it points at something the
earlier steps produced.
+
+Subject is `[TEST] Apache Struts X.Y.Z test build is ready`. Send it to
**both** lists, Bcc the private one:
+
+```
+To: [email protected], [email protected]
+Bcc: [email protected]
```
-Pass `--prerelease` on the edit so a release still under vote is not silently
promoted.
+Both audiences want it — committers to check the staged artifacts, users to
test against their own applications — and a build announced to only one of them
reaches half the people who could find a problem during the vote.
+
+The body is fixed apart from four substitutions:
+
+```
+Hello,
+
+This is a minor release of Struts <LINE> which contains <WHAT>, and it
+shouldn't break your code<RISK>. Please take your time and test the bits
+- any help is appreciated. Please report any problems you will spot.
+
+Here are the changes from the previous version:
+https://github.com/apache/struts/releases/tag/STRUTS_X_Y_Z
+
+Staging Maven repo
+https://repository.apache.org/content/groups/staging/
+
+Standalone artifacts
+https://dist.apache.org/repos/dist/dev/struts/X.Y.Z/
+
+Release notes
+https://cwiki.apache.org/confluence/display/WW/Version+Notes+X.Y.Z
+
+Kind regards
+--
+Łukasz
+```
+
+| Slot | How to fill it |
+|---|---|
+| `<LINE>` | `6.x` or `7.x` |
+| `<WHAT>` | What the issue list actually contains — `mostly bug fixes` for
6.11.0, `a few improvements and bug fixes` for 7.3.0 |
+| `<RISK>` | Empty when the release has no Breaking changes; ` but it contains
significant changes` when it does. 6.11.0 had none and said nothing; 7.3.0 had
seven and said so |
+| Tag / paths | Tag underscored (`STRUTS_7_3_0`), dist path and page title
dotted (`7.3.0`) |
+
+Do not take the recipients from a previous announcement: 6.11.0 went to `dev@`
alone and 7.3.0 to `user@` alone, and both were mistakes. Address every
announcement to the two lists above.
+
+Keep the security posture of the pages: the mail links the release notes, it
does not summarise what is in them, so no severity, CVE or S2-XXX reaches it
either.
## Re-read the page immediately before you write to it
@@ -169,6 +287,11 @@ After writing, diff against the version you meant to build
on. The diff should s
- Publishing the issue list straight from JIRA without reconciling against the
release branch
- Concluding a backport is missing from a commit-subject grep, or from the
class named in the ticket title
- Treating an untick eted patch dependency bump as a reconciliation gap
+- Dropping a `Won't Do` ticket, or listing it under Improvement or New Feature
as though it shipped
+- A Breaking changes item that runs past one sentence, or restates what the
ticket already explains
+- Reproducing a security ticket's summary in full when it names the bypass or
the leak
+- Letting GitHub pick the previous tag instead of passing `previous_tag_name`
+- Regrouping release entries by retyping them instead of scripting the split
and diffing the result
- A severity, CVE, or S2-XXX reference on the page that has not been published
- Breaking changes assembled by pasting ticket summaries
- Creating the page without adding it to the Migration Guide index
@@ -193,6 +316,10 @@ After writing, diff against the version you meant to build
on. The diff should s
| "The version diff is empty, so nothing changed" | The diff renders markdown,
which drops `ac:link` bodies. Check raw storage. |
| "GitHub generated the changelog, so the range is right" | It guesses the
previous tag by reachability. Renamed branches make it reach too far back.
Verify with `git log PREV..THIS`. |
| "Dependabot authored it, so it goes under Dependencies" | Ticketed bumps
stay in What's Changed. The ticket decides, not the author. |
+| "A Won't Do ticket isn't part of the release" | The decision is news. It
goes under Rejected requests, not into a type section and not into the bin. |
+| "More detail in Breaking changes is safer" | One sentence plus the ticket
link. The reader opens the ticket for detail; the page exists to tell them
whether to. |
+| "The summary is public in JIRA, so I can repeat it" | Not when it names the
bypass or the leak and the bulletin is unpublished. Truncate, and say you did. |
+| "GitHub will work out the previous tag" | Pass `previous_tag_name` and it is
right the first time. |
| "The fix is public, so I can describe the vulnerability" | The ticket being
public does not publish the advisory. Neutral framing until the bulletin ships.
|
| "Breaking changes are the tickets typed as breaking" | They are the changes
that break an application. Author them. |
| "7.x needs different handling from 6.x" | Same structure, same process. Only
the data differs. |
diff --git a/.claude/skills/creating-version-notes/version-notes-template.md
b/.claude/skills/creating-version-notes/version-notes-template.md
index 04b438810..1961df03e 100644
--- a/.claude/skills/creating-version-notes/version-notes-template.md
+++ b/.claude/skills/creating-version-notes/version-notes-template.md
@@ -19,7 +19,9 @@ from a copy of the previous release's page — see the Iron
Rule in `SKILL.md`.
| DONE filter id | Saved-filter id for `issues/?filter=`, labelled `Struts
X.Y.Z DONE`. Each release needs its own; a reused id lists the wrong release. |
| TODO filter id | Constant across releases: `12351174`, labelled `Struts
x.x.x TODO`. |
| Issue sections | One `<h2>` per issue type present, ordered **Bug → New
Feature → Improvement → Task → Dependency**, entries sorted by key ascending. |
-| Breaking changes | Optional. Authored prose, one `<li>` per change. Omit the
section entirely when the release has none. |
+| Breaking changes | Optional. Authored, **one sentence plus the ticket link**
per item. Omit the section when the release has none. |
+| Deprecations | Optional. Same one-line shape, for public API deprecated but
still working. |
+| Rejected requests | Optional. Tickets resolved `Won't Do` against this fix
version — never in a type section. |
| Staging Repository | Always included, on every line — see `SKILL.md`. |
## Corrected storage format
@@ -74,7 +76,20 @@ Three defects present in the published pages are fixed here.
Keep them fixed:
<!-- OPTIONAL: omit the whole section when the release has no breaking changes
-->
<h2>Breaking changes</h2>
<ul style="list-style-type: square;">
- <li>WHAT AN APPLICATION MUST NOW DO DIFFERENTLY, AND WHAT REPLACES THE OLD
BEHAVIOUR [<a
href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>].</li>
+ <li>ONE SENTENCE: WHAT AN APPLICATION SEES DIFFERENTLY [<a
href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>].</li>
+</ul>
+
+<!-- OPTIONAL: public API deprecated but still working -->
+<h2>Deprecations</h2>
+<ul style="list-style-type: square;">
+ <li><code>WHAT</code> is deprecated; use <code>REPLACEMENT</code> instead
[<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>].</li>
+</ul>
+
+<!-- OPTIONAL: tickets resolved Won't Do against this fix version -->
+<h2>Rejected requests</h2>
+<p>Two long-standing requests were closed as <em>Won't Do</em> in this cycle.
They are listed here so the decision is visible rather than silent.</p>
+<ul style="list-style-type: square;">
+ <li>[<a href="https://issues.apache.org/jira/browse/WW-XXXX">WW-XXXX</a>] -
SUMMARY - will not be implemented; REASON WHERE THE RELEASE MANAGER GAVE
ONE.</li>
</ul>
<h2>Bug</h2>
@@ -109,19 +124,29 @@ Repeat the issue `<h2>` block per type present, in the
order given above.
- [ ] `ReleaseNote.jspa` label and its `version=` id are the same release.
- [ ] `DONE` filter label and its `filter=` id are the same release.
- [ ] Issue list reconciled against the release branch via each ticket's
linked PR, not taken from JIRA alone.
-- [ ] Issue types ordered Bug → New Feature → Improvement → Task → Dependency;
empty types omitted.
-- [ ] Breaking changes authored, or the section omitted because there are none.
+- [ ] Every ticket's **resolution** checked, not just its status — `Won't Do`
goes under Rejected requests.
+- [ ] Sections ordered Breaking changes → Deprecations → Rejected requests →
Bug → New Feature → Improvement → Task → Dependency; empty ones omitted.
+- [ ] Each Breaking changes and Deprecations item is one sentence plus its
ticket link.
- [ ] Staging Repository block present.
-- [ ] No unpublished severity, CVE, or S2-XXX reference anywhere on the page.
+- [ ] No unpublished severity, CVE, or S2-XXX reference anywhere on the page,
and any security summary truncated at a clause boundary was reported to the
release manager.
- [ ] Page created as a child of Migration Guide (`13981`).
- [ ] **Listed at the top of the matching `Version Notes N.x` section on the
Migration Guide**, and that edit verified against raw storage — the version
diff renders empty even when the change landed.
- [ ] Page re-fetched immediately before every write.
## GitHub release notes
-- [ ] Original generated body saved before editing, so it can be restored.
-- [ ] Full Changelog compares against the **immediately preceding release** on
this line, verified with `git log PREV..THIS` — GitHub's guess is often wrong
after a branch rename.
-- [ ] Entries outside that range removed, including a `## New Contributors`
block citing one.
-- [ ] Entries split by **ticket, not author**: ticketed → `## What's Changed`;
untick eted dependency bumps → `### Dependencies`.
+- [ ] Body generated with `previous_tag_name` named explicitly, not left to
GitHub's guess.
+- [ ] Entry count sane against `git log PREV..THIS`.
+- [ ] Original body saved first when editing an existing release, so it can be
restored.
+- [ ] Entries split by **ticket, not author**: ticketed → `## What's Changed`;
untick eted dependency bumps → `### Dependencies`; mixed PRs stay in What's
Changed.
- [ ] Generated order and entry text preserved within each section.
-- [ ] `gh release edit` passed `--prerelease` while the vote is open.
+- [ ] Split verified by diffing the sorted entry lists before and after —
empty output.
+- [ ] `--prerelease` passed while the vote is open; `--verify-tag` when
creating.
+
+## Test-build announcement
+
+- [ ] Drafted **after** the Version Notes page and GitHub release exist — it
links both.
+- [ ] Subject `[TEST] Apache Struts X.Y.Z test build is ready`.
+- [ ] Addressed to **both** `[email protected]` and
`[email protected]`, Bcc `[email protected]`.
+- [ ] Risk clause matches reality: silent when there are no Breaking changes,
"but it contains significant changes" when there are.
+- [ ] Tag underscored in the release link, version dotted in the dist path and
page title.