Remove inbound GitHub hook in favor of using GitHub Actions to sync/commit

This commit is contained in:
Simon Prévost 2024-02-01 21:47:05 -05:00
parent 656dc7459c
commit 390bf62e35
32 changed files with 14 additions and 1446 deletions

View File

@ -15,9 +15,7 @@ end
if config_env() == :test do
events = ~w(sync add_translations create_collaborator create_comment complete_review new_conflicts)
config :accent, Accent.Hook,
outbounds: [{Accent.Hook.Outbounds.Mock, events: events}],
inbounds: [{Accent.Hook.Inbounds.Mock, events: events}]
config :accent, Accent.Hook, outbounds: [{Accent.Hook.Outbounds.Mock, events: events}]
else
config :accent, Accent.Hook,
outbounds: [
@ -26,16 +24,13 @@ else
{Accent.Hook.Outbounds.Slack, events: ~w(sync complete_review new_conflicts)},
{Accent.Hook.Outbounds.Websocket,
events: ~w(sync create_collaborator create_comment complete_review new_conflicts)}
],
inbounds: [{Accent.Hook.Inbounds.GitHub, events: ~w(sync)}]
]
end
config :accent, Accent.Endpoint,
render_errors: [accepts: ~w(json)],
pubsub_server: Accent.PubSub
config :accent, hook_github_file_server: Accent.Hook.Inbounds.GitHub.FileServer.HTTP
config :accent, Oban,
plugins: [Oban.Plugins.Pruner],
queues: [hook: 10, operations: 10],

View File

@ -4,8 +4,6 @@ config :accent, Accent.Repo,
pool: Ecto.Adapters.SQL.Sandbox,
url: System.get_env("DATABASE_URL") || "postgres://localhost/accent_test"
config :accent, hook_github_file_server: Accent.Hook.Inbounds.GitHub.FileServerMock
config :ueberauth, Ueberauth, providers: [{:dummy, {Accent.Auth.Ueberauth.DummyStrategy, []}}]
config :accent, Oban, crontab: false, testing: :manual

View File

@ -80,14 +80,6 @@ defmodule Accent.IntegrationManager do
end
end
defp changeset_data("github") do
fn model, params ->
model
|> cast(params, [:repository, :default_ref, :token])
|> validate_required([:repository, :default_ref, :token])
end
end
defp changeset_data("azure_storage_container") do
fn model, params ->
model

View File

@ -9,9 +9,6 @@ defmodule Accent.Integration do
embeds_one(:data, IntegrationData, on_replace: :update) do
field(:url)
field(:repository)
field(:token)
field(:default_ref)
field(:azure_storage_container_sas)
end

View File

@ -23,15 +23,4 @@ defmodule Accent.Scopes.Integration do
def from_service(query, service) do
from(query, where: [service: ^service])
end
@doc """
## Examples
iex> Accent.Scopes.Integration.from_data_repository(Accent.Integration, "test")
#Ecto.Query<from i0 in Accent.Integration, where: fragment(\"?->>'repository' = ?\", i0.data, ^\"test\")>
"""
@spec from_data_repository(Ecto.Queryable.t(), String.t()) :: Ecto.Queryable.t()
def from_data_repository(query, repository) do
from(i in query, where: fragment("?->>'repository' = ?", i.data, ^repository))
end
end

View File

@ -20,9 +20,6 @@ defmodule Accent.GraphQL.Mutations.Integration do
input_object :project_integration_data_input do
field(:id, :id)
field(:url, :string)
field(:repository, :string)
field(:default_ref, :string)
field(:token, :string)
field(:azure_storage_container_sas, :string)
end

View File

@ -72,8 +72,6 @@ defmodule Accent.GraphQL.Types.Integration do
object :project_integration_github_data do
field(:id, non_null(:id))
field(:repository, non_null(:string))
field(:default_ref, non_null(:string))
end
object :project_integration_azure_data do

View File

@ -1,7 +1,6 @@
defmodule Accent.Hook do
@moduledoc false
def outbound(context), do: run(outbounds_modules(), context)
def inbound(context), do: run(inbounds_modules(), context)
defp run(modules, context) do
jobs =
@ -17,8 +16,4 @@ defmodule Accent.Hook do
defp outbounds_modules do
Application.get_env(:accent, __MODULE__)[:outbounds]
end
defp inbounds_modules do
Application.get_env(:accent, __MODULE__)[:inbounds]
end
end

View File

