Archive each completed release’s metadata and binaries into customer-controlled Azure Blob Storage, then apply a locked immutable-storage policy for at least 2,557 days; Azure DevOps explicitly recommends external storage when required retention exceeds its configurable limits.^1^
Recommended archive design
For every successful production release, run an exporter that creates one immutable archive record containing:
- Full release JSON, including definition, stages/environments, approvals, deployment status, timestamps, and linked artifacts.
- For each Azure Pipelines build artifact:
- Build ID and build number.
- Repository and source branch.
-
sourceVersion, which is the commit checked out for that build. - Full build JSON.
- Downloaded artifact files.
- SHA-256 checksums for every exported JSON and artifact file.
- An archive manifest connecting release ID → artifact type → build ID → commit.
- Export timestamp, exporter version, and success/failure status.
The Release API can list releases using date filters, pagination through continuationToken, and expansion options.^2^ Retrieve each release’s full details rather than treating the list response as the complete compliance record.^3^
For an Azure Pipelines build artifact, obtain its build ID from the release artifact and call the Builds API. The build response includes id, buildNumber, repository, sourceBranch, and sourceVersion.^4^ Classic-release variables also expose the artifact BuildId and BuildNumber.^5^
Validate the artifact type before interpreting its version ID as a build ID. GitHub, Jenkins, NuGet, Git, and other release artifact sources need provider-specific export logic.
Artifact download branches
Do not use one download mechanism for every artifact type:
- Pipeline Artifacts: use
DownloadPipelineArtifact@2, specifying the associated run/build ID.^6^ - Classic Build Artifacts: use
DownloadBuildArtifacts@1with the specific build ID, or the corresponding Build Artifacts REST interface.^7^ - Azure Artifacts packages: export or preserve packages separately; pipeline-run retention does not govern NuGet, npm, Universal Packages, and similar packages.^1^
- External artifacts: archive from the originating provider and record its immutable identifier and checksum.
A metadata extraction core could resemble this; production code must add paging, retries, full release-detail retrieval, artifact-type handling, downloads, checksums, and Blob upload:
$releaseUri = "https://vsrm.dev.azure.com/$org/$project/_apis/release/releases?api-version=7.1"
$releases = Invoke-RestMethod -Uri $releaseUri -Headers $headers
foreach ($release in $releases.value) {
$release | ConvertTo-Json -Depth 100 |
Set-Content "$out/release-$($release.id).json"
foreach ($artifact in $release.artifacts) {
if ($artifact.type -in @("Build", "Azure Pipelines")) {
$buildId = $artifact.definitionReference.version.id
$buildUri = "https://dev.azure.com/$org/$project/_apis/build/builds/$buildId?api-version=7.1"
$build = Invoke-RestMethod -Uri $buildUri -Headers $headers
$build | ConvertTo-Json -Depth 100 |
Set-Content "$out/build-$buildId.json"
}
}
}
sourceVersion identifies the build’s checked-out commit; it is not necessarily the complete list of commits included since the previous production release. If compliance requires every intervening commit, additionally export the Git commit range between the previous archived production commit and the current sourceVersion, including merge commits.
Seven-year immutable storage
Configure Azure Blob Storage immutable storage with a time-based retention policy of at least 2,557 days from each blob’s creation, covering seven years including leap days. After testing uploads and recovery, lock the policy. A locked policy prevents modification or deletion during its effective interval and cannot be shortened or removed.^8^
Use a structure such as:
releases/{project}/{release-id}/
release.json
builds/{build-id}/build.json
commits/commit-range.json
artifacts/{artifact-name}/...
manifest.json
checksums.sha256
For unattended production automation, prefer a managed identity for an Azure-hosted exporter or a service principal for other hosting environments, rather than a long-lived PAT.^9^
Prevent deletion while export is pending
Until an archive has passed validation:
- Open the release or associated pipeline run.
- Use More actions and mark it Retain indefinitely. An indefinitely retained item is excluded from normal retention processing.^1^
- For YAML automation, create a retention lease through the REST API. Retention leases are the supported way to retain runs beyond project settings.^10^
- Remove that temporary hold only after verifying:
- Release and build JSON are present.
- Every expected artifact was downloaded.
- Checksums match.
- The manifest contains the release/build/commit relationships.
- Blob immutability is active for the required period.
- A test restore succeeds.
This avoids relying on audit logs as the system of record while preserving Azure DevOps data until the independent archive is complete.
References
- Retention policies
- Releases - List
- Release Management
- Builds - Get
- Use variables in Classic release pipelines
- DownloadPipelineArtifact@2 - Download Pipeline Artifacts v2 task
- DownloadBuildArtifacts@1 - Download build artifacts v1 task
- Store business-critical blob data with immutable storage in a write once, read many (WORM) state
- Use service principals and managed identities in Azure DevOps
- Pipeline Run Retention