| 332 | } |
| 333 | |
| 334 | func resolveAddWorkflowSpecAndContent(ctx context.Context, initialSpec *WorkflowSpec, verbose bool) (*WorkflowSpec, *FetchedWorkflow, error) { |
| 335 | currentSpec := *initialSpec |
| 336 | visited := make(map[string]struct{}) |
| 337 | followedRedirect := false |
| 338 | |
| 339 | for range maxRedirectDepth { |
| 340 | // Fetch workflow content - handles both local and remote. |
| 341 | fetched, err := fetchWorkflowFromSourceWithContextFn(ctx, ¤tSpec, verbose) |
| 342 | if err != nil { |
| 343 | return nil, nil, err |
| 344 | } |
| 345 | |
| 346 | // Redirects only apply to remote workflows. |
| 347 | if fetched.IsLocal { |
| 348 | return ¤tSpec, fetched, nil |
| 349 | } |
| 350 | |
| 351 | currentRef := currentSpec.Version |
| 352 | if currentRef == "" { |
| 353 | currentRef = "main" |
| 354 | } |
| 355 | locationKey := fmt.Sprintf("%s/%s@%s", currentSpec.RepoSlug, currentSpec.WorkflowPath, currentRef) |
| 356 | if _, exists := visited[locationKey]; exists { |
| 357 | return nil, nil, fmt.Errorf("redirect loop detected at %s", locationKey) |
| 358 | } |
| 359 | visited[locationKey] = struct{}{} |
| 360 | |
| 361 | redirect, err := extractRedirectFromContent(string(fetched.Content)) |
| 362 | if err != nil { |
| 363 | return nil, nil, err |
| 364 | } |
| 365 | if redirect == "" { |
| 366 | // Preserve the original WorkflowName from the user's request only when |
| 367 | // one or more redirects were followed, so the final local file keeps |
| 368 | // the requested name. |
| 369 | // Without redirects, keep any name derived during fetch, such as JSON |
| 370 | // imports where conversion picks a better filename from `name`. |
| 371 | if followedRedirect { |
| 372 | currentSpec.WorkflowName = initialSpec.WorkflowName |
| 373 | } |
| 374 | return ¤tSpec, fetched, nil |
| 375 | } |
| 376 | |
| 377 | redirectedSource, err := normalizeRedirectToSourceSpec(redirect) |
| 378 | if err != nil { |
| 379 | return nil, nil, fmt.Errorf("invalid redirect %q in %s: %w", redirect, locationKey, err) |
| 380 | } |
| 381 | |
| 382 | nextSpec := &WorkflowSpec{ |
| 383 | RepoSpec: RepoSpec{ |
| 384 | RepoSlug: redirectedSource.Repo, |
| 385 | Version: redirectedSource.Ref, |
| 386 | }, |
| 387 | WorkflowPath: redirectedSource.Path, |
| 388 | WorkflowName: normalizeWorkflowID(redirectedSource.Path), |
| 389 | Host: currentSpec.Host, |
| 390 | } |
| 391 | resolutionLog.Printf("Following redirect for add: from=%s to=%s", locationKey, nextSpec.String()) |