Hands-On: Build Artifacts, Semantic Versioning, and Package Management

A one-hour session that turns the Python and Java pipelines from the earlier sessions into a release process. You will version the code, build each artifact, verify it, publish it through GitHub Actions and Jenkins, and consume it the way a downstream team would. Docker is not part of this session.

Contents
  1. Session plan and prerequisites
  2. Part 1: Concepts (5 min)
  3. Part 2: Versioning one release (10 min)
  4. Part 3: Build the artifacts locally (10 min)
  5. Part 4: Checksums and inspection (6 min)
  6. Part 5: Release pipelines on GitHub (12 min)
  7. Part 6: Artifacts in Jenkins (6 min)
  8. Part 7: Consuming and pinning (8 min)
  9. Part 8: Recap, quiz, cleanup, troubleshooting (3 min)

Session plan and prerequisites

TimePartWhat you finish
0:00 to 0:051. ConceptsArtifact types and SemVer vocabulary
0:05 to 0:152. VersioningAll three projects at 1.1.0, version check script passing and failing as expected, annotated tag
0:15 to 0:253. Local buildsPython wheel and sdist, npm tarball, Java jar installed to the local Maven repository
0:25 to 0:314. VerificationChecksums, a tamper test, and package contents checked
0:31 to 0:435. GitHub releaseRelease created by the tag, assets downloaded and verified
0:43 to 0:496. JenkinsFingerprinted artifacts on the build page
0:49 to 0:577. ConsumingA consumer project installing 1.1.0 from each artifact type
0:57 to 1:008. RecapQuiz and cleanup

Prerequisites

Confirm each item before starting. The check command is shown for each.

RequirementCheckNeeded for
Python 3.11 or laterpython3 --versionParts 2 to 4, 7
Node.js 20 and npmnode --version and npm --versionParts 2 to 4, 7
JDK 17 and Maven 3.9java -version and mvn -versionParts 2 to 5, 7
Git with push accessgit remote -vParts 2, 5, 6
python-ci-demo repositoryPipeline is green on mainAll parts
java-ci-demo repositoryPipeline is green on mainAll parts
Jenkins (from the earlier session) running at http://localhost:8080Open the URL in a browserPart 6
Working folders. Commands assume the two repositories sit side by side: ~/work/python-ci-demo and ~/work/java-ci-demo. Adjust the paths if yours are elsewhere. Every step tells you which folder to be in.

Part 1: Concepts 5 min

Step 1.1 Read the artifact types

Goal: know what each artifact is and which tool produces it.

ArtifactWhat it isExample in this sessionProduced by
Python wheelInstallable, pre-built Python packagecalc_ci_demo-1.1.0-py3-none-any.whlpython -m build
Python sdistSource distribution, rebuildable by otherscalc_ci_demo-1.1.0.tar.gzpython -m build
npm packageJavaScript library packed as a tarballcalc-ci-demo-js-1.1.0.tgznpm pack
JARCompiled Java library or applicationcalc-ci-demo-1.1.0.jarmvn package
BinaryCompiled executable (Go, Rust, C++)Not built in this sessiongo build, cargo build --release
Helm chartKubernetes deployment packageNot built in this sessionhelm package
Container images are covered in a separate session and are not part of this one.

Step 1.2 Learn the SemVer rules

Goal: choose the correct version number for a change.

Step 1.3 Classify changes

Goal: practise the rules on realistic changes. Write your answer, then check it.

  1. Current version 2.4.1. A typo in an error message is fixed.
  2. Current version 2.4.1. An optional timeout parameter is added.
  3. Current version 2.4.1. A public function is renamed, and the old name is removed.
  4. Current version 2.5.0-rc.1. A bug is found in the release candidate and fixed.
Answers
  1. 2.4.2 (PATCH).
  2. 2.5.0 (MINOR), because the parameter is optional and old calls still work.
  3. 3.0.0 (MAJOR), because callers using the old name break.
  4. 2.5.0-rc.2. A fix to a pre-release gets a new pre-release number. The stable version is still 2.5.0.

Part 2: Versioning one release 10 min

All three projects must carry version 1.1.0 before you tag the release. This part sets the versions, adds a check, and creates the tag.

Step 2.1 Add packaging metadata to the Python project

Goal: give the Python project a version and tell the build tool how to package it.

Do

Go to the Python repository:

cd ~/work/python-ci-demo
git checkout main
git pull

Create pyproject.toml in the root of the repository:

pyproject.toml
[build-system]
requires = ["setuptools>=69", "wheel"]
build-backend = "setuptools.build_meta"

[project]
name = "calc-ci-demo"
version = "1.1.0"
description = "Calculator demo used for CI and artifact lessons"
requires-python = ">=3.11"

[tool.setuptools.packages.find]
include = ["app*"]

Explanation

Verify

python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])"

Expected output: 1.1.0.

Step 2.2 Create the npm package

Goal: add a small JavaScript package that becomes the second artifact.

Do

Still in python-ci-demo, create the npm folder and its three files.

mkdir -p npm
npm/package.json
{
  "name": "calc-ci-demo-js",
  "version": "1.1.0",
  "description": "Calculator helpers packaged as an npm artifact",
  "main": "index.js",
  "files": ["index.js", "README.md"],
  "license": "MIT",
  "private": true
}
npm/index.js
function add(a, b) {
  return a + b;
}

function divide(a, b) {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}

module.exports = { add, divide };
npm/README.md
# calc-ci-demo-js

Calculator helpers. Exports add and divide.

Explanation

Verify

cd npm
node -p "require('./package.json').version"
cd ..

Expected output: 1.1.0.

Step 2.3 Set the Java version

Goal: change the Maven project version to 1.1.0.

Do

Go to the Java repository and open pom.xml:

cd ~/work/java-ci-demo
git checkout main
git pull
grep -n "calc-ci-demo" -A 1 pom.xml | head -5

Find the <version> line that follows <artifactId>calc-ci-demo</artifactId>. Change its value from 1.0.0 to 1.1.0:

<artifactId>calc-ci-demo</artifactId>
<version>1.1.0</version>

Explanation

Verify

mvn -B -q help:evaluate -Dexpression=project.version -DforceStdout

Expected output: 1.1.0. The help:evaluate goal reads the version Maven resolves, so this confirms the change took effect.

mvn -B verify

Expected output: BUILD SUCCESS. The tests must still pass after the change.

Step 2.4 Create the version check script

Goal: have the pipeline fail when the tag and the files disagree.

Do

Go back to the Python repository and create the script:

cd ~/work/python-ci-demo
mkdir -p scripts
scripts/check_version.sh
#!/usr/bin/env bash
set -euo pipefail

TAG="${1:?usage: check_version.sh vX.Y.Z}"
VERSION="${TAG#v}"

PY_VERSION=$(python3 -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])")
NPM_VERSION=$(node -p "require('./npm/package.json').version")

[ "$PY_VERSION" = "$VERSION" ] || { echo "FAIL pyproject.toml is $PY_VERSION, tag is $VERSION"; exit 1; }
[ "$NPM_VERSION" = "$VERSION" ] || { echo "FAIL npm/package.json is $NPM_VERSION, tag is $VERSION"; exit 1; }

echo "OK: all versions match $VERSION"
chmod +x scripts/check_version.sh

Explanation

Step 2.5 Run the script in both directions

Goal: see the script pass for the right tag and fail for a wrong one.

Do

(a) Correct tag:

./scripts/check_version.sh v1.1.0
echo "exit code: $?"

Expected output: OK: all versions match 1.1.0 and exit code: 0.

(b) Wrong tag:

./scripts/check_version.sh v1.1.1
echo "exit code: $?"

Expected output: FAIL pyproject.toml is 1.1.0, tag is 1.1.1 and exit code: 1.

Explanation

Step 2.6 Commit the changes and tag the release

Goal: commit the versions and create an annotated tag. The tag starts the release workflow in Part 5.

Do

Python repository:

cd ~/work/python-ci-demo
git add pyproject.toml npm scripts
git commit -m "Release prep: 1.1.0 packaging, npm package, version check"
git push origin main
git tag -a v1.1.0 -m "Release 1.1.0"
git push origin v1.1.0

Java repository:

cd ~/work/java-ci-demo
git add pom.xml
git commit -m "Release prep: version 1.1.0"
git push origin main
git tag -a v1.1.0 -m "Release 1.1.0"
git push origin v1.1.0

Explanation

Verify