@ -1,128 +0,0 @@
defmodule Accent.Hook.Inbounds.GitHub do
@moduledoc false
use Oban.Worker, queue: :hook
alias Accent.Document
alias Accent.Hook.Inbounds.GitHub.AddTranslations
alias Accent.Hook.Inbounds.GitHub.Sync
alias Accent.Plugs.MovementContextParser
alias Accent.Repo
alias Accent.Scopes.Document, as: DocumentScope
alias Accent.Scopes.Version, as: VersionScope
alias Accent.Version
@impl Oban.Worker
def perform(%Oban.Job{args: args}) do
context = Accent.Hook.Context.from_worker(args)
context.payload["ref"]
|> ref_to_version(context.payload["default_ref"], context.project)
|> sync_and_add_translations(context.project, context.user, context.payload)
end
defp sync_and_add_translations({:ok, version}, project, user, payload) do
repo = payload["repository"]
token = payload["token"]
ref = (version && version.tag) || payload["default_ref"]
configs = fetch_config(repo, token, ref)
trees = fetch_trees(repo, token, ref)
revisions =
project
|> Ecto.assoc(:target_revisions)
|> Repo.all()
|> Repo.preload(:language)
Sync.persist(trees, configs, project, user, token, version)
Enum.each(revisions, fn revision ->
AddTranslations.persist(trees, configs, project, user, revision, token, version)
end)
end
defp sync_and_add_translations(_, _, _, _), do: :ok
def filter_by_patterns(patterns, files) do
Enum.group_by(files, fn file -> Enum.find(patterns, &ExMinimatch.match(&1["matcher"], file["path"])) end)
end
def movement_document(project, path) do
path =
path
|> Path.basename()
|> MovementContextParser.extract_path_from_filename()
Document
|> DocumentScope.from_path(path)
|> DocumentScope.from_project(project.id)
|> Repo.one()
|> Kernel.||(%Document{project_id: project.id, path: path})
end
def fetch_content(path, token) do
with {:ok, %{body: %{"content" => content}}} <- file_server().get_path(path, headers(token)),
decoded_contents =
content
|> String.split("\n")
|> Enum.reject(&(&1 === ""))
|> Enum.map(&Base.decode64/1),
true <- Enum.all?(decoded_contents, &match?({:ok, _}, &1)) do
decoded_content = Enum.map_join(decoded_contents, "", &elem(&1, 1))
{:ok, decoded_content}
else
_ -> {:ok, nil}
end
end
def ref_to_version(ref, default_ref, project) do
default_version(ref, default_ref) || version_from_ref(ref, project)
end
defp default_version(ref, default_ref) do
case Regex.named_captures(~r/refs\/heads\/(?<branch>.+)/, ref) do
%{"branch" => ^default_ref} -> {:ok, nil}
_ -> nil
end
end
defp version_from_ref(ref, project) do
with %{"tag" => tag} <- Regex.named_captures(~r/refs\/tags\/(?<tag>.+)/, ref),
%Version{} = version <-
Version
|> VersionScope.from_project(project.id)
|> VersionScope.from_tag(tag)
|> Repo.one() do
{:ok, version}
else
_ ->
nil
end
end
defp fetch_config(repo, token, ref) do
path = Path.join([repo, "contents", "accent.json"]) <> "?ref=#{ref}"
with {:ok, config} when is_binary(config) <- fetch_content(path, token),
{:ok, %{"files" => files}} <- Jason.decode(config) do
files
else
_ -> []
end
end
defp fetch_trees(repo, token, ref) do
path = Path.join([repo, "git", "trees", ref]) <> "?recursive=1"
case file_server().get_path(path, headers(token)) do
{:ok, %{body: %{"tree" => tree}}} -> Enum.filter(tree, &(&1["type"] === "blob"))
_ -> []
end
end
defp headers(token) do
[{"Authorization", "token #{token}"}]
end
defp file_server, do: Application.get_env(:accent, :hook_github_file_server)
end

View File

@ -1,72 +0,0 @@
defmodule Accent.Hook.Inbounds.GitHub.AddTranslations do
@moduledoc false
alias Accent.Hook.Inbounds.GitHub
alias Accent.Plugs.MovementContextParser
alias Movement.Builders.RevisionMerge, as: RevisionMergeBuilder
alias Movement.Context
alias Movement.Persisters.RevisionMerge, as: RevisionMergePersister
def persist(trees, configs, project, user, revision, token, version) do
trees
|> group_by_matched_target_config(configs, revision)
|> Enum.reject(&(elem(&1, 0) === nil))
|> Enum.flat_map(fn {config, files} ->
Enum.map(files, &build_context(&1, project, token, config["format"]))
end)
|> Enum.reject(&is_nil/1)
|> Enum.map(&assign_defaults(&1, user, revision, project, version))
|> Enum.each(&persist_contexts/1)
end
defp group_by_matched_target_config(files, configs, revision) do
configs
|> Enum.map(fn config ->
target =
config["target"]
|> Kernel.||("")
|> String.replace("%slug%", Accent.Revision.language(revision).slug)
|> String.replace("%original_file_name%", "*")
|> String.replace("%document_path%", "*")
Map.put(config, "matcher", ExMinimatch.compile(target))
end)
|> GitHub.filter_by_patterns(files)
end
defp persist_contexts(context) do
context
|> RevisionMergeBuilder.build()
|> RevisionMergePersister.persist()
end
defp assign_defaults(context, user, revision, project, version) do
context
|> Context.assign(:user_id, user.id)
|> Context.assign(:revision, revision)
|> Context.assign(:project, project)
|> Context.assign(:version, version)
|> Context.assign(:merge_type, "smart")
|> Context.assign(:merge_options, [])
|> Context.assign(:comparer, Movement.Comparer.comparer(:merge, "smart"))
end
defp build_context(file, project, token, format) do
with {:ok, parser} <- Langue.parser_from_format(format),
%{id: id} = document when not is_nil(id) <- GitHub.movement_document(project, file["path"]),
document = %{document | format: format},
{:ok, file_content} <- GitHub.fetch_content(file["url"], token),
%{entries: entries} <- MovementContextParser.to_entries(document, file_content, parser) do
%Context{
render: file_content,
entries: entries,
assigns: %{
options: [],
document: document,
document_update: %{}
}
}
else
_ -> nil
end
end
end

View File

@ -1,4 +0,0 @@
defmodule Accent.Hook.Inbounds.GitHub.FileServer do
@moduledoc false
@callback get_path(String.t(), list()) :: {:ok, String.t()} | {:error, any()}
end

View File

@ -1,25 +0,0 @@
defmodule Accent.Hook.Inbounds.GitHub.FileServer.HTTP do
@moduledoc false
@behaviour Accent.Hook.Inbounds.GitHub.FileServer
use HTTPoison.Base
@base_url "https://api.github.com/repos/"
@impl true
def get_path(path, options), do: get(path, options)
@impl true
def process_url(@base_url <> path), do: process_url(path)
def process_url(path), do: @base_url <> path
@impl true
def process_response_body(body) do
body
|> Jason.decode()
|> case do
{:ok, body} -> body
_ -> :error
end
end
end

View File

@ -1,67 +0,0 @@
defmodule Accent.Hook.Inbounds.GitHub.Sync do
@moduledoc false
alias Accent.Hook.Inbounds.GitHub
alias Accent.Plugs.MovementContextParser
alias Movement.Builders.ProjectSync, as: SyncBuilder
alias Movement.Context
alias Movement.Persisters.ProjectSync, as: SyncPersister
def persist(trees, configs, project, user, token, version) do
trees
|> group_by_matched_source_config(configs)
|> Enum.reject(&(elem(&1, 0) === nil))
|> Enum.flat_map(fn {config, files} ->
Enum.map(files, &build_context(&1, project, token, config["format"]))
end)
|> Enum.reject(&is_nil/1)
|> Enum.map(&assign_defaults(&1, user, project, version))
|> Enum.each(&persist_contexts/1)
end
defp group_by_matched_source_config(files, configs) do
configs
|> Enum.map(&Map.put(&1, "matcher", ExMinimatch.compile(&1["source"])))
|> GitHub.filter_by_patterns(files)
end
defp persist_contexts(context) do
context
|> SyncBuilder.build()
|> SyncPersister.persist()
end
defp assign_defaults(context, user, project, version) do
context
|> Context.assign(:user_id, user.id)
|> Context.assign(:project, project)
|> Context.assign(:version, version)
|> Context.assign(:comparer, Movement.Comparer.comparer(:sync, "smart"))
end
defp build_context(file, project, token, format) do
with {:ok, parser} <- Langue.parser_from_format(format),
document = GitHub.movement_document(project, file["path"]),
document = %{document | format: format},
{:ok, file_content} <- GitHub.fetch_content(file["url"], token),
%{entries: entries, document: parsed_document} <-
MovementContextParser.to_entries(document, file_content, parser) do
%Context{
render: file_content,
entries: entries,
assigns: %{
options: [],
document: document,
document_update: document_update(parsed_document)
}
}
else
_ -> nil
end
end
defp document_update(nil), do: nil
defp document_update(parsed_document) do
%{top_of_the_file_comment: parsed_document.top_of_the_file_comment, header: parsed_document.header}
end
end

View File

@ -1,76 +0,0 @@
defmodule Accent.Hook.GitHubController do
use Plug.Builder
import Canary.Plugs
import Ecto.Query, only: [first: 1]
alias Accent.Hook.Context, as: HookContext
alias Accent.Integration
alias Accent.Project
alias Accent.Repo
alias Accent.Scopes.Integration, as: IntegrationScope
plug(Plug.Assign, canary_action: :hook_update)
plug(:load_and_authorize_resource, model: Project, id_name: "project_id")
plug(:filter_event_type)
plug(:assign_payload)
plug(:update)
def update(conn, _) do
Accent.Hook.inbound(%HookContext{
event: "sync",
payload: conn.assigns[:payload],
project_id: conn.assigns[:project].id,
user_id: conn.assigns[:current_user].id
})
send_resp(conn, :no_content, "")
end
defp assign_payload(conn, _) do
with repository when is_binary(repository) <- conn.params["repository"]["full_name"],
ref when is_binary(ref) <- conn.params["ref"],
%{data: %{token: token, default_ref: default_ref}} <-
repository_integration(conn.assigns[:project], repository) do
assign(conn, :payload, %{
default_ref: default_ref,
ref: ref,
repository: repository,
token: token
})
else
_ ->
conn
|> send_resp(:no_content, "")
|> halt()
end
end
defp repository_integration(project, repository) do
Integration
|> IntegrationScope.from_project(project.id)
|> IntegrationScope.from_service("github")
|> IntegrationScope.from_data_repository(repository)
|> first()
|> Repo.one()
end
defp filter_event_type(conn, _) do
conn
|> get_req_header("x-github-event")
|> case do
["push"] ->
conn
["ping"] ->
conn
|> send_resp(:ok, "pong")
|> halt()
_ ->
conn
|> send_resp(:not_implemented, "")
|> halt()
end
end
end

View File

@ -79,8 +79,6 @@ defmodule Accent.Router do
# File export
get("/export", ExportController, [])
get("/jipt-export", ExportJIPTController, [])
post("/hooks/github", Hook.GitHubController, [], as: :hooks_github)
end
scope "/", Accent do

View File

