122 lines
6.7 KiB
Markdown
122 lines
6.7 KiB
Markdown
# Artifact Content System Implementation
|
|
|
|
**Date:** 2026-03-02
|
|
**Scope:** Database migration, models, repository, API routes, DTOs
|
|
|
|
## Summary
|
|
|
|
Implemented a full artifact content management system that allows actions to create, update, and manage artifact files and progress-style artifacts through the API. This builds on the existing `artifact` table (which previously only stored metadata) by adding content storage, versioning, and progress-append semantics.
|
|
|
|
## Changes
|
|
|
|
### Database Migration (`migrations/20250101000010_artifact_content.sql`)
|
|
|
|
- **Enhanced `artifact` table** with new columns:
|
|
- `name` (TEXT) — human-readable artifact name
|
|
- `description` (TEXT) — optional description
|
|
- `content_type` (TEXT) — MIME type
|
|
- `size_bytes` (BIGINT) — size of latest version content
|
|
- `execution` (BIGINT, no FK) — links artifact to the execution that produced it
|
|
- `data` (JSONB) — structured data for progress-type artifacts and metadata
|
|
- **Created `artifact_version` table** for immutable content snapshots:
|
|
- `artifact` (FK to artifact, CASCADE delete)
|
|
- `version` (INTEGER, 1-based, monotonically increasing)
|
|
- `content` (BYTEA) — binary file content
|
|
- `content_json` (JSONB) — structured JSON content
|
|
- `meta` (JSONB) — free-form metadata per version
|
|
- `created_by` (TEXT) — who created this version
|
|
- Unique constraint on `(artifact, version)`
|
|
- **Helper function** `next_artifact_version()` — auto-assigns next version number
|
|
- **Retention trigger** `enforce_artifact_retention()` — auto-deletes oldest versions when count exceeds the artifact's retention limit; also syncs `size_bytes` and `content_type` back to the parent artifact
|
|
|
|
### Models (`crates/common/src/models.rs`)
|
|
|
|
- Enhanced `Artifact` struct with new fields: `name`, `description`, `content_type`, `size_bytes`, `execution`, `data`
|
|
- Added `SELECT_COLUMNS` constant for consistent query column lists
|
|
- Added `ArtifactVersion` model with `SELECT_COLUMNS` (excludes binary content for performance) and `SELECT_COLUMNS_WITH_CONTENT` (includes BYTEA payload)
|
|
- Added `ToSchema` derive to `RetentionPolicyType` enum (was missing, needed for OpenAPI)
|
|
- Added re-exports for `Artifact` and `ArtifactVersion` in models module
|
|
|
|
### Repository (`crates/common/src/repositories/artifact.rs`)
|
|
|
|
- Updated all `ArtifactRepository` queries to use `SELECT_COLUMNS` constant
|
|
- Extended `CreateArtifactInput` and `UpdateArtifactInput` with new fields
|
|
- Added `ArtifactSearchFilters` and `ArtifactSearchResult` for paginated search
|
|
- Added `search()` method with filters for scope, owner, type, execution, name
|
|
- Added `find_by_execution()` for listing artifacts by execution ID
|
|
- Added `append_progress()` — atomic JSON array append for progress artifacts
|
|
- Added `set_data()` — replace full data payload
|
|
- Used macro `push_field!` to DRY up the dynamic UPDATE query builder
|
|
- Created `ArtifactVersionRepository` with methods:
|
|
- `find_by_id` / `find_by_id_with_content`
|
|
- `list_by_artifact`
|
|
- `find_latest` / `find_latest_with_content`
|
|
- `find_by_version` / `find_by_version_with_content`
|
|
- `create` (auto-assigns version number via `next_artifact_version()`)
|
|
- `delete` / `delete_all_for_artifact` / `count_by_artifact`
|
|
|
|
### API DTOs (`crates/api/src/dto/artifact.rs`)
|
|
|
|
- `CreateArtifactRequest` — with defaults for retention policy (versions) and limit (5)
|
|
- `UpdateArtifactRequest` — partial update fields
|
|
- `AppendProgressRequest` — single JSON entry to append
|
|
- `SetDataRequest` — full data replacement
|
|
- `ArtifactResponse` / `ArtifactSummary` — full and list response types
|
|
- `CreateVersionJsonRequest` — JSON content for a new version
|
|
- `ArtifactVersionResponse` / `ArtifactVersionSummary` — version response types
|
|
- `ArtifactQueryParams` — filters with pagination
|
|
- Conversion `From` impls for all model → DTO conversions
|
|
|
|
### API Routes (`crates/api/src/routes/artifacts.rs`)
|
|
|
|
Endpoints mounted under `/api/v1/`:
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/artifacts` | List artifacts with filters and pagination |
|
|
| POST | `/artifacts` | Create a new artifact |
|
|
| GET | `/artifacts/{id}` | Get artifact by ID |
|
|
| PUT | `/artifacts/{id}` | Update artifact metadata |
|
|
| DELETE | `/artifacts/{id}` | Delete artifact (cascades to versions) |
|
|
| GET | `/artifacts/ref/{ref}` | Get artifact by reference string |
|
|
| POST | `/artifacts/{id}/progress` | Append entry to progress artifact |
|
|
| PUT | `/artifacts/{id}/data` | Set/replace artifact data |
|
|
| GET | `/artifacts/{id}/download` | Download latest version content |
|
|
| GET | `/artifacts/{id}/versions` | List all versions |
|
|
| POST | `/artifacts/{id}/versions` | Create JSON content version |
|
|
| GET | `/artifacts/{id}/versions/latest` | Get latest version metadata |
|
|
| POST | `/artifacts/{id}/versions/upload` | Upload binary file (multipart) |
|
|
| GET | `/artifacts/{id}/versions/{version}` | Get version metadata |
|
|
| DELETE | `/artifacts/{id}/versions/{version}` | Delete a version |
|
|
| GET | `/artifacts/{id}/versions/{version}/download` | Download version content |
|
|
| GET | `/executions/{execution_id}/artifacts` | List artifacts for execution |
|
|
|
|
- File upload via multipart/form-data with 50 MB limit
|
|
- Content type auto-detection from multipart headers with explicit override
|
|
- Download endpoints serve binary with proper Content-Type and Content-Disposition headers
|
|
- All endpoints require authentication (`RequireAuth`)
|
|
|
|
### Wiring
|
|
|
|
- Added `axum` `multipart` feature to API crate's Cargo.toml
|
|
- Registered artifact routes in `routes/mod.rs` and `server.rs`
|
|
- Registered DTOs in `dto/mod.rs`
|
|
- Registered `ArtifactVersionRepository` in `repositories/mod.rs`
|
|
|
|
### Test Fixes
|
|
|
|
- Updated existing `repository_artifact_tests.rs` fixtures to include new fields in `CreateArtifactInput` and `UpdateArtifactInput`
|
|
|
|
## Design Decisions
|
|
|
|
1. **Progress vs File artifacts**: Progress artifacts use `artifact.data` (JSONB array, appended atomically in SQL). File artifacts use `artifact_version` rows. This avoids creating a version per progress tick.
|
|
|
|
2. **Binary in BYTEA**: For simplicity, binary content is stored in PostgreSQL BYTEA. A future enhancement could add external object storage (S3) for large files.
|
|
|
|
3. **Version auto-numbering**: Uses a SQL function (`next_artifact_version`) for safe concurrent version numbering.
|
|
|
|
4. **Retention enforcement via trigger**: The `enforce_artifact_retention` trigger runs after each version insert, keeping the version count within the configured limit automatically.
|
|
|
|
5. **No FK to execution**: Since execution is a TimescaleDB hypertable, `artifact.execution` is a plain BIGINT (consistent with other hypertable references in the project).
|
|
|
|
6. **SELECT_COLUMNS pattern**: Binary content is excluded from default queries for performance. Separate `*_with_content` methods exist for download endpoints. |