View Source Workspace (Workspace v0.1.2)
A Workspace
is a collection of mix projects under the same git repo.
Workspace
provides a set of tools for working with multiple projects under
the same git repo. Using path dependencies between the projects and the
provided tools you can effectively work on massive codebases properly
split into reusable packages.
Structuring a folder as a workspace root
A workspace is a normal Mix.Project
with some tweaks:
- No actual code is expected, so
:elixirc_paths
is set to[]
- It must have a
:workspace
project option configured with a:type
set to:workspace
.
def project do
[
workspace: [
type: :workspace,
# arbitrary options can be set there
],
# rest Mix.Project settings
]
end
A generator is provided for bootstrapping a new workspace project:
$ mix workspace.new NAME
Workspace projects
A mix project is considered a workspace project if:
- it is located in a subfolder of the workspace root path
- it is not included in the ignored projects or ignored paths in the workspace config
Assuming the folder structure:
my_workspace
├── apps
│ ├── api # an API app
│ └── ui # the UI project
├── mix.exs # this is the workspace root definition
├── .workspace.exs # the workspace config
└── packages # various reusable packages under packages
├── package_a
├── package_b
└── package_c
In the above example:
- We have defined a
Workspace
undermy_workspace
folder - All mix projects under
my_workspace
are by default considered workspace packages. In the above example it will include the:api
,:ui
,:package_a
,:package_b
and:package_c
packages.
Ignoring a package or a path
Assume you want to exclude
package_c
from the workspace. You can add it to the:ignore_projects
configuration option in.workspace.exs
:ignore_projects: [:package_a]
If you wanted to ignore all projects under an
other
folder you could set the:ignore_paths
option:ignore_paths: ["other"]
Notice that in the latter case the path is assumed to be relative to the workspace root.
For more details check the
Workspace.Config
documentation.
Duplicate package names
Notice that duplicate package names are not allowed. If upon initialization of a workspace two projects with the same
:name
are detected then an exception will be raised.For example the following workspace:
my_workspace ├── apps │ └── api ├── mix.exs ├── packages │ ├── package_a # package_a project defined under packages │ └── package_b └── shared └── package_a # redefinition of package_a
would fail to initialize since
:package_a
is defined twice.
Loading a workspace
A Workspace
can be constructed by calling the new/2
function. It
will use the given path and config object in order to load and validate
all internal projects.
The workspace graph
The most important concept of the Workspace
is the projects graph. The
project graph is a directed acyclic graph where each vertex is a project
and each edge a dependency between the two projects.
The workspace graph is constructed implicitly upon workspace's creation in order to ensure that all path dependencies are valid and decorate each project with graph metadata.
Inspecting the graph
You can use the
workspace.graph
command in order to see the graph of the givenworkspace
. For example:mix workspace.graph --workspace-path test/fixtures/sample_workspace
on a test fixture would produce:
package_a ├── package_b │ └── package_g ├── package_c │ ├── package_e │ └── package_f │ └── package_g └── package_d package_h └── package_d package_i └── package_j package_k
You can additionally plot is as mermaid graph by specifying the
--format mermaid
flag:flowchart TD package_a package_b package_c package_d package_e package_f package_g package_h package_i package_j package_k package_a --> package_b package_a --> package_c package_a --> package_d package_b --> package_g package_c --> package_e package_c --> package_f package_f --> package_g package_h --> package_d package_i --> package_j
Workspace filtering
As your workspace grows running a CI task on all projects becomes too slow. To address this, code change analysis is supported in order to get the minimum set of projects that need to be executed.
Workspace
supports various filtering modes. Each one should be used in
context with the underlying task. For more details check filter/2
.
Global filtering options
:ignored
- ignores these specific projects:selected
- considers only these projects:only_roots
- considers only the graph roots (sources), e.g. ignores all projects that have a parent in the graph.
Code analysis related options
:modified
- returns only the modified projects, e.g. projects for which the code has changed:affected
- returns all affected projects. Affected projects are the modified ones plus the
:modified
and :affected
can be combined with the global filtering options.
Understanding when and how to filter a workspace
Workspace filtering should be actively used on big workspaces in order to improve the local build and CI times.
Some examples follow:
- If a workspace is used by multiple teams and contains multiple apps, you should select a specific top level app when building the project. This will ignore all other irrelevant apps.
- When changing a specific set of projects, you should use
:modified
for formatting the code since everything else is not affected.- Similarly for testing you should use the
:affected
filtering since a change on a project may affect all parents.- It is advised to have generic CI pipelines on master/main branches that do not apply any filtering.
Visualizing what is affected
You can use the
--show-status
flag in most ofworkspace
commands to indicate what is unchanged, modified or affected.For instance if you have changed
package_f
andpackage_d
you can visualize the graph usingworkspace.graph --format mermaid --show-status
flowchart TD package_a package_b package_c package_d package_e package_f package_g package_h package_i package_a --> package_b package_a --> package_c package_a --> package_d package_b --> package_g package_c --> package_e package_c --> package_f package_f --> package_g package_h --> package_d package_i --> package_b class package_a affected; class package_c affected; class package_d modified; class package_f modified; class package_h affected; classDef affected fill:#FA6,color:#FFF; classDef modified fill:#F33,color:#FFF;
Modified projects are indicated with red colors, and affected projects are highlighted with orange color.
You could now use the proper filtering flags based on what you want to run:
# We want to build only the top level affected projects mix workspace.run -t compile --only-roots --affected # we want to format only the modified ones mix workspace.run -t format --modified # we want to test all the affected ones mix workspace.run -t test --affected
Environment variables
The following environment variables are supported:
WORKSPACE_DEBUG
- if set then debug information will be printed.
Environment variables that are not meant to hold a value (and act basically as
flags) should be set to either 1
or true
, for example:
$ WORKSPACE_DEBUG=true mix workspace.check
Summary
Functions
Creates a new Workspace
from the given workspace path
Similar to new/2
but raises in case of error.
Get the given project from the workspace.
Similar to project/2
but raises in case of error
Returns true
if the given app
is a workspace
project, false
otherwise.
Returns the workspace projects as a list.
Functions
@spec new(path :: binary(), config_or_path :: keyword() | binary()) :: {:ok, Workspace.State.t()} | {:error, binary()}
Creates a new Workspace
from the given workspace path
config_or_path
can be one of the following:
- A path relative to the workspace root path pointing to the workspace config
(
.workspace.exs
in most cases) - A keyword list with the config
The workspace is created by finding all valid mix projects under the workspace root.
Returns {:ok, workspace}
in case of success, or {:error, reason}
if something fails.
@spec new!(path :: binary(), config :: keyword() | binary()) :: Workspace.State.t()
Similar to new/2
but raises in case of error.
@spec project(workspace :: Workspace.State.t(), app :: atom()) :: {:ok, Workspace.Project.t()} | {:error, binary()}
Get the given project from the workspace.
If the project is not a workspace member, an error tuple is returned.
@spec project!(workspace :: Workspace.State.t(), app :: atom()) :: Workspace.Project.t()
Similar to project/2
but raises in case of error
@spec project?(workspace :: Workspace.State.t(), app :: atom()) :: boolean()
Returns true
if the given app
is a workspace
project, false
otherwise.
@spec projects(workspace :: Workspace.State.t()) :: [Workspace.Project.t()]
Returns the workspace projects as a list.