Thank you for considering contributing to the Foothold Checkpoint Tool! This document provides guidelines and instructions for developers.
- Development Environment
- Project Structure
- Coding Standards
- Testing
- Git Workflow
- Pull Request Process
- Development Guidelines
- Python: 3.10 or higher
- Poetry: Dependency management and packaging
- Git: Version control
- Windows: PowerShell environment
- VS Code: Recommended IDE
# Install Poetry (if not already installed)
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python -# Clone the repository
git clone https://github.com/VEAF/VEAF-foothold-checkpoint-tool.git
cd VEAF-foothold-checkpoint-tool
# Install dependencies with Poetry
poetry install
# Activate the virtual environment
poetry shell
# Verify installation
python -c "import foothold_checkpoint; print(foothold_checkpoint.__version__)"Recommended extensions:
- Python (Microsoft)
- Pylance
- Ruff
- Black Formatter
- Python Test Explorer
Configure VS Code to use the Poetry virtualenv:
- Open Command Palette (
Ctrl+Shift+P) - Select "Python: Select Interpreter"
- Choose the Poetry virtualenv (usually
foothold-checkpoint-xxx-py3.x)
This project uses Typer 0.9.x for the CLI, which has a critical compatibility issue with Click 8.3.x.
Required: Click must be pinned to version 8.1.7 in pyproject.toml:
[tool.poetry.dependencies]
click = "8.1.7" # DO NOT upgrade to 8.3.xIssue: Click 8.3.x introduces a breaking change where Parameter.make_metavar() requires a ctx argument. This causes a TypeError when Typer renders help text with Annotated type hints:
TypeError: Parameter.make_metavar() missing 1 required positional argument: 'ctx'
Symptoms:
--helpflag crashes with TypeError- CLI commands fail to display usage information
- Help rendering fails when using
typer.Option()withAnnotatedsyntax
Resolution: Keep Click at 8.1.7 until Typer releases a version compatible with Click 8.3.x.
Testing: Always verify --help works after any dependency updates:
poetry run foothold-checkpoint --helpVEAF-foothold-checkpoint-tool/
├── src/
│ └── foothold_checkpoint/ # Main package
│ ├── __init__.py # Package metadata
│ ├── cli.py # CLI entry point (Typer)
│ └── core/ # Business logic
│ ├── __init__.py
│ ├── config.py # Configuration management (Pydantic)
│ ├── campaign.py # Campaign detection & normalization
│ ├── checkpoint.py # Checkpoint metadata
│ └── storage.py # Save/restore/list/delete/import
├── tests/ # Test suite
│ ├── __init__.py
│ ├── test_config.py
│ ├── test_campaign.py
│ ├── test_checkpoint.py
│ ├── test_storage.py
│ ├── test_cli.py
│ └── data/foothold/ # Test data fixtures
├── openspec/ # Design artifacts
│ └── changes/foothold-checkpoint-tool/
│ ├── proposal.md
│ ├── design.md
│ ├── specs/
│ └── tasks.md
├── pyproject.toml # Poetry config & tool settings
├── poetry.lock # Locked dependencies
├── config.yaml.example # Example configuration
├── README.md # Project overview
├── USERS.md # User guide
├── CONTRIBUTING.md # This file
└── CHANGELOG.md # Change history (Keep a Changelog)
- Code: English only (functions, variables, classes, comments, docstrings)
- Communication: French with team members
- Documentation: English
- Line length: 100 characters (Black/Ruff enforced)
- Type hints: Mandatory for all public functions
- Docstrings: Required for public modules, classes, and functions
All code must pass these checks before commit:
# Format code with Black
poetry run black src/ tests/
# Lint with Ruff
poetry run ruff check src/ tests/
# Type checking with mypy
poetry run mypy src/
# Run all checks together
poetry run black src/ tests/ && poetry run ruff check src/ tests/ && poetry run mypy src/Before committing code:
- ✅ Format with Black
- ✅ Lint with Ruff (no errors)
- ✅ Type check with mypy (no errors)
- ✅ All tests pass
- ✅ No VS Code errors/warnings
- ✅ Update CHANGELOG.md
CRITICAL: Tests MUST be written BEFORE implementation.
- Write the test (it should fail)
- Implement minimal code to pass the test
- Refactor if needed
- Repeat
# Step 1: Write failing test
# tests/test_campaign.py
def test_normalize_campaign_name_removes_version():
"""Campaign name normalization should remove version suffixes."""
from foothold_checkpoint.core.campaign import normalize_campaign_name
assert normalize_campaign_name("FootHold_CA_v0.2") == "CA"
assert normalize_campaign_name("Germany_Modern_V0.1") == "Germany_Modern"
# Step 2: Run test (should fail)
# poetry run pytest tests/test_campaign.py::test_normalize_campaign_name_removes_version
# Step 3: Implement
# src/foothold_checkpoint/core/campaign.py
def normalize_campaign_name(filename: str) -> str:
"""Remove version suffixes from campaign filenames."""
import re
# Remove _v0.2, _V0.1, _0.1 patterns
return re.sub(r'_[vV]?\d+\.\d+$', '', filename)
# Step 4: Run test (should pass)
# Step 5: Refactor if needed# Run all tests
poetry run pytest
# Run specific test file
poetry run pytest tests/test_campaign.py
# Run specific test
poetry run pytest tests/test_campaign.py::test_normalize_campaign_name_removes_version
# Run with coverage
poetry run pytest --cov=foothold_checkpoint --cov-report=html
# View coverage report
start htmlcov/index.html# tests/test_example.py
"""Tests for example module."""
import pytest
from foothold_checkpoint.core.example import ExampleClass
class TestExampleClass:
"""Test suite for ExampleClass."""
def test_basic_functionality(self):
"""Test basic functionality works as expected."""
instance = ExampleClass()
assert instance.method() == expected_value
def test_error_handling(self):
"""Test that errors are handled correctly."""
instance = ExampleClass()
with pytest.raises(ValueError, match="expected error message"):
instance.method_with_error()
@pytest.fixture
def sample_data(self):
"""Fixture providing sample test data."""
return {"key": "value"}
def test_with_fixture(self, sample_data):
"""Test using a fixture."""
assert sample_data["key"] == "value"- Core modules: 100% coverage (
src/foothold_checkpoint/core/) - CLI: Integration tests for major workflows
- Overall target: ≥90%
We use the GitFlow branching model:
main (production releases)
└── develop (development branch)
├── feature/feature-name (feature branches)
├── bugfix/bug-description (bug fixes)
└── hotfix/critical-fix (urgent production fixes)
- Feature branches:
feature/descriptive-name - Bug fixes:
bugfix/issue-description - Hotfixes:
hotfix/critical-issue
- Create feature branch from develop
git checkout develop
git pull origin develop
git checkout -b feature/my-feature-
Make changes with TDD
- Write tests first
- Implement code
- Ensure all quality checks pass
-
Commit with conventional commits
git add .
git commit -m "feat: add campaign detection logic
- Implement file pattern matching
- Add version suffix normalization
- Add tests for all scenarios
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>"- Push and create Pull Request
git push -u origin feature/my-featureThen create a PR on GitHub targeting develop.
Follow Conventional Commits:
<type>: <short description>
<detailed description>
<footer>
Types:
feat:New featurefix:Bug fixdocs:Documentation onlystyle:Code style (formatting, no logic change)refactor:Code restructuringtest:Adding or updating testschore:Maintenance tasks
Example:
feat: add checkpoint import functionality
- Implement directory scanning for campaign files
- Add auto-detection of campaigns from file patterns
- Generate metadata with current timestamp
- Compute SHA-256 checksums for integrity
Closes #42
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- ✅ All tests pass:
poetry run pytest - ✅ Code formatted:
poetry run black src/ tests/ - ✅ Linting clean:
poetry run ruff check src/ tests/ - ✅ Type checks pass:
poetry run mypy src/ - ✅ CHANGELOG.md updated
- ✅ Branch rebased on latest
develop
- Descriptive title (conventional commit format)
- Description explains what/why/how
- Tests included (TDD approach documented)
- Documentation updated if needed
- CHANGELOG.md entry added
- No merge conflicts with
develop - CI checks passing (when available)
- Automated checks run (linting, tests, type checking)
- Code review by maintainer(s)
- Approval required before merge
- Squash and merge to develop
-
Code: Always English
- Function names, variables, classes
- Comments, docstrings
- Test names and messages
-
Team Communication: French
- Pull request discussions
- Issue comments
- Team meetings
All public APIs must be documented:
def save_checkpoint(
server: str,
campaign: str,
name: str | None = None,
comment: str | None = None,
) -> Path:
"""Save a checkpoint for the specified campaign.
Creates a timestamped ZIP archive containing all campaign files,
Foothold_Ranks.lua, and metadata.json with SHA-256 checksums.
Args:
server: Server name from configuration
campaign: Campaign identifier (normalized)
name: Optional checkpoint name
comment: Optional checkpoint description
Returns:
Path to the created checkpoint ZIP file
Raises:
ServerNotFoundError: If server not in configuration
CampaignNotFoundError: If campaign files not detected
CheckpointCreationError: If ZIP creation fails
Example:
>>> checkpoint = save_checkpoint(
... server="production-1",
... campaign="afghanistan",
... name="Before Mission 5"
... )
>>> print(checkpoint.name)
afghanistan_2024-02-13_14-30-00.zip
"""- Use specific exception types
- Provide helpful error messages
- Include context in error messages
# Good
if server not in config.servers:
raise ServerNotFoundError(
f"Server '{server}' not found in configuration. "
f"Available servers: {', '.join(config.servers.keys())}"
)
# Bad
if server not in config.servers:
raise ValueError("Invalid server")Use Pydantic for configuration and data validation:
from pydantic import BaseModel, Field
from pathlib import Path
class ServerConfig(BaseModel):
"""Configuration for a DCS server."""
path: Path = Field(..., description="Path to server Missions/Saves directory")
description: str = Field(..., description="Human-readable server description")
class Config:
frozen = True # Immutable after creationAlways use type hints:
# Good
def process_files(files: list[Path], output_dir: Path) -> dict[str, str]:
"""Process files and return checksums."""
checksums: dict[str, str] = {}
for file in files:
checksums[file.name] = compute_checksum(file)
return checksums
# Bad
def process_files(files, output_dir):
checksums = {}
for file in files:
checksums[file.name] = compute_checksum(file)
return checksumsWe follow Semantic Versioning:
- Major (x.0.0): Breaking changes
- Minor (1.x.0): New features (backward compatible)
- Patch (1.0.x): Bug fixes
-
Update CHANGELOG.md
- Move changes from
[Unreleased]to new version section - Add release date:
## [1.1.0] - 2026-02-15 - Add version comparison link at bottom
- Move changes from
-
Update RELEASE_NOTES.md
- Important: Add new version notes at the TOP of the file
- Keep previous versions for historical reference
- DO NOT create separate
RELEASE_NOTES_x.x.x.mdfiles - Structure:
# Release Notes ## Version 1.1.0 - February 15, 2026 [New release content here] --- ## Version 1.0.1 - February 14, 2026 [Previous release content] --- ## Version 1.0.0 - February 14, 2026 [Initial release content]
-
Create Git tag
git tag -a v1.1.0 -m "Version 1.1.0 - Brief description Major changes overview. See CHANGELOG.md for details."
-
Push tag
git push origin v1.1.0 -
Create GitHub Release
- Use tag
v1.1.0 - Title:
v1.1.0 - Release Name - Copy content from RELEASE_NOTES.md for this version
- Mark as pre-release if needed
- Use tag
- User-focused: Write for end users, not developers
- Highlights first: Most important changes at the top
- Breaking changes: Clearly marked with
⚠️ - Migration guides: Link to detailed migration docs if needed
- Credits: Acknowledge contributors
- Questions: Open a GitHub Discussion
- Bugs: Open a GitHub Issue
- Security: Email contact@veaf.org
By contributing, you agree that your contributions will be licensed under the same license as the project (TBD).