From 87398e479cdc94cba686846a366a06220e2996b5 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Wed, 7 Aug 2024 18:46:22 +0200 Subject: [PATCH] Setup CI --- .github/actions/elixir-setup/action.yml | 132 ++++++++++++++++++++ .github/workflows/elixir-build-and-test.yml | 50 ++++++++ .github/workflows/elixir-dialyzer.yml | 56 +++++++++ .github/workflows/elixir-quality-checks.yml | 41 ++++++ 4 files changed, 279 insertions(+) create mode 100644 .github/actions/elixir-setup/action.yml create mode 100644 .github/workflows/elixir-build-and-test.yml create mode 100644 .github/workflows/elixir-dialyzer.yml create mode 100644 .github/workflows/elixir-quality-checks.yml diff --git a/.github/actions/elixir-setup/action.yml b/.github/actions/elixir-setup/action.yml new file mode 100644 index 0000000..b35d5c0 --- /dev/null +++ b/.github/actions/elixir-setup/action.yml @@ -0,0 +1,132 @@ +name: Setup Elixir Project +description: Checks out the code, configures Elixir, fetches dependencies, and manages build caching. +inputs: + elixir-version: + required: true + type: string + description: Elixir version to set up + otp-version: + required: true + type: string + description: OTP version to set up + ################################################################# + # Everything below this line is optional. + # + # It's designed to make compiling a reasonably standard Elixir + # codebase "just work," though there may be speed gains to be had + # by tweaking these flags. + ################################################################# + build-deps: + required: false + type: boolean + default: true + description: True if we should compile dependencies + build-app: + required: false + type: boolean + default: true + description: True if we should compile the application itself + build-flags: + required: false + type: string + default: '--all-warnings' + description: Flags to pass to mix compile + install-rebar: + required: false + type: boolean + default: false + description: By default, we will install Rebar (mix local.rebar --force). + install-hex: + required: false + type: boolean + default: false + description: By default, we will install Hex (mix local.hex --force). + cache-key: + required: false + type: string + default: 'v1' + description: If you need to reset the cache for some reason, you can change this key. +outputs: + otp-version: + description: "Exact OTP version selected by the BEAM setup step" + value: ${{ steps.beam.outputs.otp-version }} + elixir-version: + description: "Exact Elixir version selected by the BEAM setup step" + value: ${{ steps.beam.outputs.elixir-version }} +runs: + using: "composite" + steps: + - name: Setup elixir + uses: erlef/setup-beam@v1 + id: beam + with: + elixir-version: ${{ inputs.elixir-version }} + otp-version: ${{ inputs.otp-version }} + + - name: Get deps cache + uses: actions/cache@v2 + with: + path: deps/ + key: deps-${{ inputs.cache-key }}-${{ runner.os }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + deps-${{ inputs.cache-key }}-${{ runner.os }}- + + - name: Get build cache + uses: actions/cache@v2 + id: build-cache + with: + path: _build/${{env.MIX_ENV}}/ + key: build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + build-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ env.MIX_ENV }}- + + - name: Get Hex cache + uses: actions/cache@v2 + id: hex-cache + with: + path: ~/.hex + key: build-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + build-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}- + + # In my experience, I have issues with incremental builds maybe 1 in 100 + # times 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), + # I force a full recompile only on builds that we retry. + - name: Clean to rule out incremental build as a source of flakiness + if: github.run_attempt != '1' + run: | + mix deps.clean --all + mix clean + shell: sh + + - name: Install Rebar + run: mix local.rebar --force + shell: sh + if: inputs.install-rebar == 'true' + + - name: Install Hex + run: mix local.hex --force + shell: sh + if: inputs.install-hex == 'true' + + - name: Install Dependencies + run: mix deps.get + shell: sh + + # Normally we'd use `mix deps.compile` here, however that incurs a large + # performance penalty when the dependencies are already fully compiled: + # https://elixirforum.com/t/github-action-cache-elixir-always-recompiles-dependencies-elixir-1-13-3/45994/12 + # + # According to Jose Valim at the above link `mix loadpaths` will check and + # compile missing dependencies + - name: Compile Dependencies + run: mix loadpaths + shell: sh + if: inputs.build-deps == 'true' + + - name: Compile Application + run: mix compile ${{ inputs.build-flags }} + shell: sh + if: inputs.build-app == 'true' diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml new file mode 100644 index 0000000..e0c7eb9 --- /dev/null +++ b/.github/workflows/elixir-build-and-test.yml @@ -0,0 +1,50 @@ +name: Build and Test + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + build: + name: Build and test + runs-on: ubuntu-20.04 + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + strategy: + matrix: + include: + - pair: + elixir: 1.14.5 + otp: 24.3 + build-flags: --warnings-as-errors + - pair: + elixir: 1.15.7 + otp: 25.3 + build-flags: --warnings-as-errors + - pair: + elixir: 1.16.2 + otp: 26.2 + build-flags: --warnings-as-errors + - pair: + elixir: 1.17.2 + otp: 27.0 + build-flags: --warnings-as-errors + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.pair.elixir }} + otp-version: ${{ matrix.pair.otp }} + build-flags: --all-warnings ${{ matrix.build-flags }} + + - name: Run Tests + run: mix coveralls.github ${{ matrix.build-flags }} + if: always() diff --git a/.github/workflows/elixir-dialyzer.yml b/.github/workflows/elixir-dialyzer.yml new file mode 100644 index 0000000..4ddc478 --- /dev/null +++ b/.github/workflows/elixir-dialyzer.yml @@ -0,0 +1,56 @@ +name: Dialyzer + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + build: + name: Run Dialyzer + runs-on: ubuntu-latest + env: + MIX_ENV: dev + strategy: + matrix: + elixir: ["1.17.2"] + otp: ["27.0.1"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + id: beam + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-app: false + + # 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: plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/*.ex') }} + restore-keys: | + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}-${{ hashFiles('**/*.ex') }} + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}- + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}- + plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}- + path: priv/plts + + # Create PLTs if no cache was found. + # Always rebuild PLT when a job is retried + # (If they were cached at all, they'll be updated when we run mix dialyzer with no flags.) + - name: Create PLTs + if: steps.plt_cache.outputs.cache-hit != 'true' || github.run_attempt != '1' + run: mix dialyzer --plt + + - name: Run Dialyzer + run: mix dialyzer --format github diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml new file mode 100644 index 0000000..6b07d0a --- /dev/null +++ b/.github/workflows/elixir-quality-checks.yml @@ -0,0 +1,41 @@ +name: Elixir Quality Checks + +on: + push: + branches: + - main + pull_request: + branches: + - '*' + +jobs: + quality_checks: + name: Formatting and Unused Deps + runs-on: ubuntu-latest + strategy: + matrix: + elixir: ["1.17.2"] + otp: ["27.0.1"] + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-app: false + + - name: Check for unused deps + run: mix deps.unlock --check-unused + - name: Check code formatting + run: mix format --check-formatted + # Check formatting even if there were unused deps so that + # we give devs as much feedback as possible & save some time. + if: always() + - name: Run Credo + run: mix credo suggest --min-priority=normal + # Run Credo even if formatting or the unused deps check failed + if: always()