git tag --list "v1*"
git show v1.1.0 --stat | head -5
Checkpoint: both repositories show the tag v1.1.0. Pushing the tag will start the release workflows, which you add in Part 5. If Actions shows a run before you have added the workflow files, that is expected. Use the re-tag steps in Step 5.3 to run it again after adding them.

Part 3: Build the artifacts locally 10 min

Build each artifact on your machine first. The release workflow in Part 5 runs the same commands in a clean virtual machine.

Step 3.1 Build the Python wheel and sdist

Goal: produce the two Python artifacts in dist/.

Do

cd ~/work/python-ci-demo
source .venv/bin/activate          # Windows PowerShell: .venv\Scripts\Activate.ps1
pip install --upgrade build
rm -rf dist
python -m build
ls -l dist/

Expected output

The build ends with a line like this:

Successfully built calc_ci_demo-1.1.0.tar.gz and calc_ci_demo-1.1.0-py3-none-any.whl

ls shows both files in dist/.

Explanation

Step 3.2 Build the npm tarball

Goal: pack the JavaScript package into dist/.

Do

cd ~/work/python-ci-demo/npm
npm pack --pack-destination ../dist
cd ..
ls -l dist/

Expected output

npm pack prints a list of the files it included, then the tarball name, calc-ci-demo-js-1.1.0.tgz. The file also appears in dist/.

Explanation

Step 3.3 Build and install the Java jar

Goal: build the jar, then install it into your local Maven repository, where other projects can find it.

Do

cd ~/work/java-ci-demo
mvn -B clean verify
mvn -B -DskipTests package
ls -l target/calc-ci-demo-1.1.0.jar
mvn -B -DskipTests install
ls ~/.m2/repository/com/example/calc-ci-demo/1.1.0/

Expected output

Explanation

Step 3.4 Run the artifacts

Goal: confirm each artifact runs before you publish it.

Do

# Java jar
cd ~/work/java-ci-demo
java -jar target/calc-ci-demo-1.1.0.jar

# Python wheel, installed into a throwaway environment
python3 -m venv /tmp/check-wheel
/tmp/check-wheel/bin/pip install ~/work/python-ci-demo/dist/calc_ci_demo-1.1.0-py3-none-any.whl
/tmp/check-wheel/bin/python -c "from app.calculator import add, divide; print(add(2, 3), divide(10, 4))"

# npm tarball, installed into a throwaway folder
mkdir -p /tmp/check-npm && cd /tmp/check-npm
npm init -y >/dev/null
npm install ~/work/python-ci-demo/dist/calc-ci-demo-js-1.1.0.tgz
node -p "require('calc-ci-demo-js').add(2, 3)"
cd ~/work/python-ci-demo

Expected output

Explanation

Checkpoint: you have a wheel, an sdist, an npm tarball, and a jar, and each one runs in a clean environment.

Part 4: Checksums and inspection 6 min

Step 4.1 Create the checksum file

Goal: record a SHA-256 hash for every Python and npm artifact.

Do

cd ~/work/python-ci-demo/dist
sha256sum calc_ci_demo-1.1.0-py3-none-any.whl calc_ci_demo-1.1.0.tar.gz calc-ci-demo-js-1.1.0.tgz > SHA256SUMS.txt
cat SHA256SUMS.txt
sha256sum -c SHA256SUMS.txt
cd ~/work/python-ci-demo

Expected output

The cat shows three lines, each a 64-character hash followed by a file name. The -c check prints OK after each file name.

Explanation

Step 4.2 Run a tamper test

Goal: see the checksum catch a changed file.

Do

cp ~/work/python-ci-demo/dist/calc_ci_demo-1.1.0-py3-none-any.whl /tmp/tampered.whl
echo "# changed" >> /tmp/tampered.whl
cd /tmp
sha256sum -c <(grep calc_ci_demo-1.1.0-py3-none-any.whl ~/work/python-ci-demo/dist/SHA256SUMS.txt | sed 's|calc_ci_demo-1.1.0-py3-none-any.whl|/tmp/tampered.whl|')
cd ~/work/python-ci-demo

Expected output

The check prints /tmp/tampered.whl: FAILED and a warning that one computed checksum did not match. The original file still passes the check in Step 4.1.

Explanation

rm -f /tmp/tampered.whl

Step 4.3 Inspect the wheel contents

Goal: confirm the wheel contains the application and nothing else.

Do

unzip -l ~/work/python-ci-demo/dist/calc_ci_demo-1.1.0-py3-none-any.whl

