summaryrefslogtreecommitdiff
path: root/Tools/CloudTest.Helix.targets
blob: 8e449829e23cd014636b11eab03576740a4c4eb3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <PropertyGroup>
    <!-- For the separate package scenario (not BuildTools) we need to pick up the right version of the assembly.  -->
    <!-- This will only work on versions of MSBuild that supply MSBuildRuntimeType (defaults to core in this case. -->
    <BuildToolsTaskDir Condition="'$(BuildToolsTaskDir)'=='' And '$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)</BuildToolsTaskDir>
    <BuildToolsTaskDir Condition="'$(BuildToolsTaskDir)'=='' And '$(MSBuildRuntimeType)' == 'Full'">$(MSBuildThisFileDirectory)\desktop\</BuildToolsTaskDir>
  </PropertyGroup>

  <UsingTask TaskName="CreateAzureContainer"             AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="RemoveItemMetadata"               AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="SendJobsToHelix"                  AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="UploadToAzure"                    AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="ValidateWorkItems"                AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="WriteItemsToJson"                 AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="WriteTestBuildStatsJson"          AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="ZipFileCreateFromDirectory"       AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>
  <UsingTask TaskName="ZipFileCreateFromDependencyLists" AssemblyFile="$(BuildToolsTaskDir)Microsoft.DotNet.Build.CloudTestTasks.dll"/>

  <!-- See Documentation\Samples\CloudTest.Helix.targets.sampleproject for a sample project.

       Some helpful Helix environment variables: (Use appropriate-for-OS means to access, i.e. %WINDOWS% or $Linux, $OSX )
       **********************************************************************************************************************************
        HELIX_WORKITEM_PAYLOAD      - Execution folder of helix workitem, current directory when using relative path.
        HELIX_CORRELATION_ID        - GUID identifier for a helix run.
        HELIX_PYTHONPATH            - Path to python executable
        HELIX_CORRELATION_PAYLOAD   - Correlation payload folder;  root of where all correlation payloads are unzipped.
        HELIX_WORKITEM_FRIENDLYNAME - Friendly name of work item
       **********************************************************************************************************************************

       Variables that CloudTest.Helix.Targets cares about:
       **********************************************************************************************************************************
       Required Properties:
       **********************************************************************************************************************************
       HelixJobType                 - Job Type formatting string, used for sorting and display of jobs.
       HelixSource                  - Job Source formatting string, used for sorting and display of jobs.
       TargetQueues                 - Queue(s) to send Jobs to. (TODO: Need discoverability for users)
       BuildMoniker                 - Identifying name for build when sending to Helix.  Used for labelling runs.
       CloudDropConnectionString    - Azure Storage account connection string used for payloads.
       CloudResultsConnectionString - Azure Storage account connection string used for results.
       ArchivesRoot                 - Relative path for work item payloads to use.  Blob Uris will be made relative to this.
       
       **********************************************************************************************************************************
       Optional Properties:
       **********************************************************************************************************************************
       TestListFilename  - File name to use for test list sent to Helix.                      Default: TestList.json
       OverwriteOnUpload - Whether to overwrite blobs if they already exist on upload.        Default: False
       ContainerName     - Container name for uploaded files.                                 Default: build-<Random GUID>. 
       IsOfficial        - Boolean flag for display on Mission Control.                       Default: False:
       HelixApiEndpoint  - Endpoint to send jobs to.                                          Default: https://helix.dot.net/api/2016-06-28/jobs
       CloudResultsReadTokenValidDays  - Days that result URLs produced will be readble       Default: 30
       CloudResultsWriteTokenValidDays - Days that result URLs produced will be writeable     Default: 4
       SupplementalPayloadDir - Temp (deletable) dir for assembling supplemental payloads     Default: Random folder in %TEMP%
       HelixApiAccessKey            - GitHub API Access Key for sending jobs to Helix.  Get from https://helix.dot.net/UserProfile/Index/
       HelixLogFolder    - Folder for storing JSON files describing job start and other info  Default: <Blank>
       HelixCorrelationInfoFileName - JSON file containing info on started jobs               Default: Helix-correlation-infos.json
       HelixJobProperties- Must be JSON.  String describing Helix MC-specific metadata.       Default: <Blank>
       HelixArchLabel    - If HelixJobProperties is not set, we'll use this to fill it out    Default: <Blank>
       HelixConfigLabel  - If HelixJobProperties is not set, we'll use this to fill it out    Default: <Blank>
       MaxRetryCount     - Max automatic retry of workitems which do not return 0             Default: 0 (no retry)
       HelixAttempt      - A lexically monotonic increasing string distinguishing             Default: <Blank>
                           jobs whose results should override previous executions.

       **********************************************************************************************************************************
       Re-queuing Properties:
       **********************************************************************************************************************************
       UseContinuationRunner  - Use runner script that is capable of sending to another queue when finished  Default: False
       SecondaryQueue         - When submitting multi-stage tests, queue for secondary jobs                  Default: <Blank>
       SecondarySasValidHours - SAS Valid time for token provided for multi-stage tests.                     Default: 15.0
       SecondaryPayloadDir    - Folder to make into workitem payload for continued execution                 Default: <Blank>

       **********************************************************************************************************************************
       Required ITaskItems:
       **********************************************************************************************************************************
       HelixWorkItem         - Zip files to be uploaded as payloads. 
       
       **********************************************************************************************************************************
       Optional ITaskItems:
       **********************************************************************************************************************************
       HelixCorrelationPayloadFile - Zip files to be uploaded as correlation payloads (shared by all work items, cached where possible)       
    -->

  <!-- Main entry point -->
  <Target Name="HelixCloudBuild" DependsOnTargets="VerifyInputs;PreCloudBuild;ValidateWorkItems;UploadContent;CreateTestListJson" />

  <PropertyGroup>
    <OverwriteOnUpload Condition="'$(OverwriteOnUpload)' == ''">false</OverwriteOnUpload>
    <ContainerName Condition="'$(ContainerName)'== ''">build-$([System.Guid]::NewGuid().ToString("N"))</ContainerName>
    <ContainerName>$(ContainerName.ToLower())</ContainerName>
    <TestListFilename Condition="'$(TestListFilename)'==''">TestList.json</TestListFilename>
    <SupplementalPayloadDir Condition="'$(SupplementalPayloadDir)' == ''">$([System.IO.Path]::GetTempPath())$([System.IO.Path]::GetRandomFileName())/SupplementalPayload/</SupplementalPayloadDir>
    <SupplementalPayloadFilename>SupplementalPayload.zip</SupplementalPayloadFilename>
    <SupplementalPayloadFile>$(ArchivesRoot)$(SupplementalPayloadFilename)</SupplementalPayloadFile>
    <IsOfficial Condition="'$(IsOfficial)'!=''">false</IsOfficial>
    <HelixApiEndpoint Condition="'$(HelixApiEndpoint)'==''">https://helix.dot.net/api/2016-06-28/jobs</HelixApiEndpoint>
    <MaxRetryCount Condition="'$(MaxRetryCount)' == ''">0</MaxRetryCount>
  </PropertyGroup>

  <!-- Set Helix environment vars based on target platform -->
  <!-- This is only used in the case where property 'UseScriptRunner' is true.-->
  <PropertyGroup Condition="'$(TargetsWindows)' == 'true' AND '$(UseScriptRunner)' == 'true' ">
    <HelixPythonPath>%HELIX_PYTHONPATH%</HelixPythonPath>
    <HelixScriptRoot>%HELIX_SCRIPT_ROOT%\</HelixScriptRoot>
    <RunnerScript>%HELIX_CORRELATION_PAYLOAD%\RunnerScripts\scriptrunner\scriptrunner.py</RunnerScript>
  </PropertyGroup>
  <PropertyGroup Condition="'$(TargetsWindows)' == 'true' AND '$(UseContinuationRunner)' == 'true' ">
    <RunnerScript>%HELIX_CORRELATION_PAYLOAD%\RunnerScripts\scriptrunner\continuationrunner.py --next_queue $(SecondaryQueue) --next_payload_dir $(SecondaryPayloadDir)</RunnerScript>
    <SecondarySasValidHours Condition="'$(SecondarySasValidHours)' == ''">15.0</SecondarySasValidHours>
  </PropertyGroup>
  <PropertyGroup Condition="'$(TargetsWindows)' != 'true' AND '$(UseScriptRunner)' == 'true' ">
    <HelixPythonPath>$HELIX_PYTHONPATH</HelixPythonPath>
    <HelixScriptRoot>$HELIX_SCRIPT_ROOT/</HelixScriptRoot>
    <RunnerScript>$HELIX_CORRELATION_PAYLOAD/RunnerScripts/scriptrunner/scriptrunner.py</RunnerScript>
  </PropertyGroup>

  <Target Name="VerifyInputs">
    <!-- Verify all required properties have been specified.  Update the comment above if you update this list! -->
    <Error Condition="'$(ArchivesRoot)' == ''"                                         Text="Missing required property ArchivesRoot." />
    <Error Condition="'$(HelixJobType)' == ''"                                         Text="Missing required property HelixJobType." />
    <Error Condition="'$(HelixSource)' == ''"                                          Text="Missing required property HelixSource." />
    <Error Condition="'$(TargetQueues)' == ''"                                         Text="Missing required property TargetQueues." />
    <Error Condition="'$(BuildMoniker)' == ''"                                         Text="Missing required property BuildMoniker." />
    <Error Condition="'$(CloudDropConnectionString)' == ''"                            Text="Missing required property CloudDropConnectionString." />
    <Error Condition="'$(CloudResultsConnectionString)' == ''"                         Text="Missing required property CloudResultsConnectionString." />
    <Error Condition="'$(HelixJobProperties)' == '' and ('$(HelixArchLabel)' == '' or '$(HelixConfigLabel)' == '')"
                                                                                       Text="HelixJobProperties (JSON), or HelixArchLabel and HelixConfigLabel (string) must be set to start Helix jobs" />
  </Target>

  <!-- Provided as an extensibility point for targets to run before the real work begins -->
  <Target Name="PreCloudBuild">

    <!-- Copy runner scripts so they can be uploaded as supplemental payload -->
    <ItemGroup>
      <RunnerScripts Include="$(ToolsDir)RunnerScripts/**/*.py" />
      <RunnerScripts Include="$(ToolsDir)RunnerScripts/**/*.sh" />
      <RunnerScripts Include="$(ToolsDir)RunnerScripts/**/*.txt" />
    </ItemGroup>

    <!-- Split up Target Queues list 
         In order to support all delimiters (commas, plus and semicolons), I do a little hoop-jumping to do a replace first
         Note that queues will never have ',', '+' or ';' in their name, and this greatly simplifies providing a queue list in *nix.
    -->
    <PropertyGroup>
      <ProcessedTargetQueues>$([System.String]::Copy('$(TargetQueues)').Replace(',',';').Replace('+',';'))</ProcessedTargetQueues>  
    </PropertyGroup>

    <ItemGroup>
      <!-- This Split() is needed since the semicolon in ProcessedTargetQueues is now a literal -->
      <TargetQueue Include="$(ProcessedTargetQueues.Split(';'))" />
    </ItemGroup>
    <Message Text="Will Enqueue to @(TargetQueue->Count()) Queue(s) : @(TargetQueue)" />

    <!-- Compress the supplemental payload directory for upload -->
    <!-- We might not have RunnerScripts, so skip the tasks in that case -->
    <MakeDir Directories="$(SupplementalPayloadDir)"/>
    <Copy Condition="'@(RunnerScripts->Count())'!='0'"
          SourceFiles="@(RunnerScripts)"
          DestinationFiles="@(RunnerScripts->'$(SupplementalPayloadDir)RunnerScripts/%(RecursiveDir)%(Filename)%(Extension)')"
          SkipUnchangedFiles="true" />
    <ZipFileCreateFromDirectory Condition="'@(RunnerScripts->Count())'!='0'"
        SourceDirectory="$(SupplementalPayloadDir)"
        DestinationArchive="$(SupplementalPayloadFile)"
        OverwriteDestination="true" />
    <RemoveDir Directories="$(SupplementalPayloadDir)"/>
    <ItemGroup>
      <SupplementalPayload Condition="'@(RunnerScripts->Count())'!='0'" Include="$(SupplementalPayloadFile)">
        <RelativeBlobPath>$(SupplementalPayloadFilename)</RelativeBlobPath>
      </SupplementalPayload>
    </ItemGroup>
  </Target>

  <!-- Make sure the work items included contain all the required fields, calculate relative blob paths -->
  <Target Name="ValidateWorkItems">
    <ValidateWorkItems WorkItems="@(HelixWorkItem)" WorkItemArchiveRoot="$(ArchivesRoot)">
      <Output TaskParameter="ProcessedWorkItems" ItemName="HelixProcessedWorkItem"/>
    </ValidateWorkItems>
  </Target>

  <!-- Create Azure containers and file shares -->
  <Target Name="CreateAzureStorage">
    <CreateAzureContainer
      ConnectionString="$(CloudDropConnectionString)"
      ContainerName="$(ContainerName)"
      ReadOnlyTokenDaysValid="30">
      <Output TaskParameter="StorageUri" PropertyName="DropUri" />
      <Output TaskParameter="ReadOnlyToken" PropertyName="DropUriReadOnlyToken" />
    </CreateAzureContainer>

    <PropertyGroup>
      <CloudResultsReadTokenValidDays  Condition="'$(CloudResultsReadTokenValidDays)' == ''">30</CloudResultsReadTokenValidDays>
      <CloudResultsWriteTokenValidDays Condition="'$(CloudResultsWriteTokenValidDays)' == ''">4</CloudResultsWriteTokenValidDays>
    </PropertyGroup>

    <CreateAzureContainer
      ConnectionString="$(CloudResultsConnectionString)"
      ContainerName="$(ContainerName)"
      ReadOnlyTokenDaysValid ="$(CloudResultsReadTokenValidDays)"
      WriteOnlyTokenDaysValid="$(CloudResultsWriteTokenValidDays)">

      <Output TaskParameter="StorageUri"     PropertyName="ResultsUri" />
      <Output TaskParameter="ReadOnlyToken"  PropertyName="ResultsReadOnlyToken" />
      <Output TaskParameter="WriteOnlyToken" PropertyName="ResultsWriteOnlyToken" />
    </CreateAzureContainer>
  </Target>

  <!-- Upload content to Azure (Everything except test list) -->
  <Target Name="UploadContent" DependsOnTargets="CreateAzureStorage">
    <ItemGroup>
      <LocalFileForUpload Include="@(HelixProcessedWorkItem->Metadata('PayloadFile'))">
        <RelativeBlobPath>%(RelativeBlobPath)</RelativeBlobPath>
      </LocalFileForUpload>
      <!-- For now assume these all go in the root of the container -->
      <LocalPayloadFileForUpload Include="@(HelixCorrelationPayloadFile)">
        <RelativeBlobPath>%(FileName)%(Extension)</RelativeBlobPath>
      </LocalPayloadFileForUpload>
    </ItemGroup>

    <!-- Debug output of work items, and a warning if there are none. -->
    <Message Text="Files for upload :: @(LocalFileForUpload)" Importance="Low" />

    <!-- Verify the test archives were created -->
    <Warning Condition="'@(LocalFileForUpload->Count())' == '0'" Text="Didn't find any archives in supplied HelixWorkItem(s)." />

    <!-- Work Item payloads -->
    <UploadToAzure
      Condition="'@(LocalFileForUpload->Count())' != '0'"
      ConnectionString="$(CloudDropConnectionString)"
      ContainerName="$(ContainerName)"
      Items="@(LocalFileForUpload->Distinct())"
      Overwrite="$(OverwriteOnUpload)" />

    <!-- Correlation payload(s) -->
    <UploadToAzure
      Condition="'@(LocalPayloadFileForUpload->Count())' != '0'"
      ConnectionString="$(CloudDropConnectionString)"
      ContainerName="$(ContainerName)"
      Items="@(LocalPayloadFileForUpload->Distinct())"
      Overwrite="$(OverwriteOnUpload)" />

    <!-- Supplemental payload.  TODO: This could be combined with Correlation Payload items, there's not much(any?) difference -->
    <UploadToAzure
      ConnectionString="$(CloudDropConnectionString)"
      ContainerName="$(ContainerName)"
      Items="@(SupplementalPayload)"
      Overwrite="$(OverwriteOnUpload)"
      Condition="'@(SupplementalPayload)' != ''" />

  </Target>

  <Target Name="CreateTestListJson">
    <!-- We always bring the supplemental payload folder.  
         This contains whatever's in src\Microsoft.DotNet.Build.CloudTestTasks\RunnerScripts,
         but can also contain various other stuff.  
         -->
    <ItemGroup>
      <CorrelationPayloadUri Include="@(SupplementalPayload->'$(DropUri)%(RelativeBlobPath)$(DropUriReadOnlyToken)')" />
      <CorrelationPayloadUri Include="@(LocalPayloadFileForUpload->'$(DropUri)%(RelativeBlobPath)$(DropUriReadOnlyToken)')" />
    </ItemGroup>

    <PropertyGroup>
      <!-- Flatten it into a property as msbuild chokes on @(CorrelationPayloadUri) in CorrelationPayloadUris -->
      <CorrelationPayloadUris>@(CorrelationPayloadUri)</CorrelationPayloadUris>
      <SecondaryQueuesJson Condition="'$(SecondaryQueue)' != ''">{ &quot;QueueId&quot;:&quot;$(SecondaryQueue)&quot;,&quot;SasValidHours&quot;: $(SecondarySasValidHours), &quot;EnqueueSAS&quot; : null, &quot;DropContainerUri&quot; : null,&quot;DropContainerRsas&quot; : null,&quot;DropContainerWsas&quot; : null }</SecondaryQueuesJson>
    </PropertyGroup>

    <ItemGroup>
      <HelixProcessedWorkItem>
        <Command Condition="'$(UseScriptRunner)'!='true'">%(Command)</Command>
        <!-- When UseScriptRunner is set, we'll wrap commands provided with special Helix-y goo for telemetry-->
        <Command Condition="'$(UseScriptRunner)'=='true' AND '$(TargetsWindows)' == 'true'">$(HelixPythonPath) $(RunnerScript) --script %(Command)</Command>
        <Command Condition="'$(UseScriptRunner)'=='true' AND '$(TargetsWindows)' != 'true'">chmod +x $HELIX_WORKITEM_PAYLOAD/*.sh &amp;&amp; $(HelixPythonPath) $(RunnerScript) --script %(Command)</Command>
        <CorrelationPayloadUris>[$(CorrelationPayloadUris)]</CorrelationPayloadUris>
        <PayloadUri>$(DropUri)%(RelativeBlobPath)$(DropUriReadOnlyToken)</PayloadUri>
        <WorkItemId>%(WorkItemId)</WorkItemId>
        <TimeoutInSeconds>%(TimeoutInSeconds)</TimeoutInSeconds>
        <SecondaryQueues Condition="'$(SecondaryQueue)' != ''">[$(SecondaryQueuesJson)]</SecondaryQueues>		
      </HelixProcessedWorkItem>
    </ItemGroup>

    <!-- I'd love a built-in way to do this, but I'm compromising here since this lets us use 
         arbitrary extra metadata in the future to compose work items, then strip out extra stuff used along the way.
         It lets us use a single item to bring along everything we might possibly need in the future -->
    <RemoveItemMetadata Items="@(HelixProcessedWorkItem)" FieldsToRemove="RelativeBlobPath;PayloadFile">
      <Output TaskParameter="ProcessedItems" ItemName="HelixProcessedWorkItemForList"/>
    </RemoveItemMetadata>

    <WriteItemsToJson JsonFileName="$(HelixLogFolder)$(TestListFilename)" Items="@(HelixProcessedWorkItemForList)" ForceJsonArray="true" />
    <ItemGroup>
      <HelixWorkItemList Include="$(HelixLogFolder)$(TestListFilename)">
        <RelativeBlobPath>$(TestListFilename)</RelativeBlobPath>
        <BuildCompleteJson>$(HelixLogFolder)BuildComplete.json</BuildCompleteJson>
        <OfficialBuildJson>$(HelixLogFolder)OfficialBuild.json</OfficialBuildJson>
        <HelixJobUploadCompletePath>$(HelixLogFolder)helixjobuploadcomplete.sem</HelixJobUploadCompletePath>
      </HelixWorkItemList>
    </ItemGroup>

    <UploadToAzure
     ConnectionString="$(CloudDropConnectionString)"
     ContainerName="$(ContainerName)"
     Items="@(HelixWorkItemList)"
     Overwrite="$(OverwriteOnUpload)" />
  </Target>

  <!-- Write event hub notification JSON files -->
  <Target Name="WriteCompletionEvent"
          AfterTargets="CreateTestListJson"
          Inputs="%(HelixWorkItemList.Identity)"
          Outputs="%(HelixWorkItemList.BuildCompleteJson)">

    <CreateItem Include="@(TargetQueue)" AdditionalMetadata="ResultsUri=$(ResultsUri)%(TargetQueue.Identity)/;QueueId=%(TargetQueue.Identity)">
      <Output TaskParameter="Include" ItemName="BuildCompleteTemplateV2" />
    </CreateItem>

    <!-- If the user didn't provide HelixJobProperties, generate them from HelixArchLabel, HelixConfigLabel, and QueueId -->
    <ItemGroup Condition="'$(HelixJobProperties)' == ''">
      <BuildCompleteTemplateV2>
        <Properties>{ &quot;architecture&quot; : &quot;$(HelixArchLabel)&quot;, &quot;configuration&quot;: &quot;$(HelixConfigLabel)&quot;, &quot;operatingSystem&quot; : &quot;%(QueueId)&quot; }</Properties>
      </BuildCompleteTemplateV2>
    </ItemGroup>
    <ItemGroup Condition="'$(HelixJobProperties)' != ''">
      <BuildCompleteTemplateV2>
        <Properties>$(HelixJobProperties)</Properties>
      </BuildCompleteTemplateV2>
    </ItemGroup>

    <ItemGroup Condition="'$(HelixCreator)' != ''">
      <BuildCompleteTemplateV2>
        <Creator>$(HelixCreator)</Creator>
      </BuildCompleteTemplateV2>
    </ItemGroup>

    <ItemGroup Condition="'$(HelixPullRequestId)' != ''">
      <BuildCompleteTemplateV2>
        <PullRequestId>$(HelixPullRequestId)</PullRequestId>
      </BuildCompleteTemplateV2>
    </ItemGroup>

    <ItemGroup>
      <!-- V1 is long since deprecated, and not supported here.  
           Keeping the name for clarity.                       -->
      <BuildCompleteTemplateV2>
        <DropContainerSAS>$(DropUriReadOnlyToken)</DropContainerSAS>
        <ListUri>$(DropUri)%(HelixWorkItemList.Filename)%(HelixWorkItemList.Extension)$(DropUriReadOnlyToken)</ListUri>
        <ResultsUriRSAS>$(ResultsReadOnlyToken)</ResultsUriRSAS>
        <ResultsUriWSAS>$(ResultsWriteOnlyToken)</ResultsUriWSAS>
        <Build>$(BuildMoniker)</Build>
        <Type>$(HelixJobType)</Type>
        <Source>$(HelixSource)</Source>
        <MaxRetryCount>$(MaxRetryCount)</MaxRetryCount>
        <Attempt>$(HelixAttempt)</Attempt>
      </BuildCompleteTemplateV2>
      <BuildComplete Include="@(BuildCompleteTemplateV2)"/>
    </ItemGroup>

    <WriteItemsToJson JsonFileName="%(HelixWorkItemList.BuildCompleteJson)" Items="@(BuildComplete)" />
    <Message Text="Wrote job-start (build complete) JSON for @(BuildComplete->Count()) Queues." />
    <Message Condition="$(MaxRetryCount) &gt; 1" Text="Work Items will automatically retry on non-zero exit code up to $(MaxRetryCount) times." />
    
    <CreateItem Include="%(HelixWorkItemList.BuildCompleteJson)" AdditionalMetadata="RelativeBlobPath=JobStartJsonMessages.json">
      <Output TaskParameter="Include" ItemName="JobStartJsons" />
    </CreateItem>
    
    <UploadToAzure
     ConnectionString="$(CloudDropConnectionString)"
     ContainerName="$(ContainerName)"
     Items="@(JobStartJsons)"
     Overwrite="$(OverwriteOnUpload)" />
    <Message Text="Uploaded job-start JSON files to $(ContainerName).   These can be used for resending the same job for debugging purposes." />

  </Target>

  <!-- Send completion event to Helix API -->
  <Target Name="SendCompletionEvent"
          AfterTargets="WriteCompletionEvent"
          Inputs="%(HelixWorkItemList.BuildCompleteJson)"
          Outputs="%(HelixWorkItemList.HelixJobUploadCompletePath)"
          Condition="'$(SkipSendToHelix)' != 'true'">
    <SendJobsToHelix
      AccessToken="$(HelixApiAccessKey)"
      ApiEndpoint="$(HelixApiEndpoint)"
      EventDataPath="%(HelixWorkItemList.BuildCompleteJson)">
      <Output TaskParameter="JobIds" ItemName="GeneratedCorrelationId" />
    </SendJobsToHelix>

    <!-- Upload the Correlation Ids generated to the test drop container for tracking purposes-->
    <PropertyGroup>
      <HelixCorrelationInfoFileName Condition="'$(HelixCorrelationInfoFileName)'==''">Helix-correlation-infos.json</HelixCorrelationInfoFileName>
    </PropertyGroup>

    <Message Text="Writing correlation info to: $(HelixLogFolder)$(HelixCorrelationInfoFileName) " />
    <WriteItemsToJson JsonFileName="$(HelixLogFolder)$(HelixCorrelationInfoFileName)" Items="@(GeneratedCorrelationId)" ForceJsonArray="true" />
    <ItemGroup>
      <CorrelationFile Include="$(HelixLogFolder)$(HelixCorrelationInfoFileName)">
        <RelativeBlobPath>Tracking/$(HelixCorrelationInfoFileName)</RelativeBlobPath>
      </CorrelationFile>
    </ItemGroup>

    <UploadToAzure
        ConnectionString="$(CloudDropConnectionString)"
        ContainerName="$(ContainerName)"
        Items="@(CorrelationFile)"
        Overwrite="true" />

  </Target>

</Project>