@ -55,12 +55,10 @@ defmodule AccentTest.GraphQL.Requests.ProjectIntegrations do
test "create integration successfully", %{user: user, project: project, create_mutation: mutation} do
variables = %{
"service" => "GITHUB",
"service" => "SLACK",
"projectId" => project.id,
"data" => %{
"repository" => "foo/bar",
"token" => "1234",
"defaultRef" => "master"
"url" => "https://slack.com/hook?token=foo"
}
}
@ -79,12 +77,10 @@ defmodule AccentTest.GraphQL.Requests.ProjectIntegrations do
test "create integration with errors", %{user: user, project: project, create_mutation: mutation} do
variables = %{
"service" => "GITHUB",
"service" => "SLACK",
"projectId" => project.id,
"data" => %{
"repository" => "",
"token" => "",
"defaultRef" => ""
"url" => ""
}
}
@ -101,10 +97,8 @@ defmodule AccentTest.GraphQL.Requests.ProjectIntegrations do
validation_messages = get_in(data, [:data, "createProjectIntegration", "messages"])
assert length(validation_messages) === 3
assert length(validation_messages) === 1
assert %{"code" => "required", "field" => "data.defaultRef"} in validation_messages
assert %{"code" => "required", "field" => "data.repository"} in validation_messages
assert %{"code" => "required", "field" => "data.token"} in validation_messages
assert %{"code" => "required", "field" => "data.url"} in validation_messages
end
end

View File

@ -50,39 +50,6 @@ defmodule AccentTest.GraphQL.Resolvers.Integration do
assert get_in(Repo.all(Integration), [Access.all(), Access.key(:events)]) == [["sync"]]
end
test "create github", %{project: project, user: user} do
context = %{context: %{conn: %PlugConn{assigns: %{current_user: user}}}}
{:ok, integration} =
Resolver.create(
project,
%{service: "github", data: %{repository: "root/test", default_ref: "master", token: "1234"}},
context
)
assert integration.service == "github"
assert integration.data.repository == "root/test"
assert integration.data.default_ref == "master"
assert integration.data.token == "1234"
assert get_in(Repo.all(Integration), [Access.all(), Access.key(:service)]) == ["github"]
end
test "create github error", %{project: project, user: user} do
context = %{context: %{conn: %PlugConn{assigns: %{current_user: user}}}}
{:ok, integration} =
Resolver.create(
project,
%{service: "github", data: %{repository: "", default_ref: "master", token: "1234"}},
context
)
assert integration.changes.data.errors == [repository: {"can't be blank", [validation: :required]}]
assert Repo.all(Integration) == []
end
test "create slack error", %{project: project, user: user} do
context = %{context: %{conn: %PlugConn{assigns: %{current_user: user}}}}

View File