Expected output

Explanation

Step 4.4 Inspect the npm tarball before building it

Do

cd ~/work/python-ci-demo/npm
npm pack --dry-run
cd ~/work/python-ci-demo

Expected output

The list contains package.json, index.js, and README.md. Nothing else appears.

Explanation

Step 4.5 Inspect the jar

unzip -l ~/work/java-ci-demo/target/calc-ci-demo-1.1.0.jar | head -20

Expected output: com/example/calc/ classes and META-INF/MANIFEST.MF. The manifest should name com.example.calc.App as the main class. You can check it with unzip -p ~/work/java-ci-demo/target/calc-ci-demo-1.1.0.jar META-INF/MANIFEST.MF.

Checkpoint: sha256sum -c reports OK for every file, the tamper test fails as expected, and the wheel, tarball, and jar contain only what they should.

Part 5: Release pipelines on GitHub 12 min

Each release workflow runs when a version tag is pushed. It rebuilds from the tagged commit, tests, checks the version, builds, and creates a GitHub Release with the files attached.

Step 5.1 Create the Python release workflow

Goal: automate the Python and npm artifacts and attach them to a GitHub Release.

Do

cd ~/work/python-ci-demo
mkdir -p .github/workflows
.github/workflows/release.yml
name: Release

on:
  push:
    tags: ["v*.*.*"]

