Chapter 14Lesson 02~145 minutes

Artifacts, stash/unstash, archiveArtifacts, Fingerprints, Test Reports, Coverage, and Build Evidence: Guided Hands-On Workflow and Core Operations

Create a synthetic artifact on one disposable agent, transfer it to another with stash/unstash, archive and fingerprint it, ingest JUnit evidence, optionally publish coverage, then delete workspaces and verify retained build evidence.

Hands-onTwo agentsarchiveArtifactsJUnitSHA-256Cleanup

Learning objectives

  • Produce a synthetic artifact and checksum on a disposable agent while recording source/build identity.
  • Transfer the output between two workspaces with stash/unstash and verify byte identity after transfer.
  • Archive the original artifact with fingerprinting and publish a typed JUnit report.
  • Optionally ingest a synthetic Cobertura report using the maintained Coverage plugin without making it a mandatory dependency.
  • Delete workspace state and verify that archived artifacts and parsed reports remain attached to the build.

1. Lab boundary and preflight

This lab is local, disposable, and intentionally synthetic. Reuse the disposable controller and agents from earlier chapters or create equivalent lab-only resources. Do not point the job at a production repository, artifact repository, or credentials store. Two labels—lab-linux-a and lab-linux-b—make the workspace boundary visible. They may be two disposable agents, or one disposable machine exposed as two independently configured Jenkins agents.

Plugin mutation guard: JUnit is required for the mandatory path. If it is absent, install the pinned JUnit plugin only on the disposable controller after recording the existing plugin baseline. Coverage is optional; do not add it to a shared controller merely to complete this chapter.
# Run read-only commands on each disposable agent before the lab.
uname -a
java -version
git --version
sha256sum --version | head -n 1

Also record Jenkins core/Java, Pipeline: Basic Steps, Pipeline: Job, JUnit, and optional Coverage plugin versions from Manage Jenkins → Plugins. The goal is to explain the environment that produced the evidence, not to assume “Jenkins latest.” Record the exact source SHA with the build identity.

2. Create a synthetic source repository

Create a small local or public disposable repository named jenkins-ch14-evidence-lab. It needs only a Jenkinsfile and a tiny source marker. Commit before running so the Pipeline can record an immutable revision.

mkdir jenkins-ch14-evidence-lab && cd jenkins-ch14-evidence-lab
git init
git config user.name "Jenkins Lab"
git config user.email "jenkins-lab@example.invalid"
mkdir -p src
echo 'chapter14 synthetic payload' > src/input.txt
git add src/input.txt
git commit -m "chapter14: seed evidence lab"
git rev-parse HEAD

Use a repository transport already established in Chapter 06. If the repository is private, use a lab-only credential scoped narrowly to this repository. Never embed a token in the Jenkinsfile or URL.

3. Build once; distinguish transfer from retention

Create an SCM-backed Pipeline named labs/ch14-evidence. The following Jenkinsfile intentionally uses Scripted node blocks because it makes the two workspaces visually explicit. The first node produces the artifact, JUnit XML, optional Cobertura XML, and a SHA-256 manifest. It then both stashes the package for same-run transfer and archives the release evidence for retention.

def sourceSha = ''

def buildLabel = "${env.JOB_NAME}#${env.BUILD_NUMBER}"

stage('Produce on A') {
  node('lab-linux-a') {
    deleteDir()
    checkout scm
    sourceSha = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()

    sh '''
      set -eu
      mkdir -p out reports src
      printf 'source=%s\\nbuild=%s\\n' "$GIT_COMMIT" "$BUILD_NUMBER" > out/payload.txt
      tar -czf out/ch14-payload.tgz out/payload.txt
      sha256sum out/ch14-payload.tgz > out/SHA256SUMS

      cat > reports/junit.xml <<'XML'
<testsuite name="chapter14" tests="2" failures="0" errors="0" skipped="0" time="0.01">
  <testcase classname="EvidenceLab" name="artifact_created" time="0.005"/>
  <testcase classname="EvidenceLab" name="digest_created" time="0.005"/>
</testsuite>
XML

      cat > src/Demo.java <<'JAVA'
class Demo { int answer() { return 42; } }
JAVA
      cat > reports/coverage.xml <<'XML'
<coverage line-rate="0.75" branch-rate="1.0" lines-covered="3" lines-valid="4"
          branches-covered="0" branches-valid="0" complexity="0" version="synthetic" timestamp="0">
  <sources><source>.</source></sources>
  <packages><package name="demo" line-rate="0.75" branch-rate="1.0" complexity="0">
    <classes><class name="Demo" filename="src/Demo.java" line-rate="0.75" branch-rate="1.0" complexity="0">
      <methods/><lines>
        <line number="1" hits="1" branch="false"/>
        <line number="2" hits="1" branch="false"/>
        <line number="3" hits="1" branch="false"/>
        <line number="4" hits="0" branch="false"/>
      </lines>
    </class></classes>
  </package></packages>
</coverage>
XML
    '''

    echo "producer=${buildLabel} sha=${sourceSha} node=${env.NODE_NAME} workspace=${env.WORKSPACE}"
    stash name: 'handoff', includes: 'out/ch14-payload.tgz,out/SHA256SUMS'
    archiveArtifacts artifacts: 'out/ch14-payload.tgz,out/SHA256SUMS', fingerprint: true
    junit testResults: 'reports/junit.xml', allowEmptyResults: false
  }
}

