Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions pkg/ci/migrate/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"os"
"path/filepath"
"strings"
)

// CopyMode controls behavior when destination already exists
Expand Down Expand Up @@ -165,3 +166,82 @@ func CopyGitHubToDepot(repoRoot string, dirs []string, mode CopyMode) (*CopyResu

return result, nil
}

// CopyWorkflowSiblings mirrors non-YAML files living under .github/workflows/ into
// .depot/workflows/, preserving relative layout and permissions. The workflow YAML
// itself is produced by the transform pipeline; this copies sibling assets (helper
// scripts, configs, templates) that workflows reference so those references resolve
// after migration. Symlinks are skipped. Returns the destination paths written.
func CopyWorkflowSiblings(srcWorkflowsDir, destWorkflowsDir string) ([]string, error) {
var copied []string

err := filepath.WalkDir(srcWorkflowsDir, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}

ext := strings.ToLower(filepath.Ext(path))
if ext == ".yml" || ext == ".yaml" {
return nil
}

info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return nil
}

relPath, err := filepath.Rel(srcWorkflowsDir, path)
if err != nil {
return err
}
destPath := filepath.Join(destWorkflowsDir, relPath)

if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", filepath.Dir(destPath), err)
}
if err := copyFile(path, destPath, info.Mode().Perm()); err != nil {
return err
}

copied = append(copied, destPath)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk .github/workflows for sibling files: %w", err)
}

return copied, nil
}

// copyFile copies a regular file from src to dest, truncating dest if it exists.
func copyFile(src, dest string, perm os.FileMode) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file %s: %w", src, err)
}
defer srcFile.Close()

destFile, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
if err != nil {
return fmt.Errorf("failed to create destination file %s: %w", dest, err)
}
defer destFile.Close()

if _, err := io.Copy(destFile, srcFile); err != nil {
return fmt.Errorf("failed to copy file %s to %s: %w", src, dest, err)
}

// OpenFile only applies perm when creating the file; when overwriting an
// existing sibling (a re-run over an already-migrated tree) the old mode
// sticks, so set it explicitly to keep the source's permissions.
if err := os.Chmod(dest, perm); err != nil {
return fmt.Errorf("failed to set permissions on %s: %w", dest, err)
}
return nil
}
90 changes: 90 additions & 0 deletions pkg/ci/migrate/copy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,93 @@ func TestCopyMissingSubDir(t *testing.T) {
t.Errorf("workflows/ci.yml not found: %v", err)
}
}

// TestCopyWorkflowSiblings verifies that non-YAML sibling files under
// .github/workflows/ are mirrored into .depot/workflows/ while YAML workflows and
// symlinks are skipped.
func TestCopyWorkflowSiblings(t *testing.T) {
tmpDir := t.TempDir()
srcDir := filepath.Join(tmpDir, ".github", "workflows")
if err := os.MkdirAll(filepath.Join(srcDir, "scripts"), 0755); err != nil {
t.Fatalf("failed to create source dir: %v", err)
}

// YAML workflow — should NOT be copied by this helper.
if err := os.WriteFile(filepath.Join(srcDir, "ci.yml"), []byte("name: CI\n"), 0644); err != nil {
t.Fatal(err)
}
// Sibling script at the top level and in a subdirectory — should be copied.
if err := os.WriteFile(filepath.Join(srcDir, "helper.sh"), []byte("#!/bin/sh\necho hi\n"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(srcDir, "scripts", "deploy.sh"), []byte("#!/bin/sh\n"), 0644); err != nil {
t.Fatal(err)
}

destDir := filepath.Join(tmpDir, ".depot", "workflows")
copied, err := CopyWorkflowSiblings(srcDir, destDir)
if err != nil {
t.Fatalf("CopyWorkflowSiblings failed: %v", err)
}

if len(copied) != 2 {
t.Errorf("expected 2 sibling files copied, got %d: %v", len(copied), copied)
}

// YAML must not be mirrored by this helper.
if _, err := os.Stat(filepath.Join(destDir, "ci.yml")); !os.IsNotExist(err) {
t.Errorf("expected ci.yml NOT to be copied, stat err: %v", err)
}

// Siblings must exist with content and layout preserved.
helper := filepath.Join(destDir, "helper.sh")
if data, err := os.ReadFile(helper); err != nil {
t.Errorf("expected helper.sh copied: %v", err)
} else if !strings.Contains(string(data), "echo hi") {
t.Errorf("helper.sh content not preserved: %q", data)
}
// Executable bit preserved.
if info, err := os.Stat(helper); err != nil {
t.Errorf("stat helper.sh: %v", err)
} else if info.Mode().Perm()&0100 == 0 {
t.Errorf("expected helper.sh to stay executable, got %v", info.Mode().Perm())
}
if _, err := os.Stat(filepath.Join(destDir, "scripts", "deploy.sh")); err != nil {
t.Errorf("expected scripts/deploy.sh copied preserving layout: %v", err)
}
}

// TestCopyWorkflowSiblings_OverwritePreservesMode verifies that re-copying over an
// existing destination file resets its mode to match the source, since OpenFile only
// applies the mode when creating a new file.
func TestCopyWorkflowSiblings_OverwritePreservesMode(t *testing.T) {
tmpDir := t.TempDir()
srcDir := filepath.Join(tmpDir, ".github", "workflows")
destDir := filepath.Join(tmpDir, ".depot", "workflows")
if err := os.MkdirAll(srcDir, 0755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(destDir, 0755); err != nil {
t.Fatal(err)
}

// Executable source, but a pre-existing non-executable destination.
if err := os.WriteFile(filepath.Join(srcDir, "helper.sh"), []byte("#!/bin/sh\n"), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(destDir, "helper.sh"), []byte("stale\n"), 0600); err != nil {
t.Fatal(err)
}

if _, err := CopyWorkflowSiblings(srcDir, destDir); err != nil {
t.Fatalf("CopyWorkflowSiblings failed: %v", err)
}

info, err := os.Stat(filepath.Join(destDir, "helper.sh"))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0755 {
t.Errorf("expected overwritten sibling mode 0755, got %v", info.Mode().Perm())
}
}
Loading
Loading