From a26a09e60a5ce94cafe58e7aa9258675a4f1ce10 Mon Sep 17 00:00:00 2001 From: Mijail Rondon Date: Sun, 24 Nov 2019 03:40:58 -0500 Subject: [PATCH] Add support for GitHub Actions --- .github/workflows/main.yml | 49 +++++++++++++++ README.md | 38 ++++++++++++ lib/excoveralls.ex | 9 +++ lib/excoveralls/github.ex | 103 ++++++++++++++++++++++++++++++++ lib/excoveralls/task/util.ex | 3 + lib/mix/tasks.ex | 13 ++++ test/excoveralls_test.exs | 5 ++ test/fixtures/github_event.json | 12 ++++ test/github_test.exs | 42 +++++++++++++ test/mix/tasks_test.exs | 6 ++ 10 files changed, 280 insertions(+) create mode 100644 .github/workflows/main.yml create mode 100644 lib/excoveralls/github.ex create mode 100644 test/fixtures/github_event.json create mode 100644 test/github_test.exs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..9007742c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,49 @@ +--- +name: build +env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Setup elixir + uses: actions/setup-elixir@v1.0.0 + with: + elixir-version: 1.9.x + otp-version: 22.x + + - name: Checkout repository + uses: actions/checkout@v1 + + - name: Get deps cache + uses: actions/cache@v1 + with: + path: deps/ + key: ${{ runner.os }}-deps-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-deps- + + - name: Get build cache + uses: actions/cache@v1 + with: + path: _build/test/ + key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-build- + + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + mix compile + + - name: Run Tests + run: | + mix coveralls.github diff --git a/README.md b/README.md index bf5e8f35..1930aea7 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ It uses Erlang's [cover](http://www.erlang.org/doc/man/cover.html) to generate c The following are example projects. - [coverage_sample](https://github.com/parroty/coverage_sample) is for Travis CI. + - [github_coverage_sample](https://github.com/mijailr/actions_sample) is for GitHub Actions. - [circle_sample](https://github.com/parroty/circle_sample) is for CircleCI . - [semaphore_sample](https://github.com/parroty/semaphore_sample) is for Semaphore CI. - [excoveralls_umbrella](https://github.com/parroty/excoveralls_umbrella) is for umbrella project. @@ -58,6 +59,8 @@ end - [[mix coveralls] Show coverage](#mix-coveralls-show-coverage) - [[mix coveralls.travis] Post coverage from travis](#mix-coverallstravis-post-coverage-from-travis) - [.travis.yml](#travisyml) + - [[mix coveralls.github] Post coverage from GitHub Actions](#mix-coverallsgithub-post-coverage-from-github-actions) + - [.github/workflows/main.yml](#githubworkflowsexampleyml) - [[mix coveralls.circle] Post coverage from circle](#mix-coverallscircle-post-coverage-from-circle) - [circle.yml](#circleyml) - [[mix coveralls.semaphore] Post coverage from semaphore](#mix-coverallssemaphore-post-coverage-from-semaphore) @@ -120,6 +123,9 @@ Usage: mix coveralls.detail [--filter file-name-pattern] Usage: mix coveralls.travis [--pro] Used to post coverage from Travis CI server. +Usage: mix coveralls.github + Used to post coverage from [GitHub Actions](https://github.com/features/actions). + Usage: mix coveralls.post Used to post coverage from local server using token. The token should be specified in the argument or in COVERALLS_REPO_TOKEN @@ -159,6 +165,38 @@ project, Use `coveralls.travis --pro` and ensure your coveralls.io repo token is available via the `COVERALLS_REPO_TOKEN` environment variable. +### [mix coveralls.github] Post coverage from [GitHub Actions](https://github.com/features/actions) +Specify `mix coveralls.github` as the build script in the GitHub action YML file and explicitly set the `MIX_ENV` environment to `test` and add `GITHUB_TOKEN` with the value of `{{ secrets.GITHUB_TOKEN }}`, this is required because is used internaly by coveralls.io to check the action and add statuses. + +The value of `secrets.GITHUB_TOKEN` is added automatically inside every GitHub action, so you not need to assing that. + +This task submits the result to Coveralls when the build is executed via GitHub actions and add statuses in the checks of github. + +#### .github/workflows/example.yml +```yml +on: push + +jobs: + test: + runs-on: ubuntu-latest + name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + strategy: + matrix: + otp: [21.3.8.10, 22.1.7] + elixir: [1.8.2, 1.9.4] + env: + MIX_ENV: test + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v1.0.0 + - uses: actions/setup-elixir@v1.0.0 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + - run: mix deps.get + - run: mix coveralls.github +``` + ### [mix coveralls.circle] Post coverage from circle Specify `mix coveralls.circle` in the `circle.yml`. This task is for submitting the result to the coveralls server when Circle-CI build is executed. diff --git a/lib/excoveralls.ex b/lib/excoveralls.ex index 15066f08..0c846671 100644 --- a/lib/excoveralls.ex +++ b/lib/excoveralls.ex @@ -8,6 +8,7 @@ defmodule ExCoveralls do alias ExCoveralls.ConfServer alias ExCoveralls.StatServer alias ExCoveralls.Travis + alias ExCoveralls.Github alias ExCoveralls.Circle alias ExCoveralls.Semaphore alias ExCoveralls.Drone @@ -17,6 +18,7 @@ defmodule ExCoveralls do alias ExCoveralls.Post @type_travis "travis" + @type_github "github" @type_circle "circle" @type_semaphore "semaphore" @type_drone "drone" @@ -59,6 +61,13 @@ defmodule ExCoveralls do Travis.execute(stats, options) end + @doc """ + Logic for posting from github action + """ + def analyze(stats, @type_github, options) do + Github.execute(stats, options) + end + @doc """ Logic for posting from circle-ci server """ diff --git a/lib/excoveralls/github.ex b/lib/excoveralls/github.ex new file mode 100644 index 00000000..49b71ed3 --- /dev/null +++ b/lib/excoveralls/github.ex @@ -0,0 +1,103 @@ +defmodule ExCoveralls.Github do + @moduledoc """ + Handles GitHub Actions integration with coveralls. + """ + alias ExCoveralls.Poster + + def execute(stats, options) do + json = generate_json(stats, Enum.into(options, %{})) + + if options[:verbose] do + IO.puts(json) + end + + Poster.execute(json) + end + + def generate_json(stats, options \\ %{}) + + def generate_json(stats, options) do + %{ + repo_token: get_env("GITHUB_TOKEN"), + service_name: "github", + source_files: stats, + parallel: options[:parallel], + git: git_info() + } + |> Map.merge(job_data()) + |> Jason.encode! + end + + defp get_env(env) do + env + |> System.get_env + end + + defp job_data() do + get_env("GITHUB_EVENT_NAME") + |> case do + "pull_request" -> + %{ + service_pull_request: get_pr_id(), + service_job_id: "#{get_sha("pull_request")}-PR-#{get_pr_id()}", + } + event -> + %{service_job_id: get_sha(event)} + end + end + + defp get_pr_id do + event_info() + |> Map.get("number") + |> Integer.to_string + end + + defp get_committer_name do + event_info() + |> Map.get("sender") + |> Map.get("login") + end + + defp get_sha("pull_request") do + event_info() + |> Map.get("pull_request") + |> Map.get("head") + |> Map.get("sha") + end + + defp get_sha(_) do + get_env("GITHUB_SHA") + end + + defp get_message("pull_request") do + {message, _} = System.cmd("git", ["log", get_sha("pull_request"), "-1", "--pretty=format:%s"]) + message + end + + defp get_message(_) do + {message, _} = System.cmd("git", ["log", "-1", "--pretty=format:%s"]) + message + end + + defp event_info do + get_env("GITHUB_EVENT_PATH") + |> File.read!() + |> Jason.decode!() + end + + defp git_info do + event = get_env("GITHUB_EVENT_NAME") + %{ + head: %{ + id: get_sha(event), + committer_name: get_committer_name(), + message: get_message(event) + }, + branch: get_branch() + } + end + + defp get_branch do + get_env("GITHUB_REF") + end +end diff --git a/lib/excoveralls/task/util.ex b/lib/excoveralls/task/util.ex index bcfd5a7a..8415c35d 100644 --- a/lib/excoveralls/task/util.ex +++ b/lib/excoveralls/task/util.ex @@ -27,6 +27,9 @@ Usage: mix coveralls.html Usage: mix coveralls.travis [--pro] Used to post coverage from Travis CI server. +Usage: mix coveralls.github + Used to post coverage from a GitHub Action. + Usage: mix coveralls.post Used to post coverage from local server using token. The token should be specified in the argument or in COVERALLS_REPO_TOKEN diff --git a/lib/mix/tasks.ex b/lib/mix/tasks.ex index d573d647..bbdadc14 100644 --- a/lib/mix/tasks.ex +++ b/lib/mix/tasks.ex @@ -147,6 +147,19 @@ defmodule Mix.Tasks.Coveralls do end end + defmodule Github do + @moduledoc """ + Provides an entry point for github's script. + """ + use Mix.Task + + @preferred_cli_env :test + + def run(args) do + Mix.Tasks.Coveralls.do_run(args, [type: "github"]) + end + end + defmodule Circle do @moduledoc """ Provides an entry point for CircleCI's script. diff --git a/test/excoveralls_test.exs b/test/excoveralls_test.exs index 26169ac1..4f133726 100644 --- a/test/excoveralls_test.exs +++ b/test/excoveralls_test.exs @@ -14,6 +14,11 @@ defmodule ExCoverallsTest do assert called ExCoveralls.Circle.execute(@stats,[]) end + test_with_mock "analyze github", ExCoveralls.Github, [execute: fn(_,_) -> nil end] do + ExCoveralls.analyze(@stats, "github", []) + assert called ExCoveralls.Github.execute(@stats,[]) + end + test_with_mock "analyze semaphore", ExCoveralls.Semaphore, [execute: fn(_,_) -> nil end] do ExCoveralls.analyze(@stats, "semaphore", []) assert called ExCoveralls.Semaphore.execute(@stats,[]) diff --git a/test/fixtures/github_event.json b/test/fixtures/github_event.json new file mode 100644 index 00000000..349c8cbc --- /dev/null +++ b/test/fixtures/github_event.json @@ -0,0 +1,12 @@ +{ + "_comment": "Not used data was removed, see documentation", + "number": 206, + "pull_request": { + "head": { + "sha": "7c90516a3ac9f43ab6cf46ec5668b4430a3af103" + } + }, + "sender": { + "login": "user" + } +} diff --git a/test/github_test.exs b/test/github_test.exs new file mode 100644 index 00000000..60e93712 --- /dev/null +++ b/test/github_test.exs @@ -0,0 +1,42 @@ +defmodule ExCoveralls.GithubTest do + use ExUnit.Case + import Mock + alias ExCoveralls.Github + + @content "defmodule Test do\n def test do\n end\nend\n" + @counts [0, 1, nil, nil] + @source_info [%{name: "test/fixtures/test.ex", source: @content, coverage: @counts}] + setup do + # No additional context + System.put_env("GITHUB_EVENT_PATH", "test/fixtures/github_event.json") + System.put_env("GITHUB_SHA", "sha1") + System.put_env("GITHUB_EVENT_NAME", "pull_request") + System.put_env("GITHUB_REF", "branch") + System.put_env("GITHUB_TOKEN", "token") + {:ok, []} + end + + test_with_mock "execute", ExCoveralls.Poster, execute: fn _ -> "result" end do + assert(Github.execute(@source_info, []) == "result") + end + + test "when is not a pull request" do + System.put_env("GITHUB_EVENT_NAME", "anything") + {:ok, payload} = Jason.decode(Github.generate_json(@source_info)) + + assert(payload["repo_token"] == "token") + assert(payload["service_job_id"] == "sha1") + assert(payload["service_name"] == "github") + assert(payload["service_pull_request"] == nil) + end + + test "generate from env vars" do + {:ok, payload} = Jason.decode(Github.generate_json(@source_info)) + + + assert(payload["repo_token"] == "token") + assert(payload["service_job_id"] == "7c90516a3ac9f43ab6cf46ec5668b4430a3af103-PR-206") + assert(payload["service_name"] == "github") + assert(payload["service_pull_request"] == "206") + end +end diff --git a/test/mix/tasks_test.exs b/test/mix/tasks_test.exs index 432d5faf..a2927bf7 100644 --- a/test/mix/tasks_test.exs +++ b/test/mix/tasks_test.exs @@ -121,6 +121,12 @@ defmodule Mix.Tasks.CoverallsTest do assert(ExCoveralls.ConfServer.get == [type: "semaphore", args: []]) end + test_with_mock "github", Runner, [run: fn(_, _) -> nil end] do + Mix.Tasks.Coveralls.Github.run([]) + assert(called Runner.run("test", ["--cover"])) + assert(ExCoveralls.ConfServer.get == [type: "github", args: []]) + end + test_with_mock "post with env vars", Runner, [run: fn(_, _) -> nil end] do org_token = System.get_env("COVERALLS_REPO_TOKEN") || "" org_name = System.get_env("COVERALLS_SERVICE_NAME") || ""