92 lines
6.7 KiB
Markdown
92 lines
6.7 KiB
Markdown
# Runtime Versions Feature
|
|
|
|
**Date**: 2026-02-26
|
|
**Scope**: Data model, repositories, version matching, pack loader, API, core pack definitions
|
|
|
|
## Summary
|
|
|
|
Added support for multiple versions of the same runtime (e.g., Python 3.11, 3.12, 3.13 or Node.js 18, 20, 22). Actions and sensors can now declare a semver version constraint (e.g., `>=3.12`, `~18.0`, `>=3.12,<4.0`) to specify which runtime version they require. The system selects the best matching available version at execution time.
|
|
|
|
The design is fully data-driven — no hard-coded handling for any specific runtime. All version detection, constraint matching, and execution configuration is driven by database records and YAML definitions.
|
|
|
|
## Changes
|
|
|
|
### New Migration: `20260226000000_runtime_versions.sql`
|
|
- **`runtime_version` table**: Stores version-specific execution configurations per runtime
|
|
- `runtime` (FK → runtime.id), `runtime_ref`, `version` (semver string)
|
|
- `version_major`, `version_minor`, `version_patch` (ints for efficient range queries)
|
|
- `execution_config` JSONB — complete standalone config (not a diff) replacing the parent runtime's config when this version is selected
|
|
- `distributions` JSONB — version-specific verification commands
|
|
- `is_default` (at most one per runtime), `available`, `verified_at`
|
|
- `meta` JSONB for arbitrary metadata (EOL dates, LTS codenames, etc.)
|
|
- Unique constraint on `(runtime, version)`
|
|
- **`action.runtime_version_constraint`** — new nullable TEXT column for semver constraints
|
|
- **`sensor.runtime_version_constraint`** — same as above
|
|
|
|
### New Module: `crates/common/src/version_matching.rs`
|
|
- `parse_version()` — lenient semver parsing (`"3.12"` → `3.12.0`, `"v20.11"` → `20.11.0`)
|
|
- `parse_constraint()` — constraint parsing with bare version support (`"3.12"` → `~3.12`)
|
|
- `matches_constraint()` — check if a version satisfies a constraint
|
|
- `select_best_version()` — pick the highest available version matching a constraint, with default-version preference when no constraint is specified
|
|
- `extract_version_components()` — split version string into (major, minor, patch) for DB columns
|
|
- 33 unit tests covering all constraint operators, edge cases, and selection logic
|
|
|
|
### New Repository: `crates/common/src/repositories/runtime_version.rs`
|
|
- Full CRUD (FindById, List, Create, Update, Delete)
|
|
- `find_by_runtime()` — all versions for a runtime, ordered newest-first
|
|
- `find_by_runtime_ref()` — same, by runtime ref string
|
|
- `find_available_by_runtime()` — only available versions
|
|
- `find_default_by_runtime()` — the default version
|
|
- `find_by_runtime_and_version()` — exact version lookup
|
|
- `clear_default_for_runtime()` — helper for changing defaults
|
|
- `set_availability()` — mark available/unavailable with timestamp
|
|
- `delete_by_runtime()` — bulk delete
|
|
|
|
### Model Changes
|
|
- **`RuntimeVersion`** struct in `models::runtime` module with `parsed_execution_config()` method
|
|
- **`Action`** — added `runtime_version_constraint: Option<String>`
|
|
- **`Sensor`** — added `runtime_version_constraint: Option<String>`
|
|
|
|
### Pack Loader Updates (`crates/common/src/pack_registry/loader.rs`)
|
|
- `load_runtimes()` now calls `load_runtime_versions()` after creating each runtime
|
|
- `load_runtime_versions()` parses the `versions` array from runtime YAML and creates `runtime_version` rows
|
|
- `load_actions()` reads `runtime_version` from action YAML → stored as `runtime_version_constraint`
|
|
- `load_sensors()` reads `runtime_version` from sensor YAML → same
|
|
|
|
### Repository Updates
|
|
- **Action repository**: All SELECT/INSERT/UPDATE/RETURNING queries updated to include `runtime_version_constraint`
|
|
- **Sensor repository**: Same — all queries updated
|
|
- **Input structs**: `CreateActionInput`, `UpdateActionInput`, `CreateSensorInput`, `UpdateSensorInput` all include the new field
|
|
|
|
### API Updates
|
|
- **Action DTOs**: `CreateActionRequest`, `UpdateActionRequest`, `ActionResponse`, `ActionSummary` include `runtime_version_constraint`
|
|
- **Route handlers**: `create_action`, `update_action` pass the field through
|
|
- OpenAPI annotations added for the new field
|
|
|
|
### Core Pack Runtime YAML Updates
|
|
- **`packs/core/runtimes/python.yaml`**: Added `versions` array with Python 3.11, 3.12 (default), 3.13 — each with version-specific interpreter binary (`python3.11`, `python3.12`, `python3.13`), venv commands, and verification patterns
|
|
- **`packs/core/runtimes/nodejs.yaml`**: Added `versions` array with Node.js 18, 20 (default), 22 — each with version-specific binary, verification commands, and LTS metadata
|
|
|
|
### Dependency Addition
|
|
- **`semver` 1.0** (with serde feature) added to workspace and `attune-common` Cargo.toml
|
|
|
|
### Test Fixes
|
|
- All callsites constructing `CreateActionInput`, `CreateSensorInput`, `UpdateActionInput`, `UpdateSensorInput` across the workspace updated with `runtime_version_constraint: None` (approximately 20 files touched)
|
|
|
|
## Architecture Decisions
|
|
|
|
1. **Full execution_config per version, not diffs**: Each `runtime_version` row stores a complete `execution_config` rather than overrides. This avoids merge complexity and makes each version self-contained.
|
|
|
|
2. **Constraint stored as text, matched at runtime**: The `runtime_version_constraint` column stores the raw semver string. Matching is done in Rust code using the `semver` crate rather than in SQL, because semver range logic is complex and better handled in application code.
|
|
|
|
3. **Bare version = tilde range**: A constraint like `"3.12"` is interpreted as `~3.12` (>=3.12.0, <3.13.0), which is the most common developer expectation for "compatible with 3.12".
|
|
|
|
4. **No hard-coded runtime handling**: The entire version system is data-driven through the `runtime_version` table and YAML definitions. Any runtime can define versions with arbitrary verification commands and execution configs.
|
|
|
|
## What's Next
|
|
|
|
- **Worker integration**: The worker execution pipeline should query `runtime_version` rows when an action has a `runtime_version_constraint`, use `select_best_version()` to pick the right version, and use that version's `execution_config` instead of the parent runtime's.
|
|
- **Runtime version detection**: The `RuntimeDetector` should verify individual version availability by running each version's verification commands and updating the `available`/`verified_at` fields.
|
|
- **Environment isolation per version**: The `runtime_envs_dir` path pattern may need to include the version (e.g., `{runtime_envs_dir}/{pack_ref}/{runtime_name}-{version}`) to support multiple Python versions with separate virtualenvs.
|
|
- **API endpoints**: CRUD endpoints for `runtime_version` management (list versions for a runtime, register new versions, mark availability).
|
|
- **Web UI**: Display version information in runtime/action views, version constraint field in action editor. |