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 b82f21244 docs: point the releasing-struts skill at the published
Release Guidelines (#1848)
b82f21244 is described below
commit b82f212440a8a2f8e1405f057cff72e57ccc9f9d
Author: Lukasz Lenart <[email protected]>
AuthorDate: Fri Aug 14 19:30:46 2026 +0200
docs: point the releasing-struts skill at the published Release Guidelines
(#1848)
The release process is now documented at
struts.apache.org/release-guidelines.html
(apache/struts-site#324, #325), and the cwiki pages the skill warned about
are
stubs pointing there. The skill was still treating "Building Struts 2 -
Normal
release" as a live trap and carrying a near-complete second copy of the
process
in release-runbook.md - two sources of truth that would drift on the next
release.
Cut the runbook down to what the site page cannot carry: where a step is the
release manager's to take rather than the agent's (closing and releasing the
Nexus staging repository), why the STRUTS_X_Y_Z tag prompt cannot move into
the
pom, the text/plain contract on the [ANN] mail, and the scripts this skill
ships.
Everything else is a link.
SKILL.md keeps the gates and the judgement, and now says corrections belong
in a
struts-site PR rather than here.
Two fixes that predate the port: the phase 1 gate said "BOM in sync" while
the
runbook said the BOM needs no sync, and the runbook told you never to run
the
staging script from a checkout and then gave a checkout-relative invocation.
Co-authored-by: Claude Opus 5 <[email protected]>
---
.claude/skills/releasing-struts/SKILL.md | 54 ++--
.claude/skills/releasing-struts/release-runbook.md | 282 ++++-----------------
2 files changed, 77 insertions(+), 259 deletions(-)
diff --git a/.claude/skills/releasing-struts/SKILL.md
b/.claude/skills/releasing-struts/SKILL.md
index b94beedd5..687dbddbb 100644
--- a/.claude/skills/releasing-struts/SKILL.md
+++ b/.claude/skills/releasing-struts/SKILL.md
@@ -7,21 +7,29 @@ description: Use when running or planning an Apache Struts
release on any mainte
## 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.
+A release is seven phases with a gate between each.
+
+**The process itself is published**, at
+[Release Guidelines](https://struts.apache.org/release-guidelines.html) —
every phase, every
+command, the release policy and the one-time setup a new release manager
needs. It is the source
+of truth, it is maintained in `apache/struts-site`
(`source/release-guidelines.md`), and it is
+what you follow.
+
+This skill is the agent's half of it: the judgement about *ordering* and *when
to stop*, which
+sibling skill owns which document, and the points where a step is the release
manager's to take
+rather than yours. [`release-runbook.md`](release-runbook.md) holds that last
part.
**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.
+**Corrections go to the site page.** If a release teaches you something the
guidelines get wrong,
+fix them in a PR to `apache/struts-site`. Only what is genuinely
agent-specific belongs here.
## The phases
| # | Phase | Gate before moving on |
|---|---|---|
-| 1 | Prepare | Branch green, versions decided, BOM in sync |
+| 1 | Prepare | Branch green, version decided, parent poms released |
| 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 |
@@ -51,7 +59,8 @@ handled with no release in flight at all. It is a skill in
its own right, invoke
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.
+Phases 1, 2, 5 and 6 have no sibling skill — the guidelines carry those steps,
and this skill
+carries the ordering that binds them.
## Two lines, two releases
@@ -95,24 +104,17 @@ and gives operators nothing to do about it.
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
+## The cwiki release pages are retired
-[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` |
+The wiki pages a release manager used to land on — *Building Struts 2 — Normal
release*,
+*Fast track release*, *Creating and Signing a Distribution*, *One time steps*,
+*Sample announcements* — were retired in August 2026 and now carry nothing but
a pointer to
+[Release Guidelines](https://struts.apache.org/release-guidelines.html). Their
old content
+survives only in page history, where it describes a process last revised
between 2013 and 2017:
+`develop`/`master` branches, `people.apache.org`, an svn checkout of the
production site.
-Treat it as history. If you follow it, you will publish to a repository that
no longer serves
-the site.
+**Never restore a step from that history.** If something in the guidelines
looks incomplete, the
+answer is the last release, not the last wiki revision.
## Gates that are actually load-bearing
@@ -132,9 +134,10 @@ the site.
- 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
+- Reviving a step from the history of a retired cwiki page
- One release "covering" both maintenance lines
- Inferring the release version from the `-SNAPSHOT` in the pom
+- Closing or releasing a Nexus staging repository yourself — that is the
release manager's login
## Common Mistakes
@@ -145,4 +148,5 @@ the site.
| "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. |
+| "The process is documented in this skill" | It is documented on the site.
This skill adds ordering, ownership and hand-offs. |
+| "I found the release steps on the wiki" | Those pages are stubs now. Their
history is the 2013–2017 process. |
diff --git a/.claude/skills/releasing-struts/release-runbook.md
b/.claude/skills/releasing-struts/release-runbook.md
index 1985ed611..acece3284 100644
--- a/.claude/skills/releasing-struts/release-runbook.md
+++ b/.claude/skills/releasing-struts/release-runbook.md
@@ -1,14 +1,21 @@
# Release Runbook
-The commands, in order. [`SKILL.md`](SKILL.md) holds the sequence, the gates
and the judgement;
-this file is what you type.
+**The process is published, not kept here.** Every phase, every command, every
gate is at
+[Release Guidelines](https://struts.apache.org/release-guidelines.html),
maintained in
+`apache/struts-site` as `source/release-guidelines.md`. Read it there and
follow it.
-**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.
+This file holds only what that page cannot: the points where a step is a
human's to take rather
+than yours, and the scripts this skill ships. [`SKILL.md`](SKILL.md) holds the
sequence, the
+gates and the judgement.
-**The scripts.** Phases 3 and 5 ship with this skill, in [`scripts/`](scripts):
+**When you learn something new during a release, it goes in the site page.** A
correction that
+lands only here is a correction the next release manager will never see.
+
+## The scripts
+
+Phases 3 and 5 ship with this skill, in [`scripts/`](scripts). The Release
Guidelines link to
+them by GitHub URL, so they are part of the published process — changing their
behaviour means
+updating that page too.
| Script | Phase | What it does |
|---|---|---|
@@ -19,274 +26,81 @@ Both take `$VERSION` from the environment, refuse to run
without it, and refuse
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.
+Run them from a scratch directory, never from a repository checkout:
`stage-assemblies.sh`
+creates `./$VERSION` and a temporary svn working copy in the current
directory. That means
+calling them by absolute path, since the scratch directory is not the checkout:
```bash
-mvn -v # reports the JDK Maven is actually using, not just $JAVA_HOME
+cd "$(mktemp -d)"
+VERSION=7.3.0
~/Projects/Apache/struts/.claude/skills/releasing-struts/scripts/stage-assemblies.sh
```
-**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
-```
+## Phase 1 — Prepare
-Then:
+**If the JDK is wrong, ask — do not infer.** `mvn -v` reports what Maven is
actually using, and
+the line dictates what that must be (7.x on 17, 6.x on 8). 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. `.java-version` is
gitignored in this
+repo, so it is not a signal either.
-- 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.
+**State the version number and get agreement before phase 2 begins.** The tag
is the first
+irreversible act of the release, and the pom cannot tell you the number.
## 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
+At the SCM tag prompt, `STRUTS_X_Y_Z` is typed by hand every time. **This one
cannot move into
+the pom**, so do not "fix" it: `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`.
+**Closing the staging repository 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 repository is
open.
-**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
+The gate is worth checking yourself once you are told it is done:
```
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.
+Run [`stage-assemblies.sh`](scripts/stage-assemblies.sh) as above, then
**count the files** at
+`https://dist.apache.org/repos/dist/dev/struts/$VERSION/`: six assemblies,
each with `.asc`,
+`.sha256` and `.sha512`, 24 in total. `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.
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@`.
+**prerelease**), and the `[TEST]` mail.
## 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.
+**`creating-release-vote-mail`** owns the mail. Nothing here.
## 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.
+Run [`promote-dist.sh`](scripts/promote-dist.sh). **Releasing the staging
repository in Nexus is
+again the release manager's action** in the web UI — hand over and wait, as in
phase 2.
-✔ 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.
+Pruning old releases from `dist/release/struts/` is a deliberate decision,
never an autopilot
+step: several supported versions from both lines are normally kept.
## 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 —
+**The `[ANN]` mail must be `text/plain`, and a draft made with the Gmail tool
is an HTML draft
+whatever you pass it.** `[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.
+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.
## 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.