permissions:
  contents: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout tagged commit
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt build

      - name: Test before publishing
        run: pytest

      - name: Check versions match tag
        run: ./scripts/check_version.sh "$GITHUB_REF_NAME"

      - name: Build wheel and sdist
        run: python -m build

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"

      - name: Pack npm tarball
        run: npm pack --pack-destination ../dist
        working-directory: npm

      - name: Write checksums
        working-directory: dist
        run: sha256sum * > SHA256SUMS.txt

      - name: Upload distribution
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

  release:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download distribution
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: dist/*
          generate_release_notes: true
          prerelease: ${{ contains(github.ref_name, '-') }}

Explanation: how the workflow is organised

BlockWhat it does and why
on.push.tags: ["v*.*.*"]Runs only for version tags. Pushing to main never publishes a release.
permissions: contents: writeLets the workflow create a release. The token is limited to this workflow.
Test before publishingThe tag might point at a commit that never passed CI on main. Re-running the tests ensures the published artifact was verified.
Check versions match tagRuns the script from Part 2. A mismatch stops the workflow before anything is published.
Build wheel and sdistBuilds the Python artifacts in a clean machine, from the tagged commit.
Pack npm tarballRuns npm pack in the npm folder and writes the tarball to dist. The working-directory key sets the folder for that step.
Write checksumsCreates SHA256SUMS.txt in dist. The file is attached to the release, so downloaders can verify the files.
upload-artifact and download-artifactPass the same files from the build job to the release job. The release contains the exact bytes that were tested.
softprops/action-gh-release@v2Creates the release for the tag and attaches every file in dist. generate_release_notes builds notes from merged pull requests.
prereleaseMarks a release as pre-release when the tag contains a hyphen, such as v1.2.0-rc.1. This follows the SemVer rule that a hyphen means pre-release.

Step 5.2 Create the Java release workflow

Goal: build the jar from the tagged commit and attach it to a release in the Java repository.

Do

cd ~/work/java-ci-demo
mkdir -p .github/workflows
.github/workflows/release.yml
name: Java Release

on:
  push:
    tags: ["v*.*.*"]

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout tagged commit
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "17"
          cache: maven

      - name: Check pom.xml version matches tag
        run: |
          POM_VERSION=$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' pom.xml | head -1)
          [ "$POM_VERSION" = "${GITHUB_REF_NAME#v}" ] || { echo "pom.xml is $POM_VERSION, tag is $GITHUB_REF_NAME"; exit 1; }

      - name: Verify
        run: mvn -B verify

      - name: Package jar
        run: mvn -B -DskipTests package

      - name: Write checksum
        working-directory: target
        run: sha256sum calc-ci-demo-*.jar > SHA256SUM-jar.txt

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          files: |
            target/calc-ci-demo-*.jar
            target/SHA256SUM-jar.txt
          generate_release_notes: true
          prerelease: ${{ contains(github.ref_name, '-') }}

Explanation

Step 5.3 Push the tag so both workflows run

Goal: start the release workflows for the tag from Part 2.

Do

The tag from Step 2.6 was pushed before the workflows existed, so it did not start a release. Commit the workflow files first, then move the tag to the new commit.

Python repository:

cd ~/work/python-ci-demo
git add .github/workflows/release.yml
git commit -m "Add release workflow"
git push origin main
git push --delete origin v1.1.0
git tag -d v1.1.0
git tag -a v1.1.0 -m "Release 1.1.0"
git push origin v1.1.0

Java repository:

cd ~/work/java-ci-demo
git add .github/workflows/release.yml pom.xml
git commit -m "Add release workflow"
git push origin main
git push --delete origin v1.1.0
git tag -d v1.1.0
git tag -a v1.1.0 -m "Release 1.1.0"
git push origin v1.1.0
Delete a tag only if no release was created from it. If a release already exists for v1.1.0, do not delete or move the tag. Publish 1.1.1 instead, because someone may already depend on the old artifacts.

Step 5.4 Watch the runs

Goal: follow each job and read what it did.

Do

  1. Open the python-ci-demo repository on GitHub and click Actions.
  2. Click the run named Release for the tag v1.1.0.
  3. Click the build job. Expand these steps and read the output:
    • Test before publishing: the pytest summary.
    • Check versions match tag: OK: all versions match 1.1.0.
    • Build wheel and sdist: the Successfully built line.
  4. Click the release job and confirm it finished green.
  5. Repeat steps 1 to 4 for the java-ci-demo repository and its Java Release run.

Explanation

Step 5.5 Verify the release page

Goal: confirm the release contains the expected assets.

Do

  1. Open the python-ci-demo repository and click Releases in the right-hand column.
  2. Open v1.1.0. Confirm these assets are listed:
    • calc_ci_demo-1.1.0-py3-none-any.whl
    • calc_ci_demo-1.1.0.tar.gz
    • calc-ci-demo-js-1.1.0.tgz
    • SHA256SUMS.txt
  3. Confirm the release notes list the merged pull requests since the previous tag.
  4. Open the java-ci-demo release page and confirm calc-ci-demo-1.1.0.jar and SHA256SUM-jar.txt are listed.

Step 5.6 Download and verify the release

Goal: act as a consumer who downloads the release and checks it.

Do

mkdir -p /tmp/release-check && cd /tmp/release-check
BASE=https://github.com/<your-user>/python-ci-demo/releases/download/v1.1.0
curl -sLO "$BASE/calc_ci_demo-1.1.0-py3-none-any.whl"
curl -sLO "$BASE/calc-ci-demo-js-1.1.0.tgz"
curl -sLO "$BASE/SHA256SUMS.txt"
sha256sum -c SHA256SUMS.txt
cd ~/work/python-ci-demo

Expected output

Each downloaded file prints OK. If any file prints FAILED, the download is corrupt or the release was changed. Do not install it.

Explanation

Step 5.7 Negative test: a mismatched tag must not publish

Goal: confirm the pipeline refuses a bad release.

Do

Create a deliberate mismatch on a new tag in the Python repository. It will fail, and you will not publish anything you need to keep:

cd ~/work/python-ci-demo
git tag -a v9.9.9 -m "Deliberate mismatch test"
git push origin v9.9.9

Open Actions, open the Release run for v9.9.9, and read the Check versions match tag step.

Expected output

The step fails with FAIL pyproject.toml is 1.1.0, tag is 9.9.9. The release job is skipped, and no release appears for v9.9.9.

Clean up

git push --delete origin v9.9.9
git tag -d v9.9.9
Checkpoint: v1.1.0 is a published release with the wheel, tarball, npm package, and checksum file. The mismatched v9.9.9 tag was blocked, and you deleted it.

Part 6: Artifacts in Jenkins 6 min

Jenkins does not create GitHub Releases. It builds the same artifacts and archives them on the build page, which suits internal builds.

Step 6.1 Install Node.js in the Jenkins container

Goal: give Jenkins the npm command for the npm pack stage.

Do

docker exec -u root jenkins bash -c "apt-get update && apt-get install -y nodejs npm"
docker exec jenkins node --version
docker exec jenkins npm --version

Expected output

Both commands print a version number. Debian's Node may be older than 20, which is fine for npm pack.

Explanation

Step 6.2 Replace the Jenkinsfile

Goal: add a stage that builds, checksums, and archives the artifacts after the tests pass.

Do

cd ~/work/python-ci-demo
git checkout -b ext/jenkins-artifacts
Jenkinsfile
pipeline {
    agent any

    options {
        timestamps()
        disableConcurrentBuilds()
        buildDiscarder(logRotator(numToKeepStr: '10'))
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Lint and Test') {
            steps {
                sh '''
                    python3 -m venv .venv
                    . .venv/bin/activate
                    pip install -r requirements.txt
                    flake8 app tests
                    mkdir -p reports
                    pytest --junitxml=reports/junit.xml
                '''
            }
            post {
                always {
                    junit testResults: 'reports/junit.xml', allowEmptyResults: true
                }
            }
        }

        stage('Build artifacts') {
            steps {
                sh '''
                    . .venv/bin/activate
                    pip install --upgrade build
                    rm -rf dist
                    python -m build
                    cd npm
                    npm pack --pack-destination ../dist
                    cd ../dist
                    sha256sum * > SHA256SUMS.txt
                    ls -l
                '''
            }
            post {
                success {
                    archiveArtifacts artifacts: 'dist/*', fingerprint: true
                }
            }
        }
    }

    post {
        always {
            cleanWs()
        }
    }
}

Explanation

Step 6.3 Commit and run the build

Do

git add Jenkinsfile
git commit -m "Jenkins: build and archive artifacts"
git push -u origin ext/jenkins-artifacts

Merge the branch into main on GitHub, or change the Jenkins job's branch specifier to */ext/jenkins-artifacts for this run. Then, in Jenkins:

  1. Open the python-ci-demo job and click Build Now.
  2. Open the build and read the Build artifacts stage in the console output. The ls -l at the end shows the wheel, sdist, tarball, and checksum file.
  3. Click Build Artifacts on the build page and confirm the four files are listed.

Step 6.4 Follow the fingerprint

Goal: understand what the fingerprint records.

Do

  1. On the build page, open the fingerprint link next to calc_ci_demo-1.1.0-py3-none-any.whl.
  2. Read the hash and the build that produced it.

Explanation

Step 6.5 Decide the version for internal builds

Goal: choose a version for builds that are not releases.

Jenkins builds from main do not create tags, so they need their own version. The usual convention is a pre-release with the build number:

Build typeExample versionWhy
Release1.1.0Matches the tag. Only the release pipeline produces it.
Snapshot from main1.2.0-dev.42Sorts below 1.2.0, so it is never mistaken for a final release. 42 is the build number.
Traceability only1.1.0+build.42Records the build. Precedence ignores it, so do not use it to select dependencies.
Never reuse a version number for different content. Once 1.1.0 is published, a different build must become 1.1.1.
Checkpoint: the Jenkins build archives four files, the fingerprint page opens, and you can state which version a snapshot build should use.

Part 7: Consuming and pinning 8 min

Packaging is one half of the process. The other half is how a consumer chooses to depend on a version.

Step 7.1 Read the version range syntax

EcosystemExpressionAcceptsRejects
npm^1.1.01.1.0 up to 1.x2.0.0
npm~1.1.01.1.0 up to 1.1.x1.2.0
npm1.1.0Only 1.1.0Everything else
pipcalc-ci-demo>=1.1,<21.1 up to 1.x2.0
pipcalc-ci-demo==1.1.0Only 1.1.0Everything else
Maven[1.1.0,2.0.0)1.1.0 up to, not including, 2.0.02.0.0 and above

Step 7.2 Install the wheel from the release

Goal: install version 1.1.0 into a new project using the release URL.

Do

python3 -m venv /tmp/consumer-py
/tmp/consumer-py/bin/pip install "https://github.com/<your-user>/python-ci-demo/releases/download/v1.1.0/calc_ci_demo-1.1.0-py3-none-any.whl"
/tmp/consumer-py/bin/python -c "from app.calculator import add; print(add(2, 3))"

Expected output

pip reports that it installed calc-ci-demo-1.1.0, and the Python command prints 5.

Explanation

Step 7.3 Install the npm tarball from the release

Do

mkdir -p /tmp/consumer-js && cd /tmp/consumer-js
npm init -y >/dev/null
npm install "https://github.com/<your-user>/python-ci-demo/releases/download/v1.1.0/calc-ci-demo-js-1.1.0.tgz"
node -p "require('calc-ci-demo-js').add(2, 3)"
cd ~/work/python-ci-demo

