Version: 0.4.2 Status: Active Last Updated: March 3, 2026 Project: Juniper - Dataset Generation Service
- Introduction
- Pipeline Overview
- GitHub Actions Workflows
- Pre-commit Hooks
- Dependabot Configuration
- Quality Gate
- Release Process
- Troubleshooting
juniper-data uses a multi-layer CI/CD strategy:
- Pre-commit hooks: Local code quality enforcement (ruff, mypy, bandit, yamllint, shellcheck)
- Pre-push hooks: Coverage gate (80% aggregate, 85% per-module)
- GitHub Actions CI: Automated testing across Python 3.12/3.13/3.14, security scanning, build verification, documentation validation
- GitHub Actions publishing: Two-stage PyPI publishing (TestPyPI then production PyPI)
- Dependabot: Automated dependency updates with lockfile synchronization
- CodeQL: Weekly semantic code analysis
pre-commit ──┬─→ unit-tests ──┬─→ build ──→ dependency-docs ──┐
│ │ │
│ └─→ integration-tests ──────────┤
│ │
├─→ security ────────────────────────────────────┤
├─→ docs ────────────────────────────────────────┤
└─→ lockfile-check ──────────────────────────────┘
│
└─→ required-checks ──→ notify| Workflow | Push to main/develop | Push to feature/fix | Pull Requests | Schedule | Release | Manual |
|---|---|---|---|---|---|---|
ci.yml |
Yes | Yes | Yes | Daily 6 AM UTC | No | Yes |
publish.yml |
No | No | No | No | Yes | No |
lockfile-update.yml |
No | dependabot branches | No | No | No | No |
codeql.yml |
Yes | No | Yes (to main) | Weekly Mon 6 AM UTC | No | No |
The primary CI workflow runs up to 11 jobs with concurrency control (ci-${{ github.ref }}, cancel-in-progress).
Multi-version code quality validation across Python 3.12, 3.13, and 3.14 with fail-fast: false.
- Runs all pre-commit hooks: ruff, mypy, bandit, yamllint, file checks
- Caches
~/.cache/pre-commitkeyed on.pre-commit-config.yamlhash - First job to run; blocks
unit-testsandsecurity
Validates documentation link integrity using scripts/check_doc_links.py.
- Checks all markdown files for broken internal links and anchors
- Excludes
templates/andhistory/directories - Runs independently (no dependencies)
Core test execution with coverage enforcement across Python 3.12, 3.13, and 3.14.
- Depends on:
pre-commit - Markers:
-m "unit and not slow" - Timeout: 60 seconds per test,
--maxfail=5 - Coverage:
--cov-fail-under=80(aggregate), thencheck_module_coverage.pyfor 85% per-module - Installs juniper-data-client from
mainbranch via git+https - Reports: JUnit XML, HTML coverage, Cobertura XML, JSON coverage
- Codecov upload: Python 3.14 only, using
CODECOV_TOKENsecret
Package build and verification.
- Depends on:
unit-tests - Builds sdist and wheel with
python -m build - Verifies
.tar.gzand.whlfiles exist - Uploads
dist/as artifact (30 day retention)
Generates dependency documentation snapshots.
- Depends on:
build - Installs
.[all]plus thejuniper-data-clientmain branch before capture - Runs
scripts/generate_dep_docs.sh - Outputs
conf/requirements_ci.txtandconf/conda_environment_ci.yamlas workflow artifacts, with timestamped backups if prior files exist in the checkout - Does not update the committed
conf/requirements.txtorconf/requirements-ORIG.txtenvironment snapshots - Uses Conda (Miniforge) for environment capture
- 90 day artifact retention
These generated files describe the environment that ran in CI. They are diagnostic artifacts, not source inputs for local setup. The checked-in setup script installs supplemental packages from conf/requirements.txt, and conf/requirements-ORIG.txt is kept as the paired baseline copy for that setup snapshot.
Full workflow integration tests.
- Depends on:
unit-tests - Condition: Only on PRs or main/develop branches
- Markers:
-m "integration and not slow" - Timeout: 120 seconds per test,
--maxfail=3
Multi-tool security scanning.
- Gitleaks: Secret detection in repository history
- Bandit SAST: Static security analysis with SARIF report
- Blocking check: fails on medium+ severity and confidence
- SARIF uploaded to GitHub Security tab
- pip-audit: Dependency vulnerability scanning
- Filters out
juniper-datapackages (self-referencing) - Runs in
--strictmode
- Filters out
Validates requirements.lock freshness.
- Runs independently (no dependencies)
- Uses
uv pip compileto regenerate lockfile - Diffs against committed
requirements.lock - Prints remediation command on failure
Quality gate aggregator. Runs if: always() and checks status of all required jobs.
Required to pass: pre-commit, unit-tests, build, dependency-docs, security, docs, lockfile-check
Optional (failure = error, skip = OK): integration-tests
Long-running test execution.
- Trigger: Schedule (daily 6 AM UTC) or manual dispatch only
- Markers:
-m "slow" - Timeout: 600 seconds (10 minutes)
Build completion notification summary.
Two-stage publishing triggered by GitHub Release creation.
- Build package with
python -m build - Validate with
twine check dist/* - Publish to TestPyPI via
pypa/gh-action-pypi-publish(OIDC trusted publishing) - Wait 30 seconds for index sync
- Install from TestPyPI and verify import
Environment: testpypi (5-minute wait timer)
Identical to TestPyPI stage but publishes to production.
Environment: pypi (5-minute wait timer + required reviewer approval)
Both stages use attestations: false and SHA-pinned actions. Version is extracted from the release tag (strips v prefix).
Automatically regenerates requirements.lock when Dependabot updates dependencies.
- Trigger: Push to
dependabot/pip/**branches - Condition: Only runs for
dependabot[bot]actor - Runs
uv pip compile pyproject.toml --extra api --extra observability --extra mnist --upgrade -o requirements.lock - Uses
CROSS_REPO_DISPATCH_TOKEN(notGITHUB_TOKEN) so the push re-triggers CI - Commits with
[dependabot skip]prefix to prevent Dependabot re-processing
The workflow only updates requirements.lock, which is generated from pyproject.toml for Docker and CI reproducibility. Dependabot PRs that target conf/requirements.txt or conf/requirements-ORIG.txt are changing the legacy/local setup snapshot used by util/setup_environment.bash; keep those two files in sync and do not expect the lockfile workflow to rewrite them.
GitHub CodeQL semantic analysis for Python.
- Trigger: Push to main/develop, PRs to main, weekly Monday 6 AM UTC
- Runs
security-and-qualityquery suite - Results appear in GitHub Security tab
| Hook | Stage | Tool | Files | Auto-fix? |
|---|---|---|---|---|
| check-yaml | commit | pre-commit-hooks | *.yaml, *.yml |
No |
| check-toml | commit | pre-commit-hooks | *.toml |
No |
| check-json | commit | pre-commit-hooks | *.json |
No |
| end-of-file-fixer | commit | pre-commit-hooks | All | Yes |
| trailing-whitespace | commit | pre-commit-hooks | All (md linebreaks preserved) | Yes |
| check-merge-conflict | commit | pre-commit-hooks | All | No |
| check-added-large-files | commit | pre-commit-hooks | All (max 1000 KB) | No |
| check-case-conflict | commit | pre-commit-hooks | All | No |
| check-ast | commit | pre-commit-hooks | *.py |
No |
| debug-statements | commit | pre-commit-hooks | *.py |
No |
| detect-private-key | commit | pre-commit-hooks | All | No |
| ruff | commit | Ruff v0.15.2 | juniper_data/**/*.py |
Yes (--fix) |
| ruff-format | commit | Ruff v0.15.2 | juniper_data/**/*.py |
Yes |
| mypy (prod) | commit | MyPy v1.13.0 | juniper_data/(?!tests/).*\.py |
No |
| mypy (test) | commit | MyPy v1.13.0 | juniper_data/tests/.*\.py |
No |
| bandit | commit | Bandit v1.7.9 | juniper_data/(?!tests).*\.py |
No |
| yamllint | commit | yamllint v1.35.1 | *.yaml, *.yml |
No |
| shellcheck | commit | ShellCheck v0.10.0.1 | Shell scripts | No |
| no-unencrypted-env | commit | Local | .env, .env.secrets |
No (blocks) |
| coverage-check | pre-push | Local | All (always_run) | No |
Standard pre-commit-hooks (v6.0.0) validate file integrity: YAML/TOML/JSON syntax, trailing whitespace, merge conflict markers, large files (max 1000 KB), case conflicts, Python AST validity, debug statements, and private key patterns.
Ruff replaces black, isort, flake8, and related tools. Runs on juniper_data/**/*.py only.
- Linting: Auto-fixes violations with
--fix - Formatting: Enforces consistent style
- Config:
[tool.ruff]in pyproject.toml (line-length=320, target-version=py312)
Two separate hooks with different strictness levels:
- Production code:
--ignore-missing-imports --no-strict-optional(requirestypes-redis) - Test code: Same as above plus
--allow-untyped-defs(relaxed for test functions)
Runs on production code only (excludes tests/). Skips B101 (assert) and B311 (random module).
Runs scripts/check_module_coverage.py --run-tests on the pre-push stage. Enforces 80% aggregate and 85% per-module coverage. This means you can commit freely, but cannot push code that drops coverage below thresholds.
- ShellCheck: Severity level
warning(skips style/info). Excludes specific legacy scripts. - yamllint: Relaxed preset with parsable output.
Blocks commits of unencrypted .env or .env.secrets files. Ensures secrets are SOPS-encrypted before committing.
- Schedule: Weekly on Mondays at 9 AM ET
- PR limit: 5 open PRs
- Labels:
dependencies,security - Grouping: Minor + patch updates grouped together
- Commit prefix:
deps
- Schedule: Weekly on Mondays
- PR limit: 3 open PRs
- Labels:
dependencies,ci - Commit prefix:
ci
When Dependabot pushes to dependabot/pip/**, the lockfile-update.yml workflow automatically regenerates requirements.lock and commits the update.
The required-checks job in ci.yml acts as the merge quality gate. All of these must pass:
| Check | Required | Failure Impact |
|---|---|---|
| pre-commit (code quality) | Yes | Blocks merge |
| unit-tests (all Python versions) | Yes | Blocks merge |
| build (package verification) | Yes | Blocks merge |
| dependency-docs | Soft | Failure blocks, skip OK |
| security (gitleaks + bandit + pip-audit) | Soft | Failure blocks, skip OK |
| docs (link validation) | Yes | Blocks merge |
| lockfile-check | Yes | Blocks merge |
| integration-tests | Conditional | Failure blocks, skip OK (feature branches) |
- Create a GitHub Release with a tag matching the version (e.g.,
v0.4.2) publish.ymltriggers automatically- TestPyPI stage: Build, verify, publish, install-test (5-min environment wait)
- PyPI stage: Same process, requires manual reviewer approval
- Approve the PyPI deployment via GitHub environment approval or
gh api
See PyPI Publishing Procedure for ecosystem-wide publishing lessons.
Pre-commit hooks not running: Ensure you've run both pre-commit install and pre-commit install --hook-type pre-push.
Ruff modifies files on commit: This is by design. Ruff auto-fixes lint violations and reformats code. Stage the changes and commit again.
CI fails but local passes: Check Python version matrix. CI tests on 3.12, 3.13, and 3.14. Ensure your local environment matches.
Lockfile check fails: Regenerate with:
uv pip compile pyproject.toml --extra api --extra observability --extra mnist -o requirements.lockDependabot PR missing lockfile update: The lockfile-update.yml workflow handles requirements.lock automatically when the branch matches dependabot/pip/** and the actor is dependabot[bot]. If the PR only changes conf/requirements.txt and conf/requirements-ORIG.txt, a lockfile update may not be needed.
CodeQL findings: Review in GitHub Security tab. These are informational and don't block the merge quality gate.
TestPyPI publish fails: Check that the release tag matches the version in pyproject.toml. Version is extracted by stripping the v prefix from the tag.
Coverage drops after push: Run python scripts/check_module_coverage.py --run-tests locally to identify modules below the 85% threshold.