Skip to content
Merged
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
11 changes: 10 additions & 1 deletion internal/projectbuild/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
cp "github.com/otiai10/copy"

"github.com/shopware/shopware-cli/internal/archiver"
"github.com/shopware/shopware-cli/internal/ci"
"github.com/shopware/shopware-cli/internal/shop"
"github.com/shopware/shopware-cli/logging"
)
Expand All @@ -28,6 +29,7 @@ type ArchiveOptions struct {

// PackageArchive builds a private, temporary copy and publishes a tar.gz.
// Existing output files are never overwritten. This does not deploy anything.
// Composer rebuilds vendor unless installation is disabled.
// Hooks are trusted project code, not sandboxed processes.
func PackageArchive(ctx context.Context, root string, cfg *shop.Config, env *shop.EnvironmentConfig, opts ArchiveOptions) (string, error) {
opts.Build.MirrorPathRepositories = true
Expand Down Expand Up @@ -87,7 +89,11 @@ func packageArchive(ctx context.Context, root string, cfg *shop.Config, opts Arc
return "", err
}
stage := filepath.Join(temp, "project")
if err := copyPackageSource(ctx, root, stage, temp, func(name string) bool { return archiveExcluded(name, false) }); err != nil {
excludeSource := func(name string) bool {
// Keep prebuilt dependencies only when Composer installation is disabled.
return (name == "vendor" && !cfg.DisableComposerInstall) || archiveExcluded(name, false)
Comment thread
shyim marked this conversation as resolved.
Resolved
}
if err := copyPackageSource(ctx, root, stage, temp, excludeSource); err != nil {
return "", fmt.Errorf("copy project: %w", err)
}
if err := validateBuildSymlinks(ctx, stage); err != nil {
Expand Down Expand Up @@ -130,6 +136,9 @@ func writeDeploymentConfig(stage string, cfg *shop.Config) error {
// copyPackageSource copies links without following them. The caller validates
// the copied links before building in the staging tree.
func copyPackageSource(ctx context.Context, root, stage, temp string, exclude func(string) bool) error {
section := ci.Start("Copying project")
defer section.End()

return cp.Copy(root, stage, cp.Options{
PreserveTimes: true,
OnSymlink: func(string) cp.SymlinkAction { return cp.Shallow },
Expand Down
59 changes: 59 additions & 0 deletions internal/projectbuild/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,65 @@ func TestPackageArchiveUsesTemporaryCopy(t *testing.T) {
}
}

func TestPackageArchiveCopiesVendorOnlyWhenComposerInstallDisabled(t *testing.T) {
for _, tc := range []struct {
name string
disableComposerInstall bool
}{
{"install dependencies in staging", false},
{"use prebuilt dependencies", true},
} {
t.Run(tc.name, func(t *testing.T) {
root := t.TempDir()
testhelper.WriteFile(t, filepath.Join(root, "composer.json"), "{}")
testhelper.WriteFile(t, filepath.Join(root, "vendor/local-only.txt"), "local dependency")
// Nested vendor directories can be application sources, not Composer output.
testhelper.WriteFile(t, filepath.Join(root, "src/vendor/source.txt"), "nested source")
cfg := &shop.Config{DisableComposerInstall: tc.disableComposerInstall}
reference, err := packageArchive(t.Context(), root, cfg, ArchiveOptions{}, func(_ context.Context, stage string) error {
if tc.disableComposerInstall {
assert.FileExists(t, filepath.Join(stage, "vendor/local-only.txt"))
} else {
assert.NoDirExists(t, filepath.Join(stage, "vendor"))
}
assert.FileExists(t, filepath.Join(stage, "src/vendor/source.txt"))
testhelper.WriteFile(t, filepath.Join(stage, "vendor/built.txt"), "built dependency")
return nil
})
require.NoError(t, err)
contents := readArchive(t, reference)
assert.Equal(t, "built dependency", contents["vendor/built.txt"], "staging exclusions must not exclude built dependencies from the archive")
_, copiedVendor := contents["vendor/local-only.txt"]
assert.Equal(t, tc.disableComposerInstall, copiedVendor)
assert.Equal(t, "nested source", contents["src/vendor/source.txt"])
source, err := os.ReadFile(filepath.Join(root, "vendor/local-only.txt"))
require.NoError(t, err)
assert.Equal(t, "local dependency", string(source))
assert.NoFileExists(t, filepath.Join(root, "vendor/built.txt"))
})
}
}

func TestPackageArchiveSkipsSourceVendorSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("symlinks require privileges on Windows")
}
root := t.TempDir()
outside := t.TempDir()
testhelper.WriteFile(t, filepath.Join(outside, "local-only.txt"), "external dependency")
require.NoError(t, os.Symlink(outside, filepath.Join(root, "vendor")))
reference, err := packageArchive(t.Context(), root, &shop.Config{}, ArchiveOptions{}, func(_ context.Context, stage string) error {
_, err := os.Lstat(filepath.Join(stage, "vendor"))
assert.ErrorIs(t, err, os.ErrNotExist)
testhelper.WriteFile(t, filepath.Join(stage, "vendor/built.txt"), "built dependency")
return nil
})
require.NoError(t, err)
assert.Equal(t, "built dependency", readArchive(t, reference)["vendor/built.txt"])
assert.FileExists(t, filepath.Join(outside, "local-only.txt"))
assert.NoFileExists(t, filepath.Join(outside, "built.txt"))
}

func TestPackageArchiveFailureCleansUp(t *testing.T) {
root := t.TempDir()
output := filepath.Join(t.TempDir(), "archive.tar.gz")
Expand Down
Loading