Skip to content

Add scheduled GenUsage pipeline and templates#134

Open
syrle-foronda wants to merge 44 commits intomainfrom
retire-gh-workflow-for-genusage-move-to-az-pipeline
Open

Add scheduled GenUsage pipeline and templates#134
syrle-foronda wants to merge 44 commits intomainfrom
retire-gh-workflow-for-genusage-move-to-az-pipeline

Conversation

@syrle-foronda
Copy link
Member

Add a new Azure Pipelines workflow to generate usage data and two job templates. Files added: pipelines/gen-usage-pipeline.yml (defines a pipeline that uses the 1ES official template, schedules runs every 6 hours at minute 21, and declares jobs for NuGet and Planner usage), pipelines/templates/gen-usage-nuget.yml, and pipelines/templates/gen-usage-planner.yml. Each job/template builds and runs the respective generator using .NET 8 and NuGetAuthenticate and passes AzureStorageConnectionString and ApisOfDotNetWebHookSecret as environment variables. The final generate-usage job depends on the two generator jobs but uses condition: always() so it runs even if they fail, allowing partial usage data to be produced.

Add a new Azure Pipelines workflow to generate usage data and two job templates. Files added: pipelines/gen-usage-pipeline.yml (defines a pipeline that uses the 1ES official template, schedules runs every 6 hours at minute 21, and declares jobs for NuGet and Planner usage), pipelines/templates/gen-usage-nuget.yml, and pipelines/templates/gen-usage-planner.yml. Each job/template builds and runs the respective generator using .NET 8 and NuGetAuthenticate and passes AzureStorageConnectionString and ApisOfDotNetWebHookSecret as environment variables. The final generate-usage job depends on the two generator jobs but uses condition: always() so it runs even if they fail, allowing partial usage data to be produced.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a scheduled Azure Pipelines workflow for generating usage data (NuGet + Planner) and supporting templates, along with several reliability fixes in the usage crawling/storage/generation code paths.

Changes:

  • Added a new scheduled gen-usage-pipeline.yml plus two step templates to run GenUsageNuGet and GenUsagePlanner, then run GenUsage.
  • Updated usage aggregation/storage to ignore dangling parent feature IDs (and added a regression test).
  • Improved NuGet crawling/feed fetching behavior (static HttpClient, configurable DOP/timeouts, bounded queues, truncated logs), plus minor DI cleanup in the web app.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
pipelines/gen-usage-pipeline.yml New scheduled pipeline orchestrating NuGet + Planner usage generation and a final “generate all” job.
pipelines/templates/gen-usage-nuget.yml Template to run GenUsageNuGet with configured env vars (DOP/timeouts/workers).
pipelines/templates/gen-usage-planner.yml Template to run GenUsagePlanner with required env vars.
src/GenUsageNuGet/CrawlMain.cs Adds env-driven worker count/timeout config, bounded output queue, log truncation, improved cancellation handling.
src/GenUsageNuGet/Infra/NuGetFeed.cs Makes catalog crawling configurable and more robust; reuses a static HttpClient with timeout.
src/Terrajobst.UsageCrawling.Storage/UsageDatabase.cs Fixes usage aggregation to ignore dangling parent feature IDs; cleans up parent links on feature delete; ignores duplicate parent inserts.
src/Terrajobst.UsageCrawling.Storage.Tests/UsageDatabaseTests.cs Adds test covering dangling parent feature IDs during aggregation.
src/Terrajobst.ApiCatalog.Generation/NuGet/NuGetFeed.cs Reuses a static HttpClient for feed operations and adjusts owner mapping retrieval.
src/apisof.net/Program.cs Removes redundant singleton registrations for typed HttpClient services.
src/GenUsageNuGet/scratch/worker_001.txt New scratch file added to repo (appears to be a runtime artifact).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +77
env:
DOTNET_ENVIRONMENT: "Release"
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) No newline at end of file
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions passing an AzureStorageConnectionString, but this pipeline is configured for AzureStorageServiceUrl (consistent with other pipelines/tools in this repo). Either update the PR description or adjust the pipeline variables/env names so the documented behavior matches what’s actually deployed.

Copilot uses AI. Check for mistakes.
Comment on lines 405 to 412
processLog.Add("Crawling cancelled.");
return (1, processLog);
}

if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");

processLog.Add($"Exit code = {process.ExitCode}");
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After WaitForExitAsync completes, output/error events can still be delivered while you append Output was truncated... / Exit code = .... Those processLog.Add(...) calls (and reading logWasTruncated) should be done under processLogLock to avoid races with OnDataReceived.

