Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple <<includes(file)>> and embedded <<includes(file)>> #737

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
27 changes: 18 additions & 9 deletions integration_tests/features/orb_pack.feature
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,27 @@ Feature: Orb pack
And the exit status should be 0

@mocked_home_directory
Scenario: Orb pack with multiple includes fails
Scenario: Orb pack with multiple includes
Given a file named "src/@orb.yml" with:
"""
commands:
test:
steps:
- run:
command: <<include(script.sh)>> <<include(script.sh)>>
command: <<include(script.sh)>> <<include(script2.sh)>>
"""
Given a file named "src/script.sh" with "echo Hello, world!"
Given a file named "src/script.sh" with "echo Hello,"
Given a file named "src/script2.sh" with "world!"
When I run `circleci orb pack src`
Then the output should contain:
"""
Error: An unexpected error occurred: multiple include statements: '<<include(script.sh)>> <<include(script.sh)>>'
commands:
test:
steps:
- run:
command: echo Hello, world!
"""
And the exit status should be 255
And the exit status should be 0

@mocked_home_directory
Scenario: Orb pack with include statement in bigger string
Expand All @@ -47,15 +52,19 @@ Feature: Orb pack
test:
steps:
- run:
command: include <<include(script.sh)>>
command: echo "<<include(script.sh)>>"
"""
Given a file named "src/script.sh" with "echo Hello, world!"
Given a file named "src/script.sh" with "Hello, world!"
When I run `circleci orb pack src`
Then the output should contain:
"""
Error: An unexpected error occurred: entire string must be include statement: 'include <<include(script.sh)>>'
commands:
test:
steps:
- run:
command: echo "Hello, world!"
"""
And the exit status should be 255
And the exit status should be 0

@mocked_home_directory
Scenario: Missing @orb.yml for orb packing
Expand Down
21 changes: 6 additions & 15 deletions process/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,27 @@ import (
"strings"
)

// MaybeIncludeFile replaces intsances of <<include(file)>> with the contents
// MaybeIncludeFile replaces instances of <<include(file)>> with the contents
// of "file", escaping instances of "<<" within the file before returning, when
// the <<include()>> parameter is the string passed.
func MaybeIncludeFile(s string, orbDirectory string) (string, error) {
// View: https://regexr.com/599mq
includeRegex := regexp.MustCompile(`<<[\s]*include\(([-\w\/\.]+)\)?[\s]*>>`)

// only find up to 2 matches, because we throw an error if we find >1
includeMatches := includeRegex.FindAllStringSubmatch(s, 2)
if len(includeMatches) > 1 {
return "", fmt.Errorf("multiple include statements: '%s'", s)
}
includeMatches := includeRegex.FindAllStringSubmatch(s, -1)

if len(includeMatches) == 1 {
match := includeMatches[0]
for _, match := range includeMatches {
fullMatch, subMatch := match[0], match[1]

// throw an error if the entire string wasn't matched
if fullMatch != s {
return "", fmt.Errorf("entire string must be include statement: '%s'", s)
}

filepath := filepath.Join(orbDirectory, subMatch)
file, err := ioutil.ReadFile(filepath)

if err != nil {
return "", fmt.Errorf("could not open %s for inclusion", filepath)
}
escaped := strings.ReplaceAll(string(file), "<<", "\\<<")

return escaped, nil
escaped := strings.ReplaceAll(string(file), "<<", "\\<<")
s = strings.ReplaceAll(s, fullMatch, escaped)
}

return s, nil
Expand Down
86 changes: 86 additions & 0 deletions process/process_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package process

import (
"io/ioutil"
"os"
"testing"
)

func Test_MaybeIncludeFile(t *testing.T) {
testCases := []struct {
description string
template string
expected string
files map[string]string
errExpected bool
}{
{
description: "File gets replaced",
template: "<<include(example.txt)>>",
expected: "world",
files: map[string]string{
"example.txt": "world",
},
errExpected: false,
},
{
description: "Partial line include",
template: "hello <<include(example-2.txt)>>",
expected: "hello world",
files: map[string]string{
"example-2.txt": "world",
},
errExpected: false,
},
{
description: "Multiple includes",
template: "<<include(example-1.txt)>> <<include(example-2.txt)>>",
expected: "hello world",
files: map[string]string{
"example-1.txt": "hello",
"example-2.txt": "world",
},
errExpected: false,
},
{
description: "File does not exist",
template: "<<include(file-that-does-not-exist.txt)>>",
files: map[string]string{},
errExpected: true,
},
{
description: "Included files are escaped",
template: "<<include(example-1.txt)>> world",
expected: "\\<< hello world",
files: map[string]string{
"example-1.txt": "<< hello",
},
errExpected: false,
},
}

for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
dir, err := ioutil.TempDir("", "circleci-cli-test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
for name, content := range tc.files {
if err := ioutil.WriteFile(dir+"/"+name, []byte(content), 0644); err != nil {
t.Fatal(err)
}
}

orbDirectory := dir
res, err := MaybeIncludeFile(tc.template, orbDirectory)
if err != nil && !tc.errExpected {
t.Errorf("Unexpected error: %v", err)
}

if !tc.errExpected && res != tc.expected {
t.Errorf("expected '%s', got '%s'", tc.expected, res)
}
})
}
}