Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions eng/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,53 @@ All the tools/utilities used in Microsoft Azure Java SDK's build config are defi

- `lintingconfigs` - CheckStyle and SpotBugs rule configurations.

## Sparse Checkouts

Java-owned pipeline jobs use the native Azure Pipelines
[`checkout` step](https://learn.microsoft.com/azure/devops/pipelines/yaml-schema/steps-checkout)
for both their initial sparse checkout and dependency-driven expansion. This requires agent version
3.253.0/4.253.0 or later and Git 2.25 or later.

- Use `sparseCheckoutPatterns` for non-cone patterns, including file globs and exclusions.
- Preserve the base patterns `/* !/*/ /eng /.config` and the order of job-specific patterns.
- Write literal patterns one per line in a folded YAML scalar (`>-`); YAML joins them with spaces for the checkout task.
- Use `fetchFilter: tree:0` and `fetchDepth: 0` to retain treeless fetches and full commit history.
- Set `fetchTags: false` explicitly to avoid unnecessary tag synchronization and pipeline-dependent defaults.
- Set `AGENT_SOURCE_CHECKOUT_QUIET: 'true'` in each checkout step's `env` to suppress verbose checkout progress output.
- Set `path` explicitly when checking out Java alongside build-tools; it is relative to `$(Pipeline.Workspace)`.

Initial checkouts that can run test-pipeline versioning use `fetchTags: ${{ parameters.TestPipeline }}` instead.
[SetTestPipelineVersion.ps1](common/scripts/SetTestPipelineVersion.ps1) reads local tags to choose the version, so those
jobs still need tags when `TestPipeline` is enabled. Release creation checks and creates tags through the GitHub API
and does not require local tags. Expansion checkouts do not fetch tags again.

Java builds compute additional paths after generating project lists and updating POM files. Use
[pipelines/templates/steps/sparse-checkout-repo-initialized.yml](pipelines/templates/steps/sparse-checkout-repo-initialized.yml)
with `Paths: $(SparseCheckoutDirectories)` to expand these checkouts. The variable remains a JSON array.
The template prepares a space-separated `SparseCheckoutPatterns` variable containing the original patterns followed
by the additional paths, then passes it to a second native checkout. Both checkouts use `path: s` to retain the
existing source location. Empty path lists and full checkouts do not need a second checkout.

The agent performs a forced checkout even with `clean: false`. Before checkout, the helper saves tracked-file changes
as a binary patch under `$(Agent.TempDirectory)`. After successful checkout it verifies that the source revision has
not changed and restores the patch, including changes to POM files generated during dependency discovery.
The second checkout uses `clean: false` to retain generated untracked files. Restoration failures fail the job and
leave the patch available for diagnosis; do not replace this template with an unprotected second checkout.
Comment thread
vcolin7 marked this conversation as resolved.

Layered jobs keep private mirrored repositories on full checkouts so later expansion does not require persisted credentials.
Configurable external docs repositories still use the shared custom template because their repository names are selected
at runtime. Shared pipeline templates under `common` are maintained upstream in `azure-sdk-tools`.

Run the expansion regression tests from the repository root using PowerShell 7 and Pester 5.7.1:

```powershell
Import-Module Pester -RequiredVersion 5.7.1
Invoke-Pester -Path eng/scripts/tests/Sparse-Checkout.tests.ps1 -Output Detailed
```

Before rolling out to an agent pool, validate PR, FromSource, private-mirror, and multi-repository publishing jobs on
Windows and Linux. Compare source revisions, checked-out files, preserved POM edits, and checkout time and transfer size.

---

For developer guides (building, testing, code quality, versioning), see the consolidated documentation hub:
Expand Down
8 changes: 7 additions & 1 deletion eng/containers/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,13 @@ extends:
- $(containerRegistry).azurecr.io/$(imageRepository):$(stableTag)

steps:
- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
- checkout: self
fetchFilter: tree:0
fetchDepth: 0
fetchTags: false
sparseCheckoutPatterns: /* !/*/ /eng /.config
env:
AGENT_SOURCE_CHECKOUT_QUIET: 'true'

- task: AzureCLI@2
displayName: Login to $(containerRegistry)
Expand Down
18 changes: 9 additions & 9 deletions eng/pipelines/code-quality-reports.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ extends:
# Skip sparse checkout for the `azure-sdk-for-<lang>-pr` private mirrored repositories
# as we require the GitHub service connection to be loaded.
- ${{ if not(contains(variables['Build.DefinitionName'], 'java-pr')) }}:
- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
parameters:
Paths:
- '**/*.xml'
- '!sdk/**/test-recordings'
- '!sdk/**/session-records'
- 'sdk/tools/linting-extensions'
- checkout: self
path: s
fetchFilter: tree:0
fetchDepth: 0
fetchTags: false
sparseCheckoutPatterns: /* !/*/ /eng /.config **/*.xml !sdk/**/test-recordings !sdk/**/session-records sdk/tools/linting-extensions
env:
AGENT_SOURCE_CHECKOUT_QUIET: 'true'

- task: UsePythonVersion@0
displayName: 'Use Python $(PythonVersion)'
Expand Down Expand Up @@ -77,9 +78,8 @@ extends:
arguments: '--set-skip-linting-projects SkipLintingProjects --artifacts-list $(ArtifactsList) --additional-modules-list $(AdditionalModulesList)'
workingDirectory: '$(System.DefaultWorkingDirectory)'

- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
- template: /eng/pipelines/templates/steps/sparse-checkout-repo-initialized.yml
parameters:
SkipCheckoutNone: true
Paths: $(SparseCheckoutDirectories)

- task: PowerShell@2
Expand Down
153 changes: 76 additions & 77 deletions eng/pipelines/scripts/Invoke-Sparse-Checkout.ps1
Original file line number Diff line number Diff line change
@@ -1,104 +1,103 @@
<#
.SYNOPSIS
Invokes sparse checkout on the specified repositories.
Prepares or restores working-tree changes around a native sparse checkout.

.DESCRIPTION
Invokes sparse checkout on the specified repositories.
Combines the initial sparse checkout patterns with paths computed by Java's dependency discovery.
Tracked-file changes are saved outside the repository and restored after the Azure Pipelines checkout
step resets them. The checkout must use clean: false to preserve generated untracked files.

This script is special to Java as it uses layered sparse checkout to reduce the amount of code to checkout.
The first run of sparse checkout is inlined into YAML as there is a chicken and egg problem where the script
to perform sparse checkout won't be available until after the checkout step has completed.
.PARAMETER PathsJson
JSON representation of the additional paths to checkout.

This script is used to reduce the size of YAML files as this is only called when the initial checkout has
already been completed.
.PARAMETER ChangesPath
Absolute path outside the repository for the patch containing tracked-file changes.

.PARAMETER PathsJson
JSON representation of the paths to checkout.
.PARAMETER SourceVersion
The original source revision, which must still be checked out before restoring changes.

.PARAMETER RepositoriesJson
JSON representation of the repositories to checkout from.
.PARAMETER Restore
Restore the saved changes after the native checkout has completed.
#>

[CmdletBinding(DefaultParameterSetName = 'Prepare')]
param(
[Parameter(Mandatory = $true)]
[string]$PathsJson,
[Parameter(Mandatory = $true, ParameterSetName = 'Prepare')]
[string]$PathsJson,

[Parameter(Mandatory = $true)]
[string]$ChangesPath,

[Parameter(Mandatory = $true)]
[string]$RepositoriesJson
[Parameter(Mandatory = $true, ParameterSetName = 'Restore')]
[string]$SourceVersion,

[Parameter(Mandatory = $true, ParameterSetName = 'Restore')]
[switch]$Restore
)

# Setting $PSNativeCommandArgumentPassing to 'Legacy' to use PowerShell
# 7.2 behavior for command argument passing. Newer behaviors will result
# in errors from git.exe.
$PSNativeCommandArgumentPassing = 'Legacy'
$ErrorActionPreference = 'Stop'

function SparseCheckout([Array]$paths, [Hashtable]$repository)
{
$dir = $repository.WorkingDirectory
if (!$dir) {
$dir = "./$($repository.Name)"
if ($Restore) {
$currentVersion = git rev-parse HEAD
if ($LASTEXITCODE -ne 0 -or $currentVersion -ne $SourceVersion) {
throw "The native checkout changed the source revision. Patch retained at $ChangesPath."
}
New-Item $dir -ItemType Directory -Force | Out-Null
Push-Location $dir

if (Test-Path .git/info/sparse-checkout) {
$hasInitialized = $true
Write-Host "Repository $($repository.Name) has already been initialized. Skipping this step."
} else {
Write-Host "Repository $($repository.Name) is being initialized."

if ($repository.Commitish -match '^refs/pull/\d+/merge$') {
Write-Host "git clone --no-checkout --filter=tree:0 -c remote.origin.fetch='+$($repository.Commitish):refs/remotes/origin/$($repository.Commitish)' https://github.com/$($repository.Name) ."
git clone --no-checkout --filter=tree:0 -c remote.origin.fetch=''+$($repository.Commitish):refs/remotes/origin/$($repository.Commitish)'' https://github.com/$($repository.Name) .
} else {
Write-Host "git clone --no-checkout --filter=tree:0 https://github.com/$($repository.Name) ."
git clone --no-checkout --filter=tree:0 https://github.com/$($repository.Name) .
if ((Get-Item -LiteralPath $ChangesPath).Length -gt 0) {
git apply --whitespace=nowarn -- $ChangesPath
if ($LASTEXITCODE -ne 0) {
throw "Restoring checkout changes failed with exit code $LASTEXITCODE. Patch retained at $ChangesPath."
}

# Turn off git GC for sparse checkout. Note: The devops checkout task does this by default
Write-Host "git config gc.auto 0"
git config gc.auto 0

Write-Host "git sparse-checkout init"
git sparse-checkout init

# Set non-cone mode otherwise path filters will not work in git >= 2.37.0
# See https://github.blog/2022-06-27-highlights-from-git-2-37/#tidbits
Write-Host "git sparse-checkout set --no-cone '/*' '!/*/' '/eng'"
git sparse-checkout set --no-cone '/*' '!/*/' '/eng'
}
Remove-Item -LiteralPath $ChangesPath
return
}

# Prevent wildcard expansion in Invoke-Expression (e.g. for checkout path '/*')
$quotedPaths = $paths | ForEach-Object { "'$_'" }
$gitsparsecmd = "git sparse-checkout add $quotedPaths"
Write-Host $gitsparsecmd
Invoke-Expression -Command $gitsparsecmd
Write-Output '##vso[task.setvariable variable=SparseCheckoutRequired]false'

Write-Host "Set sparse checkout paths to:"
Get-Content .git/info/sparse-checkout
# Paths may be sourced as a yaml object literal OR a dynamically generated variable json string.
# If the latter, convertToJson will wrap the 'string' in quotes, so remove them.
$paths = $PathsJson.Trim('"') | ConvertFrom-Json
if (@($paths).Count -eq 0) {
return
}

# sparse-checkout commands after initial checkout will auto-checkout again
if (!$hasInitialized) {
# Remove refs/heads/ prefix from branch names
$commitish = $repository.Commitish -replace '^refs/heads/', ''
$isWorkingTree = git rev-parse --is-inside-work-tree
if ($LASTEXITCODE -ne 0 -or $isWorkingTree -ne 'true') {
throw 'The repository is not an initialized Git working tree.'
}

# use -- to prevent git from interpreting the commitish as a path
Write-Host "git -c advice.detachedHead=false checkout $commitish --"
$isSparseCheckout = git config --type=bool --default=false --get core.sparseCheckout
if ($LASTEXITCODE -ne 0) {
throw 'Unable to determine sparse checkout mode.'
}
if ($isSparseCheckout -ne 'true') {
Write-Information 'The repository has a full checkout. Skipping expansion.' -InformationAction Continue
return
}

# This will use the default branch if repo.Commitish is empty
git -c advice.detachedHead=false checkout $commitish --
} else {
Write-Host "Skipping checkout as repo has already been initialized"
$patternsPath = git rev-parse --git-path info/sparse-checkout
if ($LASTEXITCODE -ne 0) {
throw 'Unable to locate the initial sparse checkout patterns.'
}
$patterns = @(Get-Content -LiteralPath $patternsPath) + @($paths)
$quotedPatterns = foreach ($pattern in $patterns) {
if ($pattern -match "[`r`n]") {
throw 'Sparse checkout patterns cannot contain line breaks.'
}

Pop-Location
'"' + ($pattern -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"'
}

# Paths may be sourced as a yaml object literal OR a dynamically generated variable json string.
# If the latter, convertToJson will wrap the 'string' in quotes, so remove them.
$paths = $PathsJson.Trim('"') | ConvertFrom-Json
# Replace windows backslash paths, as Azure Pipelines default directories are sometimes formatted like 'D:\a\1\s'
$repositories = $RepositoriesJson -replace '\\', '/' | ConvertFrom-Json -AsHashtable
foreach ($repo in $repositories) {
SparseCheckout $paths $repo
New-Item -ItemType Directory -Path (Split-Path -Parent $ChangesPath) -Force | Out-Null
$SourceVersion = git rev-parse HEAD
if ($LASTEXITCODE -ne 0) {
throw 'Unable to determine the original source revision.'
}
git diff --binary --no-ext-diff --no-textconv --src-prefix=a/ --dst-prefix=b/ --output=$ChangesPath $SourceVersion --
if ($LASTEXITCODE -ne 0) {
throw "Saving checkout changes failed with exit code $LASTEXITCODE."
}

$patternsValue = ($quotedPatterns -join ' ').Replace('%', '%AZP25')
Write-Output "##vso[task.setvariable variable=SparseCheckoutPatterns]$patternsValue"
Comment thread
alzimmermsft marked this conversation as resolved.
Write-Output "##vso[task.setvariable variable=SparseCheckoutSourceVersion]$SourceVersion"
Write-Output '##vso[task.setvariable variable=SparseCheckoutRequired]true'
15 changes: 7 additions & 8 deletions eng/pipelines/templates/jobs/build-validate-pom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ jobs:
os: linux

steps:
- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
parameters:
Paths:
- 'sdk/${{ parameters.ServiceDirectory }}'
- '**/*.xml'
- '**/*.md'
- '!sdk/**/test-recordings'
- '!sdk/**/session-records'
- checkout: self
fetchFilter: tree:0
fetchDepth: 0
fetchTags: false
sparseCheckoutPatterns: /* !/*/ /eng /.config sdk/${{ parameters.ServiceDirectory }} **/*.xml **/*.md !sdk/**/test-recordings !sdk/**/session-records
env:
AGENT_SOURCE_CHECKOUT_QUIET: 'true'

- script: |
echo "##vso[build.addbuildtag]Scheduled"
Expand Down
37 changes: 17 additions & 20 deletions eng/pipelines/templates/jobs/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ jobs:
componentgovernance:
enabled: true
codeql:
binaryLanguages: java # Need to specify the language because we clone after the codeql initialize step
binaryLanguages: java
compiled:
enabled: true

Expand All @@ -111,14 +111,14 @@ jobs:
# Skip sparse checkout for the `azure-sdk-for-<lang>-pr` private mirrored repositories
# as we require the GitHub service connection to be loaded.
- ${{ if not(contains(variables['Build.DefinitionName'], 'java-pr')) }}:
- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
parameters:
Paths:
- '**/*.xml'
- '**/*.md'
- '**/ci*.yml' # necessary for save-package-properties.yml
- '!sdk/**/test-recordings'
- '!sdk/**/session-records'
- checkout: self
path: s
fetchFilter: tree:0
fetchDepth: 0
fetchTags: ${{ parameters.TestPipeline }}
sparseCheckoutPatterns: /* !/*/ /eng /.config **/*.xml **/*.md **/ci*.yml !sdk/**/test-recordings !sdk/**/session-records
env:
AGENT_SOURCE_CHECKOUT_QUIET: 'true'

- template: /eng/pipelines/templates/steps/generate-project-list-and-cache-maven-repository.yml
parameters:
Expand Down Expand Up @@ -151,7 +151,6 @@ jobs:
- template: /eng/pipelines/templates/steps/sparse-checkout-repo-initialized.yml
parameters:
Paths: $(SparseCheckoutDirectories)
SkipCheckoutNone: true

- ${{ parameters.PreBuildSteps }}

Expand Down Expand Up @@ -321,15 +320,14 @@ jobs:
# Skip sparse checkout for the `azure-sdk-for-<lang>-pr` private mirrored repositories
# as we require the GitHub service connection to be loaded.
- ${{ if not(contains(variables['Build.DefinitionName'], 'java-pr')) }}:
- template: /eng/common/pipelines/templates/steps/sparse-checkout.yml
parameters:
Paths:
- '**/*.xml'
- '**/*.md'
- '**/*.yml'
- '.vscode/cspell.json'
- '!sdk/**/test-recordings'
- '!sdk/**/session-records'
- checkout: self
path: s
fetchFilter: tree:0
fetchDepth: 0
fetchTags: ${{ parameters.TestPipeline }}
sparseCheckoutPatterns: /* !/*/ /eng /.config **/*.xml **/*.md **/*.yml .vscode/cspell.json !sdk/**/test-recordings !sdk/**/session-records
env:
AGENT_SOURCE_CHECKOUT_QUIET: 'true'

- task: PowerShell@2
displayName: 'Verify versions in POM files'
Expand Down Expand Up @@ -365,7 +363,6 @@ jobs:
- template: /eng/pipelines/templates/steps/sparse-checkout-repo-initialized.yml
parameters:
Paths: $(SparseCheckoutDirectories)
SkipCheckoutNone: true

- task: UseNode@1
inputs:
Expand Down
Loading
Loading