Chapter 32Lesson 02~210 minutes

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.

Hands-onLocal repositoryPublishVerifyPromoteMetadata

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 latest coordinate.
  • 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.

Next

Choose the integration design

Lesson 3 compares Jenkins archives, external repositories, snapshot/release policy, promotion patterns and plugin versus CLI/API integration.

Knowledge check

Answer before revealing the explanation.

1. Why use a unique build-derived version in the lab?

2. What proves promotion did not rebuild?

3. Why hash the repository copy?

4. Can the Nexus raw PUT example be copied unchanged for every repository format?

Official references and version notes

Repository formats, Jenkins plugins, credentials models and promotion APIs evolve; prefer current primary documentation.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.