Add scheduled GenUsage pipeline and templates#134
Add scheduled GenUsage pipeline and templates#134syrle-foronda wants to merge 44 commits intomainfrom
Conversation
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.
There was a problem hiding this comment.
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.ymlplus two step templates to runGenUsageNuGetandGenUsagePlanner, then runGenUsage. - 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.
| env: | ||
| DOTNET_ENVIRONMENT: "Release" | ||
| AzureStorageServiceUrl: $(AzureStorageServiceUrl) | ||
| ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) No newline at end of file |
There was a problem hiding this comment.
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.
src/GenUsageNuGet/CrawlMain.cs
Outdated
| processLog.Add("Crawling cancelled."); | ||
| return (1, processLog); | ||
| } | ||
|
|
||
| if (logWasTruncated) | ||
| processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines."); | ||
|
|
||
| processLog.Add($"Exit code = {process.ExitCode}"); |
There was a problem hiding this comment.
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.
| 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}"); | |
| } |
pipelines/gen-usage-pipeline.yml
Outdated
| - name: BuildConfiguration | ||
| value: Release | ||
| - name: System.Debug | ||
| value: true |
There was a problem hiding this comment.
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.
| value: true | |
| value: false |
| - 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: |
There was a problem hiding this comment.
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.
| { | ||
| 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); |
There was a problem hiding this comment.
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.
| var data = await s_httpClient.GetStreamAsync(url); | |
| using var data = await s_httpClient.GetStreamAsync(url); |
| { | ||
| using var httpClient = new HttpClient(); | ||
| var nupkgStream = await httpClient.GetStreamAsync(url); | ||
| var nupkgStream = await s_httpClient.GetStreamAsync(url); |
There was a problem hiding this comment.
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.
| var nupkgStream = await s_httpClient.GetStreamAsync(url); | |
| using var nupkgStream = await s_httpClient.GetStreamAsync(url); |
| 1 | ||
| 4 | ||
| 2 | ||
| 3 | ||
| 5 |
There was a problem hiding this comment.
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/*).
| 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. |
| resources: | ||
| repositories: | ||
| - repository: self | ||
| type: git | ||
| ref: main |
There was a problem hiding this comment.
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).
src/GenUsageNuGet/CrawlMain.cs
Outdated
| 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); |
There was a problem hiding this comment.
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.
src/GenUsageNuGet/CrawlMain.cs
Outdated
| 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); |
There was a problem hiding this comment.
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).
| 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); | |
| } |
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.
There was a problem hiding this comment.
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.
src/GenUsageNuGet/CrawlMain.cs
Outdated
| 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."); |
There was a problem hiding this comment.
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.
| 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}."); |
| 1 | ||
| 4 | ||
| 2 | ||
| 3 | ||
| 5 |
There was a problem hiding this comment.
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).
| 1 | |
| 4 | |
| 2 | |
| 3 | |
| 5 |
| resources: | ||
| repositories: | ||
| - repository: self | ||
| type: git | ||
| ref: main | ||
|
|
There was a problem hiding this comment.
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.
| always: true | ||
|
|
||
| pool: | ||
| name: AzurePipelines-EO |
There was a problem hiding this comment.
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).
| name: AzurePipelines-EO | |
| name: AzurePipelines-EO | |
| image: 1ESPT-Ubuntu22.04 |
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.
There was a problem hiding this comment.
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.
src/GenUsageNuGet/CrawlMain.cs
Outdated
| 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."); |
There was a problem hiding this comment.
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.
src/GenUsageNuGet/CrawlMain.cs
Outdated
| lock (processLogLock) | ||
| { | ||
| if (logWasTruncated) | ||
| processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines."); |
There was a problem hiding this comment.
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.
| processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines."); | |
| { | |
| processLog.Add($"Output was truncated after {MaxLoggedLines:N0} lines."); | |
| } |
pipelines/gen-usage-pipeline.yml
Outdated
| image: 1ESPT-Windows2022 | ||
| os: windows | ||
|
|
There was a problem hiding this comment.
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).
| image: 1ESPT-Windows2022 | |
| os: windows |
There was a problem hiding this comment.
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.
| env: | ||
| AzureStorageServiceUrl: $(AzureStorageServiceUrl) | ||
| ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) |
There was a problem hiding this comment.
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.
| env: | ||
| AzureStorageServiceUrl: $(AzureStorageServiceUrl) | ||
| ApisOfDotNetWebHookSecret: $(ApisOfDotNetWebHookSecret) No newline at end of file |
There was a problem hiding this comment.
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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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) |
| 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')) |
There was a problem hiding this comment.
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.
| scriptLocation: "inlineScript" | ||
| inlineScript: | | ||
| cd "$(Build.SourcesDirectory)/src/GenUsage" | ||
| dotnet run --configuration $(BuildConfiguration) |
There was a problem hiding this comment.
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).
| dotnet run --configuration $(BuildConfiguration) | |
| dotnet run --configuration $(BuildConfiguration) --no-build |
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.
There was a problem hiding this comment.
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.
|
@danzhu54 made changes base on copilot review. re-running the pipeline for checking now. |
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.