Expected output

The Node command prints 5. The install adds calc-ci-demo-js under dependencies in the consumer's package.json, with the tarball URL as its version.

Explanation

Step 7.4 Install the jar and resolve it with Maven

Goal: make the jar available to Maven and confirm a project can resolve it by coordinates.

Do

cd ~/work/java-ci-demo
mvn -B install:install-file \
  -Dfile=target/calc-ci-demo-1.1.0.jar \
  -DgroupId=com.example -DartifactId=calc-ci-demo \
  -Dversion=1.1.0 -Dpackaging=jar
mvn -B dependency:get -Dartifact=com.example:calc-ci-demo:1.1.0 -Dtransitive=false

Expected output

Explanation

Step 7.5 Pin a dependency in a consumer's requirements file

Goal: record the chosen version in the consumer project so every build uses it.

Do

consumer requirements.txt
calc-ci-demo @ https://github.com/<your-user>/python-ci-demo/releases/download/v1.1.0/calc_ci_demo-1.1.0-py3-none-any.whl

Explanation

Checkpoint: version 1.1.0 installs from all three sources, and the consumer records the pin in a file that is committed.

Part 8: Recap, quiz, cleanup, and troubleshooting 3 min

8.1 Key takeaways

8.2 Quiz

  1. A release adds one optional function and fixes a bug. Which number changes?
  2. Why does the release workflow run the tests again when CI already passed on main?
  3. What does sha256sum -c prove to someone who downloads the release?
  4. A library depends on ^2.3.0. Does a consumer receive version 3.0.0? Why or why not?
  5. Why should Jenkins builds from main not use the plain version 1.2.0?
Answers
  1. MINOR. A new backward-compatible feature bumps MINOR, and PATCH resets to 0. The bug fix rides along in the same release.
  2. The tag might point at a commit that was never tested on main, so the release tests what it publishes.
  3. That the downloaded file is byte-for-byte the file the pipeline produced. A corrupted or altered file fails the check.
  4. No. ^2.3.0 accepts 2.3.0 up to, but not including, 3.0.0. A major release is a breaking change, so it is excluded.
  5. A snapshot must sort below the release it leads to. 1.2.0-dev.42 sorts below 1.2.0. The plain version would look like a final release that does not exist yet.

8.3 Cleanup

rm -rf /tmp/consumer-py /tmp/consumer-js /tmp/check-wheel /tmp/check-npm /tmp/release-check
cd ~/work/python-ci-demo && git checkout main
cd ~/work/java-ci-demo && git checkout main

8.4 Troubleshooting

SymptomCause and fix
Tag pushed but no release workflow startedThe tag must match v*.*.*, such as v1.1.0. A tag named 1.1.0 does not match. Also confirm the workflow file was committed before the tag was pushed; if not, re-tag as in Step 5.3.
FAIL pyproject.toml is 1.0.0, tag is 1.1.0The version was not updated before tagging. Update the file, commit, delete the tag, and tag again.
FAIL npm/package.json ...The npm version differs from the tag. Update npm/package.json and commit.
Permission denied when running the scriptRun chmod +x scripts/check_version.sh and commit the change, so the executable bit is stored in Git.
Wheel file is named calc_ci_demo, not calc-ci-demoNormal. Packaging converts hyphens in file names. The project name in pyproject.toml is unchanged.
npm pack includes unexpected filesCheck the files list in npm/package.json. Run npm pack --dry-run to see the list before building.
python -m build cannot download setuptoolsThe build fetches its backend into an isolated environment, so it needs network access. Check your connection or proxy.
sha256sum: ... No such file or directory in the checksum stepThe file list is wrong, or dist/ is empty. Run ls dist/ and check the name matches the wheel built in Step 3.1.
Maven install:install-file fails with a missing fileBuild the jar first with mvn -B -DskipTests package, and check the path in -Dfile.
Jenkins: npm: not foundInstall Node.js in the container, as in Step 6.1.
Jenkins: No such DSL method 'archiveArtifacts'The file has a syntax error above the stage. Check the braces, and that the stage is inside stages { }.
Release already exists for the tagReleases are immutable by convention. Publish a new patch version instead of reusing the tag.