diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.dialyzer_ignore.exs @@ -0,0 +1 @@ +[] diff --git a/.github/github_workflows.ex b/.github/github_workflows.ex new file mode 100644 index 0000000..f7b482f --- /dev/null +++ b/.github/github_workflows.ex @@ -0,0 +1,323 @@ +defmodule GithubWorkflows do + @moduledoc """ + Used by a custom tool to generate GitHub workflows. + Reduces repetition. + """ + + def get do + %{ + "main.yml" => main_workflow(), + "pr.yml" => pr_workflow() + } + end + + defp main_workflow do + [ + [ + name: "Main", + on: [ + push: [ + branches: ["main"] + ] + ], + jobs: [ + compile: compile_job(), + credo: credo_job(), + deps_audit: deps_audit_job(), + dialyzer: dialyzer_job(), + format: format_job(), + hex_audit: hex_audit_job(), + migrations: migrations_job(), + prettier: prettier_job(), + sobelow: sobelow_job(), + test: test_job(), + unused_deps: unused_deps_job() + ] + ] + ] + end + + defp pr_workflow() do + [ + [ + name: "PR", + on: [ + pull_request: [ + branches: ["main"], + types: ["opened", "reopened", "synchronize"] + ] + ], + jobs: [ + compile: compile_job(), + credo: credo_job(), + deps_audit: deps_audit_job(), + dialyzer: dialyzer_job(), + format: format_job(), + hex_audit: hex_audit_job(), + migrations: migrations_job(), + prettier: prettier_job(), + sobelow: sobelow_job(), + test: test_job(), + unused_deps: unused_deps_job() + ] + ] + ] + end + + defp checkout_step do + [ + name: "Checkout", + uses: "actions/checkout@v2" + ] + end + + defp compile_job do + elixir_job("Install deps and compile", + steps: [ + [ + name: "Install Elixir dependencies", + env: [MIX_ENV: "test"], + run: "mix deps.get" + ], + [ + name: "Compile", + env: [MIX_ENV: "test"], + run: "mix compile" + ] + ] + ) + end + + defp credo_job do + elixir_job("Credo", + needs: :compile, + steps: [ + [ + name: "Check code style", + env: [MIX_ENV: "test"], + run: "mix credo --strict" + ] + ] + ) + end + + defp deps_audit_job do + elixir_job("Deps audit", + needs: :compile, + steps: [ + [ + name: "Check for vulnerable Mix dependencies", + env: [MIX_ENV: "test"], + run: "mix deps.audit" + ] + ] + ) + end + + defp dialyzer_job do + elixir_job("Dialyzer", + needs: :compile, + steps: [ + [ + # Don't cache PLTs based on mix.lock hash, as Dialyzer can incrementally update even old ones + # Cache key based on Elixir & Erlang version (also useful when running in matrix) + name: "Restore PLT cache", + uses: "actions/cache@v3", + id: "plt_cache", + with: [ + key: "${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt", + "restore-keys": + "${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt", + path: "priv/plts" + ] + ], + [ + # Create PLTs if no cache was found + name: "Create PLTs", + if: "steps.plt_cache.outputs.cache-hit != 'true'", + env: [MIX_ENV: "test"], + run: "mix dialyzer --plt" + ], + [ + name: "Run dialyzer", + env: [MIX_ENV: "test"], + run: "mix dialyzer --format short 2>&1" + ] + ] + ) + end + + defp elixir_job(name, opts) do + needs = Keyword.get(opts, :needs) + steps = Keyword.get(opts, :steps, []) + services = Keyword.get(opts, :services) + + job = [ + name: name, + "runs-on": "ubuntu-latest", + env: [ + "elixir-version": "1.16.2", + "otp-version": "25.3.2.10" + ], + steps: + [ + checkout_step(), + [ + name: "Set up Elixir", + uses: "erlef/setup-beam@v1", + with: [ + "elixir-version": "${{ env.elixir-version }}", + "otp-version": "${{ env.otp-version }}" + ] + ], + [ + uses: "actions/cache@v3", + with: [ + path: "_build\ndeps", + key: "${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}", + "restore-keys": "${{ runner.os }}-mix" + ] + ] + ] ++ steps + ] + + job + |> then(fn job -> + if needs do + Keyword.put(job, :needs, needs) + else + job + end + end) + |> then(fn job -> + if services do + Keyword.put(job, :services, services) + else + job + end + end) + end + + defp format_job do + elixir_job("Format", + needs: :compile, + steps: [ + [ + name: "Check Elixir formatting", + env: [MIX_ENV: "test"], + run: "mix format --check-formatted" + ] + ] + ) + end + + defp hex_audit_job do + elixir_job("Hex audit", + needs: :compile, + steps: [ + [ + name: "Check for retired Hex packages", + env: [MIX_ENV: "test"], + run: "mix hex.audit" + ] + ] + ) + end + + defp migrations_job do + elixir_job("Migrations", + needs: :compile, + services: [ + db: db_service() + ], + steps: [ + [ + name: "Check if migrations are reversible", + env: [MIX_ENV: "test"], + run: "mix ci.migrations" + ] + ] + ) + end + + defp prettier_job do + [ + name: "Check formatting using Prettier", + "runs-on": "ubuntu-latest", + steps: [ + checkout_step(), + [ + name: "Restore npm cache", + uses: "actions/cache@v3", + id: "npm-cache", + with: [ + path: "~/.npm", + key: "${{ runner.os }}-node", + "restore-keys": "${{ runner.os }}-node" + ] + ], + [ + name: "Install Prettier", + if: "steps.npm-cache.outputs.cache-hit != 'true'", + run: "npm i -g prettier" + ], + [ + name: "Run Prettier", + run: "npx prettier -c ." + ] + ] + ] + end + + defp sobelow_job do + elixir_job("Security check", + needs: :compile, + steps: [ + [ + name: "Check for security issues using sobelow", + env: [MIX_ENV: "test"], + run: "mix sobelow --config .sobelow-conf" + ] + ] + ) + end + + defp test_job do + elixir_job("Test", + needs: :compile, + services: [ + db: db_service() + ], + steps: [ + [ + name: "Run tests", + env: [MIX_ENV: "test"], + run: "mix test --cover --warnings-as-errors" + ] + ] + ) + end + + defp unused_deps_job do + elixir_job("Check unused deps", + needs: :compile, + steps: [ + [ + name: "Check for unused Mix dependencies", + env: [MIX_ENV: "test"], + run: "mix deps.unlock --check-unused" + ] + ] + ) + end + + defp db_service do + [ + image: "postgres:13", + ports: ["5432:5432"], + env: [POSTGRES_PASSWORD: "postgres"], + options: + "--health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5" + ] + end +end diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 90c018d..0000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,98 +0,0 @@ -name: Elixir CI - -on: - push: - branches: ["main"] - pull_request: - branches: ["main"] - -permissions: - contents: read - -jobs: - test: - # Set up a Postgres DB service. By default, Phoenix applications - # use Postgres. - services: - db: - image: postgres:15 - ports: ["5432:5432"] - env: - POSTGRES_PASSWORD: postgres - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - runs-on: ubuntu-latest - name: Test on OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} - strategy: - matrix: - otp: ["25.3.2"] - elixir: ["1.15.7"] - steps: - - name: Set up Elixir - uses: erlef/setup-beam@61e01a43a562a89bfc54c7f9a378ff67b03e4a21 # v1.16.0 - with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} - - # Step: Check out the code. - - name: Checkout code - uses: actions/checkout@v3 - - # Step: Define how to cache deps. Restores existing cache if present. - - name: Cache deps - id: cache-deps - uses: actions/cache@v3 - env: - cache-name: cache-elixir-deps - with: - path: deps - key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - ${{ runner.os }}-mix-${{ env.cache-name }}- - - # Step: Define how to cache the `_build` directory. After the first run, - # this speeds up tests runs a lot. This includes not re-compiling our - # project's downloaded deps every run. - - name: Cache compiled build - id: cache-build - uses: actions/cache@v3 - env: - cache-name: cache-compiled-build - with: - path: _build - key: ${{ runner.os }}-mix-${{ env.cache-name }}-${{ hashFiles('**/mix.lock') }} - restore-keys: | - ${{ runner.os }}-mix-${{ env.cache-name }}- - ${{ runner.os }}-mix- - - # Step: Conditionally bust the cache when job is re-run. - # Sometimes, we may have issues with incremental builds that are fixed by - # doing a full recompile. In order to not waste dev time on such trivial - # issues (while also reaping the time savings of incremental builds for - # *most* day-to-day development), force a full recompile only on builds - # that are retried. - - name: Keep it clean to avoid incremental build problems (flakiness) - if: github.run_attempt != '1' - run: | - mix deps.clean --all - mix clean - shell: sh - - - name: Install dependencies - run: mix deps.get - - - name: Compiles without warnings - run: mix compile --warnings-as-errors - - - name: Check Formatting - run: mix format --check-formatted - - - name: Credo Check - run: mix credo - - - name: Run tests - run: mix test --cover --warnings-as-errors \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..b7a418e --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,295 @@ +name: Main +on: + push: + branches: + - main +jobs: + compile: + name: Install deps and compile + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Install Elixir dependencies + env: + MIX_ENV: test + run: mix deps.get + - name: Compile + env: + MIX_ENV: test + run: mix compile + credo: + needs: compile + name: Credo + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check code style + env: + MIX_ENV: test + run: mix credo --strict + deps_audit: + needs: compile + name: Deps audit + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for vulnerable Mix dependencies + env: + MIX_ENV: test + run: mix deps.audit + dialyzer: + needs: compile + name: Dialyzer + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Restore PLT cache + uses: actions/cache@v3 + id: plt_cache + with: + key: ${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt + restore-keys: ${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt + path: priv/plts + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + env: + MIX_ENV: test + run: mix dialyzer --plt + - name: Run dialyzer + env: + MIX_ENV: test + run: mix dialyzer --format short 2>&1 + format: + needs: compile + name: Format + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check Elixir formatting + env: + MIX_ENV: test + run: mix format --check-formatted + hex_audit: + needs: compile + name: Hex audit + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for retired Hex packages + env: + MIX_ENV: test + run: mix hex.audit + migrations: + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + needs: compile + name: Migrations + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check if migrations are reversible + env: + MIX_ENV: test + run: mix ci.migrations + prettier: + name: Check formatting using Prettier + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Restore npm cache + uses: actions/cache@v3 + id: npm-cache + with: + path: ~/.npm + key: ${{ runner.os }}-node + restore-keys: ${{ runner.os }}-node + - name: Install Prettier + if: steps.npm-cache.outputs.cache-hit != 'true' + run: npm i -g prettier + - name: Run Prettier + run: npx prettier -c . + sobelow: + needs: compile + name: Security check + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for security issues using sobelow + env: + MIX_ENV: test + run: mix sobelow --config .sobelow-conf + test: + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + needs: compile + name: Test + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Run tests + env: + MIX_ENV: test + run: mix test --cover --warnings-as-errors + unused_deps: + needs: compile + name: Check unused deps + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for unused Mix dependencies + env: + MIX_ENV: test + run: mix deps.unlock --check-unused diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 0000000..846ee42 --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,299 @@ +name: PR +on: + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize +jobs: + compile: + name: Install deps and compile + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Install Elixir dependencies + env: + MIX_ENV: test + run: mix deps.get + - name: Compile + env: + MIX_ENV: test + run: mix compile + credo: + needs: compile + name: Credo + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check code style + env: + MIX_ENV: test + run: mix credo --strict + deps_audit: + needs: compile + name: Deps audit + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for vulnerable Mix dependencies + env: + MIX_ENV: test + run: mix deps.audit + dialyzer: + needs: compile + name: Dialyzer + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Restore PLT cache + uses: actions/cache@v3 + id: plt_cache + with: + key: ${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt + restore-keys: ${{ runner.os }}-${{ env.elixir-version }}-${{ env.otp-version }}-plt + path: priv/plts + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' + env: + MIX_ENV: test + run: mix dialyzer --plt + - name: Run dialyzer + env: + MIX_ENV: test + run: mix dialyzer --format short 2>&1 + format: + needs: compile + name: Format + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check Elixir formatting + env: + MIX_ENV: test + run: mix format --check-formatted + hex_audit: + needs: compile + name: Hex audit + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for retired Hex packages + env: + MIX_ENV: test + run: mix hex.audit + migrations: + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + needs: compile + name: Migrations + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check if migrations are reversible + env: + MIX_ENV: test + run: mix ci.migrations + prettier: + name: Check formatting using Prettier + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Restore npm cache + uses: actions/cache@v3 + id: npm-cache + with: + path: ~/.npm + key: ${{ runner.os }}-node + restore-keys: ${{ runner.os }}-node + - name: Install Prettier + if: steps.npm-cache.outputs.cache-hit != 'true' + run: npm i -g prettier + - name: Run Prettier + run: npx prettier -c . + sobelow: + needs: compile + name: Security check + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for security issues using sobelow + env: + MIX_ENV: test + run: mix sobelow --config .sobelow-conf + test: + services: + db: + image: postgres:13 + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + needs: compile + name: Test + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Run tests + env: + MIX_ENV: test + run: mix test --cover --warnings-as-errors + unused_deps: + needs: compile + name: Check unused deps + runs-on: ubuntu-latest + env: + elixir-version: 1.16.2 + otp-version: 25.3.2.10 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Elixir + uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ env.elixir-version }} + otp-version: ${{ env.otp-version }} + - uses: actions/cache@v3 + with: + path: "_build\ndeps" + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix + - name: Check for unused Mix dependencies + env: + MIX_ENV: test + run: mix deps.unlock --check-unused diff --git a/.gitignore b/.gitignore index 7b1ee76..a3ec1f7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ npm-debug.log # Ignore Dialyzer PLTs /priv/plts/ +# Ignore uploaded files +/priv/static/uploads/ \ No newline at end of file diff --git a/README.md b/README.md index d10cd1b..41fd477 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ To start your Phoenix server: - * Run `mix setup` to install and setup dependencies - * Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` +- Run `mix setup` to install and setup dependencies +- Start Phoenix endpoint with `mix phx.server` or inside IEx with `iex -S mix phx.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. @@ -11,8 +11,8 @@ Ready to run in production? Please [check our deployment guides](https://hexdocs ## Learn more - * Official website: https://www.phoenixframework.org/ - * Guides: https://hexdocs.pm/phoenix/overview.html - * Docs: https://hexdocs.pm/phoenix - * Forum: https://elixirforum.com/c/phoenix-forum - * Source: https://github.com/phoenixframework/phoenix +- Official website: https://www.phoenixframework.org/ +- Guides: https://hexdocs.pm/phoenix/overview.html +- Docs: https://hexdocs.pm/phoenix +- Forum: https://elixirforum.com/c/phoenix-forum +- Source: https://github.com/phoenixframework/phoenix diff --git a/assets/.prettierignore b/assets/.prettierignore new file mode 100644 index 0000000..3932972 --- /dev/null +++ b/assets/.prettierignore @@ -0,0 +1,22 @@ +/../_build/ +/../.elixir_ls/ +/../.git/ +/../config/ +/../cover/ +/../deps/ +/../doc/ +/../lib/ +/../priv/ +/../rel/ +/../test/ +/../.credo.exs +/../.dialyzer_ignore.exs +/../.dockerignore +/../.env +/../.env.prod.sample +/../.env.sample +/../.formatter.exs +/../.gitignore +/../.iex.exs +/../.erl_crash.dump +/vendor/topbar.js \ No newline at end of file diff --git a/assets/.prettierrc.js b/assets/.prettierrc.js new file mode 100644 index 0000000..0614ee7 --- /dev/null +++ b/assets/.prettierrc.js @@ -0,0 +1,6 @@ +module.exports = { + trailingComma: 'es5', + tabWidth: 2, + semi: false, + singleQuote: true, +} diff --git a/assets/css/app.css b/assets/css/app.css index 378c8f9..8ac0912 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -1,5 +1,11 @@ -@import "tailwindcss/base"; -@import "tailwindcss/components"; -@import "tailwindcss/utilities"; +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; -/* This file is for your main application CSS */ +@import url('https://fonts.googleapis.com/css2?family=League+Spartan:wght@300;400;500;600;700&display=swap'); /* This file is for your main application CSS */ + +@layer base { + html { + font-family: 'League Spartan', sans-serif; + } +} diff --git a/assets/js/app.js b/assets/js/app.js index df0cdd9..c55ebdd 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -16,19 +16,23 @@ // // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. -import "phoenix_html" +import 'phoenix_html' // Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" -import topbar from "../vendor/topbar" +import { Socket } from 'phoenix' +import { LiveSocket } from 'phoenix_live_view' +import topbar from '../vendor/topbar' -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) +let csrfToken = document + .querySelector("meta[name='csrf-token']") + .getAttribute('content') +let liveSocket = new LiveSocket('/live', Socket, { + params: { _csrf_token: csrfToken }, +}) // Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) -window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) -window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) +topbar.config({ barColors: { 0: '#29d' }, shadowColor: 'rgba(0, 0, 0, .3)' }) +window.addEventListener('phx:page-loading-start', (_info) => topbar.show(300)) +window.addEventListener('phx:page-loading-stop', (_info) => topbar.hide()) // connect if there are any LiveViews on the page liveSocket.connect() @@ -38,4 +42,3 @@ liveSocket.connect() // >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session // >> liveSocket.disableLatencySim() window.liveSocket = liveSocket - diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index b927b7c..fa75e8a 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -1,68 +1,91 @@ // See the Tailwind configuration guide for advanced usage // https://tailwindcss.com/docs/configuration -const plugin = require("tailwindcss/plugin") -const fs = require("fs") -const path = require("path") +const plugin = require('tailwindcss/plugin') +const fs = require('fs') +const path = require('path') module.exports = { content: [ - "./js/**/*.js", - "../lib/invoice_app_web.ex", - "../lib/invoice_app_web/**/*.*ex" + './js/**/*.js', + '../lib/invoice_app_web.ex', + '../lib/invoice_app_web/**/*.*ex', ], theme: { extend: { colors: { - brand: "#FD4F00", - } + brand: '#FD4F00', + }, }, }, plugins: [ - require("@tailwindcss/forms"), + require('@tailwindcss/forms'), // Allows prefixing tailwind classes with LiveView classes to add rules // only when LiveView classes are applied, for example: // //
// - plugin(({addVariant}) => addVariant("phx-no-feedback", [".phx-no-feedback&", ".phx-no-feedback &"])), - plugin(({addVariant}) => addVariant("phx-click-loading", [".phx-click-loading&", ".phx-click-loading &"])), - plugin(({addVariant}) => addVariant("phx-submit-loading", [".phx-submit-loading&", ".phx-submit-loading &"])), - plugin(({addVariant}) => addVariant("phx-change-loading", [".phx-change-loading&", ".phx-change-loading &"])), + plugin(({ addVariant }) => + addVariant('phx-no-feedback', ['.phx-no-feedback&', '.phx-no-feedback &']) + ), + plugin(({ addVariant }) => + addVariant('phx-click-loading', [ + '.phx-click-loading&', + '.phx-click-loading &', + ]) + ), + plugin(({ addVariant }) => + addVariant('phx-submit-loading', [ + '.phx-submit-loading&', + '.phx-submit-loading &', + ]) + ), + plugin(({ addVariant }) => + addVariant('phx-change-loading', [ + '.phx-change-loading&', + '.phx-change-loading &', + ]) + ), // Embeds Heroicons (https://heroicons.com) into your app.css bundle // See your `CoreComponents.icon/1` for more information. // - plugin(function({matchComponents, theme}) { - let iconsDir = path.join(__dirname, "./vendor/heroicons/optimized") + plugin(function ({ matchComponents, theme }) { + let iconsDir = path.join(__dirname, './vendor/heroicons/optimized') let values = {} let icons = [ - ["", "/24/outline"], - ["-solid", "/24/solid"], - ["-mini", "/20/solid"] + ['', '/24/outline'], + ['-solid', '/24/solid'], + ['-mini', '/20/solid'], ] icons.forEach(([suffix, dir]) => { - fs.readdirSync(path.join(iconsDir, dir)).forEach(file => { - let name = path.basename(file, ".svg") + suffix - values[name] = {name, fullPath: path.join(iconsDir, dir, file)} + fs.readdirSync(path.join(iconsDir, dir)).forEach((file) => { + let name = path.basename(file, '.svg') + suffix + values[name] = { name, fullPath: path.join(iconsDir, dir, file) } }) }) - matchComponents({ - "hero": ({name, fullPath}) => { - let content = fs.readFileSync(fullPath).toString().replace(/\r?\n|\r/g, "") - return { - [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, - "-webkit-mask": `var(--hero-${name})`, - "mask": `var(--hero-${name})`, - "mask-repeat": "no-repeat", - "background-color": "currentColor", - "vertical-align": "middle", - "display": "inline-block", - "width": theme("spacing.5"), - "height": theme("spacing.5") - } - } - }, {values}) - }) - ] + matchComponents( + { + hero: ({ name, fullPath }) => { + let content = fs + .readFileSync(fullPath) + .toString() + .replace(/\r?\n|\r/g, '') + return { + [`--hero-${name}`]: `url('data:image/svg+xml;utf8,${content}')`, + '-webkit-mask': `var(--hero-${name})`, + mask: `var(--hero-${name})`, + 'mask-repeat': 'no-repeat', + 'background-color': 'currentColor', + 'vertical-align': 'middle', + display: 'inline-block', + width: theme('spacing.5'), + height: theme('spacing.5'), + } + }, + }, + { values } + ) + }), + ], } diff --git a/assets/vendor/heroicons/LICENSE.md b/assets/vendor/heroicons/LICENSE.md index 1ac3e40..64c0816 100644 --- a/assets/vendor/heroicons/LICENSE.md +++ b/assets/vendor/heroicons/LICENSE.md @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/assets/vendor/topbar.js b/assets/vendor/topbar.js index 4195727..d3ad046 100644 --- a/assets/vendor/topbar.js +++ b/assets/vendor/topbar.js @@ -4,35 +4,35 @@ * https://buunguyen.github.io/topbar * Copyright (c) 2021 Buu Nguyen */ -(function (window, document) { - "use strict"; +;(function (window, document) { + 'use strict' // https://gist.github.com/paulirish/1579671 - (function () { - var lastTime = 0; - var vendors = ["ms", "moz", "webkit", "o"]; + ;(function () { + var lastTime = 0 + var vendors = ['ms', 'moz', 'webkit', 'o'] for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { window.requestAnimationFrame = - window[vendors[x] + "RequestAnimationFrame"]; + window[vendors[x] + 'RequestAnimationFrame'] window.cancelAnimationFrame = - window[vendors[x] + "CancelAnimationFrame"] || - window[vendors[x] + "CancelRequestAnimationFrame"]; + window[vendors[x] + 'CancelAnimationFrame'] || + window[vendors[x] + 'CancelRequestAnimationFrame'] } if (!window.requestAnimationFrame) window.requestAnimationFrame = function (callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var currTime = new Date().getTime() + var timeToCall = Math.max(0, 16 - (currTime - lastTime)) var id = window.setTimeout(function () { - callback(currTime + timeToCall); - }, timeToCall); - lastTime = currTime + timeToCall; - return id; - }; + callback(currTime + timeToCall) + }, timeToCall) + lastTime = currTime + timeToCall + return id + } if (!window.cancelAnimationFrame) window.cancelAnimationFrame = function (id) { - clearTimeout(id); - }; - })(); + clearTimeout(id) + } + })() var canvas, currentProgress, @@ -41,125 +41,125 @@ fadeTimerId = null, delayTimerId = null, addEvent = function (elem, type, handler) { - if (elem.addEventListener) elem.addEventListener(type, handler, false); - else if (elem.attachEvent) elem.attachEvent("on" + type, handler); - else elem["on" + type] = handler; + if (elem.addEventListener) elem.addEventListener(type, handler, false) + else if (elem.attachEvent) elem.attachEvent('on' + type, handler) + else elem['on' + type] = handler }, options = { autoRun: true, barThickness: 3, barColors: { - 0: "rgba(26, 188, 156, .9)", - ".25": "rgba(52, 152, 219, .9)", - ".50": "rgba(241, 196, 15, .9)", - ".75": "rgba(230, 126, 34, .9)", - "1.0": "rgba(211, 84, 0, .9)", + 0: 'rgba(26, 188, 156, .9)', + '.25': 'rgba(52, 152, 219, .9)', + '.50': 'rgba(241, 196, 15, .9)', + '.75': 'rgba(230, 126, 34, .9)', + '1.0': 'rgba(211, 84, 0, .9)', }, shadowBlur: 10, - shadowColor: "rgba(0, 0, 0, .6)", + shadowColor: 'rgba(0, 0, 0, .6)', className: null, }, repaint = function () { - canvas.width = window.innerWidth; - canvas.height = options.barThickness * 5; // need space for shadow + canvas.width = window.innerWidth + canvas.height = options.barThickness * 5 // need space for shadow - var ctx = canvas.getContext("2d"); - ctx.shadowBlur = options.shadowBlur; - ctx.shadowColor = options.shadowColor; + var ctx = canvas.getContext('2d') + ctx.shadowBlur = options.shadowBlur + ctx.shadowColor = options.shadowColor - var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0); + var lineGradient = ctx.createLinearGradient(0, 0, canvas.width, 0) for (var stop in options.barColors) - lineGradient.addColorStop(stop, options.barColors[stop]); - ctx.lineWidth = options.barThickness; - ctx.beginPath(); - ctx.moveTo(0, options.barThickness / 2); + lineGradient.addColorStop(stop, options.barColors[stop]) + ctx.lineWidth = options.barThickness + ctx.beginPath() + ctx.moveTo(0, options.barThickness / 2) ctx.lineTo( Math.ceil(currentProgress * canvas.width), options.barThickness / 2 - ); - ctx.strokeStyle = lineGradient; - ctx.stroke(); + ) + ctx.strokeStyle = lineGradient + ctx.stroke() }, createCanvas = function () { - canvas = document.createElement("canvas"); - var style = canvas.style; - style.position = "fixed"; - style.top = style.left = style.right = style.margin = style.padding = 0; - style.zIndex = 100001; - style.display = "none"; - if (options.className) canvas.classList.add(options.className); - document.body.appendChild(canvas); - addEvent(window, "resize", repaint); + canvas = document.createElement('canvas') + var style = canvas.style + style.position = 'fixed' + style.top = style.left = style.right = style.margin = style.padding = 0 + style.zIndex = 100001 + style.display = 'none' + if (options.className) canvas.classList.add(options.className) + document.body.appendChild(canvas) + addEvent(window, 'resize', repaint) }, topbar = { config: function (opts) { for (var key in opts) - if (options.hasOwnProperty(key)) options[key] = opts[key]; + if (options.hasOwnProperty(key)) options[key] = opts[key] }, show: function (delay) { - if (showing) return; + if (showing) return if (delay) { - if (delayTimerId) return; - delayTimerId = setTimeout(() => topbar.show(), delay); - } else { - showing = true; - if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId); - if (!canvas) createCanvas(); - canvas.style.opacity = 1; - canvas.style.display = "block"; - topbar.progress(0); + if (delayTimerId) return + delayTimerId = setTimeout(() => topbar.show(), delay) + } else { + showing = true + if (fadeTimerId !== null) window.cancelAnimationFrame(fadeTimerId) + if (!canvas) createCanvas() + canvas.style.opacity = 1 + canvas.style.display = 'block' + topbar.progress(0) if (options.autoRun) { - (function loop() { - progressTimerId = window.requestAnimationFrame(loop); + ;(function loop() { + progressTimerId = window.requestAnimationFrame(loop) topbar.progress( - "+" + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) - ); - })(); + '+' + 0.05 * Math.pow(1 - Math.sqrt(currentProgress), 2) + ) + })() } } }, progress: function (to) { - if (typeof to === "undefined") return currentProgress; - if (typeof to === "string") { + if (typeof to === 'undefined') return currentProgress + if (typeof to === 'string') { to = - (to.indexOf("+") >= 0 || to.indexOf("-") >= 0 + (to.indexOf('+') >= 0 || to.indexOf('-') >= 0 ? currentProgress - : 0) + parseFloat(to); + : 0) + parseFloat(to) } - currentProgress = to > 1 ? 1 : to; - repaint(); - return currentProgress; + currentProgress = to > 1 ? 1 : to + repaint() + return currentProgress }, hide: function () { - clearTimeout(delayTimerId); - delayTimerId = null; - if (!showing) return; - showing = false; + clearTimeout(delayTimerId) + delayTimerId = null + if (!showing) return + showing = false if (progressTimerId != null) { - window.cancelAnimationFrame(progressTimerId); - progressTimerId = null; + window.cancelAnimationFrame(progressTimerId) + progressTimerId = null } - (function loop() { - if (topbar.progress("+.1") >= 1) { - canvas.style.opacity -= 0.05; + ;(function loop() { + if (topbar.progress('+.1') >= 1) { + canvas.style.opacity -= 0.05 if (canvas.style.opacity <= 0.05) { - canvas.style.display = "none"; - fadeTimerId = null; - return; + canvas.style.display = 'none' + fadeTimerId = null + return } } - fadeTimerId = window.requestAnimationFrame(loop); - })(); + fadeTimerId = window.requestAnimationFrame(loop) + })() }, - }; + } - if (typeof module === "object" && typeof module.exports === "object") { - module.exports = topbar; - } else if (typeof define === "function" && define.amd) { + if (typeof module === 'object' && typeof module.exports === 'object') { + module.exports = topbar + } else if (typeof define === 'function' && define.amd) { define(function () { - return topbar; - }); + return topbar + }) } else { - this.topbar = topbar; + this.topbar = topbar } -}.call(this, window, document)); +}).call(this, window, document) diff --git a/coveralls.json b/coveralls.json index 1d3fdec..50ce70a 100644 --- a/coveralls.json +++ b/coveralls.json @@ -1,6 +1,12 @@ { "coverage_options": { - "minimum_coverage": 70.0 + "minimum_coverage": 85 }, - "skip_files": ["lib/invoice_app_web/components/core_components.ex"] + "skip_files": [ + "lib/invoice_app.ex", + "lib/invoice_app_web/components/core_components", + "lib/invoice_app_web/gettext.ex", + "lib/invoice_app_web/telemetry.ex", + "test/support/data_case.ex" + ] } diff --git a/mix.exs b/mix.exs index 0339262..53aeeae 100644 --- a/mix.exs +++ b/mix.exs @@ -10,17 +10,39 @@ defmodule InvoiceApp.MixProject do start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), + test_coverage: [tool: ExCoveralls, export: "cov"], + preferred_cli_env: [ + ci: :test, + "ci.code_quality": :test, + "ci.deps": :test, + "ci.formatting": :test, + "ci.migrations": :test, + "ci.security": :test, + "ci.test": :test, + coveralls: :test, + "coveralls.detail": :test, + "coveralls.html": :test, + credo: :test, + dialyzer: :test, + sobelow: :test + ], + compilers: [:yecc] ++ Mix.compilers(), + compilers: [:leex] ++ Mix.compilers(), test_coverage: [tool: ExCoveralls], dialyzer: [ - plt_local_path: "priv/plts", - plt_add_apps: [:mix, :ex_unit] + ignore_warnings: ".dialyzer_ignore.exs", + plt_add_apps: [:ex_unit, :mix], + plt_file: {:no_warn, "priv/plts/dialyzer.plt"} ], - preferred_cli_env: %{ - coveralls: :test, - "coveralls.detail": :test, - "coveralls.post": :test, - "coveralls.html": :test - } + + # Docs + name: "InvoiceGenerator", + source_url: "https://github.com/kagure-nyakio/invoice_generator", + docs: [ + extras: ["README.md"], + main: "readme", + source_ref: "main" + ] ] end @@ -65,8 +87,11 @@ defmodule InvoiceApp.MixProject do {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:sobelow, "~> 0.13", only: [:dev, :test], runtime: false}, {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false}, - {:dialyxir, "~> 1.4", only: [:dev], runtime: false}, - {:excoveralls, "~> 0.18", only: :test} + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, + {:excoveralls, "~> 0.18", only: :test}, + {:faker, "~> 0.17", only: [:dev, :test]}, + {:github_workflows_generator, "~> 0.1", only: :dev, runtime: false}, + {:countries, "~> 1.6"} ] end @@ -86,15 +111,34 @@ defmodule InvoiceApp.MixProject do "assets.build": ["tailwind default", "esbuild default"], "assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"], ci: [ + "ci.deps_and_security", + "ci.formatting", + "ci.code_quality", + "ci.test", + "ci.migrations" + ], + "ci.code_quality": [ + "compile --force --warnings-as-errors", + "credo --strict", + "dialyzer" + ], + "ci.deps_and_security": [ "deps.unlock --check-unused", - "hex.audit", "deps.audit", - "sobelow --config .sobelow-conf", - "format --check-formatted", - "compile --warnings-as-errors --force", - "credo --strict", - "dialyzer --format short 2>&1" - ] + "sobelow --config .sobelow-conf" + ], + "ci.formatting": ["format --check-formatted", "cmd --cd assets npx prettier -c .."], + "ci.migrations": [ + "ecto.create --quiet", + "ecto.migrate --quiet", + "ecto.rollback --all --quiet" + ], + "ci.test": [ + "ecto.create --quiet", + "ecto.migrate --quiet", + "test --cover --warnings-as-errors" + ], + prettier: ["cmd --cd assets npx prettier -w .."] ] end end diff --git a/mix.lock b/mix.lock index 2e0825d..7d8f32c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,7 @@ %{ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.5", "9eeebb394cc9a0f3ae56b813459f990abb0a3dedee1be6b27fdb50301930502f", [:mix], [], "hexpm", "8d7c597c3e4a64c395980882d4bca3cebb8d74197c590dc272cfd3b6a6310578"}, + "countries": {:hex, :countries, "1.6.0", "0776d78e80105944a4ea4d9d4286e852f83497e57222844ca51f9a22ed2d8fd9", [:mix], [{:yamerl, "~> 0.7", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "a1e4d0fdd2a799f16a95ae2e842edeaabd9ac7639624ac5e139c54da7a6bccb0"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, @@ -15,10 +16,13 @@ "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, "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"}, "expo": {:hex, :expo, "0.5.1", "249e826a897cac48f591deba863b26c16682b43711dd15ee86b92f25eafd96d9", [:mix], [], "hexpm", "68a4233b0658a3d12ee00d27d37d856b1ba48607e7ce20fd376958d0ba6ce92b"}, + "faker": {:hex, :faker, "0.17.0", "671019d0652f63aefd8723b72167ecdb284baf7d47ad3a82a15e9b8a6df5d1fa", [:mix], [], "hexpm", "a7d4ad84a93fd25c5f5303510753789fc2433ff241bf3b4144d3f6f291658a6a"}, + "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, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.17.0", "17d06e1d44d891d20dbd437335eebe844e2426a0cd7e3a3e220b461127c73f70", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8d014a661bb6a437263d4b5abf0bcbd3cf0deb26b1e8596f2a271d22e48934c7"}, "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"}, "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, + "github_workflows_generator": {:hex, :github_workflows_generator, "0.1.0", "446ee8f8d39d0e386705f35eded837e53d17b5e8cd056b97ad6cb37abad290f0", [:mix], [{:fast_yaml, "~> 1.0", [hex: :fast_yaml, repo: "hexpm", optional: false]}], "hexpm", "8523183e8bfd8c9c97e0df30f1f06e500ca06a34f763462f25e85404a4297bb1"}, "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, @@ -26,6 +30,7 @@ "mix_audit": {:hex, :mix_audit, "2.1.2", "6cd5c5e2edbc9298629c85347b39fb3210656e541153826efd0b2a63767f3395", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "68d2f06f96b9c445a23434c9d5f09682866a5b4e90f631829db1c64f140e795b"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, + "p1_utils": {:hex, :p1_utils, "1.0.25", "2d39b5015a567bbd2cc7033eeb93a7c60d8c84efe1ef69a3473faa07fa268187", [:rebar3], [], "hexpm", "9219214428f2c6e5d3187ff8eb9a8783695c2427420be9a259840e07ada32847"}, "phoenix": {:hex, :phoenix, "1.7.10", "02189140a61b2ce85bb633a9b6fd02dff705a5f1596869547aeb2b2b95edd729", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "cf784932e010fd736d656d7fead6a584a4498efefe5b8227e9f383bf15bb79d0"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.3", "86e9878f833829c3f66da03d75254c155d91d72a201eb56ae83482328dc7ca93", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d36c401206f3011fefd63d04e8ef626ec8791975d9d107f9a0817d426f61ac07"}, "phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},