stage('Verify on B') {
  node('lab-linux-b') {
    deleteDir()
    unstash 'handoff'
    sh 'sha256sum -c out/SHA256SUMS'
    echo "consumer=${buildLabel} node=${env.NODE_NAME} workspace=${env.WORKSPACE}"
    deleteDir()
  }
}

stage('Final evidence') {
  echo "job=${env.JOB_NAME} build=${env.BUILD_NUMBER} url=${env.BUILD_URL} source=${sourceSha}"
}

Why each action exists: stash proves same-run transfer; archiveArtifacts creates retained build-owned files; fingerprint: true creates relationship metadata; junit parses typed test evidence; deleteDir() on the consumer proves the final verification cannot depend on leftover workspace state.

4. Optional maintained coverage extension

If the disposable controller has Coverage 3.3358.v9487dde48783, add this immediately after the JUnit step while the report and source file still exist in workspace:

recordCoverage(
  tools: [[parser: 'COBERTURA', pattern: 'reports/coverage.xml']],
  id: 'synthetic-coverage',
  name: 'Synthetic Coverage',
  sourceCodeRetention: 'NEVER',
  skipPublishingChecks: true,
  failOnError: true
)

This is intentionally local. skipPublishingChecks: true prevents the lesson from needing an SCM checks integration, and sourceCodeRetention: 'NEVER' keeps the example from copying source into retained coverage data. The report remains synthetic teaching evidence rather than a quality claim about real code.

5. Expected observations

Observation Layer Expected evidence
Artifact created on A Agent workspace out/ch14-payload.tgz + SHA-256 manifest
Stash created Pipeline transfer state handoff can be unstashed in the same run
Artifact archived Build retention Artifact visible from build record after workspace cleanup
Fingerprint recorded Jenkins relationship database Fingerprint page associates file with producer build
JUnit ingested Typed report state 2 tests, 0 failures associated with this build
Coverage ingested (optional) Typed report/plugin state Coverage result attached to same build
Consumer workspace deleted Agent filesystem Workspace file gone; retained build evidence still available

6. Verify retained evidence after workspace cleanup

After the build finishes, use the Jenkins UI or authenticated Remote API to inspect the build. Do not put API tokens in URLs or console output. A browser session is sufficient for the mandatory path.

  • Build page: record build number, URL, result, duration, and cause.
  • Artifacts: download ch14-payload.tgz and SHA256SUMS; verify checksum locally.
  • Test Result: verify 2 tests, 0 failures.
  • Fingerprints: follow the artifact fingerprint and record its producer relationship.
  • Coverage (optional): record parser ID, metric summary, and plugin version.
  • Agent workspaces: confirm the consumer workspace was deleted and do not use workspace persistence as evidence.

7. Small challenge: pick the correct layer

You need the same package in a later stage of the same run, and you also need auditors to retrieve it three days later after both agents are destroyed. Which mechanisms do you use?

Expected reasoning: stash/unstash solves same-run transfer; archiveArtifacts (or an external repository) solves retention. A fingerprint can add producer/consumer relationship tracking, but neither stash nor fingerprint alone is a retained release store.

8. Cleanup

Download the evidence packet first. Then delete only labs/ch14-evidence, the disposable source repository, and Chapter 14 lab agents if they were created specifically for this exercise. Do not remove shared plugins or unrelated agents. Build-history deletion is intentionally not part of the lab because the retained build record is the evidence under study.

Next lesson

Configuration, Design Choices, and Tradeoffs

Choose among stash, Jenkins artifacts, external repositories, fingerprints, raw reports, typed visualization, and retention policies without confusing their ownership or guarantees.

Knowledge check

Why does the lab both stash and archive the same package?

What proves the bytes on agent B match agent A?

Why is Coverage optional in this lab?

What should remain after deleteDir() on agent B?

Which layer is wrong if JUnit XML exists but the Jenkins build shows no test result?

Official references and version notes

Version and compatibility note

Rechecked on 2026-09-16. Examples assume Jenkins 2.568.3 LTS (tested with Java 21 and 25), Pipeline: Basic Steps 1098.v808b_fd7f8cf4, Pipeline: Job 1600.v6f36ed83529d, and JUnit 1425.v9c7318dca_96d. The optional coverage extension uses Coverage 3.3358.v9487dde48783, which requires Jenkins 2.555.3 or newer and is therefore compatible with this LTS baseline. The mandatory path is free/local/disposable. Record the versions actually installed on your controller before applying the examples.

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.