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 environmentdeploy: _hello @echo "Deploying to {{environment}}"Anatomy of a justfile
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.
❯ justAvailable recipes: deploy # Deploys infrastructure to defined environment help # Lists all available recipiesThird, 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 deployHello, worldDeploying to devMy 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
shebang := if os() == 'windows' { 'pwsh.exe'} else { '/usr/bin/env pwsh'}set shell := ["pwsh", "-c"]set windows-shell := ["pwsh.exe", "-NoProfile", "-Command"]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 withoutwithout: Write-Host "Without" $some_var = "Without" Write-Host $some_varRecipe without shebang
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 withoutWrite-Host "Without"Without$some_var = "Without"Write-Host $some_varOutput is missing $some_var content
Using now the shebang defined above, it works
# Test withwith: #!{{shebang}} Set-PSDebug -Trace 1 Write-Host "With" $some_var = "With" Write-Host $some_var Set-PSDebug -OffRecipe with shebang and tracing enabled
The output does keep the variable because it executes it with the defined shell environment in one context
just withDEBUG: 32+ >>>> Write-Host "With"
WithDEBUG: 33+ >>>> $some_var = "With"
DEBUG: 34+ >>>> Write-Host $some_var
WithDEBUG: 35+ >>>> Set-PSDebug -OffShebang output with tracing
The First Recipe is Help
I always define this as my very first recipe
# Lists all available recipieshelp: @just --listSo, when I invoke just every recipe is listed.
❯ justAvailable recipes: deploy # Deploys infrastructure to defined environment help # Lists all available recipies with # Test with without # Test withoutOutput of just —list using the help recipe
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}}" -Force Install-K3sCluster -VerboseUsing PowerShell module functions from just recipe
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"Here string in PowerShell - not working with just
It would just complain about the content inside the here string.
❯ just --listerror: Unknown attribute `package` ——▶ justfile:5:2 │5 │ [package] │ ^^^^^^^Just complains about 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
- task: Bash@3 inputs: targetType: "inline" script: | wget -qO - 'https://proget.makedeb.org/debian-feeds/prebuilt-mpr.pub' | gpg --dearmor | sudo tee /usr/share/keyrings/prebuilt-mpr-archive-keyring.gpg 1> /dev/null echo "deb [arch=all,$(dpkg --print-architecture) signed-by=/usr/share/keyrings/prebuilt-mpr-archive-keyring.gpg] https://proget.makedeb.org prebuilt-mpr $(lsb_release -cs)" | sudo tee /etc/apt/sources.list.d/prebuilt-mpr.list sudo apt update sudo apt install -y just displayName: "Install just"Install just on Azure DevOps Microsoft Agent
I also only use either AzureCLI@2 or Bash@3 tasks in my pipelines (on Azure DevOps). To invoke a recipe e.g.
- task: Bash@3 displayName: "Build APIs" inputs: targetType: "inline" script: | just containerize-apis env: REGISTRY: $(registry) TAG: $(buildTag)Build containers and push them to the registry
The recipe (simplified version) looks like this
targetRegistry := env("REGISTRY", "localhost:5050")buildTag := env("TAG", "latest-dev")
# Build all API containerscontainerize-apis: (containerize-facilitymanagement targetRegistry) (containerize-storymanagement targetRegistry) (containerize-queuemanagement targetRegistry) (containerize-videostreaming targetRegistry)
# Build Docker container for Queue Management APIcontainerize-queuemanagement registry: #!{{ shebang }} docker build ` --tag {{registry}}/api-queuemanagement:{{buildTag}} ` --file src/Api.QueueManagement/Dockerfile ` .Simplified version of the justfile showing relevant parts
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 := if os() == 'windows' { 'pwsh.exe'} else { '/usr/bin/env pwsh'}set shell := ["pwsh", "-c"]set windows-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: az bicep lint --file main.bicep
# Validates the bicep deploymentvalidate: _ensure-subscription #!{{ shebang }} az deployment sub validate ` --name {{deployment-name}} ` --location {{location}} ` --template-file main.bicep ` --parameters environments/{{environment}}.bicepparam ` --parameters resourceGroupName={{resourceGroupName}}
# Shows what changes would be made by the deploymentwhat-if: _ensure-subscription #!{{ shebang }} az deployment sub what-if ` --name {{deployment-name}} ` --location {{location}} ` --template-file main.bicep ` --parameters environments/{{environment}}.bicepparam ` --parameters resourceGroupName={{resourceGroupName}}
# Validates and shows what changes would be made by the deploymentvalidate-all: lint validate what-if
_ensure-subscription: az account set --subscription {{subscription-id}}
# Deploys the bicep deploymentdeploy: _ensure-subscription #!{{ shebang }} az deployment sub create ` --name {{deployment-name}} ` --location {{location}} ` --template-file main.bicep ` --parameters environments/{{environment}}.bicepparam ` --parameters resourceGroupName={{resourceGroupName}}
# Deletes the deployment and the resource groupdestroy: _ensure-subscription #!{{ shebang }} Write-Host "Deleting deployment..." az deployment sub delete ` --name {{deployment-name}} ` --no-wait Write-Host "Checking if resource group exists..." $rgExists = az group exists --resource-group {{resourceGroupName}} if ($rgExists -eq "true") { Write-Host "Resource group '{{resourceGroupName}}' exists. Deleting..." az group delete --resource-group {{resourceGroupName}} --yes --no-wait Write-Host "Resource group deletion initiated" } else { Write-Host "Resource group '{{resourceGroupName}}' does not exist" }Sample justfile
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.
❯ justAvailable recipes: deploy # Deploys the bicep deployment destroy # Deletes the deployment and the resource group help # 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 deploymentAvailable recipes in this sample project
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.