@ -1,610 +0,0 @@
defmodule AccentTest.Hook.Inbounds.GitHub do
@moduledoc false
use Accent.RepoCase, async: true
import Ecto.Query
import Mox
alias Accent.Document
alias Accent.Hook.Inbounds.GitHub, as: Consumer
alias Accent.Hook.Inbounds.GitHub.FileServerMock
alias Accent.Integration
alias Accent.Language
alias Accent.Operation
alias Accent.ProjectCreator
alias Accent.Repo
alias Accent.Revision
alias Accent.Translation
alias Accent.User
alias Accent.Version
alias Ecto.UUID
setup :verify_on_exit!
setup do
user = Repo.insert!(%User{email: "test@test.com"})
language = Repo.insert!(%Language{name: "English", slug: UUID.generate()})
{:ok, project} =
ProjectCreator.create(params: %{main_color: "#f00", name: "My project", language_id: language.id}, user: user)
document = Repo.insert!(%Document{project_id: project.id, path: "admin", format: "json"})
[project: project, document: document, user: user]
end
def gettext_file do
Base.encode64(~S(
msgid "key"
msgstr "value"
))
end
def json_file do
Base.encode64(~S(
{
"key": "value"
}
))
end
test "sync default version on default_ref develop", %{project: project, user: user} do
config =
%{
"files" => [
%{
"format" => "gettext",
"language" => "fr",
"source" => "priv/fr/**/*.po"
}
]
}
|> Jason.encode!()
|> Base.encode64()
FileServerMock
|> expect(:get_path, fn "accent/test-repo/contents/accent.json?ref=develop", [{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => config}}}
end)
|> expect(:get_path, fn "accent/test-repo/git/trees/develop?recursive=1", [{"Authorization", "token 1234"}] ->
{:ok,
%{
body: %{
"tree" => [
%{
"path" => "accent.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/1"
},
%{
"path" => "Dockerfile",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/2"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/3"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/4"
},
%{
"path" => "priv/fr/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/5"
},
%{
"path" => "priv/en/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/6"
}
]
}
}}
end)
|> expect(:get_path, fn "https://api.github.com/repos/accent/test-repo/git/blobs/5",
[{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => gettext_file()}}}
end)
data = %{default_ref: "develop", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/heads/develop",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
batch_operation =
Operation
|> where([o], o.batch == true)
|> Repo.one()
operation =
Operation
|> where([o], o.batch == false)
|> Repo.one()
translation =
Translation
|> where([t], t.key == ^"key")
|> Repo.one()
assert batch_operation.action === "sync"
assert operation.action === "new"
assert operation.translation_id === translation.id
assert translation.proposed_text === "value"
end
test "sync with json file", %{project: project, user: user} do
config =
%{
"files" => [
%{
"format" => "json",
"language" => "fr",
"source" => "priv/fr/**/*.json"
}
]
}
|> Jason.encode!()
|> Base.encode64()
FileServerMock
|> expect(:get_path, fn "accent/test-repo/contents/accent.json?ref=develop", [{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => config}}}
end)
|> expect(:get_path, fn "accent/test-repo/git/trees/develop?recursive=1", [{"Authorization", "token 1234"}] ->
{:ok,
%{
body: %{
"tree" => [
%{
"path" => "accent.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/1"
},
%{
"path" => "Dockerfile",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/2"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/3"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/4"
},
%{
"path" => "priv/fr/admin.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/5"
},
%{
"path" => "priv/en/admin.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/6"
}
]
}
}}
end)
|> expect(:get_path, fn "https://api.github.com/repos/accent/test-repo/git/blobs/5",
[{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => json_file()}}}
end)
data = %{default_ref: "develop", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/heads/develop",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
batch_operation =
Operation
|> where([o], o.batch == true)
|> Repo.one()
operation =
Operation
|> where([o], o.batch == false)
|> Repo.one()
translation =
Translation
|> where([t], t.key == ^"key")
|> Repo.one()
assert batch_operation.action === "sync"
assert operation.action === "new"
assert operation.translation_id === translation.id
assert translation.proposed_text === "value"
end
test "dont sync when default ref does not match", %{project: project, user: user} do
data = %{default_ref: "master", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/heads/feature/my-feature",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
translation =
Translation
|> where([t], t.key == ^"key")
|> Repo.one()
refute translation
end
test "sync tag version on matching ref tag", %{project: project, user: user} do
config =
%{
"files" => [
%{
"format" => "gettext",
"language" => "fr",
"source" => "priv/fr/**/*.po"
}
]
}
|> Jason.encode!()
|> Base.encode64()
FileServerMock
|> expect(:get_path, fn "accent/test-repo/contents/accent.json?ref=v1.0.0", [{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => config}}}
end)
|> expect(:get_path, fn "accent/test-repo/git/trees/v1.0.0?recursive=1", [{"Authorization", "token 1234"}] ->
{:ok,
%{
body: %{
"tree" => [
%{
"path" => "accent.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/1"
},
%{
"path" => "Dockerfile",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/2"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/3"
},
%{
"path" => "priv/fr",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/4"
},
%{
"path" => "priv/fr/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/5"
},
%{
"path" => "priv/en/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/6"
}
]
}
}}
end)
|> expect(:get_path, fn "https://api.github.com/repos/accent/test-repo/git/blobs/5",
[{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => gettext_file()}}}
end)
version = Repo.insert!(%Version{project_id: project.id, user_id: user.id, tag: "v1.0.0", name: "First release"})
data = %{default_ref: "master", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/tags/v1.0.0",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
batch_operation =
Operation
|> where([o], o.batch == true and o.version_id == ^version.id)
|> Repo.one()
operation =
Operation
|> where([o], o.batch == false and o.version_id == ^version.id)
|> Repo.one()
translation =
Translation
|> where([t], t.key == ^"key" and t.version_id == ^version.id)
|> Repo.one()
assert batch_operation.action === "sync"
assert operation.action === "new"
assert operation.translation_id === translation.id
assert translation.proposed_text === "value"
end
test "add translations default version on default_ref develop", %{project: project, document: document, user: user} do
language_slug = UUID.generate()
language = Repo.insert!(%Language{name: "Other french", slug: language_slug})
revision = Repo.insert!(%Revision{project_id: project.id, master: false, language: language})
translation =
Repo.insert!(%Translation{
revision_id: revision.id,
document_id: document.id,
key: "key",
proposed_text: "a",
corrected_text: "a"
})
config =
%{
"files" => [
%{
"format" => "gettext",
"language" => "fr",
"source" => "priv/fr/**/*.po",
"target" => "priv/%slug%/**/%document_path%.po"
}
]
}
|> Jason.encode!()
|> Base.encode64()
FileServerMock
|> expect(:get_path, fn "accent/test-repo/contents/accent.json?ref=develop", [{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => config}}}
end)
|> expect(:get_path, fn "accent/test-repo/git/trees/develop?recursive=1", [{"Authorization", "token 1234"}] ->
{:ok,
%{
body: %{
"tree" => [
%{
"path" => "accent.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/1"
},
%{
"path" => "Dockerfile",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/2"
},
%{
"path" => "priv/#{language_slug}",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/4"
},
%{
"path" => "priv/#{language_slug}/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/6"
}
]
}
}}
end)
|> expect(:get_path, fn "https://api.github.com/repos/accent/test-repo/git/blobs/6",
[{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => gettext_file()}}}
end)
data = %{default_ref: "develop", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/heads/develop",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
batch_operation =
Operation
|> where([o], o.batch == true)
|> Repo.one()
operation =
Operation
|> where([o], o.batch == false)
|> Repo.one()
updated_translation =
Translation
|> where([t], t.key == ^"key")
|> Repo.one()
assert batch_operation.action === "merge"
assert operation.action === "merge_on_proposed"
assert operation.translation_id === translation.id
assert operation.previous_translation === %Accent.PreviousTranslation{
corrected_text: "a",
proposed_text: "a",
value_type: "string"
}
assert updated_translation.conflicted_text === "a"
assert updated_translation.proposed_text === "value"
end
test "add translations with language overrides", %{project: project, document: document, user: user} do
language_override = UUID.generate()
language_slug = UUID.generate()
language = Repo.insert!(%Language{name: "Other french", slug: language_slug})
revision =
Repo.insert!(%Revision{project_id: project.id, master: false, language: language, slug: language_override})
translation =
Repo.insert!(%Translation{
revision_id: revision.id,
document_id: document.id,
key: "key",
proposed_text: "a",
corrected_text: "a"
})
config =
%{
"files" => [
%{
"format" => "gettext",
"language" => "fr",
"source" => "priv/fr/**/*.po",
"target" => "priv/%slug%/**/%document_path%.po"
}
]
}
|> Jason.encode!()
|> Base.encode64()
FileServerMock
|> expect(:get_path, fn "accent/test-repo/contents/accent.json?ref=develop", [{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => config}}}
end)
|> expect(:get_path, fn "accent/test-repo/git/trees/develop?recursive=1", [{"Authorization", "token 1234"}] ->
{:ok,
%{
body: %{
"tree" => [
%{
"path" => "accent.json",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/1"
},
%{
"path" => "Dockerfile",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/2"
},
%{
"path" => "priv/#{language_override}",
"type" => "tree",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/4"
},
%{
"path" => "priv/#{language_override}/admin.po",
"type" => "blob",
"url" => "https://api.github.com/repos/accent/test-repo/git/blobs/6"
}
]
}
}}
end)
|> expect(:get_path, fn "https://api.github.com/repos/accent/test-repo/git/blobs/6",
[{"Authorization", "token 1234"}] ->
{:ok, %{body: %{"content" => gettext_file()}}}
end)
data = %{default_ref: "develop", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
context =
to_worker_args(%Accent.Hook.Context{
user_id: user.id,
project_id: project.id,
event: "push",
payload: %{
default_ref: data.default_ref,
ref: "refs/heads/develop",
repository: data.repository,
token: data.token
}
})
Consumer.perform(%Oban.Job{args: context})
batch_operation =
Operation
|> where([o], o.batch == true)
|> Repo.one()
operation =
Operation
|> where([o], o.batch == false)
|> Repo.one()
updated_translation =
Translation
|> where([t], t.key == ^"key")
|> Repo.one()
assert batch_operation.action === "merge"
assert operation.action === "merge_on_proposed"
assert operation.translation_id === translation.id
assert operation.previous_translation === %Accent.PreviousTranslation{
corrected_text: "a",
proposed_text: "a",
value_type: "string"
}
assert updated_translation.conflicted_text === "a"
assert updated_translation.proposed_text === "value"
end
end

View File

@ -1,15 +1,3 @@
Mox.defmock(Accent.Hook.Inbounds.GitHub.FileServerMock, for: Accent.Hook.Inbounds.GitHub.FileServer)
defmodule Accent.Hook.Inbounds.Mock do
@moduledoc false
use Oban.Worker, queue: :hook
@impl Oban.Worker
def perform(_job) do
:ok
end
end
defmodule Accent.Hook.Outbounds.Mock do
@moduledoc false
use Oban.Worker, queue: :hook

View File

@ -1,130 +0,0 @@
defmodule AccentTest.Hook.GitHubController do
use Accent.ConnCase
alias Accent.AccessToken
alias Accent.Collaborator
alias Accent.Integration
alias Accent.Project
alias Accent.Repo
alias Accent.User
@user %User{email: "test@test.com"}
setup do
user = Repo.insert!(@user)
access_token = Repo.insert!(%AccessToken{user_id: user.id, token: "test-token"})
project = Repo.insert!(%Project{main_color: "#f00", name: "My project"})
Repo.insert!(%Collaborator{project_id: project.id, user_id: user.id, role: "bot"})
{:ok, [access_token: access_token, user: user, project: project]}
end
test "acknowledge ping", %{access_token: access_token, conn: conn, project: project} do
params = %{
"ref" => "refs/heads/master",
"repository" => %{
"full_name" => "accent/test-repo"
}
}
response =
conn
|> put_req_header("x-github-event", "ping")
|> post(hooks_github_path(conn, []) <> "?authorization=#{access_token.token}&project_id=#{project.id}", params)
assert response.status == 200
assert response.resp_body == "pong"
end
test "broadcast event on push", %{user: user, access_token: access_token, conn: conn, project: project} do
params = %{
"ref" => "refs/heads/master",
"repository" => %{
"full_name" => "accent/test-repo"
}
}
data = %{default_ref: "master", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
response =
conn
|> put_req_header("x-github-event", "push")
|> post(hooks_github_path(conn, []) <> "?authorization=#{access_token.token}&project_id=#{project.id}", params)
assert response.status == 204
assert_enqueued(
worker: Accent.Hook.Inbounds.Mock,
args: %{
"event" => "sync",
"payload" => %{
"default_ref" => "master",
"ref" => "refs/heads/master",
"repository" => "accent/test-repo",
"token" => "1234"
},
"project_id" => project.id,
"user_id" => user.id
}
)
end
test "dont broadcast event on other event", %{user: user, access_token: access_token, conn: conn, project: project} do
params = %{
"ref" => "refs/heads/master",
"repository" => %{
"full_name" => "accent/test-repo"
}
}
data = %{default_ref: "master", repository: "accent/test-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
response =
conn
|> put_req_header("x-github-event", "pull_request_comment")
|> post(hooks_github_path(conn, []) <> "?authorization=#{access_token.token}&project_id=#{project.id}", params)
assert response.status == 501
end
test "dont broadcast event on non existing integration", %{access_token: access_token, conn: conn, project: project} do
params = %{
"ref" => "refs/heads/master",
"repository" => %{
"full_name" => "accent/test-repo"
}
}
response =
conn
|> put_req_header("x-github-event", "push")
|> post(hooks_github_path(conn, []) <> "?authorization=#{access_token.token}&project_id=#{project.id}", params)
assert response.status == 204
end
test "dont broadcast event on non matching integration", %{
user: user,
access_token: access_token,
conn: conn,
project: project
} do
params = %{
"ref" => "refs/heads/master",
"repository" => %{
"full_name" => "accent/test-repo"
}
}
data = %{default_ref: "master", repository: "accent/other-repo", token: "1234"}
Repo.insert!(%Integration{project_id: project.id, user_id: user.id, service: "github", data: data})
response =
conn
|> put_req_header("x-github-event", "push")
|> post(hooks_github_path(conn, []) <> "?authorization=#{access_token.token}&project_id=#{project.id}", params)
assert response.status == 204
end
end

View File

@ -494,9 +494,6 @@
"last_executed_at": "Last executed at:",
"data": {
"azure_storage_container_sas": "SAS URL",
"default_ref": "Default ref",
"repository": "Repository",
"token": "Token",
"url": "URL"
},
"token_how": "How?",
@ -979,7 +976,6 @@
},
"integration_services": {
"DISCORD": "Discord",
"GITHUB": "GitHub",
"SLACK": "Slack",
"AZURE_STORAGE_CONTAINER": "Azure Storage Container"
},

View File

@ -510,9 +510,6 @@
"delete": "Supprimer",
"data": {
"azure_storage_container_sas": "SAS URL",
"default_ref": "Réf par défaut",
"repository": "Dépôt",
"token": "Jeton",
"url": "URL"
},
"token_how": "Comment?",
@ -979,7 +976,6 @@
},
"integration_services": {
"DISCORD": "Discord",
"GITHUB": "GitHub",
"SLACK": "Slack",
"AZURE_STORAGE_CONTAINER": "Azure Storage Container"
},

View File

@ -18,16 +18,13 @@ interface Args {
service,
events,
integration,
data: {url, repository, token, defaultRef, azureStorageContainerSas},
data: {url, azureStorageContainerSas},
}: {
service: any;
events: any;
integration: any;
data: {
url: string;
repository: string;
token: string;
defaultRef: string;
azureStorageContainerSas: string;
};
}) => Promise<{errors: any}>;
@ -58,22 +55,13 @@ export default class IntegrationsForm extends Component<Args> {
@tracked
events: string[];
@tracked
repository: string;
@tracked
token: string;
@tracked
azureStorageContainerSas: string;
@tracked
azureStorageContainerSasBaseUrl: string;
@tracked
defaultRef = 'main';
services = ['AZURE_STORAGE_CONTAINER', 'SLACK', 'GITHUB', 'DISCORD'];
services = ['AZURE_STORAGE_CONTAINER', 'SLACK', 'DISCORD'];
@not('url')
emptyUrl: boolean;
@ -112,8 +100,6 @@ export default class IntegrationsForm extends Component<Args> {
events: [],
data: {
url: this.url,
repository: this.repository,
defaultRef: this.defaultRef,
},
};
}
@ -121,8 +107,6 @@ export default class IntegrationsForm extends Component<Args> {
this.service = this.integration.service || this.services[0];
this.url = this.integration.data.url;
this.events = this.integration.events;
this.repository = this.integration.data.repository;
this.defaultRef = this.integration.data.defaultRef;
this.azureStorageContainerSasBaseUrl = this.integration.data.sasBaseUrl;
}
@ -146,21 +130,6 @@ export default class IntegrationsForm extends Component<Args> {
this.events = events;
}
@action
setRepository(repository: string) {
this.repository = repository;
}
@action
setToken(token: string) {
this.token = token;
}
@action
setDefaultRef(defaultRef: string) {
this.defaultRef = defaultRef;
}
@action
setAzureStorageContainerSas(sas: string) {
this.azureStorageContainerSas = sas;
@ -176,9 +145,6 @@ export default class IntegrationsForm extends Component<Args> {
integration: this.integration.newRecord ? null : this.integration,
data: {
url: this.url,
repository: this.repository,
token: this.token,
defaultRef: this.defaultRef,
azureStorageContainerSas: this.azureStorageContainerSas,
},
});

