Artifact Repositories, Nexus/Artifactory Integration, Package Promotion, Build Metadata, and Release Traceability: Guided Hands-On Workflow and Core Operations
Publish a synthetic candidate under unique coordinates, verify its repository digest, promote the exact same bytes, and retain metadata that traces both locations to Jenkins and source.
Learning objectives
- Create a unique candidate coordinate from Jenkins build identity.
- Generate SHA-256 and build metadata before publication.
- Verify repository bytes independently from workspace bytes.
- Promote without rebuilding and prove digest continuity.
- Map the local simulation to optional Nexus/Artifactory integrations.
1. Preflight: disposable resources only
Use a synthetic Git repository and a disposable Jenkins agent
labelled repo-lab. The “repository” is a local
directory with candidates/ and releases/.
This models identity and promotion without requiring a server,
plugin, paid service or production credentials.
- Do not run routine builds on the built-in node/controller.
- Do not use proprietary packages or production repository credentials.
- Do not publish to a mutable
latestcoordinate. - Do not delete an existing release just to make a rerun pass.
2. Build a unique synthetic candidate
set -euo pipefail
mkdir -p dist evidence lab-repo/candidates lab-repo/releases
SOURCE_SHA="$(git rev-parse HEAD 2>/dev/null || printf '0000000000000000000000000000000000000000')"
SHORT_SHA="$(printf '%s' "$SOURCE_SHA" | cut -c1-8)"
VERSION="1.0.${BUILD_NUMBER:-0}-${SHORT_SHA}"
NAME="widget"
ARTIFACT="dist/${NAME}-${VERSION}.txt"
printf 'name=%s\nversion=%s\nsource=%s\nbuild=%s\n' "$NAME" "$VERSION" "$SOURCE_SHA" "${BUILD_NUMBER:-0}" > "$ARTIFACT"
sha256sum "$ARTIFACT" | tee evidence/artifact.sha256
printf '%s\n' "$VERSION" > evidence/version.txt
printf '%s\n' "$SOURCE_SHA" > evidence/source-sha.txt
The unique version makes a rerun create a different candidate instead of mutating an earlier release coordinate.
3. Write non-secret build metadata
set -euo pipefail
ARTIFACT="$(find dist -maxdepth 1 -type f -name 'widget-*.txt' -print -quit)"
DIGEST="$(sha256sum "$ARTIFACT" | awk '{print $1}')"
python3 -c 'import json,os,sys,pathlib; p,d,s=sys.argv[1:]; pathlib.Path("evidence/build-metadata.json").write_text(json.dumps({"schema":"devops-academy.repo-metadata/v1","artifact":pathlib.Path(p).name,"sha256":d,"job":os.getenv("JOB_NAME","local-lab"),"buildNumber":os.getenv("BUILD_NUMBER","0"),"buildUrl":os.getenv("BUILD_URL","local://build/0"),"sourceSha":s,"node":os.getenv("NODE_NAME","local")},indent=2)+"\\n")' "$ARTIFACT" "$DIGEST" "$(cat evidence/source-sha.txt)"
cat evidence/build-metadata.json
Metadata says which build should own the bytes. The digest independently identifies the bytes. Keep both.
4. Publish under unique candidate coordinates
set -euo pipefail
VERSION="$(cat evidence/version.txt)"
ARTIFACT="$(find dist -maxdepth 1 -type f -name 'widget-*.txt' -print -quit)"
CANDIDATE_DIR="lab-repo/candidates/com/example/widget/$VERSION"
test ! -e "$CANDIDATE_DIR" || { echo "Refusing overwrite: $CANDIDATE_DIR" >&2; exit 23; }
mkdir -p "$CANDIDATE_DIR"
cp "$ARTIFACT" evidence/build-metadata.json evidence/artifact.sha256 "$CANDIDATE_DIR/"
printf '%s\n' "$CANDIDATE_DIR" > evidence/candidate-coordinate.txt
find "$CANDIDATE_DIR" -maxdepth 1 -type f -printf '%f\n' | sort
The no-overwrite check models a repository deployment policy. Publication is a state mutation; preserve the coordinate and response evidence.
5. Retrieve from repository state and verify
set -euo pipefail
EXPECTED="$(awk '{print $1}' evidence/artifact.sha256)"
CANDIDATE_DIR="$(cat evidence/candidate-coordinate.txt)"
CANDIDATE_FILE="$(find "$CANDIDATE_DIR" -maxdepth 1 -type f -name 'widget-*.txt' -print -quit)"
ACTUAL="$(sha256sum "$CANDIDATE_FILE" | awk '{print $1}')"
printf 'expected=%s\nrepository=%s\n' "$EXPECTED" "$ACTUAL" | tee evidence/candidate-verification.txt
test "$EXPECTED" = "$ACTUAL"
This checks repository state, not just the workspace. In a real Nexus/Artifactory lab, perform a GET/download by exact coordinates and hash the retrieved file.
6. Promote the same bytes
set -euo pipefail
VERSION="$(cat evidence/version.txt)"
CANDIDATE_DIR="$(cat evidence/candidate-coordinate.txt)"
RELEASE_DIR="lab-repo/releases/com/example/widget/$VERSION"
test ! -e "$RELEASE_DIR" || { echo "Refusing release overwrite: $RELEASE_DIR" >&2; exit 24; }
mkdir -p "$RELEASE_DIR"
cp "$CANDIDATE_DIR"/* "$RELEASE_DIR/"
CANDIDATE_FILE="$(find "$CANDIDATE_DIR" -maxdepth 1 -type f -name 'widget-*.txt' -print -quit)"
RELEASE_FILE="$(find "$RELEASE_DIR" -maxdepth 1 -type f -name 'widget-*.txt' -print -quit)"
CANDIDATE_SHA="$(sha256sum "$CANDIDATE_FILE" | awk '{print $1}')"
RELEASE_SHA="$(sha256sum "$RELEASE_FILE" | awk '{print $1}')"
printf 'candidate=%s\nrelease=%s\n' "$CANDIDATE_SHA" "$RELEASE_SHA" | tee evidence/promotion-verification.txt
test "$CANDIDATE_SHA" = "$RELEASE_SHA"
printf '%s\n' "$RELEASE_DIR" > evidence/release-coordinate.txt
No compile or package command runs during promotion. The repository coordinate changes; the digest does not.
7. Orchestrate it with Jenkins
pipeline {
agent { label 'repo-lab' }
options { timestamps(); disableConcurrentBuilds() }
stages {
stage('Build candidate') { steps { sh './ci/build-candidate.sh' } }
stage('Publish candidate') { steps { sh './ci/publish-candidate.sh' } }
stage('Verify candidate') { steps { sh './ci/verify-candidate.sh' } }
stage('Promote same bytes') { steps { sh './ci/promote-candidate.sh' } }
stage('Verify release') { steps { sh './ci/verify-release.sh' } }
}
post {
always { archiveArtifacts artifacts: 'evidence/**', fingerprint: true, allowEmptyArchive: true }
}
}
Keeping shell logic in reviewed scripts makes it testable. In production, promotion is often better as a separate trusted job so ordinary application code cannot self-promote.
8. Optional Nexus mapping
Nexus documents HTTP PUT for hosted raw repositories. Bind a narrowly scoped credential and never echo it.
curl --fail --silent --show-error \
--user "$NEXUS_USER:$NEXUS_PASS" \
--upload-file "$ARTIFACT" \
"$NEXUS_URL/repository/lab-raw/com/example/widget/$VERSION/$(basename "$ARTIFACT")"
Do not apply raw-repository commands blindly to Maven/npm/PyPI: format-aware repositories have their own metadata and APIs.
9. Optional Artifactory/JFrog mapping
The JFrog Jenkins plugin wraps JFrog CLI and can publish Build-Info. Artifactory promotion can use a build name/number plus target repository. Keep those identities mapped to Jenkins job/build and independently verify the artifact digest across the promotion boundary.
10. Small challenge
A rerun finds that the intended release coordinate already exists. The correct action is to stop, preserve the existing digest/metadata and decide whether a new unique version is required. Deleting or overwriting the prior release destroys traceability.
Knowledge check
Answer before revealing the explanation.
1. Why use a unique build-derived version in the lab?
It prevents reruns from silently overwriting an earlier coordinate and ties each object to one source/build identity.
2. What proves promotion did not rebuild?
Candidate and release digests are equal and the promotion path contains only copy/move/lifecycle operations.
3. Why hash the repository copy?
It independently verifies external state rather than assuming the upload selected and stored the intended bytes.
4. Can the Nexus raw PUT example be copied unchanged for every repository format?
No. Use format-specific clients/APIs and semantics for Maven, npm, PyPI and other repository types.
Official references and version notes
Repository formats, Jenkins plugins, credentials models and promotion APIs evolve; prefer current primary documentation.
- Jenkins LTS changelog
- Jenkins Java Support Policy
- Jenkins Security Advisories
- Nexus Artifact Uploader plugin
- JFrog Jenkins plugin
- Sonatype Nexus Repository — Raw repositories
- Sonatype Nexus Repository — Components API
- Sonatype Nexus Repository — Uploading components
- JFrog Artifactory — Build-Info and build promotion
- JFrog — Jenkins integration
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.