feat: artifact upload endpoint with pluggable storage backend (BGSTM #298) - #310
Closed
bg-playground with Copilot wants to merge 7 commits into
Closed
feat: artifact upload endpoint with pluggable storage backend (BGSTM #298)#310bg-playground with Copilot wants to merge 7 commits into
bg-playground with Copilot wants to merge 7 commits into
Conversation
…ests Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/1a97867b-9a96-45d2-9c4e-da61008b2fb5 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/1a97867b-9a96-45d2-9c4e-da61008b2fb5 Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
…t-upload-endpoint Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
…nd (BGSTM#298) Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/e8dd1b96-4437-41c6-be62-2a9b050201cc Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bg-playground/BGSTM/sessions/e8dd1b96-4437-41c6-be62-2a9b050201cc Co-authored-by: bg-playground <259109604+bg-playground@users.noreply.github.com>
Copilot
AI
changed the title
[WIP] Add artifact upload endpoint for External Results API
feat: artifact upload endpoint with pluggable storage backend (BGSTM #298)
May 7, 2026
5 tasks
bg-playground
marked this pull request as ready for review
May 7, 2026 13:08
Owner
|
#311 is the known root cause of the E2E Playwright failures here. Merging this one and will address in that PR. |
Merged
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
POST /api/v1/external-results/artifact— the multipart artifact upload slice of the External Results API. Also merges the case-result endpoints (BGSTM #303, PR #307) as a prerequisite since the artifact FK depends onexternal_case_results.Storage abstraction
StorageBackendABC (put/url_for/delete) inbackend/app/storage/base.pyLocalFsBackend— streams to disk viaasyncio.to_thread, no third-party depsS3Backendstub — importable but raisesNotImplementedErroron construction with a clear messageget_storage_backend()FastAPIDependsfactory; singleton selected bysettings.STORAGE_BACKENDEndpoint
POST /api/v1/external-results/artifactacceptsmultipart/form-data:STORAGE_MAX_UPLOAD_BYTESmid-stream → 413 on overflowcase_result_idexistence checked → 404 on missexternal-artifacts/{case_result_id}/{uuid4()}-{safe_filename}ArtifactResponseincluding permanent URLDatabase
New
external_case_artifactstable (migrationj9k0l1m2n3o4, chained fromi8j9k0l1m2n3):case_result_id → external_case_results(id) ON DELETE CASCADErunner_token_id → runner_tokens(id)for provenancestorage_keyunique constraintSettings additions
Dev serving
StaticFilesmounted at/artifactspointing toSTORAGE_LOCAL_ROOT, gated bysettings.STORAGE_BACKEND == "local"so production with S3 doesn't expose a local directory.Other
ArtifactResponse.created_atrenamed touploaded_atto match the DB column namepython-multipart==0.0.27added torequirements.txt(required forForm/Fileendpoints; 0.0.27 is the current patched version with no known CVEs)Original prompt
Implement the artifact upload endpoint for the External Results API with a pluggable storage backend. Closes #298.
This is the next slice of the External Results API contract (parent: #291), following #299 (spec), #296 (auth), #300 (router/session), and #303 (case results) which are all merged.
Scope
Backend only. Add:
StorageBackendABC with two concrete implementationsPOST /api/v1/external-results/artifactendpointexternal_case_artifactstable + Alembic migrationDo NOT touch:
audit_logwiring (that's #297, comes next), the spec doc (already merged),case_results.py,auth.py, or any frontend code.Files to add/modify
New files
backend/app/storage/__init__.pybackend/app/storage/base.py—StorageBackendABCbackend/app/storage/local.py—LocalFsBackend(default for dev/tests)backend/app/storage/s3.py—S3Backendstub that raisesNotImplementedErrorclearlybackend/app/models/external_artifact.py—ExternalCaseArtifactSQLAlchemy modelbackend/app/crud/external_artifact.py— CRUD helpersbackend/alembic/versions/<new>_add_external_case_artifacts_table.py— migrationbackend/tests/api/test_external_artifacts.py— integration testsModified files
backend/app/api/external_results.py— add the artifact endpoint and aDepends(get_storage_backend)dependencybackend/app/core/config.py(or whereverSettingslives) — add storage settingsbackend/app/main.py— only if a static-files mount is needed for serving local artifacts in dev (see "Static serving" below)Storage abstraction
Provide a
get_storage_backend()dependency inbackend/app/storage/__init__.py(or a newdependencies.py) that selects the backend based onsettings.STORAGE_BACKENDand is injectable in FastAPI routes.Settings additions
Add to the existing
Settingsclass:STORAGE_BACKEND: Literal["local", "s3"] = "local"STORAGE_LOCAL_ROOT: Path = Path("./var/artifacts")STORAGE_LOCAL_PUBLIC_BASE_URL: str = "http://localhost:8000/artifacts"STORAGE_MAX_UPLOAD_BYTES: int = 52_428_800# 50 MBSTORAGE_ALLOWED_CONTENT_TYPES: list[str] = ["image/png", "image/jpeg", "video/mp4", "video/webm", "application/zip", "text/plain", "application/json"]Match the project's existing settings convention (likely
pydantic-settings/BaseSettings).Endpoint
POST /api/v1/external-results/artifact—multipart/form-dataForm fields:
case_result_id: UUID(required) — must exist inexternal_case_resultskind: str— enumscreenshot | trace | video | log | otherfilename: str(optional, defaults to upload's filename)File field:
UploadFile)Behavior:
kindagainst the enum (422 on miss)case_result_idexists (404 on miss)STORAGE_MAX_UPLOAD_BYTESwhile streaming. On overflow, return 413.Content-TypeagainstSTORAGE_ALLOWED_CONTENT_TYPES(415 on miss)external-artifacts/{case_result_id}/{uuid4()}-{safe_filename}backend.put(key, stream, content_type)external_case_artifactswith:id,case_result_id(FK),kind,filename,content_type,size_bytes,storage_key,url,uploaded_at,runner_token_id(FK to the auth token, mirrors how case results capture provenance — checkexternal_results.pyandcase_results.pyfor the existing pattern)201with the new artifact row + permanent URL (fro...This pull request was created from Copilot chat.