diff --git a/.github/workflows/check-go-dependencies-task.yml b/.github/workflows/check-go-dependencies-task.yml index be478ab..6cb341d 100644 --- a/.github/workflows/check-go-dependencies-task.yml +++ b/.github/workflows/check-go-dependencies-task.yml @@ -65,7 +65,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -76,12 +76,12 @@ jobs: version: 3.x - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Install Task - uses: arduino/setup-task@v1 + uses: arduino/setup-task@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} version: 3.x @@ -102,9 +102,10 @@ jobs: # Some might find it convenient to have CI generate the cache rather than setting up for it locally - name: Upload cache to workflow artifact if: failure() && steps.diff.outcome == 'failure' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: if-no-files-found: error + include-hidden-files: true name: dep-licenses-cache path: .licenses/ @@ -115,7 +116,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: submodules: recursive @@ -126,12 +127,12 @@ jobs: version: 3.x - name: Install Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Install Task - uses: arduino/setup-task@v1 + uses: arduino/setup-task@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} version: 3.x diff --git a/.github/workflows/check-license.yml b/.github/workflows/check-license.yml index 77c6d18..9fcd4cf 100644 --- a/.github/workflows/check-license.yml +++ b/.github/workflows/check-license.yml @@ -63,7 +63,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Ruby uses: ruby/setup-ruby@v1 diff --git a/.github/workflows/check-markdown-task.yml b/.github/workflows/check-markdown-task.yml index 9b53c3a..5f2a772 100644 --- a/.github/workflows/check-markdown-task.yml +++ b/.github/workflows/check-markdown-task.yml @@ -69,18 +69,18 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Initialize markdownlint-cli problem matcher - uses: xt0rted/markdownlint-problem-matcher@v2 + uses: xt0rted/markdownlint-problem-matcher@v3 - name: Install Task - uses: arduino/setup-task@v1 + uses: arduino/setup-task@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} version: 3.x @@ -95,15 +95,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} - name: Install Task - uses: arduino/setup-task@v1 + uses: arduino/setup-task@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} version: 3.x diff --git a/.github/workflows/check-taskfiles.yml b/.github/workflows/check-taskfiles.yml index cc41d93..d369f69 100644 --- a/.github/workflows/check-taskfiles.yml +++ b/.github/workflows/check-taskfiles.yml @@ -7,6 +7,7 @@ env: # See: https://docs.github.com/actions/using-workflows/events-that-trigger-workflows on: + create: push: paths: - ".github/workflows/check-taskfiles.ya?ml" @@ -26,9 +27,37 @@ on: repository_dispatch: jobs: + run-determination: + runs-on: ubuntu-latest + permissions: {} + outputs: + result: ${{ steps.determination.outputs.result }} + steps: + - name: Determine if the rest of the workflow should run + id: determination + run: | + RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x" + # The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead. + if [[ + "${{ github.event_name }}" != "create" || + "${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX + ]]; then + # Run the other jobs. + RESULT="true" + else + # There is no need to run the other jobs. + RESULT="false" + fi + + echo "result=$RESULT" >> $GITHUB_OUTPUT + validate: name: Validate ${{ matrix.file }} + needs: run-determination + if: needs.run-determination.outputs.result == 'true' runs-on: ubuntu-latest + permissions: + contents: read strategy: fail-fast: false @@ -39,10 +68,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} @@ -50,8 +79,8 @@ jobs: id: download-schema uses: carlosperate/download-file-action@v2 with: - # See: https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/taskfile.json - file-url: https://json.schemastore.org/taskfile.json + # Source: https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/taskfile.json + file-url: https://taskfile.dev/schema.json location: ${{ runner.temp }}/taskfile-schema - name: Install JSON schema validator diff --git a/.github/workflows/sync-labels.yml b/.github/workflows/sync-labels.yml index 70085bd..78723a3 100644 --- a/.github/workflows/sync-labels.yml +++ b/.github/workflows/sync-labels.yml @@ -19,7 +19,7 @@ on: env: CONFIGURATIONS_FOLDER: .github/label-configuration-files - CONFIGURATIONS_ARTIFACT: label-configuration-files + CONFIGURATIONS_ARTIFACT_PREFIX: label-configuration-file- jobs: check: @@ -27,7 +27,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download JSON schema for labels configuration file id: download-schema @@ -71,13 +71,13 @@ jobs: file-url: https://raw.githubusercontent.com/arduino/tooling-project-assets/main/workflow-templates/assets/sync-labels/${{ matrix.filename }} - name: Pass configuration files to next job via workflow artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: | *.yaml *.yml if-no-files-found: error - name: ${{ env.CONFIGURATIONS_ARTIFACT }} + name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}${{ matrix.filename }} sync: needs: download @@ -106,18 +106,19 @@ jobs: echo "::set-output name=flag::--dry-run" - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - - name: Download configuration files artifact - uses: actions/download-artifact@v3 + - name: Download configuration file artifacts + uses: actions/download-artifact@v4 with: - name: ${{ env.CONFIGURATIONS_ARTIFACT }} + merge-multiple: true + pattern: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}* path: ${{ env.CONFIGURATIONS_FOLDER }} - - name: Remove unneeded artifact - uses: geekyeggo/delete-artifact@v2 + - name: Remove unneeded artifacts + uses: geekyeggo/delete-artifact@v5 with: - name: ${{ env.CONFIGURATIONS_ARTIFACT }} + name: ${{ env.CONFIGURATIONS_ARTIFACT_PREFIX }}* - name: Merge label configuration files run: | diff --git a/properties.go b/properties.go index b7cccb6..216fdee 100644 --- a/properties.go +++ b/properties.go @@ -76,6 +76,8 @@ import ( "reflect" "regexp" "runtime" + "sort" + "strconv" "strings" "unicode/utf8" @@ -630,6 +632,9 @@ func (m *Map) ExtractSubIndexSets(root string) []*Map { // "tre.discovery.required.1": "itemA", // "tre.discovery.required.2": "itemB", // "tre.discovery.required.3": "itemC", +// "quattro.discovery.required.1": "itemA", +// "quattro.discovery.required.4": "itemB", +// "quattro.discovery.required.5": "itemC", // } // // calling ExtractSubIndexLists("uno.discovery.required") returns the array: @@ -644,23 +649,45 @@ func (m *Map) ExtractSubIndexSets(root string) []*Map { // // [ "itemA", "itemB", "itemC" ] // +// also the list may contains holes, so calling ExtractSubIndexLists("quattro.discovery.required") returns: +// +// [ "itemA", "itemB", "itemC" ] +// // Numeric subindex cannot be mixed with non-numeric, in that case only the numeric sub // index sets will be returned. func (m *Map) ExtractSubIndexLists(root string) []string { - // First check the properties with numeric sub index "root.N.xxx" + isNotDigit := func(in string) bool { + for _, r := range in { + if r < '0' || r > '9' { + return true + } + } + return false + } + + // Extract numeric keys + subProps := m.SubTree(root) + indexes := []int{} + for _, key := range subProps.o { + if isNotDigit(key) { + continue + } + if idx, err := strconv.Atoi(key); err == nil { + indexes = append(indexes, idx) + } + } + sort.Ints(indexes) + res := []string{} - portIDPropsSet := m.SubTree(root) - idx := 0 haveIndexedProperties := false - for { - k := fmt.Sprintf("%d", idx) - idx++ - if v, ok := portIDPropsSet.GetOk(k); ok { + for i, idx := range indexes { + if i > 0 && idx == indexes[i-1] { + // de-duplicate cases like "05" and "5" + continue + } + if v, ok := subProps.GetOk(strconv.Itoa(idx)); ok { haveIndexedProperties = true res = append(res, v) - } else if idx > 1 { - // Always check sub-id 0 and 1 (https://github.com/arduino/arduino-cli/issues/456) - break } } diff --git a/properties_test.go b/properties_test.go index 1283c64..16a4c01 100644 --- a/properties_test.go +++ b/properties_test.go @@ -357,32 +357,24 @@ func TestExtractSubIndexLists(t *testing.T) { "quattro.discovery.required.1": "itemB", "quattro.discovery.required.2": "itemC", "cinque.discovery.something": "itemX", + "sei.discovery.something.1": "itemA", + "sei.discovery.something.2": "itemB", + "sei.discovery.something.5": "itemC", + "sei.discovery.something.12": "itemD", + "sette.discovery.something.01": "itemA", + "sette.discovery.something.2": "itemB", + "sette.discovery.something.05": "itemC", + "sette.discovery.something.5": "itemD", } m := NewFromHashmap(data) - s1 := m.ExtractSubIndexLists("uno.discovery.required") - require.Len(t, s1, 1) - require.Equal(t, s1[0], "item") - - s2 := m.ExtractSubIndexLists("due.discovery.required") - require.Len(t, s2, 3) - require.Equal(t, s2[0], "item1") - require.Equal(t, s2[1], "item2") - require.Equal(t, s2[2], "item3") - - s3 := m.ExtractSubIndexLists("tre.discovery.required") - require.Len(t, s3, 3) - require.Equal(t, s3[0], "itemA") - require.Equal(t, s3[1], "itemB") - require.Equal(t, s3[2], "itemC") - - s4 := m.ExtractSubIndexLists("quattro.discovery.required") - require.Len(t, s4, 2) - require.Equal(t, s4[0], "itemB") - require.Equal(t, s4[1], "itemC") - - s5 := m.ExtractSubIndexLists("cinque.discovery.required") - require.Len(t, s5, 0) + require.Equal(t, []string{"item"}, m.ExtractSubIndexLists("uno.discovery.required")) + require.Equal(t, []string{"item1", "item2", "item3"}, m.ExtractSubIndexLists("due.discovery.required")) + require.Equal(t, []string{"itemA", "itemB", "itemC"}, m.ExtractSubIndexLists("tre.discovery.required")) + require.Equal(t, []string{"itemB", "itemC"}, m.ExtractSubIndexLists("quattro.discovery.required")) + require.Equal(t, []string{}, m.ExtractSubIndexLists("cinque.discovery.required")) + require.Equal(t, []string{"itemA", "itemB", "itemC", "itemD"}, m.ExtractSubIndexLists("sei.discovery.something")) + require.Equal(t, []string{"itemB", "itemD"}, m.ExtractSubIndexLists("sette.discovery.something")) } func TestLoadingNonUTF8Properties(t *testing.T) { diff --git a/strings.go b/strings.go index f64b15e..4eae333 100644 --- a/strings.go +++ b/strings.go @@ -39,9 +39,29 @@ import ( // to use spaces in a single element of the split using quote characters. // // For example the call: -// SplitQuotedString(`This 'is an' "Hello World!" example`, `'"`, false) +// +// SplitQuotedString(`This 'is an' "Hello World!" example`, `'"`, false) +// // returns the following array: -// []string{"This", "is an", "Hello World!", "example"} +// +// []string{"This", "is an", "Hello World!", "example"} +// +// The quoteChars parameter is a string containing all the characters that +// are considered as quote characters. If a quote character is found, the +// function will consider the text between the quote character and the next +// quote character as a single element of the split. +// +// The acceptEmptyArguments parameter is a boolean that indicates if the +// function should consider empty arguments as valid elements of the split. +// If set to false, the function will ignore empty arguments. +// +// If the function finds an opening quote character and does not find the +// closing quote character, it will return an error. In any case, the function +// will return the split array up to the point where the error occurred. +// +// The function does not support escaping of quote characters. +// +// The function is UTF-8 safe. func SplitQuotedString(src string, quoteChars string, acceptEmptyArguments bool) ([]string, error) { // Make a map of valid quote runes isQuote := map[rune]bool{} @@ -83,7 +103,7 @@ func SplitQuotedString(src string, quoteChars string, acceptEmptyArguments bool) } if escapingChar != 0 { - return nil, fmt.Errorf("invalid quoting, no closing `%c` char found", escapingChar) + return result, fmt.Errorf("invalid quoting, no closing `%c` char found", escapingChar) } return result, nil diff --git a/strings_test.go b/strings_test.go index 2f5319d..9245f04 100644 --- a/strings_test.go +++ b/strings_test.go @@ -64,8 +64,11 @@ func TestSplitQuotedStringWithUTF8(t *testing.T) { } func TestSplitQuotedStringInvalid(t *testing.T) { - _, err := SplitQuotedString(`'this is' a 'test of quoting`, `"'`, true) - require.Error(t, err) - _, err = SplitQuotedString(`'this is' a "'test" of "quoting`, `"'`, true) - require.Error(t, err) + res, err := SplitQuotedString(`'this is' a 'test of quoting`, `"'`, true) + require.EqualError(t, err, "invalid quoting, no closing `'` char found") + require.Equal(t, res, []string{"this is", "a"}) + + res, err = SplitQuotedString(`'this is' a "'test" of "quoting`, `"'`, true) + require.EqualError(t, err, "invalid quoting, no closing `\"` char found") + require.Equal(t, res, []string{"this is", "a", "'test", "of"}) }