Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
plugins: [Styler]
plugins: [DoctestFormatter, Styler]
]
8 changes: 7 additions & 1 deletion lib/mix/tasks/mix_dependency_submission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ if Mix.env() == :dev do

defmodule Mix.Tasks.MixDependencySubmission do
@shortdoc "Run mix_depdendency_submission"
@moduledoc @shortdoc
@moduledoc """
#{@shortdoc}

Only intented for development purposes. Use the burrito binary for
production.
"""

use Mix.Task

alias MixDependencySubmission.CLI.Submit

@requirements ["app.start"]

@doc false
@impl Mix.Task
def run(args) do
Submit.run(args)
Expand Down
108 changes: 103 additions & 5 deletions lib/mix_dependency_submission.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,94 @@
defmodule MixDependencySubmission do
@moduledoc false
@moduledoc """
Builds the dependency submission payload from one or more Mix projects.

Responsible for collecting dependency data, resolving manifests, and
assembling the top-level submission struct to be sent to GitHub.

See https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository

> #### API Interface {: .warning}
> This project is **not a library** and is intended **only** for use as a
> GitHub Action. All modules, functions, and types are considered internal and
> may change at any time without notice.
>
> If you're looking to integrate this functionality into your own tooling, open
> an issue to discuss the use case, but do not rely on this as a stable library
> interface.

"""

alias MixDependencySubmission.Fetcher
alias MixDependencySubmission.Submission
alias MixDependencySubmission.Submission.Manifest
alias MixDependencySubmission.Util

@doc """
Builds a new `MixDependencySubmission.Submission` from a set of project
options.

Finds nested Mix projects (unless ignored), resolves dependencies,
and prepares the manifest payload.

## Examples

iex> MixDependencySubmission.submission(
...> github_job_id: "job123",
...> github_workflow: "ci.yml",
...> sha: "sha",
...> ref: "refs/heads/main",
...> project_path: ".",
...> install_deps?: false,
...> ignore: []
...> )
%MixDependencySubmission.Submission{
version: 0,
job: %MixDependencySubmission.Submission.Job{
id: "job123",
correlator: "ci.yml job123",
html_url: nil
},
sha: "sha",
ref: "refs/heads/main",
detector: %MixDependencySubmission.Submission.Detector{
name: "mix_dependency_submission",
version: %Version{major: 1, minor: 0, patch: 0, pre: ["beta", 8]},
url: %URI{
scheme: "https",
userinfo: nil,
host: "github.com",
port: 443,
path: "/erlef/mix-dependency-submission",
query: nil,
fragment: nil
}
},
scanned: ~U[2025-04-19 10:15:11.656801Z],
metadata: %{},
manifests: %{
"mix.exs" => %MixDependencySubmission.Submission.Manifest{
name: "mix.exs",
file: %MixDependencySubmission.Submission.Manifest.File{
source_location: "mix.exs"
},
metadata: %{},
resolved: %{
"expo" => %MixDependencySubmission.Submission.Manifest.Dependency{
package_url:
Purl.parse!(
"pkg:github/elixir-gettext/expo@2ae85019d62288001bdc4a949d65bf650beee315"
),
metadata: %{},
relationship: :direct,
scope: :runtime,
dependencies: []
}
}
}
}
}

"""
@spec submission(
options :: [
{:github_job_id, String.t()}
Expand Down Expand Up @@ -33,7 +116,9 @@ defmodule MixDependencySubmission do
options[:project_path]
|> find_mix_projects(options[:ignore] || [], options[:paths_relative_to])
|> Map.new(fn project_path ->
manifest = manifest(project_path, Keyword.take(options, [:paths_relative_to, :install_deps?]))
manifest =
manifest(project_path, Keyword.take(options, [:paths_relative_to, :install_deps?]))

{manifest.file.source_location, manifest}
end)

Expand All @@ -46,7 +131,15 @@ defmodule MixDependencySubmission do
})
end

@spec manifest(project_path :: Path.t(), options :: [{:paths_relative_to, Path.t()} | {:install_deps?, boolean()}]) ::
@doc """
Resolves the dependency manifest for a single Mix project.

Optionally installs dependencies beforehand if `install_deps?` is true.
"""
@spec manifest(
project_path :: Path.t(),
options :: [{:paths_relative_to, Path.t()} | {:install_deps?, boolean()}]
) ::
Manifest.t()
def manifest(project_path, options) do
Util.in_project(project_path, fn _mix_module ->
Expand All @@ -58,7 +151,11 @@ defmodule MixDependencySubmission do
end)
end

@spec find_mix_projects(project_path :: Path.t(), ignore :: [Path.t()], paths_relative_to :: Path.t()) :: [Path.t()]
@spec find_mix_projects(
project_path :: Path.t(),
ignore :: [Path.t()],
paths_relative_to :: Path.t()
) :: [Path.t()]
defp find_mix_projects(project_path, ignore, paths_relative_to) do
ignore = Enum.map(ignore, &Path.expand(&1, paths_relative_to))

Expand Down Expand Up @@ -102,6 +199,7 @@ defmodule MixDependencySubmission do
}
end

@spec drop_empty(map :: %{key => value | nil}) :: %{key => value} when key: term(), value: term()
@spec drop_empty(map :: %{key => value | nil}) :: %{key => value}
when key: term(), value: term()
defp drop_empty(map), do: map |> Enum.reject(fn {_key, value} -> value in [nil, ""] end) |> Map.new()
end
39 changes: 38 additions & 1 deletion lib/mix_dependency_submission/api_client.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
defmodule MixDependencySubmission.ApiClient do
@moduledoc false
@moduledoc """
Handles submission of the dependency snapshot to the GitHub Dependency Submission API.
"""

alias MixDependencySubmission.Submission

@doc """
Submits a dependency snapshot to the GitHub API.

Returns `{:ok, response}` if the submission was accepted, or
`{:error, {:unexpected_response, response}}` for other HTTP status codes.

## Examples

iex> submission = %MixDependencySubmission.Submission{
...> version: 0,
...> job: %MixDependencySubmission.Submission.Job{
...> id: "job123",
...> correlator: "workflow job123"
...> },
...> sha: String.duplicate("a", 40),
...> ref: "refs/heads/main",
...> detector: %MixDependencySubmission.Submission.Detector{
...> name: "example",
...> version: Version.parse!("1.0.0"),
...> url: URI.parse("https://example.com")
...> },
...> scanned: DateTime.utc_now(),
...> manifests: %{}
...> }
...>
...> {:ok, %Req.Response{} = response} =
...> MixDependencySubmission.ApiClient.submit(
...> submission,
...> "https://api.github.com",
...> "owner/repo",
...> "ghp_exampletoken"
...> )

"""
@spec submit(
submission :: Submission.t(),
github_api_url :: String.t(),
Expand Down
22 changes: 21 additions & 1 deletion lib/mix_dependency_submission/cli.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
defmodule MixDependencySubmission.CLI do
@moduledoc false
@moduledoc """
Handles parsing of CLI arguments using `Optimus`.

Used to configure and validate inputs for submitting a dependency snapshot.
"""

@app Mix.Project.config()[:app]
@description Mix.Project.config()[:description]
@version Mix.Project.config()[:version]

@doc """
Parses CLI arguments and returns the parsed result.

Raises on invalid input.

## Examples

iex> argv =
...> ~w[--github-repository org/repo --github-job-id 123 --github-workflow build --sha sha --ref refs/heads/main --github-token ghp_xxx]
...>
...> result = MixDependencySubmission.CLI.parse!(argv)
...> result.options.github_repository
"org/repo"

"""
@spec parse!([String.t()]) :: Optimus.ParseResult.t()
def parse!(argv) do
cli_definition()
Expand Down
43 changes: 42 additions & 1 deletion lib/mix_dependency_submission/cli/submit.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,51 @@
defmodule MixDependencySubmission.CLI.Submit do
@moduledoc false
@moduledoc """
Handles the CLI submit command for Mix Dependency Submission.

This module parses CLI arguments, builds the dependency submission payload,
and sends it to the GitHub Dependency Submission API. It logs relevant details
about the submission process and handles success or failure scenarios
accordingly.
"""

alias MixDependencySubmission.ApiClient
alias MixDependencySubmission.CLI

require Logger

@doc """
Parses command-line arguments and submits the dependency snapshot to the
GitHub API.

This function is intended to be called from the CLI. It:

- Parses CLI arguments using `Optimus`.
- Generates a dependency submission using
`MixDependencySubmission.submission/1`.
- Logs the resulting submission in pretty-printed JSON.
- Sends the submission to GitHub using
`MixDependencySubmission.ApiClient.submit/4`.
- Logs the response or error and exits with code 0 or 1 accordingly.

## Parameters

- `argv`: A list of command-line argument strings.

## Behavior

This function does not return. It will halt or stop the system depending on
the outcome of the submission.

## Examples

iex> MixDependencySubmission.CLI.Submit.run([
...> "--project-path",
...> ".",
...> "--github-repository",
...> "org/repo"
...> ])

"""
@spec run(argv :: [String.t()]) :: no_return()
def run(argv) do
%Optimus.ParseResult{
Expand Down
Loading
Loading