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.
`opts` may also carry `:head_repo`, the `repository` object the head
came from (a fork's, or `federated_head/2`'s), else the head is in
`repo`; and `:author_login`, for an author who isn't a local user.
"""
def pull_request(%Repository{} = repo, pr, action, %{} = opts) do
repository = repository(repo)
owner = repository["owner"]["login"]
head_repo = Map.get(opts, :head_repo) || repository
head_owner = get_in(head_repo, ["owner", "login"]) || owner
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 || "",
# GitHub's states are open and closed; merged is closed + merged.
"state" => if(pr.state in ["closed", "merged"], do: "closed", else: "open"),
"html_url" => repo_url(repo) <> "/pulls/#{pr.number}",
"draft" => false,
# A real boolean: forgejo-runner reads it to scope the cache of a
# `closed` run.
"merged" => pr.state == "merged",
"merged_at" => iso8601(Map.get(pr, :merged_at)),
"merge_commit_sha" =>
if(pr.state == "merged", do: sha_hex(Map.get(pr, :merge_commit_sha)), else: nil),
"user" => %{
"login" => Map.get(opts, :author_login) || author_login(repo, Map.get(pr, :author))
},
"head" => %{
"ref" => source,
"sha" => sha_hex(pr.head_sha),
"label" => head_owner <> ":" <> source,
"repo" => head_repo
},
"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 """
`pull_request/4`'s `:head_repo` and `:author_login` for a pull request
offered over federation by `actor` (a `GitGud.Federation.Actor`, or nil)
from `clone_url`: the author is `user@host`, never a local name — a
workflow comparing `pull_request.user.login` against a maintainer's must
not take a remote account for them.
"""
def federated_head(actor, clone_url) do
login =
case actor do
%{preferred_username: name, actor_url: url} when is_binary(name) and is_binary(url) ->
name <> "@" <> (URI.parse(url).host || "")
%{actor_url: url} when is_binary(url) ->
url
_ ->
"unknown@federated"
end
%{
author_login: login,
head_repo: %{
"full_name" => login,
"owner" => %{"login" => login},
"clone_url" => clone_url || "",
"html_url" => clone_url || "",
"fork" => true
}
}
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 `workflow_run`: `run` (a `%WorkflowRun{}`) reached `action` —
`"requested"`, `"in_progress"` or `"completed"`. Also the body of the
`workflow_run` webhook.
"""
def workflow_run(%Repository{} = repo, run, action, sender) do
%{
"action" => action,
"workflow_run" => workflow_run_object(repo, run, action),
"workflow" => %{
"name" => run.workflow_name || run.workflow_path,
"path" => run.workflow_path
},
"repository" => repository(repo),
"sender" => sender(repo, sender)
}
end
defp workflow_run_object(repo, run, action) do
status =
case action do
"requested" -> "queued"
other -> other
end
%{
"id" => run.id,
"run_number" => run.number,
"name" => run.workflow_name || run.workflow_path,
"path" => run.workflow_path,
"event" => run.trigger,
"status" => status,
"conclusion" => if(action == "completed", do: run.conclusion || run.status),
"head_sha" => sha_hex(run.head_sha),
"head_branch" => run_head_branch(run),
"html_url" => repo_url(repo) <> "/actions/#{run.number}",
"run_attempt" => 1,
"run_started_at" => iso8601(run.started_at),
"created_at" => iso8601(run.inserted_at),
"updated_at" => iso8601(run.completed_at || run.updated_at)
}
end
@doc """
A run's `head_branch`, as GitHub reports it: the branch (or tag) name,
and a pull request run's source branch.
"""
def run_head_branch(%{trigger: "pull_request", pr_head_ref: b}) when is_binary(b), do: b
def run_head_branch(%{head_ref: ref}), do: Ref.name(ref)
@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 iso8601(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
defp iso8601(_), do: nil
defp sha_hex(<<_::binary-size(20)>> = sha), do: Git.to_hex(sha)
defp sha_hex(_), do: @zero_sha
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
9.9 KiB · text
History
1b35c31