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-site.git
The following commit(s) were added to refs/heads/main by this push:
new 9bf4cca78 ci: convert Struts-site-javadocs job to a declarative
pipeline (#309)
9bf4cca78 is described below
commit 9bf4cca7858f8d4a760ae5ad4defc255faa7c3c5
Author: Lukasz Lenart <[email protected]>
AuthorDate: Wed Jul 1 08:57:33 2026 +0200
ci: convert Struts-site-javadocs job to a declarative pipeline (#309)
* docs: add design spec for javadocs Jenkins pipeline conversion
Converts the shell-based Struts-site-javadocs freestyle job into a
declarative Jenkins pipeline (Jenkinsfile.javadocs), mirroring the
existing root Jenkinsfile conventions. Two intended deviations: use
plain `mvn` instead of `./mvnw`, and push via the explicit `asf`
remote.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* docs: add implementation plan for javadocs Jenkins pipeline
Co-Authored-By: Claude Opus 4.8 <[email protected]>
* ci: add declarative pipeline for the javadocs job
Converts the shell-based Struts-site-javadocs freestyle job into
Jenkinsfile.javadocs. Builds Struts with plain `mvn` and publishes the
staged Maven site into source/maven on main via the asf remote.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 <[email protected]>
---
Jenkinsfile.javadocs | 113 ++++++++++
.../plans/2026-07-01-jenkins-javadocs-pipeline.md | 234 +++++++++++++++++++++
.../2026-07-01-jenkins-javadocs-pipeline-design.md | 127 +++++++++++
3 files changed, 474 insertions(+)
diff --git a/Jenkinsfile.javadocs b/Jenkinsfile.javadocs
new file mode 100644
index 000000000..4fc90a4f6
--- /dev/null
+++ b/Jenkinsfile.javadocs
@@ -0,0 +1,113 @@
+#!groovy
+
+pipeline {
+ agent {
+ label 'git-websites'
+ }
+ parameters {
+ string(name: 'STRUTS_TAG', defaultValue: 'main',
+ description: 'Struts release tag (e.g. STRUTS_7_2_1) or "main"')
+ }
+ options {
+ buildDiscarder logRotator(numToKeepStr: '5')
+ timeout(90)
+ disableConcurrentBuilds()
+ skipStagesAfterUnstable()
+ }
+ environment {
+ MAVEN_OPTS = '-Xmx2048m -Dhttps.protocols=TLSv1.2 -DfailOnError=false'
+ // Required: resolves `mvn` on the ASF agent (we use plain mvn, not the
wrapper).
+ PATH = "${MAVEN_3_LATEST_HOME}:${env.PATH}"
+ }
+ stages {
+ stage('Build Struts & Maven site') {
+ steps {
+ sh '''
+ echo "Building Struts ${STRUTS_TAG} and staging the Maven site"
+
+ rm -rf target/struts
+ git clone https://gitbox.apache.org/repos/asf/struts.git
target/struts
+
+ cd target/struts
+ if [ "${STRUTS_TAG}" = "main" ]; then
+ git checkout main
+ else
+ git checkout "tags/${STRUTS_TAG}"
+ fi
+
+ mvn -B -V clean install -DskipTests
+ mvn -B -V site:site site:stage
+ '''
+ }
+ }
+ stage('Publish to source/maven') {
+ steps {
+ sh '''
+ echo "Publishing the staged Maven site into source/maven on main"
+
+ if ! git config remote.asf.url > /dev/null; then
+ git remote add asf
https://gitbox.apache.org/repos/asf/struts-site.git
+ fi
+
+ git fetch asf
+ git checkout main
+ git pull asf main
+
+ rm -rf source/maven
+ mkdir -p source/maven
+ mv target/struts/target/staging/* source/maven/
+
+ git add source/maven
+ git status
+
+ git diff --cached --quiet || git commit -m "Updates Maven site by
Jenkins"
+ git push asf main
+ '''
+ }
+ }
+ }
+ post {
+ // If this build failed, send an email to the developers.
+ failure {
+ script {
+ emailext(
+ recipientProviders: [[$class: 'DevelopersRecipientProvider']],
+ from: "Mr. Jenkins <[email protected]>",
+ subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} failed",
+ body: """
+There is a build failure in ${env.JOB_NAME}.
+
+Build: ${env.BUILD_URL}
+Logs: ${env.BUILD_URL}console
+Changes: ${env.BUILD_URL}changes
+
+--
+Mr. Jenkins
+Director of Continuous Integration
+"""
+ )
+ }
+ }
+ // Send an email, if the last build was not successful and this one is.
+ fixed {
+ script {
+ emailext(
+ recipientProviders: [[$class: 'DevelopersRecipientProvider']],
+ from: 'Mr. Jenkins <[email protected]>',
+ subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} back to
normal",
+ body: """
+The build for ${env.JOB_NAME} completed successfully and is back to normal.
+
+Build: ${env.BUILD_URL}
+Logs: ${env.BUILD_URL}console
+Changes: ${env.BUILD_URL}changes
+
+--
+Mr. Jenkins
+Director of Continuous Integration
+"""
+ )
+ }
+ }
+ }
+}
diff --git a/docs/superpowers/plans/2026-07-01-jenkins-javadocs-pipeline.md
b/docs/superpowers/plans/2026-07-01-jenkins-javadocs-pipeline.md
new file mode 100644
index 000000000..3b6fb75c8
--- /dev/null
+++ b/docs/superpowers/plans/2026-07-01-jenkins-javadocs-pipeline.md
@@ -0,0 +1,234 @@
+# Jenkins JavaDocs Pipeline Conversion Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development (recommended) or
superpowers:executing-plans to implement this plan task-by-task. Steps use
checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Replace the shell-based `Struts-site-javadocs` freestyle Jenkins job
with a version-controlled declarative pipeline, `Jenkinsfile.javadocs`.
+
+**Architecture:** A declarative Jenkins pipeline at the repo root, run on the
ASF `git-websites` agent, manually triggered with a `STRUTS_TAG` parameter.
Stage 1 clones + builds Struts and stages its Maven site under the gitignored
`target/`; Stage 2 publishes the staged site into `source/maven/` on `main` and
pushes to the `asf` gitbox remote. Mirrors the existing root `Jenkinsfile`
conventions.
+
+**Tech Stack:** Jenkins declarative pipeline (Groovy DSL), shell, Maven (`mvn`
on the ASF agent via `MAVEN_3_LATEST_HOME`), git (gitbox).
+
+## Global Constraints
+
+- Spec:
`docs/superpowers/specs/2026-07-01-jenkins-javadocs-pipeline-design.md`.
+- Behavior must match the current shell script exactly, except two intended
deviations:
+ - Build with plain `mvn`, not `./mvnw`.
+ - Push via the explicit `asf` remote, not `origin`.
+- Commit message that publishes the site MUST be exactly `Updates Maven site
by Jenkins`.
+- Empty-commit guard MUST be preserved: `git diff --cached --quiet || git
commit ...`.
+- Clone location MUST remain `target/struts` (under the gitignored `target/`).
+- `STRUTS_TAG` default is `main`; any other value is treated as a git tag
(`tags/${STRUTS_TAG}`).
+- Do NOT modify the existing root `Jenkinsfile` (that is the separate
`Struts-site` job).
+- Declarative pipelines cannot be validated by a standalone `groovy` binary;
local checks are structural only, real acceptance is a manual Jenkins run (Task
2).
+
+---
+
+### Task 1: Author `Jenkinsfile.javadocs`
+
+**Files:**
+- Create: `Jenkinsfile.javadocs`
+- Reference (do not modify): `Jenkinsfile` (source of the `post` email blocks
and style)
+
+**Interfaces:**
+- Consumes: ASF Jenkins agent env var `MAVEN_3_LATEST_HOME`; agent label
`git-websites`; `emailext` and `DevelopersRecipientProvider` (provided by the
Jenkins controller, same as the existing `Jenkinsfile`).
+- Produces: a pipeline definition file that the Jenkins job (Task 2) points at
via "Pipeline script from SCM".
+
+- [ ] **Step 1: Create the file with the full pipeline**
+
+Create `Jenkinsfile.javadocs` with exactly this content:
+
+```groovy
+#!groovy
+
+pipeline {
+ agent {
+ label 'git-websites'
+ }
+ parameters {
+ string(name: 'STRUTS_TAG', defaultValue: 'main',
+ description: 'Struts release tag (e.g. STRUTS_7_2_1) or "main"')
+ }
+ options {
+ buildDiscarder logRotator(numToKeepStr: '5')
+ timeout(90)
+ disableConcurrentBuilds()
+ skipStagesAfterUnstable()
+ }
+ environment {
+ MAVEN_OPTS = '-Xmx2048m -Dhttps.protocols=TLSv1.2 -DfailOnError=false'
+ // Required: resolves `mvn` on the ASF agent (we use plain mvn, not the
wrapper).
+ PATH = "${MAVEN_3_LATEST_HOME}:${env.PATH}"
+ }
+ stages {
+ stage('Build Struts & Maven site') {
+ steps {
+ sh '''
+ echo "Building Struts ${STRUTS_TAG} and staging the Maven site"
+
+ rm -rf target/struts
+ git clone https://gitbox.apache.org/repos/asf/struts.git
target/struts
+
+ cd target/struts
+ if [ "${STRUTS_TAG}" = "main" ]; then
+ git checkout main
+ else
+ git checkout "tags/${STRUTS_TAG}"
+ fi
+
+ mvn -B -V clean install -DskipTests
+ mvn -B -V site:site site:stage
+ '''
+ }
+ }
+ stage('Publish to source/maven') {
+ steps {
+ sh '''
+ echo "Publishing the staged Maven site into source/maven on main"
+
+ if ! git config remote.asf.url > /dev/null; then
+ git remote add asf
https://gitbox.apache.org/repos/asf/struts-site.git
+ fi
+
+ git fetch asf
+ git checkout main
+ git pull asf main
+
+ rm -rf source/maven
+ mkdir -p source/maven
+ mv target/struts/target/staging/* source/maven/
+
+ git add source/maven
+ git status
+
+ git diff --cached --quiet || git commit -m "Updates Maven site by
Jenkins"
+ git push asf main
+ '''
+ }
+ }
+ }
+ post {
+ // If this build failed, send an email to the developers.
+ failure {
+ script {
+ emailext(
+ recipientProviders: [[$class: 'DevelopersRecipientProvider']],
+ from: "Mr. Jenkins <[email protected]>",
+ subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} failed",
+ body: """
+There is a build failure in ${env.JOB_NAME}.
+
+Build: ${env.BUILD_URL}
+Logs: ${env.BUILD_URL}console
+Changes: ${env.BUILD_URL}changes
+
+--
+Mr. Jenkins
+Director of Continuous Integration
+"""
+ )
+ }
+ }
+ // Send an email, if the last build was not successful and this one is.
+ fixed {
+ script {
+ emailext(
+ recipientProviders: [[$class: 'DevelopersRecipientProvider']],
+ from: 'Mr. Jenkins <[email protected]>',
+ subject: "Jenkins job ${env.JOB_NAME}#${env.BUILD_NUMBER} back to
normal",
+ body: """
+The build for ${env.JOB_NAME} completed successfully and is back to normal.
+
+Build: ${env.BUILD_URL}
+Logs: ${env.BUILD_URL}console
+Changes: ${env.BUILD_URL}changes
+
+--
+Mr. Jenkins
+Director of Continuous Integration
+"""
+ )
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Structural sanity checks**
+
+Run:
+```bash
+# Balanced braces (should print: braces OK)
+awk
'{for(i=1;i<=length($0);i++){c=substr($0,i,1);if(c=="{")n++;if(c=="}")n--}}
END{print (n==0)?"braces OK":"UNBALANCED: "n}' Jenkinsfile.javadocs
+
+# Required blocks are present (each grep should print a match)
+grep -n "agent {" Jenkinsfile.javadocs
+grep -n "string(name: 'STRUTS_TAG'" Jenkinsfile.javadocs
+grep -n "mvn -B -V clean install -DskipTests" Jenkinsfile.javadocs
+grep -n "mvn -B -V site:site site:stage" Jenkinsfile.javadocs
+grep -n 'git push asf main' Jenkinsfile.javadocs
+grep -n 'Updates Maven site by Jenkins' Jenkinsfile.javadocs
+
+# The wrapper must NOT be referenced anymore (should print: no mvnw refs OK)
+grep -q './mvnw' Jenkinsfile.javadocs && echo "FOUND mvnw — must be removed"
|| echo "no mvnw refs OK"
+```
+Expected: `braces OK`; every `grep -n` prints its line; final line prints `no
mvnw refs OK`.
+
+- [ ] **Step 3: Confirm the existing Jenkinsfile is untouched**
+
+Run: `git status --porcelain Jenkinsfile`
+Expected: no output (the `Struts-site` job's file is unchanged).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add Jenkinsfile.javadocs
+git commit -m "ci: add declarative pipeline for the javadocs job
+
+Converts the shell-based Struts-site-javadocs freestyle job into
+Jenkinsfile.javadocs. Builds Struts with plain \`mvn\` and publishes the
+staged Maven site into source/maven on main via the asf remote.
+
+Co-Authored-By: Claude Opus 4.8 <[email protected]>"
+```
+
+---
+
+### Task 2: Wire up and verify the Jenkins job (operational — runs in Jenkins,
not the repo)
+
+This task has no repo commit; its deliverable is a green build that publishes
the site. It cannot be done until Task 1's branch is merged to `main` (the job
checks out `main`).
+
+**Interfaces:**
+- Consumes: `Jenkinsfile.javadocs` on `main`; ASF Jenkins credentials on the
`git-websites` agent.
+- Produces: an updated `source/maven/` on `main` (and, via the ASF Jekyll
buildbot, `asf-site`).
+
+- [ ] **Step 1: Merge Task 1 to `main`**
+
+Open a PR from the Task 1 branch and merge it, so `Jenkinsfile.javadocs`
exists on `main`.
+
+- [ ] **Step 2: Reconfigure the Jenkins job**
+
+In `Struts-site-javadocs` → Configure:
+- Definition: **Pipeline script from SCM**
+- SCM: Git, repo `https://gitbox.apache.org/repos/asf/struts-site.git`, branch
`*/main`
+- Script Path: `Jenkinsfile.javadocs`
+- Save.
+
+- [ ] **Step 3: Dry-run against `main`**
+
+Trigger **Build with Parameters** with `STRUTS_TAG=main`.
+Expected: both stages green; console shows `mvn` (not `mvnw`) being used;
final step either commits `Updates Maven site by Jenkins` and pushes to `asf`
`main`, or reports nothing to commit if the site is unchanged.
+
+- [ ] **Step 4: Verify the published output**
+
+On `main` after the run, confirm `source/maven/` contains freshly generated
Maven site HTML (e.g. `source/maven/index.html` present, timestamps current).
Confirm the ASF buildbot republished to `asf-site` (check
https://struts.apache.org/maven/).
+
+- [ ] **Step 5: (Optional) Tag run**
+
+Re-run with a real tag (e.g. `STRUTS_TAG=STRUTS_7_2_1`) to confirm the
`tags/${STRUTS_TAG}` checkout path works.
+
+---
+
+## Follow-ups (tracked in the spec, not part of this plan)
+
+- Delete `mvnw`, `mvnw.cmd`, `.mvn/` from the **apache/struts** repo (separate
repo/commit) so the wrapper and CI don't drift now that CI uses plain `mvn`.
+- Revisit PR #293's "sole source of truth" note to confirm the `source/maven`
on `main` publish path is still intended.
diff --git
a/docs/superpowers/specs/2026-07-01-jenkins-javadocs-pipeline-design.md
b/docs/superpowers/specs/2026-07-01-jenkins-javadocs-pipeline-design.md
new file mode 100644
index 000000000..9f72588c7
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-01-jenkins-javadocs-pipeline-design.md
@@ -0,0 +1,127 @@
+# Convert the Struts-site-javadocs Jenkins job to a declarative pipeline
+
+**Date:** 2026-07-01
+**Status:** Approved — ready for implementation plan
+
+## Problem
+
+The `Struts-site-javadocs` Jenkins job
(https://ci-builds.apache.org/job/Struts/job/Struts-site-javadocs/)
+is currently a freestyle job with an inline `#!/bin/sh` build step. It:
+
+1. Clones `apache/struts` into `target/struts`, checks out `main` or a release
tag.
+2. Builds the project and generates + stages the Maven site.
+3. Publishes the staged site into `source/maven/` on the `struts-site` `main`
branch,
+ commits, and pushes. The ASF Jekyll buildbot then republishes `main →
asf-site`.
+
+We want this expressed as a declarative Jenkins pipeline (Groovy), consistent
with the
+existing website-build pipeline already in this repo.
+
+## Goals
+
+- Replace the shell freestyle job with a reviewable, version-controlled
declarative pipeline.
+- Preserve current behavior exactly, except for the two deliberate changes
below.
+- Match the conventions of the existing root `Jenkinsfile` (the `Struts-site`
job).
+
+## Non-goals
+
+- No change to what content is published or where (`source/maven/` on `main`).
+- No change to the ASF `main → asf-site` republish mechanism.
+- Removing the `mvnw`/`.mvn/` wrapper files from the **apache/struts** repo
(separate
+ repo, separate commit — see Follow-ups).
+
+## Deliberate changes vs. the current shell script
+
+1. **`./mvnw` → `mvn`.** Use the Maven installation on the ASF agent instead
of the
+ Struts project's Maven wrapper. This makes the `MAVEN_3_LATEST_HOME` PATH
entry
+ load-bearing (it is what resolves `mvn`), so it is kept.
+2. **`git push origin main` → `git push asf main`.** Mirror the existing
`Jenkinsfile`'s
+ explicit `asf` remote pattern rather than relying on the ambient `origin`.
+
+## Design
+
+### Placement
+
+A new file **`Jenkinsfile.javadocs`** at the repo root. The
`Struts-site-javadocs` job is
+reconfigured in Jenkins as **Pipeline script from SCM** → this repo, branch
`main`, script
+path `Jenkinsfile.javadocs`. Both job definitions then live in git.
+
+### Trigger & parameters
+
+Manually triggered ("Build with Parameters"). One parameter:
+
+```groovy
+parameters {
+ string(name: 'STRUTS_TAG', defaultValue: 'main',
+ description: 'Struts release tag (e.g. STRUTS_7_2_1) or "main"')
+}
+```
+
+### Agent, options, environment
+
+```groovy
+agent { label 'git-websites' }
+
+options {
+ buildDiscarder logRotator(numToKeepStr: '5')
+ timeout(90) // full Struts build + site:site is slow
+ disableConcurrentBuilds()
+ skipStagesAfterUnstable()
+}
+
+environment {
+ MAVEN_OPTS = '-Xmx2048m -Dhttps.protocols=TLSv1.2 -DfailOnError=false'
+ PATH = "${MAVEN_3_LATEST_HOME}:${env.PATH}" // required: resolves `mvn` on
the ASF agent
+}
+```
+
+### Stages
+
+**Stage 1 — Build Struts & Maven site**
+
+Single `sh` step, all work under the gitignored `target/` so the struts clone
never
+pollutes the site repo:
+
+- `rm -rf target/struts`
+- clone `https://gitbox.apache.org/repos/asf/struts.git` into `target/struts`
+- checkout: if `STRUTS_TAG == main` → `git checkout main`, else `git checkout
"tags/${STRUTS_TAG}"`
+- `mvn -B -V clean install -DskipTests`
+- `mvn -B -V site:site site:stage`
+
+**Stage 2 — Publish to source/maven**
+
+Mirror the existing `Jenkinsfile` deploy pattern:
+
+- add the `asf` remote if missing →
`https://gitbox.apache.org/repos/asf/struts-site.git`
+- `git checkout main` && `git pull asf main`
+- `rm -rf source/maven && mkdir -p source/maven`
+- `mv target/struts/target/staging/* source/maven/`
+- `git add source/maven`
+- `git diff --cached --quiet || git commit -m "Updates Maven site by Jenkins"`
+- `git push asf main`
+
+### Post
+
+Copy the `post { failure {} fixed {} }` `emailext` blocks verbatim from the
existing
+root `Jenkinsfile` (from "Mr. Jenkins", recipients via
`DevelopersRecipientProvider`).
+
+## Behavior parity checklist
+
+| Aspect | Shell script | Pipeline
| Same? |
+|-------------------------------|-------------------------|-------------------------|-------|
+| Clone location | `target/struts` | `target/struts`
| ✅ |
+| Tag/main selection | `STRUTS_TAG` | `STRUTS_TAG` param
| ✅ |
+| Build command | `./mvnw clean install` | `mvn clean
install` | ⚠️ intended |
+| Site command | `./mvnw site:site …` | `mvn site:site …`
| ⚠️ intended |
+| MAVEN_OPTS | same | same
| ✅ |
+| Publish target | `source/maven` on main | `source/maven` on
main | ✅ |
+| Commit message | "Updates Maven site …" | identical
| ✅ |
+| Empty-commit guard | `git diff --cached -q` | identical
| ✅ |
+| Push remote | `origin` | `asf`
| ⚠️ intended |
+
+## Follow-ups (out of scope for this change)
+
+- Delete `mvnw`, `mvnw.cmd`, and `.mvn/` from the **apache/struts** repo so
the wrapper and
+ the CI job do not drift now that CI uses plain `mvn`.
+- Optional: revisit PR #293's note that the javadocs job is the "sole source
of truth" for
+ `maven/` on `asf-site`, to confirm the `source/maven` on `main` publish path
is still the
+ intended flow.