Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Contributing

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

Forking

Getting the Source Code

Collaborators and team members should clone the repository:

git clone git@github.com:sandialabs/rattlesnake-vibration-controller.git

Others 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 .venv

Activating the environment tells your shell to use the Python interpreter and pip packages located inside the .venv folder.

macOS / Linux:

source .venv/bin/activate

Windows (PowerShell):

.venv\Scripts\Activate.ps1

Windows (Command Prompt), DOS

.venv\Scripts\activate.bat

Once 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 python

The output should point to a path inside your project’s .venv folder.

To exit the virtual environment and return to the global system stack:

deactivate

Best Practice: Never commit the .venv directory to version control. Add .venv/ to your .gitignore file.

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 --strict

This 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 start

The 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.

  1. 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}
}
  1. Configure myst.yml

The bibliography is configured in documentation/myst.yml under the project.bibliography section:

project:
  bibliography:
    - book/bibliography.bib
  1. Add in-text citations

In a markdown file, use the cite role to reference an entry by its key:

{cite} knuth1986computer

  1. 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 build

Continuous Integration/Continuous Deployment (CI/CD)

Synopsis

ci.yml — Continuous Integration

Triggered on every push to any branch, plus manual dispatch. Six jobs:

  1. changes

    • Uses dorny/paths-filter to detect whether docs or code files changed. Downstream jobs use this to skip unnecessary work.

  2. pytest_matrix

    • Runs tests on all combinations of [macos, ubuntu, windows] × [3.11, 3.12] using pip install .[dev] (not uv) for PyQt wheel compatibility. Test scope is adaptive:

      • Default: tests/*.py tests/short

      • Full suite triggered by: commit message containing [all tests], manual dispatch with test_level=full, or branch is main or dev

  3. lint

    • Runs pylint src/rattlesnake via uv, captures output, then calls report_lint.py to generate an HTML lint report artifact.

  4. coverage

    • Runs pytest --cov via uv with the same adaptive test scope, then calls report_coverage.py to generate an HTML coverage report artifact.

  5. docs_jupyter_book

    • Updates myst.yml metadata via report_jupyter_book.py, then builds the Jupyter Book. Only runs when docs changed (or on main/dev).

  6. deploy

    • Runs only on main/dev, assembles all artifacts into a pages/ 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:

  1. validate_tag

    • Verifies the tag was created on the main or dev branch, that it conforms to PEP 440, and that it is strictly newer than all existing tags.

  2. test

    • Calls ci.yml as a reusable workflow (workflow_call).

  3. build

    • Runs uv build and generates a Supply chain Levels for Software Artifacts (SLSA, aka “salsa”) provenance attestation for the dist artifacts.

  4. github-release

    • Creates a GitHub Release with auto-generated notes and attaches the dist files.

  5. publish

    • Publishes to PyPI or TestPyPI via Trusted Publishing. Tags containing rc or dev go 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.

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.

CI/CD workflow execution for documentation-only changes.

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.py

Only 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,

Full suite of CI/CD jobs triggered for main or dev branch updates.

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:

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 preflight

Modes 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).

optiondescription
(none)Default scope: ruff format check + pylint (+ pytest tests/ --ignore=tests/long when re-enabled)
--all-testsFull suite including tests/long/; matches CI on main/dev
--coverageAdds --cov=rattlesnake --cov-report=term-missing to the pytest run (no effect while pytest is disabled)
--tag TAGValidates 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.
--docsBuilds the Jupyter Book with --strict; matches the docs_jupyter_book CI job. Requires network access to api.mystmd.org.
--no-syncSkips uv sync (useful when offline or behind a firewall)
--skip-network-checkSkips the initial PyPI connectivity check
--forceContinues 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 check

Trusted 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:

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:

Finally, the PyPI (respectively, Test PyPI) site needs to be configured.

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:

tagdescription
v1.1.0a1The first alpha for version 1.1.0
v1.1.0b2The second beta for version 1.1.0
v1.1.0rc1The 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):

tagdescription
v1.0.1Patch Release: Backwards-compatible bug fixes
v1.1.0Minor Release: New features that are backwards-compatible
v2.0.0Major Release: Significant changes or breaking API updates

Following are Development and Post-Release tags:

tagdescription
v1.1.0.dev1A version currently under development
v1.0.0.post1Fix 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:

# 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.0rc1

Create a Release

To create a release on PyPI:

# 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.0

Manual 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.

  1. Navigate to the repository on GitHub.

  2. Click the Settings tab.

  3. In the left sidebar under Code and automation, click Environments.

  4. Click on the pypi environment.

  5. Under Deployment protection rules, check the box next to Required reviewers.

  6. 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.

  7. 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.