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.
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.
# 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.tgzandSHA256SUMS; 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?
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.
Knowledge check
Why does the lab both stash and archive the same package?
They demonstrate different lifecycles: stash transfers within the run; archiveArtifacts retains bytes with the build.
What proves the bytes on agent B match agent A?
The SHA-256 manifest created on A and verified after unstash on B.
Why is Coverage optional in this lab?
It is a maintained plugin extension, while the mandatory evidence model can be learned with core artifact/fingerprint behavior plus JUnit. This avoids unnecessary plugin mutation on shared controllers.
What should remain after deleteDir() on agent
B?
The archived artifact, fingerprint record, parsed JUnit report, and optional coverage result remain build evidence; the consumer workspace files do not.
Which layer is wrong if JUnit XML exists but the Jenkins build shows no test result?
The report-ingestion layer. Preserve the XML and step log, then inspect the junit path/pattern and plugin execution rather than rebuilding the artifact.
Official references and version notes
-
Pipeline: Basic Steps reference
— current
stash/unstashsemantics and guidance for cross-stage transfer. -
Running Pipelines
— Declarative stage restart and
preserveStashesbehavior. -
Recording tests and artifacts
—
archiveArtifacts, fingerprinting, and JUnit publishing patterns. - Jenkins fingerprints — dependency/use tracking and the MD5-based fingerprint record.
- JUnit plugin — maintained test-result ingestion and build/test history.
- Coverage plugin — maintained coverage ingestion, quality gates, and source-retention choices.
-
Coverage Pipeline step reference
— current
recordCoverageparameters and supported parsers. - Artifact Manager on S3 plugin — optional example of external artifact-manager integration; not required by the labs.
- Jenkins LTS changelog — current LTS and tested Java configurations.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.