From 16d16b3ec1594087b0359d644ec285ad44b11c15 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Mon, 9 Nov 2026 16:03:48 -0300 Subject: [PATCH] features: each run stages its Features in a folder of its own The features folder is /container-features/-, TS createFeaturesTempFolder's name, made with MkdirAll. Two runs started in the same millisecond shared it, and one run's realignFeatureDirs renamed and removed the other's .stage- folders: "stage feature h_0: lstat .../.stage-0: no such file or directory", which failed main's parity job on 2026-11-07 and 2026-11-08. The folder is now created with Mkdir, and a name another run holds moves to the next millisecond. The compose path that made the folder by hand goes through the same function. Co-Authored-By: Claude Opus 5.5 --- internal/cli/compose_up.go | 6 +++--- internal/cli/feature_install.go | 19 ++++++++++++++++--- internal/cli/feature_install_test.go | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/internal/cli/compose_up.go b/internal/cli/compose_up.go index e1bcc04..4573cc5 100644 --- a/internal/cli/compose_up.go +++ b/internal/cli/compose_up.go @@ -469,9 +469,9 @@ func (r *upRunner) buildAndExtendCompose(ctx context.Context, s composeStart, pe if opts.omitSyntaxDirective { syntax = "" } - dstFolder = filepath.Join(cacheFolder(), "container-features", fmt.Sprintf("%s-%d", cliVersion(), time.Now().UnixMilli())) - if err := os.MkdirAll(dstFolder, 0o755); err != nil { - return nil, fmt.Errorf("create features folder: %w", err) + var err error + if dstFolder, err = createFeaturesFolder(); err != nil { + return nil, err } info = imagemeta.GenerateComposeLabelBuild(baseName, labelMetadata, syntax) default: diff --git a/internal/cli/feature_install.go b/internal/cli/feature_install.go index 8b09244..d0b814f 100644 --- a/internal/cli/feature_install.go +++ b/internal/cli/feature_install.go @@ -127,12 +127,25 @@ func (r localFeatureRoots) resolve(logger log.Logger, id string) (string, bool, // createFeaturesFolder creates the folder the Features are staged in, TS // createFeaturesTempFolder: /container-features/-. +// The folder is this run's alone: runs started in the same millisecond would +// otherwise share it, and one run's realignFeatureDirs renames and removes the +// other's .stage- folders. A name another run holds moves to the next +// millisecond. func createFeaturesFolder() (string, error) { - dir := filepath.Join(cacheFolder(), "container-features", fmt.Sprintf("%s-%d", cliVersion(), time.Now().UnixMilli())) - if err := os.MkdirAll(dir, 0o755); err != nil { + parent := filepath.Join(cacheFolder(), "container-features") + if err := os.MkdirAll(parent, 0o755); err != nil { return "", fmt.Errorf("create features folder: %w", err) } - return dir, nil + for ms := time.Now().UnixMilli(); ; ms++ { + dir := filepath.Join(parent, fmt.Sprintf("%s-%d", cliVersion(), ms)) + err := os.Mkdir(dir, 0o755) + if err == nil { + return dir, nil + } + if !errors.Is(err, os.ErrExist) { + return "", fmt.Errorf("create features folder: %w", err) + } + } } // fetchFeatureSetsWithOrder resolves the feature dependency graph through the diff --git a/internal/cli/feature_install_test.go b/internal/cli/feature_install_test.go index 8f19669..08716e4 100644 --- a/internal/cli/feature_install_test.go +++ b/internal/cli/feature_install_test.go @@ -460,3 +460,22 @@ func TestReadConfigurationRefusesALocalFeatureOutsideDevcontainer(t *testing.T) } wantExitCode(t, err, 1) } + +// Runs started in the same millisecond each get a features folder of their +// own: a shared one let one run's realignFeatureDirs remove the other's +// .stage- folders ("stage feature h_0: lstat .../.stage-0: no such file or +// directory" in the parity lane). +func TestCreateFeaturesFolderIsTheRunsAlone(t *testing.T) { + t.Setenv("TMPDIR", t.TempDir()) + seen := map[string]bool{} + for range 50 { + dir, err := createFeaturesFolder() + if err != nil { + t.Fatal(err) + } + if seen[dir] { + t.Fatalf("%s handed out twice", dir) + } + seen[dir] = true + } +}