Suggested change
processLog.Add("Crawling cancelled.");
return (1, processLog);
}
if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
processLog.Add($"Exit code = {process.ExitCode}");
lock (processLogLock)
{
processLog.Add("Crawling cancelled.");
}
return (1, processLog);
}
lock (processLogLock)
{
if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
processLog.Add($"Exit code = {process.ExitCode}");
}

Copilot uses AI. Check for mistakes.
- name: BuildConfiguration
value: Release
- name: System.Debug
value: true
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.Debug is set to true for this scheduled pipeline. That can significantly increase log volume and may include more verbose task output than needed for a production scheduled run. Consider removing it or setting it to false unless you’re actively troubleshooting.

Suggested change
value: true
value: false

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +53
- generate_usage_nuget
- generate_usage_planner
# Run this job only if both dependent jobs succeed
condition: and(succeeded('generate_usage_nuget'), succeeded('generate_usage_planner'))
steps:
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says the final generate_usage job should run even if generate_usage_nuget or generate_usage_planner fail (to allow partial usage output), but the current condition requires both to succeed. If the intent is partial generation, switch this condition to always() (or equivalent) and handle missing inputs inside the job.

Copilot uses AI. Check for mistakes.
{
var url = new Uri($"https://feeds.dev.azure.com/{organization}/{project}/_apis/packaging/Feeds/{feed}/packages?api-version=7.1&$skip={skip}", UriKind.Absolute);
var data = await client.GetStreamAsync(url);
var data = await s_httpClient.GetStreamAsync(url);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stream returned by GetStreamAsync isn’t disposed. Over many pages this can keep HTTP connections open longer than necessary and lead to socket/connection exhaustion. Use await using (or otherwise dispose) the stream after JsonNode.Parse finishes reading it.

Suggested change
var data = await s_httpClient.GetStreamAsync(url);
using var data = await s_httpClient.GetStreamAsync(url);

Copilot uses AI. Check for mistakes.
{
using var httpClient = new HttpClient();
var nupkgStream = await httpClient.GetStreamAsync(url);
var nupkgStream = await s_httpClient.GetStreamAsync(url);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TryCopyPackageStreamAsync doesn’t dispose the nupkgStream returned by GetStreamAsync. If the stream isn’t disposed, the underlying HTTP response/connection may remain open, especially problematic now that the HttpClient is static and long-lived. Dispose the stream (e.g., await using) after copying.

Suggested change
var nupkgStream = await s_httpClient.GetStreamAsync(url);
using var nupkgStream = await s_httpClient.GetStreamAsync(url);

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
1
4
2
3
5
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a scratch/runtime artifact rather than source-controlled input. If it’s only produced by the crawler at runtime, it should be removed from the repo and added to .gitignore (e.g., ignore src/GenUsageNuGet/scratch/*).

Suggested change
1
4
2
3
5
This file is an intentional placeholder under src/GenUsageNuGet/scratch/.
It is kept in source control as an example of a scratch/output file layout
and does not represent runtime-generated data from the crawler or any other
part of the system. Tools that flag probable scratch artifacts can safely
ignore this file.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
resources:
repositories:
- repository: self
type: git
ref: main
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pipeline doesn’t extend the 1ES official pipeline template, unlike other pipelines in this repo (e.g., pipelines/build-pipeline.yml and pipelines/gen-design-notes.yml). If 1ES templates are required for SDL/baseline/compliance, this file should use extends: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates as well (and include the 1ESPipelineTemplates repo resource).

Copilot uses AI. Check for mistakes.
Comment on lines +313 to +317
static TimeSpan? GetCrawlingTimeout()
{
var timeoutHours = Environment.GetEnvironmentVariable("GENUSAGE_NUGET_CRAWL_TIMEOUT_HOURS");
if (!double.TryParse(timeoutHours, out var hours) || hours <= 0)
return TimeSpan.FromHours(2);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetCrawlingTimeout() never returns null, but the calling code treats null as “timeout disabled”, making that branch/logging unreachable. Also, double.TryParse uses the current culture, which can misparse values like 3.5 on non-invariant locales. Consider returning null when the env var is missing/empty/0 to truly disable timeouts, and parse using CultureInfo.InvariantCulture.

Copilot uses AI. Check for mistakes.
Comment on lines 405 to 413
processLog.Add("Crawling cancelled.");
return (1, processLog);
}

if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");

processLog.Add($"Exit code = {process.ExitCode}");
return (process.ExitCode, processLog);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processLog is guarded by processLogLock in OnDataReceived, but it’s mutated outside the lock in the cancellation path. Since output/error events can still arrive concurrently, this can race and potentially throw or reorder log lines. Wrap this processLog.Add(...) under the same lock (or switch to a thread-safe collection).

Suggested change
processLog.Add("Crawling cancelled.");
return (1, processLog);
}
if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
processLog.Add($"Exit code = {process.ExitCode}");
return (process.ExitCode, processLog);
lock (processLogLock)
{
processLog.Add("Crawling cancelled.");
return (1, processLog);
}
}
lock (processLogLock)
{
if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
processLog.Add($"Exit code = {process.ExitCode}");
return (process.ExitCode, processLog);
}

Copilot uses AI. Check for mistakes.
Make several robustness and cleanup changes: dispose HTTP response streams by using 'using var' in NuGetFeed to avoid leaking network/stream resources; synchronize access to processLog in CrawlMain by adding locks and adjust the cancellation log message; remove a temporary scratch file and drop the System.Debug pipeline variable. These changes improve resource management and thread-safety and remove leftover debug/temp artifacts.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +191 to +199
using var cts = crawlingTimeout is { } timeout
? new CancellationTokenSource(timeout)
: new CancellationTokenSource();
var cancellationToken = cts.Token;

if (crawlingTimeout is { } timeoutValue)
Console.WriteLine($"Crawling timeout is set to {timeoutValue}.");
else
Console.WriteLine("Crawling timeout is disabled.");
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetCrawlingTimeout() never returns null, but the caller treats null as “timeout disabled” and logs that possibility. As written, the “disabled” branch is unreachable and there’s no way to disable the timeout via GENUSAGE_NUGET_CRAWL_TIMEOUT_HOURS. Either (1) make GetCrawlingTimeout() return null when the env var is missing/empty/<=0, or (2) simplify the call site to always create a timed CancellationTokenSource and remove the disabled logging.

Suggested change
using var cts = crawlingTimeout is { } timeout
? new CancellationTokenSource(timeout)
: new CancellationTokenSource();
var cancellationToken = cts.Token;
if (crawlingTimeout is { } timeoutValue)
Console.WriteLine($"Crawling timeout is set to {timeoutValue}.");
else
Console.WriteLine("Crawling timeout is disabled.");
using var cts = new CancellationTokenSource(crawlingTimeout);
var cancellationToken = cts.Token;
Console.WriteLine($"Crawling timeout is set to {crawlingTimeout}.");

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
1
4
2
3
5
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an accidental scratch/debug artifact and isn’t referenced by the code. Please remove it from the repo (or add it to .gitignore if this directory is intended for local-only scratch files).

Suggested change
1
4
2
3
5

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +15
resources:
repositories:
- repository: self
type: git
ref: main

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pipeline file doesn’t use the repo’s standard 1ES official pipeline template (extends: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates) even though the PR description says it does and other pipelines in this repo do. Please switch this pipeline to use the 1ES template (including adding the 1ESPipelineTemplates repository resource and extends block) to stay consistent and keep SDL/compliance settings aligned with the rest of the repo.

Copilot uses AI. Check for mistakes.
always: true

pool:
name: AzurePipelines-EO
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pipeline pool only specifies name: AzurePipelines-EO without an image/os. Since the jobs run bash scripts (AzureCLI scriptType: bash), running on a Windows agent could fail. Please explicitly target a Linux image (as other pipelines do via the 1ES template, e.g. 1ESPT-Ubuntu22.04).

Suggested change
name: AzurePipelines-EO
name: AzurePipelines-EO
image: 1ESPT-Ubuntu22.04

Copilot uses AI. Check for mistakes.
Add image and os fields to the Azure pipeline pool configuration and simplify crawler timeout handling. CrawlMain.cs now constructs a CancellationTokenSource directly from GetCrawlingTimeout(), and GetCrawlingTimeout() was changed to return a non-nullable TimeSpan with the default increased from 2 to 3 hours. These changes simplify timeout logic and raise the default crawl duration.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +215 to +220
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine($"Crawling interrupted because timeout of {crawlingTimeout} was exceeded.");
if (crawlingTimeout is { } configuredTimeout)
Console.WriteLine($"Crawling interrupted because timeout of {configuredTimeout} was exceeded.");
else
Console.WriteLine("Crawling was cancelled.");
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: crawlingTimeout can never be null, so the "Crawling was cancelled." branch can’t execute as written. Either remove this branch or change the timeout representation so a non-timeout cancellation path is possible/observable.

Copilot uses AI. Check for mistakes.
lock (processLogLock)
{
if (logWasTruncated)
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The if (logWasTruncated) block is missing braces and the indentation suggests the following lines might have been intended to be conditional. Adding braces (and fixing indentation) would prevent accidental logic errors during future edits and make it clear what’s guarded by the condition.

Suggested change
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
{
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
}

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +26
image: 1ESPT-Windows2022
os: windows

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top-level pool here uses image and os, which are parameters used under the 1ES template, not valid keys for a standard Azure Pipelines pool definition. If this pipeline isn’t converted to extends: the 1ES template, this will likely fail YAML validation; if it is converted, move these settings under extends.parameters.pool (as in pipelines/gen-design-notes.yml).

Suggested change
image: 1ESPT-Windows2022
os: windows

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +21 to +23
env:
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions passing an AzureStorageConnectionString, but these templates pass AzureStorageServiceUrl instead. If the description is outdated, update it; otherwise, align the templates/implementation with the expected setting to avoid misconfiguration when the pipeline is wired up.

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +23
env:
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) No newline at end of file
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions passing an AzureStorageConnectionString, but this template passes AzureStorageServiceUrl. If the description is outdated, update it; otherwise ensure the pipeline and generator agree on the configuration source/name.

Copilot uses AI. Check for mistakes.
Comment on lines +390 to +417
try
{
process.Kill();
processLog.Add("Crawling cancelled.");
return (1, processLog);
await process.WaitForExitAsync(cancellationToken);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
if (!process.HasExited)
process.Kill(entireProcessTree: true);

lock (processLogLock)
{
processLog.Add($"Process was killed due to cancellation.");
}

return(1, processLog);
}

processLog.Add($"Exit code = {process.ExitCode}");
return (process.ExitCode, processLog);
lock (processLogLock)
{
if (logWasTruncated)
{
processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines.");
}

processLog.Add($"Exit code = {process.ExitCode}");

return (process.ExitCode, processLog);
}
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BeginOutputReadLine/BeginErrorReadLine can still raise DataReceived events after WaitForExitAsync completes (and especially after Kill on cancellation). Since the returned List<string> may still be mutated by those callbacks, the caller enumerating it can throw InvalidOperationException or miss trailing output. Consider waiting for the async reads to finish (e.g., an additional WaitForExit() after WaitForExitAsync, or otherwise synchronizing completion) and/or returning an immutable snapshot of the log.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +75
pool:
name: AzurePipelines-EO

stages:
- stage: GenerateUsage
jobs:
- job: generate_usage_nuget
displayName: Generate nuget usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-nuget.yml@self

- job: generate_usage_planner
displayName: Generate planner usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-planner.yml@self

- job: generate_usage
displayName: Generate all usage data
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
dependsOn:
- generate_usage_nuget
- generate_usage_planner
# Run this job only if both dependent jobs succeed
condition: and(succeeded('generate_usage_nuget'), succeeded('generate_usage_planner'))
steps:
- checkout: self
- task: UseDotNet@2
displayName: Use .NET 8 SDK
inputs:
version: 8.x
- task: NuGetAuthenticate@1
displayName: "NuGet Authenticate"
- script: |
dotnet build "$(Build.SourcesDirectory)/src/GenUsage/GenUsage.csproj" --configuration $(BuildConfiguration)
displayName: "Build GenUsage"
- task: AzureCLI@2
displayName: "Run GenUsage"
timeoutInMinutes: 0
inputs:
azureSubscription: "$(AzureSubscriptionConnection)"
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
cd "$(Build.SourcesDirectory)/src/GenUsage"
dotnet run --configuration $(BuildConfiguration)
env:
DOTNET_ENVIRONMENT: "Release"
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) No newline at end of file
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pipeline defines pool: directly but the other pipelines in this repo typically use the 1ES official template via extends: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates (e.g. pipelines/build-pipeline.yml:28+, pipelines/gen-design-notes.yml:24+). Using extends here would keep SDL/baseline settings consistent and ensures pool image/os are explicitly set.

Suggested change
pool:
name: AzurePipelines-EO
stages:
- stage: GenerateUsage
jobs:
- job: generate_usage_nuget
displayName: Generate nuget usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-nuget.yml@self
- job: generate_usage_planner
displayName: Generate planner usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-planner.yml@self
- job: generate_usage
displayName: Generate all usage data
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
dependsOn:
- generate_usage_nuget
- generate_usage_planner
# Run this job only if both dependent jobs succeed
condition: and(succeeded('generate_usage_nuget'), succeeded('generate_usage_planner'))
steps:
- checkout: self
- task: UseDotNet@2
displayName: Use .NET 8 SDK
inputs:
version: 8.x
- task: NuGetAuthenticate@1
displayName: "NuGet Authenticate"
- script: |
dotnet build "$(Build.SourcesDirectory)/src/GenUsage/GenUsage.csproj" --configuration $(BuildConfiguration)
displayName: "Build GenUsage"
- task: AzureCLI@2
displayName: "Run GenUsage"
timeoutInMinutes: 0
inputs:
azureSubscription: "$(AzureSubscriptionConnection)"
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
cd "$(Build.SourcesDirectory)/src/GenUsage"
dotnet run --configuration $(BuildConfiguration)
env:
DOTNET_ENVIRONMENT: "Release"
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret)
extends:
template: v1/1ES.Official.PipelineTemplate.yml@1ESPipelineTemplates
parameters:
pool:
name: AzurePipelines-EO
stages:
- stage: GenerateUsage
jobs:
- job: generate_usage_nuget
displayName: Generate nuget usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-nuget.yml@self
- job: generate_usage_planner
displayName: Generate planner usage
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
steps:
- template: templates/gen-usage-planner.yml@self
- job: generate_usage
displayName: Generate all usage data
timeoutInMinutes: 0
cancelTimeoutInMinutes: 5
dependsOn:
- generate_usage_nuget
- generate_usage_planner
# Run this job only if both dependent jobs succeed
condition: and(succeeded('generate_usage_nuget'), succeeded('generate_usage_planner'))
steps:
- checkout: self
- task: UseDotNet@2
displayName: Use .NET 8 SDK
inputs:
version: 8.x
- task: NuGetAuthenticate@1
displayName: "NuGet Authenticate"
- script: |
dotnet build "$(Build.SourcesDirectory)/src/GenUsage/GenUsage.csproj" --configuration $(BuildConfiguration)
displayName: "Build GenUsage"
- task: AzureCLI@2
displayName: "Run GenUsage"
timeoutInMinutes: 0
inputs:
azureSubscription: "$(AzureSubscriptionConnection)"
scriptType: "bash"
scriptLocation: "inlineScript"
inlineScript: |
cd "$(Build.SourcesDirectory)/src/GenUsage"
dotnet run --configuration $(BuildConfiguration)
env:
DOTNET_ENVIRONMENT: "Release"
AzureStorageServiceUrl: $(AzureStorageServiceUrl)
ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret)

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
dependsOn:
- generate_usage_nuget
- generate_usage_planner
# Run this job only if both dependent jobs succeed
condition: and(succeeded('generate_usage_nuget'), succeeded('generate_usage_planner'))
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says the final aggregation job should run with condition: always() to allow partial usage output, but this job is currently gated on both generator jobs succeeding (condition: and(succeeded(...), ...)). If partial generation is desired, update the condition (and ensure GenUsage can tolerate missing inputs) so this job still runs when one generator fails.

Copilot uses AI. Check for mistakes.
scriptLocation: "inlineScript"
inlineScript: |
cd "$(Build.SourcesDirectory)/src/GenUsage"
dotnet run --configuration $(BuildConfiguration)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dotnet run implicitly builds by default, so this job currently builds GenUsage twice (dotnet build ... and then dotnet run ...). To avoid extra time/cost, either remove the explicit build step or run with --no-build (or switch to dotnet publish and execute the published output).

Suggested change
dotnet run --configuration $(BuildConfiguration)
dotnet run --configuration $(BuildConfiguration) --no-build

Copilot uses AI. Check for mistakes.
Ensure async process output readers are drained by calling WaitForExit after WaitForExitAsync and unsubscribe DataReceived handlers in both normal and cancellation paths. Catch InvalidOperationException around process.Kill to handle races where the process already exited. WaitForExitAsync with CancellationToken.None after a cancellation, record the process exit code to the process log, and return an immutable array copy of the process log.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@syrle-foronda
Copy link
Member Author

@danzhu54 made changes base on copilot review. re-running the pipeline for checking now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants