Reference Python package demonstrating CI/CD with GitHub (PRs), Jenkins, Artifactory, and JFrog Xray.
The application is a small CLI with two subcommands:
greet— personalized greetings (casual or formal)stats— text statistics rendered as a Rich table
Versioning uses setuptools-scm from git tags (v1.0.0 → 1.0.0). CI is Jenkins-only (no GitHub Actions workflows).
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
hello-world greet
hello-world greet --name Alice --style formal
hello-world stats "Hello, CI/CD demo!"
hello-world stats --file README.md
python -m buildRun the full local pipeline (same steps as Jenkins, without publish):
chmod +x scripts/ci.sh
./scripts/ci.shWith JFrog CLI v2 and credentials:
export JF_URL="https://your-instance.jfrog.io"
export JF_ACCESS_TOKEN="***"
jf scan dist/ --fail # after ./scripts/ci.sh build, or:
CI_PUBLISH=true BUILD_NUMBER=1 ./scripts/ci.sh| Command | Description |
|---|---|
hello-world greet |
Casual greeting (Hello, World!) |
hello-world greet --name Alice --style formal |
Formal greeting |
hello-world stats "some text" |
Word/line/char stats as a table |
hello-world stats --file path/to/file |
Stats from a file |
hello-world --version |
Package version |
flowchart LR
subgraph cli [cli.py]
greetCmd[greet]
statsCmd[stats]
end
subgraph core [package modules]
greetings[greetings.py]
textStats[text_stats.py]
models[models.py]
end
greetCmd --> greetings
statsCmd --> textStats
textStats --> models
statsCmd --> richLib[rich]
| Module | Role |
|---|---|
models.py |
GreetingStyle enum, TextStats dataclass |
greetings.py |
format_greeting() |
text_stats.py |
analyze_text() |
cli.py |
argparse subcommands and Rich table output |
| Kind | Packages |
|---|---|
| Runtime | rich (formatted CLI output) |
Dev (pip install -e ".[dev]") |
pytest, mypy, ruff, pylint, bandit, build, … |
The runtime dependency gives JFrog Xray a real third-party package to scan in built wheels.
| Git state | Example version |
|---|---|
Tag v1.0.0 on commit |
1.0.0 |
| Commits after last tag | 1.0.1.dev3+g1a2b3c4 (illustrative) |
| PR / feature branch | dev version from git describe |
Release workflow
-
Merge changes to
main. -
Create and push an annotated tag:
git tag -a v1.0.0 -m "Release 1.0.0" git push origin v1.0.0 -
Jenkins builds the tag, runs tests,
jf scan, and publishes wheels to Artifactory.
Troubleshooting setuptools-scm
- Jenkins checkout must use full history (
depth: 0); shallow clones break versioning. - Ensure tags are fetched:
git fetch --tags. - Tag format must match
v*(e.g.v1.0.0), configured inpyproject.toml.
- New Item → Multibranch Pipeline.
- Branch Sources → GitHub → select repository.
- Enable Discover pull requests (from origin).
- Behaviors → Discover tags with filter
v*(optional: tag build strategy). - Build Configuration → Script Path:
Jenkinsfile.
Create Jenkins credentials (IDs must match Jenkinsfile or update the file):
| Credential ID | Type | Purpose |
|---|---|---|
jf-url |
Secret text | Platform URL (JF_URL) |
jf-access-token |
Secret text | Access token (JF_ACCESS_TOKEN) |
Optional job environment variable:
| Variable | Default | Purpose |
|---|---|---|
JF_RT_REPO |
pypi-local |
Artifactory repository key for uploads |
Agents need Python 3.10+, git, and jf (JFrog CLI v2) on PATH for scan/publish stages.
The scripted pipeline groups work into these top-level stages:
| Stage | Contents |
|---|---|
| Prepare | Checkout (full git history + tags), Python venv, pip install -e '.[dev]' |
| Static Code Analysis | isort, flake8, pylint, ruff, mypy, bandit (in parallel), then Analysis Report |
| Test | pytest + Cobertura (coverage.xml) + JUnit |
| Build | python -m build, CLI smoke tests, write version.txt |
| Release | Xray scan (jf scan), Artifactory publish (conditional) |
| Archive | dist/*, logs, coverage, test results |
Static analysis tools (run in parallel inside Static Code Analysis):
| Tool | Purpose |
|---|---|
| isort | import order |
| flake8 | style / pyflakes |
| pylint | lint |
| ruff | fast lint |
| mypy | type check |
| bandit | security |
Analysis Report runs recordIssues (Warnings NG) and fails the build if any tool exited non-zero.
Requires Jenkins plugins: Warnings NG (recordIssues), Code Coverage (recordCoverage), JUnit.
Local equivalent: ./scripts/analyse.sh (all tools) or ./scripts/analyse.sh pylint flake8.
| Build type | Analysis / test / build | jf scan |
jf rt upload |
|---|---|---|---|
| Pull request | yes | yes (if jf present) |
no |
| Feature branch | yes | yes | no |
main |
yes | yes | yes |
Tag v* |
yes | yes | yes |
Publish runs when IS_RELEASE is true (main or tag) and the build is not a change request (CHANGE_ID unset).
- Configure the multibranch job webhook / GitHub App (push +
pull_request). - In GitHub branch protection for
main, require the Jenkins check (e.g.Jenkins / … / PR-123) before merge. - Opening or updating a PR triggers Jenkins; Xray failures fail the PR build.
Upload target (configurable via JF_RT_REPO):
{pypi-local}/hello-world/{version}/
hello_world-{version}-py3-none-any.whl
hello_world-{version}.tar.gz
Build info: hello-world / ${BUILD_NUMBER} via jf rt build-publish.
├── Jenkinsfile
├── pyproject.toml
├── src/hello_world/
│ ├── cli.py
│ ├── greetings.py
│ ├── models.py
│ └── text_stats.py
├── tests/
└── scripts/ci.sh
MIT