From 58a9a013ad3e07f0cf33267ec00f906ca8646749 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 18:16:44 +0200 Subject: [PATCH] docs: point the releasing-struts skill at the published Release Guidelines 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 --- .claude/skills/releasing-struts/SKILL.md | 54 ++-- .../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 b94beedd55..687dbddbb7 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 1985ed6115..acece3284c 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 `` must not point at a snapshot. -- ✔ The BOM needs no version sync. `bom/pom.xml` inherits the root version through its - `` 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 (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 (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: dev@struts.apache.org`, `Bcc: private@struts.apache.org`. 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 -. 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: user@struts.apache.org -Cc: announce@apache.org, announcements@struts.apache.org -``` - -`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.** `announce@apache.org` 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.** `announce@apache.org` 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 `announcements@struts.apache.org` 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 , 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 `nvd@nist.gov` 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.