-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Docker connector, artifact system, and site improvements #28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 43 commits
4f5044c
a07483a
d1b4959
c1b7879
a4f6744
3913d47
0e5d8e9
ccb63e5
b0e4d64
a636201
3f42c17
e735241
7085f68
ee7f873
3473d2a
2d826be
9a70a0e
ed5845c
7ca272a
2fd37e9
c4788c3
6c1064e
29ae600
e988295
d9592f4
e1d6e3e
75cf239
5167ec8
abfe339
02d631a
03f5b4d
6e7207f
4c03535
7dc73d8
e83d30c
aa0e380
4015b58
5e680f6
fe4c688
25edd31
e6cc139
27cac6d
5899033
0314876
2e2a457
0bd9a9b
b798402
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| name: docker-data-transform | ||
| description: > | ||
| Query a database for recent records, process them through | ||
| a containerized tool, and post the results to Slack. | ||
| Demonstrates the docker/run connector with stdin/stdout | ||
| data passing. | ||
|
|
||
| steps: | ||
| - name: fetch-records | ||
| action: postgres/query | ||
| credential: my-db | ||
| timeout: "15s" | ||
| params: | ||
| query: "SELECT id, name, status, updated_at FROM tasks WHERE updated_at > now() - interval '24 hours' ORDER BY updated_at DESC" | ||
|
|
||
| - name: transform-data | ||
| action: docker/run | ||
| credential: my-docker | ||
| timeout: "30s" | ||
| params: | ||
| image: giantswarm/tiny-tools | ||
| cmd: ["jq", "[.[] | {id, name, status}]"] | ||
| stdin: "{{ steps['fetch-records'].output.json }}" | ||
| memory: "128m" | ||
|
|
||
| - name: post-results | ||
| action: slack/send | ||
| credential: slack-token | ||
| if: "steps['transform-data'].output.exit_code == 0" | ||
| params: | ||
| channel: "#daily-report" | ||
| text: "Daily task summary:\n{{ steps['transform-data'].output.stdout }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| name: docker-volume-backup | ||
| description: > | ||
| Back up a Docker volume to S3 on a daily schedule. | ||
| Compresses the volume contents, uploads to S3, and | ||
| sends a Slack alert on success or failure. | ||
|
|
||
| triggers: | ||
| - type: cron | ||
| schedule: "0 2 * * *" | ||
|
|
||
| steps: | ||
| - name: backup-volume | ||
| action: docker/run | ||
| credential: my-docker | ||
| timeout: "10m" | ||
| params: | ||
| image: alpine | ||
| cmd: ["tar", "-czf", "/mantle/artifacts/backup.tar.gz", "-C", "/data", "."] | ||
| mounts: | ||
| - source: "my-app-data" | ||
| target: "/data" | ||
| readonly: true | ||
| memory: "256m" | ||
| remove: true | ||
| artifacts: | ||
| - path: /mantle/artifacts/backup.tar.gz | ||
| name: backup-archive | ||
|
|
||
| - name: upload-to-s3 | ||
| action: s3/put | ||
| credential: aws-prod | ||
| timeout: "5m" | ||
| params: | ||
| bucket: my-backups | ||
| key: "volumes/my-app-data/{{ date }}/backup.tar.gz" | ||
| content: "{{ artifacts['backup-archive'].url }}" | ||
| content_type: "application/gzip" | ||
|
|
||
| - name: notify-success | ||
| action: slack/send | ||
| credential: slack-token | ||
| if: "steps['upload-to-s3'].error == null && steps['upload-to-s3'].output.size > 0" | ||
| params: | ||
| channel: "#ops-alerts" | ||
| text: "Volume backup completed — {{ artifacts['backup-archive'].size }} bytes uploaded to s3://my-backups/volumes/my-app-data/" | ||
|
|
||
| - name: notify-failure | ||
| action: slack/send | ||
| credential: slack-token | ||
| if: "steps['upload-to-s3'].error != null" | ||
| params: | ||
| channel: "#ops-alerts" | ||
| text: "Volume backup FAILED — {{ steps['upload-to-s3'].error }}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Failure notifications are unreachable in this example. The engine stops on the first failed step, so 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| package artifact | ||
|
|
||
| import "context" | ||
|
|
||
| type artifactsDirKey struct{} | ||
|
|
||
| // WithArtifactsDir returns a context carrying the artifacts directory path. | ||
| func WithArtifactsDir(ctx context.Context, dir string) context.Context { | ||
| return context.WithValue(ctx, artifactsDirKey{}, dir) | ||
| } | ||
|
|
||
| // ArtifactsDirFromContext extracts the artifacts directory from context. | ||
| func ArtifactsDirFromContext(ctx context.Context) string { | ||
| dir, _ := ctx.Value(artifactsDirKey{}).(string) | ||
| return dir | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| package artifact | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "log/slog" | ||
| "time" | ||
| ) | ||
|
|
||
| // Reaper cleans up expired artifacts from both tmp storage and the database. | ||
| type Reaper struct { | ||
| Store *Store | ||
| TmpStorage TmpStorage | ||
| Retention time.Duration | ||
| Logger *slog.Logger | ||
| } | ||
|
|
||
| // Sweep finds and removes artifacts older than the retention period. | ||
| // Returns the number of artifacts cleaned up. | ||
| func (r *Reaper) Sweep(ctx context.Context) (int, error) { | ||
| if r.Retention <= 0 { | ||
| return 0, nil // cleanup disabled | ||
| } | ||
|
|
||
| logger := r.Logger | ||
| if logger == nil { | ||
| logger = slog.Default() | ||
| } | ||
|
|
||
| expired, err := r.Store.ListExpired(ctx, r.Retention) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("listing expired artifacts: %w", err) | ||
| } | ||
|
|
||
| if len(expired) == 0 { | ||
| return 0, nil | ||
| } | ||
|
|
||
| // Group by execution for batch deletion. | ||
| byExecution := make(map[string][]Artifact) | ||
| for _, a := range expired { | ||
| byExecution[a.ExecutionID] = append(byExecution[a.ExecutionID], a) | ||
| } | ||
|
|
||
| cleaned := 0 | ||
| for execID, arts := range byExecution { | ||
| // Delete files from tmp storage. | ||
| fileDeleteFailed := false | ||
| for _, a := range arts { | ||
| if delErr := r.TmpStorage.DeleteByPrefix(ctx, a.URL); delErr != nil { | ||
| logger.Error("failed to delete artifact file", | ||
| "artifact", a.Name, "url", a.URL, "error", delErr) | ||
| fileDeleteFailed = true | ||
| } | ||
| } | ||
|
|
||
| // Delete metadata from database. | ||
| if err := r.Store.DeleteByExecution(ctx, execID); err != nil { | ||
| logger.Error("failed to delete artifact metadata", | ||
| "execution_id", execID, "error", err) | ||
| continue | ||
| } | ||
|
|
||
| if fileDeleteFailed { | ||
| logger.Warn("some artifact files could not be deleted", | ||
| "execution_id", execID) | ||
| } | ||
| cleaned += len(arts) | ||
| logger.Info("cleaned expired artifacts", | ||
| "execution_id", execID, "count", len(arts)) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
|
Comment on lines
+39
to
+57
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check what Put returns and how URL is used
# Find what Put returns
echo "=== FilesystemTmpStorage.Put return value ==="
ast-grep --pattern $'func (fs *FilesystemTmpStorage) Put($_, $_, $_) (string, error) {
$$$
}'
# Check if URL is stored as-is from Put
echo ""
echo "=== Store.Create usage of URL ==="
rg -n -A5 'store\.Create' --type=go | head -40
# Check how engine stores artifacts
echo ""
echo "=== Engine artifact persistence ==="
rg -n 'URL.*=' internal/engine/engine.go | head -20Repository: dvflw/mantle Length of output: 4659 🏁 Script executed: # Read DeleteByPrefix implementation
echo "=== DeleteByPrefix implementation (tmp.go lines 67-83) ==="
sed -n '67,83p' internal/artifact/tmp.go
echo ""
echo "=== Full DeleteByPrefix and surrounding context ==="
sed -n '65,95p' internal/artifact/tmp.go
echo ""
echo "=== Artifact struct definition ==="
rg -n -B2 -A8 'type Artifact struct' internal/artifact/
echo ""
echo "=== How artifacts are created in engine ==="
rg -n -B3 -A3 'Store\.Create' internal/engine/ --type=goRepository: dvflw/mantle Length of output: 2480 🏁 Script executed: # See the full artifact creation context
sed -n '510,535p' internal/engine/engine.go
echo ""
echo "=== Look for where putResp is assigned ==="
rg -n -B5 'putResp.*:=' internal/engine/engine.go | head -30Repository: dvflw/mantle Length of output: 1100 Fix The issue is valid. In In Compare this to 🤖 Prompt for AI Agents |
||
|
|
||
| return cleaned, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,87 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package artifact | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestReaper_CleansExpiredArtifacts(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dir := t.TempDir() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs := &FilesystemTmpStorage{BasePath: dir} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db := setupTestDB(t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| store := &Store{DB: db} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx := context.Background() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| execID := createTestExecution(t, db) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Write a file and create metadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| srcPath := filepath.Join(dir, "src.bin") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := os.WriteFile(srcPath, []byte("data"), 0644); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("os.WriteFile: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key := "wf/" + execID + "/artifact/file.bin" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url, err := fs.Put(ctx, key, srcPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("fs.Put: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := store.Create(ctx, &Artifact{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExecutionID: execID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| StepName: "step-a", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Name: "test-artifact", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| URL: url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Size: 4, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("store.Create: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Backdate the artifact so it appears expired | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _, err = db.ExecContext(ctx, ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| UPDATE execution_artifacts SET created_at = NOW() - interval '48 hours' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| WHERE execution_id = $1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `, execID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("backdate: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reaper := &Reaper{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Store: store, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TmpStorage: fs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Retention: 24 * time.Hour, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleaned, err := reaper.Sweep(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("Sweep: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cleaned != 1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("cleaned = %d, want 1", cleaned) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Metadata should be gone | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| arts, _ := store.ListByExecution(ctx, execID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if len(arts) != 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("expected 0 artifacts after sweep, got %d", len(arts)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestReaper_SkipsWhenRetentionZero(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dir := t.TempDir() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fs := &FilesystemTmpStorage{BasePath: dir} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reaper := &Reaper{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Store: &Store{}, // won't be called | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TmpStorage: fs, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Retention: 0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cleaned, err := reaper.Sweep(context.Background()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("Sweep: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if cleaned != 0 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Errorf("cleaned = %d, want 0 when retention is 0", cleaned) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+73
to
+90
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider making the zero-retention test more robust. The test uses ♻️ Optional: use a real store or document the intent func TestReaper_SkipsWhenRetentionZero(t *testing.T) {
dir := t.TempDir()
fs := &FilesystemTmpStorage{BasePath: dir}
reaper := &Reaper{
- Store: &Store{}, // won't be called
+ Store: nil, // explicitly nil; Sweep must return early before Store access
TmpStorage: fs,
Retention: 0,
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| package artifact | ||
|
|
||
| import ( | ||
| "context" | ||
| "database/sql" | ||
| "fmt" | ||
| "time" | ||
| ) | ||
|
|
||
| // Artifact represents metadata for a persisted artifact. | ||
| type Artifact struct { | ||
| ID string | ||
| ExecutionID string | ||
| StepName string | ||
| Name string | ||
| URL string | ||
| Size int64 | ||
| CreatedAt time.Time | ||
| } | ||
|
|
||
| // Store manages artifact metadata in Postgres. | ||
| type Store struct { | ||
| DB *sql.DB | ||
| } | ||
|
|
||
| // Create inserts artifact metadata into the database. | ||
| func (s *Store) Create(ctx context.Context, a *Artifact) error { | ||
| _, err := s.DB.ExecContext(ctx, ` | ||
| INSERT INTO execution_artifacts (execution_id, step_name, name, url, size) | ||
| VALUES ($1, $2, $3, $4, $5) | ||
| `, a.ExecutionID, a.StepName, a.Name, a.URL, a.Size) | ||
| if err != nil { | ||
| return fmt.Errorf("artifact create: %w", err) | ||
| } | ||
| return nil | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // GetByName retrieves a single artifact by execution ID and artifact name. | ||
| func (s *Store) GetByName(ctx context.Context, executionID, name string) (*Artifact, error) { | ||
| var a Artifact | ||
| err := s.DB.QueryRowContext(ctx, ` | ||
| SELECT id, execution_id, step_name, name, url, size, created_at | ||
| FROM execution_artifacts | ||
| WHERE execution_id = $1 AND name = $2 | ||
| `, executionID, name).Scan(&a.ID, &a.ExecutionID, &a.StepName, &a.Name, &a.URL, &a.Size, &a.CreatedAt) | ||
| if err == sql.ErrNoRows { | ||
| return nil, fmt.Errorf("artifact %q not found for execution %s", name, executionID) | ||
| } | ||
| if err != nil { | ||
| return nil, fmt.Errorf("artifact get: %w", err) | ||
| } | ||
| return &a, nil | ||
| } | ||
|
|
||
| // ListByExecution returns all artifacts associated with the given execution, ordered by creation time. | ||
| func (s *Store) ListByExecution(ctx context.Context, executionID string) ([]Artifact, error) { | ||
| rows, err := s.DB.QueryContext(ctx, ` | ||
| SELECT id, execution_id, step_name, name, url, size, created_at | ||
| FROM execution_artifacts | ||
| WHERE execution_id = $1 | ||
| ORDER BY created_at ASC | ||
| `, executionID) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("artifact list: %w", err) | ||
| } | ||
| defer rows.Close() | ||
|
|
||
| var artifacts []Artifact | ||
| for rows.Next() { | ||
| var a Artifact | ||
| if err := rows.Scan(&a.ID, &a.ExecutionID, &a.StepName, &a.Name, &a.URL, &a.Size, &a.CreatedAt); err != nil { | ||
| return nil, fmt.Errorf("artifact scan: %w", err) | ||
| } | ||
| artifacts = append(artifacts, a) | ||
| } | ||
| return artifacts, rows.Err() | ||
| } | ||
|
|
||
| // DeleteByExecution removes all artifact metadata for an execution. | ||
| func (s *Store) DeleteByExecution(ctx context.Context, executionID string) error { | ||
| _, err := s.DB.ExecContext(ctx, ` | ||
| DELETE FROM execution_artifacts WHERE execution_id = $1 | ||
| `, executionID) | ||
| if err != nil { | ||
| return fmt.Errorf("artifact delete: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // ListExpired returns artifacts older than the given duration. | ||
| func (s *Store) ListExpired(ctx context.Context, olderThan time.Duration) ([]Artifact, error) { | ||
| cutoff := time.Now().Add(-olderThan) | ||
| rows, err := s.DB.QueryContext(ctx, ` | ||
| SELECT id, execution_id, step_name, name, url, size, created_at | ||
| FROM execution_artifacts | ||
| WHERE created_at < $1 | ||
| ORDER BY created_at ASC | ||
| `, cutoff) | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| if err != nil { | ||
| return nil, fmt.Errorf("artifact list expired: %w", err) | ||
| } | ||
| defer rows.Close() | ||
|
|
||
| var artifacts []Artifact | ||
| for rows.Next() { | ||
| var a Artifact | ||
| if err := rows.Scan(&a.ID, &a.ExecutionID, &a.StepName, &a.Name, &a.URL, &a.Size, &a.CreatedAt); err != nil { | ||
| return nil, fmt.Errorf("artifact scan: %w", err) | ||
| } | ||
| artifacts = append(artifacts, a) | ||
| } | ||
| return artifacts, rows.Err() | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.