From Manual WAR Deployments to Jenkins CI/CD and a JEUS Rollback Pipeline
A masked PMWORKS case study: moving Vue/Vite and Maven builds into Jenkins, deploying a WAR to JEUS with restricted service-account wrappers, and restoring a pre-deployment backup through a separate rollback job.
- Case-study environment
- Node.js 22, JDK 17, JEUS 9
On this page
- 1. Define the migration boundary before writing a Jenkinsfile
- 2. Build the frontend before Maven packages the WAR
- 3. Debug the build where the failure actually occurs
- 4. Give deployment automation only the authority it needs
- 5. Back up the current WAR immediately before replacing it
- Important JEUS-managed-storage boundary
- 6. Keep rollback separate, but be precise about what it guarantees
- 7. Treat a JEUS exit code as a signal, not a verdict by itself
- 8. Verify the path, then harden what is still missing
- References
The old deployment path for PMWORKS began on a developer’s laptop. Someone built the Vue frontend, built the Java backend into a WAR, transferred the file to a development VM, stopped JEUS, replaced the WAR, and started JEUS again. Each individual action was understandable. The problem was that no single system could prove which Git revision produced the deployed file, whether the frontend build had been included, or which file should be restored after a bad release.
The replacement was not just a Maven command in Jenkins. It was a repeatable chain from source to artifact to VM, plus a separate, operator-invoked rollback pipeline that restores a server-side WAR backup. That distinction matters: the case did not yet implement automatic failure detection, an automatic rollback trigger, or an HTTP health-check gate. Those are follow-up controls, not features this article claims were already deployed. The legacy process also copied a WAR into JEUS-managed .applications storage; JEUS 9 documentation does not endorse manually replacing files there. This article documents that implementation and its boundary, not a supported template to copy.
This is a technically reconstructed project record. PMWORKS, paths, account names, group names, and addresses below are masked examples. JEUS, Jenkins, Maven, Node.js, npm, and Vite describe the technology used in the case. Adapt every command to your own deployment product, directory ownership, and change process before running it.
1. Define the migration boundary before writing a Jenkinsfile
The original and new paths answer different operational questions:
| Question | Manual process | Implemented development-environment process |
|---|---|---|
| Where is the frontend built? | A developer’s PC | Linux Jenkins agent with an explicitly selected Node.js 22 tool |
| Where is the backend built? | A developer’s PC | Jenkins Maven stage using the development profile |
| What is transferred? | A locally generated WAR | The WAR produced by the Jenkins run; the legacy deployment group copied it into a JEUS-managed location |
| Who restarts JEUS? | A person performing server steps | A narrowly permitted service-account wrapper invoked by the deployment workflow |
| What is preserved before replacement? | No uniform backup step | A timestamped copy of the currently deployed WAR |
| How is rollback started? | Ad hoc manual recovery | A separate rollback job/deployment group, deliberately invoked by an operator |
| What proves application health? | Manual observation | JEUS start evidence and manual application access; automated HTTP health checking remains planned |
This is a development-environment case, not evidence that the same controls were already applied to production. A green Jenkins run, a successfully transferred WAR, a RUNNING JEUS process, and a healthy web application are four different observations.
Git revision
-> Jenkins checkout
-> Vue/Vite build on Node.js 22
-> Maven build on JDK 17 -> WAR
-> copy current WAR to backup
-> stop JEUS -> transfer WAR -> start JEUS
-> inspect runtime and application response
Separate rollback job, when an operator decides to invoke it:
select server-side backup -> stop JEUS -> restore WAR -> start JEUS
-> inspect runtime and application response
The sequence makes the artifact boundary explicit. The WAR deployed to JEUS must be the one produced by this pipeline, and the backup must represent the file that was on the VM immediately before that particular replacement. The diagram describes what this development project did; it does not imply that direct replacement of JEUS-managed files is a generally supported deployment API.
2. Build the frontend before Maven packages the WAR
PMWORKS has a frontend/ directory beside its root pom.xml. In the initial Jenkins attempt, Maven ran without the frontend build. The resulting WAR differed substantially from the locally produced WAR. The missing stage, rather than Maven itself, explained the difference.
repository/
├── pom.xml
├── src/
└── frontend/
├── package.json
├── package-lock.json
├── vite.config.js
└── src/
The important part of the masked Declarative Pipeline is the order and working directory:
stage('Frontend Build') {
tools { nodejs 'node22' }
steps {
dir('frontend') {
sh 'npm install && npm run build-dev'
}
}
}
stage('Backend Build') {
tools { maven 'maven' }
steps {
sh 'mvn clean package -Pdev && ls -lh target/*.war'
}
}
dir('frontend') makes npm run where package.json exists. When that block ends, Jenkins restores the previous workspace context; adding a compensating cd .. before Maven is unnecessary. The example reflects the command used in the case, npm install. For a reproducible CI rebuild, the next hardening step is npm ci with a committed, valid lockfile: it performs a clean install and fails if the lockfile and manifest disagree. Change that only after confirming the existing lockfile and dependency supply path support it.
The environment table named JDK 17, but the shortened Jenkinsfile only selected Node.js and Maven tools. An agent already configured with JDK 17 can make this work, yet the pipeline itself does not prove that selection. Pin the JDK in the agent image or Jenkins tool configuration and record java -version, node --version, and mvn -version in the build evidence. Likewise, a shared library loaded from @main can change between runs; a protected release tag or immutable revision is better when exact replay matters.
The custom deployVM(...) and convertTag(...) calls in the original Jenkinsfile came from an internal shared library and deployment platform. They are not built-in Jenkins Pipeline steps. Their masked names explain where the built artifact crosses from Jenkins into VM deployment, not a drop-in recipe for another installation.
3. Debug the build where the failure actually occurs
Moving a working local build to a Linux CI agent exposed three distinct failure classes.
| Symptom observed | Boundary that failed | Useful diagnosis and correction |
|---|---|---|
npm WARN EBADENGINE, dependency requires Node 20+ while agent has Node 16 | Runtime version | Select a compatible Node.js tool explicitly; the case used Node.js 22. Record the effective version in the job log. |
npm install returns a 503 while fetching a tarball from an external CDN | Package download path, not Vue or Vite compilation | Check the requested URL, registry configuration, proxy/egress rules, and internal artifact repository. In the case, the needed artifact was made available through the internal Nexus path. |
Vite cannot resolve a .vue import that builds on a Windows workstation | File-name case on the Linux workspace | Compare the import’s exact bytes with the Git-tracked file name. Correct the import and perform a two-step git mv if the filesystem or Git index fails to record a case-only rename. |
For example, RiskLeaderThruDate.vue and RIskLeaderThruDate.vue are different names to a case-sensitive Linux build. Fixing the source is preferable to adding a CI-only alias that conceals the mismatch. A two-step rename might look like:
git mv RIskLeaderThruDate.vue intermediate.vue
git mv intermediate.vue RiskLeaderThruDate.vue
The diagnostic ordering is simple: if dependency installation fails, examine package supply and network reachability before debugging frontend source; if installation succeeds but npm run build-dev fails, inspect the compiler output and source tree. Do not treat every failed Jenkins stage as a Jenkins problem.
4. Give deployment automation only the authority it needs
The SSH/SFTP identity and the JEUS runtime identity were separated. In this masked example, pmworks.cicd transfers files and invokes a small set of commands; pmworks.sec owns the JEUS operation. Jenkins does not receive a general-purpose root shell or unrestricted impersonation of the service account.
Jenkins / deployment platform
-> SSH and SFTP as pmworks.cicd
-> sudo -n -u pmworks.sec for four approved wrappers only
-> JEUS stop, start, backup, or restore
An illustrative sudoers rule names only those wrapper entry points:
pmworks.cicd ALL=(pmworks.sec) NOPASSWD: \
/opt/pmworks/cicd/bin/pmworks-stop.sh "", \
/opt/pmworks/cicd/bin/pmworks-start.sh "", \
/opt/pmworks/cicd/bin/pmworks-backup.sh "", \
/opt/pmworks/cicd/bin/pmworks-rollback.sh ""
sudo -n fails instead of waiting for a password prompt in a noninteractive job. In sudoers, "" after each command means no arguments; omitting an argument specification would permit arbitrary arguments to that command. The apparent least privilege is real only if the deployment identity cannot modify those scripts, their parent directories, or files they execute or source. Protect wrapper ownership, permissions, and the service account’s environment file; review the effective sudoers policy with an administrator. Giving the deployer a writable wrapper and then allowing it through sudo would defeat the restriction.
The service account’s JEUS commands also need a predictable noninteractive environment. A login shell may read .bash_profile; an SSH command or sudo -u call might not. The case therefore set HOME, JAVA_HOME, JEUS_HOME, and PATH in wrapper scripts and loaded a service-owned environment file. Keep secrets out of Jenkinsfile, source control, command-line arguments, and debug logs. A chmod 600 on the environment file is useful only when its owner and parent-directory permissions also prevent unintended access or replacement.
This design intentionally separates what the two identities can do. The legacy SFTP overwrite had its own permission boundary: a group-writable directory was not enough when the existing WAR was owned by the service account and was mode 644. Group membership, directory permissions, and effective file permission explained that failure. The lesson is not to broaden access to JEUS-managed storage: recursive chmod 777 and recursive ownership changes would compound the problem. A supported redesign should transfer to a service-owned staging location and let JEUS’s deployment tooling manage its own application store.
5. Back up the current WAR immediately before replacing it
The development deployment group performed the server operations in this order:
All build stages pass
-> backup the WAR currently installed on the VM
-> stop JEUS
-> transfer the new WAR through SFTP
-> start JEUS
-> inspect start state and application response
The backup wrapper copied the existing target to a dedicated directory with a sortable timestamp, for example 20260917-134512_pmworks.war. This gives an inspectable recovery source, but it also creates a first-deployment exception: without an installed WAR, the backup step fails and the initial install needs its own explicit path. The original implementation did not record a SHA-256 digest or guard against two backups receiving the same second-level timestamp.
Important JEUS-managed-storage boundary
The source case targeted a path under DOMAIN_HOME/.applications, JEUS’s domain-managed application store. JEUS 9 describes that directory as managed by installation commands and documents application management and deployment through JEUS tools. Direct SFTP replacement and cp -f restoration in that directory were characteristics of this legacy environment, not a portable or officially recommended JEUS deployment method. A reboot or synchronization with the Master Server can also make a file-level change an unreliable representation of the managed application state.
For a new implementation, first preserve the built WAR and its digest in an artifact store or controlled staging directory outside .applications. Then use the JEUS-supported installation/deployment or redeployment workflow appropriate to that domain; use the same mechanism to return to a selected earlier artifact. JEUS documents install-application, deploy-application, and redeploy-application. The exact command sequence depends on application ID, repository mode, Master Server, and target servers, so it must be tested in the target environment rather than copied from this case study.
Hash recording, collision detection, a cross-job lock, and a verified artifact-to-release mapping remain recommended hardening, not completed PMWORKS controls. A direct overwrite can leave a partial file after a failed transfer, which is another reason to retire the legacy file-replacement path instead of merely adding chmod or cp options.
The displayed Jenkins pipeline calls an internal deployVM step, while pre- and post-deployment commands are configured in the VM deployment group. If those commands are hidden from the Jenkinsfile, retain a versioned copy or change record of that group configuration; otherwise a build log alone cannot reproduce the full deployment sequence.
6. Keep rollback separate, but be precise about what it guarantees
The rollback job uses a separate VM deployment group configured for command execution only. It does not check out an earlier commit or rebuild with npm and Maven. That makes the recovery path shorter and removes dependency downloads and build-tool drift from the incident response itself.
Operator invokes rollback job
-> stop JEUS
-> restore wrapper automatically selects the latest backup WAR
-> restore it over the target WAR
-> start JEUS
-> inspect runtime and application response
The original restore wrapper selected the lexicographically latest *_pmworks.war file from the backup directory and copied it over the legacy JEUS-managed target. The timestamp format makes selection easy, but latest does not mean known-good. Suppose deployment A is unhealthy, deployment B is attempted before A is fully resolved, and the second run backs up A. The newest backup is now the unhealthy artifact. A repeated rollback can also select the same backup again without changing the outcome. In a redesigned workflow, the selected earlier artifact should be restored through the supported JEUS deployment path, not by writing into .applications.
The current job does not pause for backup approval: it selects the latest matching filename when the restore wrapper runs. An operator can inspect the backup directory before starting the job, but that does not freeze the selection or prove the file was healthy, especially while another deployment can run concurrently. A stronger design would store a release manifest with Git revision, artifact SHA-256, deploy time, validation result, and explicit last-known-good status, then allow a specific backup ID to be selected. Until health validation and that provenance exist, describe the operation as “restore the latest saved WAR,” not “automatically restore the last healthy release.”
The command-only group’s stop && restore && start chain is fail-fast, which is useful, but it has another consequence: if the restore fails after a successful stop, the start command is skipped and the service can remain down. Document an operator recovery procedure for that state, including how to inspect the target file, choose another verified backup, and restart safely. Never solve that by blindly appending ; start, which could boot a missing or partial WAR.
Separate Jenkins jobs can still act on the same VM at the same time. disableConcurrentBuilds() within one job does not serialize a deployment job against a different rollback job. Add one target-environment lock across both workflows and decide how to handle a queued release while recovery is in progress.
7. Treat a JEUS exit code as a signal, not a verdict by itself
One confusing observation in this case was a Jenkins stage marked failed even though the JEUS output included:
Successfully started the server.
The server state is now RUNNING.
Failed command ... with status 10
Jenkins marks a shell step failed when the process returns a nonzero status. A log line saying RUNNING and an exit code of 10 are contradictory signals that require investigation. The legacy start wrapper did map exit code 10 to 0 after environment-specific log review and repeated testing; it did not perform a fresh runtime-state or HTTP check inside that exception branch. That means the mapping records a local observation, not an officially documented JEUS-wide rule or proof of application health.
Do not copy a blanket 10 -> 0 mapping into another JEUS environment. The wrapper should first verify the service state and, ideally, a real application request; only then should it report success. If either check fails, preserve a failing exit status and keep the deployment visible as failed. Also preserve the original command output and return code in the audit trail so the exception does not hide a future behavior change.
The case’s current manual application-access check belongs after the server process check. A healthy JEUS process can still serve a broken WAR, and a successful file transfer says nothing about whether the application’s dependencies, routes, or database connections work.
8. Verify the path, then harden what is still missing
For the implemented development workflow, an operator should be able to connect one release attempt across these observations:
| Boundary | Evidence to retain | What it does not prove alone |
|---|---|---|
| Checkout | Git revision and repository/branch | That the packaged WAR contains the frontend output |
| Frontend build | Tool version, dependency result, Vite output | That Maven included the new assets |
| Backend build | Maven profile, JDK version, WAR name and size | That this exact WAR reached the VM |
| Backup | File name, timestamp, size, optional checksum | That the backed-up application was healthy |
| Transfer | Target path, file size, optional checksum | That JEUS started the new artifact |
| Restart | JEUS command exit code and observed state | That HTTP requests and dependencies work |
| Application | A real response from the expected deployment | That every business journey is healthy |
| Rollback | Selected backup ID, restored file and response | That “latest” was the intended prior release |
This matrix separates build success, deployment success, and service health. The original case reached a repeatable build/deploy path and a separate manual rollback path. It did not yet close every evidence gap above. The next changes should be prioritized by the risk they remove:
- Add a real application health endpoint and fail the deployment when it does not respond as expected. Only then consider an automatic rollback trigger, with explicit safeguards against loops and false positives.
- Move deployment and rollback off direct writes to JEUS-managed
.applications: stage versioned WARs outside that directory and use the supported JEUS application-management workflow for both directions. - Record the Git revision and SHA-256 of the built, transferred, and backed-up WAR. Maintain a last-known-good release manifest rather than selecting by timestamp alone.
- Replace
npm installwithnpm ciafter validating the lockfile and internal Nexus dependency path. Pin Node.js, JDK, Maven, plugins, and the shared-library revision sufficiently for repeatable builds. - Add a cross-job lock for deployment and rollback, plus a first-deployment path and a procedure for restore failure after JEUS has stopped.
- Define backup retention and restore drills before deleting old artifacts. A policy such as “keep ten” or “delete after 30 days” is only safe once recovery requirements and storage limits are understood.
- For production, add a change approval gate, separate production identities and backup locations, and a tested recovery decision tree. A development job should not be promoted by changing only a branch parameter.
The practical lesson is not that a Jenkinsfile makes deployment safe. It is that a release needs a traceable artifact, an explicit authority boundary, a verified replacement sequence, and a recovery path whose selected file is actually the one the operator intends to restore.
References
- Jenkins: Pipeline syntax
- Jenkins: Using a Jenkinsfile
- Jenkins: Credentials in Pipeline
- Jenkins: Shared Libraries
- Jenkins: Lockable Resources plugin
- NodeJS Jenkins plugin
- sudoers manual
- npm:
npm ci - JEUS 9: Environment and managed application directory
- JEUS 9: Application management in a domain
- JEUS 9: Application commands