diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE deleted file mode 100644 index 9a7997b..0000000 --- a/.github/PULL_REQUEST_TEMPLATE +++ /dev/null @@ -1,5 +0,0 @@ -Addresses [ID-XXX](https://shedul.atlassian.net/browse/ID-XXX). - - - -You can publish the dev version to hex.pm, check this [guide](https://www.notion.so/fresha-app/Private-Hex-packages-cc91aea5fea9412cb19c9b35ec2390cd#4ef415a7045840f7afbb68444ab5fbb3) \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c786e28..540f0cb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,12 +3,12 @@ name: Elixir CI Checks env: DEBIAN_FRONTEND: noninteractive DEPENDENCY_FILE: mix.lock - ELIXIR_VERSION: 1.16.2 # Elixir version used during package publishing + ELIXIR_VERSION: 1.16.2 JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - OTP_VERSION: 26.2.5 # OTP version used during package publishing - RELEVANT_FILES: "mix.lock mix.exs lib config test" # Important, this controls the caching, make sure to keep this right + OTP_VERSION: 26.2.5 + RELEVANT_FILES: "mix.lock mix.exs lib priv config test" REPOSITORY: absinthe_helpers - RUNNER_OS: ubuntu22 # Must match Elixir/OTP version in described in action erlef/setup-beam@v1 + RUNNER_OS: ubuntu22 SHA: ${{ github.sha }} concurrency: @@ -30,95 +30,199 @@ jobs: name: Static Checks (Elixir ${{ matrix.versions.elixir-version }}) runs-on: runs-on,runner=4cpu-linux-x64 outputs: - HASH: ${{ steps.prepare.outputs.HASH }} + HASH: ${{ steps.hash.outputs.HASH }} strategy: fail-fast: false matrix: versions: - - { elixir-version: 1.16.2, otp-version: 26.2.5, runner-os: 'ubuntu22' } + - { + elixir-version: 1.16.2, + otp-version: 26.2.5, + runner-os: "ubuntu22", + } steps: - - name: Checkout and compile dependencies - id: prepare - uses: surgeventures/platform-tribe-actions/elixir/precompile@fast-elixir-repo-setup + - name: Checkout latest codebase + uses: actions/checkout@v4 with: - repository: ${{ env.REPOSITORY }} - dependency-file: ${{ env.DEPENDENCY_FILE }} - sha: ${{ env.SHA }} - token: ${{ secrets.GITHUB_TOKEN }} - hex-token: ${{ secrets.HEX_ORGANIZATION_KEY }} - mix-env: dev - relevant-files: ${{ env.RELEVANT_FILES }} - elixir-version: ${{ matrix.versions.elixir-version }} - otp-version: ${{ matrix.versions.otp-version }} - runner-os: ${{ matrix.versions.runner-os }} - - name: Compile the application - id: compile - uses: surgeventures/platform-tribe-actions/elixir/compile@fast-elixir-repo-setup + ref: ${{ env.SHA }} + clean: false + persist-credentials: true + - name: Setup Elixir + uses: erlef/setup-beam@v1 + env: + ImageOS: ${{ matrix.versions.runner-os }} with: - build-hash: ${{ steps.prepare.outputs.HASH }} - warnings-as-errors: 'true' elixir-version: ${{ matrix.versions.elixir-version }} otp-version: ${{ matrix.versions.otp-version }} - - name: Run Static Checks - uses: surgeventures/platform-tribe-actions/elixir/static-check@fast-elixir-repo-setup - id: static + version-type: strict + - name: Get SHA sum (HASH) of relevant files + id: hash + run: | + git config --global --add safe.directory /__w/${{ env.repository }}/${{ env.repository }} + echo "Get SHA sum (HASH) of relevant files" + HASH="$(git ls-tree ${{ env.SHA }} -- ${{ env.RELEVANT_FILES }} | sha1sum | cut -d' ' -f1)" + echo "BUILD HASH FOR THE CODEBASE IS: $HASH" + echo "HASH=$HASH" >> $GITHUB_OUTPUT + - name: Hex auth + run: mix hex.organization auth fresha --key ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + - uses: runs-on/cache@v4 + id: deps-cache + with: + path: | + deps + _build/dev + key: ${{ runner.os }}-${{ matrix.versions.elixir-version }}-${{ matrix.versions.otp-version }}-precompile-deps-dev-${{ hashFiles('mix.lock') }} + - name: Install dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' + env: + MIX_ENV: dev + run: | + echo "Installing dependencies" + mix deps.get + mix deps.compile + - uses: runs-on/cache@v4 + id: build-cache with: - dialyzer: true - hex-token: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + path: "**/*" + key: ${{ runner.os }}-${{ matrix.versions.elixir-version }}-${{ matrix.versions.otp-version }}-compile-dev-${{ steps.hash.outputs.HASH }} + - name: Compile with warning as --warnings-as-errors + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + echo "Compiling the app with --warnings-as-errors" + mix compile --warnings-as-errors --force + - name: Run credo + run: | + echo "Running credo" + mix credo --strict + - name: Run format + run: | + echo "Running format" + mix format --check-formatted --dry-run + - name: Run publish --dry-run + env: + HEX_API_KEY: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + run: | + echo "Running publish --dry-run" + mix hex.publish --dry-run test: name: Unit Tests (Elixir ${{ matrix.versions.elixir-version }}) - runs-on: runs-on,runner=1cpu-linux-x64 + runs-on: runs-on,runner=2cpu-linux-x64 strategy: fail-fast: false matrix: versions: - - { elixir-version: 1.16.2, otp-version: 26.2.5, runner-os: 'ubuntu22' } + - { + elixir-version: 1.16.2, + otp-version: 26.2.5, + runner-os: "ubuntu22", + } steps: - - name: Checkout and compile dependencies - id: prepare - uses: surgeventures/platform-tribe-actions/elixir/precompile@fast-elixir-repo-setup + - name: Checkout latest codebase + uses: actions/checkout@v4 with: - repository: ${{ env.REPOSITORY }} - dependency-file: ${{ env.DEPENDENCY_FILE }} - sha: ${{ env.SHA }} - token: ${{ secrets.GITHUB_TOKEN }} - hex-token: ${{ secrets.HEX_ORGANIZATION_KEY }} - mix-env: test - relevant-files: ${{ env.RELEVANT_FILES }} - elixir-version: ${{ matrix.versions.elixir-version }} - otp-version: ${{ matrix.versions.otp-version }} - runner-os: ${{ matrix.versions.runner-os }} - - name: Compile the application - id: compile - uses: surgeventures/platform-tribe-actions/elixir/compile@fast-elixir-repo-setup + ref: ${{ env.SHA }} + clean: false + persist-credentials: true + - name: Setup Elixir + uses: erlef/setup-beam@v1 + env: + ImageOS: ${{ matrix.versions.runner-os }} with: - build-hash: ${{ steps.prepare.outputs.HASH }} - mix-env: test elixir-version: ${{ matrix.versions.elixir-version }} otp-version: ${{ matrix.versions.otp-version }} - - name: Run Unit Tests - uses: surgeventures/platform-tribe-actions/elixir/test@fast-elixir-repo-setup - id: test + version-type: strict + - name: Get SHA sum (HASH) of relevant files + id: hash + run: | + git config --global --add safe.directory /__w/${{ env.repository }}/${{ env.repository }} + echo "Get SHA sum (HASH) of relevant files" + HASH="$(git ls-tree ${{ env.SHA }} -- ${{ env.RELEVANT_FILES }} | sha1sum | cut -d' ' -f1)" + echo "BUILD HASH FOR THE CODEBASE IS: $HASH" + echo "HASH=$HASH" >> $GITHUB_OUTPUT + - name: Hex auth + run: mix hex.organization auth fresha --key ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + - uses: runs-on/cache@v4 + id: deps-cache + with: + path: | + deps + _build/test + key: ${{ runner.os }}-${{ matrix.versions.elixir-version }}-${{ matrix.versions.otp-version }}-precompile-deps-test-${{ hashFiles('mix.lock') }} + - name: Install dependencies + if: steps.deps-cache.outputs.cache-hit != 'true' + env: + MIX_ENV: test + run: | + echo "Installing dependencies" + mix deps.get + mix deps.compile + - uses: runs-on/cache@v4 + id: build-cache with: - # Updated so that we are not forced to have 90% code - # coverage - test-params: '' + path: "**/*" + key: ${{ runner.os }}-${{ matrix.versions.elixir-version }}-${{ matrix.versions.otp-version }}-compile-test-${{ steps.hash.outputs.HASH }} + - name: Compile with MIX_ENV=test + if: steps.build-cache.outputs.cache-hit != 'true' + env: + MIX_ENV: test + run: | + echo "Compiling the app with MIX_ENV=test" + mix compile --force + - name: Run tests + run: | + echo "Running tests" + mix test --cover permit: name: Permit Package Publishing needs: [static, test] runs-on: runs-on,runner=1cpu-linux-x64 outputs: - PUBLISH: ${{ steps.permit.outputs.PUBLISH }} + PUBLISH: ${{ steps.version.outputs.PUBLISH }} steps: - - name: Verify elibility for publishing the package - uses: surgeventures/platform-tribe-actions/elixir/permit@fast-elixir-repo-setup - id: permit + - name: Checkout latest codebase + uses: actions/checkout@v4 with: - repository: ${{ env.REPOSITORY }} - sha: ${{ env.SHA }} - relevant-files: ${{ env.RELEVANT_FILES}} + fetch-depth: 2 + ref: ${{ env.SHA }} + clean: false + persist-credentials: true + - name: Create Approval File + shell: bash + run: | + echo "CI Checks Passed for SHA ${{ env.SHA }} and HASH ${{ needs.static.outputs.HASH }}" > approval.txt + - name: Process Package Version + shell: bash + id: version + run: | + echo "===============================================" + echo "" + git show HEAD~1:mix.exs > mix.old.exs + diff mix.old.exs mix.exs > diff.txt || true + old_version=$(grep -oP 'version: "\K[^"]+' mix.old.exs) + new_version=$(grep -oP 'version: "\K[^"]+' mix.exs) + echo "Old Version: $old_version | New Version: $new_version" + if [ "$new_version" != "$old_version" ]; then + if [ "$new_version" \> "$old_version" ]; then + echo "Version is upped - WILL publish upon merging the PR" + echo "PUBLISH=true" >> $GITHUB_OUTPUT + else + echo "Version is lower than the original version - blocking publication" + echo "PUBLISH=false" >> $GITHUB_OUTPUT + exit 1 + fi + else + echo "PUBLISH=false" >> $GITHUB_OUTPUT + echo "Version is unchanged - WONT publish upon merging the PR" + fi + echo "" + echo "===============================================" + - name: Cache Approval File + uses: runs-on/cache/save@v4 + with: + path: approval.txt + key: ${{ runner.os }}-${{ env.REPOSITORY }}-approval-${{ needs.static.outputs.HASH }} publish: name: Publish Hex Package @@ -126,13 +230,32 @@ jobs: runs-on: runs-on,runner=2cpu-linux-x64 if: needs.permit.outputs.PUBLISH == 'true' && github.event_name == 'push' steps: - - name: Publish Package - uses: surgeventures/platform-tribe-actions/elixir/publish@fast-elixir-repo-setup + - name: Checkout latest codebase + uses: actions/checkout@v4 + with: + ref: ${{ env.SHA }} + clean: false + persist-credentials: true + - name: Setup Elixir + uses: erlef/setup-beam@v1 + env: + ImageOS: ${{ env.RUNNER_OS }} with: - repository: ${{ env.REPOSITORY }} - sha: ${{ env.SHA }} - token: ${{ secrets.GITHUB_TOKEN }} - hex-token: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} elixir-version: ${{ env.ELIXIR_VERSION }} otp-version: ${{ env.OTP_VERSION }} - runner-os: ${{ env.RUNNER_OS }} + version-type: strict + - name: Hex auth + run: mix hex.organization auth fresha --key ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + shell: bash + - name: Get dependencies + shell: bash + run: | + echo "Getting dependencies" + mix deps.get + - name: Publish dev package + shell: bash + env: + HEX_API_KEY: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + run: | + echo "Publishing package" + mix hex.publish --yes diff --git a/.github/workflows/dev-publish.yaml b/.github/workflows/dev-publish.yaml index 8ee282d..b189383 100644 --- a/.github/workflows/dev-publish.yaml +++ b/.github/workflows/dev-publish.yaml @@ -3,12 +3,12 @@ name: Elixir Dev Publish env: DEBIAN_FRONTEND: noninteractive DEPENDENCY_FILE: mix.lock - ELIXIR_VERSION: 1.16.2 # Elixir version used during package publishing + ELIXIR_VERSION: 1.16.2 JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - OTP_VERSION: 26.2.5 # OTP version used during package publishing - RELEVANT_FILES: 'mix.lock mix.exs lib config test' # Important, this controls the caching, make sure to keep this right + OTP_VERSION: 26.2.5 + RELEVANT_FILES: "mix.lock mix.exs lib priv config test" REPOSITORY: absinthe_helpers - RUNNER_OS: ubuntu22 # Must match Elixir/OTP version in described in action erlef/setup-beam@v1 + RUNNER_OS: ubuntu22 SHA: ${{ github.sha }} concurrency: @@ -22,21 +22,55 @@ on: jobs: dev-publish: name: Dev Publish - runs-on: - - runs-on - - runner=4cpu-linux-x64 - + runs-on: runs-on,runner=4cpu-linux-x64 steps: - - name: Publish Hex package - uses: surgeventures/platform-tribe-actions/elixir/dev-publish@fast-elixir-repo-setup + - name: Checkout latest codebase + uses: actions/checkout@v4 + with: + ref: ${{ env.sha }} + clean: false + persist-credentials: true + - name: Get SHA sum (HASH) of relevant files + id: hash + shell: bash + run: | + git config --global --add safe.directory /__w/${{ env.REPOSITORY }}/${{ env.REPOSITORY }} + echo "Get SHA sum (HASH) of relevant files" + HASH="$(git ls-tree ${{ env.SHA }} -- ${{ env.RELEVANT_FILES }} | sha1sum | cut -d' ' -f1)" + echo "BUILD HASH FOR THE CODEBASE IS: $HASH" + echo "IT WILL BE USED TO DETERMINE ELIGIBILITY OF THE CODEBASE FOR THE RELEASE" + echo "APPROVAL PRODUCED BY SUCCESSFULL CHECKS EXECUTION WILL LAND IN CACHE" + echo "HASH=$HASH" >> $GITHUB_OUTPUT + - name: Check for CI successes + uses: runs-on/cache/restore@v4 + with: + key: ${{ runner.os }}-${{ env.repository }}-approval-${{ steps.hash.outputs.HASH }} + path: approval.txt + fail-on-cache-miss: true + - name: Setup Elixir + uses: erlef/setup-beam@v1 + env: + ImageOS: ${{ env.RUNNER_OS }} with: - repository: ${{ env.REPOSITORY}} - dependency-file: ${{ env.DEPENDENCY_FILE }} - sha: ${{ env.SHA }} - token: ${{ secrets.GITHUB_TOKEN }} - hex-token: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} - mix-env: dev elixir-version: ${{ env.ELIXIR_VERSION }} otp-version: ${{ env.OTP_VERSION }} - runner-os: ${{ env.RUNNER_OS }} - relevant-files: ${{ env.RELEVANT_FILES}} + version-type: strict + - name: Hex auth + run: mix hex.organization auth fresha --key ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + shell: bash + - name: Get dependencies + shell: bash + run: | + echo "Getting dependencies" + mix deps.get + - name: Mark package version with dev suffix + shell: bash + run: | + sed -i "s/version: \"[[:digit:]]\+\.[[:digit:]]\+\.[[:digit:]]\+/&-git-$(git rev-parse --verify --short=4 HEAD)/" mix.exs + - name: Publish dev package + shell: bash + env: + HEX_API_KEY: ${{ secrets.HEX_ORGANIZATION_WRITE_KEY }} + run: | + echo "Publishing dev package" + mix hex.publish --yes diff --git a/lib/transforms/to_integer.ex b/lib/transforms/to_integer.ex index d1d2230..1dfa5a7 100644 --- a/lib/transforms/to_integer.ex +++ b/lib/transforms/to_integer.ex @@ -12,16 +12,14 @@ defmodule AbsintheHelpers.Transforms.ToInteger do end """ - alias Absinthe.Blueprint.Input - @behaviour AbsintheHelpers.Transform - def call(%Input.Value{data: data} = item, _opts) when is_binary(data) do + def call(%{data: data} = item, _opts) when is_binary(data) do case Integer.parse(data) do {int, ""} -> {:ok, %{item | data: int}} _ -> {:error, :invalid_integer, %{}} end end - def call(%Input.Value{data: _data}, _opts), do: {:error, :invalid_integer, %{}} + def call(%{data: _data}, _opts), do: {:error, :invalid_integer, %{}} end diff --git a/lib/transforms/trim.ex b/lib/transforms/trim.ex index 0eef40b..bc10357 100644 --- a/lib/transforms/trim.ex +++ b/lib/transforms/trim.ex @@ -14,13 +14,11 @@ defmodule AbsintheHelpers.Transforms.Trim do end """ - alias Absinthe.Blueprint.Input - @behaviour AbsintheHelpers.Transform - def call(%Input.Value{data: data} = item, _opts) when is_binary(data) do + def call(%{data: data} = item, _opts) when is_binary(data) do {:ok, %{item | data: String.trim(data)}} end - def call(%Input.Value{data: _data}, _), do: {:error, :invalid_value, %{}} + def call(%{data: _data}, _), do: {:error, :invalid_value, %{}} end diff --git a/test/absinthe_helpers/constraints/max_items_test.exs b/test/absinthe_helpers/constraints/max_items_test.exs new file mode 100644 index 0000000..031436c --- /dev/null +++ b/test/absinthe_helpers/constraints/max_items_test.exs @@ -0,0 +1,18 @@ +defmodule AbsintheHelpers.Constraints.MaxItemsTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Constraints.MaxItems + + test "max_items constraint on list" do + input = %{ + items: [ + %{data: 1}, + %{data: 2}, + %{data: 3} + ] + } + + assert MaxItems.call(input, {:max_items, 5}) == {:ok, input} + assert MaxItems.call(input, {:max_items, 2}) == {:error, :max_items_exceeded, %{max_items: 2}} + end +end diff --git a/test/absinthe_helpers/constraints/max_test.exs b/test/absinthe_helpers/constraints/max_test.exs new file mode 100644 index 0000000..b95fb7c --- /dev/null +++ b/test/absinthe_helpers/constraints/max_test.exs @@ -0,0 +1,26 @@ +defmodule AbsintheHelpers.Constraints.MaxTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Constraints.Max + + test "max constraint on integer" do + input = %{data: 5} + + assert {:ok, %{data: 5}} = Max.call(input, {:max, 10}) + assert {:error, :max_exceeded, %{max: 3}} = Max.call(input, {:max, 3}) + end + + test "max constraint on string length" do + input = %{data: "hello"} + + assert {:ok, %{data: "hello"}} = Max.call(input, {:max, 10}) + assert {:error, :max_exceeded, %{max: 3}} = Max.call(input, {:max, 3}) + end + + test "max constraint on decimal" do + input = %{data: Decimal.new("5.00")} + + assert {:ok, %{data: %Decimal{}}} = Max.call(input, {:max, 10}) + assert {:error, :max_exceeded, %{max: 3}} = Max.call(input, {:max, 3}) + end +end diff --git a/test/absinthe_helpers/constraints/min_items_test.exs b/test/absinthe_helpers/constraints/min_items_test.exs new file mode 100644 index 0000000..3155750 --- /dev/null +++ b/test/absinthe_helpers/constraints/min_items_test.exs @@ -0,0 +1,18 @@ +defmodule AbsintheHelpers.Constraints.MinItemsTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Constraints.MinItems + + test "min_items constraint on list" do + input = %{ + items: [ + %{data: 1}, + %{data: 2}, + %{data: 3} + ] + } + + assert MinItems.call(input, {:min_items, 2}) == {:ok, input} + assert MinItems.call(input, {:min_items, 5}) == {:error, :min_items_not_met, %{min_items: 5}} + end +end diff --git a/test/absinthe_helpers/constraints/min_test.exs b/test/absinthe_helpers/constraints/min_test.exs new file mode 100644 index 0000000..0765364 --- /dev/null +++ b/test/absinthe_helpers/constraints/min_test.exs @@ -0,0 +1,26 @@ +defmodule AbsintheHelpers.Constraints.MinTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Constraints.Min + + test "min constraint on integer" do + input = %{data: 5} + + assert Min.call(input, {:min, 3}) == {:ok, input} + assert Min.call(input, {:min, 10}) == {:error, :min_not_met, %{min: 10}} + end + + test "min constraint on string length" do + input = %{data: "hello"} + + assert Min.call(input, {:min, 3}) == {:ok, input} + assert Min.call(input, {:min, 10}) == {:error, :min_not_met, %{min: 10}} + end + + test "min constraint on decimal" do + input = %{data: Decimal.new("5.0")} + + assert Min.call(input, {:min, 3}) == {:ok, input} + assert Min.call(input, {:min, 10}) == {:error, :min_not_met, %{min: 10}} + end +end diff --git a/test/absinthe_helpers/transforms/to_integer_test.exs b/test/absinthe_helpers/transforms/to_integer_test.exs new file mode 100644 index 0000000..9f253e1 --- /dev/null +++ b/test/absinthe_helpers/transforms/to_integer_test.exs @@ -0,0 +1,23 @@ +defmodule AbsintheHelpers.Transforms.ToIntegerTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Transforms.ToInteger + + test "to_integer transformation on valid string" do + input = %{data: "123"} + + assert {:ok, %{data: 123}} = ToInteger.call(input, []) + end + + test "to_integer transformation on invalid string" do + input = %{data: "abc"} + + assert ToInteger.call(input, []) == {:error, :invalid_integer, %{}} + end + + test "to_integer transformation on non-string" do + input = %{data: 123} + + assert ToInteger.call(input, []) == {:error, :invalid_integer, %{}} + end +end diff --git a/test/absinthe_helpers/transforms/trim_test.exs b/test/absinthe_helpers/transforms/trim_test.exs new file mode 100644 index 0000000..18fc5a6 --- /dev/null +++ b/test/absinthe_helpers/transforms/trim_test.exs @@ -0,0 +1,17 @@ +defmodule AbsintheHelpers.Transforms.TrimTest do + use ExUnit.Case, async: true + + alias AbsintheHelpers.Transforms.Trim + + test "trim transformation on string" do + input = %{data: " hello "} + + assert {:ok, %{data: "hello"}} = Trim.call(input, []) + end + + test "trim transformation on non-string" do + input = %{data: 123} + + assert Trim.call(input, []) == {:error, :invalid_value, %{}} + end +end