yamlizr converts Azure DevOps Classic Designer Build/Release Definitions, and any Task Groups they reference, en-masse into their YAML Pipeline or GitHub Actions equivalent. It runs as a container, or as a .NET Global Tool.
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!
If you find this tool of use then please give it a thumbs-up by giving this repository a ⭐ ... 😉
You need Docker and an Azure DevOps Personal Access Token, nothing else.
docker run --pull always --rm \
--user "$(id -u):$(id -g)" \
-e CasCap__AzureDevOpsOptions__PAT="<your token here>" \
-v "$(pwd)/out:/data" \
ghcr.io/f2calv/yamlizr generate \
-org https://dev.azure.com/myorg \
-proj MyProject \
-out /data \
--create-directoryThe same thing in PowerShell. Docker Desktop maps file ownership for you, so no --user is needed;
docker run --pull always --rm `
-e CasCap__AzureDevOpsOptions__PAT="<your token here>" `
-v "${PWD}/out:/data" `
ghcr.io/f2calv/yamlizr generate `
-org https://dev.azure.com/myorg `
-proj MyProject `
-out /data `
--create-directoryThe generated YAML appears in ./out/MyProject/, see Output Layout.
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.0.0 |
An exact release. Published image versions are immutable and are never overwritten, so pin this in 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. --create-directoryis effectively required. yamlizr always writes into a sub-folder named after the project, and creating that folder 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. - Never bake a token into an image, every file in an image is public.
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 the
c:/temp/myoutputfolderoutput folder;yamlizr generate -pat <your token here> -org https://dev.azure.com/myorg -proj MyProject -out c:/temp/myoutputfolder
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 |
| Deployment Groups | Read & Manage |
| Release | Read |
| Task Groups | Read |
| Variable Groups | Read |
The token is never persisted by yamlizr. It can also be an access token issued to a pipeline's build service identity, see Running Inside a Pipeline.
For context-sensitive help execute;
yamlizr --help
yamlizr generate --helpRequired values, each of which 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, defaults to 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.*.--phasetype <phase type here>filter deployment jobs by Deploy Phase Type the default isAgentBasedDeploymentDeployPhaseTypes(tested), other (un-tested) options areRunOnServer,MachineGroupBasedDeployment&DeploymentGates.
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;
docker run --pull always --rm \
--user "$(id -u):$(id -g)" \
--env-file ./yamlizr.env \
-v "$(pwd)/out:/data" \
ghcr.io/f2calv/yamlizr generate -org https://dev.azure.com/myorg -proj MyProject -out /data --create-directoryConvert only the definitions whose name contains wibble, and emit GitHub Actions workflows as well,
as a global tool;
yamlizr generate `
-pat <your token here> `
-org https://dev.azure.com/myorg `
-proj MyProject `
-out c:/temp/myoutputfolder `
--filter wibble `
--githubactionsAll generated YAML is written into sub-folders of a project folder, so a run with
-out c:/temp/myoutputfolder creates;
c:/temp/myoutputfolder/<your AzDO project>/AzureDevOpsBuilds/*.ymlc:/temp/myoutputfolder/<your AzDO project>/AzureDevOpsReleases/*.ymlc:/temp/myoutputfolder/<your AzDO project>/AzureDevOpsTaskGroups/*.ymlc:/temp/myoutputfolder/<your AzDO project>/GitHubBuilds/*.yml(with--githubactions)c:/temp/myoutputfolder/<your AzDO project>/GitHubReleases/*.yml(with--githubactions)
Each run finishes with a summary of anything it could not convert, 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.
Sources are read in the following order, each overriding the last;
appsettings.jsonalongside the toolappsettings.jsonin the current working directory- .NET User Secrets
- environment variables
- the predefined Azure Pipelines variables, see Running Inside a Pipeline
{
"CasCap": {
"AzureDevOpsOptions": {
"PAT": null,
"OrganisationUri": "https://dev.azure.com/myorg",
"Project": "MyProject"
}
}
}The equivalent environment variables use the standard double-underscore separator, and are what the container expects;
$env:CasCap__AzureDevOpsOptions__PAT = '<your token here>'
$env:CasCap__AzureDevOpsOptions__OrganisationUri = 'https://dev.azure.com/myorg'
$env:CasCap__AzureDevOpsOptions__Project = 'MyProject'
yamlizr generate -out c:/temp/myoutputfolderNever 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 same
read scopes listed above and pass $(System.AccessToken);
- script: |
dotnet tool update --global yamlizr
yamlizr generate --token $(System.AccessToken) -org $(System.CollectionUri) -proj $(System.TeamProject) -out $(Build.ArtifactStagingDirectory) --create-directory
displayName: yamlizrAlternatively supply nothing and let yamlizr fall 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 line and out ofdocker inspect.- 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.
This tool was created when there was no means of exporting a designer/classic build/release definition to YAML. As of November 2020 there is a new Export to YAML feature which allows you to export a Build pipeline to YAML with a single click. This official function covers more edge cases than this CLI for Build pipelines. Where this CLI still has benefits is that it also converts Release definitions to YAML, which the official tool does not. It also allows the conversion of every single Build/Release definition en-masse, so much less clicking! Then you can then cut/copy/paste/manipulate the generated YAML steps as required to fit into a build and/or deployment pipeline unique to your own requirements.
The tool itself uses the Azure DevOps .NET Client Libraries to pre-cache relevant data from your Azure DevOps organisation/account. This data includes build/release definitions, task groups, tasks/extensions data and variable groups. This Azure DevOps data is converted into Azure DevOps Pipeline objects (stages/jobs/steps/variables) which are then persisted to YAML using the YamlDotNet library.
This is not a delicate tool to create perfectly constructed YAML pipelines. Instead consider it to be a blunt instrument which will spawn as many YAML files as possible and from this YAML you can pick/choose and copy/paste the relevant stages/jobs/steps/variables into your own preferred YAML CI/CD deployment architecture.
With --githubactions the same Azure DevOps Pipeline objects are additionally passed into the AzurePipelinesToGitHubActionsConverter library (by @samsmithnz) and exported as GitHub Actions compliant workflows.
Every run finishes with a summary naming each construct it could not convert and the definition it came from, so nothing is dropped without a trace. 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 |
❌ | Generated stages run concurrently rather than in the classic environment order. |
| 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. |
| Project | Purpose |
|---|---|
CasCap.Api.AzureDevOps |
Library: Azure DevOps REST access, pipeline models, and the YAML generator |
CasCap.DevOpsYamlizrCli |
The yamlizr global tool: command surface, console presentation, orchestration |
CasCap.Api.AzureDevOps.Tests |
xUnit v3 tests running on Microsoft.Testing.Platform |
- Azure DevOps .NET Client Libraries
- AzurePipelinesToGitHubActionsConverter
- YamlDotNet
- CommandLineUtils
- ShellProgressBar
Please post any issues or feedback to GitHub issues.
This project is released under The Unlicense. See the LICENSE file for details.