6.7 KiB
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
artifacttable with new columns:name(TEXT) — human-readable artifact namedescription(TEXT) — optional descriptioncontent_type(TEXT) — MIME typesize_bytes(BIGINT) — size of latest version contentexecution(BIGINT, no FK) — links artifact to the execution that produced itdata(JSONB) — structured data for progress-type artifacts and metadata
- Created
artifact_versiontable for immutable content snapshots:artifact(FK to artifact, CASCADE delete)version(INTEGER, 1-based, monotonically increasing)content(BYTEA) — binary file contentcontent_json(JSONB) — structured JSON contentmeta(JSONB) — free-form metadata per versioncreated_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 syncssize_bytesandcontent_typeback to the parent artifact
Models (crates/common/src/models.rs)
- Enhanced
Artifactstruct with new fields:name,description,content_type,size_bytes,execution,data - Added
SELECT_COLUMNSconstant for consistent query column lists - Added
ArtifactVersionmodel withSELECT_COLUMNS(excludes binary content for performance) andSELECT_COLUMNS_WITH_CONTENT(includes BYTEA payload) - Added
ToSchemaderive toRetentionPolicyTypeenum (was missing, needed for OpenAPI) - Added re-exports for
ArtifactandArtifactVersionin models module
Repository (crates/common/src/repositories/artifact.rs)
- Updated all
ArtifactRepositoryqueries to useSELECT_COLUMNSconstant - Extended
CreateArtifactInputandUpdateArtifactInputwith new fields - Added
ArtifactSearchFiltersandArtifactSearchResultfor 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
ArtifactVersionRepositorywith methods:find_by_id/find_by_id_with_contentlist_by_artifactfind_latest/find_latest_with_contentfind_by_version/find_by_version_with_contentcreate(auto-assigns version number vianext_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 fieldsAppendProgressRequest— single JSON entry to appendSetDataRequest— full data replacementArtifactResponse/ArtifactSummary— full and list response typesCreateVersionJsonRequest— JSON content for a new versionArtifactVersionResponse/ArtifactVersionSummary— version response typesArtifactQueryParams— filters with pagination- Conversion
Fromimpls 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
axummultipartfeature to API crate's Cargo.toml - Registered artifact routes in
routes/mod.rsandserver.rs - Registered DTOs in
dto/mod.rs - Registered
ArtifactVersionRepositoryinrepositories/mod.rs
Test Fixes
- Updated existing
repository_artifact_tests.rsfixtures to include new fields inCreateArtifactInputandUpdateArtifactInput
Design Decisions
-
Progress vs File artifacts: Progress artifacts use
artifact.data(JSONB array, appended atomically in SQL). File artifacts useartifact_versionrows. This avoids creating a version per progress tick. -
Binary in BYTEA: For simplicity, binary content is stored in PostgreSQL BYTEA. A future enhancement could add external object storage (S3) for large files.
-
Version auto-numbering: Uses a SQL function (
next_artifact_version) for safe concurrent version numbering. -
Retention enforcement via trigger: The
enforce_artifact_retentiontrigger runs after each version insert, keeping the version count within the configured limit automatically. -
No FK to execution: Since execution is a TimescaleDB hypertable,
artifact.executionis a plain BIGINT (consistent with other hypertable references in the project). -
SELECT_COLUMNS pattern: Binary content is excluded from default queries for performance. Separate
*_with_contentmethods exist for download endpoints.