Just is a simple command runner. You can get if from https://just.systems. I use it meanwhile regularly, and it improves consistency between my local environment and my CI/CD pipelines. Let me explain this with a trivial walkthrough.
Deploying Infrastructure
A common task is to deploy infrastructure. As I use mostly Azure for infrastructure deployments, I use bicep to do them. For this scenario a simple storage deployment will act as the canonical Hello, World. example. You can find the code for the scripts at my GitHub.
The workflow usually consists of these steps to make the deployment
Lint the file az bicep lint --file main.bicep
Validate the script az deployment sub validate --template-file main.bicep --parameters environments/dev.bicepparam
Make a what-if analysis az deployment sub what-if --template-file main.bicep --parameters environments/dev.bicepparam
Deploy to Infrastructure az deployment sub create --template-file main.bicep --parameters environments/dev.bicepparam
This can now be scripted or in this case I use just so I will write recipes to execute those steps.
Simple Just Anatomy
I don't want to explain things that are already explained in the Just manuals, but it helps to have a quick context without digging into this.
environment :=env("DEPLOYMENT_ENVIRONMENT", "dev")# Lists all available recipieshelp:
@just --list
_hello name="world":
@echo "Hello, {{name}}"# Deploys infrastructure to defined environment
deploy: _hello
@echo "Deploying to {{environment}}"
A lot happens here
First, the variable environment gets either assigned an available environment variable called DEPLOYMENT_ENVIRONMENT or if not available, the value dev. So just has functions that can be used in recipes
Second, the first function will be executed when no parameter is applied to the just command. Therefore, I often use help as my first recipe.
❯ just
Available recipes:
deploy # Deploys infrastructure to defined environmenthelp# Lists all available recipies
Third, just can have private recipes like _hello which also contains a parameter with a default value
Fourth, deploy leverages the private recipe when called and executes it first without providing any parameter. Therefore, the default is taken.
Please note that comments applied before recipes are used as description in the --list parameter of just and private recipes are not listed.
Calling now just deploy outputs
❯ just deploy
Hello, world
Deploying to dev
My Just Approach
In my working environment and especially in the teams I am working with a lot of people still use Windows. PowerShell is set as the de-facto shell across all operating systems.
PowerShell Shebang
To enable just now to execute all commands using PowerShell I always define at the very top of my justfile
This will instruct the execution engine to use PowerShell as the execution shell. There are two variables depending on the environment shell and windows-shell that are used for that.
The shebang is used when a recipe will spawn multiple lines and needs to keep the context. If you don't specify this, the commands will be executed in their own environment without knowing anything about the previous line.
# Test without
without:
Write-Host "Without"$some_var="Without"
Write-Host $some_var
When executing without you see in the output that the variable is set, but it is not available when writing to console anymore, as each line is in its own context
❯ just without
Write-Host "Without"
Without
$some_var="Without"
Write-Host $some_var
Using now the shebang defined above, it works
# Test withwith:#!{{shebang}}Set-PSDebug-Trace1Write-Host"With"$some_var="With"Write-Host$some_varSet-PSDebug-Off
The output does keep the variable because it executes it with the defined shell environment in one context
just with
DEBUG: 32+ >>>> Write-Host "With"
With
DEBUG: 33+ >>>>$some_var="With"
DEBUG: 34+ >>>> Write-Host $some_var
With
DEBUG: 35+ >>>> Set-PSDebug -Off
The First Recipe is Help
I always define this as my very first recipe
# Lists all available recipieshelp:
@just --list
So, when I invoke just every recipe is listed.
❯ just
Available recipes:
deploy # Deploys infrastructure to defined environmenthelp# Lists all available recipies
with # Test with
without # Test without
Use Short Recipe Scripts
Using a shebang allows writing scripts as complex as you want. Usually if I tend to use more than one branching path in my script, I extract them into my own PowerShell modules. I invoke them e.g.
module_path:="path/to/psmd"# Install k3s using Ansiblek3s-install:#!{{ shebang }}Import-Module"./{{module_path}}"-ForceInstall-K3sCluster-Verbose
No Here Strings
I haven't found a way to use here strings with just. test. For instance, if I have a recipe like this
# Initializes the configuration tomlinit:#!{{shebang}}$toml=@"
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
toml = "0.7"
"@$toml|Out-File-FilePath"Cargo.toml"Write-Host"Created Cargo.toml"
It would just complain about the content inside the here string.
So, I put things like this in an external PowerShell function.
Use Just Recipes in CI/CD
I aim to have my recipes agnostic to the environment. That said, I use them as well in the CI/CD environment. One drawback to using just is that the current GitHub runners or Azure DevOps agent don't have just pre-configure on their environments. So before using them, e.g. in Azure DevOps I have a task installing just
targetRegistry:=env("REGISTRY","localhost:5050")buildTag:=env("TAG","latest-dev")# Build all API containerscontainerize-apis:(containerize-facilitymanagementtargetRegistry)(containerize-storymanagementtargetRegistry)(containerize-queuemanagementtargetRegistry)(containerize-videostreamingtargetRegistry)# Build Docker container for Queue Management APIcontainerize-queuemanagementregistry:#!{{ shebang }}dockerbuild`
--tag{{registry}}/api-queuemanagement:{{buildTag}}`--filesrc/Api.QueueManagement/Dockerfile`
.
Back Deploying Infrastructure
I started to discuss how I deploy infrastructure using just. When you have a look at the sample justfile I published, you will see that my approach is applied to this. You could argue that destroy is too many lines of code. Yes, this can be extracted to an external script or module. I usually like to use PowerShell modules, but one function is not enough for me to consider this.
shebang:=ifos()=='windows'{'pwsh.exe'}else{'/usr/bin/env pwsh'}setshell:=["pwsh","-c"]setwindows-shell:=["pwsh.exe","-NoProfile","-Command"]subscription-id:=env("AZURE_SUBSCRIPTION_ID")environment:=env("DEPLOYMENT_ENVIRONMENT","dev")location:=env("AZURE_LOCATION","northeurope")deployment-name:=env("DEPLOYMENT_NAME","just-demo")resourceGroupName:=env("RESOURCE_GROUP_NAME","rg-just-demo")# Lists all available recipieshelp:@just--list# Lint the bicep fileslint:azbiceplint--filemain.bicep# Validates the bicep deploymentvalidate:_ensure-subscription#!{{ shebang }}azdeploymentsubvalidate`
--name{{deployment-name}}`--location{{location}}`--template-filemain.bicep`
--parametersenvironments/{{environment}}.bicepparam`--parametersresourceGroupName={{resourceGroupName}}# Shows what changes would be made by the deploymentwhat-if:_ensure-subscription#!{{ shebang }}azdeploymentsubwhat-if`
--name{{deployment-name}}`--location{{location}}`--template-filemain.bicep`
--parametersenvironments/{{environment}}.bicepparam`--parametersresourceGroupName={{resourceGroupName}}# Validates and shows what changes would be made by the deploymentvalidate-all:lintvalidatewhat-if_ensure-subscription:azaccountset--subscription{{subscription-id}}# Deploys the bicep deploymentdeploy:_ensure-subscription#!{{ shebang }}azdeploymentsubcreate`
--name{{deployment-name}}`--location{{location}}`--template-filemain.bicep`
--parametersenvironments/{{environment}}.bicepparam`--parametersresourceGroupName={{resourceGroupName}}# Deletes the deployment and the resource groupdestroy:_ensure-subscription#!{{ shebang }}Write-Host"Deleting deployment..."azdeploymentsubdelete`
--name{{deployment-name}}`--no-waitWrite-Host"Checking if resource group exists..."$rgExists=azgroupexists--resource-group{{resourceGroupName}}if($rgExists-eq"true"){Write-Host"Resource group '{{resourceGroupName}}' exists. Deleting..."azgroupdelete--resource-group{{resourceGroupName}}--yes--no-waitWrite-Host"Resource group deletion initiated"}else{Write-Host"Resource group '{{resourceGroupName}}' does not exist"}
The only environment variable you have to provide is AZURE_SUBSCRIPTION_ID and you are good to execute the commands. I assume that you have subscription ownership, else you have to tweak the bicep scripts.
❯ just
Available recipes:
deploy # Deploys the bicep deployment
destroy # Deletes the deployment and the resource grouphelp# Lists all available recipies
lint # Lint the bicep files
validate # Validates the bicep deployment
validate-all # Validates and shows what changes would be made by the deployment
what-if # Shows what changes would be made by the deployment
Conclusion
There are many other runners you could use, starting from good old make to a python-based one like tox and many others out there. You can achieve similar things with all of them, I stick to Just. I like it.