6.6 KiB · text History 587faf4
defmodule GitGud.Workflows.EventPayload do
  @moduledoc """
  The event payload a job reads as `github.event`: GitHub's webhook shapes,
  trimmed to the fields workflows actually read
  (`github.event.head_commit.message`, `github.event.pull_request.number`,
  `github.event.inputs.*`, …).

  Built once, when a run is created, and stored on it
  (`WorkflowRun.event_payload`); `RunnerController.build_task/1` sends it
  as the `event` object of the task's context. Keys are strings, shas are
  lowercase hex. Pure: the repository must have `:owner` and
  `:organization` preloaded, and users are passed in already loaded.

  `sender` is the triggering user's handle, or the repository owner's when
  the trigger isn't attributable — the same rule as `github.actor`.
  """

  alias GitGud.Accounts.User
  alias GitGud.Git
  alias GitGud.Repositories.Repository
  alias GitGud.Repositories.Storage
  alias GitGud.Workflows.Ref

  @zero_sha String.duplicate("0", 40)

  @doc """
  A `push`. `before`/`after` are binary shas (`before` nil or all zeros
  for a new ref); `commits` are `Git.commit()` maps, newest first, as
  `Git.log/3` returns them; `pusher`/`sender` are `%User{}` or nil.
  """
  def push(%Repository{} = repo, %{ref: ref, before: before, after: after_sha} = event) do
    commits = Enum.map(Map.get(event, :commits, []), &commit(repo, &1))
    after_hex = Git.to_hex(after_sha)
    before_hex = sha_hex(before)
    created = before_hex == @zero_sha

    %{
      "ref" => ref,
      "before" => before_hex,
      "after" => after_hex,
      "created" => created,
      "deleted" => false,
      # Not tracked: the post-receive hook doesn't say whether the update
      # was a fast-forward, and working it out costs a merge-base per push.
      "forced" => false,
      "base_ref" => nil,
      "compare" =>
        if(created,
          do: commit_url(repo, after_hex),
          else: repo_url(repo) <> "/compare?base=#{before_hex}&head=#{after_hex}"
        ),
      # GitHub lists oldest first.
      "commits" => Enum.reverse(commits),
      "head_commit" => List.first(commits),
      "pusher" => pusher(Map.get(event, :pusher)),
      "repository" => repository(repo),
      "sender" => sender(repo, Map.get(event, :sender))
    }
  end

  @doc """
  A `pull_request`. `pr` is the `%PullRequest{}` (its `author` preloaded
  or nil), `action` the activity type; `target_sha` is the target branch's
  head (binary sha or nil), `sender` a `%User{}` or nil.

  `head.repo` is the target repository: fork and federated pull requests
  aren't dispatched (`Workflows.dispatch_pull_request/3`).
  """
  def pull_request(%Repository{} = repo, pr, action, %{} = opts) do
    repository = repository(repo)
    owner = repository["owner"]["login"]
    source = short_branch(pr.source_ref)
    target = short_branch(pr.target_ref)

    %{
      "action" => action,
      "number" => pr.number,
      "pull_request" => %{
        "number" => pr.number,
        "title" => pr.title || "",
        "body" => pr.body || "",
        "state" => "open",
        "html_url" => repo_url(repo) <> "/pulls/#{pr.number}",
        "draft" => false,
        "merged" => false,
        "user" => %{"login" => author_login(repo, Map.get(pr, :author))},
        "head" => %{
          "ref" => source,
          "sha" => sha_hex(pr.head_sha),
          "label" => owner <> ":" <> source,
          "repo" => repository
        },
        "base" => %{
          "ref" => target,
          "sha" => sha_hex(Map.get(opts, :target_sha)),
          "label" => owner <> ":" <> target,
          "repo" => repository
        }
      },
      "repository" => repository,
      "sender" => sender(repo, Map.get(opts, :sender))
    }
  end

  @doc """
  A `workflow_dispatch`. `inputs` is a string → string map; `ref` the full
  ref; `workflow_path` the file's path in the repository.
  """
  def workflow_dispatch(%Repository{} = repo, ref, workflow_path, inputs, sender) do
    %{
      "inputs" => Map.new(inputs || %{}, fn {k, v} -> {to_string(k), to_string(v)} end),
      "ref" => ref,
      "workflow" => workflow_path,
      "repository" => repository(repo),
      "sender" => sender(repo, sender)
    }
  end

  @doc """
  A `create` or `delete`: `ref` is the full ref created or deleted; the
  payload carries its short name and `branch` / `tag`.
  """
  def ref_event(%Repository{} = repo, ref, sender) do
    %{
      "ref" => Ref.name(ref),
      "ref_type" => Ref.type(ref),
      "pusher_type" => "user",
      "master_branch" => repo.default_branch,
      "repository" => repository(repo),
      "sender" => sender(repo, sender)
    }
  end

  @doc "A `schedule`: the cron that fired. Nobody triggered it, so `sender` is the owner."
  def schedule(%Repository{} = repo, cron) do
    %{
      "schedule" => cron,
      "repository" => repository(repo),
      "sender" => sender(repo, nil)
    }
  end

  @doc "The `repository` object every payload carries."
  def repository(%Repository{} = repo) do
    owner = Storage.repo_handle(repo)

    %{
      "id" => repo.id,
      "name" => repo.name,
      "full_name" => owner <> "/" <> repo.name,
      "owner" => %{"login" => owner},
      "default_branch" => repo.default_branch,
      "private" => repo.visibility != "public",
      "html_url" => repo_url(repo),
      "clone_url" => GitGudWeb.Endpoint.url() <> "/" <> owner <> "/" <> repo.name <> ".git"
    }
  end

  defp commit(repo, c) do
    hex = Git.to_hex(c.sha)

    %{
      "id" => hex,
      "message" => c.message,
      "timestamp" => c.committed_at && DateTime.to_iso8601(c.committed_at),
      "url" => commit_url(repo, hex),
      "author" => %{"name" => c.author_name, "email" => c.author_email},
      "committer" => %{"name" => c.committer_name, "email" => c.committer_email}
    }
  end

  # The account's public email only: a job's payload ends up in logs.
  defp pusher(%User{} = u), do: %{"name" => u.handle, "email" => u.public_email || ""}
  defp pusher(_), do: nil

  defp sender(_repo, %User{handle: h, id: id}) when is_binary(h) and h != "",
    do: %{"login" => h, "id" => id}

  defp sender(repo, _), do: %{"login" => Storage.repo_handle(repo)}

  defp author_login(_repo, %User{handle: h}) when is_binary(h) and h != "", do: h
  defp author_login(repo, _), do: Storage.repo_handle(repo)

  defp repo_url(repo),
    do: GitGudWeb.Endpoint.url() <> "/r/" <> Storage.repo_handle(repo) <> "/" <> repo.name

  defp commit_url(repo, hex), do: repo_url(repo) <> "/commit/" <> hex

  defp short_branch("refs/heads/" <> b), do: b
  defp short_branch(b) when is_binary(b), do: b
  defp short_branch(_), do: ""

  defp sha_hex(<<_::binary-size(20)>> = sha), do: Git.to_hex(sha)
  defp sha_hex(_), do: @zero_sha
end