5.0 KiB · text 5af55d9
defmodule GitGudWeb.ActionsArtifactController do
@moduledoc """
GitHub Actions Pipelines v1 artifact API, used by
`actions/upload-artifact` and `actions/download-artifact`.
Endpoints under `/api/actions/_apis/pipelines/workflows/:run_id/`:
* `POST /artifacts` — create artifact (returns
`fileContainerResourceUrl`)
* `POST /artifacts/upload?itemPath` — upload bytes
* `PATCH /artifacts?artifactName=` — finalize
* `GET /artifacts` — list
* `GET /artifacts/:id/download?itemPath` — download
Storage: `GitGud.Storage` (Garage). Each artifact row in
`workflow_artifacts` records `name`, `size`, `content_type`, and
`storage_key`.
"""
use GitGudWeb, :controller
import Ecto.Query, warn: false
alias GitGud.Repo
alias GitGud.Storage
alias GitGud.Workflows.WorkflowArtifact
@bucket "gitgud-artifacts"
# ── create ──────────────────────────────────────────────────────────
def create(conn, %{"run_id" => run_id} = params) do
name = (params["body"] && params["body"]["name"]) || params["name"] || params["Name"]
if name == nil or name == "" do
send_resp(conn, 400, "")
else
run_id = String.to_integer(run_id)
artifact =
%WorkflowArtifact{workflow_run_id: run_id}
|> WorkflowArtifact.changeset(%{
name: name,
size: 0,
content_type: "application/octet-stream",
storage_key: storage_key(run_id, name)
})
|> Repo.insert!()
body =
Jason.encode!(%{
"containerId" => artifact.id,
"size" => 0,
"name" => name,
"fileContainerResourceUrl" => upload_url(conn, run_id, artifact.id)
})
conn
|> put_resp_content_type("application/json", nil)
|> send_resp(201, body)
end
end
# ── upload ──────────────────────────────────────────────────────────
def upload(conn, %{"run_id" => run_id, "id" => artifact_id} = _params) do
artifact = Repo.get!(WorkflowArtifact, String.to_integer(artifact_id))
if artifact.workflow_run_id != String.to_integer(run_id) do
send_resp(conn, 404, "")
else
{:ok, body, conn} = Plug.Conn.read_body(conn, length: 1_000_000_000)
case Storage.put(@bucket, artifact.storage_key, body, []) do
:ok ->
{:ok, _} =
artifact
|> WorkflowArtifact.changeset(%{size: byte_size(body)})
|> Repo.update()
send_resp(conn, 200, "")
{:error, reason} ->
conn
|> put_resp_content_type("application/json", nil)
|> send_resp(500, Jason.encode!(%{"error" => inspect(reason)}))
end
end
end
# ── finalize / list ─────────────────────────────────────────────────
def finalize(conn, %{"run_id" => _run_id}) do
# No-op — `upload/2` already persisted bytes + size.
send_resp(conn, 200, "")
end
def list(conn, %{"run_id" => run_id}) do
artifacts =
from(a in WorkflowArtifact, where: a.workflow_run_id == ^String.to_integer(run_id))
|> Repo.all()
body =
Jason.encode!(%{
"count" => length(artifacts),
"value" =>
Enum.map(artifacts, fn a ->
%{
"containerId" => a.id,
"name" => a.name,
"size" => a.size,
"fileContainerResourceUrl" => upload_url(conn, a.workflow_run_id, a.id)
}
end)
})
conn
|> put_resp_content_type("application/json", nil)
|> send_resp(200, body)
end
# ── download ────────────────────────────────────────────────────────
def download(conn, %{"run_id" => _run_id, "id" => artifact_id}) do
artifact = Repo.get!(WorkflowArtifact, String.to_integer(artifact_id))
case Storage.get(@bucket, artifact.storage_key) do
{:ok, body} ->
conn
|> put_resp_content_type(artifact.content_type || "application/octet-stream", nil)
|> send_resp(200, body)
{:error, _} ->
send_resp(conn, 404, "")
end
end
# ── helpers ─────────────────────────────────────────────────────────
defp storage_key(run_id, name) do
safe = String.replace(name, ~r/[^A-Za-z0-9._\-]/, "_")
"run-#{run_id}/#{safe}"
end
defp upload_url(_conn, run_id, artifact_id) do
GitGudWeb.Endpoint.url() <>
"/api/actions/_apis/pipelines/workflows/#{run_id}/artifacts/#{artifact_id}/upload"
end
end