Developer Guide¶
This guide provides comprehensive information for developers contributing to Fit File Faker, including architecture, testing, and release processes.
Getting Started with Development¶
Prerequisites¶
- Python 3.12 or higher
- uv (preferred) or pip
- Git
Development Setup¶
Clone the repository and install dependencies:
Pre-commit Hooks¶
The project uses pre-commit to run code quality checks before committing. After setting up your development environment:
This automatically runs the following checks:
ruff checkandruff format: Code linting and formatting on staged filesgitlint: Validates commit messages follow Conventional Commits format
Run hooks manually on all files:
Common Development Commands¶
# Show help
fit-file-faker -h
# Interactive profile management menu
fit-file-faker --config-menu
# Show directories used for configuration and cache
fit-file-faker --show-dirs
# Edit a single FIT file
fit-file-faker path/to/file.fit
# Edit and upload to Garmin Connect
fit-file-faker -u path/to/file.fit
# Upload all new files in configured directory
fit-file-faker -ua
# Monitor directory for new files
fit-file-faker -m
# Dry run (no changes or uploads)
fit-file-faker -d path/to/file.fit
Linting¶
Build and Distribution¶
Release strategy¶
Releases are done built and pushed to PyPI automatically by the GitHub action in .github/workflows/publish_and_release.yml, which is triggered whenever a tag is pushed to the repository.
Architecture Overview¶
Package Structure¶
The application is organized as a modular Python package (fit_file_faker/) with ~1,800 total lines across six files:
fit_file_faker/
├── __init__.py # Package initialization
├── app.py # Main application, CLI, uploads, monitoring (550 lines)
├── app_registry.py # NEW: Trainer app detection system (305 lines)
├── config.py # Configuration management (750 lines)
├── fit_editor.py # FIT file editing core logic (313 lines)
└── utils.py # Utility functions and monkey patches (103 lines)
Entry Point: fit_file_faker.app:run (defined in pyproject.toml)
Design Philosophy
The modular structure improves maintainability while keeping the codebase compact:
- Separation of concerns: config, editing, upload, utilities, app detection
- Easier testing: Each module can be tested independently
- Extensible architecture: New trainer apps can be added via
app_registry.py - Backward compatibility: Legacy single-profile configs auto-migrate
- Clear boundaries between functionality
- Still simple to understand and contribute to
Core Workflow¶
The tool follows a six-step process:
- Read FIT file: Uses the
fit_toollibrary to parse binary FIT files - Apply fit_tool patch: Applies monkey patch from
utils.pyto handle malformed FIT files (e.g., COROS) - Identify device messages: Locates
FileIdMessage,FileCreatorMessage, andDeviceInfoMessagerecords -
Rewrite manufacturer/product IDs: Changes manufacturer codes from:
DEVELOPMENT(255)ZWIFTWAHOO_FITNESSPEAKSWAREHAMMERHEADCOROSMYWHOOSH(331)
to
GARMIN(1) with Edge 830 product ID (3122) -
Rebuild FIT file: Uses
FitFileBuilderto reconstruct the file with modified messages - Upload (optional): Authenticates to Garmin Connect via
garthlibrary and uploads the modified file
Module Breakdown¶
config.py - Configuration Management (Multi-Profile Architecture)¶
Core Data Structures:
AppTypeenum:TP_VIRTUAL,ZWIFT,MYWHOOSH,CUSTOMfor trainer app typesProfiledataclass: Individual profile configurationname: Unique profile identifierapp_type: Trainer app type (AppType enum)garmin_username: Garmin account usernamegarmin_password: Garmin account passwordfitfiles_path: Path to FIT files directory
Configdataclass: Multi-profile containerprofiles: List of Profile objectsdefault_profile: Optional default profile name
Configuration Management:
ConfigManagerclass: Handles config file I/O, validation, and auto-migration_load_config(): Loads or creates configuration, auto-migrates legacy formatsave_config(): Persists configuration to diskis_valid(): Validates configuration completenessmigrate_legacy_config(): Converts v1.2.4 single-profile to multi-profile format
ProfileManagerclass: CRUD operations for profile managementcreate_profile(): Create new profile with validationget_profile(): Retrieve profile by nameupdate_profile(): Modify existing profiledelete_profile(): Remove profile with safety checksset_default_profile(): Set default profilelist_profiles(): Get all profiles
TUI Components:
display_profiles_table(): Rich table display of profilesinteractive_menu(): Questionary-based menu system- Profile creation wizard: App-first flow (select app → auto-detect → credentials → name)
- Profile edit wizard: Field-specific editing
- Profile deletion wizard: Confirmation with safety checks
- Set default profile wizard: Interactive selection
Utilities:
get_garth_dir(profile_name): Profile-specific credential isolationPathEncoder: Custom JSON encoder for Path and Enum objects- Stored in platform-specific user config directory (via
platformdirs) as.config.json - Auto-detection via
app_registry.pyfor TPV, Zwift, MyWhoosh directories
Supplemental Device Registry:
The tool maintains a curated list of modern Garmin devices (2019-2026) to supplement the outdated device list in the fit_tool library.
-
GarminDeviceInfodataclass: Metadata for each devicename: Human-readable name (e.g., "Edge 1050")product_id: FIT file product ID integercategory: Device category (bike_computer,multisport_watch,trainer)year_released: Release year for sortingis_common: Flag for two-level menu filteringdescription: Brief description for UI displaysoftware_version: Latest stable firmware version (integer format, e.g., 2922 = v29.22)software_date: Latest firmware release date (YYYY-MM-DD format)
-
SUPPLEMENTAL_GARMIN_DEVICES: Registry containing 43 modern devices- Common devices (
is_common=True): 11 popular devices shown in first-level menu- Bike computers: Edge 1050, 1040, 840, 830, 540, 530
- Multisport watches: Fenix 8 47mm, Fenix 7, Epix Gen 2, Forerunner 965, 955
- All devices: Complete catalog accessible via "View all devices" option
- Edge series (9 models)
- Fenix/Epix series (18 models)
- Forerunner series (12 models)
- Tacx Training App (4 variants)
- Common devices (
-
get_supported_garmin_devices(show_all: bool = False): Device list generator- Returns 3-tuples:
(name, product_id, description) - Merges
fit_toolenum with supplemental registry (supplemental takes priority) - Filters by
is_commonwhenshow_all=False(default) - Sorting: common devices first, then by year (newest first), then alphabetically
- Returns 3-tuples:
Device Selection UI:
The profile creation and editing wizards use a two-level menu system:
-
Level 1: Common devices grouped by category with visual separators
- Shows only 11 curated devices for reduced cognitive load
- Includes "View all devices" and "Custom (enter numeric ID)" options
-
Level 2: Full device catalog (70+ devices)
- Accessed via "View all devices" option
- Categorized and sorted by release year
- "Back to common devices" navigation option
Device Reference Data:
Device metadata is maintained in docs/reference/FitSDK_21.188.00_device_ids.csv:
Name,Value,Comment,Software Version,Software Date
edge_1050,4440,,2922,2025-11-04
fenix8,4536,,2029,2026-01-14
fr965,4315,,2709,2026-01-15
edge_830,3122,,975,2023-03-22
Column definitions:
- Name: Device name (lowercase with underscores)
- Value: Product ID (integer used in FIT files)
- Comment: Optional notes (usually empty)
- Software Version: Firmware version in FIT format (integer, last 2 digits are decimals)
- Software Date: Firmware release date (YYYY-MM-DD)
Firmware Version Format:
FIT files store firmware versions as integers where the last two digits represent decimal places:
v29.22→2922v9.75→975v27.09→2709
Firmware Data Source:
Firmware versions are sourced from gpsinformation.net, a comprehensive database of Garmin device firmware releases.
- URL pattern:
http://gpsinformation.net/allory/test/garfeat_<device>.htm - Example: Edge 1050 firmware history
- Data extracted: Latest stable (non-beta) firmware version and release date
- SSL note: The site uses an expired certificate, use
curl -kto bypass SSL verification
Extraction scripts (for maintenance):
# Extract firmware versions from gpsinformation.net
./extract_firmware_versions.sh
# Update CSV with extracted data
python3 update_firmware_csv.py
Adding New Devices:
To add support for a new Garmin device:
- Find product ID: Check the FIT SDK or examine a FIT file from the device
- Extract firmware data: Visit
http://gpsinformation.net/allory/test/garfeat_<device>.htm - Add to supplemental registry in
config.py: - Update CSV file: Add entry to
docs/reference/FitSDK_21.188.00_device_ids.csv - Add tests: Update
tests/test_config.pywith new device validation
Updating Firmware Versions:
Firmware versions should be periodically updated to reflect latest stable releases:
- Run
./extract_firmware_versions.shto fetch latest versions - Review output and update
update_firmware_csv.pywith new data - Run
python3 update_firmware_csv.pyto update the CSV - Manually update corresponding entries in
SUPPLEMENTAL_GARMIN_DEVICES - Verify with tests:
python3 run_tests.py tests/test_config.py
Backward Compatibility:
- Existing profiles with numeric device IDs continue to work unchanged
Profile.get_device_name()prioritizesfit_toolenum, then falls back to supplemental registry- Unknown device IDs display as
UNKNOWN (<id>) - Custom device IDs can still be entered manually via profile wizard
fit_editor.py - FIT File Editing¶
FitEditorclass: Main editor with logging filter for fit_tool warningsedit_fit(): Main function that reads, modifies, and saves FIT filesrewrite_file_id_message(): Converts FileIdMessage to Garmin Edge 830 formatstrip_unknown_fields(): Handles unknown field definitions to prevent file corruption_should_modify_manufacturer(): Determines if manufacturer should be changed_should_modify_device_info(): Determines if device info should be changedget_date_from_fit(): Extracts creation date from FIT fileprint_message(): Debug output for FIT messages
FitFileLogFilter: Custom logging filter to suppress noisy fit_tool warnings- Device info messages are rewritten to Garmin Edge 830
- Activity data is always preserved (records, laps, sessions) - only modifies device metadata
- Special handling for Activity messages (reordered to end for COROS compatibility)
app_registry.py - Trainer App Detection System¶
AppDetector ABC: Abstract base class defining the detector interface
get_display_name(): Human-readable app name for UIget_default_path(): Platform-specific FIT files directory detectionvalidate_path(): Path validation for the specific app
Concrete Detectors:
TPVDetector: TrainingPeaks Virtual directory detection- macOS:
~/TPVirtual/<user_id>/FITFiles - Windows:
~/Documents/TPVirtual/<user_id>/FITFiles - Linux: User prompt (no standard path)
- Uses
TPV_DATA_PATHenvironment variable override
- macOS:
ZwiftDetector: Zwift activities directory detection- macOS:
~/Documents/Zwift/Activities/ - Windows:
%USERPROFILE%\Documents\Zwift\Activities\ - Linux: Wine/Proton path detection
- macOS:
MyWhooshDetector: MyWhoosh data directory detection- macOS: Epic container path scanning
- Windows: AppData package directory scanning
- Linux: User prompt (not officially supported)
CustomDetector: Manual path specification for unsupported apps
Registry System:
APP_REGISTRY: Dictionary mappingAppType→ detector classget_detector(app_type): Factory function for detector instances- Extensible design: Add new apps by implementing AppDetector and registering
app.py - Main Application¶
- CLI argument parsing and validation (using
argparse) - Multi-Profile Support: New CLI arguments
--profile/-p: Use specific profile for operation--list-profiles: Display all configured profiles--config-menu: Launch interactive profile management
select_profile(): Profile selection logic (arg → default → prompt)upload(): Garmin Connect upload with OAuth authentication viagarth(now acceptsProfileparameter)- Handles authentication and credential prompting
- Caches credentials in profile-specific
.garth_{profile_name}directories - Gracefully handles HTTP 409 conflicts (duplicate activities)
upload_all(): Batch processes all FIT files in a directory (profile-aware)- Maintains
.uploaded_files.jsonto track processed files - Creates temporary files for uploads (discarded after upload)
- Maintains
monitor(): Watches directory for new FIT files usingwatchdog(profile-specific)NewFileEventHandler: Event handler class for monitoring mode (uses profile)- 5-second delay after file creation to ensure write completion
- Automatically processes and uploads new files
- Rich console output with colored logs and tracebacks
utils.py - Utility Functions¶
apply_fit_tool_patch(): Monkey patches fit_tool to handle malformed FIT files_lenient_get_length_from_size(): Lenient field size validation (truncates instead of raising)fit_crc_get16(): FIT file CRC-16 checksum calculation- Required for COROS and other manufacturers with non-standard FIT files
Supported Source Platforms¶
The tool recognizes and modifies FIT files from:
| Platform | Manufacturer Code | Notes |
|---|---|---|
| TrainingPeaks Virtual | DEVELOPMENT or PEAKSWARE | Formerly indieVelo |
| Zwift | ZWIFT | Popular virtual cycling platform |
| Wahoo devices | WAHOO_FITNESS | Wahoo bike computers |
| Hammerhead Karoo | HAMMERHEAD | Karoo bike computers |
| MyWhoosh | 331 | Not in fit_tool's enum |
| COROS | COROS | Requires fit_tool patch for malformed fields |
Logging and Output¶
- Uses
richlibrary for formatted console output (configured inapp.py) RichHandlerfor colored, timestamped logs with traceback support- Custom
FitFileLogFilterinfit_editor.pyto suppress fit_tool's "actual:" warnings - Debug mode (
-v) provides detailed message-by-message processing logs - Separate log level configuration for different modules (urllib3, oauth1_auth, watchdog, asyncio, etc.)
📚 Extensibility¶
Adding New Trainer Apps¶
The architecture is designed to be extensible. To add support for a new trainer app:
- Add enum value: Add to
AppTypeenum inconfig.py - Create detector: Implement
AppDetectorsubclass inapp_registry.py - Register detector: Add to
APP_REGISTRYdictionary - Done!: App automatically appears in creation menu
Example: Adding Rouvy Support¶
# 1. Add to AppType enum
class AppType(str, Enum):
ROUVY = "rouvy"
# 2. Create detector class
class RouvyDetector(AppDetector):
def get_display_name(self) -> str:
return "Rouvy"
def get_default_path(self) -> Path | None:
# Implement platform-specific detection
pass
def validate_path(self, path: Path) -> bool:
# Implement path validation
pass
# 3. Register in APP_REGISTRY
APP_REGISTRY = {
# ... existing entries
AppType.ROUVY: RouvyDetector,
}
Important Implementation Notes¶
FIT File Structure¶
Critical Information
FIT files contain a series of messages (records). Each data message must be preceded by a definition message.
When rewriting messages, always write:
then the message itself.FitFileBuilder(auto_define=True) handles definition messages automatically when add() is called.
Device Simulation¶
The tool emulates Garmin devices by rewriting manufacturer and product IDs in FIT files. The specific device can be configured per-profile.
Default device (if not configured):
- Manufacturer: 1 (
GARMIN) - Product: 3122 (
EDGE_830) - Software version: 975 (v9.75 in FIT format)
- Hardware version: 255
- Serial number: Auto-generated random 10-digit number instead of real Unit ID
Supported devices: 70+ devices from the supplemental registry and fit_tool library, including:
- Modern bike computers (Edge 1050, 1040, 840, 540, etc.)
- Multisport watches (Fenix 8, Fenix 7, Epix Gen 2, etc.)
- Running watches (Forerunner 965, 955, 265, 255, etc.)
- Training apps (Tacx Training App variants)
Custom device IDs: Users can enter any numeric device ID manually during profile configuration.
Serial Numbers and Garmin Connect Recognition¶
Critical: Device Serial Numbers Must be a valid Unit ID for a given Device Type
For Garmin Connect to correctly recognize an activity as coming from a specific device, both the device product ID and serial number must represent a valid Garmin device. The serial number stored in the FIT file must be a valid Unit ID for the device. This affects:
- Training Effect calculations: VO2 Max, Training Load, Recovery Time
- Training Status: Productive, Maintaining, Peaking, etc.
- Challenges and Badges: Activity may not count toward goals
- Device attribution: Incorrect device shown in activity details
The mapping of serial number/Unit ID ranges to device models is proprietary Garmin information and not publicly documented.
Serial Number Behavior:
- Auto-generated (default): Random 10-digit integer (1,000,000,000 to 4,294,967,295)
- May not be recognized as valid by Garmin Connect
-
Activities will upload but advanced features may not work correctly
-
User-provided (recommended for full functionality): During profile creation/editing, users can:
- Enter their actual Garmin device's Unit ID as the serial number
- This ensures full Garmin Connect integration
-
The Unit ID must match the selected device model for proper recognition
-
Validation: The tool validates serial numbers are valid
uint32zformat but cannot validate device-specific ranges
Best Practice: If users own a Garmin device and want full Garmin Connect features, they should: - Configure their profile to use the same device model as their physical device - Enter their actual device's Unit ID as the serial number (found in Settings → About → Copyright Info → Unit ID) - This guarantees proper activity recognition and feature availability
File Naming Convention¶
Modified files are saved as {original_stem}_modified.fit unless uploading in batch mode (which uses temp files).
Platform Detection¶
The tool auto-detects TrainingPeaks Virtual user directories on:
- macOS:
~/TPVirtual - Windows:
~/Documents/TPVirtual - Linux: Prompts user for path (no auto-detection)
Override with TPV_DATA_PATH environment variable.
Testing¶
Quick Start¶
Test Suite Overview¶
The test suite includes 53+ tests with 100% code coverage for all major functionality.
Test File Structure¶
tests/
├── conftest.py # Shared fixtures and test configuration
├── test_fit_editor.py # FIT editing tests (32 tests)
├── test_config.py # Configuration tests (55 tests)
├── test_app_registry.py # NEW: App registry and detector tests (28 tests)
├── test_app.py # Application and upload tests (30 tests)
├── test_utils.py # Utility function tests
└── files/ # Test FIT files from various platforms
├── tpv_20250111.fit
├── tpv_20251120.fit
├── zwift_20250401.fit
├── mywhoosh_20260111.fit
├── karoo_20251119.fit
└── coros_20251118.fit
Test Isolation¶
All tests should be completely isolated from a real environment:
- ✅ Config directories redirected to temporary locations
- ✅ Cache directories use temp space
- ✅ No network calls (all external services mocked)
- ✅ Automatic cleanup after each test
- ✅ Safe to run in parallel
The isolate_config_dirs autouse fixture in conftest.py ensures that no test ever touches:
- Your real Garmin credentials
- Your actual FIT files
- Your user configuration directory
- Your system cache
Test Fixtures and Helpers¶
The test suite uses shared fixtures in conftest.py to reduce duplication:
Shared Mock Classes¶
MockQuestion: Mock for questionary interactive promptsMockGarthHTTPError: Configurable HTTP error mock with status codesMockGarthException: Standard Garth exception for auth flow testing
Shared Fixtures¶
mock_garth_basic: Basic Garmin Connect mock for successful operationsmock_garth_with_login: Garmin mock requiring authenticationisolate_config_dirs(autouse): Automatically isolates all tests from real user directoriestemp_dir: Creates temporary directories for test outputsmock_config_file: Creates mock configuration files
Mocking Strategy¶
External Services¶
- Garmin Connect (
garth): Mocked usingsys.modulespatching with shared fixtures - User prompts (
questionary): Mocked usingMockQuestionhelper class - File system (
platformdirs): Automatically redirected to temp directories viaisolate_config_dirs
Why sys.modules for garth?
The garth library is imported inside functions (lazy import), so we use patch.dict('sys.modules') to inject mock modules that get imported at runtime.
Running Tests¶
Using the Helper Script¶
# Basic usage
python3 run_tests.py
# With coverage
python3 run_tests.py --coverage
# HTML coverage report
python3 run_tests.py --html
# Verbose output
python3 run_tests.py -v
# Specific test file
python3 run_tests.py tests/test_fit_editor.py
# Specific test
python3 run_tests.py tests/test_fit_editor.py::TestFitEditor::test_edit_tpv_fit_file
# Combined options
python3 run_tests.py --coverage --html -v
Using pytest Directly¶
# Run all tests
uv run pytest tests/
# With coverage
uv run pytest tests/ --cov=fit_file_faker --cov-report=html
# Verbose
uv run pytest tests/ -v
Continuous Integration¶
The test suite runs automatically on GitHub Actions for:
- Python versions: 3.12, 3.13, 3.14
- Operating systems: Ubuntu, macOS, Windows
- Triggers: Push to main/develop/refactor branches, pull requests
Workflow file: .github/workflows/test.yml
Coverage reports are uploaded to Codecov on successful Ubuntu + Python 3.12 runs.
Adding New Tests¶
When adding support for a new platform:
- Add the FIT file to
tests/files/(sanitize it of any personally identifiable information) - Create a fixture in
conftest.py: - Add a test in
test_fit_editor.py:def test_edit_new_platform_fit_file(self, fit_editor, new_platform_fit_file, temp_dir): output_file = temp_dir / "new_platform_modified.fit" result = fit_editor.edit_fit(new_platform_fit_file, output=output_file) assert result == output_file assert output_file.exists() # Verify modifications modified_fit = FitFile.from_file(str(output_file)) for record in modified_fit.records: if isinstance(record.message, FileIdMessage): assert record.message.manufacturer == Manufacturer.GARMIN.value
Test Best Practices¶
- Always use fixtures for test data (don't hardcode paths)
- Mock external services (no real network calls)
- Use temp directories for output files
- Test both success and failure paths
- Keep tests independent (no shared state)
- Use descriptive test names that explain what's being tested
- Verify behavior, not implementation (test outcomes, not internals)
Contributing¶
We welcome contributions! Here's how to get started:
Contribution Workflow¶
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature-name - Make your changes
- Run tests:
python3 run_tests.py --coverage - Run linting:
ruff check . && ruff format . - Commit your changes following conventional commits
- Push to your fork:
git push origin feature/your-feature-name - Open a Pull Request
Commit Message Format¶
We use Conventional Commits for automatic changelog generation. Commit messages are automatically validated by gitlint through pre-commit hooks.
Required format:
Allowed types:
feat:New featuresminor-feat:New minor featuresfix:Bug fixesdocs:Documentation changestest:Test additions or modificationsrefactor:Code refactoringchore:Maintenance tasksci:CI/CD changesbuild:Build system changesperf:Performance improvementsstyle:Code style changesrevert:Revert previous commits
Example:
feat: add support for COROS FIT files
Add manufacturer code recognition and device ID modification for COROS
devices to enable Garmin Connect Training Effect calculations.
Commit Message Validation
The pre-commit hook will reject commits that don't follow the conventional commits format. The hook configuration is in .gitlint and validates:
- Type is one of the allowed types
- Format follows
type(optional-scope): description - Title is ≤100 characters
- Body is optional (not required)
Code Style¶
- Follow PEP 8
- Use
rufffor formatting and linting - Maximum line length: 100 characters (configured in
pyproject.toml) - Use type hints where appropriate
Pull Request Guidelines¶
- Provide a clear description of the changes
- Reference any related issues
- Ensure all tests pass
- Maintain 100% code coverage
- Update documentation if needed
Release Process¶
Releases are automated via .github/workflows/publish_and_release.yml:
- All pushes build the package and publish to TestPyPI
- Tag pushes (e.g.,
v1.2.3) trigger PyPI publication and GitHub Release creation - Version is defined in
pyproject.tomland must be manually updated before tagging
To Release a New Version¶
Option 1: Using the release script (Recommended)
The release.sh script automates the entire release process:
# Interactive release with custom message
./release.sh 2.0.1 "Fix changelog generation and dependencies"
# Quick release with default message
./release.sh 2.0.1
The script will: 1. Validate version format and git status 2. Update version in pyproject.toml 3. Commit the version change 4. Create an annotated git tag 5. Push to origin (with confirmation prompts)
Option 2: Manual release
-
Update version in
pyproject.toml: -
Commit the version change:
-
Create and push a git tag with a detailed message:
git tag v1.2.5 -m "Release v1.2.5: Add support for new platforms This release includes support for MyWhoosh and COROS devices, along with improved error handling and comprehensive test coverage." git push origin main git push origin v1.2.5Tag Messages in Changelog
Detailed tag messages (using the
-mflag) will be rendered in the auto-generated changelog and GitHub Release notes. Use this to provide release highlights, breaking changes, or upgrade instructions that won't fit in individual commit messages.
Automated steps (handled by GitHub Actions):
After pushing the tag, GitHub Actions will automatically: - Build package - Publish to PyPI - Create GitHub Release - Generate changelog - Deploy documentation
Version Numbering¶
We follow Semantic Versioning:
- MAJOR (1.x.x): Breaking changes
- MINOR (x.1.x): New features (backwards-compatible)
- PATCH (x.x.1): Bug fixes (backwards-compatible)
Documentation¶
The project has a comprehensive documentation site built with MkDocs Material and hosted on GitHub Pages.
Documentation Site¶
- URL: https://jat255.github.io/Fit-File-Faker/
- Framework: MkDocs with Material theme
- Deployment: Automated via GitHub Actions to
gh-pagesbranch - Changelog: Auto-generated from git commits using git-cliff
Documentation Structure¶
docs/
├── index.md # Home page (user guide, from README.md)
├── developer-guide.md # Developer guide (this file)
├── changelog.md # Auto-generated changelog
└── assets/ # Images, custom CSS, and other assets
Building Documentation Locally¶
Prerequisites¶
Install documentation dependencies:
# Using uv (recommended)
uv sync --group docs
# Using pip
pip install mkdocs mkdocs-material mkdocs-minify-plugin
Local Development¶
Option 1: Using the build script (Recommended)
The build_docs.sh script generates the complete changelog (including historical releases) and builds/serves the documentation, mirroring the CI/CD workflow:
# Build static documentation with changelog
./build_docs.sh
# Serve with live reload for development
./build_docs.sh serve
# Opens at http://127.0.0.1:8000
# Changes to docs/ files will automatically reload the browser
Option 2: Using mkdocs directly
# Serve documentation locally with live reload
uv run mkdocs serve
# Opens at http://127.0.0.1:8000
# Changes to docs/ files will automatically reload the browser
Changelog Generation
When using mkdocs serve directly, the changelog won't be regenerated. Use ./build_docs.sh serve to include the latest changelog in your local preview.
Build Static Site¶
# Using the build script (includes changelog generation)
./build_docs.sh
# Or build directly with mkdocs (without changelog update)
mkdocs build
# The generated site can be found in the site/ directory
Deploy to GitHub Pages¶
Manual Deployment
Manual deployment via mkdocs gh-deploy is rarely needed since documentation is automatically deployed by GitHub Actions. Only use this if the automated deployment fails.
Documentation Automation¶
Documentation automatically rebuilds and deploys in two scenarios:
1. Documentation Changes¶
When changes are pushed to main branch that affect: - docs/** (any documentation files) - mkdocs.yml (MkDocs configuration) - pyproject.toml (contains git-cliff changelog generation config)
Workflow: .github/workflows/docs.yml
This workflow:
- Checks out the repository
- Sets up Python and installs dependencies
- Builds the documentation with
mkdocs build - Deploys to GitHub Pages (
gh-pagesbranch)
2. Release Process¶
After a new release is created (when a tag like v1.2.5 is pushed):
Workflow: .github/workflows/publish_and_release.yml
This workflow:
- Builds and publishes the package to PyPI
- Creates a GitHub Release
- Generates the changelog using
git-cliff - Deploys updated documentation
Changelog Generation¶
The changelog is automatically generated using git-cliff based on conventional commit messages.
Configuration¶
Changelog generation is configured in pyproject.toml under the [tool.git-cliff.*] sections:
- Commit parsers: Categorize commits by type (feat, fix, docs, etc.)
- Format: Markdown with links to commits and releases
- Sections: Features, Bug Fixes, Documentation, etc.
- Skipped tags: Old releases (v1.0.0 - v1.2.4) that predate conventional commits
Historical Releases¶
The project adopted conventional commits starting with v1.3.0. Earlier releases (v1.0.0 - v1.2.4) don't follow this format, so they are handled specially:
Configuration in pyproject.toml:
[tool.git-cliff.git]
# Skip old tags that predate conventional commits
skip_tags = "v0.0.1-beta.1|v1.0.0|v1.0.1|v1.0.2|v1.0.3|v1.1.0|v1.1.1|v1.2.0|v1.2.1|v1.2.2|v1.2.3|v1.2.4"
Workflow integration:
- Git-cliff generates the changelog from conventional commits only (v1.3.0+)
- The historical changelog from
docs/.changelog_pre_1.3.0.mdis appended - Result: Complete changelog with both new and legacy releases
This approach: - ✅ Keeps the generated changelog clean with conventional commits - ✅ Preserves historical release information - ✅ Works automatically in both CI/CD and local builds (./build_docs.sh)
Conventional Commits¶
For commits to appear in the changelog, they must follow the Conventional Commits format:
Types:
feat:- New featuresminor-feat:- New minor featuresfix:- Bug fixesdocs:- Documentation changestest:- Test additions or modificationsrefactor:- Code refactoringchore:- Maintenance tasksci:- CI/CD changesbuild:- Build system changes
Example:
feat: add support for COROS FIT files
Add lenient field size validation to handle malformed FIT files from
COROS devices. This enables Training Effect calculations for COROS
activities uploaded to Garmin Connect.
Manual Changelog Generation¶
To generate the changelog locally (for testing):
# Install git-cliff
cargo install git-cliff
# or
brew install git-cliff
# Generate changelog (reads config from pyproject.toml automatically)
git cliff --output docs/changelog.md
# Generate changelog for specific version range
git cliff --output docs/changelog.md v1.3.0..HEAD
Github API limits
git cliff will make requests to the Github API to get information about various bits of information, and without authentication, the API limit is very low, so you may see errors such as:
thread 'main' (37956221) panicked at git-cliff-core/src/changelog.rs:558:18:
Could not get github metadata: HttpClientError(reqwest::Error { kind: Status(403, None), url: "https://api.github.com/repos/jat255/Fit-File-Faker/commits?per_page=100&page=0&sha=v1.2.3" })
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
GITHUB_TOKEN environment variable, or to do this dynamically with the Github CLI, you can run a command such as $ gh auth token | GITHUB_TOKEN=$(cat) git cliff v1.0.2..v1.2.4 Documentation Best Practices¶
When contributing to documentation:
- Write in Markdown: Use standard Markdown with MkDocs Material extensions
- Use admonitions: Highlight important information with note/warning/tip boxes
-
Code blocks: Always specify language for syntax highlighting
will render as: -
Link to code: Use relative links for internal documentation
- Test locally: Always run
mkdocs serveto preview changes - Keep synchronized: Ensure README.md and docs/index.md stay in sync for the user guide
MkDocs Configuration¶
The site is configured in mkdocs.yml:
site_name: FIT File Faker
theme:
name: material
# ... theme configuration
nav:
- Home: index.md
- Developer Guide: developer-guide.md
- API Reference: api.md
- Changelog: changelog.md
plugins:
- search # Built-in search
- minify: # Minify HTML/CSS/JS
...
- autorefs # cross reference support
- mkdocstrings: # auto generation of API docs
...
Key features:
- Material theme: Modern, responsive design
- Search: Built-in search functionality
- Minification: Optimized HTML/CSS/JS output
- Code highlighting: Syntax highlighting for all code blocks
- Navigation: Organized sidebar navigation
Troubleshooting Documentation¶
Local build fails¶
# Ensure dependencies are installed
uv sync --group docs
# Clear MkDocs cache
rm -rf site/
# Rebuild
mkdocs build
Changes not appearing on GitHub Pages¶
- Check the GitHub Actions workflow status
- Ensure the
gh-pagesbranch exists - Verify GitHub Pages is enabled in repository settings
- Wait a few minutes for deployment to propagate
Changelog not updating¶
- Ensure commits follow conventional commit format
- Check git-cliff configuration in
pyproject.tomlunder[tool.git-cliff.*]sections - Verify the release workflow completed successfully
Resources¶
- GitHub Repository: jat255/Fit-File-Faker
- PyPI Package: fit-file-faker
- Issue Tracker: GitHub Issues
- pytest Documentation: https://docs.pytest.org/
- GitHub Actions:
.github/workflows/
Getting Help¶
If you need help:
- Check the documentation
- Search existing issues
- Create a new issue
Note
As this is a side-project provided for free, support times may vary 😅.