From b953d8c9c42650d5d3cffe3e4fc0997ee9610ab7 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 10:11:50 +0200 Subject: [PATCH 1/4] 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 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 --- .../creating-release-vote-mail/SKILL.md | 3 +- .../skills/creating-version-notes/SKILL.md | 2 + .claude/skills/releasing-struts/SKILL.md | 140 +++++++++++ .../releasing-struts/release-runbook.md | 221 ++++++++++++++++++ 4 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/releasing-struts/SKILL.md create mode 100644 .claude/skills/releasing-struts/release-runbook.md diff --git a/.claude/skills/creating-release-vote-mail/SKILL.md b/.claude/skills/creating-release-vote-mail/SKILL.md index e34815b88c..4c14a96fdb 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 e52ca187d5..fd8a0fd3ca 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 0000000000..627539a7c6 --- /dev/null +++ b/.claude/skills/releasing-struts/SKILL.md @@ -0,0 +1,140 @@ +--- +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 it is **strictly after** phase +6 — see *Security work is a separate clock* below. + +## 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. All of phase 7. + +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` and `download.cgi` | + +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 0000000000..7dac56367b --- /dev/null +++ b/.claude/skills/releasing-struts/release-runbook.md @@ -0,0 +1,221 @@ +# 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. + +--- + +## 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. + +```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 `` 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. + +## 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 -DautoVersionSubmodules=true +``` + +✔ **At the SCM tag prompt, type `STRUTS_X_Y_Z`.** The plugin's default would be +`struts2-parent-X.Y.Z`; every Struts tag in history is the underscore form, and the GitHub +release, the Version Notes and the site all assume it. + +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 -DretryFailedDeploymentCount=10 +``` + +⚠ unverified: the fallback for re-running `perform` elsewhere — +`git checkout STRUTS_X_Y_Z && mvn javadoc:javadoc deploy -DperformRelease=true -Papache-release`. + +**Then close the staging repository** at — Staging Repositories +→ select → Close. ⚠ unverified in detail, but the gate is checkable: 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 + +⚠ unverified: the assembly-copying script on the cwiki page targets `people.apache.org`, which no +longer exists. What must be true at the end is checkable: + +``` +https://dist.apache.org/repos/dist/dev/struts/$VERSION/ +``` + +holds the assemblies. ✔ Verified naming, from `dist/release/struts/7.3.0/`: `struts-$VERSION-all.zip`, +`-apps.zip`, `-docs.zip`, `-lib.zip`, `-src.zip`, each with `.asc`, `.sha256` and `.sha512` +alongside. The `2-assembly` infix Nexus uses is stripped. `KEYS` lives one level up, in +`dist/release/struts/`. + +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: dev@struts.apache.org`, `Bcc: private@struts.apache.org`. Never `user@`. +- Close with a result mail on the same thread. + +## Phase 5 — Promote + +⚠ unverified command, from the cwiki: + +```bash +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 +. 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/index.html` — the GA boxes read from `_config.yml`; the security boxes are hand-edited. +- `source/download.cgi` — the Prior Releases section. +- `source/dtds/` — only if a new DTD shipped. + +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 — + +``` +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. + +## Phase 7 — Advisories + +Only when the release carries a security fix, and only after phase 6. + +**`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. + +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. From d700c5e3036ea7af6294e203773b580853ce564d Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 11:27:16 +0200 Subject: [PATCH 2/4] 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 --- .../releasing-struts/release-runbook.md | 56 +++++++++---- .../releasing-struts/scripts/promote-dist.sh | 32 ++++++++ .../scripts/stage-assemblies.sh | 82 +++++++++++++++++++ .gitignore | 2 + 4 files changed, 158 insertions(+), 14 deletions(-) create mode 100755 .claude/skills/releasing-struts/scripts/promote-dist.sh create mode 100755 .claude/skills/releasing-struts/scripts/stage-assemblies.sh diff --git a/.claude/skills/releasing-struts/release-runbook.md b/.claude/skills/releasing-struts/release-runbook.md index 7dac56367b..50acb1dcb5 100644 --- a/.claude/skills/releasing-struts/release-runbook.md +++ b/.claude/skills/releasing-struts/release-runbook.md @@ -3,10 +3,25 @@ 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. +**Provenance.** Everything marked ✔ was verified against the repository, a completed release +(7.3.0 / 6.11.0, August 2026), or the release manager's scripts. 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 and refuse to run without it. They are ports of the +release manager's local toolbox (`~/Projects/Apache/minatour/bin/`), unchanged in behaviour +apart from `set -eu`, the `$VERSION` guard, tolerant hash cleanup and an explicit svn commit +message — each deviation is listed in the script's own header. + +A third script in that toolbox, `update-struts2-draft-docs.sh`, exports Confluence into the +retired svn production site. **It is dead and is deliberately not ported.** Do not run it. --- @@ -91,17 +106,30 @@ two; drop the stale one, checking the dates. ## Phase 3 — Stage -⚠ unverified: the assembly-copying script on the cwiki page targets `people.apache.org`, which no -longer exists. What must be true at the end is checkable: +✔ [`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 the assemblies. ✔ Verified naming, from `dist/release/struts/7.3.0/`: `struts-$VERSION-all.zip`, -`-apps.zip`, `-docs.zip`, `-lib.zip`, `-src.zip`, each with `.asc`, `.sha256` and `.sha512` -alongside. The `2-assembly` infix Nexus uses is stripped. `KEYS` lives one level up, in -`dist/release/struts/`. +holds `struts-$VERSION-all.zip`, `-apps.zip`, `-docs.zip`, `-lib.zip` and `-src.zip`, each with +`.asc`, `.sha256` and `.sha512` beside it. No `.md5`, no `.sha1`, no `.pom`. `KEYS` lives one +level up, in `dist/release/struts/`. + +**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 @@ -117,12 +145,12 @@ its Staging Repository block, the Migration Guide entry, the GitHub release (cre ## Phase 5 — Promote -⚠ unverified command, from the cwiki: +✔ [`scripts/promote-dist.sh`](scripts/promote-dist.sh) does this — one server-side `svn mv`: ```bash -svn mv https://dist.apache.org/repos/dist/dev/struts/$VERSION/ \ - https://dist.apache.org/repos/dist/release/struts/ \ - -m "Release Struts $VERSION" +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. 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 0000000000..7671f953a7 --- /dev/null +++ b/.claude/skills/releasing-struts/scripts/promote-dist.sh @@ -0,0 +1,32 @@ +#!/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. +# +# Ported from the release manager's local toolbox +# (~/Projects/Apache/minatour/bin/update-struts2-dist.sh). Behaviour is unchanged +# except: +# - set -eu and a required $VERSION, so an unset variable cannot move the whole +# dev directory +# - an explicit -m, so the move does not drop into $EDITOR + +set -eu + +if [ -z "${VERSION:-}" ]; then + echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2 + exit 1 +fi + +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 0000000000..6c2ee1a0ae --- /dev/null +++ b/.claude/skills/releasing-struts/scripts/stage-assemblies.sh @@ -0,0 +1,82 @@ +#!/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: VERSION=7.3.0 ./stage-assemblies.sh +# +# 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. +# +# Ported from the release manager's local toolbox +# (~/Projects/Apache/minatour/bin/update-struts2-assemblies.sh). Behaviour is +# unchanged except: +# - set -eu, so a failed step stops the run instead of committing a partial set +# - $VERSION is required up front rather than producing an empty directory +# - the md5/sha1 cleanup no longer fails when there is nothing to remove +# - the staged file list is printed before the commit + +set -eu + +if [ -z "${VERSION:-}" ]; then + echo "VERSION is not set. Usage: VERSION=7.3.0 $0" >&2 + exit 1 +fi + +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 + +echo "Creating working dir $VERSION" +mkdir "$VERSION" +cd "$VERSION" + +echo "Getting distro $VERSION from the staging repository" +wget -erobots=off -nv -l 1 --accept=zip,md5,sha1,asc -r --no-check-certificate -nd -nH "$STAGING_URL" + +if ! ls ./*.zip >/dev/null 2>&1; then + echo "No assemblies downloaded. Is the staging repository closed?" >&2 + exit 1 +fi + +# struts2-assembly-7.3.0-all.zip -> struts-7.3.0-all.zip +echo "Renaming files" +for f in *2-assembly*; do + [ -e "$f" ] || continue + mv "$f" "$(echo "$f" | sed s/2-assembly//g)" +done + +echo "Removing unneeded files" +rm -f struts-"$VERSION"*.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 .. + +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 .. +rm -rf struts-dev + +echo "Done - verify https://dist.apache.org/repos/dist/dev/struts/$VERSION/" diff --git a/.gitignore b/.gitignore index 85c62f2c4b..621f263b2a 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 From 3554a3f052fa0c356da94c1aa41e9c0361b7a2be Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 11:35:59 +0200 Subject: [PATCH 3/4] 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 --- .../releasing-struts/release-runbook.md | 17 ++++++++++-- pom.xml | 27 +++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/.claude/skills/releasing-struts/release-runbook.md b/.claude/skills/releasing-struts/release-runbook.md index 50acb1dcb5..f2219c91eb 100644 --- a/.claude/skills/releasing-struts/release-runbook.md +++ b/.claude/skills/releasing-struts/release-runbook.md @@ -71,13 +71,21 @@ as a source for the release number. ✔ `maven-release-plugin` 3.3.1, driven interactively, on that branch: ```bash -mvn release:prepare -DautoVersionSubmodules=true +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-parent-X.Y.Z`; 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. @@ -87,9 +95,14 @@ stopped. `[maven-release-plugin] prepare for next development iteration`, plus the tag. ```bash -mvn release:perform -DretryFailedDeploymentCount=10 +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`. diff --git a/pom.xml b/pom.xml index 03a86c1b15..05de4760a8 100644 --- a/pom.xml +++ b/pom.xml @@ -450,6 +450,19 @@ maven-site-plugin 3.22.0 + + org.apache.maven.plugins + maven-deploy-plugin + + + 10 + + @@ -458,6 +471,20 @@ org.apache.maven.plugins maven-release-plugin 3.3.1 + + + true + org.apache.maven.plugins From f61e020ef560b310501ff5b4dae918da0697e33d Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Fri, 14 Aug 2026 12:10:03 +0200 Subject: [PATCH 4/4] 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 --- .claude/skills/releasing-struts/SKILL.md | 16 +++-- .../releasing-struts/release-runbook.md | 70 +++++++++++++------ .../releasing-struts/scripts/promote-dist.sh | 18 +++-- .../scripts/stage-assemblies.sh | 55 ++++++++++----- 4 files changed, 111 insertions(+), 48 deletions(-) diff --git a/.claude/skills/releasing-struts/SKILL.md b/.claude/skills/releasing-struts/SKILL.md index 627539a7c6..b94beedd55 100644 --- a/.claude/skills/releasing-struts/SKILL.md +++ b/.claude/skills/releasing-struts/SKILL.md @@ -29,8 +29,10 @@ the judgement. | 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 it is **strictly after** phase -6 — see *Security work is a separate clock* below. +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 @@ -41,7 +43,13 @@ Cross-references, not copies. Do not restate what these settle: 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. All of phase 7. + 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. @@ -101,7 +109,7 @@ wrong about everything downstream: | 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` and `download.cgi` | +| `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. diff --git a/.claude/skills/releasing-struts/release-runbook.md b/.claude/skills/releasing-struts/release-runbook.md index f2219c91eb..1985ed6115 100644 --- a/.claude/skills/releasing-struts/release-runbook.md +++ b/.claude/skills/releasing-struts/release-runbook.md @@ -3,10 +3,10 @@ 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, a completed release -(7.3.0 / 6.11.0, August 2026), or the release manager's scripts. 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. +**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): @@ -15,13 +15,12 @@ current run — check it before relying on it, and correct this file when you do | [`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 and refuse to run without it. They are ports of the -release manager's local toolbox (`~/Projects/Apache/minatour/bin/`), unchanged in behaviour -apart from `set -eu`, the `$VERSION` guard, tolerant hash cleanup and an explicit svn commit -message — each deviation is listed in the script's own header. +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. -A third script in that toolbox, `update-struts2-draft-docs.sh`, exports Confluence into the -retired svn production site. **It is dead and is deliberately not ported.** Do not run it. +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. --- @@ -36,6 +35,19 @@ retired svn production site. **It is dead and is deliberately not ported.** Do n 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 @@ -80,8 +92,8 @@ yourself passing `-D` to the release plugin, the setting belongs in the pom inst 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-parent-X.Y.Z`; every Struts tag in history is the underscore form, and the GitHub -release, the Version Notes and the site all assume it. +`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. @@ -106,9 +118,14 @@ 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 close the staging repository** at — Staging Repositories -→ select → Close. ⚠ unverified in detail, but the gate is checkable: the artifacts must resolve -under +**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 ``` https://repository.apache.org/content/groups/staging/org/apache/struts/struts2-core/$VERSION/ @@ -137,9 +154,12 @@ It fetches `zip`, `md5`, `sha1` and `asc` from the **closed** staging repo, stri https://dist.apache.org/repos/dist/dev/struts/$VERSION/ ``` -holds `struts-$VERSION-all.zip`, `-apps.zip`, `-docs.zip`, `-lib.zip` and `-src.zip`, each with -`.asc`, `.sha256` and `.sha512` beside it. No `.md5`, no `.sha1`, no `.pom`. `KEYS` lives one -level up, in `dist/release/struts/`. +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. @@ -203,10 +223,17 @@ 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/download.cgi` — the Prior Releases section. - `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 @@ -242,10 +269,13 @@ requirements for that line, and the download page. ## Phase 7 — Advisories -Only when the release carries a security fix, and only after phase 6. +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 diff --git a/.claude/skills/releasing-struts/scripts/promote-dist.sh b/.claude/skills/releasing-struts/scripts/promote-dist.sh index 7671f953a7..89e4087792 100755 --- a/.claude/skills/releasing-struts/scripts/promote-dist.sh +++ b/.claude/skills/releasing-struts/scripts/promote-dist.sh @@ -9,13 +9,6 @@ # 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. -# -# Ported from the release manager's local toolbox -# (~/Projects/Apache/minatour/bin/update-struts2-dist.sh). Behaviour is unchanged -# except: -# - set -eu and a required $VERSION, so an unset variable cannot move the whole -# dev directory -# - an explicit -m, so the move does not drop into $EDITOR set -eu @@ -24,6 +17,17 @@ if [ -z "${VERSION:-}" ]; then 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" diff --git a/.claude/skills/releasing-struts/scripts/stage-assemblies.sh b/.claude/skills/releasing-struts/scripts/stage-assemblies.sh index 6c2ee1a0ae..967c746820 100755 --- a/.claude/skills/releasing-struts/scripts/stage-assemblies.sh +++ b/.claude/skills/releasing-struts/scripts/stage-assemblies.sh @@ -4,19 +4,14 @@ # into https://dist.apache.org/repos/dist/dev/struts/$VERSION so they can be tested # and voted on. # -# Usage: VERSION=7.3.0 ./stage-assemblies.sh +# 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. -# -# Ported from the release manager's local toolbox -# (~/Projects/Apache/minatour/bin/update-struts2-assemblies.sh). Behaviour is -# unchanged except: -# - set -eu, so a failed step stops the run instead of committing a partial set -# - $VERSION is required up front rather than producing an empty directory -# - the md5/sha1 cleanup no longer fails when there is nothing to remove -# - the staged file list is printed before the commit set -eu @@ -25,6 +20,16 @@ if [ -z "${VERSION:-}" ]; then 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/" @@ -32,28 +37,45 @@ 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" -wget -erobots=off -nv -l 1 --accept=zip,md5,sha1,asc -r --no-check-certificate -nd -nH "$STAGING_URL" +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?" >&2 + 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 +# 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*; do +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 struts-"$VERSION"*.pom* +rm -f struts2-assembly-*.pom* rm -f ./*.md5 ./*.sha1 # The ASF publishes sha256/sha512; Nexus only carries the legacy hashes. @@ -67,7 +89,7 @@ done echo "Staging the following files:" ls -1 -cd .. +cd "$START_DIR" echo "Publishing artifacts for test" svn --no-auth-cache co --depth empty "$DIST_DEV_URL" struts-dev @@ -76,7 +98,6 @@ cd struts-dev svn add --force ./ svn --no-auth-cache commit -m "Updates test release $VERSION" -cd .. -rm -rf struts-dev +cd "$START_DIR" echo "Done - verify https://dist.apache.org/repos/dist/dev/struts/$VERSION/"