Contributing
Thank you for your interest in contributing to Anvil! This document provides guidelines and instructions for contributing to the project.
Code of Conduct
This project follows the Contributor Covenant Code of Conduct. Please be respectful and constructive in all interactions.
How to Contribute
Reporting Bugs
Before reporting a bug:
- Check existing issues to avoid duplicates
- Gather relevant information:
- Anvil version (
anvil --version) - Windows version
- Steps to reproduce
- Expected vs actual behavior
- Verbose output (
anvil -vvv <command>)
- Anvil version (
Create a bug report with this template:
## Environment
- Anvil version:
- Windows version:
- PowerShell version:
## Description
Brief description of the bug.
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Verbose Output
(paste -vvv output here)
## Workload (if applicable)
```yaml
(paste workload.yaml here)
### Suggesting Features
1. Check existing issues and discussions for similar suggestions
2. Open a feature request issue with:
- Clear description of the feature
- Use case and motivation
- Proposed implementation (if you have ideas)
### Submitting Code
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests (`cargo test`)
5. Run lints (`cargo clippy`)
6. Format code (`cargo fmt`)
7. Commit with a descriptive message
8. Push to your fork
9. Open a Pull Request
---
## Development Setup
### Prerequisites
- **Rust**: 1.75 or later ([rustup.rs](https://rustup.rs/))
- **Windows**: Visual Studio Build Tools (for linking)
- **Windows Package Manager (winget)**: For package operations (integration tests)
- **Linux/macOS**: Standard build toolchain (gcc/clang)
### Building
```sh
# Clone your fork
git clone https://github.com/YOUR_USERNAME/anvil.git
cd anvil
# Build debug version
cargo build
# Build release version
cargo build --release
# Run tests
cargo test
# Run clippy lints
cargo clippy --all-targets --all-features -- -D warnings
# Format code
cargo fmt
# Run with debug output
cargo run -- -vvv list
Running Tests
# Run all tests
cargo test
# Run unit tests only
cargo test --bin anvil
# Run integration tests only
cargo test --test cli_tests
# Run a specific test
cargo test test_name
# Run tests with output
cargo test -- --nocapture
Project Structure
anvil/
├── src/
│ ├── main.rs # Entry point
│ ├── cli/ # Command line interface
│ │ ├── mod.rs # CLI definitions (clap)
│ │ ├── commands.rs # Command argument structs
│ │ ├── completions.rs # Shell completions
│ │ ├── output.rs # Output handling
│ │ ├── progress.rs # Progress indicators
│ │ └── formats/ # Output formatters
│ │ ├── mod.rs
│ │ ├── table.rs # Table formatter
│ │ ├── json.rs # JSON formatter
│ │ ├── yaml.rs # YAML formatter
│ │ └── html.rs # HTML report generator
│ ├── config/ # Configuration handling
│ │ ├── mod.rs
│ │ ├── schema.rs # Workload schema validation
│ │ ├── workload.rs # Workload parsing
│ │ ├── inheritance.rs # Inheritance resolution
│ │ └── global.rs # Global configuration
│ ├── assertions/ # Assertion evaluation engine
│ │ └── mod.rs
│ ├── commands/ # Inline command execution
│ │ └── mod.rs
│ ├── conditions/ # Condition/predicate engine
│ │ └── mod.rs
│ ├── operations/ # Command implementations
│ │ ├── mod.rs
│ │ ├── install.rs # Install command
│ │ ├── health.rs # Health check command
│ │ ├── list.rs # List command
│ │ ├── show.rs # Show command
│ │ ├── validate.rs # Validate command
│ │ ├── init.rs # Init command
│ │ ├── status.rs # Status command
│ │ ├── backup.rs # Backup command
│ │ └── config.rs # Config command
│ ├── providers/ # External integrations
│ │ ├── mod.rs
│ │ ├── winget.rs # Winget package manager
│ │ ├── filesystem.rs # File operations
│ │ ├── script.rs # Script execution
│ │ ├── template.rs # Template processing
│ │ └── backup.rs # Backup provider
│ └── state/ # State management
│ ├── mod.rs
│ ├── installation.rs # Installation state
│ ├── cache.rs # Cache management
│ └── files.rs # File state tracking
├── examples/ # Example workloads
│ ├── minimal/
│ ├── rust-developer/
│ └── python-developer/
├── tests/ # Integration tests
│ ├── cli_tests.rs
│ └── common/
│ └── mod.rs
└── docs/ # Documentation
└── src/
├── SUMMARY.md
├── introduction.md
├── user-guide.md
├── workload-authoring.md
├── troubleshooting.md
├── specification.md
├── architecture.md
├── changelog.md
└── contributing.md
Key Modules
- cli: Handles command-line parsing with clap and output formatting
- config: Parses workload YAML files and handles inheritance
- assertions: Evaluates named assertions for health reporting
- commands: Executes inline commands with timeout and elevation support
- conditions: Composable predicate engine for system state checks
- operations: Implements each CLI command's business logic
- providers: Interfaces with external systems (winget, filesystem, PowerShell)
- state: Tracks installation state and manages caching
Coding Standards
Rust Style
- Follow standard Rust conventions and idioms
- Use
rustfmtfor formatting (default settings) - Address all
clippywarnings - Use
thiserrorfor error types - Use
anyhowfor error propagation in application code
Code Organization
- Keep functions focused and small
- Use descriptive names for functions and variables
- Add doc comments for public APIs
- Use modules to organize related functionality
Error Handling
#![allow(unused)] fn main() { // Define custom errors with thiserror #[derive(Debug, thiserror::Error)] pub enum WorkloadError { #[error("Workload '{name}' not found")] NotFound { name: String }, #[error("Invalid workload schema: {message}")] InvalidSchema { message: String }, } // Use anyhow for propagation pub fn load_workload(name: &str) -> anyhow::Result<Workload> { // ... } }
Documentation
#![allow(unused)] fn main() { /// Brief description of the function. /// /// More detailed description if needed. /// /// # Arguments /// /// * `name` - The workload name to load /// /// # Returns /// /// The loaded workload or an error /// /// # Errors /// /// Returns an error if the workload is not found /// /// # Examples /// /// ``` /// let workload = load_workload("my-workload")?; /// ``` pub fn load_workload(name: &str) -> Result<Workload> { // ... } }
Commit Messages
- Use present tense ("Add feature" not "Added feature")
- Use imperative mood ("Fix bug" not "Fixes bug")
- Keep the first line under 72 characters
- Reference issues when applicable
Examples:
Add shell completions for Fish
Implement Fish shell completion generation using clap_complete.
Closes #42
Fix file backup path on Windows
Handle UNC paths correctly when creating backups.
Fixes #55
Testing
Unit Tests
Add unit tests in the same file as the code being tested:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_function_name() { // Arrange let input = "test"; // Act let result = function_under_test(input); // Assert assert_eq!(result, expected); } } }
Integration Tests
Add integration tests in tests/cli_tests.rs:
#![allow(unused)] fn main() { #[test] fn new_command_works() { anvil() .args(["new-command", "arg"]) .assert() .success() .stdout(predicate::str::contains("expected output")); } }
Test Guidelines
- Each test should be independent
- Use descriptive test names
- Test both success and failure cases
- Use
tempfile::TempDirfor file operations - Don't rely on external system state where avoidable
Submitting Changes
Pull Request Process
- Update documentation if needed
- Add tests for new features
- Ensure CI passes (format, lint, test, build)
- Write a clear PR description explaining:
- What the change does
- Why it's needed
- How to test it
- Request review from maintainers
- Address feedback promptly
- Squash commits if requested
PR Title Format
feat: Add new featurefix: Fix specific bugdocs: Update documentationtest: Add testsrefactor: Restructure codechore: Update dependencies
Creating Workloads
Contributions of new bundled workloads are welcome! See the Workload Authoring guide for details.
Workload Guidelines
- Include meaningful health checks - Verify the workload achieves its purpose
- Document the workload purpose - Clear description and comments
- Test on clean Windows installation - Ensure it works from scratch
- Use inheritance for common bases (extend
essentialsif appropriate) - Keep packages minimal - Only include what's necessary
- Validate before submitting -
anvil validate your-workload --strict
Workload Structure
workload-name/
├── workload.yaml # Required: workload definition
├── files/ # Optional: configuration files
└── scripts/ # Optional: setup/health scripts
Example
name: my-workload
version: "1.0.0"
description: "Brief description of what this workload provides"
extends:
- essentials # If applicable
packages:
winget:
- id: Package.ID
commands:
post_install:
- run: "echo Setting up..."
description: "Install and configure"
assertions:
- name: "Tool is available"
check:
type: command_exists
command: my-tool
Releasing
Releases are automated via the scripts/release.ps1 script and GitHub Actions.
Cutting a release
# Preview what will happen
./scripts/release.ps1 minor -DryRun
# Bump version, stamp changelog, commit, tag, and push
./scripts/release.ps1 minor -Push
# Patch release (0.6.0 → 0.6.1)
./scripts/release.ps1 patch -Push
# Major release (0.6.0 → 1.0.0)
./scripts/release.ps1 major -Push
The script validates (clean tree, main branch, tests pass, clippy clean) before making any changes. On push, the release workflow automatically:
- Builds binaries for 5 platforms (Linux x86_64/aarch64, macOS x86_64/aarch64, Windows x86_64)
- Creates a GitHub Release with changelog notes and SHA256 checksums
- Publishes to crates.io
Required secrets
| Secret | Where to create | Used by |
|---|---|---|
CARGO_REGISTRY_TOKEN | crates.io/settings/tokens | cargo publish |
The GITHUB_TOKEN is provided automatically by GitHub Actions.
Setting up crates.io publishing (one-time)
- Log in at crates.io with your GitHub account
- Go to Account Settings → API Tokens
- Click New Token
- Name:
anvil-release(or any descriptive name) - Scopes: select publish-update (allows publishing new versions of existing crates)
- Click Create
- Copy the token (it's shown only once)
- In the GitHub repo, go to Settings → Secrets and variables → Actions
- Click New repository secret
- Name:
CARGO_REGISTRY_TOKEN, Value: paste the token - Click Add secret
For the first publish, you must also run cargo publish locally once to claim the crate name on crates.io.
License
By contributing to Anvil, you agree that your contributions will be licensed under the MIT or Apache-2.0 license, at the user's choice.
Questions?
- Open a Discussion
- Check the Documentation
- Review existing Issues
Thank you for contributing to Anvil!