diff --git a/.github/github_workflows.ex b/.github/github_workflows.ex index 4caaed8..c1a5cda 100644 --- a/.github/github_workflows.ex +++ b/.github/github_workflows.ex @@ -112,16 +112,16 @@ defmodule GithubWorkflows do "fail-fast": false, matrix: [ versions: [ - %{ + [ elixir: "1.11", otp: "21.3", "runner-image": "ubuntu-20.04" - }, - %{ + ], + [ elixir: "1.16", otp: "26.2", "runner-image": "ubuntu-latest" - } + ] ] ] ], diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7fa00f..f0463d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,11 @@ name: CI + on: - pull_request: [] + pull_request: push: branches: - main + jobs: compile: name: Install deps and compile @@ -12,11 +14,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -28,9 +30,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Install Elixir dependencies env: MIX_ENV: test @@ -47,11 +52,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -63,9 +68,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Check code style env: MIX_ENV: test @@ -78,11 +86,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -94,9 +102,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Check for vulnerable Mix dependencies env: MIX_ENV: test @@ -109,11 +120,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -125,15 +136,19 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Restore PLT cache uses: actions/cache@v3 with: path: priv/plts key: plt-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: plt-${{ matrix.versions.runner-image }}- + restore-keys: | + plt-${{ matrix.versions.runner-image }}- - name: Create PLTs env: MIX_ENV: test @@ -150,11 +165,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -166,9 +181,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Check Elixir formatting env: MIX_ENV: test @@ -181,11 +199,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -197,9 +215,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Check for retired Hex packages env: MIX_ENV: test @@ -229,11 +250,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -245,9 +266,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Run tests env: MIX_ENV: test @@ -262,11 +286,11 @@ jobs: fail-fast: false matrix: versions: - - otp: 21.3 - elixir: 1.11 + - elixir: 1.11 + otp: 21.3 runner-image: ubuntu-20.04 - - otp: 26.2 - elixir: 1.16 + - elixir: 1.16 + otp: 26.2 runner-image: ubuntu-latest steps: - name: Checkout @@ -278,9 +302,12 @@ jobs: otp-version: ${{ matrix.versions.otp }} - uses: actions/cache@v3 with: - path: "_build\ndeps" + path: | + _build + deps key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} - restore-keys: mix-${{ matrix.versions.runner-image }}- + restore-keys: | + mix-${{ matrix.versions.runner-image }}- - name: Check for unused Mix dependencies env: MIX_ENV: test diff --git a/lib/github_workflows_generator.ex b/lib/github_workflows_generator.ex index 2d18051..29ddd39 100644 --- a/lib/github_workflows_generator.ex +++ b/lib/github_workflows_generator.ex @@ -9,7 +9,12 @@ defmodule GithubWorkflowsGenerator do {:ok, workflows} <- get_workflows(source_path) do for {filename, data} <- workflows do path = Path.join(dir_path, filename) - yml = YmlEncoder.encode(data) + + yml = + data + |> List.first() + |> YmlEncoder.encode() + File.write!(path, yml) end @@ -41,7 +46,7 @@ defmodule GithubWorkflowsGenerator do %{} -> {:error, "File #{source_path} does not contain any workflows."} - _ -> + _other -> {:error, "File #{source_path} does not have a valid structure."} end end diff --git a/lib/github_workflows_generator/yml_encoder.ex b/lib/github_workflows_generator/yml_encoder.ex index c217d4b..02fa3ba 100644 --- a/lib/github_workflows_generator/yml_encoder.ex +++ b/lib/github_workflows_generator/yml_encoder.ex @@ -4,18 +4,68 @@ defmodule GithubWorkflowsGenerator.YmlEncoder do @spec encode(term()) :: String.t() def encode(data) do data - |> Enum.at(0) - |> :fast_yaml.encode() - |> to_string() - # Trim trailing space - |> String.replace("\s\n", "\n") - # Avoid putting list element in new line after - - |> String.replace(~r/-\n\s+/, "-\s") - # Trim trailing newline inside strings - |> String.replace(~S(\n"), ~S(")) - # Unquote strings - |> String.replace(~r<"([a-z0-9@_\-\.,: \{\}\$/\*\(\)'!=&\<\>~]+)">i, "\\1") - # Add newline to the EOF + |> to_yml() + |> String.trim() |> Kernel.<>("\n") end + + defp to_yml(data, level \\ 0, indent? \\ false) + + defp to_yml(data, level, indent?) when is_list(data) do + if Keyword.keyword?(data) do + handle_keyword_list(data, level, indent?) + else + Enum.map_join( + data, + "\n", + &(String.duplicate(" ", level) <> "- " <> to_yml(&1, level + 1, false)) + ) + end + end + + defp to_yml(data, level, _indent?) do + cond do + !is_binary(data) -> + to_string(data) + + String.contains?(data, "\n") -> + values = + data + |> String.split("\n", trim: true) + |> Enum.map_join("\n", &(String.duplicate(" ", level) <> &1)) + + "|\n#{values}" + + true -> + data + end + end + + defp handle_keyword_list(data, level, indent?) do + data + |> Enum.map_reduce(indent?, fn {key, value}, indent? -> + indentation = + if indent? do + String.duplicate(" ", level) + else + "" + end + + {delimiter, string_value} = + case value do + [] -> + {"", ""} + + value when is_list(value) -> + {"\n", to_yml(value, level + 1, true)} + + value -> + {" ", to_yml(value, level + 1, false)} + end + + {"#{indentation}#{key}:#{delimiter}#{string_value}", true} + end) + |> elem(0) + |> Enum.join(if(level == 0, do: "\n\n", else: "\n")) + end end diff --git a/mix.exs b/mix.exs index 0d6def6..6fc20ed 100644 --- a/mix.exs +++ b/mix.exs @@ -66,7 +66,6 @@ defmodule GithubWorkflowsGenerator.MixProject do [ {:credo, "~> 1.7", only: [:test], runtime: false}, {:dialyxir, "~> 1.4", only: [:test], runtime: false}, - {:fast_yaml, "~> 1.0"}, {:ex_doc, "~> 0.34", only: [:dev], runtime: false}, {:excoveralls, "~> 0.18", only: [:test], runtime: false}, {:mix_audit, "~> 1.0", only: [:test], runtime: false} diff --git a/mix.lock b/mix.lock index 5fd22b9..6c18a23 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,6 @@ "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, "excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"}, - "fast_yaml": {:hex, :fast_yaml, "1.0.36", "65413a34a570fd4e205a460ba602e4ee7a682f35c22d2e1c839025dbf515105c", [:rebar3], [{:p1_utils, "1.0.25", [hex: :p1_utils, repo: "hexpm", optional: false]}], "hexpm", "1abe8f758fc2a86b08edff80bbc687cfd41ebc1412cfec0ef4a0acfcd032052f"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"}, @@ -14,7 +13,6 @@ "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "mix_audit": {:hex, :mix_audit, "1.0.1", "9dd114408961b8db214f42fee40b2f632ecd7e4fd29500403068c82c77db8361", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.8.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "65066bb7757078aa49faaa2f7c1e2d52f56ff6fe6cff01723dbaf5be2a75771b"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, - "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.8.0", "c7ff0034daf57279c2ce902788ce6fdb2445532eb4317e8df4b044209fae6832", [:mix], [{:yamerl, "~> 0.8", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4b674bd881e373d1ac6a790c64b2ecb69d1fd612c2af3b22de1619c15473830b"}, } diff --git a/test/github_workflows_generator/yml_encoder_test.exs b/test/github_workflows_generator/yml_encoder_test.exs new file mode 100644 index 0000000..11a216f --- /dev/null +++ b/test/github_workflows_generator/yml_encoder_test.exs @@ -0,0 +1,92 @@ +defmodule GithubWorkflowsGenerator.YmlEncoderTest do + use ExUnit.Case, async: true + + alias GithubWorkflowsGenerator.YmlEncoder + + describe "encode/1" do + test "encodes a list" do + assert YmlEncoder.encode([:pull_request, :push]) == """ + - pull_request + - push + """ + end + + test "encodes a multiline string" do + assert YmlEncoder.encode(path: "_build\ndeps") == """ + path: | + _build + deps + """ + end + + test "encodes a list of lists" do + assert YmlEncoder.encode(on: [pull_request: [], push: [branches: ["main"]]]) == """ + on: + pull_request: + push: + branches: + - main + """ + end + + test "encodes a complex data structure" do + assert YmlEncoder.encode( + on: [ + schedule: [ + [cron: "30 5 * * 1,3"], + [cron: "30 5 * * 2,4"] + ] + ], + jobs: [ + test_schedule: [ + "runs-on": "ubuntu-latest", + steps: [ + [ + name: "Not on Monday or Wednesday", + if: "github.event.schedule != '30 5 * * 1,3'", + run: "echo \"This step will be skipped on Monday and Wednesday\"" + ], + [ + name: "Every time", + run: "echo \"This step will always run\"" + ], + [ + uses: "actions/cache@v3", + with: [ + path: "_build\ndeps", + key: + "mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }}", + "restore-keys": "\nmix-${{ matrix.versions.runner-image }}" + ] + ] + ] + ] + ] + ) == + """ + on: + schedule: + - cron: 30 5 * * 1,3 + - cron: 30 5 * * 2,4 + + jobs: + test_schedule: + runs-on: ubuntu-latest + steps: + - name: Not on Monday or Wednesday + if: github.event.schedule != '30 5 * * 1,3' + run: echo "This step will be skipped on Monday and Wednesday" + - name: Every time + run: echo "This step will always run" + - uses: actions/cache@v3 + with: + path: | + _build + deps + key: mix-${{ matrix.versions.runner-image }}-${{ matrix.versions.otp }}-${{ matrix.versions.elixir }}-${{ github.sha }} + restore-keys: | + mix-${{ matrix.versions.runner-image }} + """ + end + end +end