|
| 1 | +parameters: |
| 2 | +# Maximum run time of the monitor job in minutes. Also used for --max-wait-minutes. |
| 3 | +- name: timeoutInMinutes |
| 4 | + type: number |
| 5 | + default: 360 |
| 6 | + |
| 7 | +# Owner segment of the source repository (e.g. 'dotnet' for 'dotnet/runtime') passed via --organization. |
| 8 | +# Defaults to the owner segment of BUILD_REPOSITORY_NAME when empty. |
| 9 | +- name: organization |
| 10 | + type: string |
| 11 | + default: '' |
| 12 | + |
| 13 | +# Name of the source repository (e.g. 'runtime' for 'dotnet/runtime') passed via --repository. |
| 14 | +# Defaults to the repo segment of BUILD_REPOSITORY_NAME when empty. |
| 15 | +- name: repository |
| 16 | + type: string |
| 17 | + default: '' |
| 18 | + |
| 19 | +# Optional dependency list for the generated job. |
| 20 | +- name: dependsOn |
| 21 | + type: object |
| 22 | + default: [] |
| 23 | + |
| 24 | +# Optional condition for the generated job. |
| 25 | +- name: condition |
| 26 | + type: string |
| 27 | + default: '' |
| 28 | + |
| 29 | +# Whether failures in the monitor job should allow the pipeline to continue. |
| 30 | +- name: continueOnError |
| 31 | + type: boolean |
| 32 | + default: false |
| 33 | + |
| 34 | +# NuGet package id of the Helix job monitor tool. |
| 35 | +- name: toolPackageId |
| 36 | + type: string |
| 37 | + default: Microsoft.DotNet.Helix.JobMonitor |
| 38 | + |
| 39 | +# Console command exposed by the installed tool package. |
| 40 | +- name: toolCommand |
| 41 | + type: string |
| 42 | + default: dotnet-helix-job-monitor |
| 43 | + |
| 44 | +# Optional explicit tool version. Only honored when 'toolNupkgArtifactName' is set; in the |
| 45 | +# default code path the version is taken from the consuming repo's .config/dotnet-tools.json. |
| 46 | +- name: toolVersion |
| 47 | + type: string |
| 48 | + default: '' |
| 49 | + |
| 50 | +# Base URI for the Helix service (--helix-base-uri). |
| 51 | +- name: helixBaseUri |
| 52 | + type: string |
| 53 | + default: https://helix.dot.net/ |
| 54 | + |
| 55 | +# Helix API access token forwarded to the tool via the HELIX_ACCESSTOKEN environment variable. |
| 56 | +- name: helixAccessToken |
| 57 | + type: string |
| 58 | + default: '' |
| 59 | + |
| 60 | +# Polling interval in seconds (--polling-interval-seconds). |
| 61 | +- name: pollingIntervalSeconds |
| 62 | + type: number |
| 63 | + default: 30 |
| 64 | + |
| 65 | +# Maximum number of work items whose results may be downloaded, parsed, and |
| 66 | +# uploaded concurrently. |
| 67 | +- name: testResultUploadParallelism |
| 68 | + type: number |
| 69 | + default: 48 |
| 70 | + |
| 71 | +# When 'true' (the default), Helix work items that exit 0 but have failed AzDO test results |
| 72 | +# are treated as failed: they count toward the monitor's exit code and are resubmitted by a |
| 73 | +# later invocation's retry pass. Set to 'false' to fall back to exit-code-only outcomes. |
| 74 | +# Forwarded as --fail-on-failed-tests. |
| 75 | +- name: failWorkItemsWithFailedTests |
| 76 | + type: boolean |
| 77 | + default: true |
| 78 | + |
| 79 | +# When true, allow the monitor to succeed when this stage produces no Helix jobs in any attempt. |
| 80 | +# Forwarded as --allow-no-helix-jobs. |
| 81 | +- name: allowNoHelixJobs |
| 82 | + type: boolean |
| 83 | + default: false |
| 84 | + |
| 85 | +# When true, test results are reported to Azure DevOps using the fully qualified test name |
| 86 | +# (Namespace.Type.Method) as the stable automatedTestName and the visible title is qualified as |
| 87 | +# well (--use-fully-qualified-test-name). Opt-in because it changes AzDO test identity and display; |
| 88 | +# primarily useful for frameworks like MSTest whose display name is only the method name. |
| 89 | +- name: useFullyQualifiedTestName |
| 90 | + type: boolean |
| 91 | + default: false |
| 92 | + |
| 93 | +# Controls per-test output attachments. Defaults to Failed. |
| 94 | +- name: testResultAttachmentMode |
| 95 | + type: string |
| 96 | + default: Failed |
| 97 | + values: |
| 98 | + - Failed |
| 99 | + - All |
| 100 | + - None |
| 101 | + |
| 102 | +# Advanced: optional pipeline artifact (produced earlier in this run) that contains the tool |
| 103 | +# nupkg. When set, the artifact is downloaded and the tool is installed from the nupkg into |
| 104 | +# a local tool-path; this bypasses the repo's .config/dotnet-tools.json manifest and is |
| 105 | +# primarily intended for the Arcade repository itself, where the Helix job monitor tool is |
| 106 | +# built in the same pipeline that runs this template. |
| 107 | +# |
| 108 | +# When this parameter is empty (the default), the consuming repository must declare the tool |
| 109 | +# in its .config/dotnet-tools.json manifest (alongside other local .NET tools); the template |
| 110 | +# will check out the repo and run 'dotnet tool restore' to install the version pinned there. |
| 111 | +- name: toolNupkgArtifactName |
| 112 | + type: string |
| 113 | + default: '' |
| 114 | + |
| 115 | +# Advanced: sub-path within the downloaded artifact where the tool nupkg is located. Defaults |
| 116 | +# to the standard Arcade non-shipping packages location for a Release build (relative to the |
| 117 | +# pipeline artifact root, which is itself the build's 'artifacts' directory). |
| 118 | +- name: toolNupkgArtifactSubPath |
| 119 | + type: string |
| 120 | + default: 'packages/Release/NonShipping' |
| 121 | + |
| 122 | +jobs: |
| 123 | +- job: HelixJobMonitor |
| 124 | + displayName: Monitor Helix Jobs |
| 125 | + timeoutInMinutes: ${{ parameters.timeoutInMinutes }} |
| 126 | + continueOnError: ${{ parameters.continueOnError }} |
| 127 | + ${{ if ne(length(parameters.dependsOn), 0) }}: |
| 128 | + dependsOn: ${{ parameters.dependsOn }} |
| 129 | + ${{ if ne(parameters.condition, '') }}: |
| 130 | + condition: ${{ parameters.condition }} |
| 131 | + pool: |
| 132 | + ${{ if eq(variables['System.TeamProject'], 'public') }}: |
| 133 | + name: $(DncEngPublicBuildPool) |
| 134 | + os: linux |
| 135 | + demands: ImageOverride -equals build.azurelinux.3.amd64.open |
| 136 | + ${{ else }}: |
| 137 | + name: $(DncEngInternalBuildPool) |
| 138 | + os: linux |
| 139 | + demands: ImageOverride -equals build.azurelinux.3.amd64 |
| 140 | + steps: |
| 141 | + - checkout: self |
| 142 | + fetchDepth: 1 |
| 143 | + |
| 144 | + - ${{ if ne(parameters.toolNupkgArtifactName, '') }}: |
| 145 | + - task: DownloadPipelineArtifact@2 |
| 146 | + displayName: Download Helix Job Monitor artifact |
| 147 | + inputs: |
| 148 | + buildType: current |
| 149 | + artifactName: ${{ parameters.toolNupkgArtifactName }} |
| 150 | + itemPattern: '${{ parameters.toolNupkgArtifactSubPath }}/${{ parameters.toolPackageId }}.*.nupkg' |
| 151 | + targetPath: $(Agent.TempDirectory)/helix-job-monitor-nupkg |
| 152 | + |
| 153 | + - bash: | |
| 154 | + set -euo pipefail |
| 155 | +
|
| 156 | + toolPath="$AGENT_TEMPDIRECTORY/helix-job-monitor-tool" |
| 157 | + mkdir -p "$toolPath" |
| 158 | +
|
| 159 | + packageId='${{ parameters.toolPackageId }}' |
| 160 | + toolVersion='${{ parameters.toolVersion }}' |
| 161 | + nupkgArtifactSubPath='${{ parameters.toolNupkgArtifactSubPath }}' |
| 162 | + nupkgDir="$AGENT_TEMPDIRECTORY/helix-job-monitor-nupkg/$nupkgArtifactSubPath" |
| 163 | +
|
| 164 | + if [ ! -d "$nupkgDir" ]; then |
| 165 | + echo "Expected nupkg directory '$nupkgDir' was not produced by the artifact download." >&2 |
| 166 | + exit 1 |
| 167 | + fi |
| 168 | +
|
| 169 | + nupkg=$(find "$nupkgDir" -maxdepth 1 -type f -name "$packageId.*.nupkg" | head -n 1) |
| 170 | + if [ -z "$nupkg" ]; then |
| 171 | + echo "No '$packageId.*.nupkg' found in '$nupkgDir'." >&2 |
| 172 | + exit 1 |
| 173 | + fi |
| 174 | +
|
| 175 | + # Derive the version from the nupkg filename so the local package is selected |
| 176 | + # deterministically instead of resolving against any other configured feed. |
| 177 | + nupkgBase=$(basename "$nupkg" .nupkg) |
| 178 | + derivedVersion="${nupkgBase#${packageId}.}" |
| 179 | + if [ -z "$toolVersion" ]; then |
| 180 | + toolVersion="$derivedVersion" |
| 181 | + fi |
| 182 | +
|
| 183 | + echo "Using locally built '$packageId' version '$toolVersion' from '$nupkgDir'." |
| 184 | +
|
| 185 | + # Create a minimal NuGet.config that only references the local nupkg directory. |
| 186 | + # This avoids conflicts with the repo's package source mapping which blocks --add-source. |
| 187 | + toolNugetConfig="$AGENT_TEMPDIRECTORY/helix-job-monitor-nuget.config" |
| 188 | + printf '<?xml version="1.0" encoding="utf-8"?>\n<configuration>\n <packageSources>\n <clear />\n <add key="local-tool" value="%s" />\n </packageSources>\n</configuration>\n' "$nupkgDir" > "$toolNugetConfig" |
| 189 | +
|
| 190 | + pushd "$(Build.SourcesDirectory)" > /dev/null |
| 191 | + ./eng/common/dotnet.sh tool install \ |
| 192 | + --tool-path "$toolPath" "$packageId" \ |
| 193 | + --version "$toolVersion" \ |
| 194 | + --configfile "$toolNugetConfig" |
| 195 | +
|
| 196 | + # Locate the tool DLL so the run step can invoke it via ./eng/common/dotnet.sh exec. |
| 197 | + toolDll=$(find "$toolPath/.store" -path '*/tools/*/any/*.deps.json' -type f | head -n 1) |
| 198 | + toolDll="${toolDll%.deps.json}.dll" |
| 199 | + if [ ! -f "$toolDll" ]; then |
| 200 | + echo "Could not find tool DLL in '$toolPath/.store'." >&2 |
| 201 | + exit 1 |
| 202 | + fi |
| 203 | +
|
| 204 | + echo "Tool DLL: $toolDll" |
| 205 | + echo "##vso[task.setvariable variable=HelixJobMonitorDll]$toolDll" |
| 206 | + displayName: Install Helix Job Monitor |
| 207 | +
|
| 208 | + - ${{ else }}: |
| 209 | + - bash: ./eng/common/dotnet.sh tool restore |
| 210 | + displayName: Restore Helix Job Monitor |
| 211 | + |
| 212 | + - bash: | |
| 213 | + set -euo pipefail |
| 214 | +
|
| 215 | + toolArgs=( |
| 216 | + --helix-base-uri '${{ parameters.helixBaseUri }}' |
| 217 | + --polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}' |
| 218 | + --fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}' |
| 219 | + --allow-no-helix-jobs '${{ parameters.allowNoHelixJobs }}' |
| 220 | + --use-fully-qualified-test-name '${{ parameters.useFullyQualifiedTestName }}' |
| 221 | + --max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully. |
| 222 | + --stage-name '$(System.StageName)' |
| 223 | + --stage-attempt '$(System.StageAttempt)' |
| 224 | + --job-attempt '$(System.JobAttempt)' |
| 225 | + --test-result-upload-parallelism '${{ parameters.testResultUploadParallelism }}' |
| 226 | + ) |
| 227 | +
|
| 228 | + organization='${{ parameters.organization }}' |
| 229 | + repository='${{ parameters.repository }}' |
| 230 | + testResultAttachmentMode='${{ parameters.testResultAttachmentMode }}' |
| 231 | +
|
| 232 | + # Fall back to Azure DevOps-provided environment variables when the caller did not |
| 233 | + # supply organization / repository explicitly. BUILD_REPOSITORY_NAME is typically |
| 234 | + # 'owner/repo' for GitHub-backed builds and 'owner-repo' for internal builds. |
| 235 | + if [ -z "$organization" ] || [ -z "$repository" ]; then |
| 236 | + buildRepoName="${BUILD_REPOSITORY_NAME:-}" |
| 237 | + if [ -n "$buildRepoName" ] && [[ "$buildRepoName" == */* ]]; then |
| 238 | + repoOwner="${buildRepoName%%/*}" |
| 239 | + repoName="${buildRepoName#*/}" |
| 240 | + elif [ -n "$buildRepoName" ] && [[ "$buildRepoName" == *-* ]]; then |
| 241 | + repoOwner="${buildRepoName%%-*}" |
| 242 | + repoName="${buildRepoName#*-}" |
| 243 | + fi |
| 244 | +
|
| 245 | + if [ -n "${repoOwner:-}" ] && [ -n "${repoName:-}" ]; then |
| 246 | + if [ -z "$organization" ]; then organization="$repoOwner"; fi |
| 247 | + if [ -z "$repository" ]; then repository="$repoName"; fi |
| 248 | + fi |
| 249 | + fi |
| 250 | +
|
| 251 | + if [ -n "$organization" ]; then toolArgs+=( --organization "$organization" ); fi |
| 252 | + if [ -n "$repository" ]; then toolArgs+=( --repository "$repository" ); fi |
| 253 | + if [ -n "$testResultAttachmentMode" ]; then |
| 254 | + toolArgs+=( --test-result-attachment-mode "$testResultAttachmentMode" ) |
| 255 | + fi |
| 256 | +
|
| 257 | + # Build.Reason and Build.SourceBranch are required to derive the Helix source filter |
| 258 | + # the same way the Helix SDK submitter does (PR -> 'pr', internal -> 'official', |
| 259 | + # otherwise -> 'ci'). Without these, manually-queued / scheduled / CI builds would |
| 260 | + # be looked up under the wrong source prefix and find zero jobs. |
| 261 | + toolArgs+=( --build-reason "$(Build.Reason)" ) |
| 262 | + toolArgs+=( --source-branch "$(Build.SourceBranch)" ) |
| 263 | +
|
| 264 | + if [ -n '${{ parameters.toolNupkgArtifactName }}' ]; then |
| 265 | + # Tool was installed from a local nupkg; run the DLL via the repo-local dotnet. |
| 266 | + export DOTNET_ROOT="$(Build.SourcesDirectory)/.dotnet" |
| 267 | + ./eng/common/dotnet.sh exec "$(HelixJobMonitorDll)" "${toolArgs[@]}" |
| 268 | + else |
| 269 | + # Tool was restored from the local .config/dotnet-tools.json manifest; invoke it |
| 270 | + # through the manifest from the repo root. |
| 271 | + pushd "$BUILD_SOURCESDIRECTORY" > /dev/null |
| 272 | + trap 'popd > /dev/null' EXIT |
| 273 | + ./eng/common/dotnet.sh tool run '${{ parameters.toolCommand }}' -- "${toolArgs[@]}" |
| 274 | + fi |
| 275 | + displayName: Monitor Helix Jobs |
| 276 | + env: |
| 277 | + SYSTEM_ACCESSTOKEN: $(System.AccessToken) |
| 278 | + HELIX_ACCESSTOKEN: ${{ parameters.helixAccessToken }} |
0 commit comments