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 f6fef7396 docs(skills): add releasing-struts, and configure the 
release plugin in the pom (#1844)
f6fef7396 is described below

commit f6fef7396c9a8daba7f0e87d5ab6ebb3a23cf30b
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Aug 14 12:39:54 2026 +0200

    docs(skills): add releasing-struts, and configure the release plugin in the 
pom (#1844)
    
    * docs(skills): add releasing-struts, the end-to-end release runbook
    
    The only end-to-end description of the release process was the cwiki
    page "Building Struts 2 - Normal release", last revised in 2017. It is
    still right about JIRA, the release plugin, Nexus and dist.apache.org,
    and wrong about everything downstream: branches develop/master, tag
    STRUTS_2_3_x, exporting the wiki to /docs, building the site with
    Docker Jekyll and committing content/ into an svn production repo, and
    publishing via people.apache.org. Following it today publishes to a
    repository that no longer serves the site.
    
    The four existing skills cover the paperwork -- Version Notes, the
    [VOTE] mail, bulletins, report triage. Nothing covered the mechanics,
    the ordering, or the last mile: the GA [ANN] mail, CVE registration,
    the site update, and post-release follow-through.
    
    releasing-struts is the spine: seven phases, each with a gate that
    someone else can verify, cross-referencing the existing skills rather
    than restating them. release-runbook.md holds the commands, with every
    step marked as verified against the 7.3.0/6.11.0 run or as carried over
    from the cwiki page and still unconfirmed, so the next release manager
    knows which is which.
    
    Three things the cwiki got wrong that were verified here:
    
    - Releases are cut on a release/X.Y.Z-RC1 branch off the line, not on
      main. The [maven-release-plugin] commits never reach main, which is
      why the root pom still read 7.2.2-SNAPSHOT after 7.3.0 shipped -- and
      why the pom is worthless as a source for the release number.
    - The BOM needs no version sync; bom/pom.xml inherits through <parent>
      and the struts-version.version property no longer exists.
    - The GA [ANN] mail goes To user@ with Cc announce@ and
      announcements@struts, not to dev@.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
    
    * docs(skills): ship the stage and promote scripts with releasing-struts
    
    Phases 3 and 5 were automated all along, in the release manager's local
    toolbox at ~/Projects/Apache/minatour/bin. The runbook pointed at a
    directory that only exists on one machine, which is how those steps came
    to be marked unverified in the first place.
    
    Ported both into scripts/, so the skill is self-contained:
    
      stage-assemblies.sh  closed staging repo -> dist/dev, renamed, re-hashed
      promote-dist.sh      dist/dev -> dist/release
    
    Behaviour is unchanged. The deviations, listed in each script's header:
    set -eu so a failed step cannot commit a partial set, a required $VERSION
    so an unset variable cannot create an empty directory or move the whole
    dev tree, tolerant md5/sha1 cleanup, a printed file list before the
    commit, and an explicit -m so the svn move does not open $EDITOR.
    
    .gitignore carries a blanket *.sh, which silently swallowed both files on
    the first attempt. Negated for .claude/skills/**/scripts/*.sh only -- a
    skill that ships a tool has to be able to ship it.
    
    The toolbox's third script, update-struts2-draft-docs.sh, exports
    Confluence into the retired svn production site. Deliberately not ported,
    and called out as dead so nobody runs it.
    
    Both phases are now marked verified. The remaining unverified steps are
    the Nexus close/release clicks, the release:perform fallback, and whether
    the omnibus JIRA ticket is still practice.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
    
    * build(release): configure the release plugin in the pom, not on the 
command line
    
    The release ran on remembered flags: release:prepare needed
    -DautoVersionSubmodules=true and release:perform 
-DretryFailedDeploymentCount=10,
    neither recorded anywhere but a 2017 wiki page. A flag that has to be
    remembered is a flag that will eventually be forgotten, and forgetting
    autoVersionSubmodules means answering a version prompt once per module.
    
    autoVersionSubmodules moves onto maven-release-plugin. Verified against
    the effective pom that it merges with, rather than replaces, the three
    settings inherited from the ASF parent: useReleaseProfile=false,
    goals=deploy, releaseProfiles=apache-release.
    
    retryFailedDeploymentCount moves onto maven-deploy-plugin in
    pluginManagement, which is the only place it can work: release:perform
    forks a new Maven build and the fork does not inherit -D properties from
    the outer invocation, so the flag on the command line was never reaching
    the deploy.
    
    The SCM tag stays interactive. Our tags are STRUTS_X_Y_Z and
    tagNameFormat can only interpolate @{project.version}, which would give
    STRUTS_7.3.0. Noted in the pom so the next reader does not retry it.
    
    Both commands in the runbook lose their flags to match.
    
    Only the 7.x line is changed here; support/struts-6-x-x needs the same
    edit on its own branch.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
    
    * docs(skills): address review on releasing-struts
    
    Review comments:
    
    - creating-security-bulletins is not "phase 7". A bulletin is written
      when the report is triaged, often months before a release carries the
      fix, and plenty are handled with no release in flight. Phase 7 is only
      the reverse dependency: if this release carries a security fix, follow
      that skill once phase 6 is done.
    - Dropped every reference to the minatour toolbox, including the note
      about its dead draft-docs script. The scripts live here now.
    - Phase 1 checks the JDK before building. 7.x builds on 17 and 6.x on 8,
      and whichever JDK is active in the shell is the one that produces the
      release. On a mismatch the instruction is to stop and ask how to
      switch: jenv, SDKMAN, asdf and a hand-set JAVA_HOME all differ, and
      .java-version is gitignored, so there is nothing safe to infer from.
    - Closing the Nexus staging repository is the release manager's action
      in a web UI behind an ASF login. Now written as hand over, wait for
      confirmation, then verify the group URL yourself.
    
    Review findings:
    
    - The site checklist named source/download.cgi, a six-line wrapper
      around mirrors.cgi with no release content, and omitted
      source/releases.md, which holds the release table and which both
      August 2026 site PRs changed. Following it shipped an incomplete PR --
      as PR #323 did earlier today.
    - promote-dist.sh only rejected an empty VERSION while its header
      claimed an unset variable could not move the whole dev directory.
      svn resolves a "." path element rather than rejecting it (".." is
      rejected, "." is not), so VERSION="." moved dist/dev/struts wholesale
      in one irreversible commit. Both scripts now require a version-shaped
      value.
    - set -eu had silently dropped the original's unconditional cleanup, so
      a failed run left an svn working copy with $VERSION already added; a
      later run for a different version would have committed the stale tree
      alongside the new one. Restored via trap, and struts-dev is now
      guarded up front like $VERSION.
    - The phase 3 gate listed five assemblies. There are six: min-lib was
      missed by a [a-z]+ pattern that cannot match a hyphen. 24 files.
    - The default tag would be struts2-project-X.Y.Z; the root artifactId is
      struts2-project, not struts2-parent.
    - The "is the staging repository closed?" diagnostic was unreachable in
      the case it described: under set -e a 404 killed wget first. Wrapped.
    - The rename glob had been widened from *2-assembly*.zip* to
      *2-assembly*, which forced a compensating change to the .pom cleanup
      and was not in the header's deviation list. Restored to the original
      pair, with a comment noting the two lines are coupled.
    - Documented invocation ran in the repository tree, creating ./$VERSION
      and an svn working copy there. Now cd "$(mktemp -d)" first.
    
    Guards exercised: empty, ".", "..", "abc" all rejected by both scripts;
    the wget failure path prints its diagnostic and leaves no working copy.
    
    Co-authored-by: Claude Opus 5 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 <[email protected]>
---
 .claude/skills/creating-release-vote-mail/SKILL.md |   3 +-
 .claude/skills/creating-version-notes/SKILL.md     |   2 +
 .claude/skills/releasing-struts/SKILL.md           | 148 +++++++++++
 .claude/skills/releasing-struts/release-runbook.md | 292 +++++++++++++++++++++
 .../releasing-struts/scripts/promote-dist.sh       |  36 +++
 .../releasing-struts/scripts/stage-assemblies.sh   | 103 ++++++++
 .gitignore                                         |   2 +
 pom.xml                                            |  27 ++
 8 files changed, 612 insertions(+), 1 deletion(-)

diff --git a/.claude/skills/creating-release-vote-mail/SKILL.md 
b/.claude/skills/creating-release-vote-mail/SKILL.md
index e34815b88..4c14a96fd 100644
--- a/.claude/skills/creating-release-vote-mail/SKILL.md
+++ b/.claude/skills/creating-release-vote-mail/SKILL.md
@@ -14,7 +14,8 @@ boilerplate, around a plain-text rendering of the release's 
Version Notes page.
 
 **This is the step after `creating-version-notes`.** That skill produces the 
page, the GitHub
 release and the `[TEST]` announcement; this one consumes all three. If they do 
not exist yet,
-you are in the wrong skill.
+you are in the wrong skill. `releasing-struts` holds the surrounding phases 
and what happens
+once the vote passes.
 
 [`vote-mail-template.md`](vote-mail-template.md) is the source of truth for 
the artifact.
 
diff --git a/.claude/skills/creating-version-notes/SKILL.md 
b/.claude/skills/creating-version-notes/SKILL.md
index e52ca187d..fd8a0fd3c 100644
--- a/.claude/skills/creating-version-notes/SKILL.md
+++ b/.claude/skills/creating-version-notes/SKILL.md
@@ -13,6 +13,8 @@ A Version Notes page answers one question for a user deciding 
whether to upgrade
 
 **One skill covers every maintenance line.** 6.x and 7.x pages share an 
identical structure. The line changes the data (version, prior page, JIRA ids), 
never the process.
 
+**This is phase 3 of a seven-phase release.** `releasing-struts` holds the 
sequence, the gates and the mechanics either side of it; this skill owns the 
paperwork.
+
 ## The Iron Rule
 
 ```
diff --git a/.claude/skills/releasing-struts/SKILL.md 
b/.claude/skills/releasing-struts/SKILL.md
new file mode 100644
index 000000000..b94beedd5
--- /dev/null
+++ b/.claude/skills/releasing-struts/SKILL.md
@@ -0,0 +1,148 @@
+---
+name: releasing-struts
+description: Use when running or planning an Apache Struts release on any 
maintenance line (6.x, 7.x) - cutting the tag, staging artifacts, opening the 
vote, promoting, updating the site and announcing - or when asked what the next 
step in a release is.
+---
+
+# Releasing Struts
+
+## Overview
+
+A release is seven phases with a gate between each. Most of the *writing* is 
already covered by
+other skills; this one owns the **order, the gates, and the mechanics** — and 
it is the only
+place that covers the last mile after the vote passes.
+
+**Core principle:** a phase is finished when its gate is verifiable by someone 
other than you.
+"I ran the command" is not a gate; "the URL resolves" is.
+
+[`release-runbook.md`](release-runbook.md) holds the commands. This page holds 
the sequence and
+the judgement.
+
+## The phases
+
+| # | Phase | Gate before moving on |
+|---|---|---|
+| 1 | Prepare | Branch green, versions decided, BOM in sync |
+| 2 | Cut | Tag pushed, artifacts in a **closed** Nexus staging repo |
+| 3 | Stage | Assemblies in `dist/dev`, Version Notes page live, `[TEST]` mail 
sent |
+| 4 | Vote | 72 h elapsed, three binding `+1`, result mail sent |
+| 5 | Promote | Nexus repo released, `dist/dev` → `dist/release`, 24 h rsync 
waited |
+| 6 | Publish | Site PR merged, GitHub release un-flagged, `[ANN]` mail 
delivered |
+| 7 | Advisories | Bulletins public, CVE records filled, advisory mails 
delivered |
+
+Phase 7 only exists when the release carries a security fix, and *publishing* 
the advisory is
+**strictly after** phase 6 — see *Security work is a separate clock* below. 
Writing the bulletin
+is not: it is usually drafted long before the release exists, and often on its 
own timetable
+entirely.
+
+## Which skill owns which artifact
+
+Cross-references, not copies. Do not restate what these settle:
+
+- **`creating-version-notes`** — the Version Notes page, its Staging 
Repository block, the
+  Migration Guide entry, the GitHub release notes, and the `[TEST]` mail. All 
of phase 3's
+  paperwork.
+- **`creating-release-vote-mail`** — the `[VOTE]` mail. All of phase 4's 
paperwork.
+- **`creating-security-bulletins`** — the S2-XXX page, what may be disclosed 
and when,
+  publication, and the advisory mails.
+
+**That last one is not a phase of this process.** A bulletin gets written when 
the report is
+triaged, which may be months before a release carries the fix, and plenty of 
bulletins are
+handled with no release in flight at all. It is a skill in its own right, 
invoked whenever it is
+needed. Phase 7 is the reverse direction: *if* this release carries a security 
fix, then once
+phase 6 is done, go and follow that skill.
+
+This skill covers what none of them do: phases 1, 2, 5 and 6, and the ordering 
that binds them.
+
+## Two lines, two releases
+
+`main` is the 7.x line; `support/struts-6-x-x` is 6.x. Both are protected and 
both require their
+build to pass. A change that lands on both is **two releases**, each with its 
own tag, vote,
+site entry and announcement — not one release mentioned twice.
+
+They can be cut in parallel and voted in parallel, and usually are. Keep the 
version numbers
+independent: 6.11.0 and 7.3.0 shipped together and share nothing but a date.
+
+**Neither line branch is where the release is cut.** Both August 2026 releases 
were built on a
+`release/X.Y.Z-RC1` branch off the line, so the `[maven-release-plugin]` 
commits never reach
+`main`. A failed vote is then a deleted branch, not a revert.
+
+## The version number is chosen at release time
+
+The `-SNAPSHOT` in the pom is a placeholder, not a decision. Pick the number 
from the semver
+impact of what actually landed since the last tag, and say so out loud before 
cutting — the tag
+is the first irreversible act of the release.
+
+The pom cannot tell you: because releases are cut on a side branch, `main` 
still read
+`7.2.2-SNAPSHOT` after 7.3.0 had shipped.
+
+## Security work is a separate clock
+
+**Nothing about an unpublished advisory goes into the release paperwork.** Not 
the Version
+Notes, not the `[TEST]` mail, not the `[VOTE]`, not the commit messages, not 
the site entry.
+The tickets are neutral; that is deliberate and it is what makes the embargo 
survive a public
+release process.
+
+The advisory follows the release, and the ordering is not negotiable:
+
+```
+release GA  →  bulletin unrestricted  →  advisory mails  →  CVE pushed to MITRE
+```
+
+A bulletin published before the fixed artifact is downloadable tells attackers 
what to look for
+and gives operators nothing to do about it.
+
+**A 6.x release containing only embargoed fixes is self-disclosing** — the 
diff between the two
+tags is the vulnerability whatever the commit messages say. That is a reason 
to bundle it with
+unrelated work, or to publish the bulletins with the release, not a reason to 
pretend otherwise.
+
+## What the old cwiki page gets wrong
+
+[Building Struts 2 — Normal 
release](https://cwiki.apache.org/confluence/pages/viewpage.action?pageId=27832970)
+was last revised in **2017** and is the page a release manager is most likely 
to find. It is
+still right about JIRA, `release:prepare`/`release:perform`, Nexus and 
`dist.apache.org`, and
+wrong about everything downstream:
+
+| It says | Reality |
+|---|---|
+| Branches `develop` / `master` | `main` and `support/struts-6-x-x` |
+| Tag `STRUTS_2_3_x` | `STRUTS_X_Y_Z` for the version being cut |
+| Export the wiki to `/docs` | The site no longer embeds exported Confluence 
pages |
+| Build the site with Docker Jekyll, commit `content/` | The site builds from 
a PR to `apache/struts-site` |
+| `svn co .../infra/websites/production/struts` | Gone; publishing is the 
merge |
+| `people.apache.org`, `source/announce.md`, `downloads.html` | Dead host, and 
the files are `announce-YYYY.md`, `releases.md` and `index.html` |
+
+Treat it as history. If you follow it, you will publish to a repository that 
no longer serves
+the site.
+
+## Gates that are actually load-bearing
+
+- **A closed Nexus staging repo, not just a successful `release:perform`.** 
Until it is closed
+  the URL in the Version Notes resolves to nothing and every tester is blocked.
+- **72 hours, and three binding `+1`.** PMC votes are the binding ones; 
`private@` is on the
+  vote mail so binding voters see it.
+- **24 hours after the `dist` move, before announcing.** ASF mirroring 
guidance. Announcing into
+  an unmirrored release sends everyone to a 404.
+- **The GitHub release stops being a prerelease at phase 6, not at phase 3.** 
During the vote it
+  must still be flagged, or the vote is on an artifact the world already 
treats as final.
+
+## Red Flags — STOP
+
+- Cutting a tag before the version number has been stated and agreed
+- A `[VOTE]` opened on a staging repo that is not closed, or on a link that 
404s
+- Announcing before the 24-hour mirror wait
+- Any severity, CVE, S2-XXX or bulletin link in release paperwork
+- A bulletin unrestricted before the fixed release is downloadable
+- Following the 2017 cwiki page for anything after the Nexus step
+- One release "covering" both maintenance lines
+- Inferring the release version from the `-SNAPSHOT` in the pom
+
+## Common Mistakes
+
+| Mistake | Reality |
+|---|---|
+| "`release:perform` succeeded, so the artifacts are staged" | They are staged 
and *open*. Close the repo or nobody can fetch them. |
+| "The vote passed, so it's released" | Nexus release, dist move and the 
mirror wait all come after. |
+| "I'll announce now and fix the site after" | The announcement links the 
site. Merge the site PR first. |
+| "The 6.x fix is the same change, so one announcement covers both" | Two 
artifacts, two downloads, two sets of affected users. |
+| "The pom says 7.3.1-SNAPSHOT, so this is 7.3.1" | The placeholder is not a 
decision. Semver impact decides. |
+| "The cwiki page is the official process" | It is the 2017 process. Where 
they disagree, this skill is current. |
diff --git a/.claude/skills/releasing-struts/release-runbook.md 
b/.claude/skills/releasing-struts/release-runbook.md
new file mode 100644
index 000000000..1985ed611
--- /dev/null
+++ b/.claude/skills/releasing-struts/release-runbook.md
@@ -0,0 +1,292 @@
+# Release Runbook
+
+The commands, in order. [`SKILL.md`](SKILL.md) holds the sequence, the gates 
and the judgement;
+this file is what you type.
+
+**Provenance.** Everything marked ✔ was verified against the repository or a 
completed release
+(7.3.0 / 6.11.0, August 2026). Everything marked **⚠ unverified** is carried 
over from the 2017
+cwiki page and has *not* been confirmed against a current run — check it 
before relying on it,
+and correct this file when you do.
+
+**The scripts.** Phases 3 and 5 ship with this skill, in [`scripts/`](scripts):
+
+| Script | Phase | What it does |
+|---|---|---|
+| [`stage-assemblies.sh`](scripts/stage-assemblies.sh) | 3 | Closed staging 
repo → `dist/dev`, renamed and re-hashed |
+| [`promote-dist.sh`](scripts/promote-dist.sh) | 5 | `dist/dev` → 
`dist/release` |
+
+Both take `$VERSION` from the environment, refuse to run without it, and 
refuse a value that is
+not a version number — `svn` resolves a `.` path element rather than rejecting 
it, so a stray
+`VERSION` would otherwise move the whole staging tree in one irreversible 
commit.
+
+Run them from a scratch directory (`cd "$(mktemp -d)"`), never from a 
repository checkout: the
+staging script creates `./$VERSION` and a temporary svn working copy in the 
current directory.
+
+---
+
+## Phase 1 — Prepare
+
+✔ Two lines, two releases:
+
+| Line | Branch | Build check that must pass |
+|---|---|---|
+| 7.x | `main` | `Build and Test (JDK 17)` |
+| 6.x | `support/struts-6-x-x` | `Build and Test (8)` |
+
+Both branches are protected in `.asf.yaml` and must be green before you start.
+
+**Check the JDK before building anything.** The line dictates it — 7.x builds 
on **JDK 17**, 6.x
+on **JDK 8** — and the whole release is produced by whichever JDK happens to 
be active in the
+shell. Cutting 6.x on 17 produces artifacts that will not run for the users 
that line exists for.
+
+```bash
+mvn -v          # reports the JDK Maven is actually using, not just $JAVA_HOME
+```
+
+**If it is the wrong version, stop and ask the release manager how to 
switch.** Local
+environments differ — jenv, SDKMAN, asdf, `JAVA_HOME` by hand, a Homebrew 
symlink — and guessing
+at someone's toolchain is how you end up building against a JDK they did not 
intend. Ask, do not
+infer. (`.java-version` is gitignored in this repo, so it is not a reliable 
signal either.)
+
+```bash
+git checkout main && git pull --ff-only
+mvn clean install -DskipAssembly
+```
+
+Then:
+
+- Decide the version number from semver impact. Do not read it off the 
`-SNAPSHOT`.
+- ✔ Confirm `struts-master` (currently `15`) and `struts-annotations` are 
released versions, not
+  snapshots. The root pom's `<parent>` must not point at a snapshot.
+- ✔ The BOM needs no version sync. `bom/pom.xml` inherits the root version 
through its
+  `<parent>` and declares members as `${project.version}`. The cwiki's
+  `struts-version.version` property no longer exists — ignore that step.
+- Review JIRA: every issue fixed since the last tag has a fix version; nothing 
unresolved carries
+  this one.
+- ⚠ unverified: the cwiki's "omnibus ticket" step. The 7.3.0 and 6.11.0 runs 
show no such ticket
+  — treat it as abandoned unless the PMC says otherwise.
+
+## Phase 2 — Cut
+
+✔ **Cut from a release branch, not from the line branch.** Both August 2026 
releases were built
+on `release/X.Y.Z-RC1` branched off the line:
+
+```bash
+git checkout -b release/7.3.0-RC1 main        # or off support/struts-6-x-x 
for 6.x
+git push -u origin release/7.3.0-RC1
+```
+
+The two `[maven-release-plugin]` commits land there and **`main` is never 
touched** — which is
+why the root pom still said `7.2.2-SNAPSHOT` after 7.3.0 shipped, and why the 
pom is worthless
+as a source for the release number.
+
+✔ `maven-release-plugin` 3.3.1, driven interactively, on that branch:
+
+```bash
+mvn release:prepare
+```
+
+✔ No flags. `autoVersionSubmodules` is configured in the root pom, along with 
the ASF parent's
+`useReleaseProfile=false`, `goals=deploy` and 
`releaseProfiles=apache-release`. If you find
+yourself passing `-D` to the release plugin, the setting belongs in the pom 
instead — a flag
+that has to be remembered is a flag that will be forgotten.
+
+✔ **At the SCM tag prompt, type `STRUTS_X_Y_Z`.** The plugin's default would be
+`struts2-project-X.Y.Z` (the root artifactId); every Struts tag in history is 
the underscore
+form, and the GitHub release, the Version Notes and the site all assume it.
+
+This one cannot move into the pom: `tagNameFormat` interpolates 
`@{project.version}` and has no
+string functions, so the best it could produce is `STRUTS_7.3.0`. The prompt 
stays.
+
+Dry run first if you want one — add `-DdryRun=true`, then `mvn release:clean` 
before the real
+run. On failure, re-run the same command: `-Dresume` defaults to true and it 
picks up where it
+stopped.
+
+✔ The result is two commits on the release branch,
+`[maven-release-plugin] prepare release STRUTS_X_Y_Z` and
+`[maven-release-plugin] prepare for next development iteration`, plus the tag.
+
+```bash
+mvn release:perform
+```
+
+✔ `retryFailedDeploymentCount=10` is configured on `maven-deploy-plugin` in 
the root pom, not
+passed here. It has to be in the pom to work at all: `release:perform` forks a 
fresh Maven
+build, and that fork does not inherit `-D` properties from the outer 
invocation — the flag the
+cwiki tells you to pass was doing nothing.
+
+⚠ unverified: the fallback for re-running `perform` elsewhere —
+`git checkout STRUTS_X_Y_Z && mvn javadoc:javadoc deploy -DperformRelease=true 
-Papache-release`.
+
+**Then the staging repository has to be closed — and that is the release 
manager's action, not
+yours.** It happens in the Nexus web UI at <https://repository.apache.org/> 
(Staging Repositories
+→ select → Close), behind an ASF login. Say so, hand over, and **wait for 
confirmation before
+continuing** — phase 3 fetches from the staging *group* URL and gets nothing 
while the repo is
+open.
+
+⚠ unverified in detail, but the gate is checkable and worth checking yourself 
once you are told
+it is done: the artifacts must resolve under
+
+```
+https://repository.apache.org/content/groups/staging/org/apache/struts/struts2-core/$VERSION/
+```
+
+The staging repo is keyed by user *and* public IP. If your IP changed 
mid-release you will have
+two; drop the stale one, checking the dates.
+
+## Phase 3 — Stage
+
+✔ [`scripts/stage-assemblies.sh`](scripts/stage-assemblies.sh) does this. It 
runs on your own
+machine — the cwiki's "log in to `people.apache.org`" step is dead, that host 
is gone.
+
+```bash
+VERSION=7.3.0 .claude/skills/releasing-struts/scripts/stage-assemblies.sh
+```
+
+It fetches `zip`, `md5`, `sha1` and `asc` from the **closed** staging repo, 
strips the
+`2-assembly` infix, drops the `.pom*` files and the legacy `md5`/`sha1` 
hashes, generates
+`.sha256` and `.sha512` locally with `shasum`, prints what it is about to 
publish, then
+`svn add`s the directory to `dist/dev/struts` and commits. It needs your ASF 
svn credentials.
+
+✔ Gate, verified against `dist/release/struts/7.3.0/`:
+
+```
+https://dist.apache.org/repos/dist/dev/struts/$VERSION/
+```
+
+holds **six** assemblies — `struts-$VERSION-all.zip`, `-apps.zip`, 
`-docs.zip`, `-lib.zip`,
+`-min-lib.zip` and `-src.zip` — each with `.asc`, `.sha256` and `.sha512` 
beside it: 24 files.
+No `.md5`, no `.sha1`, no `.pom`. `KEYS` lives one level up, in 
`dist/release/struts/`.
+
+Count them. `set -eu` stops the script on a step that *fails*, not on a crawl 
that quietly
+returns a subset, so a short upload reaches `dist/dev` looking healthy.
+
+**The staging repo must be closed before you run this** — the script pulls 
from the staging
+*group* URL, and an open repo serves nothing there.
+
+Everything else in this phase belongs to **`creating-version-notes`**: the 
Version Notes page,
+its Staging Repository block, the Migration Guide entry, the GitHub release 
(created as a
+**prerelease**), and the `[TEST]` mail to `dev@` and `user@`.
+
+## Phase 4 — Vote
+
+**`creating-release-vote-mail`** owns the mail. The mechanics around it:
+
+- 72 hours minimum, three binding `+1` (PMC members).
+- ✔ `To: [email protected]`, `Bcc: [email protected]`. Never 
`user@`.
+- Close with a result mail on the same thread.
+
+## Phase 5 — Promote
+
+✔ [`scripts/promote-dist.sh`](scripts/promote-dist.sh) does this — one 
server-side `svn mv`:
+
+```bash
+VERSION=7.3.0 .claude/skills/releasing-struts/scripts/promote-dist.sh
+# svn mv https://dist.apache.org/repos/dist/dev/struts/$VERSION/ \
+#        https://dist.apache.org/repos/dist/release/struts/ -m "Release Struts 
$VERSION"
+```
+
+Then **release** the staging repository in Nexus, which replicates to Maven 
Central.
+
+✔ On pruning old releases: the cwiki says to keep only the latest. Current 
practice does not —
+`dist/release/struts/` held 6.8.0, 6.9.0, 6.10.0, 6.11.0, 7.1.1, 7.2.1, 7.3.0 
and `KEYS` in
+August 2026. Everything removed stays available at
+<https://archive.apache.org/dist/struts/>. Decide deliberately; do not prune 
on autopilot.
+
+**Then wait 24 hours** for mirrors before anything in phase 6.
+
+## Phase 6 — Publish
+
+### The site — a PR to `apache/struts-site`
+
+✔ Verified against PR #322 (the 7.3.0 / 6.11.0 GA announcement) and #323.
+
+`_config.yml` — all of these move together:
+
+```yaml
+current_version: 7.3.0
+current_version_short: 730
+prev_version: 6.11.0
+prev_version_short: 6110
+release_date: 1 August 2026
+prev_release_date: 1 August 2026
+release_date_short: 20260801
+prev_release_date_short: 20260801-6110
+```
+
+`release_date` is the **tag** date, not the announcement date — 7.2.1 was 
tagged 15 June and
+announced 30 June, and the site says 15 June. The `*_date_short` values are 
the anchors in
+`announce-YYYY.md`; when two releases share a tag date, disambiguate the second
+(`20260801-6110`) so the two home-page boxes link to their own entries.
+
+Then:
+
+- `source/announce-YYYY.md` — a new `####` entry at the top, newest first, 
with its `{#aYYYYMMDD}`
+  anchor.
+- ✔ `source/releases.md` — the release table (Release / Release Date / 
Vulnerability / Version
+  Notes). **Easy to miss and both August 2026 PRs changed it**; a site PR 
without it is
+  incomplete.
+- `source/index.html` — the GA boxes read from `_config.yml`; the security 
boxes are hand-edited.
+- `source/dtds/` — only if a new DTD shipped.
+
+✔ **Not** `source/download.cgi` — that is a six-line wrapper around 
`mirrors.cgi` with no release
+content in it. `source/download.md` interpolates versions from `_config.yml` 
and its *Prior
+releases* section is a static pointer to `archive.apache.org`, so neither 
needs a per-release
+edit.
+
+Publishing is the merge. There is no separate deploy step and no svn.
+
+### GitHub release
+
+✔ Un-flag the prerelease. Title `Struts X.Y.Z`, tag `STRUTS_X_Y_Z`.
+
+### The `[ANN]` mail
+
+✔ Recipients, from the 7.3.0 and 6.11.0 announcements:
+
+```
+To: [email protected]
+Cc: [email protected], [email protected]
+```
+
+`dev@` is not on it — the list already saw the `[TEST]` mail and the vote.
+
+✔ **Plain text only, and sent from the `@apache.org` identity.** 
`[email protected]` rejects
+any message carrying a `text/html` part —
+
+```
+ezmlm-reject: fatal: Sorry, a message part has an unacceptable MIME 
Content-Type: 'text/html' (#5.2.3)
+```
+
+— and `[email protected]` answers *"Must be sent from an 
@apache.org address."*
+A draft made with the Gmail tool is an HTML draft whatever you pass it; see 
*The mail must be
+text/plain* in `creating-release-vote-mail` for the full contract. One list 
accepting the mail
+is not evidence the format was right.
+
+Body: the GA boilerplate ("pleased to announce … General Availability … 
highest quality grade"),
+the Version Notes link, the Migration Guide link for a major line, the minimum 
JDK/spec
+requirements for that line, and the download page.
+
+## Phase 7 — Advisories
+
+Only when the release carries a security fix, and the *publication* only after 
phase 6. The
+bulletin itself was almost certainly written when the report was triaged, long 
before this
+release existed.
+
+**`creating-security-bulletins`** owns all of it: unrestricting the bulletin, 
the CVE record on
+<https://cveprocess.apache.org>, and the advisory mails from that record's 
*OSS/ASF Emails* tab.
+Follow that skill from here; it is not a step in this runbook.
+
+The order that matters here: the CVE record goes `RESERVED → DRAFT → READY`, 
and **READY is the
+last state a PMC sets**. ASF Security submits it to the CVE Program and sets 
`PUBLIC`, so
+`cve.org` links 404 until they do. That is expected, and it is not a reason to 
delay the
+bulletin or the mails.
+
+## Post-release
+
+- Add the site announcement entry for any advisory (`announce-YYYY.md`), same 
form as the GA one.
+- Check NVD once the CVE is public — affected ranges have been wrong before, 
and the fix is an
+  email to `[email protected]` citing the CVE record.
+- Answer any coordinator (JPCERT/CC and similar) in their existing thread once 
the bulletin is
+  live; they hold their advisory until you confirm.
+- Update the Version Notes page if the vote forced a re-cut.
diff --git a/.claude/skills/releasing-struts/scripts/promote-dist.sh 
b/.claude/skills/releasing-struts/scripts/promote-dist.sh
new file mode 100755
index 000000000..89e408779
--- /dev/null
+++ b/.claude/skills/releasing-struts/scripts/promote-dist.sh
@@ -0,0 +1,36 @@
+#!/bin/sh
+#
+# Phase 5 - promote a release that passed its vote, moving the assemblies from
+# dist/dev to dist/release. This is the point at which the artifacts start
+# replicating to the mirrors.
+#
+# Usage:  VERSION=7.3.0 ./promote-dist.sh
+#
+# Run it only after the vote has passed. Wait 24 hours after this before
+# announcing anything - the announcement links a download page that the mirrors
+# have to have caught up with first.
+
+set -eu
+
+if [ -z "${VERSION:-}" ]; then
+    echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2
+    exit 1
+fi
+
+# Not cosmetic. svn resolves a "." path element instead of rejecting it, so
+# VERSION="." would move the whole of dist/dev/struts into dist/release in one
+# irreversible server-side commit. ".." is rejected by svn; "." is not.
+case "$VERSION" in
+    [0-9]*.[0-9]*.[0-9]*) ;;
+    *)
+        echo "VERSION must look like 7.3.0 (got '$VERSION')" >&2
+        exit 1
+        ;;
+esac
+
+svn mv "https://dist.apache.org/repos/dist/dev/struts/$VERSION/"; \
+       "https://dist.apache.org/repos/dist/release/struts/"; \
+       -m "Release Struts $VERSION"
+
+echo "Done - verify 
https://dist.apache.org/repos/dist/release/struts/$VERSION/";
+echo "Now release the staging repository in Nexus, then wait 24 hours before 
announcing."
diff --git a/.claude/skills/releasing-struts/scripts/stage-assemblies.sh 
b/.claude/skills/releasing-struts/scripts/stage-assemblies.sh
new file mode 100755
index 000000000..967c74682
--- /dev/null
+++ b/.claude/skills/releasing-struts/scripts/stage-assemblies.sh
@@ -0,0 +1,103 @@
+#!/bin/sh
+#
+# Phase 3 - move the release assemblies from the closed Nexus staging 
repository
+# into https://dist.apache.org/repos/dist/dev/struts/$VERSION so they can be 
tested
+# and voted on.
+#
+# Usage:  cd "$(mktemp -d)" && VERSION=7.3.0 /path/to/stage-assemblies.sh
+#
+# Run it from a scratch directory, not from a repository checkout: it creates
+# ./$VERSION and a temporary svn working copy in the current directory.
+#
+# Requires: the staging repository must already be CLOSED in Nexus (an open 
repo
+# serves nothing under the staging *group* URL this fetches from), and your ASF
+# svn credentials for dist.apache.org.
+
+set -eu
+
+if [ -z "${VERSION:-}" ]; then
+    echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2
+    exit 1
+fi
+
+# Not cosmetic: a VERSION of "." resolves server-side to the parent directory,
+# which would publish the whole staging tree.
+case "$VERSION" in
+    [0-9]*.[0-9]*.[0-9]*) ;;
+    *)
+        echo "VERSION must look like 7.3.0 (got '$VERSION')" >&2
+        exit 1
+        ;;
+esac
+
+STAGING_URL="https://repository.apache.org/content/groups/staging/org/apache/struts/struts2-assembly/$VERSION";
+DIST_DEV_URL="https://dist.apache.org/repos/dist/dev/struts/";
+
+if [ -e "$VERSION" ]; then
+    echo "Directory $VERSION already exists here - remove it or run 
elsewhere." >&2
+    exit 1
+fi
+if [ -e struts-dev ]; then
+    echo "Directory struts-dev already exists here - remove it or run 
elsewhere." >&2
+    exit 1
+fi
+
+# Unconditional, as in the original: a half-built working copy left behind can 
be
+# picked up and committed by a later run for a different version.
+cleanup() {
+    rm -rf "$START_DIR/struts-dev"
+}
+START_DIR=$(pwd)
+trap cleanup EXIT
+
+echo "Creating working dir $VERSION"
+mkdir "$VERSION"
+cd "$VERSION"
+
+echo "Getting distro $VERSION from the staging repository"
+if ! wget -erobots=off -nv -l 1 --accept=zip,md5,sha1,asc -r 
--no-check-certificate -nd -nH "$STAGING_URL"; then
+    echo "Download failed. Is the staging repository closed in Nexus?" >&2
+    exit 1
+fi
+
+if ! ls ./*.zip >/dev/null 2>&1; then
+    echo "No assemblies downloaded. Is the staging repository closed in 
Nexus?" >&2
+    exit 1
+fi
+
+# struts2-assembly-7.3.0-all.zip -> struts-7.3.0-all.zip, and the same for the
+# .asc/.md5/.sha1 beside each zip. The .pom files keep their name and are 
removed
+# below - narrowing this glob without widening that one republishes them.
+echo "Renaming files"
+for f in *2-assembly*.zip*; do
+    [ -e "$f" ] || continue
+    mv "$f" "$(echo "$f" | sed s/2-assembly//g)"
+done
+
+echo "Removing unneeded files"
+rm -f struts2-assembly-*.pom*
+rm -f ./*.md5 ./*.sha1
+
+# The ASF publishes sha256/sha512; Nexus only carries the legacy hashes.
+echo "Generating SHA signatures"
+for f in *.zip; do
+    [ -f "$f" ] || continue
+    shasum -a 256 "$f" > "$f.sha256"
+    shasum -a 512 "$f" > "$f.sha512"
+done
+
+echo "Staging the following files:"
+ls -1
+
+cd "$START_DIR"
+
+echo "Publishing artifacts for test"
+svn --no-auth-cache co --depth empty "$DIST_DEV_URL" struts-dev
+mv "$VERSION" struts-dev/
+cd struts-dev
+svn add --force ./
+svn --no-auth-cache commit -m "Updates test release $VERSION"
+
+cd "$START_DIR"
+
+echo "Done - verify https://dist.apache.org/repos/dist/dev/struts/$VERSION/";
diff --git a/.gitignore b/.gitignore
index 85c62f2c4..621f263b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,6 +22,8 @@ Servers/
 
 # Scripts
 *.sh
+# ... except the ones that are part of a skill and have to travel with it
+!.claude/skills/**/scripts/*.sh
 
 # jenv
 .java-version
diff --git a/pom.xml b/pom.xml
index 03a86c1b1..05de4760a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -450,6 +450,19 @@
                     <artifactId>maven-site-plugin</artifactId>
                     <version>3.22.0</version>
                 </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-deploy-plugin</artifactId>
+                    <!--
+                      Releasing pushes a large number of artifacts to Nexus 
and a single dropped
+                      connection fails the whole deploy. Configured here 
rather than passed on
+                      the command line: release:perform forks a new Maven 
build, which does not
+                      inherit -D properties from the outer invocation.
+                    -->
+                    <configuration>
+                        
<retryFailedDeploymentCount>10</retryFailedDeploymentCount>
+                    </configuration>
+                </plugin>
             </plugins>
         </pluginManagement>
 
@@ -458,6 +471,20 @@
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-release-plugin</artifactId>
                 <version>3.3.1</version>
+                <configuration>
+                    <!--
+                      Every module shares the project version, so the release 
plugin should
+                      version them automatically instead of prompting once per 
module.
+
+                      The SCM tag is still entered at the prompt: our tags are 
STRUTS_X_Y_Z and
+                      tagNameFormat can only interpolate @{project.version}, 
which would give
+                      STRUTS_7.3.0.
+
+                      useReleaseProfile, goals and releaseProfiles are 
inherited from the ASF
+                      parent pom and are intentionally not repeated here.
+                    -->
+                    <autoVersionSubmodules>true</autoVersionSubmodules>
+                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>

Reply via email to