Skip to content

Commit 9f023fd

Browse files
committed
fix: improve path handling on Windows (#155)
1 parent 83e1963 commit 9f023fd

12 files changed

+70
-27
lines changed

.github/workflows/test.yml

+8-1
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ on:
44
pull_request:
55
workflow_call:
66

7+
env:
8+
DOCKER_DEFAULT_PLATFORM: linux/amd64
9+
710
jobs:
811
test:
912
name: Run tests
10-
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
os: [ubuntu-latest, windows-latest]
17+
runs-on: ${{ matrix.os }}
1118
steps:
1219
- uses: actions/checkout@v4
1320

internal/cli/execute.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ func executeRecipe(cmd *cobra.Command, opts executeOptions, re *recipe.Recipe) e
212212
return fmt.Errorf("%w\n\n%s", err, retryMessage)
213213
}
214214

215-
sauce.SubPath = opts.Subpath
215+
sauce.SubPath = filepath.ToSlash(opts.Subpath)
216216

217217
// Automatically add recipe origin if the recipe was remote
218218
if recipe.DetermineRecipeURLType(opts.RecipeURL) == recipe.OCIType {
@@ -240,7 +240,7 @@ func executeRecipe(cmd *cobra.Command, opts executeOptions, re *recipe.Recipe) e
240240
if opts.Subpath != "" {
241241
files = make(map[string]recipe.File, len(sauce.Files))
242242
for path, file := range sauce.Files {
243-
files[filepath.Join(opts.Subpath, path)] = file
243+
files[filepath.ToSlash(filepath.Join(opts.Subpath, path))] = file
244244
}
245245
}
246246

internal/cli/why.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ func runWhy(cmd *cobra.Command, opts whyOptions) error {
7878

7979
for _, sauce := range sauces {
8080
for file := range sauce.Files {
81-
if fileinfo.IsDir() {
82-
if strings.HasPrefix(file, opts.Filepath) {
83-
cmd.Printf("Directory '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID)
84-
return nil
85-
}
81+
cleanedFilePath := filepath.Clean(file)
82+
if fileinfo.IsDir() && strings.HasPrefix(cleanedFilePath, opts.Filepath) {
83+
cmd.Printf("Directory '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID)
84+
return nil
8685
}
87-
if opts.Filepath == file {
86+
87+
if opts.Filepath == cleanedFilePath {
8888
// TODO: Check if the file is modified by the user by comparing hashes
8989
cmd.Printf("File '%s' is created by the recipe '%s' (sauce ID %s).\n", opts.Filepath, sauce.Recipe.Name, sauce.ID)
9090
return nil

pkg/recipe/execute.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package recipe
33
import (
44
"errors"
55
"maps"
6+
"path/filepath"
67
"strings"
78

89
"github.com/gofrs/uuid"
@@ -72,7 +73,7 @@ func (re *Recipe) Execute(engine RenderEngine, values VariableValues, id uuid.UU
7273
continue
7374
}
7475

75-
filename = strings.TrimSuffix(filename, re.TemplateExtension)
76+
filename = filepath.ToSlash(strings.TrimSuffix(filename, re.TemplateExtension))
7677

7778
sauce.Files[filename] = NewFile(content)
7879
idx += 1

test/execute_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"path/filepath"
7+
"runtime"
78
"strings"
89

910
"github.com/cucumber/godog"
@@ -63,6 +64,11 @@ func iExecuteRemoteRecipe(ctx context.Context, repository string) (context.Conte
6364

6465
func recipesWillBeExecutedToTheSubPath(ctx context.Context, path string) (context.Context, error) {
6566
additionalFlags := ctx.Value(cmdAdditionalFlagsCtxKey{}).(map[string]string)
67+
68+
if runtime.GOOS == "windows" && path[0] == '/' {
69+
path = filepath.Clean(filepath.Join("C:/", path[1:]))
70+
}
71+
6672
additionalFlags["subpath"] = path
6773

6874
return ctx, nil

test/features/check-file-origin.feature

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Feature: Check origin of a file in project directory using "why" command
1212
And recipe "foo" generates file "foo/bar.yml" with content "initial"
1313
And I execute recipe "foo"
1414
When I check why the file "foo/bar.yml" is created
15-
Then CLI produced an output "File 'foo/bar.yml' is created by the recipe 'foo'"
15+
Then CLI produced an output "File 'foo[/|\\]bar.yml' is created by the recipe 'foo'"
1616

1717
Scenario: Directory generated by a recipe
1818
Given a recipe "foo"
@@ -33,11 +33,11 @@ Feature: Check origin of a file in project directory using "why" command
3333
And recipe "foo" generates file "README.md" with content "initial"
3434
And I execute recipe "foo"
3535
When I check why the file ".jalapeno/sauces.yml" is created
36-
Then CLI produced an output "File '.jalapeno/sauces.yml' is created by Jalapeno"
36+
Then CLI produced an output "File '.jalapeno[/|\\]sauces.yml' is created by Jalapeno"
3737

3838
Scenario: File not found
3939
Given a recipe "foo"
4040
And recipe "foo" generates file "README.md" with content "initial"
4141
And I execute recipe "foo"
4242
When I check why the file "not-found" is created
43-
Then CLI produced an error "file '.*/not-found' does not exist"
43+
Then CLI produced an error "file '.*[/|\\]not-found' does not exist"

test/features/check-recipes.feature

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
Feature: Check for new recipe versions
2-
2+
@docker
33
Scenario: Find newer version for a recipe
44
Given a recipe "foo"
55
And recipe "foo" generates file "README.md" with content "initial"
@@ -16,6 +16,7 @@ Feature: Check for new recipe versions
1616
Then CLI produced an output "new versions found: v0\.0\.2"
1717
Then CLI produced an output "To upgrade recipes to the latest version run:\n (.*) upgrade oci://localhost:\d+/foo:v0.0.2\n"
1818

19+
@docker
1920
Scenario: Find multiple newer version for a recipe
2021
Given a recipe "foo"
2122
And recipe "foo" generates file "README.md" with content "initial"
@@ -33,6 +34,7 @@ Feature: Check for new recipe versions
3334
Then CLI produced an output "new versions found: v0\.0\.2, v0\.0\.3"
3435
Then CLI produced an output "To upgrade recipes to the latest version run:\n (.*) upgrade oci://localhost:\d+/foo:v0\.0\.3\n"
3536

37+
@docker
3638
Scenario: Find newer version for multiple recipes
3739
Given a recipe "foo"
3840
And recipe "foo" generates file "foo.md" with content "initial"
@@ -59,6 +61,7 @@ Feature: Check for new recipe versions
5961
Then CLI produced an output "foo: new versions found: v0\.0\.2"
6062
And CLI produced an output "bar: new versions found: v0\.0\.2"
6163

64+
@docker
6265
Scenario: Unable to find newer recipe versions
6366
Given a recipe "foo"
6467
And recipe "foo" generates file "README.md" with content "initial"
@@ -73,6 +76,7 @@ Feature: Check for new recipe versions
7376
And I check new versions for recipe "foo"
7477
Then CLI produced an output "no new versions found"
7578

79+
@docker
7680
Scenario: Unable to find newer recipe versions for all recipes
7781
Given a recipe "foo"
7882
And recipe "foo" generates file "foo.md" with content "initial"
@@ -95,6 +99,7 @@ Feature: Check for new recipe versions
9599
Then CLI produced an output "foo: new versions found: v0\.0\.2"
96100
And CLI produced an output "bar: no new versions found"
97101

102+
@docker
98103
Scenario: Executing remote recipe automatically adds the repo as source for the sauce
99104
Given a recipe "foo"
100105
And recipe "foo" generates file "README.md" with content "initial"
@@ -108,6 +113,7 @@ Feature: Check for new recipe versions
108113
And I check new versions for recipe "foo"
109114
Then CLI produced an output "new versions found: v0\.0\.2"
110115

116+
@docker
111117
Scenario: Manually override the check from URL for locally executed recipe
112118
Given a recipe "foo"
113119
And recipe "foo" generates file "README.md" with content "initial"
@@ -121,6 +127,7 @@ Feature: Check for new recipe versions
121127
Then CLI produced an output "new versions found: v0\.0\.2"
122128
And the sauce in index 0 which should have property "CheckFrom" with value "^oci://localhost:\d+/foo$"
123129

130+
@docker
124131
Scenario: Find and upgrade newer version for recipes
125132
Given a recipe "foo"
126133
And recipe "foo" generates file "README.md" with content "initial"
@@ -136,6 +143,7 @@ Feature: Check for new recipe versions
136143
Then CLI produced an output "new versions found: v0\.0\.2"
137144
Then CLI produced an output "Upgrade completed"
138145

146+
@docker
139147
Scenario: Find and upgrade newer version for a specific recipe
140148
Given a recipe "foo"
141149
And recipe "foo" generates file "README.md" with content "initial"
@@ -157,6 +165,7 @@ Feature: Check for new recipe versions
157165
Then CLI produced an output "new versions found: v0\.0\.2"
158166
Then CLI produced an output "Upgrade completed"
159167

168+
@docker
160169
Scenario: Find and upgrade newer versions for multiple recipes
161170
Given a recipe "foo"
162171
And recipe "foo" generates file "foo.md" with content "initial"

test/features/execute-manifest.feature

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Feature: Execute manifests
1717
And the project directory should contain file "foo.md"
1818
And the project directory should contain file "bar.md"
1919

20+
@docker
2021
Scenario: Execute a manifest with remote recipes
2122
Given a local OCI registry
2223
And a recipe "foo"

test/features/execute-recipes.feature

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Feature: Execute recipes
1010
And the sauce in index 0 which should have property "Recipe.Name" with value "^foo$"
1111
And the sauce in index 0 which has a valid ID
1212

13+
@docker
1314
Scenario: Execute single recipe from remote registry
1415
Given a recipe "foo"
1516
And recipe "foo" generates file "README.md" with content "initial"

test/features/recipes-as-oci-artifacts.feature

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ Feature: Recipes as OCI artifacts
22
By pushing and pulling recipes as artifacts to OCI compatible repositories, we can improve
33
recipe availability and discoverability
44

5+
@docker
56
Scenario: Push a recipe to OCI repository
67
Given a recipe "foo"
78
And recipe "foo" generates file "README.md" with content "initial"
89
And a local OCI registry
910
When I push the recipe "foo" to the local OCI repository
1011
Then no errors were printed
11-
12+
@docker
1213
Scenario: Pull a recipe from OCI repository
1314
Given a recipe "foo"
1415
And recipe "foo" generates file "README.md" with content "initial"
@@ -18,6 +19,7 @@ Feature: Recipes as OCI artifacts
1819
Then no errors were printed
1920
And the project directory should contain file "foo/recipe.yml"
2021

22+
@docker
2123
Scenario: Push a recipe to OCI repository using the 'latest' tag
2224
Given a recipe "foo"
2325
And recipe "foo" generates file "README.md" with content "initial"
@@ -28,6 +30,7 @@ Feature: Recipes as OCI artifacts
2830
Then no errors were printed
2931
And the project directory should contain file "foo/recipe.yml" with "version: v0.0.1"
3032

33+
@docker
3134
Scenario: Pushing a recipe to OCI repository using the 'latest' tag pushes the version tag also
3235
Given a recipe "foo"
3336
And recipe "foo" generates file "README.md" with content "initial"
@@ -38,6 +41,7 @@ Feature: Recipes as OCI artifacts
3841
Then no errors were printed
3942
And the project directory should contain file "foo/recipe.yml" with "version: v0.0.1"
4043

44+
@docker
4145
Scenario: Pushing a recipe to OCI repository using the 'latest' tag replaces the previous tag
4246
Given a recipe "foo"
4347
And recipe "foo" generates file "README.md" with content "initial"
@@ -51,13 +55,15 @@ Feature: Recipes as OCI artifacts
5155
Then no errors were printed
5256
And the project directory should contain file "foo/recipe.yml" with "version: v0.0.2"
5357

58+
@docker
5459
Scenario: Push a recipe to OCI repository with authentication
5560
Given a recipe "foo"
5661
And recipe "foo" generates file "README.md" with content "initial"
5762
And a local OCI registry with authentication
5863
When I push the recipe "foo" to the local OCI repository
5964
Then no errors were printed
6065

66+
@docker
6167
Scenario: Pull a recipe from OCI repository with authentication
6268
Given a recipe "foo"
6369
And recipe "foo" generates file "README.md" with content "initial"
@@ -66,7 +72,8 @@ Feature: Recipes as OCI artifacts
6672
When I pull recipe from the local OCI repository "foo:v0.0.1"
6773
Then no errors were printed
6874
And the project directory should contain file "foo/recipe.yml"
69-
75+
76+
@docker
7077
Scenario: Try to push a recipe to OCI repository without authentication
7178
Given a recipe "foo"
7279
And recipe "foo" generates file "README.md" with content "initial"
@@ -75,11 +82,13 @@ Feature: Recipes as OCI artifacts
7582
When I push the recipe "foo" to the local OCI repository
7683
Then CLI produced an error "basic credential not found"
7784

85+
@docker
7886
Scenario: Try to pull a recipe from OCI repository which not exist
7987
Given a local OCI registry with authentication
8088
When I pull recipe from the local OCI repository "foo:v0.0.1"
8189
Then CLI produced an error "recipe not found"
8290

91+
@docker
8392
Scenario: Push a recipe from OCI repository using credentials from config file
8493
Given a recipe "foo"
8594
And recipe "foo" generates file "README.md" with content "initial"

test/features/upgrade-recipe.feature

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Feature: Upgrade sauce
9595
And the project directory should contain file "./foo/README.md" with "New version"
9696
And the project directory should contain file "./bar/README.md" with "initial"
9797

98+
@docker
9899
Scenario: Upgrade sauce from remote recipe
99100
Given a local OCI registry
100101
And a recipe "foo"

test/main_test.go

+19-11
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,21 @@ const (
6767
*/
6868

6969
func TestFeatures(t *testing.T) {
70+
opts := &godog.Options{
71+
Format: "pretty",
72+
Strict: true,
73+
Concurrency: runtime.NumCPU(),
74+
Paths: []string{"features"},
75+
TestingT: t,
76+
}
77+
78+
// Skip tests needing OCI registry on Windows because there is no windows/amd64 image available
79+
if runtime.GOOS == "windows" {
80+
opts.Tags = "~@docker"
81+
}
82+
7083
suite := godog.TestSuite{
71-
Options: &godog.Options{
72-
Format: "pretty",
73-
Strict: true,
74-
Concurrency: runtime.NumCPU(),
75-
Paths: []string{"features"},
76-
TestingT: t,
77-
},
84+
Options: opts,
7885
ScenarioInitializer: func(s *godog.ScenarioContext) {
7986
AddCommonSteps(s)
8087

@@ -335,7 +342,7 @@ func iRemoveFileFromTheRecipe(ctx context.Context, filename, recipeName string)
335342
}
336343

337344
func aLocalOCIRegistry(ctx context.Context) (context.Context, error) {
338-
resource, err := createLocalRegistry(&dockertest.RunOptions{Repository: "registry", Tag: "2"})
345+
resource, err := createLocalRegistry(&dockertest.RunOptions{Repository: "registry", Tag: "2", Platform: "linux/amd64"})
339346
if err != nil {
340347
return ctx, err
341348
}
@@ -360,6 +367,7 @@ func aLocalOCIRegistryWithAuth(ctx context.Context) (context.Context, error) {
360367
resource, err := createLocalRegistry(&dockertest.RunOptions{
361368
Repository: "registry",
362369
Tag: "2",
370+
Platform: "linux/amd64",
363371
Env: []string{
364372
"REGISTRY_AUTH_HTPASSWD_REALM=jalapeno-test-realm",
365373
fmt.Sprintf("REGISTRY_AUTH_HTPASSWD_PATH=/auth/%s", HTPASSWD_FILENAME),
@@ -429,7 +437,7 @@ func iClearTheOutput(ctx context.Context) (context.Context, error) {
429437

430438
func theProjectDirectoryShouldContainFile(ctx context.Context, filename string) error {
431439
dir := ctx.Value(projectDirectoryPathCtxKey{}).(string)
432-
info, err := os.Stat(filepath.Join(dir, filename))
440+
info, err := os.Stat(filepath.Join(dir, filepath.Clean(filename)))
433441
if err == nil && !info.Mode().IsRegular() {
434442
return fmt.Errorf("%s is not a regular file", filename)
435443
}
@@ -438,11 +446,11 @@ func theProjectDirectoryShouldContainFile(ctx context.Context, filename string)
438446

439447
func iCreateAFileWithContentsToTheProjectDir(ctx context.Context, filename, contents string) error {
440448
dir := ctx.Value(projectDirectoryPathCtxKey{}).(string)
441-
return os.WriteFile(filepath.Join(dir, filename), []byte(contents), 0644)
449+
return os.WriteFile(filepath.Join(dir, filepath.Clean(filename)), []byte(contents), 0644)
442450
}
443451

444452
func theProjectDirectoryShouldContainFileWith(ctx context.Context, filename, searchTerm string) error {
445-
content, err := readProjectDirectoryFile(ctx, filename)
453+
content, err := readProjectDirectoryFile(ctx, filepath.Clean(filename))
446454
if err != nil {
447455
return err
448456
}

0 commit comments

Comments
 (0)