View File

@ -2,18 +2,12 @@ import Component from '@glimmer/component';
import {action} from '@ember/object';
interface Args {
repository: any;
defaultRef: any;
token: any;
errors: any;
url: any;
project: any;
events: any;
onChangeUrl: (url: string) => void;
onChangeEventsChecked: (events: string[]) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;
}
export default class Discord extends Component<Args> {

View File

@ -1,51 +0,0 @@
import fmt from 'simple-fmt';
import Component from '@glimmer/component';
import {action} from '@ember/object';
import config from 'accent-webapp/config/environment';
interface Args {
repository: any;
defaultRef: any;
token: any;
errors: any;
url: any;
project: any;
onChangeUrl: (url: string) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;
}
export default class GitHub extends Component<Args> {
get webhookUrl() {
const host = window.location.origin;
return `${host}${fmt(
config.API.HOOKS_PATH,
'github',
this.args.project.id,
'<YOUR_API_TOKEN_HERE>'
)}`;
}
@action
changeRepository(event: Event) {
const target = event.target as HTMLInputElement;
this.args.onChangeRepository(target.value);
}
@action
changeToken(event: Event) {
const target = event.target as HTMLInputElement;
this.args.onChangeToken(target.value);
}
@action
changeDefaultRef(event: Event) {
const target = event.target as HTMLInputElement;
this.args.onChangeDefaultRef(target.value);
}
}

View File

@ -1,60 +0,0 @@
.instructions {
border-top: 1px solid var(--background-light-highlight);
padding-top: 10px;
margin: 20px 0 10px;
}
.instructions-text {
margin-top: 7px;
font-style: italic;
color: #555;
a {
font-family: var(--font-monospace);
color: var(--color-primary);
text-decoration: none;
&:focus,
&:hover {
text-decoration: underline;
opacity: 0.8;
}
}
}
.data-control {
margin-bottom: 15px;
}
.data-title {
display: block;
margin-bottom: 5px;
font-size: 13px;
font-weight: bold;
}
.data-title-help {
margin-left: 4px;
font-size: 11px;
font-weight: normal;
text-decoration: none;
color: var(--color-primary);
&:focus,
&:hover {
text-decoration: underline;
}
}
.textInput {
@extend %textInput;
flex-grow: 1;
flex-shrink: 0;
padding: 8px 10px;
margin-right: 10px;
background: #fafafa;
max-width: 500px;
width: 100%;
font-family: var(--font-monospace);
font-size: 11px;
}

View File

@ -1,47 +0,0 @@
<ProjectSettings::Integrations::Form::DataControlText
@error={{field-error @errors 'data.repository'}}
@label={{t 'components.project_settings.integrations.data.repository'}}
@placeholder='mirego/accent'
@value={{@repository}}
@onChange={{this.changeRepository}}
/>
<ProjectSettings::Integrations::Form::DataControlText
@error={{field-error @errors 'data.token'}}
@label={{t 'components.project_settings.integrations.data.token'}}
@placeholder='aaaa-bbbb-cccc'
@value={{@token}}
@onChange={{this.changeToken}}
@helpLinkHref='https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line'
@helpLinkTitle={{t 'components.project_settings.integrations.token_how'}}
/>
<ProjectSettings::Integrations::Form::DataControlText
@error={{field-error @errors 'data.defaultRef'}}
@label={{t 'components.project_settings.integrations.data.default_ref'}}
@value={{@defaultRef}}
@onChange={{this.changeDefaultRef}}
/>
{{#if this.webhookUrl}}
<div local-class='instructions'>
<label local-class='data-title'>
{{t 'components.project_settings.integrations.github_webhook_url'}}
<a local-class='data-title-help' target='_blank' rel='noopener noreferrer' href='https://developer.github.com/webhooks/creating'>
{{t 'components.project_settings.integrations.github_webhook_url_how'}}
</a>
</label>
<input readonly='' onClick='this.select();' local-class='textInput' value={{this.webhookUrl}} />
<div local-class='instructions-text'>
<p>
{{t 'components.project_settings.integrations.github_webhook_accent_cli_1' htmlSafe=true}}
</p>
<p>
{{t 'components.project_settings.integrations.github_webhook_accent_cli_2' htmlSafe=true}}
</p>
</div>
</div>
{{/if}}

View File

@ -2,18 +2,12 @@ import Component from '@glimmer/component';
import {action} from '@ember/object';
interface Args {
repository: any;
defaultRef: any;
token: any;
errors: any;
url: any;
project: any;
events: any;
onChangeUrl: (url: string) => void;
onChangeEventsChecked: (events: string[]) => void;
onChangeRepository: (repository: string) => void;
onChangeToken: (token: string) => void;
onChangeDefaultRef: (defaultRef: string) => void;
}
export default class Slack extends Component<Args> {

View File

@ -22,19 +22,13 @@
<div local-class='data'>
{{component
this.dataFormComponent
repository=this.repository
defaultRef=this.defaultRef
token=this.token
errors=this.errors
url=this.url
project=@project
events=this.events
onChangeUrl=(fn this.setUrl)
onChangeEventsChecked=(fn this.setEventsChecked)
onChangeRepository=(fn this.setRepository)
onChangeToken=(fn this.setToken)
onChangeDefaultRef=(fn this.setDefaultRef)
onChangeSas=(fn this.setAzureStorageContainerSas)
onChangeEventsChecked=(fn this.setEventsChecked)
}}
</div>
@ -50,4 +44,4 @@
{{/if}}
</div>
{{/if}}
</div>
</div>

View File

@ -17,7 +17,6 @@
<span local-class='details-preview'>
{{@integration.data.url}}
{{@integration.data.repository}}
{{@integration.data.sasBaseUrl}}
{{#if @integration.lastExecutedAt}}
@ -53,4 +52,4 @@
<AccModal @small={{true}} @onClose={{fn this.toggleExecuting}}>
{{component this.dataExecuteComponent integration=@integration close=this.toggleExecuting}}
</AccModal>
{{/if}}
{{/if}}

View File

@ -27,14 +27,6 @@ export default gql`
}
}
... on ProjectIntegrationGithub {
data {
id
repository
defaultRef
}
}
... on ProjectIntegrationAzureStorageContainer {
lastExecutedAt
data {

View File

@ -27,8 +27,7 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
possibleTypes: [
{name: 'ProjectIntegrationAzureStorageContainer'},
{name: 'ProjectIntegrationDiscord'},
{name: 'ProjectIntegrationSlack'},
{name: 'ProjectIntegrationGithub'},
{name: 'ProjectIntegrationSlack'}
],
},
],