Compare commits
No commits in common. "3247ce3ca0e70e7b902b73b14c7a70e89019c3c5" and "3979ef2d085d883ed003de61781463c08591f619" have entirely different histories.
3247ce3ca0
...
3979ef2d08
@ -1,9 +1,8 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
@ -208,7 +207,7 @@ func (h *Album) GenerateAllShots(w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
jobIDs, err := h.albums.GenerateAllShots(r.Context(), id, user.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, album.ErrAnchorRequired) {
|
||||
if err.Error() == "anchor must be generated before shots" {
|
||||
return httperror.UnprocessableEntity("anchor must be generated before shots")
|
||||
}
|
||||
return httperror.NotFound("album not found")
|
||||
@ -235,14 +234,16 @@ func (h *Album) GenerateShot(w http.ResponseWriter, r *http.Request) error {
|
||||
return httperror.BadRequest("album ID is required")
|
||||
}
|
||||
|
||||
shotIndex, err := parseShotIndex(chi.URLParam(r, "index"))
|
||||
if err != nil {
|
||||
return httperror.BadRequest("shot index must be a non-negative integer")
|
||||
shotIndex := 0
|
||||
if idx := chi.URLParam(r, "index"); idx != "" {
|
||||
if _, err := fmt.Sscanf(idx, "%d", &shotIndex); err != nil {
|
||||
return httperror.BadRequest("invalid shot index")
|
||||
}
|
||||
}
|
||||
|
||||
jobID, err := h.albums.GenerateShot(r.Context(), id, user.ID, shotIndex)
|
||||
if err != nil {
|
||||
if errors.Is(err, album.ErrAnchorRequired) {
|
||||
if err.Error() == "anchor must be generated before shots" {
|
||||
return httperror.UnprocessableEntity("anchor must be generated before shots")
|
||||
}
|
||||
return httperror.NotFound("album or shot not found")
|
||||
@ -264,9 +265,11 @@ func (h *Album) ResetShot(w http.ResponseWriter, r *http.Request) error {
|
||||
return httperror.BadRequest("album ID is required")
|
||||
}
|
||||
|
||||
shotIndex, err := parseShotIndex(chi.URLParam(r, "index"))
|
||||
if err != nil {
|
||||
return httperror.BadRequest("shot index must be a non-negative integer")
|
||||
shotIndex := 0
|
||||
if idx := chi.URLParam(r, "index"); idx != "" {
|
||||
if _, err := fmt.Sscanf(idx, "%d", &shotIndex); err != nil {
|
||||
return httperror.BadRequest("invalid shot index")
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.albums.ResetShot(r.Context(), id, user.ID, shotIndex); err != nil {
|
||||
@ -276,16 +279,3 @@ func (h *Album) ResetShot(w http.ResponseWriter, r *http.Request) error {
|
||||
httpresponse.NoContent(w)
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseShotIndex parses and validates the shot index URL parameter.
|
||||
// Returns an error if the value is missing, non-numeric, or negative.
|
||||
func parseShotIndex(idx string) (int, error) {
|
||||
if idx == "" {
|
||||
return 0, errors.New("missing shot index")
|
||||
}
|
||||
n, err := strconv.Atoi(idx)
|
||||
if err != nil || n < 0 {
|
||||
return 0, errors.New("shot index must be a non-negative integer")
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
@ -122,7 +122,7 @@ func (s *AlbumService) GenerateAllShots(ctx context.Context, id album.AlbumID, u
|
||||
return nil, fmt.Errorf("album not found: %w", err)
|
||||
}
|
||||
if a.AnchorURL == "" {
|
||||
return nil, album.ErrAnchorRequired
|
||||
return nil, fmt.Errorf("anchor must be generated before shots")
|
||||
}
|
||||
|
||||
var jobIDs []string
|
||||
|
||||
@ -4,4 +4,3 @@ export { useMediaGeneration, type GenerationStatus, type UseMediaGenerationConfi
|
||||
export { useChat, type UseChatConfig, type UseChatResult, type ChatMessage } from './useChat';
|
||||
export { useMediaUpload, type UploadProgress, type UploadResult, type UseMediaUploadConfig, type UseMediaUploadResult } from './useMediaUpload';
|
||||
export { useAlbumGeneration, type Album, type Shot, type ShotStatus, type UseAlbumGenerationConfig, type UseAlbumGenerationResult } from './useAlbumGeneration';
|
||||
export { usePersonaGeneration, type PersonaGenerationState, type UsePersonaGenerationConfig, type UsePersonaGenerationResult } from './usePersonaGeneration';
|
||||
|
||||
@ -22,17 +22,7 @@ export type EventType =
|
||||
| 'album_anchor_failed'
|
||||
| 'album_shot_generating'
|
||||
| 'album_shot_complete'
|
||||
| 'album_shot_failed'
|
||||
// Persona generation events (pkg/personagen)
|
||||
| 'persona_spec_started'
|
||||
| 'persona_spec_complete'
|
||||
| 'persona_image_started'
|
||||
| 'persona_image_progress'
|
||||
| 'persona_image_complete'
|
||||
| 'persona_video_started'
|
||||
| 'persona_video_complete'
|
||||
| 'persona_video_failed'
|
||||
| 'persona_failed';
|
||||
| 'album_shot_failed';
|
||||
|
||||
/**
|
||||
* Chat message data payload.
|
||||
|
||||
@ -3,6 +3,7 @@ package album
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -75,23 +76,13 @@ func AnchorHandler(mg *mediagen.Manager, store storage.Store, pub realtime.Event
|
||||
|
||||
// Persist anchor to storage.
|
||||
anchorURL := img.URL
|
||||
if store != nil {
|
||||
if len(img.Data) > 0 {
|
||||
storagePath := fmt.Sprintf("albums/%s/%s/anchor.png", userID, albumID)
|
||||
url, uploadErr := store.Upload(ctx, storagePath, img.Data, "image/png")
|
||||
if uploadErr != nil {
|
||||
logger.Warn("failed to persist anchor to storage, using inline URL", "error", uploadErr, "job_id", job.ID)
|
||||
} else {
|
||||
anchorURL = url
|
||||
}
|
||||
}
|
||||
// Save caption alongside the image regardless of whether bytes were uploaded.
|
||||
// This ensures we always record what generated the image (URL-only providers included).
|
||||
if anchorURL != "" {
|
||||
captionPath := fmt.Sprintf("albums/%s/%s/anchor.caption", userID, albumID)
|
||||
if _, captionErr := store.Upload(ctx, captionPath, []byte(subjectDesc), "text/plain"); captionErr != nil {
|
||||
logger.Warn("failed to persist anchor caption", "error", captionErr, "job_id", job.ID)
|
||||
}
|
||||
if store != nil && len(img.Data) > 0 {
|
||||
storagePath := fmt.Sprintf("albums/%s/%s/anchor.png", userID, albumID)
|
||||
url, uploadErr := store.Upload(ctx, storagePath, img.Data, "image/png")
|
||||
if uploadErr != nil {
|
||||
logger.Warn("failed to persist anchor to storage, using inline URL", "error", uploadErr, "job_id", job.ID)
|
||||
} else {
|
||||
anchorURL = url
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,11 +146,10 @@ func ShotHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventPu
|
||||
})
|
||||
|
||||
// Fetch anchor image bytes from storage.
|
||||
const anchorMaxBytes = 20 << 20 // 20 MB — anchor images should be small PNGs
|
||||
var anchorBytes []byte
|
||||
var anchorMime string
|
||||
if anchorURL != "" {
|
||||
data, err := storage.FetchURL(ctx, httpClient, anchorURL, anchorMaxBytes)
|
||||
data, err := fetchBytes(ctx, anchorURL)
|
||||
if err != nil {
|
||||
logger.Warn("failed to fetch anchor image, proceeding without reference",
|
||||
"error", err, "job_id", job.ID, "anchor_url", anchorURL)
|
||||
@ -215,23 +205,14 @@ func ShotHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventPu
|
||||
|
||||
// Persist shot image to storage.
|
||||
imageURL := img.URL
|
||||
if store != nil {
|
||||
if len(img.Data) > 0 {
|
||||
storagePath := fmt.Sprintf("albums/%s/%s/shots/%d.png", userID, albumID, shotIndex)
|
||||
url, uploadErr := store.Upload(ctx, storagePath, img.Data, "image/png")
|
||||
if uploadErr != nil {
|
||||
logger.Warn("failed to persist shot to storage, using inline URL",
|
||||
"error", uploadErr, "job_id", job.ID)
|
||||
} else {
|
||||
imageURL = url
|
||||
}
|
||||
}
|
||||
// Save caption alongside the image regardless of whether bytes were uploaded.
|
||||
if imageURL != "" {
|
||||
captionPath := fmt.Sprintf("albums/%s/%s/shots/%d.caption", userID, albumID, shotIndex)
|
||||
if _, captionErr := store.Upload(ctx, captionPath, []byte(prompt), "text/plain"); captionErr != nil {
|
||||
logger.Warn("failed to persist shot caption", "error", captionErr, "job_id", job.ID)
|
||||
}
|
||||
if store != nil && len(img.Data) > 0 {
|
||||
storagePath := fmt.Sprintf("albums/%s/%s/shots/%d.png", userID, albumID, shotIndex)
|
||||
url, uploadErr := store.Upload(ctx, storagePath, img.Data, "image/png")
|
||||
if uploadErr != nil {
|
||||
logger.Warn("failed to persist shot to storage, using inline URL",
|
||||
"error", uploadErr, "job_id", job.ID)
|
||||
} else {
|
||||
imageURL = url
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,3 +241,25 @@ func ShotHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventPu
|
||||
}
|
||||
}
|
||||
|
||||
// fetchBytes downloads raw bytes from a URL.
|
||||
// Used to load anchor images at shot-generation time.
|
||||
func fetchBytes(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("fetch: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
const maxSize = 20 << 20 // 20 MB limit for anchor images
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@ -13,14 +13,9 @@ package album
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrAnchorRequired is returned when shot generation is attempted before the anchor image exists.
|
||||
// Handlers should map this to HTTP 422 Unprocessable Entity.
|
||||
var ErrAnchorRequired = errors.New("anchor must be generated before shots")
|
||||
|
||||
// AlbumID is the unique identifier for an album.
|
||||
type AlbumID string
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
@ -99,10 +100,6 @@ func ImageHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventP
|
||||
if uploadErr != nil {
|
||||
logger.Warn("failed to persist image to storage", "error", uploadErr, "job_id", job.ID)
|
||||
} else {
|
||||
captionPath := fmt.Sprintf("media/%s/images/%s_%d.caption", userID, job.ID, i)
|
||||
if _, captionErr := store.Upload(ctx, captionPath, []byte(prompt), "text/plain"); captionErr != nil {
|
||||
logger.Warn("failed to persist image caption", "error", captionErr, "job_id", job.ID)
|
||||
}
|
||||
images[i] = GeneratedImage{Data: url, IsURL: true, Seed: resp.Seed}
|
||||
continue
|
||||
}
|
||||
@ -178,7 +175,6 @@ func VideoHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventP
|
||||
|
||||
// Build videos array matching frontend VideoResult shape:
|
||||
// { videos: [{ data, isUrl, mimeType }], provider, latencyMs }
|
||||
const videoMaxBytes = 500 << 20 // 500 MB — videos can be large
|
||||
videos := make([]map[string]any, 0, len(resp.Videos))
|
||||
for i, vid := range resp.Videos {
|
||||
videoURL := vid.URL
|
||||
@ -193,7 +189,7 @@ func VideoHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventP
|
||||
if len(vid.Data) > 0 {
|
||||
videoData = vid.Data
|
||||
} else if vid.URL != "" {
|
||||
downloaded, downloadErr := storage.FetchURL(ctx, httpClient, vid.URL, videoMaxBytes)
|
||||
downloaded, downloadErr := downloadURL(ctx, vid.URL)
|
||||
if downloadErr != nil {
|
||||
logger.Warn("failed to download video from provider", "error", downloadErr, "job_id", job.ID)
|
||||
} else {
|
||||
@ -209,14 +205,6 @@ func VideoHandler(mg *mediagen.Manager, store storage.Store, pub realtime.EventP
|
||||
videoURL = persistedURL
|
||||
}
|
||||
}
|
||||
|
||||
// Save caption alongside the video regardless of where it's stored.
|
||||
if videoURL != "" && prompt != "" {
|
||||
captionPath := fmt.Sprintf("media/%s/videos/%s_%d.caption", userID, job.ID, i)
|
||||
if _, captionErr := store.Upload(ctx, captionPath, []byte(prompt), "text/plain"); captionErr != nil {
|
||||
logger.Warn("failed to persist video caption", "error", captionErr, "job_id", job.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videos = append(videos, map[string]any{
|
||||
@ -347,3 +335,26 @@ func ChatResponseHandler(tg *textgen.Manager, pub realtime.EventPublisher, logge
|
||||
}
|
||||
}
|
||||
|
||||
// downloadURL fetches content from a URL and returns the bytes.
|
||||
// Used to download provider-hosted videos before persisting to storage.
|
||||
func downloadURL(ctx context.Context, url string) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("download: status %d", resp.StatusCode)
|
||||
}
|
||||
// Limit body to 500MB to prevent OOM from unexpected large responses.
|
||||
const maxBodySize = 500 << 20
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxBodySize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read body: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@ -228,13 +228,6 @@ func (s *Service) generatePosition(ctx context.Context, spec *persona.PersonaSpe
|
||||
return fmt.Errorf("storing position %d: %w", pos, err)
|
||||
}
|
||||
|
||||
if imgSpec.Prompt != "" {
|
||||
captionPath := fmt.Sprintf("personas/%s/images/%02d.caption", spec.ID, pos)
|
||||
if _, captionErr := s.store.Upload(ctx, captionPath, []byte(imgSpec.Prompt), "text/plain"); captionErr != nil {
|
||||
s.logger.Warn("failed to persist image caption", "error", captionErr, "position", pos)
|
||||
}
|
||||
}
|
||||
|
||||
imgSpec.URL = url
|
||||
imgSpec.Status = persona.ImageStatusComplete
|
||||
return nil
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FetchURL downloads content from a URL using the provided HTTP client.
|
||||
// maxBytes caps the download to prevent OOM from unexpectedly large responses.
|
||||
// Callers control the timeout via the http.Client they pass in.
|
||||
func FetchURL(ctx context.Context, client *http.Client, url string, maxBytes int64) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("fetch: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, maxBytes))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@ -88,14 +88,6 @@ func (c ComponentType) NeedsPort() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
|
||||
}
|
||||
|
||||
// NeedsDeployment returns true if this component type requires a Kubernetes Deployment.
|
||||
// All code components except CLI need a Deployment so CI can use kubectl set image.
|
||||
// Workers have no HTTP port but still need a Deployment to run as background processes.
|
||||
func (c ComponentType) NeedsDeployment() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeWorker ||
|
||||
c == ComponentTypeAppAstro || c == ComponentTypeAppReact || c == ComponentTypeAppNextJS
|
||||
}
|
||||
|
||||
// IsGoComponent returns true if this component type uses Go (and needs go.work entry).
|
||||
func (c ComponentType) IsGoComponent() bool {
|
||||
return c == ComponentTypeService || c == ComponentTypeWorker || c == ComponentTypeCLI
|
||||
|
||||
@ -77,7 +77,4 @@ const (
|
||||
|
||||
// Resend (email provider for per-project domain provisioning)
|
||||
CredKeyResendAPIKey = "RESEND_API_KEY"
|
||||
|
||||
// Project-scoped auth secret (unique per project, auto-generated on first code component)
|
||||
CredKeyJWTSecret = "JWT_SECRET"
|
||||
)
|
||||
|
||||
@ -3,7 +3,6 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
@ -238,11 +237,7 @@ func (s *ComponentService) AddComponent(ctx context.Context, projectID string, r
|
||||
Dependencies: []string{}, // Could be parsed from component.yaml
|
||||
}
|
||||
|
||||
// 13. Ensure a JWT_SECRET exists for this project (required by skeleton service startup).
|
||||
// Generated once per project on the first code component; reused for all subsequent components.
|
||||
s.ensureProjectJWTSecret(ctx, projectID)
|
||||
|
||||
// 14. Create initial K8s deployment for components that need one.
|
||||
// 13. Create initial K8s deployment for components that need one.
|
||||
// This ensures kubectl set image will find the deployment when CI runs.
|
||||
s.createInitialComponentDeployment(ctx, projectID, projectDomain, component)
|
||||
|
||||
@ -332,58 +327,6 @@ func (s *ComponentService) prepareMonorepoUpdates(
|
||||
return fileOps, nil
|
||||
}
|
||||
|
||||
// ensureProjectJWTSecret generates and stores a random JWT_SECRET for the project
|
||||
// if one does not already exist. Called on first code component add.
|
||||
// The secret is stored as "{projectID}:JWT_SECRET" in the credential store.
|
||||
func (s *ComponentService) ensureProjectJWTSecret(ctx context.Context, projectID string) {
|
||||
if s.credentialStore == nil {
|
||||
return
|
||||
}
|
||||
|
||||
log := logging.FromContext(ctx).WithService("component")
|
||||
|
||||
key := projectID + ":" + domain.CredKeyJWTSecret
|
||||
existing, err := s.credentialStore.Get(ctx, key)
|
||||
if err != nil {
|
||||
log.Warn("failed to check JWT secret existence",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
if existing != "" {
|
||||
return // Already provisioned - don't overwrite
|
||||
}
|
||||
|
||||
// Generate a cryptographically random 32-byte secret
|
||||
secret := make([]byte, 32)
|
||||
if _, err := rand.Read(secret); err != nil {
|
||||
log.Warn("failed to generate JWT secret",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
secretValue := base64.URLEncoding.EncodeToString(secret)
|
||||
if err := s.credentialStore.Set(ctx, domain.Credential{
|
||||
Key: key,
|
||||
Value: secretValue,
|
||||
Description: "JWT signing secret for project " + projectID,
|
||||
Category: "project",
|
||||
}); err != nil {
|
||||
log.Warn("failed to store JWT secret",
|
||||
logging.FieldProjectID, projectID,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("provisioned JWT secret for project",
|
||||
logging.FieldProjectID, projectID,
|
||||
)
|
||||
}
|
||||
|
||||
// findFirstServiceComponent returns the first service component in a project, or nil.
|
||||
func (s *ComponentService) findFirstServiceComponent(ctx context.Context, projectID string) *domain.Component {
|
||||
components, err := s.ListComponents(ctx, projectID)
|
||||
|
||||
@ -23,7 +23,7 @@ func (s *ComponentService) createInitialComponentDeployment(
|
||||
component *domain.Component,
|
||||
) {
|
||||
// Skip if no deployer or component doesn't need a deployment
|
||||
if s.deployer == nil || !component.Type.NeedsDeployment() {
|
||||
if s.deployer == nil || !component.Type.NeedsPort() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -67,18 +67,16 @@ func (s *ComponentService) createInitialComponentDeployment(
|
||||
return
|
||||
}
|
||||
|
||||
// Add path to project's unified Ingress (only for components with an HTTP port)
|
||||
if component.Port > 0 && basePath != "" {
|
||||
serviceName := spec.DeploymentName()
|
||||
if err := s.deployer.AddIngressPath(ctx, projectID, projectDomain, basePath, serviceName, component.Port); err != nil {
|
||||
log.Warn("failed to add ingress path for component",
|
||||
logging.FieldProjectID, projectID,
|
||||
"component", component.Name,
|
||||
"path", basePath,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
// Continue anyway - the deployment exists and CI will work
|
||||
}
|
||||
// Add path to project's unified Ingress
|
||||
serviceName := spec.DeploymentName()
|
||||
if err := s.deployer.AddIngressPath(ctx, projectID, projectDomain, basePath, serviceName, component.Port); err != nil {
|
||||
log.Warn("failed to add ingress path for component",
|
||||
logging.FieldProjectID, projectID,
|
||||
"component", component.Name,
|
||||
"path", basePath,
|
||||
logging.FieldError, err,
|
||||
)
|
||||
// Continue anyway - the deployment/service exist and CI will work
|
||||
}
|
||||
|
||||
log.Info("created initial component deployment",
|
||||
@ -187,7 +185,6 @@ func (s *ComponentService) fetchProjectCredentials(ctx context.Context, projectI
|
||||
domain.CredKeyNotifyAPIKey,
|
||||
domain.CredKeyNotifyHost,
|
||||
domain.CredKeyNotifyFrom,
|
||||
domain.CredKeyJWTSecret,
|
||||
}
|
||||
|
||||
// Global credentials (stored without project prefix, shared across all projects)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user