Users may contribute to the Rattlesnake project by cloning or forking the Rattlesnake repository.
Direct cloning is reserved for authorized collaborators of the Rattlesnake repository; however, because the project is open-source, all other contributors can obtain their own copy by forking the repository.
Cloning¶
Cloning is a Git action.
It creates a copy of a repository on your physical computer.
It allows you to edit files and run code locally. Collaborators “clone” the original repository directly because they have permission to “push” (save) their changes directly back to the main project.
External contributors usually “clone” their own fork.
Forking¶
Forking is a GitHub action.
It is a personal copy of the entire project on your own GitHub account.
It acts as a bridge for external contributors. You can make any changes you want to your fork without affecting the original project. When you are ready to share those changes, you submit a Pull Request to the original repository.
Getting the Source Code¶
Collaborators and team members should clone the repository:
git clone git@github.com:sandialabs/rattlesnake-vibration-controller.gitOthers should first fork the repository to their own GitHub account. Once forked, you can then clone your personal version of the repo to work on it locally.
Installation¶
A virtual environment is highly recommended. This ensures project dependencies do not conflict with the system-wide Python installation.
Create a new virtual environment folder within your project directory. It is conventional to name this folder .venv.
# macOS / Linux / Windows
python3 -m venv .venvActivating the environment tells your shell to use the Python interpreter and pip packages located inside the .venv folder.
macOS / Linux:¶
source .venv/bin/activateWindows (PowerShell):¶
.venv\Scripts\Activate.ps1Windows (Command Prompt), DOS¶
.venv\Scripts\activate.batOnce activated, your terminal prompt will typically show (.venv). You can now install dependencies safely.
# Install specific packages, for example, the "requests" package
pip install requests
# Or install the entire Rattlesnake development in editable mode
pip install -e .[dev]Confirm that your shell is pointing to the correct Python binary.
# macOS / Linux
which python
# Windows
where pythonThe output should point to a path inside your project’s .venv folder.
To exit the virtual environment and return to the global system stack:
deactivateBest Practice: Never commit the
.venvdirectory to version control. Add.venv/to your.gitignorefile.
Documentation¶
The online documentation is made with Jupyter Book. Below are instructions for setting up a local development environment, building the book locally, and publishing the updates to the repository.
Install documentation dependencies either with pip
pip install "jupyter-book>=2.0.0"or with uv
uv add "jupyter-book"Local Build¶
Within this documentation folder, the myst.yml file specifies how Jupyter Book should build the documentation. Importantly, it links to Markdown files that contain the book’s content.
cd rattlesnake-vibration-controller/documentation
jupyter book build --html --strictThis will build the Jupyter Book documentation.
The output will be similar to:
building myst-cli session with API URL: https://api.mystmd.org
(node:93011) Warning: `--localstorage-file` was provided without a valid path
(Use `node --trace-warnings ...` to show where the warning was created)
🌎 Building Jupyter Book (via myst) site
📖 Built book/src/_generated/random_vibration_run_doc.md in 64 ms.
📖 Built book/src/chapter_13.md in 124 ms.
📖 Built book/src/notation.md in 117 ms.
📖 Built book/src/contributing.md in 117 ms.
<--(snip)-->
📚 Built 32 pages for project in 813 ms.To view the Jupyter Book output locally:
jupyter book startThe output will be similar to:
📚 Built 32 pages for project in 974 ms.
<--(snip)-->
🔌 Server started on port 3000! 🥳 🎉
👉 http://localhost:3000 👈In a local web browser, navigate to the web address indicated above.
Bibliography¶
The documentation uses the myst-nb and standard MyST bibliography support.
Prepare your bibliography file:
References are stored in documentation/book/bibliography.bib using the standard BibLaTeX (.bib) format. Populate the file with references, e.g.,
@book{knuth1986computer,
title={The Computer Science of TeX and Metafont: An Inaugural Lecture},
author={Knuth, Donald E},
year={1986},
publisher={American Mathematical Society}
}Configure
myst.yml
The bibliography is configured in documentation/myst.yml under the project.bibliography section:
project:
bibliography:
- book/bibliography.bibAdd in-text citations
In a markdown file, use the cite role to reference an entry by its key:
{cite} knuth1986computer
Build the book
Run the jupyter book build command from the documentation directory. The build system will automatically process the citations and generate the bibliography.
cd documentation
jupyter book buildContinuous Integration/Continuous Deployment (CI/CD)¶
Synopsis¶
ci.yml — Continuous Integration
Triggered on every push to any branch, plus manual dispatch. Six jobs:
changes
Uses dorny/paths-filter to detect whether docs or code files changed. Downstream jobs use this to skip unnecessary work.
pytest_matrix
Runs tests on all combinations of [macos, ubuntu, windows] × [3.11, 3.12] using
pip install .[dev](notuv) for PyQt wheel compatibility. Test scope is adaptive:Default:
tests/*.py tests/shortFull suite triggered by: commit message containing
[all tests], manual dispatch withtest_level=full, or branch ismainordev
lint
Runs
pylint src/rattlesnakeviauv, captures output, then callsreport_lint.pyto generate an HTML lint report artifact.
coverage
Runs
pytest --covviauvwith the same adaptive test scope, then callsreport_coverage.pyto generate an HTML coverage report artifact.
docs_jupyter_book
Updates
myst.ymlmetadata viareport_jupyter_book.py,then builds the Jupyter Book. Only runs when docs changed (or onmain/dev).
deploy
Runs only on
main/dev, assembles all artifacts into apages/tree, generates the dashboard (report_dashboard.py), creates SVG badges, and deploys to GitHub Pages via peaceiris/actions-gh-pages.
release.yml — Release Pipeline
Triggered only on v* tags. Five sequential jobs:
validate_tag
Verifies the tag was created on the
mainordevbranch, that it conforms to PEP 440, and that it is strictly newer than all existing tags.
test
Calls
ci.ymlas a reusable workflow (workflow_call).
build
Runs
uv buildand generates a Supply chain Levels for Software Artifacts (SLSA, aka “salsa”) provenance attestation for the dist artifacts.
github-release
Creates a GitHub Release with auto-generated notes and attaches the
distfiles.
publish
Publishes to PyPI or TestPyPI via Trusted Publishing. Tags containing
rcordevgo to TestPyPI; all others go to production PyPI.
Details¶
The separate concerns of test, build, release, and publish are contained in the .github/workflows/ files.
Continuous Integration (CI)
Test (Verification)
Purpose: To ensure that the code is functional and hasn’t introduced regressions (broken existing features).
Scope: Tests are run on one or more versions of Python and on multiple operating systems (e.g., Linux, macOS, Windows).
What happens: Automated unit tests, integration tests, and code quality assessments are performed.
Testing (e.g., pytest) runs your unit and integration tests.
Code coverage (e.g., pytest with a coverage report) assesses the number of lines of code covered by tests.
Linting (static code analysis, e.g., pylint) and
Code Formatting (e.g., ruff) checks ensure code consistency.
Documentation may also be assembled and compiled. This is particularly important for interactive documentation that has examples that depend on source code functionality.
Key Outcome: Confidence. If this stage fails, the process stops immediately, preventing broken code from ever reaching a user.
Build (Packaging)
Purpose: To transform your “human-readable” source code into “machine-installable” artifacts. This is the bridge between CI and CD. Once the code is verified (integrated), it can be packaged into a deployable format (Wheels/SDists).
What happens: Tools (like
uv build) bundle your code into standard formats, such as a Wheel (.whl) or a Source Distribution (.tar.gz).Key Outcome: Portability. You now have a single file (an “artifact”) that contains everything needed to install your library on any compatible system.
Release (Documentation & Tagging)
Purpose: To create an official “point-in-time” snapshot of the project for project management and users. It uses an immutable Git tag and GitHub Release page.
What happens: A permanent Git tag (like v1.0.0) is assigned to a specific commit. A GitHub Release page is generated with a Changelog (i.e., What’s New?) and the build artifacts are attached to it as “Release Assets.”
Key Outcome: Traceability. It provides a clear history of the project’s evolution and a stable place for users to download specific versions.
Continuous Delivery (CD)
Publish (Distribution)
Purpose: To make the software easily available to the global ecosystem.
What happens: The built artifacts are uploaded to a package registry, such as PyPI (the Python Package Index).
Key Outcome: Accessibility. Once published, anyone in the world can install your software using a simple command like
pip install rattlesnake-vibration-controller.
Efficiency¶
When a user pushes to the repository, the changes job in the main workflow
determines the types of the files that were committed. The job determines
if only docs (documentation) files changed, only code (source code, project code)
files changed, or both.
Updates to docs only¶
For example, upon pushing updates only to a markdown file (i.e., *.md),
the job makes this determination:
📂 Docs changed: true
📂 Docs files: documentation/book/src/contributing.md
💻 Code changed: false
💻 Code files:In this scenario, only jobs that rely on updates to documentation file types are run. This avoids running unnecessary tests that don’t rely on documentation updates.
Figure 1:CI/CD workflow execution for documentation-only changes.
Updates to code only¶
For example, upon pushing updates to source code (e.g., *.py),
the job makes this determination:
📂 Docs changed: false
📂 Docs files:
💻 Code changed: true
💻 Code files: src/rattlesnake/cicd/report_dashboard.py src/rattlesnake/cicd/report_jupyter_book.py src/rattlesnake/cicd/report_lint.py tests/test_cicd_utilities.pyOnly the pytest_matrix, lint, and coverage jobs will be run. The docs_jupyter_book and deploy jobs will be skipped.
All test¶
Regardless of the file type, if either the main or the dev branch is target
of an update, all tests are run, for example,
Figure 2:Full suite of CI/CD jobs triggered for main or dev branch updates.
Running the full suite is significantly more time-consuming than executing only the specific tests relevant to the modified files.
Matrix scope¶
The pytest_matrix job runs across combinations of operating systems and Python versions. The scope is adaptive:
Feature branches — runs only
ubuntu-latest×3.12(1 runner). This keeps per-push feedback fast.mainanddevbranches — runs the full matrix:macos-latest,ubuntu-latest, andwindows-latest×3.11and3.12(6 runners). Full cross-platform coverage is enforced before anything reaches a release branch.
This means OS-specific or Python-version-specific bugs are caught on main/dev before a release, without slowing down every feature branch push.
Test¶
Tests are grouped by the amount of time required to run the tests. The current groups are
tests/
tests/long/
tests/short/Whenever there is a push or pull request to main or dev, all tests will run (which includes long tests). For pushes to branches other than main or dev, only tests in tests/ and tests/short are run.
Developers can force a full test, which includes tests/long in addition to tests/ and tests/short, by adding the string [all tests] to the commit message. For example, on the dev-cicd-docs branch
(dev-cicd-docs) > git commit -m 'test feature foo with [all tests]'will trigger all tests to be run.
Preflight¶
The preflight command is a local CI/CD readiness check. It mirrors the checks that GitHub Actions would run on a push, allowing developers to catch errors before they reach the pipeline.
preflight is a command line entry point installed with the rattlesnake-vibration-controller package. The package must be installed in the active environment before the command is available:
uv pip install -e .[dev]Once installed, run it from the repository root:
uv run preflightModes and options¶
By default, preflight matches CI’s scope on non-main/dev branches: ruff format check and full pylint on src/rattlesnake/. When pytest is re-enabled, the default scope will also run tests/ --ignore=tests/long; use --all-tests to include tests/long/ (matching CI on main/dev).
| option | description |
|---|---|
| (none) | Default scope: ruff format check + pylint (+ pytest tests/ --ignore=tests/long when re-enabled) |
--all-tests | Full suite including tests/long/; matches CI on main/dev |
--coverage | Adds --cov=rattlesnake --cov-report=term-missing to the pytest run (no effect while pytest is disabled) |
--tag TAG | Validates TAG before pushing a release: checks current branch is main or dev, that the tag conforms to PEP 440, and that it is strictly newer than all existing tags. Runs before lint and tests. |
--docs | Builds the Jupyter Book with --strict; matches the docs_jupyter_book CI job. Requires network access to api.mystmd.org. |
--no-sync | Skips uv sync (useful when offline or behind a firewall) |
--skip-network-check | Skips the initial PyPI connectivity check |
--force | Continues even if the network or sync checks fail |
Examples¶
uv run preflight # default scope
uv run preflight --all-tests # full suite
uv run preflight --coverage # default scope + coverage report
uv run preflight --all-tests --coverage # full suite + coverage report
uv run preflight --tag v1.0.0rc1 # validate tag, then default scope
uv run preflight --tag v1.0.0 --all-tests # validate tag, then full suite
uv run preflight --docs # build Jupyter Book
uv run preflight --no-sync # skip dependency sync
uv run preflight --force # continue past network/sync failures
uv run preflight --skip-network-check # skip initial PyPI connectivity checkTrusted Publishing¶
In release.yml we have removed the manual -p ${{ secrets.PYPI_TOKEN }}. The industry standard is now Trusted Publishing (also called OpenID Connect or OIDC). You configure this in your PyPI project settings once, and GitHub Actions authenticates securely without you needing to store and rotate secrets.
OpenID Connect (OIDC) provides a flexible, credential-free mechanism for delegating publishing authority for a PyPI package to a trusted third party service, like GitHub Actions. PyPI users and projects can use trusted publishers to automate their release processes, without needing to use API tokens or passwords.
To configure Trusted Publishing, you tell PyPI, “Trust any code from this specific GitHub repository and workflow.” This removes the need to manage long-lived API tokens or passwords in your secrets.
Steps:
In
release.yml, the environment must be set to eitherpypiortestpypidepending on the version string. Hence the logic inrelease.yml:
environment: ${{ (contains(github.ref, 'rc') || contains(github.ref, 'dev')) && 'testpypi' || 'pypi' }} # If the tag contains 'rc' or 'dev', use the 'testpypi' environment, otherwise use 'pypi'The GitHub repository itself must have both a pypi and a testpypi environment:
On the GitHub repo:
Click on the Settings tab (usually the last tab on the right in the top navigation bar).
On the left-hand sidebar, look for the Environments link (it’s under the “Code and automation” section).
If the environment doesn’t exist yet:
Click the New environment button.
Name the environment
pypi(and then make a second item calledtestpypi) and click Configure environment.
If it does exist but is named differently, you can click on it to rename it or delete it and create a new one.
For a basic setup using Trusted Publishing, you don’t actually need to add any secrets or configuration on this page. Just having the environment named testpypi exist is enough to link it to your workflow.
Optionally, we add the following protections:
Under the Deployment branches and tags, under the No Restriction button, select Selected branches and tags.
Click Add deployment branch or tag rule.
Select Ref type: Tag.
Set the Name Pattern: to ‘v*’. This ensures that only version tags can ever use this environment, adding a layer of security.
Finally, the PyPI (respectively, Test PyPI) site needs to be configured.
Go to your project’s Manage page (or your account’s Publishing settings if you are setting it up for the first time.)
Look for the Publishing tab
Click Add new publisher
Select GitHub as the source
Enter the following details:
Owner: sandialabs
Repository name: rattlesnake-vibration-controller
Workflow name:
release.yml(This must match your filename in your.github/workflows/directory)Environment name: You can leave this blank or name it
pypi(if you use it in your YAML). We usedpypifor live publishing to the PyPI site, andtestpypifor test publishing to the TestPyPI site.
Click the Add button
Tags and Semantic Versioning¶
We follow PEP 440 (the Python standard for versioning), which requires version strings to follow this specific structure:
N.N.N[{a|b|rc}N][.postN][.devN]The validate_tag job in release.yml enforces that a tag can be added only when the
branch is main or dev, that the tag follows PEP 440, and that the version is
strictly newer than all existing tags.
Example Tags¶
Following are prerelease tags:
| tag | description |
|---|---|
v1.1.0a1 | The first alpha for version 1.1.0 |
v1.1.0b2 | The second beta for version 1.1.0 |
v1.1.0rc1 | The first release candidate for version 1.1.0 |
A release candidate is made during the final testing stage before a full release.
Following are stable release tags (e.g., starting from the v1.0.0 release):
| tag | description |
|---|---|
v1.0.1 | Patch Release: Backwards-compatible bug fixes |
v1.1.0 | Minor Release: New features that are backwards-compatible |
v2.0.0 | Major Release: Significant changes or breaking API updates |
Following are Development and Post-Release tags:
| tag | description |
|---|---|
v1.1.0.dev1 | A version currently under development |
v1.0.0.post1 | Fix a minor error in the release process, such as a fix of a typo in the documentation, without changing the code |
Release on Tag¶
Following is an example of creating a release with a tag.
Create a Prerelease¶
To create a prerelease on TestPyPI:
On the
devbranch, create a tag and then push, e.g.,
# Ensure you are on the dev branch
git checkout dev
git pull
# View existing tags, if any
git tag
# Create the new tag, e.g.,
git tag -a v1.0.0rc1 -m "Test of prerelease version 1.0.0, release candidate 1"
# Push the tag to GitHub
git push origin v1.0.0rc1Create a Release¶
To create a release on PyPI:
Merge the
devbranch into themainbranch.On the
mainbranch, create a tag usinggit tagand push it to themainbranch on GitHub, e.g.,
# Ensure you are on the main branch
git checkout main
git pull
# View existing tags, if any
git tag
# Create the new tag, e.g.,
git tag -a v1.0.0 -m "Release version 1.0.0"
# On the main branch, push the tag to GitHub
git push origin v1.0.0Manual Approval Gate¶
By default, a tag push triggers the full release pipeline automatically — including the final publish to PyPI — with no human checkpoint. The manual approval gate pauses the publish job and requires a named reviewer to explicitly approve before the package is uploaded to PyPI.
This is an industry-standard safeguard for production releases. It gives a release manager a final opportunity to confirm that the correct tag is being published, the changelog looks right, and no last-minute issues have been flagged.
The approval gate applies only to the production pypi environment. The testpypi environment (used for prereleases) does not require approval, since prereleases are low-risk by design.
Setup (GitHub Settings UI)¶
No changes to release.yml are required. The publish job dynamically selects environment: pypi for stable releases or environment: testpypi for prereleases — GitHub uses this environment name as the hook to enforce the approval rule.
Navigate to the repository on GitHub.
Click the Settings tab.
In the left sidebar under Code and automation, click Environments.
Click on the pypi environment.
Under Deployment protection rules, check the box next to Required reviewers.
In the text field that appears, type the GitHub username(s) or team name(s) who are authorized to approve a PyPI release. Add up to 6 reviewers.
Click Save protection rules.
When a release tag is pushed, the pipeline will run validate_tag, test, build, and github-release automatically. The publish job will then pause with status Waiting. The designated reviewer(s) will receive a GitHub notification and must click Review deployments → Approve and deploy before the package is uploaded to PyPI.
If no reviewer approves within 30 days, the deployment times out and must be re-triggered.