Skip to content

Commit b88cb86

Browse files
authored
Refresh aw.yml package dependencies during update (#53974)
1 parent bece4a9 commit b88cb86

3 files changed

Lines changed: 191 additions & 14 deletions

File tree

pkg/cli/includes.go

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ func FetchIncludeFromSource(ctx context.Context, includePath string, baseSpec *W
245245
// markdown body; this function handles the YAML frontmatter 'imports:' field.
246246
// Import failures are non-fatal (best-effort); the compiler will report any still-missing files.
247247
func fetchAndSaveRemoteFrontmatterImports(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error {
248+
return fetchAndSaveRemoteFrontmatterImportsWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, false)
249+
}
250+
251+
func fetchAndSaveRemoteFrontmatterImportsWithOptions(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, strict bool) error {
248252
if spec.RepoSlug == "" {
249253
return nil
250254
}
@@ -280,7 +284,7 @@ func fetchAndSaveRemoteFrontmatterImports(ctx context.Context, content string, s
280284
// cycles (A imports B, B imports A) are broken without infinite recursion.
281285
seen := make(map[string]struct {
282286
})
283-
fetchFrontmatterImportsRecursive(ctx, content, workflowBaseDir, frontmatterImportsOpts{
287+
return fetchFrontmatterImportsRecursive(ctx, content, workflowBaseDir, frontmatterImportsOpts{
284288
owner: owner,
285289
repo: repo,
286290
ref: ref,
@@ -289,9 +293,9 @@ func fetchAndSaveRemoteFrontmatterImports(ctx context.Context, content string, s
289293
verbose: verbose,
290294
force: force,
291295
tracker: tracker,
296+
strict: strict,
292297
seen: seen,
293298
})
294-
return nil
295299
}
296300

297301
// frontmatterImportsOpts holds the constant parameters for fetchFrontmatterImportsRecursive.
@@ -305,6 +309,7 @@ type frontmatterImportsOpts struct {
305309
verbose bool
306310
force bool
307311
tracker *FileTracker
312+
strict bool
308313
seen map[string]struct{}
309314
// downloadFn is the function used to fetch file content from the source repository.
310315
// When nil, parser.DownloadFileFromGitHub is used. Tests may inject a stub to avoid
@@ -323,15 +328,15 @@ type frontmatterImportsOpts struct {
323328
// - originalBaseDir: directory of the top-level workflow (used to map remote paths → local paths)
324329
// - targetDir: the `.github/workflows` directory in the user's repo
325330
// - seen: shared visited set (keyed by fully-resolved remote path) — prevents cycles & duplicates
326-
func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseDir string, opts frontmatterImportsOpts) {
331+
func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseDir string, opts frontmatterImportsOpts) error {
327332
result, err := parser.ExtractFrontmatterFromContent(content)
328333
if err != nil || result.Frontmatter == nil {
329-
return
334+
return nil
330335
}
331336

332337
importsField, exists := result.Frontmatter["imports"]
333338
if !exists {
334-
return
339+
return nil
335340
}
336341

337342
var importPaths []string
@@ -359,15 +364,18 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
359364
}
360365

361366
if len(importPaths) == 0 {
362-
return
367+
return nil
363368
}
364369

365370
remoteWorkflowLog.Printf("Processing %d frontmatter imports recursively: owner=%s, repo=%s, ref=%s", len(importPaths), opts.owner, opts.repo, opts.ref)
366371

367372
// Pre-compute the absolute target directory once for path-traversal boundary checks.
368373
absTargetDir, err := filepath.Abs(opts.targetDir)
369374
if err != nil {
370-
return
375+
if opts.strict {
376+
return fmt.Errorf("failed to resolve import target directory: %w", err)
377+
}
378+
return nil
371379
}
372380

373381
for _, importPath := range importPaths {
@@ -431,6 +439,9 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
431439

432440
// Reject paths that try to escape the repository root (e.g. "../../etc/passwd")
433441
if remoteFilePath == ".." || strings.HasPrefix(remoteFilePath, "../") {
442+
if opts.strict {
443+
return fmt.Errorf("import path %q escapes repository root", importPath)
444+
}
434445
if opts.verbose {
435446
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Skipping import with unsafe path: %q", importPath)))
436447
}
@@ -466,16 +477,25 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
466477
// ".." cannot appear here because remoteFilePath was already rejected above if it
467478
// started with "..", and path.Clean cannot introduce new ".." components.
468479
if localRelPath == "" || localRelPath == "." {
480+
if opts.strict {
481+
return fmt.Errorf("invalid import path %q", importPath)
482+
}
469483
continue
470484
}
471485
targetPath := filepath.Join(opts.targetDir, localRelPath)
472486

473487
// Belt-and-suspenders: verify the resolved path is inside targetDir
474488
absTargetPath, absErr := filepath.Abs(targetPath)
475489
if absErr != nil {
490+
if opts.strict {
491+
return fmt.Errorf("failed to resolve import target path %q: %w", importPath, absErr)
492+
}
476493
continue
477494
}
478495
if rel, relErr := filepath.Rel(absTargetDir, absTargetPath); relErr != nil || strings.HasPrefix(rel, "..") {
496+
if opts.strict {
497+
return fmt.Errorf("refusing to write import outside target directory: %q", importPath)
498+
}
479499
if opts.verbose {
480500
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Refusing to write import outside target directory: %q", importPath)))
481501
}
@@ -498,8 +518,13 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
498518
// any missing transitive dependencies.
499519
if existingContent, readErr := os.ReadFile(targetPath); readErr == nil {
500520
importedBaseDir := path.Dir(remoteFilePath)
501-
fetchFrontmatterImportsRecursive(ctx, string(existingContent), importedBaseDir, opts)
521+
if err := fetchFrontmatterImportsRecursive(ctx, string(existingContent), importedBaseDir, opts); err != nil && opts.strict {
522+
return err
523+
}
502524
} else {
525+
if opts.strict {
526+
return fmt.Errorf("failed to read existing import %s: %w", targetPath, readErr)
527+
}
503528
remoteWorkflowLog.Printf("Failed to read existing import %s for recursion: %v", targetPath, readErr)
504529
}
505530
continue
@@ -513,6 +538,9 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
513538
}
514539
importContent, err := downloadFn(ctx, opts.owner, opts.repo, remoteFilePath, opts.ref)
515540
if err != nil {
541+
if opts.strict {
542+
return fmt.Errorf("failed to fetch import %s: %w", remoteFilePath, err)
543+
}
516544
remoteWorkflowLog.Printf("Failed to download import %s from %s/%s@%s: %v", remoteFilePath, opts.owner, opts.repo, opts.ref, err)
517545
if opts.verbose {
518546
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch import %s: %v", remoteFilePath, err)))
@@ -522,6 +550,9 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
522550

523551
// Create the parent directory if needed
524552
if err := os.MkdirAll(filepath.Dir(targetPath), constants.DirPermPublic); err != nil {
553+
if opts.strict {
554+
return fmt.Errorf("failed to create directory for import %s: %w", remoteFilePath, err)
555+
}
525556
if opts.verbose {
526557
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to create directory for import %s: %v", remoteFilePath, err)))
527558
}
@@ -530,6 +561,9 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
530561

531562
// Write the file
532563
if err := os.WriteFile(targetPath, importContent, constants.FilePermSensitive); err != nil {
564+
if opts.strict {
565+
return fmt.Errorf("failed to write import %s: %w", remoteFilePath, err)
566+
}
533567
if opts.verbose {
534568
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to write import %s: %v", remoteFilePath, err)))
535569
}
@@ -552,13 +586,20 @@ func fetchFrontmatterImportsRecursive(ctx context.Context, content, currentBaseD
552586
// Recurse into the imported file's imports. Use the imported file's directory as
553587
// currentBaseDir so that relative paths inside it resolve correctly.
554588
importedBaseDir := path.Dir(remoteFilePath)
555-
fetchFrontmatterImportsRecursive(ctx, string(importContent), importedBaseDir, opts)
589+
if err := fetchFrontmatterImportsRecursive(ctx, string(importContent), importedBaseDir, opts); err != nil && opts.strict {
590+
return err
591+
}
556592
}
593+
return nil
557594
}
558595

559596
// fetchAndSaveRemoteIncludes parses the workflow content for @include directives and fetches them from the remote source.
560597
// The optional fetchFn parameter overrides the default FetchIncludeFromSource implementation; pass nil to use the default.
561598
func fetchAndSaveRemoteIncludes(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, fetchFn includesFetcher) error {
599+
return fetchAndSaveRemoteIncludesWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, fetchFn, false)
600+
}
601+
602+
func fetchAndSaveRemoteIncludesWithOptions(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, fetchFn includesFetcher, strict bool) error {
562603
remoteWorkflowLog.Printf("Fetching remote includes for workflow: %s", spec.String())
563604
if fetchFn == nil {
564605
fetchFn = FetchIncludeFromSource
@@ -667,7 +708,10 @@ func fetchAndSaveRemoteIncludes(ctx context.Context, content string, spec *Workf
667708
}
668709

669710
// Recursively fetch includes from the fetched file
670-
if err := fetchAndSaveRemoteIncludes(ctx, string(includeContent), spec, targetDir, verbose, force, tracker, fetchFn); err != nil {
711+
if err := fetchAndSaveRemoteIncludesWithOptions(ctx, string(includeContent), spec, targetDir, verbose, force, tracker, fetchFn, strict); err != nil {
712+
if strict {
713+
return fmt.Errorf("failed to fetch nested includes from %s: %w", filePath, err)
714+
}
671715
if verbose {
672716
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch nested includes from %s: %v", filePath, err)))
673717
}
@@ -686,9 +730,20 @@ func fetchAndSaveRemoteIncludes(ctx context.Context, content string, spec *Workf
686730
// verbose is true but do not stop the overall operation.
687731
// - Dispatch-workflow and resource errors are fatal and are returned to the caller.
688732
func fetchAllRemoteDependencies(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error {
733+
return fetchAllRemoteDependenciesWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, false)
734+
}
735+
736+
func fetchAllRemoteDependenciesStrict(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker) error {
737+
return fetchAllRemoteDependenciesWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, true)
738+
}
739+
740+
func fetchAllRemoteDependenciesWithOptions(ctx context.Context, content string, spec *WorkflowSpec, targetDir string, verbose bool, force bool, tracker *FileTracker, strict bool) error {
689741
remoteWorkflowLog.Printf("Fetching all remote dependencies: spec=%s, targetDir=%s, force=%v", spec.String(), targetDir, force)
690742
// Fetch and save @include directive dependencies (best-effort: errors are not fatal).
691-
if err := fetchAndSaveRemoteIncludes(ctx, content, spec, targetDir, verbose, force, tracker, nil); err != nil {
743+
if err := fetchAndSaveRemoteIncludesWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, nil, strict); err != nil {
744+
if strict {
745+
return fmt.Errorf("failed to fetch include dependencies: %w", err)
746+
}
692747
if verbose {
693748
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch include dependencies: %v", err)))
694749
}
@@ -697,14 +752,20 @@ func fetchAllRemoteDependencies(ctx context.Context, content string, spec *Workf
697752
// locally during compilation. Keeping these as relative paths (not workflowspecs)
698753
// ensures the compiler resolves them from disk rather than downloading from GitHub.
699754
// Best-effort: errors are not fatal.
700-
if err := fetchAndSaveRemoteFrontmatterImports(ctx, content, spec, targetDir, verbose, force, tracker); err != nil {
755+
if err := fetchAndSaveRemoteFrontmatterImportsWithOptions(ctx, content, spec, targetDir, verbose, force, tracker, strict); err != nil {
756+
if strict {
757+
return fmt.Errorf("failed to fetch frontmatter import dependencies: %w", err)
758+
}
701759
if verbose {
702760
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to fetch frontmatter import dependencies: %v", err)))
703761
}
704762
}
705763
// Fetch and save required runtime-import dependencies so installs include the
706764
// explicit runtime-import closure without copying unrelated .github contents.
707765
if err := fetchAndSaveRemoteRuntimeImports(ctx, content, spec, targetDir, verbose, force, tracker); err != nil {
766+
if strict {
767+
return fmt.Errorf("failed to fetch runtime-import dependencies: %w", err)
768+
}
708769
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Failed to fetch runtime-import dependencies; activation may fail"))
709770
if verbose {
710771
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(err.Error()))

pkg/cli/update_manifest.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,17 @@ type manifestManagedWorkflowUpdate struct {
2525
manifestSource string
2626
}
2727

28+
func fetchManifestManagedDependencies(ctx context.Context, content []byte, repo, workflowPath, ref, targetDir string, verbose bool) error {
29+
spec := &WorkflowSpec{
30+
RepoSpec: RepoSpec{
31+
RepoSlug: repo,
32+
Version: ref,
33+
},
34+
WorkflowPath: workflowPath,
35+
}
36+
return fetchAllRemoteDependenciesStrict(ctx, string(content), spec, targetDir, verbose, true, nil)
37+
}
38+
2839
func parseManifestSourceSpec(source string) (*RepoSpec, bool, error) {
2940
repoSpec, ok, err := parseRepositoryPackageSpec(strings.TrimSpace(source))
3041
if !ok {
@@ -275,6 +286,9 @@ func updateManifestManagedWorkflow(ctx context.Context, update manifestManagedWo
275286
}
276287
}
277288

289+
if err := fetchManifestManagedDependencies(ctx, newContent, update.repo, update.latestPath, update.latestRef, filepath.Dir(update.wf.Path), opts.Verbose); err != nil {
290+
return fmt.Errorf("failed to update workflow dependencies: %w", err)
291+
}
278292
if err := os.WriteFile(update.wf.Path, []byte(finalContent), constants.FilePermPublic); err != nil {
279293
return fmt.Errorf("failed to write updated workflow: %w", err)
280294
}
@@ -320,6 +334,9 @@ func addManifestManagedWorkflow(ctx context.Context, targetDir, name, repo, late
320334
}
321335

322336
destPath := filepath.Join(targetDir, name+".md")
337+
if err := fetchManifestManagedDependencies(ctx, []byte(content), repo, latestPath, latestRef, targetDir, opts.Verbose); err != nil {
338+
return fmt.Errorf("failed to install workflow dependencies: %w", err)
339+
}
323340
if err := os.WriteFile(destPath, []byte(content), constants.FilePermPublic); err != nil {
324341
return fmt.Errorf("failed to write new manifest workflow %s: %w", destPath, err)
325342
}

0 commit comments

Comments
 (0)