yamlizr is an Azure DevOps Classic Designer-to-YAML pipeline conversion CLI for teams migrating Build and Release definitions in bulk. Run it as a .NET global tool or Linux container to produce Azure Pipelines YAML, with optional GitHub Actions conversion.
Disclaimer: Do not consider any of the YAML generated by this tool to be 'production ready'. Do your own testing/research and post any issues and/or make a PR!
| Component | Use it for | Delivery |
|---|---|---|
| yamlizr CLI | Bulk conversion and output files | NuGet global tool, GHCR image |
| CasCap.Api.AzureDevOps | REST calls, pipeline models and conversion in .NET applications | NuGet library |
| Conversion tests | Offline cases and opt-in live preview validation | Source only |
| Repository scripts | Fixture provisioning and editor MCP launcher | Source only; not required to use the CLI |
Library consumers upgrading to 4.4 must recompile for the breaking model-member renames. The CLI command syntax and generated JSON/YAML wire names are unchanged.
Install the global tool with a .NET 8, 9 or 10 SDK and matching .NET and ASP.NET Core runtimes (included in the corresponding SDK). Read the token permissions, then run this PowerShell 7 example from the directory where you want the output:
dotnet tool install --global yamlizr
$env:CasCap__AzureDevOpsOptions__PAT = Read-Host 'Azure DevOps token' -MaskInput
yamlizr generate `
-org https://dev.azure.com/example-org `
-proj ExampleProject `
-out "$PWD\out" `
--create-directoryThe generated YAML appears in out/ExampleProject/; inspect the warnings and
conversion limits before using it. The CLI reads definitions and writes
local files; it does not update Azure DevOps definitions or queue pipelines. Clear the token
environment variable when finished.
The container is the easiest way to run yamlizr because it needs no .NET installation. A
multi-architecture image supporting linux/amd64, linux/arm64 and linux/arm/v7 is published to
GitHub Container Registry alongside the NuGet package.
docker pull ghcr.io/f2calv/yamlizr| Tag | Meaning |
|---|---|
latest |
The most recent release, moves with every release. |
4.3.4 |
Verified exact release; pin a version for repeatable automation. |
Notes on running the image;
- Staying current.
--pull alwaysre-checks the registry on every run, so a floating tag such aslatestcannot go stale. Omit it once you have pinned an exact version, which never changes. - File ownership. The image runs as a non-root user (uid 1654). On Linux pass
--user "$(id -u):$(id -g)"so the generated files belong to you. This is unnecessary on Docker Desktop for Windows or macOS. - Use
--create-directoryfor unattended runs. yamlizr appends the project name unless the selected output directory already has that name. Creating a missing directory raises an interactive prompt which cannot be answered without a TTY. Either pass--create-directory, or run with-it. - Passing the token. Prefer
--env-file, or a bare-e CasCap__AzureDevOpsOptions__PATwhich forwards the value from your shell. An inline-e NAME=valueis visible indocker inspectand in your shell history. - Environment forwarding avoids putting the token in the command text, but container inspection can still expose environment values. Do not bake credentials into an image.
If you already have the .NET SDK then the global tool is the lighter option, and it writes directly to your filesystem with no volume or user mapping to think about.
-
Download and install one of;
-
From a command line shell install (or update) the tool;
dotnet tool update --global yamlizr -
Generate YAML into a chosen output folder (the token can come from configuration);
yamlizr generate -org https://dev.azure.com/example-org -proj ExampleProject -out "$PWD\out" --create-directory
Every example below works with either form. Where a command reads yamlizr generate ... the
container equivalent is docker run --rm ... ghcr.io/f2calv/yamlizr generate ....
Create a Personal Access Token (PAT) with the following scopes/permissions;
| Scope | Permission |
|---|---|
| Build | Read |
| Project and Team | Read |
| Release | Read |
| Task Groups | Read |
| Variable Groups | Read |
Generation reads definitions; Deployment Groups read/manage is needed by the separate fixture provisioning script, not the normal conversion path. Start with read scopes and project access appropriate to the definitions you convert.
The CLI does not save the supplied token, but generated YAML and diagnostics can contain definition names, variable values and other project content. Protect the output and logs. A pipeline build service access token is also accepted; see Running Inside a Pipeline. Tokens must be valid, unexpired and authorized for the target project. Follow Microsoft's current PAT guidance for organization policies, expiry and revocation; source inspection is not a live authentication check.
For context-sensitive help execute;
yamlizr --help
yamlizr generate --helpThe generate command
defines these options. Required values may instead come from configuration:
-pat|--tokenPersonal Access Token, or a pipeline access token.-org|--organisationAzure DevOps organisation Uri, e.g.https://dev.azure.com/myorg.-proj|--projectAzure DevOps project name.
Optional arguments;
-out|--outputpathpath to the YAML output folder. Pass it explicitly: the implementation defaults to the application base directory, despite the help text claiming the current directory.--filter <some string here>only convert definitions whose name contains this text. It is a case-insensitivecontainsmatch, not a wildcard pattern, so passyamlizr.test.rather thanyamlizr.test.*.--phasetypeselects the release deployment phase type, defaultAgentBasedDeployment. Accepting another enum value does not establish equivalent conversion fidelity.
Optional switches;
--inlinemerge the tasks from task groups into the steps of the calling job instead of creating additional template files.--githubactionsgenerate GitHub Actions workflows via AzurePipelinesToGitHubActionsConverter.--create-directorycreate the output folder without prompting, for unattended runs.--parallelismgenerate pipelines in parallel.
Convert every Build and Release Definition in a project, as a container;
mkdir -p out
docker run --pull always --rm \
--user "$(id -u):$(id -g)" \
-e CasCap__AzureDevOpsOptions__PAT \
-v "$(pwd)/out:/data" \
ghcr.io/f2calv/yamlizr:4.3.4 generate -org https://dev.azure.com/example-org -proj ExampleProject -out /data --create-directoryThis Linux shell example assumes the token environment variable is already exported. Create the bind-mount directory as your user first; Docker-created directories may not be writable by that user.
Convert only the definitions whose name contains wibble, and emit GitHub Actions workflows as well,
as a global tool;
yamlizr generate `
-org https://dev.azure.com/example-org `
-proj ExampleProject `
-out "$PWD\out" `
--create-directory `
--filter wibble `
--githubactionsThe output directory gets the project name appended unless its final segment already matches.
Files are named <sanitized-definition-name>-<id>.yml and existing matching files are overwritten:
<output>/<project>/AzureDevOpsBuilds/*.yml<output>/<project>/AzureDevOpsReleases/*.yml<output>/<project>/AzureDevOpsTaskGroups/*.yml(non-inlined templates, named<group>-v<major>.yml)<output>/<project>/GitHubBuilds/*.yml(with--githubactions)<output>/<project>/GitHubReleases/*.yml(with--githubactions)
Use a fresh output directory when comparing runs: files from earlier runs are not cleaned up.
--githubactions forces task groups to be inlined. The final summary includes conversion warnings
and collected errors, but the command can still return 0 with conversion errors; a zero exit
code is not proof of complete conversion. See Conversion Coverage.
The token, organisation and project can also come from configuration, so an unattended run does not have to put a credential on a process command line. A command line option always wins over a configured value.
AppHost reads sources in this order, each overriding the last:
appsettings.jsonalongside the toolappsettings.jsonin the current working directory- .NET User Secrets
- environment variables
The AzureDevOpsOptions
section supplies connection values. Explicit command options win over configuration; predefined
Azure Pipelines variables are fallbacks only, used when both are empty.
Minimal non-secret appsettings.json for the working directory:
{
"CasCap": {
"AzureDevOpsOptions": {
"PAT": null,
"OrganisationUri": "https://dev.azure.com/example-org",
"Project": "ExampleProject"
}
}
}The equivalent environment variables use the standard double-underscore separator, and are what the container expects;
$env:CasCap__AzureDevOpsOptions__PAT = Read-Host 'Azure DevOps token' -MaskInput
$env:CasCap__AzureDevOpsOptions__OrganisationUri = 'https://dev.azure.com/example-org'
$env:CasCap__AzureDevOpsOptions__Project = 'ExampleProject'
yamlizr generate -out "$PWD\out" --create-directoryNever commit a token. Use User Secrets locally and a secret-backed environment variable in CI.
--token also accepts an access token issued to a pipeline's build service identity, so yamlizr can
run as a step in an Azure Pipeline without a Personal Access Token. Grant the build service the
necessary project permissions. Prefer secret-backed environment mapping over a token in arguments.
When options and configuration are absent, yamlizr falls back to the predefined pipeline variables
SYSTEM_ACCESSTOKEN, SYSTEM_COLLECTIONURI and SYSTEM_TEAMPROJECT. The latter two are exposed to
every step automatically, but SYSTEM_ACCESSTOKEN is secret and reaches a step only when you map it
explicitly;
- script: |
dotnet tool update --global yamlizr
yamlizr generate -out $(Build.ArtifactStagingDirectory) --create-directory
displayName: yamlizr
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)The container needs no .NET on the agent. Nothing from the agent reaches it unless you forward it, so the predefined variables above are not available inside the container and the organisation and project have to be passed as arguments;
- script: |
docker run --pull always --rm \
--user "`id -u`:`id -g`" \
-e CasCap__AzureDevOpsOptions__PAT \
-v "$(Build.ArtifactStagingDirectory):/data" \
ghcr.io/f2calv/yamlizr generate \
-org $(System.CollectionUri) \
-proj $(System.TeamProject) \
-out /data \
--create-directory
displayName: yamlizr
env:
CasCap__AzureDevOpsOptions__PAT: $(System.AccessToken)Two details worth keeping;
-e NAMEwith no value forwards the variable from the step environment, keeping the token out of the command text, not out of container inspection.- The user mapping uses backticks because Azure Pipelines resolves its own
$( )macros before bash sees the script. Without it the generated files belong to uid 1654 and the agent cannot clean them up.
GenerateCommand retrieves classic
definitions plus task and variable catalogues.
YamlPipelineGenerator
produces Pipeline objects, which serialize
through YamlDotNet. Optional GitHub Actions conversion consumes that Azure Pipelines YAML.
flowchart LR
A[(Azure DevOps definitions and catalogues)] --> C[yamlizr generate]
C --> G[Pipeline generator]
G --> Y[(Azure Pipelines YAML and templates)]
G --> W[Warnings for unsupported constructs]
Y --> X{GitHub Actions requested?}
X -->|yes, task groups inlined| H[External converter]
H --> F[(GitHub Actions YAML)]
classDef owned fill:#e8f0fe,stroke:#315da8
class C,G owned
The generator reports unsupported constructs through warnings surfaced by the CLI. Review the output even when no warning is reported: this is not a completeness guarantee. Anything marked ❌ below is absent from the generated YAML and must be re-created by hand. Progress on closing these gaps is tracked in issue #182.
✅ converted
| Classic construct | Notes | |
|---|---|---|
| Agent phases | ✅ | Each phase becomes a job. |
Job dependsOn |
✅ | Mapped from the phase's declared dependencies, including fan-in, so phases meant to run in parallel still do. |
| Job condition, timeout and cancel timeout | ✅ | |
| Agent queue | ✅ | Becomes the pipeline pool. |
| Continuous integration trigger | ✅ | Including branch and path include/exclude filters, and batching. |
| Pull request, scheduled and build completion triggers | ❌ | Reported in the run summary. |
| Server and deployment group phases | ❌ | Only agent phases are converted. |
| A definition with exactly one job | Flattened to a bare step list, dropping a non-default job condition, see issue #376. |
| Classic construct | Notes | |
|---|---|---|
| Environments | ✅ | Each environment becomes a stage named after it. |
Deploy phases matching --phasetype |
✅ | Defaults to AgentBasedDeployment. |
Job dependsOn within a stage |
✅ | Sequential, in rank order. |
| Job condition, timeout and cancel timeout | ✅ | |
Stage dependsOn |
❌ | Classic stage dependencies and conditions are not mapped. With no explicit dependency, Azure Pipelines runs stages sequentially in declaration order, not according to the classic graph. |
| Release artifacts | ❌ | A generated release pipeline therefore has no inputs. |
| Pre- and post-deployment approvals and gates | ❌ | Detected and reported, but not converted, see issue #374. A generated release pipeline deploys straight through where the classic definition had a gate. |
Deploy phases other than --phasetype |
❌ | Reported per stage. |
| A definition with exactly one stage | Flattened, dropping stage-level variables and variable groups, see issue #376. |
| Classic construct | Notes | |
|---|---|---|
| Steps for installed tasks | ✅ | With condition, continueOnError, timeoutInMinutes and env. |
| Task groups | ✅ | Emitted as template files, or merged into the calling job with --inline. |
| Task group parameters | ✅ | Become template parameters. |
| Variables | ✅ | Pipeline, stage and environment scope. |
| Variable groups | ✅ | Emitted as a group reference. |
| GitHub Actions workflows | ✅ | With --githubactions, via AzurePipelinesToGitHubActionsConverter. |
| Steps referencing an extension not installed in the organisation | ❌ | Reported by display name and task id. |
| Steps whose task version cannot be parsed | ❌ | Reported by display name and task id. |
| Combined build plus release multi-stage pipelines | ❌ | Build and release definitions are emitted as separate files. |
Azure Pipelines' default stage ordering is sequential; the CLI's warning describing stages as concurrent should not be taken as the execution contract. Also re-supply secret values and service connections in the destination rather than assuming they were exported or migrated.
The projects target .NET 8, 9 and 10. Use a .NET 10 SDK for the current C# source. Release configuration uses published shared dependencies; Debug requires sibling source projects. See the test guide before choosing offline or live checks, and the script guide before provisioning fixtures.
Released under the MIT License.