
Geschreven door Funs Janssen
Software Consultant
Ik ben Funs Janssen. Ik ontwikkel software en schrijf over de beslissingen die daarbij komen kijken: architectuur, ontwikkelmethoden, AI-tools en de zakelijke impact van technische keuzes. Deze blog is een verzameling praktische aantekeningen van echte projecten: wat schaalbaar is, wat misgaat en wat in blogvriendelijke voorbeelden vaak over het hoofd wordt gezien.
Since October 15, 2026, a lot of Azure Pipelines runs carry a red retirement error. The build still goes green, so most teams shrug and move on. That is exactly the trap. Microsoft has retired 73 built-in task versions, and on March 15, 2027 every pipeline that still references one of them stops with a blocking error.
The obvious fix is to grep your repos for CopyFiles@1 and friends. That finds the easy half of the problem. Classic pipelines, task groups and templates living in other repositories never show up.
In this post I'll show you how to find every retired task your pipelines actually run, using data Azure DevOps already records for each run. Then I'll sort the fixes into three buckets by effort, walk through a rollout that doesn't break production, and show how I track the work so it reaches done before March.
What changed on October 15, and what breaks on March 15, 2027
Retirement and removal are two separate dates
Microsoft's Azure Pipelines task retirement announcement sets two dates for Azure DevOps Services:
- October 15, 2026: retirement. The 73 task versions are no longer supported and receive no updates, including security updates. Pipelines show a retirement error, but the error does not fail the task or the pipeline.
- March 15, 2027: removal. Azure Pipelines no longer resolves these task versions. Jobs that still reference them fail with a blocking error.
So right now you are running unsupported code that will never get another security patch, and you have five months before it stops running at all. Five months sounds generous until you remember the holiday freeze and that half your pipelines belong to teams who don't read build warnings.
The tasks most teams hit
The list splits into 34 tasks from the public azure-pipelines-tasks repository and 39 legacy built-in tasks. The ones I expect most teams to hit are the everyday build and deploy tasks:
CopyFiles@1, replaced byCopyFiles@2PowerShell@1, replaced byPowerShell@2CmdLine@1, replaced byCmdLine@2DotNetCoreCLI@0and@1, replaced byDotNetCoreCLI@2NuGet@0andNuGetInstaller@0, replaced byNuGetCommand@2(plusNuGetToolInstaller@1)AzureCLI@0, replaced byAzureCLI@3AzurePowerShell@1, replaced byAzurePowerShell@5Maven@1and@2, replaced byMaven@4Xcode@2,@3and@4, replaced byXcode@5
Anything mobile that still touches App Center or Xamarin is on the list too, and those are the painful ones. More on that below.
Azure DevOps Server is not on this schedule yet
The dates apply to Azure DevOps Services only. Microsoft says the schedule for Azure DevOps Server will be communicated separately. If you run on-premises, don't panic, but don't assume you're exempt either. The same task versions will go eventually, and the inventory method below works against Server too.
Why a repo grep is not enough
Classic pipelines, task groups and cross-repo templates
A grep across your cloned YAML finds - task: CopyFiles@1 lines. It does not find:
- Classic build and release pipelines. Their steps live in the pipeline definition, not in a repository.
- Task groups. A task group bundles steps that dozens of classic pipelines reuse. One retired task inside it affects every consumer.
- Templates from other repositories. If your pipelines use
extendsortemplatereferences to a shared templates repo, the retired task lives in a repo you may not have cloned, owned by a platform team you may not be on.
Task references by GUID
YAML lets you reference a task by its GUID instead of its name, and some generated or converted pipelines do exactly that. A grep for CopyFiles@1 sails right past <task-guid>@1. You won't notice until March.
Find every retired task your runs actually use
Here is the trick that makes this tractable: every pipeline run already records which task, at which version, ran in each step. The build Timeline API returns a record per step, and each task record carries a task reference with the task's id, name and version. It doesn't matter whether the step came from inline YAML, a template in another repo, a task group or a classic definition. If it ran, it's in the timeline.
Scan recent run timelines with the REST API
The script below takes the latest run of every pipeline in a project from the last 60 days, reads its timeline, and flags any step whose task name and major version match the retired list. It needs a personal access token with Build (read) scope, scoped to the one organization you're scanning.
1# Find retired Azure Pipelines task versions in recent runs.2# Requires a PAT with Build (read) scope in $env:AZDO_PAT.3$org = "https://dev.azure.com/your-org"4$project = "YourProject"5$days = 606$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:AZDO_PAT)"))7$headers = @{ Authorization = "Basic $auth" }89# Retired on October 15, 2026: task name -> retired major versions10$retired = @{11 AndroidBuild=@(1); AndroidSigning=@(1,2); AppCenterDistribute=@(0,1,2); ArchiveFiles=@(1)12 AutomatedAnalysis=@(0); AzureCLI=@(0); AzureCloudPowerShellDeployment=@(1,2); AzureFunction=@(0)13 AzureMonitor=@(0); AzureNLBManagement=@(1); AzurePowerShell=@(1); AzureResourceGroupDeployment=@(1)14 AzureRmWebAppDeployment=@(2); AzureWebPowerShellDeployment=@(1); CacheBeta=@(0); Chef=@(1)15 ChefKnife=@(1); CmdLine=@(1); CondaEnvironment=@(1); CopyFiles=@(1); CopyPublishBuildArtifacts=@(1)16 cURLUploader=@(1); DeployVisualStudioTestAgent=@(1); DotNetCoreCLI=@(0,1); DotNetCoreInstaller=@(0,1)17 DownloadPackage=@(0); DownloadPipelineArtifact=@(0); DuffleInstaller=@(0); FtpUpload=@(1)18 GitHubRelease=@(0); Gradle=@(1); InstallAppleCertificate=@(0,1); InstallAppleProvisioningProfile=@(0)19 InvokeRESTAPI=@(0); JenkinsQueueJob=@(1); Maven=@(1,2); MysqlDeploymentOnMachineGroup=@(1)20 NuGet=@(0); NuGetInstaller=@(0); NuGetPackager=@(0); NuGetPublisher=@(0); NuGetRestore=@(1)21 PackerBuild=@(0); PowerShell=@(1); PowerShellOnTargetMachines=@(1); PublishSymbols=@(1)22 PublishToAzureServiceBus=@(0); PyPIPublisher=@(0); QuickPerfTest=@(1)23 RunVisualStudioTestsusingTestAgent=@(1); ServiceFabricComposeDeploy=@(0)24 ServiceFabricUpdateAppVersions=@(1); SonarQubePostTest=@(1); SonarQubePreBuild=@(1)25 VSMobileCenterTest=@(0); XamarinAndroid=@(1); XamarinComponentRestore=@(0); XamariniOS=@(1,2)26 XamarinLicense=@(1); XamarinTestCloud=@(1); Xcode=@(2,3,4); XcodePackageiOS=@(0)27}2829$since = (Get-Date).AddDays(-$days).ToString("o")30$builds = (Invoke-RestMethod -Headers $headers -Uri `31 "$org/$project/_apis/build/builds?minTime=$since&queryOrder=finishTimeDescending&api-version=7.1").value3233# Latest run per pipeline34$latest = $builds | Group-Object { $_.definition.id } | ForEach-Object { $_.Group[0] }3536$hits = foreach ($b in $latest) {37 $tl = Invoke-RestMethod -Headers $headers -Uri `38 "$org/$project/_apis/build/builds/$($b.id)/timeline?api-version=7.1"39 foreach ($r in ($tl.records | Where-Object { $_.type -eq "Task" -and $_.task })) {40 $major = [int]($r.task.version -split "\.")[0]41 if ($retired.ContainsKey($r.task.name) -and $retired[$r.task.name] -contains $major) {42 [pscustomobject]@{43 Pipeline = $b.definition.name44 Task = "$($r.task.name)@$major"45 Step = $r.name46 Run = $b._links.web.href47 }48 }49 }50}5152$hits | Sort-Object Pipeline, Task, Step -Unique | Export-Csv retired-tasks.csv -NoTypeInformation53$hits | Group-Object Task | Sort-Object Count -Descending | Format-Table Count, Name
A few things to keep in mind when you run it:
- PowerShell hashtables are case-insensitive, so
cURLUploaderversuscurluploaderdoesn't matter. - The latest run may have skipped a conditional stage, such as a production deploy that only runs on
main. For important pipelines, scan the last few runs per pipeline or pick the latest run on your release branch. - Large projects return the build list in pages. If you have more pipelines than the first page holds, follow the continuation token in the response header.
- Run it per project, or wrap it in a loop over
_apis/projects.
Cover pipelines that haven't run recently
The timeline scan only sees pipelines that ran inside your window. For the rest, check the definitions themselves:
- Classic build pipelines: the build definitions endpoint returns each step with a
task.idandversionSpec, such as1.*. Map the GUID to a name with the organization's task list at_apis/distributedtask/tasks, the same endpoint I used in my Node 16 task inventory. - Classic release pipelines: release definitions live on the
vsrm.dev.azure.comhost. Each environment's deploy phases listworkflowTaskswith ataskIdandversion. - Task groups:
_apis/distributedtask/taskgroupslists the tasks inside each group, again by GUID and version spec. - YAML that hasn't run: use Code Search for both the task name and its GUID across all repos, including your shared templates repo.
A pipeline that hasn't run in 60 days and contains a retired task is a great candidate for deletion rather than migration. Ask the owner before you spend an afternoon fixing it.
Turn the output into a pipeline-by-task inventory
You now have a CSV with one row per pipeline, task and step. Add two columns: owning team and bucket (see the next section). That sheet is your migration backlog. Don't skip the owner column, because "the platform team will fix everyone's pipelines" is how you end up fixing everyone's pipelines in March.
Sort the fixes into three buckets
Not every retired task is equal work. Sorting by effort lets you clear the bulk in a day and focus your attention where it's needed.
Bucket 1: version bumps
These are the tasks where the new major version accepts the same inputs for typical usage. CopyFiles@1 to @2, CmdLine@1 to @2, ArchiveFiles@1 to @2 and DownloadPipelineArtifact@0 to @2 usually fall here.
1# Before2- task: CopyFiles@13 inputs:4 SourceFolder: '$(Build.SourcesDirectory)'5 Contents: '**/*.json'6 TargetFolder: '$(Build.ArtifactStagingDirectory)'78# After9- task: CopyFiles@210 inputs:11 SourceFolder: '$(Build.SourcesDirectory)'12 Contents: '**/*.json'13 TargetFolder: '$(Build.ArtifactStagingDirectory)'
"Usually" is doing work in that sentence. Still run the pipeline once after the bump, because a default may have changed.
Bucket 2: upgrades that change inputs
Here the replacement exists but the inputs don't map one to one. Read the new task's documentation before you edit anything.
NuGet@0,NuGetInstaller@0,NuGetPackager@0,NuGetPublisher@0,NuGetRestore@1all collapse intoNuGetCommand@2, which uses acommandinput (restore,pack,push) to pick the behavior. Feed and authentication settings need re-entering.PowerShell@1to@2changes how you choose between a script file and an inline script, so check the script type inputs.AzureCLI@0to@3andAzurePowerShell@1to@5jump several major versions. Expect different input names for the script type, location and service connection, and test against a non-production subscription.Maven@1and@2to@4, andXcode@2-4to@5: check JDK, Xcode and signing inputs in particular.
For tasks from the public repository you can diff the task.json files between versions on GitHub. The 39 legacy built-ins aren't in that repo, so the docs and a test run are your only sources.
Bucket 3: no direct replacement
These need a decision, not a find-and-replace:
- App Center (
AppCenterDistribute@0-2,VSMobileCenterTest@0). App Center is retired. Microsoft points to the App Store/TestFlight and Google Play extensions for distribution, and to a device-testing service for tests. - Xamarin (
XamarinAndroid@1,XamariniOS@1-2,XamarinComponentRestore@0,XamarinLicense@1,XamarinTestCloud@1). The real fix is migrating the app to SDK-style .NET for Android/iOS or .NET MAUI and building withDotNetCoreCLI@2. That's an app migration, not a pipeline tweak. Plan it as one. - Legacy SonarQube (
SonarQubePreBuild@1,SonarQubePostTest@1). Switch to the supported SonarQube extension from the Marketplace. - Chef, Duffle, MySQL machine-group deploys,
CondaEnvironment@1,PyPIPublisher@0. Replace with a script step that calls the current CLI, or with a supported extension.
If anything in bucket 3 shows up in your inventory, start it this sprint. It's the only part of this migration that can realistically miss March.
Roll out without breaking production
Fix templates and task groups first
One fix in a shared template or task group repairs every pipeline that consumes it. Start there, version the template if your consumers pin a ref, and announce the change before you merge it. Then work through the remaining per-pipeline hits.
Validate on a branch, then a non-production run
For each pipeline:
- Make the change on a branch and run the pipeline from that branch.
- Check the timeline for the step: it should now show the new major version and no retirement error.
- For deployment pipelines, run against a test environment before the change reaches the production stage.
- Merge, and confirm the next run on the default branch is clean.
Rerun the inventory script weekly. The number of hits should only go down. When it creeps up, someone copied an old snippet from a wiki page, and you know which wiki page to fix next.
Bundle it with the Node 16 cleanup
The Node 6, 10 and 16 runners are removed from the agent on November 24, 2026. Different problem, same pipelines, same owners. If you're already touching a pipeline for a retired task, check its Node handlers in the same pull request. My post on finding every Node 16 task before November 24 has the inventory script for that side.
Track the migration so it reaches done before March
One work item per pipeline
Turn every row of your inventory into a work item in Azure Boards, assigned to the team that owns the pipeline, tagged task-retirement-2027, and parented to one Feature. That gives you a burndown leadership can read without asking you for a status update, and it puts the work in the owning team's backlog instead of your inbox.
A checklist that blocks "Done" until it's verified
The weak spot in migrations like this is the item that moves to Done when the YAML is edited, not when the pipeline is proven to work. I built the Checklist Extension for Azure DevOps for exactly that gap. Put a reusable checklist template on each migration work item:
- Retired task replaced in YAML, template or task group
- Inputs reviewed against the new major version
- Branch run green, timeline shows the new version
- Non-production deployment verified (if it deploys)
- Node handler checked for the November 24 change
- Default-branch run green, no retirement error
Then configure the extension to block the transition to Done until every item is checked. Nobody has to police it, and in March you won't discover that "Done" meant "edited, not tested". I've written more about that pattern in making Done mean done in Azure Boards.
Conclusion
The October 15 retirement looks harmless because builds still pass. It isn't. You're running task versions that get no security fixes, and on March 15, 2027 they stop resolving altogether. The fix is mostly mechanical, as long as you find everything:
- Inventory from run timelines, not just a repo grep, so classic pipelines, task groups and cross-repo templates show up.
- Cover the gaps with a definition scan and Code Search for names and GUIDs.
- Sort by effort: clear the version bumps in a day, review input changes carefully, and start App Center and Xamarin work now.
- Fix shared templates first, validate on branches, and rerun the inventory weekly.
- Track it as work items with a checklist that only lets an item close when the pipeline is proven.
If you want that last step without building it yourself, install the Checklist Extension from the Visual Studio Marketplace. It's free, it takes a few minutes to set up a migration template, and it keeps 40 small fixes across eight teams from quietly stalling at 90 percent.
Frequently asked questions
Reacties
Nog geen reacties. Wees de eerste om te reageren.
Plaats een reactie

Geschreven door Funs Janssen
Software Consultant
Ik ben Funs Janssen. Ik ontwikkel software en schrijf over de beslissingen die daarbij komen kijken: architectuur, ontwikkelmethoden, AI-tools en de zakelijke impact van technische keuzes. Deze blog is een verzameling praktische aantekeningen van echte projecten: wat schaalbaar is, wat misgaat en wat in blogvriendelijke voorbeelden vaak over het hoofd wordt gezien.
Inhoud


Enforce your Definition of Done in Azure DevOps by blocking state transitions until checklists are complete, plus a built-in audit trail.

