Webhooks, Jenkins and CI Integrations, Maven Deployments, Build Promotion, and Event Automation: Guided Hands-On Workflow and Core Operations
Build a disposable end-to-end path: create one synthetic Maven release, publish it with a scoped identity, capture or replay a signed repository event, and transfer the already-built artifact to the next stage without recompilation.
Learning objectives
- Create a disposable Maven hosted repository path and publish one synthetic release with a temporary client credential context.
- Capture a repository webhook on loopback when the capability exists, or replay an equivalent signed fixture when it does not.
- Verify HMAC and delivery UUID before allowing an event to trigger a downstream action.
- Promote the already-built JAR without recompilation and prove SHA-256 identity across both repositories.
- Produce before/after repository, client, event, and checksum evidence, then clean up only Chapter 21 resources.
com.example coordinates, and disposable service
identities.
1. Disposable lab contract
Use a local self-hosted Nexus archive installation on loopback/private networking. For the small lab, embedded H2 and a file blob store are acceptable teaching assumptions; they are not the production recommendation. Do not run Nexus in an H2 container, because current Sonatype requirements do not support container-based H2 deployments.
Lab names: learner-ch21-build and
learner-ch21-release (Maven 2 hosted, release policy),
coordinate com.example:learner-ch21:1.0.0, service user
svc-learner-ch21, receiver 127.0.0.1:9001.
2. Preflight: prove target and compatibility
set -euo pipefail
export NEXUS_URL='http://127.0.0.1:8081'
curl --fail --silent --show-error "$NEXUS_URL/service/rest/v1/status"
curl --fail --silent --show-error "$NEXUS_URL/service/rest/v1/repositories" > /tmp/ch21-repositories-before.json
mvn --version
java -version
# Inspect Capabilities in the UI for "Webhook: Repository"; do not assume availability.
Stop if the target is not the disposable instance, if the repository names collide with pre-existing resources, or if the Maven client is using a global corporate mirror/settings file you did not intend to modify.
3. Build once and freeze the evidence
Create a tiny Java project under a temporary directory. Its only
purpose is to produce harmless bytes. Pin 1.0.0; do not
use SNAPSHOT or a mutable alias for the release
exercise.
4.0.0
com.example
learner-ch21
1.0.0
17
mvn -B clean package
ARTIFACT='target/learner-ch21-1.0.0.jar'
sha256sum "$ARTIFACT" | tee /tmp/ch21-build.sha256
# From this point forward, promotion stages must not invoke package/compile again.
4. Provision two narrowly-scoped hosted repositories
Reuse the documented REST reconciliation pattern from Chapter 20 or create them in the UI. Both are Maven 2 hosted repositories with release version policy. Keep them on a disposable file blob store. The build repository accepts the initial CI publication; the release repository represents the next delivery stage.
Create only the privileges the service identity needs to
add/browse/read the two lab repositories. Do not grant repository
creation, security administration, task administration, or
nx-admin.
5. Generate a temporary Maven credential context
The POM may identify the deployment target, but credentials stay in
a temporary settings file created by the runner or lab shell. In
real CI, inject the secret from the provider's credential store. For
this disposable lab use a password you created only for
svc-learner-ch21; do not reuse an administrator
password.
learner-ch21-build
svc-learner-ch21
password-FAKE_DO_NOT_USE
Set restrictive filesystem permissions on the real temporary file and delete it at cleanup. The literal fake password above is intentionally unusable and is safe for course text.
6. Publish the exact build output
Add distributionManagement with ID
learner-ch21-build and the hosted URL, then deploy with
the temporary settings. A successful deploy should add
component/database metadata plus POM/JAR/checksum assets in the blob
store.
mvn -B -s "$CH21_SETTINGS" deploy -DskipTests
# Immediately re-query the component/assets and download the JAR from Nexus.
curl --fail --silent --show-error -u "$NEXUS_SERVICE_USER:$NEXUS_CH21_PASSWORD" -o /tmp/ch21-from-build.jar "$NEXUS_URL/repository/learner-ch21-build/com/example/learner-ch21/1.0.0/learner-ch21-1.0.0.jar"
sha256sum /tmp/ch21-from-build.jar
The downloaded digest must equal
/tmp/ch21-build.sha256. If it does not, stop before any
webhook-driven action or promotion.
7. Start a disposable signed webhook receiver
Use this receiver only on loopback. It validates the raw request body with HMAC-SHA1, rejects invalid signatures, records the delivery UUID, and suppresses duplicates. Replace the fake secret only in the disposable process environment, not in this file.
# receiver.py -- minimal lab receiver, Python standard library only
from http.server import BaseHTTPRequestHandler, HTTPServer
import hashlib, hmac, json, os
SECRET = os.environ["CH21_WEBHOOK_SECRET"].encode()
seen = set()
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
raw = self.rfile.read(int(self.headers.get("Content-Length", "0")))
supplied = self.headers.get("X-Nexus-Webhook-Signature", "")
expected = hmac.new(SECRET, raw, hashlib.sha1).hexdigest()
if not hmac.compare_digest(expected, supplied):
self.send_response(401); self.end_headers(); return
delivery = self.headers.get("X-Nexus-Webhook-Delivery", "")
event = self.headers.get("X-Nexus-Webhook-Id", "")
if not delivery:
self.send_response(400); self.end_headers(); return
duplicate = delivery in seen
seen.add(delivery)
print(json.dumps({"delivery": delivery, "event": event,
"duplicate": duplicate, "bytes": len(raw)}))
self.send_response(200); self.end_headers()
HTTPServer(("127.0.0.1", 9001), Handler).serve_forever()
8. Configure or simulate the repository event
If the pinned instance exposes Webhook: Repository, create
a disposable capability targeting learner-ch21-build,
select only the relevant asset/component event types, set
http://127.0.0.1:9001/, and configure the disposable
shared secret. Publish a second harmless test asset only if needed
to create a fresh event.
If the capability is absent or edition/version behavior differs, do not change edition assumptions. Instead save a synthetic JSON body and POST it to the receiver with the same headers and an HMAC calculated from the raw bytes. This still teaches validation, event routing, and idempotence without requiring a paid capability.
body='{"repositoryName":"learner-ch21-build","action":"CREATED","name":"learner-ch21-1.0.0.jar"}'
export CH21_WEBHOOK_SECRET='secret-FAKE_DO_NOT_USE'
sig=$(BODY="$body" python -c 'import hashlib,hmac,os; print(hmac.new(os.environ["CH21_WEBHOOK_SECRET"].encode(), os.environ["BODY"].encode(), hashlib.sha1).hexdigest())')
curl --fail --silent --show-error -X POST http://127.0.0.1:9001/ -H 'Content-Type: application/json' -H 'X-Nexus-Webhook-Id: rm:repository:asset' -H 'X-Nexus-Webhook-Delivery: 00000000-0000-4000-8000-000000000021' -H "X-Nexus-Webhook-Signature: $sig" --data-binary "$body"
9. Community path: promote without rebuilding
Promotion begins with the repository copy, not the source tree.
Download the existing JAR and POM from
learner-ch21-build, compare the JAR digest to the
original build evidence, then deploy those existing files to
learner-ch21-release. For a richer Maven repository,
deploy the POM together with the JAR; do not rerun compilation.
EXPECTED=$(cut -d' ' -f1 /tmp/ch21-build.sha256)
ACTUAL=$(sha256sum /tmp/ch21-from-build.jar | cut -d' ' -f1)
test "$EXPECTED" = "$ACTUAL"
# Use Maven Deploy Plugin's deploy-file goal with the existing artifact/POM.
# Pin/verify the plugin version in your current Maven environment before production use.
mvn -B -s "$CH21_RELEASE_SETTINGS" deploy:deploy-file -DrepositoryId=learner-ch21-release -Durl="$NEXUS_URL/repository/learner-ch21-release/" -Dfile=/tmp/ch21-from-build.jar -DpomFile=/tmp/ch21-from-build.pom
curl --fail --silent --show-error -u "$NEXUS_SERVICE_USER:$NEXUS_CH21_PASSWORD" -o /tmp/ch21-from-release.jar "$NEXUS_URL/repository/learner-ch21-release/com/example/learner-ch21/1.0.0/learner-ch21-1.0.0.jar"
test "$EXPECTED" = "$(sha256sum /tmp/ch21-from-release.jar | cut -d' ' -f1)"
10. Optional Jenkins mapping
A Jenkins pipeline would map the same stages to credentials binding,
mvn deploy, webhook/event reception or polling,
checksum verification, and release publication. The Sonatype Jenkins
publisher supports Maven 2 release publication; current Sonatype
docs direct snapshot builds to the Maven Deploy Plugin. Keep this
optional so the lesson does not become a Jenkins course.
11. Challenge: choose the correct repository control
A team asks you to “promote build 812” but can provide only the Git commit and Jenkins job URL. What evidence is missing? Require them to identify the exact repository coordinate/version and byte checksum from the original publication before any release-stage action. Then decide whether the Community copy pattern or a Pro Staging move is appropriate.
12. Verification and cleanup
- Verify build JAR, build-repository JAR, and release-repository JAR have identical SHA-256.
- Verify the service user cannot create repositories or change security configuration.
- Verify duplicate webhook delivery UUID produces no second downstream action.
- Export redacted component/asset lists and receiver evidence.
- Delete/disable only the Chapter 21 webhook capability, service user/role, and the two disposable repositories. Use supported Nexus mechanisms; never remove blob files/database rows directly.
- Delete temporary Maven settings and local artifact copies after preserving non-secret checksums.
13. Knowledge check
Why download the artifact from Nexus before promotion instead of using target/ from the source workspace?
The repository copy is the governed published artifact. Promotion should prove and move that exact published identity, independent of a mutable build workspace.
What should happen when the same webhook delivery UUID arrives twice?
The second delivery should be acknowledged safely but must not perform the downstream action again.
Why is a temporary Maven settings file preferable to putting a password in pom.xml?
Credentials are client/runner state and should not be distributed with project source. The POM can identify the repository while CI injects authentication separately.
A release repository JAR has the same filename but a different SHA-256. Did promotion succeed?
No. The byte identity changed; investigate before release.
Why is native Staging not mandatory here?
The current feature matrix marks Staging & Build Promotion as Pro, so the required path uses a Community-compatible byte-preserving handoff.
14. Summary and next step
You built the core workflow with evidence at every boundary: one build, scoped publication, signed/idempotent event handling, exact-byte promotion, and controlled cleanup. Lesson 3 turns these mechanics into deliberate integration choices.
Official references and version notes
- Sonatype: Webhooks — repository/global webhook purpose and capability model.
- Sonatype: Enabling a Repository Webhook Capability — repository-scoped event configuration and shared-secret HMAC behavior.
-
Sonatype: Secure Webhook Deliveries
— HMAC-SHA1 verification using
X-Nexus-Webhook-Signature. - Sonatype: Example Headers and Payloads — event ID and unique delivery UUID used for receiver routing/idempotence.
- Sonatype: Platform Plugin for Jenkins — Nexus Repository — optional Jenkins publishing integration and Maven release behavior.
- Sonatype: Staging — current Pro-only staging/build-promotion model.
- Sonatype: Nexus Repository Maven Plugin — Pro staging-oriented Maven integration and current Java requirements for plugin versions.
- Sonatype: Self-Hosted Feature Matrix — current Community versus Pro entitlement boundaries.
- Sonatype: System Requirements — Java 21, H2/PostgreSQL, storage, and deployment constraints.
-
Apache Maven: Security and Deployment Settings
—
distributionManagementtarget and matchingsettings.xmlserver ID/credentials. - Apache Maven: Settings Reference — server credentials and client-local configuration boundary.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.