From 219e76dae4c7cbeaa2ca5ccb1c58f95f86ccf64f Mon Sep 17 00:00:00 2001 From: "Tyler A. Young" Date: Thu, 14 Sep 2023 15:24:38 -0500 Subject: [PATCH] Add GitHub Actions CI (#181) * Add GitHub Actions CI Adds GitHub Actions for: - build & test for all combinations of Elixir 1.10, 1.11, 1.12, 1.13, 1.14, and 1.15, plus OTP 22, 23, 24, 25, and 26 - basic code quality checks (and a formatting fix and unused dependency removal to match) - checking for retired Hex dependencies Also updates the maintainers list and URL to reflect the new Felt org. * Run mix format * Fix build & test action * Test a failure case discovered by the test matrix --- .github/actions/elixir-setup/action.yml | 141 ++++++++++++++++++ .github/dependabot.yml | 10 ++ .github/workflows/elixir-build-and-test.yml | 77 ++++++++++ .github/workflows/elixir-quality-checks.yml | 45 ++++++ .../elixir-retired-packages-check.yml | 32 ++++ lib/geo/geometry_collection.ex | 6 +- lib/geo/line_string.ex | 6 +- lib/geo/line_stringz.ex | 6 +- lib/geo/multi_point.ex | 6 +- lib/geo/multi_pointz.ex | 6 +- lib/geo/pointm.ex | 6 +- lib/geo/pointz.ex | 6 +- lib/geo/polygonz.ex | 6 +- lib/geo/wkb.ex | 2 +- mix.exs | 6 +- test/geo/json_test.exs | 12 +- test/geo/wkt_test.exs | 47 ++++-- 17 files changed, 390 insertions(+), 30 deletions(-) create mode 100644 .github/actions/elixir-setup/action.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/elixir-build-and-test.yml create mode 100644 .github/workflows/elixir-quality-checks.yml create mode 100644 .github/workflows/elixir-retired-packages-check.yml diff --git a/.github/actions/elixir-setup/action.yml b/.github/actions/elixir-setup/action.yml new file mode 100644 index 0000000..7dcb328 --- /dev/null +++ b/.github/actions/elixir-setup/action.yml @@ -0,0 +1,141 @@ +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: true + description: By default, we will install Rebar (mix local.rebar --force). + install-hex: + required: false + type: boolean + default: true + 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.15.4 + 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: hex-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + hex-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}- + + - name: Get Mix cache + uses: actions/cache@v2 + id: mix-cache + with: + path: ${{ env.MIX_HOME || '~/.mix' }} + key: mix-${{ inputs.cache-key }}-${{ runner.os }}-${{ inputs.otp-version }}-${{ inputs.elixir-version }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + mix-${{ inputs.cache-key }}-${{ 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 --if-missing + shell: sh + if: inputs.install-rebar == 'true' + + - name: Install Hex + run: mix local.hex --force --if-missing + 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 + # + # Accoring 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/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..d28cb0e --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: mix + directory: "/" + schedule: + interval: daily + time: "11:00" + open-pull-requests-limit: 10 + assignees: + - s3cur3 diff --git a/.github/workflows/elixir-build-and-test.yml b/.github/workflows/elixir-build-and-test.yml new file mode 100644 index 0000000..496d7ef --- /dev/null +++ b/.github/workflows/elixir-build-and-test.yml @@ -0,0 +1,77 @@ +name: Elixir Unit Tests + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + +jobs: + build: + name: Elixir Unit Tests + runs-on: ubuntu-20.04 + env: + MIX_ENV: test + strategy: + matrix: + elixir: ["1.10.4", "1.11.4", "1.12.3", "1.13.4", "1.14.4", "1.15.5"] + otp: ["22.3", "23.3.4", "24.3.4", "25.3.2", "26.0.2"] + exclude: + # Elixir 1.10 only supports up to OTP 23 + - elixir: "1.10.4" + otp: "26.0.2" + - elixir: "1.10.4" + otp: "25.3.2" + - elixir: "1.10.4" + otp: "24.3.4" + # Elixir 1.11 doesn't support the latest OTP + - elixir: "1.11.4" + otp: "25.3.2" + - elixir: "1.11.4" + otp: "26.0.2" + # Elixir 1.12 doesn't support the latest OTP + - elixir: "1.12.3" + otp: "25.3.2" + - elixir: "1.12.3" + otp: "26.0.2" + # Elixir 1.13 doesn't support the latest OTP + - elixir: "1.13.4" + otp: "26.0.2" + # Elixir 1.14 requires at least OTP 23 + - elixir: "1.14.4" + otp: "22.3" + # Elixir 1.15 requires at least OTP 24 + - elixir: "1.15.5" + otp: "22.3" + - elixir: "1.15.5" + otp: "23.3.4" + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ matrix.elixir }} + otp-version: ${{ matrix.otp }} + build-app: false + + - name: Compile with warnings as errors + if: ${{ matrix.elixir != '1.11.4' && matrix.elixir != '1.10.4' }} + run: mix compile --warnings-as-errors + + # stream_data config doesn't work on Elixir 1.10, but we can at least compile + - name: Compile + if: ${{ matrix.elixir == '1.10.4' }} + run: mix compile + + - name: Run tests with warnings as errors + if: ${{ matrix.elixir != '1.11.4' && matrix.elixir != '1.10.4' }} + run: mix test --warnings-as-errors + + - name: Run tests + if: ${{ matrix.elixir == '1.11.4' }} + run: mix test diff --git a/.github/workflows/elixir-quality-checks.yml b/.github/workflows/elixir-quality-checks.yml new file mode 100644 index 0000000..4d65051 --- /dev/null +++ b/.github/workflows/elixir-quality-checks.yml @@ -0,0 +1,45 @@ +name: Elixir Quality Checks + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + +jobs: + quality_checks: + name: Elixir Quality Checks + runs-on: ubuntu-latest + env: + # In MIX_ENV=test, `$ mix xref graph` shows us a whole bunch of + # test stuff that isn't really relevant. + # The other checks don't really care what environment they run in. + MIX_ENV: dev + elixir: "1.15.4" + otp: "26.0.2" + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ env.elixir }} + otp-version: ${{ env.otp }} + build-app: false + + - name: Check for unused deps + run: mix deps.unlock --check-unused + + - name: Check code formatting + run: mix format --check-formatted + # We run all checks here even if others failed so that + # we give devs as much feedback as possible & save some time. + if: always() + + - name: Check for compile-time dependencies between modules + run: mix xref graph --label compile-connected --fail-above 0 + if: always() diff --git a/.github/workflows/elixir-retired-packages-check.yml b/.github/workflows/elixir-retired-packages-check.yml new file mode 100644 index 0000000..e49a0c1 --- /dev/null +++ b/.github/workflows/elixir-retired-packages-check.yml @@ -0,0 +1,32 @@ +name: Elixir Retired Packages Check + +on: + push: + branches: + - main + pull_request: + branches: + - "**" + +jobs: + retired_packages: + name: Elixir Retired Packages Check + runs-on: ubuntu-latest + env: + MIX_ENV: dev + elixir: "1.15.4" + otp: "26.0.2" + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Setup Elixir Project + uses: ./.github/actions/elixir-setup + with: + elixir-version: ${{ env.elixir }} + otp-version: ${{ env.otp }} + build-app: false + + - name: Check for retired/abandoned deps + run: mix hex.audit diff --git a/lib/geo/geometry_collection.ex b/lib/geo/geometry_collection.ex index 847f201..aaab6a4 100644 --- a/lib/geo/geometry_collection.ex +++ b/lib/geo/geometry_collection.ex @@ -3,6 +3,10 @@ defmodule Geo.GeometryCollection do Defines the GeometryCollection struct. """ - @type t :: %Geo.GeometryCollection{geometries: [Geo.geometry()], srid: integer | nil, properties: map} + @type t :: %Geo.GeometryCollection{ + geometries: [Geo.geometry()], + srid: integer | nil, + properties: map + } defstruct geometries: [], srid: nil, properties: %{} end diff --git a/lib/geo/line_string.ex b/lib/geo/line_string.ex index 506e5f9..e6aa486 100644 --- a/lib/geo/line_string.ex +++ b/lib/geo/line_string.ex @@ -3,6 +3,10 @@ defmodule Geo.LineString do Defines the LineString struct. """ - @type t :: %Geo.LineString{coordinates: [{number, number}], srid: integer | nil, properties: map} + @type t :: %Geo.LineString{ + coordinates: [{number, number}], + srid: integer | nil, + properties: map + } defstruct coordinates: [], srid: nil, properties: %{} end diff --git a/lib/geo/line_stringz.ex b/lib/geo/line_stringz.ex index 7a47e25..fc7fdeb 100644 --- a/lib/geo/line_stringz.ex +++ b/lib/geo/line_stringz.ex @@ -3,6 +3,10 @@ defmodule Geo.LineStringZ do Defines the LineStringZ struct. """ - @type t :: %__MODULE__{coordinates: [{number, number, number}], srid: integer | nil, properties: map} + @type t :: %__MODULE__{ + coordinates: [{number, number, number}], + srid: integer | nil, + properties: map + } defstruct coordinates: [], srid: nil, properties: %{} end diff --git a/lib/geo/multi_point.ex b/lib/geo/multi_point.ex index 2c4cff7..acdc549 100644 --- a/lib/geo/multi_point.ex +++ b/lib/geo/multi_point.ex @@ -3,6 +3,10 @@ defmodule Geo.MultiPoint do Defines the MultiPoint struct. """ - @type t :: %Geo.MultiPoint{coordinates: [{number, number}], srid: integer | nil, properties: map} + @type t :: %Geo.MultiPoint{ + coordinates: [{number, number}], + srid: integer | nil, + properties: map + } defstruct coordinates: [], srid: nil, properties: %{} end diff --git a/lib/geo/multi_pointz.ex b/lib/geo/multi_pointz.ex index 4611078..a5df321 100644 --- a/lib/geo/multi_pointz.ex +++ b/lib/geo/multi_pointz.ex @@ -3,6 +3,10 @@ defmodule Geo.MultiPointZ do Defines the MultiPointZ struct. """ - @type t :: %__MODULE__{coordinates: [{number, number, number}], srid: integer | nil, properties: map} + @type t :: %__MODULE__{ + coordinates: [{number, number, number}], + srid: integer | nil, + properties: map + } defstruct coordinates: [], srid: nil, properties: %{} end diff --git a/lib/geo/pointm.ex b/lib/geo/pointm.ex index c8bcf91..927c035 100644 --- a/lib/geo/pointm.ex +++ b/lib/geo/pointm.ex @@ -3,6 +3,10 @@ defmodule Geo.PointM do Defines the PointM struct. """ - @type t :: %Geo.PointM{coordinates: {number, number, number}, srid: integer | nil, properties: map} + @type t :: %Geo.PointM{ + coordinates: {number, number, number}, + srid: integer | nil, + properties: map + } defstruct coordinates: {0, 0, 0}, srid: nil, properties: %{} end diff --git a/lib/geo/pointz.ex b/lib/geo/pointz.ex index 77754a0..42be084 100644 --- a/lib/geo/pointz.ex +++ b/lib/geo/pointz.ex @@ -3,6 +3,10 @@ defmodule Geo.PointZ do Defines the PointZ struct. """ - @type t :: %Geo.PointZ{coordinates: {number, number, number}, srid: integer | nil, properties: map} + @type t :: %Geo.PointZ{ + coordinates: {number, number, number}, + srid: integer | nil, + properties: map + } defstruct coordinates: {0, 0, 0}, srid: nil, properties: %{} end diff --git a/lib/geo/polygonz.ex b/lib/geo/polygonz.ex index fcf99c6..4bae861 100644 --- a/lib/geo/polygonz.ex +++ b/lib/geo/polygonz.ex @@ -3,6 +3,10 @@ defmodule Geo.PolygonZ do Defines the Polygon struct. """ - @type t :: %__MODULE__{coordinates: [[{number, number, number}]], srid: integer | nil, properties: map} + @type t :: %__MODULE__{ + coordinates: [[{number, number, number}]], + srid: integer | nil, + properties: map + } defstruct coordinates: [], srid: nil, properties: %{} end diff --git a/lib/geo/wkb.ex b/lib/geo/wkb.ex index 5386b33..03e3c25 100644 --- a/lib/geo/wkb.ex +++ b/lib/geo/wkb.ex @@ -21,7 +21,7 @@ defmodule Geo.WKB do @doc """ Takes a Geometry and returns a base-16 encoded WKB string. - + The endian decides what the byte order will be. """ @spec encode!(Geo.geometry(), Geo.endian()) :: binary diff --git a/mix.exs b/mix.exs index b3dde7c..064db0d 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Geo.Mixfile do use Mix.Project - @source_url "https://github.com/bryanjos/geo" + @source_url "https://github.com/felt/geo" @version "3.5.1" def project do @@ -42,9 +42,9 @@ defmodule Geo.Mixfile do # These are the default files included in the package [ files: ["lib", "mix.exs", "README.md", "CHANGELOG.md"], - maintainers: ["Bryan Joseph"], + maintainers: ["Tyler Young", "Bryan Joseph"], licenses: ["MIT"], - links: %{"GitHub" => "https://github.com/bryanjos/geo"} + links: %{"GitHub" => "https://github.com/felt/geo"} ] end diff --git a/test/geo/json_test.exs b/test/geo/json_test.exs index c8f661e..8b68115 100644 --- a/test/geo/json_test.exs +++ b/test/geo/json_test.exs @@ -82,7 +82,9 @@ defmodule Geo.JSON.Test do end test "GeoJson to LineStringZ and back" do - json = "{ \"type\": \"LineStringZ\", \"coordinates\": [ [100.0, 0.0, 50.0], [101.0, 1.0, 20.0] ]}" + json = + "{ \"type\": \"LineStringZ\", \"coordinates\": [ [100.0, 0.0, 50.0], [101.0, 1.0, 20.0] ]}" + exjson = Jason.decode!(json) geom = Jason.decode!(json) |> Geo.JSON.decode!() @@ -339,15 +341,17 @@ defmodule Geo.JSON.Test do end property "encodes and decodes back to the correct Point struct" do - check all x <- float(), - y <- float() do + check all( + x <- float(), + y <- float() + ) do geom = %Geo.Point{coordinates: {x, y}} assert geom == Geo.JSON.encode!(geom) |> Geo.JSON.decode!() end end property "encodes and decodes back to the correct LineString struct" do - check all list <- list_of({float(), float()}, min_length: 1) do + check all(list <- list_of({float(), float()}, min_length: 1)) do geom = %Geo.LineString{coordinates: list} assert geom == Geo.JSON.encode!(geom) |> Geo.JSON.decode!() end diff --git a/test/geo/wkt_test.exs b/test/geo/wkt_test.exs index b462af6..3ba7a0c 100644 --- a/test/geo/wkt_test.exs +++ b/test/geo/wkt_test.exs @@ -278,52 +278,71 @@ defmodule Geo.WKT.Test do end property "encodes and decodes back to the correct Point struct" do - check all x <- float(), - y <- float() do + check all( + x <- float(), + y <- float() + ) do geom = %Geo.Point{coordinates: {x, y}} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end property "encodes and decodes back to the correct PointM struct" do - check all x <- float(), - y <- float(), - m <- float() do + check all( + x <- float(), + y <- float(), + m <- float() + ) do geom = %Geo.PointM{coordinates: {x, y, m}} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end property "encodes and decodes back to the correct PointZ struct" do - check all x <- float(), - y <- float(), - z <- float() do + check all( + x <- float(), + y <- float(), + z <- float() + ) do geom = %Geo.PointZ{coordinates: {x, y, z}} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end property "encodes and decodes back to the correct PointZM struct" do - check all x <- float(), - y <- float(), - z <- float(), - m <- float() do + check all( + x <- float(), + y <- float(), + z <- float(), + m <- float() + ) do geom = %Geo.PointZM{coordinates: {x, y, z, m}} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end property "encodes and decodes back to the correct LineString struct" do - check all list <- list_of({float(), float()}, min_length: 1) do + check all(list <- list_of({float(), float()}, min_length: 1)) do geom = %Geo.LineString{coordinates: list} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end property "encodes and decodes back to the correct LineStringZ struct" do - check all list <- list_of({float(), float(), float()}, min_length: 1) do + check all(list <- list_of({float(), float(), float()}, min_length: 1)) do geom = %Geo.LineStringZ{coordinates: list} assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() end end + + test "check LineString regression on Elixir 1.134 + OTP 24.3.4" do + geom = %Geo.LineString{coordinates: [{6.082409157592909e19, 1.2576001607965288e20}]} + assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() + + geom = %Geo.LineStringZ{ + coordinates: [{6.082409157592909e19, 1.2576001607965288e20, -0.20340391884184397}] + } + + assert geom == Geo.WKT.encode!(geom) |> Geo.WKT.decode!() + end end