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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ and this project adheres to

### Changed

- Show specific workflow names in sandbox merge dialog when target project has
diverged, instead of generic warning message
[#4001](https://github.com/OpenFn/lightning/issues/4001)
- Use distinct `api_provisioning` action type in provisioner to return a
context-appropriate error message for CLI deploy vs GitHub sync
[#4426](https://github.com/OpenFn/lightning/issues/4426)

### Fixed

- Ensure workflows have version history before sandbox creation, merging, and
importing to prevent squashing of the first workflow version
[#3958](https://github.com/OpenFn/lightning/issues/3958)
- Enforce `external_id` uniqueness on credentials per user and per project to
prevent ambiguous keychain resolution
[#4170](https://github.com/OpenFn/lightning/issues/4170)
Expand Down
69 changes: 60 additions & 9 deletions lib/lightning/projects/merge_projects.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule Lightning.Projects.MergeProjects do
alias Lightning.Repo
alias Lightning.Workflows.Workflow
alias Lightning.Workflows.WorkflowVersion
alias Lightning.WorkflowVersions

@doc """
Merges a source project onto a target project using workflow name matching.
Expand Down Expand Up @@ -91,6 +92,9 @@ defmodule Lightning.Projects.MergeProjects do
source_workflow = Repo.preload(source_workflow, [:jobs, :triggers, :edges])
target_workflow = Repo.preload(target_workflow, [:jobs, :triggers, :edges])

# Ensure target workflow has a version before merging
{:ok, _} = WorkflowVersions.ensure_version_recorded(target_workflow)

merge_workflow(
Map.from_struct(source_workflow),
Map.from_struct(target_workflow),
Expand Down Expand Up @@ -822,6 +826,40 @@ defmodule Lightning.Projects.MergeProjects do
"""
@spec has_diverged?(Project.t(), Project.t()) :: boolean()
def has_diverged?(%Project{} = source_project, %Project{} = target_project) do
diverged_workflows(
source_project,
target_project
) != []
end

@doc """
Returns the list of workflow names that have diverged between source and target projects.

A workflow is considered diverged when the target project (parent) has changed since the
sandbox was forked. Specifically, a workflow has diverged if the target's current HEAD
version is not present in the sandbox's version history.

## Parameters
* `source_project` - The sandbox project
* `target_project` - The target project to compare against

## Returns
* List of workflow names (strings) that have diverged
* Empty list if no workflows have diverged

## Examples

iex> MergeProjects.diverged_workflows(sandbox, parent)
["Payment Processing", "Data Sync"]

iex> MergeProjects.diverged_workflows(sandbox, parent)
[]
"""
@spec diverged_workflows(Project.t(), Project.t()) :: [String.t()]
def diverged_workflows(
%Project{} = source_project,
%Project{} = target_project
) do
source_project = Repo.preload(source_project, :workflows)
target_project = Repo.preload(target_project, :workflows)

Expand All @@ -831,12 +869,25 @@ defmodule Lightning.Projects.MergeProjects do
target_workflow_versions =
get_workflow_version_hashes_by_name(target_project.workflows)

Enum.any?(target_workflow_versions, fn {workflow_name, target_hash} ->
case Map.get(sandbox_workflow_versions, workflow_name) do
nil -> false
sandbox_hash -> target_hash != sandbox_hash
Enum.reduce(
target_workflow_versions,
[],
fn {workflow_name, target_hashes}, acc ->
case Map.get(sandbox_workflow_versions, workflow_name) do
sandbox_hashes when is_list(sandbox_hashes) ->
target_head = hd(target_hashes)

if target_head in sandbox_hashes do
acc
else
[workflow_name | acc]
end

_ ->
acc
end
end
end)
)
end

defp get_workflow_version_hashes_by_name(workflows) do
Expand All @@ -845,7 +896,6 @@ defmodule Lightning.Projects.MergeProjects do

from(version in WorkflowVersion,
where: version.workflow_id in ^workflow_ids,
distinct: version.workflow_id,
order_by: [
asc: version.workflow_id,
desc: version.inserted_at,
Expand All @@ -854,8 +904,9 @@ defmodule Lightning.Projects.MergeProjects do
select: {version.workflow_id, version.hash}
)
|> Repo.all()
|> Map.new(fn {workflow_id, hash} ->
{Map.get(workflow_name_map, workflow_id), hash}
end)
|> Enum.group_by(
fn {workflow_id, _hash} -> Map.get(workflow_name_map, workflow_id) end,
fn {_workflow_id, hash} -> hash end
)
end
end
12 changes: 9 additions & 3 deletions lib/lightning/projects/provisioner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ defmodule Lightning.Projects.Provisioner do
|> optimistic_lock(:lock_version)
|> validate_required([:id])
|> maybe_mark_for_deletion()
|> validate_extraneous_params()
|> validate_extraneous_params(ignore: ["version_history"])
|> cast_assoc(:jobs, with: &job_changeset/2)
|> cast_assoc(:triggers, with: &trigger_changeset/2)
|> cast_assoc(:edges, with: &edge_changeset/2)
Expand Down Expand Up @@ -546,8 +546,14 @@ defmodule Lightning.Projects.Provisioner do
For all params in the changeset, ensure that the param is in the list of
known fields in the schema.
"""
def validate_extraneous_params(changeset) do
param_keys = changeset.params |> Map.keys() |> MapSet.new(&to_string/1)
def validate_extraneous_params(changeset, opts \\ []) do
ignore_keys = opts |> Keyword.get(:ignore, []) |> Enum.map(&to_string/1)

param_keys =
changeset.params
|> Map.keys()
|> Enum.reject(fn key -> to_string(key) in ignore_keys end)
|> MapSet.new(&to_string/1)

field_keys = changeset.types |> Map.keys() |> MapSet.new(&to_string/1)

Expand Down
3 changes: 3 additions & 0 deletions lib/lightning/projects/sandboxes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ defmodule Lightning.Projects.Sandboxes do
alias Lightning.Workflows.Trigger
alias Lightning.Workflows.Workflow
alias Lightning.Workflows.WorkflowVersion
alias Lightning.WorkflowVersions

@typedoc """
Attributes for creating a new sandbox via `provision/3`.
Expand Down Expand Up @@ -386,6 +387,8 @@ defmodule Lightning.Projects.Sandboxes do

defp create_sandbox_workflows(parent, sandbox) do
Enum.reduce(parent.workflows, %{}, fn parent_workflow, mapping ->
{:ok, _} = WorkflowVersions.ensure_version_recorded(parent_workflow)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will raise a MatchError if ensure_version_recorded returns {:error, _}. While unlikely in practice, a DB issue or constraint violation would crash the caller. Can we consider using with or propagating the error ? I think same pattern is in lib/lightning/projects/merge_projects.ex:96 too

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch, but error propagation at this point was kinda difficult. The existing pattern even with workflow creation was all matching {:ok, var} = insert(workflow | edge | ...) so I just used the same pattern


{:ok, sandbox_workflow} =
%Workflow{}
|> Workflow.changeset(%{
Expand Down
Loading