chore(deps): bump vendored oban_web from 2.12.3 to 2.12.5
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 5m5s
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 5m5s
Fixes two vulnerabilities: - CVE-2026-48592: Missing authorization on save-job event handler - CVE-2026-48593: Unbounded cron range expansion causing DoS
This commit is contained in:
parent
079346a1b9
commit
ed8bb33acb
11 changed files with 186 additions and 49 deletions
|
|
@ -212,6 +212,9 @@ defmodule Oban.Web.Components.Core do
|
|||
defp badge_icon(%{name: "lock_closed"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-lock-closed" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "signal"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-signal" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
defp badge_icon(%{name: "sparkles"} = assigns),
|
||||
do: ~H[<Icons.icon name="icon-sparkles" class="h-4 w-4 shrink-0" />]
|
||||
|
||||
|
|
|
|||
39
vendor/oban_web/lib/oban/web/cron_expr.ex
vendored
39
vendor/oban_web/lib/oban/web/cron_expr.ex
vendored
|
|
@ -45,10 +45,10 @@ defmodule Oban.Web.CronExpr do
|
|||
|
||||
defp describe_parsed(expression) do
|
||||
with [min, hrs, dom, "*", dow] <- String.split(expression, " ", parts: 5),
|
||||
{:ok, parsed_min} <- parse_field(min),
|
||||
{:ok, parsed_hrs} <- parse_field(hrs),
|
||||
{:ok, parsed_dom} <- parse_field(dom),
|
||||
{:ok, parsed_dow} <- parse_field(dow, @days_of_week_translations) do
|
||||
{:ok, parsed_min} <- parse_field(min, 0..59),
|
||||
{:ok, parsed_hrs} <- parse_field(hrs, 0..23),
|
||||
{:ok, parsed_dom} <- parse_field(dom, 1..31),
|
||||
{:ok, parsed_dow} <- parse_field(dow, 0..7, @days_of_week_translations) do
|
||||
combine_description(parsed_min, parsed_hrs, parsed_dom, parsed_dow)
|
||||
else
|
||||
_ -> nil
|
||||
|
|
@ -57,11 +57,11 @@ defmodule Oban.Web.CronExpr do
|
|||
|
||||
# Field Parsing
|
||||
|
||||
defp parse_field(field, translations \\ %{}) do
|
||||
defp parse_field(field, bounds, translations \\ %{}) do
|
||||
parts =
|
||||
field
|
||||
|> String.split(",")
|
||||
|> Enum.map(&parse_part(&1, translations))
|
||||
|> Enum.map(&parse_part(&1, bounds, translations))
|
||||
|
||||
if Enum.any?(parts, &(&1 == :error)) do
|
||||
:error
|
||||
|
|
@ -70,37 +70,40 @@ defmodule Oban.Web.CronExpr do
|
|||
end
|
||||
end
|
||||
|
||||
defp parse_part("*", _translations), do: :wildcard
|
||||
defp parse_part("*", _bounds, _translations), do: :wildcard
|
||||
|
||||
defp parse_part("*/" <> step, _translations) do
|
||||
defp parse_part("*/" <> step, bounds, _translations) do
|
||||
case Integer.parse(step) do
|
||||
{num, ""} when num > 0 -> {:step, num}
|
||||
{num, ""} when num > bounds.first and num <= bounds.last -> {:step, num}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_part(part, translations) do
|
||||
defp parse_part(part, bounds, translations) do
|
||||
if String.contains?(part, "-") do
|
||||
parse_range(part, translations)
|
||||
parse_range(part, bounds, translations)
|
||||
else
|
||||
parse_value(part, translations)
|
||||
parse_value(part, bounds, translations)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_range(part, translations) do
|
||||
defp parse_range(part, bounds, translations) do
|
||||
with [start_str, end_str] <- String.split(part, "-", parts: 2),
|
||||
{:ok, start_val} <- translate_or_parse(start_str, translations),
|
||||
{:ok, end_val} <- translate_or_parse(end_str, translations) do
|
||||
{:ok, end_val} <- translate_or_parse(end_str, translations),
|
||||
true <- start_val in bounds and end_val in bounds and end_val >= start_val do
|
||||
{:range, start_val, end_val}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_value(part, translations) do
|
||||
case translate_or_parse(part, translations) do
|
||||
{:ok, val} -> {:value, val}
|
||||
:error -> :error
|
||||
defp parse_value(part, bounds, translations) do
|
||||
with {:ok, val} <- translate_or_parse(part, translations),
|
||||
true <- val in bounds do
|
||||
{:value, val}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
6
vendor/oban_web/lib/oban/web/helpers.ex
vendored
6
vendor/oban_web/lib/oban/web/helpers.ex
vendored
|
|
@ -28,7 +28,7 @@ defmodule Oban.Web.Helpers do
|
|||
|
||||
def oban_path(route, params) when is_list(route) do
|
||||
route
|
||||
|> Enum.join("/")
|
||||
|> Enum.map_join("/", &encode_segment/1)
|
||||
|> oban_path(params)
|
||||
end
|
||||
|
||||
|
|
@ -50,6 +50,10 @@ defmodule Oban.Web.Helpers do
|
|||
end
|
||||
end
|
||||
|
||||
defp encode_segment(segment) do
|
||||
segment |> to_string() |> URI.encode(&URI.char_unreserved?/1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Toggle filterable params into a patch path.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@ defmodule Oban.Web.Crons.Helpers do
|
|||
@doc """
|
||||
Converts a NaiveDateTime to unix milliseconds, returning empty string for nil.
|
||||
"""
|
||||
def maybe_to_unix(nil), do: ""
|
||||
|
||||
def maybe_to_unix(timestamp) do
|
||||
def maybe_to_unix(timestamp) when is_struct(timestamp) do
|
||||
timestamp
|
||||
|> DateTime.from_naive!("Etc/UTC")
|
||||
|> DateTime.to_unix(:millisecond)
|
||||
end
|
||||
|
||||
def maybe_to_unix(_timestamp), do: ""
|
||||
|
||||
def show_name?(%{dynamic?: true, name: name, worker: worker}), do: name != worker
|
||||
def show_name?(_cron), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
<Core.status_badge :if={@job.meta["chunk"]} icon="user_group" label="Chunk" />
|
||||
<Core.status_badge :if={@job.meta["chain"]} icon="link" label="Chain" />
|
||||
<Core.status_badge :if={@job.meta["recorded"]} icon="camera" label="Recorded" />
|
||||
<Core.status_badge :if={signal_status(@job) != :none} icon="signal" label="Signal" />
|
||||
<Core.status_badge :if={@job.meta["encrypted"]} icon="lock_closed" label="Encrypted" />
|
||||
<Core.status_badge :if={@job.meta["structured"]} icon="table_cells" label="Structured" />
|
||||
<Core.status_badge :if={@job.meta["decorated"]} icon="sparkles" label="Decorated" />
|
||||
|
|
@ -502,7 +503,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
</button>
|
||||
|
||||
<div id="edit-content" class={["mt-3", if(executing?(@job), do: "hidden")]}>
|
||||
<fieldset disabled={executing?(@job)}>
|
||||
<fieldset disabled={executing?(@job) or not can?(:update_jobs, @access)}>
|
||||
<form
|
||||
id="job-edit-form"
|
||||
class="grid grid-cols-4 gap-4 bg-gray-50 dark:bg-gray-800 rounded-md p-4"
|
||||
|
|
@ -570,7 +571,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={not @edit_changed?}
|
||||
disabled={not @edit_changed? or not can?(:update_jobs, @access)}
|
||||
class="px-6 py-2 bg-blue-500 text-white text-sm font-medium rounded-md hover:bg-blue-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Save Changes
|
||||
|
|
@ -701,6 +702,33 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-all">{format_recorded(@job, @resolver)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div :if={signal_status(@job) != :none} class="mt-4">
|
||||
<div class="relative bg-gray-50 dark:bg-gray-800 rounded-md p-4">
|
||||
<div class="flex justify-between items-start mb-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<h4 class="font-medium text-xs uppercase text-gray-500 dark:text-gray-400">
|
||||
{signal_heading(@job)}
|
||||
</h4>
|
||||
<.pro_badge id="signal-pro-badge" tooltip="Awaitable signal from Oban.Pro.Worker" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="copy-signal"
|
||||
class={[
|
||||
"w-9 h-9 -mr-2 -mt-2 flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 hover:bg-white dark:hover:bg-gray-700 cursor-pointer",
|
||||
signal_status(@job) != :received && "invisible"
|
||||
]}
|
||||
data-title="Copy to clipboard"
|
||||
phx-hook="Tippy"
|
||||
phx-click={copy_to_clipboard(format_signal(@job, @resolver))}
|
||||
>
|
||||
<Icons.icon name="icon-clipboard" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<pre class="font-mono text-sm text-gray-600 dark:text-gray-400 whitespace-pre-wrap break-all">{format_signal(@job, @resolver)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
|
@ -785,14 +813,16 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
def handle_event("save-job", params, socket) do
|
||||
job = socket.assigns.job
|
||||
|
||||
changes =
|
||||
params
|
||||
|> parse_edit_params(job)
|
||||
|> Enum.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|> Map.new()
|
||||
if can?(:update_jobs, socket.assigns.access) do
|
||||
changes =
|
||||
params
|
||||
|> parse_edit_params(job)
|
||||
|> Enum.reject(fn {_key, val} -> is_nil(val) end)
|
||||
|> Map.new()
|
||||
|
||||
if map_size(changes) > 0 do
|
||||
send(self(), {:update_job, job, changes})
|
||||
if map_size(changes) > 0 do
|
||||
send(self(), {:update_job, job, changes})
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, assign(socket, edit_changed?: false)}
|
||||
|
|
@ -805,12 +835,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
end
|
||||
|
||||
defp format_meta(%{meta: meta} = job, resolver) do
|
||||
job =
|
||||
if meta["recorded"] do
|
||||
%{job | meta: Map.delete(meta, "return")}
|
||||
else
|
||||
job
|
||||
end
|
||||
job = %{job | meta: Map.drop(meta, ["return", "signal"])}
|
||||
|
||||
Resolver.call_with_fallback(resolver, :format_job_meta, [job])
|
||||
end
|
||||
|
|
@ -828,6 +853,41 @@ defmodule Oban.Web.Jobs.DetailComponent do
|
|||
end
|
||||
end
|
||||
|
||||
defp format_signal(%{meta: %{"signal" => value}} = job, resolver) do
|
||||
Resolver.call_with_fallback(resolver, :format_signal, [value, job])
|
||||
end
|
||||
|
||||
defp format_signal(%{meta: %{"wait_until" => wait_until}}, _resolver) do
|
||||
case wait_until_to_datetime(wait_until) do
|
||||
{:ok, datetime} -> "Deadline #{Timing.datetime_to_words(datetime)}"
|
||||
:infinity -> "No deadline"
|
||||
:error -> ""
|
||||
end
|
||||
end
|
||||
|
||||
defp signal_status(%{meta: %{"signal" => _}}), do: :received
|
||||
defp signal_status(%{meta: %{"wait_until" => _}}), do: :awaiting
|
||||
defp signal_status(_), do: :none
|
||||
|
||||
defp signal_heading(job) do
|
||||
case signal_status(job) do
|
||||
:received -> "Received Signal"
|
||||
:awaiting -> "Awaiting Signal"
|
||||
:none -> "Signal"
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_until_to_datetime("infinity"), do: :infinity
|
||||
|
||||
defp wait_until_to_datetime(ms) when is_integer(ms) do
|
||||
case DateTime.from_unix(ms, :millisecond) do
|
||||
{:ok, datetime} -> {:ok, datetime |> DateTime.to_naive() |> NaiveDateTime.truncate(:second)}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_until_to_datetime(_), do: :error
|
||||
|
||||
defp error_entry(assigns) do
|
||||
error =
|
||||
assigns.errors
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ defmodule Oban.Web.Jobs.NewComponent do
|
|||
|
||||
defp parse_scheduled_at(str) when is_binary(str) do
|
||||
case DateTime.from_iso8601(str <> ":00Z") do
|
||||
{:ok, datetime, _offset} -> datetime
|
||||
{:ok, datetime} -> datetime
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ defmodule Oban.Web.WorkflowsPage do
|
|||
)
|
||||
|
||||
true ->
|
||||
workflows = WorkflowQuery.all_workflows(params, conf)
|
||||
workflows = WorkflowQuery.all_workflows(conf, params)
|
||||
limit = params.limit
|
||||
|
||||
assign(socket,
|
||||
|
|
|
|||
|
|
@ -195,7 +195,11 @@ defmodule Oban.Web.CronQuery do
|
|||
state: j.state,
|
||||
scheduled_at: j.scheduled_at,
|
||||
attempted_at: j.attempted_at,
|
||||
finished_at: fragment("COALESCE(?, ?, ?)", j.completed_at, j.cancelled_at, j.discarded_at)
|
||||
finished_at:
|
||||
type(
|
||||
fragment("COALESCE(?, ?, ?)", j.completed_at, j.cancelled_at, j.discarded_at),
|
||||
:utc_datetime_usec
|
||||
)
|
||||
})
|
||||
|
||||
conf
|
||||
|
|
@ -280,7 +284,10 @@ defmodule Oban.Web.CronQuery do
|
|||
attempted_at: o.attempted_at,
|
||||
scheduled_at: o.scheduled_at,
|
||||
finished_at:
|
||||
fragment("COALESCE(?, ?, ?)", o.completed_at, o.cancelled_at, o.discarded_at)
|
||||
type(
|
||||
fragment("COALESCE(?, ?, ?)", o.completed_at, o.cancelled_at, o.discarded_at),
|
||||
:utc_datetime_usec
|
||||
)
|
||||
},
|
||||
select_merge: %{
|
||||
rn: over(row_number(), partition_by: o.meta["cron_name"], order_by: [desc: o.id])
|
||||
|
|
|
|||
|
|
@ -126,30 +126,32 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
end
|
||||
end
|
||||
|
||||
defmacrop sub_workflow_parent_dep(sub_workflow_id, sup_workflow_id) do
|
||||
defmacrop sub_workflow_parent_dep(prefix, sub_workflow_id, sup_workflow_id) do
|
||||
quote do
|
||||
fragment(
|
||||
"""
|
||||
(SELECT dep->>1
|
||||
FROM oban_jobs j2,
|
||||
FROM ?.oban_jobs j2,
|
||||
LATERAL jsonb_array_elements(j2.meta->'deps') AS dep
|
||||
WHERE j2.meta->>'workflow_id' = ?
|
||||
AND jsonb_typeof(dep) = 'array'
|
||||
AND dep->>0 = ?
|
||||
LIMIT 1)
|
||||
""",
|
||||
identifier(^unquote(prefix)),
|
||||
unquote(sub_workflow_id),
|
||||
unquote(sup_workflow_id)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmacrop sub_workflow_states(parent_id) do
|
||||
defmacrop sub_workflow_states(prefix, parent_id) do
|
||||
quote do
|
||||
fragment(
|
||||
"""
|
||||
(SELECT COALESCE(array_agg(state), '{}') FROM oban_workflows WHERE parent_id = ?)
|
||||
(SELECT COALESCE(array_agg(state), '{}') FROM ?.oban_workflows WHERE parent_id = ?)
|
||||
""",
|
||||
identifier(^unquote(prefix)),
|
||||
unquote(parent_id)
|
||||
)
|
||||
end
|
||||
|
|
@ -279,18 +281,19 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
|
||||
# Querying
|
||||
|
||||
def all_workflows(params, conf) do
|
||||
def all_workflows(conf, params) do
|
||||
limit = Map.get(params, :limit, 10)
|
||||
sort_by = Map.get(params, :sort_by, "inserted")
|
||||
sort_dir = Map.get(params, :sort_dir, "desc")
|
||||
dir = String.to_existing_atom(sort_dir)
|
||||
prefix = conf.prefix
|
||||
|
||||
Workflow
|
||||
|> where([wf], is_nil(wf.parent_id))
|
||||
|> apply_filters(params)
|
||||
|> apply_sort(sort_by, dir)
|
||||
|> limit(^limit)
|
||||
|> select([wf], {wf, sub_workflow_states(wf.id)})
|
||||
|> select([wf], {wf, sub_workflow_states(prefix, wf.id)})
|
||||
|> then(&Repo.all(conf, &1))
|
||||
|> Enum.map(&build_workflow/1)
|
||||
end
|
||||
|
|
@ -446,6 +449,8 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
end
|
||||
|
||||
defp workflow_graph_subs(conf, workflow_id) do
|
||||
prefix = conf.prefix
|
||||
|
||||
base_query =
|
||||
Job
|
||||
|> where([j], has_sup_workflow_id(j.meta, ^workflow_id))
|
||||
|
|
@ -464,7 +469,7 @@ defmodule Oban.Web.WorkflowQuery do
|
|||
workflow_name: s.workflow_name,
|
||||
sub_name: s.sub_name,
|
||||
state: s.state,
|
||||
parent_dep: sub_workflow_parent_dep(s.workflow_id, ^workflow_id)
|
||||
parent_dep: sub_workflow_parent_dep(prefix, s.workflow_id, ^workflow_id)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
55
vendor/oban_web/lib/oban/web/resolver.ex
vendored
55
vendor/oban_web/lib/oban/web/resolver.ex
vendored
|
|
@ -41,6 +41,13 @@ defmodule Oban.Web.Resolver do
|
|||
|> inspect(charlists: :as_lists, pretty: true)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal()
|
||||
|> inspect(charlists: :as_lists, pretty: true)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def jobs_query_limit(_state), do: 100_000
|
||||
|
||||
|
|
@ -138,6 +145,7 @@ defmodule Oban.Web.Resolver do
|
|||
| :scale_queues
|
||||
| :stop_queues
|
||||
| :update_crons
|
||||
| :update_jobs
|
||||
|
||||
@type qualifier :: :args | :meta | :nodes | :queues | :tags | :workers
|
||||
|
||||
|
|
@ -234,6 +242,35 @@ defmodule Oban.Web.Resolver do
|
|||
"""
|
||||
@callback format_recorded(recorded :: term(), job :: Job.t()) :: iodata()
|
||||
|
||||
@doc """
|
||||
Customize the formatting of awaitable signal payloads wherever they are displayed.
|
||||
|
||||
This callback is similar to `c:format_recorded/2`, but it accepts both the signal binary and
|
||||
the job to help augment the output.
|
||||
|
||||
Note that you **must decode the signal binary** prior to inspecting it.
|
||||
|
||||
## Examples
|
||||
|
||||
Disable pretty printing and change the output width to 98 characters:
|
||||
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal()
|
||||
|> inspect(pretty: false, width: 98)
|
||||
end
|
||||
|
||||
Decode the signal value without the `:safe` flag set, to allow decoding terms with unknown
|
||||
atoms:
|
||||
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> Oban.Web.Resolver.decode_signal([])
|
||||
|> inspect(pretty: false, width: 98)
|
||||
end
|
||||
"""
|
||||
@callback format_signal(signal :: term(), job :: Job.t()) :: iodata()
|
||||
|
||||
@doc """
|
||||
Extract the current user from a `Plug.Conn` when the dashboard mounts.
|
||||
|
||||
|
|
@ -283,6 +320,7 @@ defmodule Oban.Web.Resolver do
|
|||
* `:scale_queues`
|
||||
* `:stop_queues`
|
||||
* `:update_crons`
|
||||
* `:update_jobs`
|
||||
|
||||
Actions which aren't listed are considered disabled.
|
||||
|
||||
|
|
@ -411,6 +449,7 @@ defmodule Oban.Web.Resolver do
|
|||
@optional_callbacks format_job_args: 1,
|
||||
format_job_meta: 1,
|
||||
format_recorded: 2,
|
||||
format_signal: 2,
|
||||
bulk_action_limit: 1,
|
||||
hint_query_limit: 1,
|
||||
jobs_query_limit: 1,
|
||||
|
|
@ -451,6 +490,15 @@ defmodule Oban.Web.Resolver do
|
|||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Decode an awaitable signal payload from a compressed, base64 binary into proper terms.
|
||||
|
||||
Signals share the same wire format as recorded output, so this is a thin alias around
|
||||
`decode_recorded/2` provided for documentation symmetry.
|
||||
"""
|
||||
@spec decode_signal(binary(), [:safe]) :: term()
|
||||
def decode_signal(bin, opts \\ [:safe]), do: decode_recorded(bin, opts)
|
||||
|
||||
@doc false
|
||||
def call_with_fallback(resolver, fun, args) when is_atom(fun) and is_list(args) do
|
||||
resolver =
|
||||
|
|
@ -482,6 +530,13 @@ defmodule Oban.Web.Resolver do
|
|||
|> inspect(@inspect_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_signal(signal, _job) do
|
||||
signal
|
||||
|> decode_signal()
|
||||
|> inspect(@inspect_opts)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def resolve_user(_conn), do: nil
|
||||
|
||||
|
|
|
|||
2
vendor/oban_web/mix.exs
vendored
2
vendor/oban_web/mix.exs
vendored
|
|
@ -2,7 +2,7 @@ defmodule Oban.Web.MixProject do
|
|||
use Mix.Project
|
||||
|
||||
@source_url "https://github.com/oban-bg/oban_web"
|
||||
@version "2.12.3"
|
||||
@version "2.12.5"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue