From 02ec6bdb8af4fad6e2754893da69a6ae4a3ecbe2 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 6 Aug 2026 12:53:16 -0700 Subject: [PATCH 1/2] Make PR pipeline artifact names unique per job attempt Pipeline artifact names must be unique within a build, so re-running a failed job in the sqlclient-pr pipeline failed with "Artifact build_and_pack_projects already exists for build ." Suffix the published artifact names with $(System.JobAttempt) in the pack, coverage, and test-results publish steps. System.JobId is stable across attempts of the same job, so the test-results artifact had the same latent collision. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8612610a-cd1b-4668-b818-ce555b5ffd14 --- eng/pipelines/pr/stages/collect-coverage-stage.yml | 7 +++++-- eng/pipelines/pr/stages/pack-stage.yml | 10 ++++++++-- eng/pipelines/pr/steps/publish-test-results-step.yml | 10 +++++++--- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/eng/pipelines/pr/stages/collect-coverage-stage.yml b/eng/pipelines/pr/stages/collect-coverage-stage.yml index 2d03bb064f..9c3382de81 100644 --- a/eng/pipelines/pr/stages/collect-coverage-stage.yml +++ b/eng/pipelines/pr/stages/collect-coverage-stage.yml @@ -61,11 +61,14 @@ stages: # Part 2) Publish merged results to the pipeline results/artifacts - # Publish the merged coverage file as a pipeline artifact + # Publish the merged coverage file as a pipeline artifact. + # + # The artifact name includes the job attempt number so that re-running a failed job does + # not collide with the artifact published by a previous attempt. - task: PublishPipelineArtifact@1 displayName: Publish coverage artifact inputs: - artifact: merged_coverage + artifact: merged_coverage_attempt$(System.JobAttempt) targetPath: "${{ variables.workingDir }}/merge" # Publish the merged coverage file as coverage results so they can be viewed in ADO UI. diff --git a/eng/pipelines/pr/stages/pack-stage.yml b/eng/pipelines/pr/stages/pack-stage.yml index 5473027731..238a40a196 100644 --- a/eng/pipelines/pr/stages/pack-stage.yml +++ b/eng/pipelines/pr/stages/pack-stage.yml @@ -121,9 +121,15 @@ stages: displayName: Output Build Output Tree condition: succeededOrFailed() - # Upload the build output as the artifact of the job + # Upload the build output as the artifact of the job. + # + # The artifact name includes the job attempt number because pipeline artifact names must + # be unique within a build. Re-running a failed job would otherwise attempt to publish + # an artifact that already exists and fail with: + # "Artifact build_and_pack_projects already exists for build ." - publish: $(BUILD_OUTPUT) - artifact: build_and_pack_projects + artifact: build_and_pack_projects_attempt$(System.JobAttempt) + displayName: Publish Build Output condition: succeededOrFailed() diff --git a/eng/pipelines/pr/steps/publish-test-results-step.yml b/eng/pipelines/pr/steps/publish-test-results-step.yml index fa8e5839b7..be3533314d 100644 --- a/eng/pipelines/pr/steps/publish-test-results-step.yml +++ b/eng/pipelines/pr/steps/publish-test-results-step.yml @@ -31,7 +31,7 @@ parameters: type: string # Absolute path to the test results folder. This folder will be published to the artifact defined - # by ${{ testResultsArtifactBaseName }}_$(System.JobId) + # by ${{ testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt) - name: testResultsPath type: string @@ -52,11 +52,15 @@ steps: testRunTitle: "${{ parameters.platformDisplayName }}_${{ parameters.testDisplayName }}" condition: succeededOrFailed() - # Publish the test results as artifacts for the pipeline + # Publish the test results as artifacts for the pipeline. + # + # The artifact name includes the job attempt number because System.JobId is stable across + # attempts of the same job. Without it, re-running a failed test job would fail with + # "Artifact ... already exists for build ." - task: PublishPipelineArtifact@1 displayName: 'Publish Test Artifacts' inputs: - artifact: ${{ parameters.testResultsArtifactBaseName }}_$(System.JobId) + artifact: ${{ parameters.testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt) targetPath: ${{ parameters.testResultsPath }} condition: succeededOrFailed() From 15a8b570e0c6e5b21632d9a441e6a9c52108aec7 Mon Sep 17 00:00:00 2001 From: Cheena Malhotra Date: Thu, 6 Aug 2026 13:48:01 -0700 Subject: [PATCH 2/2] Centralize PR pipeline artifact base names Address review feedback by moving the artifact base names into pr-variables.yml and passing them into the pack and coverage stage templates, matching the existing testResultsArtifactBaseName pattern. This keeps producers and any future consumers in sync via a single definition. Also correct a doc comment to reference parameters.testResultsArtifactBaseName. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8612610a-cd1b-4668-b818-ce555b5ffd14 --- eng/pipelines/pr/sqlclient-pr-pipeline.yml | 2 ++ .../pr/stages/collect-coverage-stage.yml | 7 ++++++- eng/pipelines/pr/stages/pack-stage.yml | 7 ++++++- .../pr/steps/publish-test-results-step.yml | 2 +- eng/pipelines/pr/variables/pr-variables.yml | 18 ++++++++++++++++++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/pr/sqlclient-pr-pipeline.yml b/eng/pipelines/pr/sqlclient-pr-pipeline.yml index 8df34ac600..b72e15ef53 100644 --- a/eng/pipelines/pr/sqlclient-pr-pipeline.yml +++ b/eng/pipelines/pr/sqlclient-pr-pipeline.yml @@ -109,6 +109,7 @@ stages: buildConfiguration: Debug buildSuffix: pr stageName: ${{ variables.stageNamePack }} + packArtifactBaseName: ${{ variables.packArtifactBaseName }} # Stage 1b: Generate secrets - template: /eng/pipelines/pr/stages/generate-secrets-stage.yml@self @@ -142,6 +143,7 @@ stages: # Stage 3: Collect code coverage - template: /eng/pipelines/pr/stages/collect-coverage-stage.yml@self parameters: + coverageArtifactBaseName: ${{ variables.coverageArtifactBaseName }} dependsOn: - ${{ each platform in parameters.platforms }}: - "test_${{ platform.displayName }}" diff --git a/eng/pipelines/pr/stages/collect-coverage-stage.yml b/eng/pipelines/pr/stages/collect-coverage-stage.yml index 9c3382de81..9a0328ec50 100644 --- a/eng/pipelines/pr/stages/collect-coverage-stage.yml +++ b/eng/pipelines/pr/stages/collect-coverage-stage.yml @@ -11,6 +11,11 @@ parameters: type: object default: [] + # Base name of the artifact to publish the merged coverage report to. The job attempt number is + # appended to this to prevent collision across re-runs of the job. + - name: coverageArtifactBaseName + type: string + stages: - stage: collect_code_coverage displayName: "Collect code coverage" @@ -68,7 +73,7 @@ stages: - task: PublishPipelineArtifact@1 displayName: Publish coverage artifact inputs: - artifact: merged_coverage_attempt$(System.JobAttempt) + artifact: ${{ parameters.coverageArtifactBaseName }}_attempt$(System.JobAttempt) targetPath: "${{ variables.workingDir }}/merge" # Publish the merged coverage file as coverage results so they can be viewed in ADO UI. diff --git a/eng/pipelines/pr/stages/pack-stage.yml b/eng/pipelines/pr/stages/pack-stage.yml index 238a40a196..2b43c8632a 100644 --- a/eng/pipelines/pr/stages/pack-stage.yml +++ b/eng/pipelines/pr/stages/pack-stage.yml @@ -37,6 +37,11 @@ parameters: - name: stageName type: string + # Base name of the artifact to publish the build output to. The job attempt number is appended + # to this to prevent collision across re-runs of the job. + - name: packArtifactBaseName + type: string + stages: - stage: ${{ parameters.stageName }} displayName: Build and Pack Projects @@ -128,7 +133,7 @@ stages: # an artifact that already exists and fail with: # "Artifact build_and_pack_projects already exists for build ." - publish: $(BUILD_OUTPUT) - artifact: build_and_pack_projects_attempt$(System.JobAttempt) + artifact: ${{ parameters.packArtifactBaseName }}_attempt$(System.JobAttempt) displayName: Publish Build Output condition: succeededOrFailed() diff --git a/eng/pipelines/pr/steps/publish-test-results-step.yml b/eng/pipelines/pr/steps/publish-test-results-step.yml index be3533314d..b3b11cf5e1 100644 --- a/eng/pipelines/pr/steps/publish-test-results-step.yml +++ b/eng/pipelines/pr/steps/publish-test-results-step.yml @@ -31,7 +31,7 @@ parameters: type: string # Absolute path to the test results folder. This folder will be published to the artifact defined - # by ${{ testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt) + # by ${{ parameters.testResultsArtifactBaseName }}_$(System.JobId)_attempt$(System.JobAttempt) - name: testResultsPath type: string diff --git a/eng/pipelines/pr/variables/pr-variables.yml b/eng/pipelines/pr/variables/pr-variables.yml index cea536b0ba..5d856c4859 100644 --- a/eng/pipelines/pr/variables/pr-variables.yml +++ b/eng/pipelines/pr/variables/pr-variables.yml @@ -35,6 +35,24 @@ variables: - name: stageNameSecrets value: "secrets_stage" + # Base names of the artifacts published by this pipeline. + # + # Pipeline artifact names must be unique within a build, and neither the job ID nor the stage + # name changes when a job is re-run. Every publish step therefore appends + # $(System.JobAttempt) (and, where relevant, $(System.JobId)) to these base names so that + # re-running a failed job does not collide with the artifact published by a previous attempt. + # + # These are defined here, rather than inline at the publish step, so that producers and any + # future consumers of the artifacts stay in sync through a single definition. + + # Base name of the build output artifact published by the pack stage. + - name: packArtifactBaseName + value: "build_and_pack_projects" + + # Base name of the merged code coverage artifact published by the coverage stage. + - name: coverageArtifactBaseName + value: "merged_coverage" + # Base name of the test result artifacts. Job ID should be appended to this to prevent collision. - name: testResultsArtifactBaseName value: "test_results"