Repository: Azure/azure-rest-api-specs
Pull request #33158: New workflow for sdk breaking change labeling
Author: login: raych1
Created: 2025-03-12T16:55:10Z | Merged: 2025-03-20T20:16:39Z

### Workflow and Event Handling Enhancements:
* [`.github/src/context.js`]: Added support for the `check_run` event and removed the requirement to find an 'issue-number' artifact for certain events. 
* [`.github/workflows/update-labels.yaml`]: Included the "SDK Breaking Change Labels (Preview)" workflow in the list of upstream workflows.

### SDK Label Management:
* [`.github/src/sdk-types.js`]: Defined `SdkName` types and mapped SDK labels for breaking change labels.
* [`.github/workflows/sdk-breaking-change-labels.yaml`]: Created a new workflow for processing SDK breaking change labels.
* [`.github/workflows/src/sdk-breaking-change-labels.js`]: Implemented the logic to fetch check run data and determine label actions based on artifacts.

### SDK Generation Improvements:
* [`eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml`]: Updated the SDK generation pipeline to publish breaking change label artifacts conditionally. 
* [`eng/tools/spec-gen-sdk-runner/src/commands.ts`]: Enhanced logging and artifact processing during SDK generation, and added handling for breaking change label artifacts. 



Diff:
diff --git a/.github/src/sdk-types.js b/.github/src/sdk-types.js
new file mode 100644
index 000000000000..1d3b057b48a4
--- /dev/null
+++ b/.github/src/sdk-types.js
@@ -0,0 +1,50 @@
+// @ts-check
+
+/**
+ * @typedef {'azure-sdk-for-go' | 'azure-sdk-for-java' | 'azure-sdk-for-js' | 'azure-sdk-for-net' | 'azure-sdk-for-python'} SdkName
+ */
+
+/**
+ * SDK labels mapping for breaking change labels
+ * @type {Object.<SdkName, {
+ *   breakingChange: string | undefined,
+ *   breakingChangeApproved: string | undefined,
+ *   breakingChangeSuppression: string | undefined,
+ *   breakingChangeSuppressionApproved: string | undefined
+ * }>}
+ */
+export const sdkLabels = {
+  "azure-sdk-for-go": {
+    breakingChange: "BreakingChange-Go-Sdk",
+    breakingChangeApproved: "BreakingChange-Go-Sdk-Approved",
+    breakingChangeSuppression: "BreakingChange-Go-Sdk-Suppression",
+    breakingChangeSuppressionApproved:
+      "BreakingChange-Go-Sdk-Suppression-Approved",
+  },
+  "azure-sdk-for-java": {
+    breakingChange: undefined,
+    breakingChangeApproved: undefined,
+    breakingChangeSuppression: undefined,
+    breakingChangeSuppressionApproved: undefined,
+  },
+  "azure-sdk-for-js": {
+    breakingChange: "BreakingChange-JavaScript-Sdk",
+    breakingChangeApproved: "BreakingChange-JavaScript-Sdk-Approved",
+    breakingChangeSuppression: "BreakingChange-JavaScript-Sdk-Suppression",
+    breakingChangeSuppressionApproved:
+      "BreakingChange-JavaScript-Sdk-Suppression-Approved",
+  },
+  "azure-sdk-for-net": {
+    breakingChange: undefined,
+    breakingChangeApproved: undefined,
+    breakingChangeSuppression: undefined,
+    breakingChangeSuppressionApproved: undefined,
+  },
+  "azure-sdk-for-python": {
+    breakingChange: "BreakingChange-Python-Sdk",
+    breakingChangeApproved: "BreakingChange-Python-Sdk-Approved",
+    breakingChangeSuppression: "BreakingChange-Python-Sdk-Suppression",
+    breakingChangeSuppressionApproved:
+      "BreakingChange-Python-Sdk-Suppression-Approved",
+  },
+};
diff --git a/.github/workflows/sdk-breaking-change-labels.yaml b/.github/workflows/sdk-breaking-change-labels.yaml
new file mode 100644
index 000000000000..136c516db646
--- /dev/null
+++ b/.github/workflows/sdk-breaking-change-labels.yaml
@@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        type: string
+
+permissions:
+  contents: read
+
+jobs:
+  sdk-breaking-change-labels:
+    if: ${{ contains(github.event.check_run.name, '(SDK Generation Job)') && github.event.check_run.check_suite.app.name == 'Azure Pipelines'}}
+    name: SDK Breaking Change Labels (Preview)
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            .github
+      
+      - name: Get label and action
+        id: get-label-and-action
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const { getLabelAndAction } =
+              await import('${{ github.workspace }}/.github/workflows/src/sdk-breaking-change-labels.js');
+            return await getLabelAndAction({ github, context, core });
+        env:
+          CHECK_RUN_ID: ${{ inputs.check_run_id }}
+
+      - if: |
+          (fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' ||
+          fromJson(steps.get-label-and-action.outputs.result).labelAction == 'remove')
+        name: Upload artifact with results
+        uses: ./.github/actions/add-label-artifact
+        with:
+          name: "${{ fromJson(steps.get-label-and-action.outputs.result).labelName}}"
+          # Convert "add/remove" to "true/false"
+          value: "${{ fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' }}"
+
+      - if: |
+          (fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' ||
+          fromJson(steps.get-label-and-action.outputs.result).labelAction == 'remove')
+        name: Upload artifact with issue number
+        uses: ./.github/actions/add-empty-artifact
+        with:
+          name: "issue-number"
+          value: "${{ fromJson(steps.get-label-and-action.outputs.result).issueNumber }}"
diff --git a/.github/workflows/src/context.js b/.github/workflows/src/context.js
index b7b6829087b2..3a9730e7dd12 100644
--- a/.github/workflows/src/context.js
+++ b/.github/workflows/src/context.js
@@ -9,7 +9,7 @@ import { PER_PAGE_MAX } from "./github.js";
  * @param {import('github-script').AsyncFunctionArguments['github']} github
  * @param {import('github-script').AsyncFunctionArguments['context']} context
  * @param {import('github-script').AsyncFunctionArguments['core']} core
- * @returns {Promise<{owner: string, repo: string, head_sha: string, issue_number: number, run_id: number }>}
+ * @returns {Promise<{owner: string, repo: string, head_sha: string, issue_number: number, run_id: number, ado_project_url?: string, ado_build_id?: string }>}
  */
 export async function extractInputs(github, context, core) {
   core.info("extractInputs()");
@@ -23,7 +23,7 @@ export async function extractInputs(github, context, core) {
   // with debug enabled to replay the previous context.
   core.isDebug() && core.debug(`context: ${JSON.stringify(context)}`);
 
-  /** @type {{ owner: string, repo: string, head_sha: string, issue_number: number, run_id: number }} */
+  /** @type {{ owner: string, repo: string, head_sha: string, issue_number: number, run_id: number, ado_project_url?: string, ado_build_id?: string }} */
   let inputs;
 
   // Add support for more event types as needed
@@ -79,7 +79,6 @@ export async function extractInputs(github, context, core) {
       /** @type {import("@octokit/webhooks-types").WorkflowDispatchEvent} */ (
         context.payload
       );
-
     inputs = {
       owner: payload.repository.owner.login,
       repo: payload.repository.name,
@@ -151,7 +150,8 @@ export async function extractInputs(github, context, core) {
       }
     } else if (
       payload.workflow_run.event === "issue_comment" ||
-      payload.workflow_run.event == "workflow_run"
+      payload.workflow_run.event == "workflow_run" ||
+      payload.workflow_run.event == "check_run"
     ) {
       // Attempt to extract issue number from artifact.  This can be trusted, because it was uploaded from a workflow that is trusted,
       // because "issue_comment" and "workflow_run" only trigger on workflows in the default branch.
@@ -191,11 +191,11 @@ export async function extractInputs(github, context, core) {
           }
         }
       }
-
       if (!issue_number) {
-        throw new Error(
+        core.info(
           `Could not find 'issue-number' artifact, which is required to associate the triggering workflow run with a PR`,
         );
+        issue_number = NaN;
       }
     } else {
       throw new Error(
@@ -210,6 +210,36 @@ export async function extractInputs(github, context, core) {
       issue_number: issue_number,
       run_id: payload.workflow_run.id,
     };
+  } else if (context.eventName === "check_run") {
+    let checkRun = context.payload.check_run;
+
+    // Extract the ADO build ID and project URL from the check run details URL
+    const buildUrlRegex = /^(.*?)(?=\/_build\/).*?[?&]buildId=(\d+)/;
+    const match = checkRun.details_url.match(buildUrlRegex);
+    if (!match) {
+      throw new Error(
+        `Could not extract build ID or project URL from check run details URL: ${checkRun.details_url}`,
+      );
+    }
+    if (
+      !context.payload.repository ||
+      !context.payload.repository.owner ||
+      !context.payload.repository.owner.login ||
+      !context.payload.repository.name
+    ) {
+      throw new Error(
+        `Could not extract repository owner or name from context payload: ${JSON.stringify(context.payload.repository)}`,
+      );
+    }
+    inputs = {
+      owner: context.payload.repository.owner.login,
+      repo: context.payload.repository.name,
+      head_sha: checkRun.head_sha,
+      ado_build_id: match[2],
+      ado_project_url: match[1],
+      issue_number: NaN,
+      run_id: NaN,
+    };
   } else {
     throw new Error(
       `Context '${context.eventName}:${context.payload.action}' is not yet supported.`,
diff --git a/.github/workflows/src/issues.js b/.github/workflows/src/issues.js
new file mode 100644
index 000000000000..0a356a16a24e
--- /dev/null
+++ b/.github/workflows/src/issues.js
@@ -0,0 +1,52 @@
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {String} params.head_sha - The head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+export async function getIssueNumber({ head_sha, core, github }) {
+  let issueNumber = NaN;
+
+  if (!head_sha) {
+    core.info("No head_sha found in check run");
+    return { issueNumber };
+  }
+
+  core.info(`Searching for PRs with commit SHA: ${head_sha}`);
+
+  try {
+    const searchResponse = await github.rest.search.issuesAndPullRequests({
+      q: `sha:${head_sha} type:pr state:open`,
+    });
+
+    const totalCount = searchResponse.data.total_count;
+    const itemsCount = searchResponse.data.items.length;
+
+    core.info(
+      `Search results: ${totalCount} total matches, ${itemsCount} items returned`,
+    );
+
+    if (itemsCount > 0) {
+      const firstItem = searchResponse.data.items[0];
+      issueNumber = firstItem.number;
+      core.info(
+        `Found the first matched PR #${issueNumber}: ${firstItem.html_url}`,
+      );
+
+      if (itemsCount > 1) {
+        core.warning(
+          `Multiple PRs found for commit ${head_sha}: ${searchResponse.data.items.map((item) => `#${item.html_url}`).join(", ")}`,
+        );
+      }
+    } else {
+      core.info(`No open PRs found for commit ${head_sha}`);
+    }
+  } catch (error) {
+    core.error(`Error searching for PRs with commit ${head_sha}: ${error}`);
+    throw error;
+  }
+
+  return { issueNumber };
+}
diff --git a/.github/workflows/src/sdk-breaking-change-labels.js b/.github/workflows/src/sdk-breaking-change-labels.js
new file mode 100644
index 000000000000..4b210ed9ec23
--- /dev/null
+++ b/.github/workflows/src/sdk-breaking-change-labels.js
@@ -0,0 +1,140 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "./label.js";
+import { extractInputs } from "./context.js";
+import { getIssueNumber } from "./issues.js";
+
+/**
+ * @typedef {Object} ArtifactResource
+ * @property {string} [downloadUrl]
+ */
+
+/**
+ * @typedef {Object} Artifacts
+ * @property {ArtifactResource} [resource]
+ */
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  const inputs = await extractInputs(github, context, core);
+  const ado_build_id = inputs.ado_build_id;
+  const ado_project_url = inputs.ado_project_url;
+  const head_sha = inputs.head_sha;
+  if (!ado_build_id || !ado_project_url || !head_sha) {
+    throw new Error(
+      `Required inputs are not valid: ado_build_id:${ado_build_id}, ado_project_url:${ado_project_url}, head_sha:${head_sha}`,
+    );
+  }
+  return await getLabelAndActionImpl({
+    ado_build_id,
+    ado_project_url,
+    head_sha,
+    core,
+    github,
+  });
+}
+
+/**
+ * @param {Object} params
+ * @param {string} params.ado_build_id
+ * @param {string} params.ado_project_url
+ * @param {string} params.head_sha
+ * @param {(import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api)} params.github
+ * @param {typeof import("@actions/core")} params.core
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number}>}
+ */
+export async function getLabelAndActionImpl({
+  ado_build_id,
+  ado_project_url,
+  head_sha,
+  core,
+  github,
+}) {
+  let issue_number = NaN;
+  let labelAction;
+  let labelName = "";
+  const artifactName = "spec-gen-sdk-breaking-change-artifact";
+  const artifactFileName = artifactName + ".json";
+  const apiUrl = `${ado_project_url}/_apis/build/builds/${ado_build_id}/artifacts?artifactName=${artifactName}&api-version=7.0`;
+  core.info(`Calling Azure DevOps API to get the artifact: ${apiUrl}`);
+
+  // Use Node.js fetch to call the API
+  const response = await fetch(apiUrl, {
+    method: "GET",
+    headers: {
+      "Content-Type": "application/json",
+    },
+  });
+
+  if (response.status === 404) {
+    core.info(
+      `Artifact '${artifactName}' not found (404). This might be expected if there are no breaking changes.`,
+    );
+  } else if (response.ok) {
+    // Step 1: Get the download URL for the artifact
+    /** @type {Artifacts} */
+    const artifacts = /** @type {Artifacts} */ (await response.json());
+    core.info(`Artifacts found: ${JSON.stringify(artifacts)}`);
+    if (!artifacts.resource || !artifacts.resource.downloadUrl) {
+      throw new Error(
+        `Download URL not found for the artifact ${artifactName}`,
+      );
+    }
+
+    let downloadUrl = artifacts.resource.downloadUrl;
+    const index = downloadUrl.indexOf("?format=zip");
+    if (index !== -1) {
+      // Keep everything up to (but not including) "?format=zip"
+      downloadUrl = downloadUrl.substring(0, index);
+    }
+    downloadUrl += `?format=file&subPath=/${artifactFileName}`;
+    core.info(`Downloading artifact from: ${downloadUrl}`);
+
+    // Step 2: Fetch Artifact Content (as a Buffer)
+    const artifactResponse = await fetch(downloadUrl);
+    if (!artifactResponse.ok) {
+      throw new Error(
+        `Failed to fetch artifact: ${artifactResponse.statusText}`,
+      );
+    }
+
+    const artifactData = await artifactResponse.text();
+    core.info(`Artifact content: ${artifactData}`);
+
+    // Parse the JSON data
+    const breakingChangeResult = JSON.parse(artifactData);
+    const labelActionText = breakingChangeResult.labelAction;
+    const breakingChangeLanguage = breakingChangeResult.language;
+    if (breakingChangeLanguage) {
+      labelName = sdkLabels[`${breakingChangeLanguage}`].breakingChange;
+    }
+
+    // Set label action and name based on the artifacts
+    if (labelActionText === true) {
+      labelAction = LabelAction.Add;
+    } else if (labelActionText === false) {
+      labelAction = LabelAction.Remove;
+    }
+
+    // Get the issue number from the check run
+    if (!issue_number) {
+      const { issueNumber } = await getIssueNumber({ head_sha, core, github });
+      issue_number = issueNumber;
+    }
+  } else {
+    core.error(
+      `Failed to fetch artifacts: ${response.status}, ${response.statusText}`,
+    );
+    const errorText = await response.text();
+    core.error(`Error details: ${errorText}`);
+  }
+  if (!labelAction) {
+    core.info("No label action found, defaulting to None");
+    labelAction = LabelAction.None;
+  }
+
+  return { labelName, labelAction, issueNumber: issue_number };
+}
diff --git a/.github/workflows/src/update-labels.js b/.github/workflows/src/update-labels.js
index 65c8c1ed4e39..1ec70e2136ca 100644
--- a/.github/workflows/src/update-labels.js
+++ b/.github/workflows/src/update-labels.js
@@ -97,6 +97,18 @@ export async function updateLabelsImpl({
   core.info(`labelsToAdd: ${JSON.stringify(labelsToAdd)}`);
   core.info(`labelsToRemove: ${JSON.stringify(labelsToRemove)}`);
 
+  if (
+    (labelsToAdd.length > 0 || labelsToRemove.length > 0) &&
+    Number.isNaN(issue_number)
+  ) {
+    throw new Error(
+      `Invalid value for 'issue_number':${issue_number}. Expected an 'issue-number' artifact created by the workflow run.`,
+    );
+  }
+
+  const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${issue_number}`;
+  core.info(`pull request url: ${pullRequestUrl}`);
+
   if (labelsToAdd.length > 0) {
     await github.rest.issues.addLabels({
       owner: owner,
diff --git a/.github/workflows/test/context.test.js b/.github/workflows/test/context.test.js
index cc4943a35829..27f3a9944a7f 100644
--- a/.github/workflows/test/context.test.js
+++ b/.github/workflows/test/context.test.js
@@ -306,7 +306,13 @@ describe("extractInputs", () => {
     });
     await expect(
       extractInputs(github, context, createMockCore()),
-    ).rejects.toThrow(/could not find/i);
+    ).resolves.toEqual({
+      owner: "TestRepoOwnerLogin",
+      repo: "TestRepoName",
+      head_sha: "abc123",
+      issue_number: NaN,
+      run_id: 456,
+    });
   });
 
   it("workflow_run:completed:unsupported", async () => {
@@ -325,3 +331,98 @@ describe("extractInputs", () => {
     ).rejects.toThrow();
   });
 });
+
+it("workflow_run:completed:check_run", async () => {
+  const github = createMockGithub();
+  github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({
+    data: { artifacts: [] },
+  });
+
+  const context = {
+    eventName: "workflow_run",
+    payload: {
+      action: "completed",
+      workflow_run: {
+        event: "check_run",
+        head_sha: "abc123",
+        id: 456,
+        repository: {
+          name: "TestRepoName",
+          owner: {
+            login: "TestRepoOwnerLogin",
+          },
+        },
+      },
+    },
+  };
+
+  github.rest.actions.listWorkflowRunArtifacts.mockResolvedValue({
+    data: { artifacts: [] },
+  });
+  await expect(
+    extractInputs(github, context, createMockCore()),
+  ).resolves.toEqual({
+    owner: "TestRepoOwnerLogin",
+    repo: "TestRepoName",
+    head_sha: "abc123",
+    issue_number: NaN,
+    run_id: 456,
+  });
+});
+
+it("check_run:completed", async () => {
+  const github = createMockGithub();
+  const context = {
+    eventName: "check_run",
+    payload: {
+      action: "completed",
+      check_run: {
+        details_url:
+          "https://dev.azure.com/abc/123-456/_build/results?buildId=56789",
+        head_sha: "abc123",
+      },
+      repository: {
+        name: "TestRepoName",
+        owner: {
+          login: "TestRepoOwnerLogin",
+        },
+      },
+    },
+  };
+
+  await expect(
+    extractInputs(github, context, createMockCore()),
+  ).resolves.toEqual({
+    owner: "TestRepoOwnerLogin",
+    repo: "TestRepoName",
+    issue_number: NaN,
+    head_sha: "abc123",
+    run_id: NaN,
+    ado_build_id: "56789",
+    ado_project_url: "https://dev.azure.com/abc/123-456",
+  });
+});
+
+it("check_run:completed throw error when the build url is invalid", async () => {
+  const github = createMockGithub();
+  const context = {
+    eventName: "check_run",
+    payload: {
+      action: "completed",
+      check_run: {
+        details_url: "https://debc/123-456/_build/result/buildId=56789",
+        head_sha: "abc123",
+      },
+      repository: {
+        name: "TestRepoName",
+        owner: {
+          login: "TestRepoOwnerLogin",
+        },
+      },
+    },
+  };
+
+  await expect(
+    extractInputs(github, context, createMockCore()),
+  ).rejects.toThrow();
+});
diff --git a/.github/workflows/test/issues.test.js b/.github/workflows/test/issues.test.js
new file mode 100644
index 000000000000..69ccc855b86b
--- /dev/null
+++ b/.github/workflows/test/issues.test.js
@@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify appropriate message was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      "No head_sha found in check run",
+    );
+  });
+
+  it("should handle multiple PRs for the same commit", async () => {
+    // Mock multiple PRs found
+    mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+      data: {
+        total_count: 2,
+        items: [
+          { number: 123, html_url: "https://github.com/pr/123" },
+          { number: 456, html_url: "https://github.com/pr/456" },
+        ],
+      },
+    });
+
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "abc",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result uses first PR
+    expect(result.issueNumber).toBe(123);
+
+    // Verify warning was logged
+    expect(mockCore.warning).toHaveBeenCalledWith(
+      expect.stringContaining("Multiple PRs found for commit"),
+    );
+  });
+
+  it("should handle no PRs found", async () => {
+    // Mock no PRs found
+    mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+      data: {
+        total_count: 0,
+        items: [],
+      },
+    });
+
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "abc",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify info was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      expect.stringContaining("No open PRs found for commit"),
+    );
+  });
+
+  it("should handle GitHub search API errors", async () => {
+    // Mock GitHub API error
+    const searchError = new Error("GitHub API failure");
+    mockGithub.rest.search.issuesAndPullRequests.mockRejectedValue(searchError);
+
+    // Call function and expect it to throw
+    await expect(
+      getIssueNumber({ head_sha: "abc", github: mockGithub, core: mockCore }),
+    ).rejects.toThrow();
+
+    // Verify error was logged
+    expect(mockCore.error).toHaveBeenCalledWith(
+      expect.stringContaining("Error searching for PRs"),
+    );
+  });
+});
diff --git a/.github/workflows/test/mocks.js b/.github/workflows/test/mocks.js
index 5ab541a954c6..ed845f6f8d5b 100644
--- a/.github/workflows/test/mocks.js
+++ b/.github/workflows/test/mocks.js
@@ -36,6 +36,9 @@ export function createMockGithub() {
           data: [],
         }),
       },
+      search: {
+        issuesAndPullRequests: vi.fn(),
+      },
     },
   };
 }
@@ -45,6 +48,8 @@ export function createMockCore() {
   return {
     debug: vi.fn(console.debug),
     info: vi.fn(console.log),
+    error: vi.fn(console.error),
+    warning: vi.fn(console.warn),
     isDebug: vi.fn().mockReturnValue(true),
     setOutput: vi.fn((name, value) =>
       console.log(`setOutput('${name}', '${value}')`),
@@ -58,3 +63,14 @@ export function createMockRequestError(status) {
     request: { url: "test url", headers: {} },
   });
 }
+
+// Partial mock of `context` parameter passed into github-script actions
+export function createMockContext() {
+  return {
+    payload: {},
+    repo: {
+      owner: "owner",
+      repo: "repo",
+    },
+  };
+}
diff --git a/.github/workflows/test/sdk-breaking-change-labels.test.js b/.github/workflows/test/sdk-breaking-change-labels.test.js
new file mode 100644
index 000000000000..c2c64e225bb4
--- /dev/null
+++ b/.github/workflows/test/sdk-breaking-change-labels.test.js
@@ -0,0 +1,432 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { LabelAction } from "../src/label.js";
+import {
+  createMockCore,
+  createMockGithub,
+  createMockContext,
+} from "./mocks.js";
+import {
+  getLabelAndAction,
+  getLabelAndActionImpl,
+} from "../src/sdk-breaking-change-labels.js";
+import { sdkLabels } from "../../src/sdk-types.js";
+
+// Mock dependencies
+vi.mock("../src/context.js", () => ({
+  extractInputs: vi.fn(),
+}));
+
+// Mock global fetch
+global.fetch = vi.fn();
+
+const mockGithub = createMockGithub();
+const mockContext = createMockContext();
+const mockCore = createMockCore();
+
+describe("sdk-breaking-change-labels", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+
+  describe("getLabelAndAction", () => {
+    it("should extract inputs and call getLabelAndActionImpl", async () => {
+      // Mock extracted inputs
+      const mockInputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Setup mock implementation for extractInputs
+      const { extractInputs } = await import("../src/context.js");
+      extractInputs.mockResolvedValue(mockInputs);
+
+      // Mock fetch responses
+      // First fetch - artifact metadata
+      const mockArtifactResponse = {
+        ok: true,
+        json: vi.fn().mockResolvedValue({
+          resource: {
+            downloadUrl: "https://dev.azure.com/download?format=zip",
+          },
+        }),
+        text: vi.fn(),
+      };
+
+      // Second fetch - artifact content
+      const language = "azure-sdk-for-js";
+      const mockContentResponse = {
+        ok: true,
+        text: vi.fn().mockResolvedValue(
+          JSON.stringify({
+            labelAction: true,
+            language,
+          }),
+        ),
+      };
+
+      // Mock PR search results
+      mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+        data: {
+          total_count: 1,
+          items: [{ number: 123, html_url: "https://github.com/pr/123" }],
+        },
+      });
+
+      // Setup fetch to return different responses for each call
+      global.fetch.mockImplementation((url) => {
+        if (url.includes("artifacts?artifactName=")) {
+          return mockArtifactResponse;
+        } else {
+          return mockContentResponse;
+        }
+      });
+
+      // Call the function
+      const result = await getLabelAndAction({
+        github: mockGithub,
+        context: mockContext,
+        core: mockCore,
+      });
+
+      // Verify result
+      expect(result).toEqual({
+        labelName: sdkLabels[language].breakingChange,
+        labelAction: LabelAction.Add,
+        issueNumber: 123,
+      });
+
+      // Verify mocks were called correctly
+      expect(mockGithub.rest.search.issuesAndPullRequests).toHaveBeenCalledWith(
+        {
+          q: `sha:abc123 type:pr state:open`,
+        },
+      );
+    });
+    it("should correctly set labelAction to Remove", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Setup mock for extractInputs
+      const { extractInputs } = await import("../src/context.js");
+      extractInputs.mockResolvedValue(inputs);
+
+      // Mock artifact responses with 'remove' action
+      const mockArtifactResponse = {
+        ok: true,
+        json: vi.fn().mockResolvedValue({
+          resource: {
+            downloadUrl: "https://dev.azure.com/download?format=zip",
+          },
+        }),
+      };
+
+      const language = "azure-sdk-for-js";
+      const mockContentResponse = {
+        ok: true,
+        text: vi.fn().mockResolvedValue(
+          JSON.stringify({
+            labelAction: false,
+            language,
+          }),
+        ),
+      };
+
+      // Setup fetch to return different responses for each call
+      global.fetch.mockImplementation((url) => {
+        if (url.includes("artifacts?artifactName=")) {
+          return mockArtifactResponse;
+        } else {
+          return mockContentResponse;
+        }
+      });
+
+      // Mock PR search
+      mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+        data: {
+          total_count: 1,
+          items: [{ number: 123, html_url: "https://github.com/pr/123" }],
+        },
+      });
+
+      // Call function
+      const result = await getLabelAndAction({
+        github: mockGithub,
+        context: mockContext,
+        core: mockCore,
+      });
+
+      // Verify result has Remove action
+      expect(result).toEqual({
+        labelName: sdkLabels[language].breakingChange,
+        labelAction: LabelAction.Remove,
+        issueNumber: 123,
+      });
+    });
+    it("should throw error with invalid inputs", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Setup mock for extractInputs
+      const { extractInputs } = await import("../src/context.js");
+      extractInputs.mockResolvedValue(inputs);
+
+      // Call function and expect it to throw
+      await expect(
+        getLabelAndAction({
+          github: mockGithub,
+          context: mockContext,
+          core: mockCore,
+        }),
+      ).rejects.toThrow();
+    });
+  });
+
+  describe("getLabelAndActionImpl error handling", () => {
+    it("should handle API failure", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock fetch failure
+      global.fetch.mockResolvedValue({
+        ok: false,
+        status: 500,
+        statusText: "Server Error",
+        text: vi.fn().mockResolvedValue("Artifact not found"),
+      });
+
+      // Mock PR search success
+      mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+        data: {
+          total_count: 1,
+          items: [{ number: 123, html_url: "https://github.com/pr/123" }],
+        },
+      });
+
+      // Call function
+      const result = await getLabelAndActionImpl({
+        ado_build_id: inputs.ado_build_id,
+        ado_project_url: inputs.ado_project_url,
+        head_sha: inputs.head_sha,
+        github: mockGithub,
+        core: mockCore,
+      });
+
+      // Verify result uses default values when artifact fetch fails
+      expect(result).toEqual({
+        labelName: "",
+        labelAction: LabelAction.None,
+        issueNumber: NaN,
+      });
+
+      // Verify error was logged
+      expect(mockCore.error).toHaveBeenCalledWith(
+        expect.stringContaining("Failed to fetch artifacts"),
+      );
+    });
+
+    it("should complete without op when artifact does not exist", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock fetch failure
+      global.fetch.mockResolvedValue({
+        ok: false,
+        status: 404,
+        statusText: "Not Found",
+        text: vi.fn().mockResolvedValue("Artifact not found"),
+      });
+
+      // Mock PR search success
+      mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+        data: {
+          total_count: 1,
+          items: [{ number: 123, html_url: "https://github.com/pr/123" }],
+        },
+      });
+
+      // Call function
+      const result = await getLabelAndActionImpl({
+        ado_build_id: inputs.ado_build_id,
+        ado_project_url: inputs.ado_project_url,
+        head_sha: inputs.head_sha,
+        github: mockGithub,
+        core: mockCore,
+      });
+
+      // Verify result uses default values when artifact fetch fails
+      expect(result).toEqual({
+        labelName: "",
+        labelAction: LabelAction.None,
+        issueNumber: NaN,
+      });
+    });
+
+    it("should throw error if resource is empty from the artifact api response", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock artifact responses with 'remove' action
+      const mockArtifactResponse = {
+        ok: true,
+        json: vi.fn().mockResolvedValue({}),
+      };
+
+      // Setup fetch to return different responses for each call
+      global.fetch.mockImplementation((url) => {
+        if (url.includes("artifacts?artifactName=")) {
+          return mockArtifactResponse;
+        }
+      });
+
+      // Call function and expect it to throw
+      await expect(
+        getLabelAndActionImpl({
+          ado_build_id: inputs.ado_build_id,
+          ado_project_url: inputs.ado_project_url,
+          head_sha: inputs.head_sha,
+          github: mockGithub,
+          core: mockCore,
+        }),
+      ).rejects.toThrow();
+    });
+
+    it("should throw error if missing download url from the artifact api response", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock artifact responses with 'remove' action
+      const mockArtifactResponse = {
+        ok: true,
+        json: vi.fn().mockResolvedValue({
+          resource: {},
+        }),
+      };
+
+      // Setup fetch to return different responses for each call
+      global.fetch.mockImplementation((url) => {
+        if (url.includes("artifacts?artifactName=")) {
+          return mockArtifactResponse;
+        }
+      });
+
+      // Call function and expect it to throw
+      await expect(
+        getLabelAndActionImpl({
+          ado_build_id: inputs.ado_build_id,
+          ado_project_url: inputs.ado_project_url,
+          head_sha: inputs.head_sha,
+          github: mockGithub,
+          core: mockCore,
+        }),
+      ).rejects.toThrow();
+    });
+
+    it("should throw error when fail to fetch artifact content", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock fetch responses
+      // First fetch - artifact metadata
+      const mockArtifactResponse = {
+        ok: true,
+        json: vi.fn().mockResolvedValue({
+          resource: {
+            downloadUrl: "https://dev.azure.com/download?format=zip",
+          },
+        }),
+        text: vi.fn(),
+      };
+
+      // Second fetch - artifact content
+      const mockContentResponse = {
+        ok: false,
+        status: 404,
+        statusText: "Not Found",
+        text: vi.fn().mockResolvedValue("Artifact not found"),
+      };
+
+      // Setup fetch to return different responses for each call
+      global.fetch.mockImplementation((url) => {
+        if (url.includes("artifacts?artifactName=")) {
+          return mockArtifactResponse;
+        } else {
+          return mockContentResponse;
+        }
+      });
+
+      // Call function and expect it to throw
+      await expect(
+        getLabelAndActionImpl({
+          ado_build_id: inputs.ado_build_id,
+          ado_project_url: inputs.ado_project_url,
+          head_sha: inputs.head_sha,
+          github: mockGithub,
+          core: mockCore,
+        }),
+      ).rejects.toThrow();
+    });
+
+    it("should handle exception during processing", async () => {
+      // Setup inputs
+      const inputs = {
+        ado_build_id: "12345",
+        ado_project_url: "https://dev.azure.com/project",
+        head_sha: "abc123",
+      };
+
+      // Mock fetch to throw an error
+      global.fetch.mockImplementation(() => {
+        throw new Error("Network error");
+      });
+
+      // Mock PR search success
+      mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+        data: {
+          total_count: 1,
+          items: [{ number: 123, html_url: "https://github.com/pr/123" }],
+        },
+      });
+
+      // Call function and expect it to throw
+      await expect(
+        getLabelAndActionImpl({
+          ado_build_id: inputs.ado_build_id,
+          ado_project_url: inputs.ado_project_url,
+          head_sha: inputs.head_sha,
+          github: mockGithub,
+          core: mockCore,
+        }),
+      ).rejects.toThrow();
+    });
+  });
+});
diff --git a/.github/workflows/update-labels.yaml b/.github/workflows/update-labels.yaml
index 3a3627f5333e..fc6c106f0d84 100644
--- a/.github/workflows/update-labels.yaml
+++ b/.github/workflows/update-labels.yaml
@@ -7,7 +7,7 @@ on:
   # If an upstream workflow if completed, get only the artifacts from that workflow, and update labels
   workflow_run:
     workflows:
-      ["ARM Auto SignOff (Preview)", "SDK Suppressions", "TypeSpec Requirement"]
+      ["ARM Auto SignOff (Preview)", "SDK Breaking Change Labels (Preview)", "SDK Suppressions", "TypeSpec Requirement"]
     types: [completed]
   workflow_dispatch:
     inputs:
diff --git a/eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml b/eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml
index 21c8d8d1738e..eee536b112a2 100644
--- a/eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml
+++ b/eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml
@@ -152,7 +152,7 @@ extends:
 
           - script: |
               optional_params=""
-              sdk_gen_info="sdk generation from Config : "
+              sdk_gen_info="Configurations: "
 
               if [ "$(Build.Reason)" != "PullRequest" ] && [ "${{ parameters.ConfigType }}" = "TypeSpec" ]; then
                 optional_params="$optional_params --tsp-config-relative-path ${{ parameters.ConfigPath }}"
@@ -174,10 +174,12 @@ extends:
               fi
               sdk_gen_info="$sdk_gen_info and CommitSHA: '$(SpecRepoCommit)', in SpecRepo: '${{ parameters.SpecRepoUrl }}'"
               echo "##vso[task.setvariable variable=GeneratedSDKInformation]$sdk_gen_info"
-              echo "Generated SDK Information : $sdk_gen_info"
+              echo "$sdk_gen_info"
 
               cd $(SpecRepoDirectory)
+              echo "##[group]Run npm ci"
               npm ci
+              echo "##[endgroup]"
               node $(SpecRepoDirectory)/eng/tools/spec-gen-sdk-runner/cmd/spec-gen-sdk-runner.js \
                 --scp $(SpecRepoDirectory) \
                 --sdp $(SdkRepoDirectory) \
@@ -189,15 +191,11 @@ extends:
                 $optional_params
             displayName: 'Generate SDK'
 
-          - task: Powershell@2
-            inputs:
-              filePath: $(SdkRepoDirectory)/eng/common/scripts/Save-Package-Properties.ps1
-              arguments: >
-                -ServiceDirectory $(GeneratedSDK.ServiceName)
-                -OutDirectory $(GeneratedSDK.StagedArtifactsFolder)/PackageInfo
-              pwsh: true
-              workingDirectory: $(SdkRepoDirectory)
-            displayName: Dump Package properties
+          - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
+            parameters:
+              ArtifactName: $(BreakingChangeLabelArtifactName)
+              ArtifactPath: "$(System.DefaultWorkingDirectory)/$(BreakingChangeLabelArtifactPath)"
+              CustomCondition: and(succeeded(), ne(variables['BreakingChangeLabelArtifactName'], ''))
 
           - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
             parameters:
@@ -263,10 +261,3 @@ extends:
                       -PRTitle "$(PrTitle)-generated-from-$(Build.DefinitionName)-$(Build.BuildId)"
                       -PRBody "$(GeneratedSDKInformation)"
                       -OpenAsDraft $true
-
-            - ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
-              - template: /eng/common/pipelines/templates/steps/detect-api-changes.yml
-                parameters:
-                  ArtifactPath: $(GeneratedSDK.StagedArtifactsFolder)
-                  ArtifactName: $(PipelineArtifactsName)
-                  RepoRoot: $(SdkRepoDirectory)
diff --git a/eng/tools/spec-gen-sdk-runner/eslint.config.js b/eng/tools/spec-gen-sdk-runner/eslint.config.js
index d7d2b6b9c373..cc13ff9fc4a3 100644
--- a/eng/tools/spec-gen-sdk-runner/eslint.config.js
+++ b/eng/tools/spec-gen-sdk-runner/eslint.config.js
@@ -73,6 +73,8 @@ const config = tseslint.config(
       "@typescript-eslint/consistent-type-definitions": "off",
       "@typescript-eslint/no-inferrable-types": "off",
       "@typescript-eslint/no-dynamic-delete": "off",
+      "@typescript-eslint/no-unsafe-member-access": "off",
+      "@typescript-eslint/no-explicit-any": "off",
 
       // We want more flexibility with file names.
       // https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/filename-case.md
diff --git a/eng/tools/spec-gen-sdk-runner/src/commands.ts b/eng/tools/spec-gen-sdk-runner/src/commands.ts
index b950f9870c10..16a111b2adfc 100644
--- a/eng/tools/spec-gen-sdk-runner/src/commands.ts
+++ b/eng/tools/spec-gen-sdk-runner/src/commands.ts
@@ -7,45 +7,50 @@ import {
   runSpecGenSdkCommand,
   getAllTypeSpecPaths,
   resetGitRepo,
+  objectToMap,
 } from "./utils.js";
-import { LogLevel, logMessage, vsoAddAttachment, vsoLogIssue } from "./log.js";
-import { CommandLog, SpecGenSdkCmdInput } from "./types.js";
+import { LogLevel, logMessage, setVsoVariable, vsoAddAttachment, vsoLogIssue } from "./log.js";
+import { SpecGenSdkCmdInput, VsoLogs } from "./types.js";
 import { detectChangedSpecConfigFiles } from "./change-files.js";
 
 export async function generateSdkForSingleSpec(): Promise<number> {
   // Parse the arguments
   const commandInput: SpecGenSdkCmdInput = parseArguments();
-  const specConfigPath = `${commandInput.tspConfigPath} ${commandInput.readmePath}`;
+  const specConfigPathText = `${commandInput.tspConfigPath} ${commandInput.readmePath}`;
+
   // Construct the spec-gen-sdk command
   const specGenSdkCommand = prepareSpecGenSdkCommand(commandInput);
-  logMessage(`Generating SDK from ${specConfigPath}`, LogLevel.Group);
-  logMessage(`Command:${specGenSdkCommand.join(" ")}`);
+  logMessage(`Generating SDK from ${specConfigPathText}`, LogLevel.Group);
+  logMessage(`Runner command:${specGenSdkCommand.join(" ")}`);
   let statusCode = 0;
   try {
     await runSpecGenSdkCommand(specGenSdkCommand);
-    logMessage("Command executed successfully");
+    logMessage("Runner command executed successfully");
   } catch (error) {
-    logMessage(`Error executing command:${error}`, LogLevel.Error);
+    logMessage(`Runner: error executing command:${error}`, LogLevel.Error);
     statusCode = 1;
   }
+
   // Read the execution report to determine if the generation was successful
   const executionReportPath = path.join(
     commandInput.workingFolder,
     `${commandInput.sdkRepoName}_tmp/execution-report.json`,
   );
+  let executionReport;
   try {
-    const executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+    executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
     const executionResult = executionReport.executionResult;
-    logMessage(`Execution Result:${executionResult}`);
+    logMessage(`Runner command execution result:${executionResult}`);
   } catch (error) {
-    logMessage(`Error reading execution report at ${executionReportPath}:${error}`, LogLevel.Error);
+    logMessage(
+      `Runner: error reading execution report at ${executionReportPath}:${error}`,
+      LogLevel.Error,
+    );
     statusCode = 1;
   }
   logMessage("ending group logging", LogLevel.EndGroup);
+  logIssuesToPipeline(executionReport.vsoLogPath, specConfigPathText);
 
-  logIssuesToPipeline(commandInput, specConfigPath);
-  
   return statusCode;
 }
 
@@ -60,9 +65,12 @@ export async function generateSdkForSpecPr(): Promise<number> {
 
   let statusCode = 0;
   let pushedSpecConfigCount;
+  let shouldLabelBreakingChange = false;
+  let breakingChangeLabel = "";
+  let executionReport;
   for (const changedSpec of changedSpecs) {
     if (!changedSpec.typespecProject && !changedSpec.readmeMd) {
-      logMessage("No spec config file found in the changed files", LogLevel.Warn);
+      logMessage("Runner: no spec config file found in the changed files", LogLevel.Warn);
       continue;
     }
     pushedSpecConfigCount = 0;
@@ -74,16 +82,16 @@ export async function generateSdkForSpecPr(): Promise<number> {
       specGenSdkCommand.push("--readme-relative-path", changedSpec.readmeMd);
       pushedSpecConfigCount++;
     }
-    const changedSpecPath = `${changedSpec.typespecProject} ${changedSpec.readmeMd}`;
-    logMessage(`Generating SDK from ${changedSpecPath}`, LogLevel.Group);
-    logMessage(`Command:${specGenSdkCommand.join(" ")}`);
+    const changedSpecPathText = `${changedSpec.typespecProject} ${changedSpec.readmeMd}`;
+    logMessage(`Generating SDK from ${changedSpecPathText}`, LogLevel.Group);
+    logMessage(`Runner command:${specGenSdkCommand.join(" ")}`);
 
     try {
       await resetGitRepo(commandInput.localSdkRepoPath);
       await runSpecGenSdkCommand(specGenSdkCommand);
-      logMessage("Command executed successfully");
+      logMessage("Runner command executed successfully");
     } catch (error) {
-      logMessage(`Error executing command:${error}`, LogLevel.Error);
+      logMessage(`Runner: error executing command:${error}`, LogLevel.Error);
       statusCode = 1;
     }
     // Pop the spec config path from specGenSdkCommand
@@ -95,22 +103,28 @@ export async function generateSdkForSpecPr(): Promise<number> {
       commandInput.workingFolder,
       `${commandInput.sdkRepoName}_tmp/execution-report.json`,
     );
+
     try {
-      const executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+      executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
       const executionResult = executionReport.executionResult;
-      logMessage(`Execution Result:${executionResult}`);
+      [shouldLabelBreakingChange, breakingChangeLabel] = getBreakingChangeInfo(executionReport);
+      logMessage(`Runner command execution result:${executionResult}`);
     } catch (error) {
       logMessage(
-        `Error reading execution report at ${executionReportPath}:${error}`,
+        `Runner: error reading execution report at ${executionReportPath}:${error}`,
         LogLevel.Error,
       );
       statusCode = 1;
     }
     logMessage("ending group logging", LogLevel.EndGroup);
-
-    logIssuesToPipeline(commandInput, changedSpecPath);
+    logIssuesToPipeline(executionReport.vsoLogPath, changedSpecPathText);
   }
+  // Process the breaking change label artifacts
+  statusCode = processBreakingChangeLabelArtifacts(
+    commandInput,
+    shouldLabelBreakingChange,
+    breakingChangeLabel,
+  );
   return statusCode;
 }
 
@@ -134,22 +148,24 @@ export async function generateSdkForBatchSpecs(runMode: string): Promise<number>
   let failedCount = 0;
   let notEnabledCount = 0;
   let succeededCount = 0;
+  let executionReport;
 
   // Generate SDKs for each spec
   for (const specConfigPath of specConfigPaths) {
     logMessage(`Generating SDK from ${specConfigPath}`, LogLevel.Group);
+
     if (specConfigPath.endsWith("tspconfig.yaml")) {
       specGenSdkCommand.push("--tsp-config-relative-path", specConfigPath);
     } else {
       specGenSdkCommand.push("--readme-relative-path", specConfigPath);
     }
-    logMessage(`Command:${specGenSdkCommand.join(" ")}`);
+    logMessage(`Runner command:${specGenSdkCommand.join(" ")}`);
     try {
       await resetGitRepo(commandInput.localSdkRepoPath);
       await runSpecGenSdkCommand(specGenSdkCommand);
-      logMessage("Command executed successfully");
+      logMessage("Runner command executed successfully");
     } catch (error) {
-      logMessage(`Error executing command:${error}`, LogLevel.Error);
+      logMessage(`Runner: error executing command:${error}`, LogLevel.Error);
       statusCode = 1;
     }
 
@@ -162,10 +178,9 @@ export async function generateSdkForBatchSpecs(runMode: string): Promise<number>
       `${commandInput.sdkRepoName}_tmp/execution-report.json`,
     );
     try {
-      const executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
-      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+      executionReport = JSON.parse(fs.readFileSync(executionReportPath, "utf8"));
       const executionResult = executionReport.executionResult;
-      logMessage(`Execution Result:${executionResult}`);
+      logMessage(`Runner: command execution result:${executionResult}`);
 
       if (executionResult === "succeeded" || executionResult === "warning") {
         succeededContent += `${specConfigPath},`;
@@ -179,14 +194,13 @@ export async function generateSdkForBatchSpecs(runMode: string): Promise<number>
       }
     } catch (error) {
       logMessage(
-        `Error reading execution report at ${executionReportPath}:${error}`,
+        `Runner: error reading execution report at ${executionReportPath}:${error}`,
         LogLevel.Error,
       );
       statusCode = 1;
     }
     logMessage("ending group logging", LogLevel.EndGroup);
-    
-    logIssuesToPipeline(commandInput, specConfigPath || '');
+    logIssuesToPipeline(executionReport.vsoLogPath, specConfigPath);
   }
   if (failedCount > 0) {
     markdownContent += `${failedContent}\n`;
@@ -211,10 +225,10 @@ export async function generateSdkForBatchSpecs(runMode: string): Promise<number>
       fs.rmSync(markdownFilePath);
     }
     fs.writeFileSync(markdownFilePath, markdownContent);
-    logMessage(`Markdown file written to ${markdownFilePath}`);
+    logMessage(`Runner: markdown file written to ${markdownFilePath}`);
     vsoAddAttachment("Generation Summary", markdownFilePath);
   } catch (error) {
-    logMessage(`Error writing markdown file ${markdownFilePath}:${error}`, LogLevel.Error);
+    vsoLogIssue(`Runner: error writing markdown file ${markdownFilePath}:${error}`);
     statusCode = 1;
   }
   return statusCode;
@@ -334,55 +348,99 @@ function getSpecPaths(runMode: string, specRepoPath: string): string[] {
 }
 
 /**
- * Extract and format the prefix from tspConfigPath or readmePath.
- * This function is copied from 'spec-gen-sdk'.
- * Source code: [Azure SDK Tools - spec-gen-sdk](https://github.com/Azure/azure-sdk-tools/blob/main/tools/spec-gen-sdk/src/utils/utils.ts#L171)
- * @param {string | undefined} tspConfigPath The tspConfigPath to extract the prefix from.
- * @param {string | undefined} readmePath The readmePath to extract the prefix from.
- * @returns {string} The formatted prefix.
+ * Logs issues to Azure DevOps Pipeline *
+ * @param logPath - The vso log file path.
+ * @param specConfigDisplayText - The display text for the spec configuration.
  */
-function extractPathFromSpecConfig(tspConfigPath: string | undefined, readmePath: string | undefined): string {
-  let prefix = '';
-  if (tspConfigPath) {
-    const regex = /specification\/(.+)\/tspconfig\.yaml$/
-    const match = regex.exec(tspConfigPath);
-    if (match) {
-      const segments = match[1].split('/');
-      prefix = segments.join('-').toLowerCase().replaceAll('.', '-');
+function logIssuesToPipeline(logPath: string, specConfigDisplayText: string): void {
+  let vsoLogs: VsoLogs;
+  try {
+    const logContent = JSON.parse(fs.readFileSync(logPath, "utf8"));
+    vsoLogs = objectToMap(logContent);
+  } catch (error) {
+    throw new Error(`Runner: error reading log at ${logPath}:${error}`);
+  }
+
+  if (vsoLogs) {
+    const errors = [...vsoLogs.values()].flatMap((entry) => entry.errors ?? []);
+    const warnings = [...vsoLogs.values()].flatMap((entry) => entry.warnings ?? []);
+    if (errors.length > 0) {
+      const errorTitle = `Errors occurred while generating SDK from ${specConfigDisplayText}`;
+      logMessage(errorTitle, LogLevel.Group);
+      const errorsWithTitle = [errorTitle, ...errors];
+      vsoLogIssue(errorsWithTitle.join("%0D%0A"));
+      logMessage("ending group logging", LogLevel.EndGroup);
     }
-  } else if (readmePath) {
-    const regex = /specification\/(.+?)\/readme\.md$/i
-    const match = regex.exec(readmePath);
-    if (match) {
-      const segments = match[1].split('/');
-      prefix = segments.join('-').toLowerCase().replaceAll('.', '-');
+    if (warnings.length > 0) {
+      const warningTitle = `Warnings occurred while generating SDK from ${specConfigDisplayText}`;
+      logMessage(warningTitle, LogLevel.Group);
+      const warningsWithTitle = [warningTitle, ...warnings];
+      vsoLogIssue(warningsWithTitle.join("%0D%0A"));
+      logMessage("ending group logging", LogLevel.EndGroup);
     }
   }
-  return prefix;
 }
 
 /**
- * Logs SDK generation issues to Azure DevOps (ADO)
- * @param commandInput 
- * @param specConfigPath 
+ * Process the breaking change label artifacts.
+ *
+ * @param executionReport - The spec-gen-sdk execution report.
+ * @returns [flag of lable breaking change, breaking change label].
  */
-function logIssuesToPipeline(commandInput: SpecGenSdkCmdInput, specConfigPath: string): void {
-  const fileNamePrefix = extractPathFromSpecConfig(commandInput.tspConfigPath, commandInput.readmePath)
-  const logFolder = path.join(commandInput.workingFolder, 'out/logs');
-  const logPath = path.join(logFolder, `${fileNamePrefix}-filtered.log`);
-  let logIssues:CommandLog[] = []
-  try {
-    const log = JSON.parse(fs.readFileSync(logPath, "utf8"));
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
-    logIssues = log.logIssues;
-  } catch (error) {
-    logMessage(`Error reading log at ${logPath}:${error}`, LogLevel.Error);
+function getBreakingChangeInfo(executionReport: any): [boolean, string] {
+  let breakingChangeLabel = "";
+  for (const packageInfo of executionReport.packages) {
+    breakingChangeLabel = packageInfo.breakingChangeLabel;
+    if (packageInfo.shouldLabelBreakingChange) {
+      return [true, breakingChangeLabel];
+    }
   }
+  return [false, breakingChangeLabel];
+}
 
-  if(logIssues.length > 0) {
-    logMessage(`Error generating SDK from ${specConfigPath}`, LogLevel.Group);
-    const logIssuesList = logIssues.flatMap(entry => [`Get log issues from script ${entry.command} `, ...entry.logIssues]);;
-    vsoLogIssue(logIssuesList.join('%0D%0A'));
+/**
+ * Process the breaking change label artifacts.
+ * @param commandInput - The command input.
+ * @param shouldLabelBreakingChange - A flag indicating whether to label breaking changes.
+ * @returns the run status code.
+ */
+function processBreakingChangeLabelArtifacts(
+  commandInput: SpecGenSdkCmdInput,
+  shouldLabelBreakingChange: boolean,
+  breakingChangeLabel: string,
+): number {
+  const breakingChangeLabelArtifactName = "spec-gen-sdk-breaking-change-artifact";
+  const breakingChangeLabelArtifactFileName = breakingChangeLabelArtifactName + ".json";
+  const breakingChangeLabelArtifactPath = "out/breaking-change-label-artifact";
+  const breakingChangeLabelArtifactAbsoluteFolder = path.join(
+    commandInput.workingFolder,
+    breakingChangeLabelArtifactPath,
+  );
+  try {
+    if (!fs.existsSync(breakingChangeLabelArtifactAbsoluteFolder)) {
+      fs.mkdirSync(breakingChangeLabelArtifactAbsoluteFolder, { recursive: true });
+    }
+    // Write breaking change label artifact
+    fs.writeFileSync(
+      path.join(
+        commandInput.workingFolder,
+        breakingChangeLabelArtifactPath,
+        breakingChangeLabelArtifactFileName,
+      ),
+      JSON.stringify({
+        language: commandInput.sdkRepoName,
+        labelAction: shouldLabelBreakingChange,
+      }),
+    );
+    setVsoVariable("BreakingChangeLabelArtifactName", breakingChangeLabelArtifactName);
+    setVsoVariable("BreakingChangeLabelArtifactPath", breakingChangeLabelArtifactPath);
+    setVsoVariable("BreakingChangeLabelAction", shouldLabelBreakingChange ? "add" : "remove");
+    setVsoVariable("BreakingChangeLabel", breakingChangeLabel);
+  } catch (error) {
+    logMessage("Runner: errors occurred while processing breaking change", LogLevel.Group);
+    vsoLogIssue(`Runner: errors writing breaking change label artifacts:${error}`);
     logMessage("ending group logging", LogLevel.EndGroup);
+    return 1;
   }
-}
\ No newline at end of file
+  return 0;
+}
diff --git a/eng/tools/spec-gen-sdk-runner/src/log.ts b/eng/tools/spec-gen-sdk-runner/src/log.ts
index 4bc1b30582b9..dc46d1fff2eb 100644
--- a/eng/tools/spec-gen-sdk-runner/src/log.ts
+++ b/eng/tools/spec-gen-sdk-runner/src/log.ts
@@ -30,6 +30,10 @@ export function logMessage(message: string, level?: LogLevel): void {
       console.error(message);
       break;
     }
+    case LogLevel.Warn: {
+      console.warn(message);
+      break;
+    }
     default: {
       console.log(message);
       break;
@@ -44,3 +48,9 @@ export function vsoAddAttachment(name: string, path: string): void {
 export function vsoLogIssue(message: string, type = "error"): void {
   console.log(`##vso[task.logissue type=${type}]${message}`);
 }
+
+export function setVsoVariable(variable: string, value: string, isOutput = false): void {
+  console.log(
+    `##vso[task.setVariable variable=${variable}${isOutput ? ";isoutput=true" : ""}]${value}`,
+  );
+}
diff --git a/eng/tools/spec-gen-sdk-runner/src/types.ts b/eng/tools/spec-gen-sdk-runner/src/types.ts
index 3cc2270e9bb4..c22f4f53c6a3 100644
--- a/eng/tools/spec-gen-sdk-runner/src/types.ts
+++ b/eng/tools/spec-gen-sdk-runner/src/types.ts
@@ -17,7 +17,13 @@ export interface SpecGenSdkCmdInput {
   headBranch?: string;
 }
 
-export type CommandLog = {
-  command: string;
-  logIssues: string[];
-};
\ No newline at end of file
+/*
+ * VsoLogs is a map of task names to log entries. Each log entry contains an array of errors and warnings.
+ */
+export type VsoLogs = Map<
+  string,
+  {
+    errors?: string[];
+    warnings?: string[];
+  }
+>;
diff --git a/eng/tools/spec-gen-sdk-runner/src/utils.ts b/eng/tools/spec-gen-sdk-runner/src/utils.ts
index d486fb9e78e0..b459d613dff1 100644
--- a/eng/tools/spec-gen-sdk-runner/src/utils.ts
+++ b/eng/tools/spec-gen-sdk-runner/src/utils.ts
@@ -422,3 +422,25 @@ export function createCombinedSpecs(
     typespecProject: path.join(tsPath, "tspconfig.yaml"),
   }));
 }
+
+/**
+ * Converts a Map<string, T> to a plain object for JSON serialization.
+ */
+export function mapToObject<T>(map: Map<string, T>): Record<string, T> {
+  const obj: Record<string, T> = {};
+  for (const [key, value] of map.entries()) {
+    obj[key] = value;
+  }
+  return obj;
+}
+
+/**
+ * Converts a plain object (the result of JSON.parse) back into a Map<string, T>.
+ */
+export function objectToMap<T>(obj: Record<string, T>): Map<string, T> {
+  const map = new Map<string, T>();
+  for (const [key, value] of Object.entries(obj)) {
+    map.set(key, value);
+  }
+  return map;
+}
diff --git a/package-lock.json b/package-lock.json
index 8c2e64b4cbac..d13693ba3a67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,7 +7,7 @@
       "name": "azure-rest-api-specs",
       "devDependencies": {
         "@autorest/openapi-to-typespec": "0.10.13",
-        "@azure-tools/spec-gen-sdk": "0.2.2",
+        "@azure-tools/spec-gen-sdk": "^0.3.0",
         "@azure-tools/typespec-apiview": "0.5.1",
         "@azure-tools/typespec-autorest": "0.53.0",
         "@azure-tools/typespec-azure-core": "0.53.0",
@@ -848,9 +848,9 @@
       "link": true
     },
     "node_modules/@azure-tools/spec-gen-sdk": {
-      "version": "0.2.2",
-      "resolved": "https://registry.npmjs.org/@azure-tools/spec-gen-sdk/-/spec-gen-sdk-0.2.2.tgz",
-      "integrity": "sha512-GP/NLAKHyUs67/50Iskirtaui46HcgqykDKg87pajJ0ggpOS6FlfdrE0YKTfXHoz+SqU/CtNDZGFI+txM2I21g==",
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/@azure-tools/spec-gen-sdk/-/spec-gen-sdk-0.3.0.tgz",
+      "integrity": "sha512-g/UiXY+ChSdSDMdYcgijtAmc0+c0hGoY4LbZsodH7d61HcMcdO8smVYXdIIC3fIXevW65cmaEQGDfM5dw3w1ZA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 0aef20cff25d..08a27e231837 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
 {
   "name": "azure-rest-api-specs",
   "devDependencies": {
+    "@azure-tools/spec-gen-sdk": "^0.3.0",
     "@azure-tools/typespec-apiview": "0.5.1",
     "@azure-tools/typespec-autorest": "0.53.0",
     "@azure-tools/typespec-azure-core": "0.53.0",
@@ -10,9 +11,8 @@
     "@azure-tools/typespec-client-generator-cli": "0.15.3",
     "@azure-tools/typespec-client-generator-core": "0.53.0",
     "@azure-tools/typespec-liftr-base": "0.8.0",
-    "@azure-tools/spec-gen-sdk": "0.2.2",
-    "@azure/avocado": "^0.9.1",
     "@autorest/openapi-to-typespec": "0.10.13",
+    "@azure/avocado": "^0.9.1",
     "@typespec/compiler": "0.67.1",
     "@typespec/http": "0.67.1",
     "@typespec/sse": "0.67.1",


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:56:28Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces a new workflow for SDK breaking change labeling with enhancements to event handling, SDK label management, and SDK generation improvements. Key changes include:
- Implementation of a new workflow and script (sdk-breaking-change-labels.js) to process check_run events and determine label actions.
- Updates to workflow configurations (YAML files) to include the new SDK Breaking Change Labels workflow and adjust upstream workflows.
- Modifications in SDK generation pipelines and logging to support the new label artifacts.

### Reviewed Changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| .github/workflows/src/sdk-breaking-change-labels.js | New script to fetch check run data and trigger label actions based on artifacts |
| .github/workflows/sdk-breaking-change-labels.yaml | Configuration for the new SDK breaking change labels workflow |
| .github/src/sdk-types.js | Mapping definitions for SDK label types |
| .github/workflows/src/update-labels.js | Added a check to skip execution when issue_number or run_id are invalid |
| eng/tools/spec-gen-sdk-runner/src/log.ts | Updated logging functions to include setting of VSO variables |
| .github/workflows/update-labels.yaml | Updated upstream workflows list to include the new label workflow |
| eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml | Pipeline changes to conditionally publish breaking change label artifacts |
| eng/tools/spec-gen-sdk-runner/src/commands.ts | Adjusted command generation and logging to reflect additional artifact information |
| .github/src/context.js | Enhancement of context extraction to support check_run events and proper initialization of issue_number |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-12T16:57:07Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:26:51Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:28:25Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:33:04Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:34:30Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:38:25Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:40:32Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:41:56Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:42:12Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:48:20Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T17:53:10Z
user: login: mikeharder
body: See comments
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-12T18:00:45Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T21:50:56Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T21:55:34Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T21:58:15Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-12T22:02:07Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T00:17:00Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T00:17:54Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:31:26Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:37:15Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:39:39Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:40:17Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:47:18Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:47:57Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:51:18Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:53:04Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:53:54Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:57:45Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:59:53Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T21:09:14Z
user: login: mikeharder
body: see comments
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-13T21:10:16Z
user: login: weshaggard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T22:27:29Z
user: login: weshaggard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T22:29:07Z
user: login: weshaggard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T22:29:55Z
user: login: weshaggard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T22:30:13Z
user: login: weshaggard
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T22:31:57Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:06:37Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:07:25Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:14:51Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:16:09Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:17:30Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:33:41Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:34:27Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T15:35:23Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T16:10:30Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T17:16:35Z
user: login: mikeharder
body: Please fix linting issues first:

https://github.com/Azure/azure-rest-api-specs/actions/runs/13929427110/job/38982515798?pr=33158#step:4:16
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-18T17:24:53Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:02:44Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:07:55Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:08:45Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:10:47Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:27:49Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:28:47Z
user: login: mikeharder
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T18:29:13Z
user: login: mikeharder
body: Approved pending fix to package.json
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-20T18:45:23Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:18:24Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:21:29Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:22:34Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:23:50Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:24:06Z
user: login: raych1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T20:26:59Z

Review comments:
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
+jobs:
+  process-check-run:
+    name: SDK Breaking Change Labels
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            .github
+      
+      - name: Setup Node and install deps
+        uses: ./.github/actions/setup-node-install-deps
+
+      - name: Process Check Run Event
+        id: get-label-and-action
+        uses: actions/github-script@v7
+        with:
+          script: |
+            const { getLabelAndAction } =
+              await import('${{ github.workspace }}/.github/workflows/src/sdk-breaking-change-labels.js');
+            return await getLabelAndAction({ github, context, core });
+        env:
+          CHECK_RUN_ID: ${{ inputs.check_run_id }}
+
+      - if: |
+          (fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' ||
+          fromJson(steps.get-label-and-action.outputs.result).labelAction == 'remove')
+        name: Upload artifact with results
+        uses: ./.github/actions/add-label-artifact
+        with:
+          name: "${{ fromJson(steps.get-label-and-action.outputs.result).labelName}}"
+          # Convert "add/remove" to "true/false"
+          value: "${{ fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' }}"
+
+      - if: |
+          (fromJson(steps.get-label-and-action.outputs.result).labelAction == 'add' ||
+          fromJson(steps.get-label-and-action.outputs.result).labelAction == 'remove')
+        name: Upload artifact with issue number
+        uses: ./.github/actions/add-empty-artifact
+        with:
+          name: "issue-number"
+          value: "${{ fromJson(steps.get-label-and-action.outputs.result).issueNumber }}"
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: github-advanced-security[bot]
body: ## Workflow does not contain permissions

Actions Job or Workflow does not set permissions

[Show more details](https://github.com/Azure/azure-rest-api-specs/security/code-scanning/11)
created_at: 2025-03-12T16:56:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -20,6 +20,12 @@ export default async function updateLabels({ github, context, core }) {
     run_id = run_id || inputs.run_id;
   }
 
+  if (Number.isNaN(issue_number) || Number.isNaN(run_id)) {
+    core.info(`skipping run because issue_number or run_id is not a valid number. issue_numer: ${issue_number}, run_id: ${run_id}`);
path: .github/workflows/src/update-labels.js
user: login: Copilot
body: Typo detected in the log message: 'issue_numer' should be corrected to 'issue_number' for clarity.
```suggestion
    core.info(`skipping run because issue_number or run_id is not a valid number. issue_number: ${issue_number}, run_id: ${run_id}`);
```
created_at: 2025-03-12T16:57:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: Do you need to trigger on check_run:completed, or would check_suite:completed be sufficient?
created_at: 2025-03-12T17:26:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
```


created_at: 2025-03-12T17:28:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
permissions:
  contents: read
```

Start with these minimal permissions, and only add more if needed.
created_at: 2025-03-12T17:33:04Z
author_association: MEMBER
diff_hunk: @@ -96,7 +96,7 @@ export async function extractInputs(github, context, core) {
         context.payload
       );
 
-    let issue_number;
+    let issue_number = NaN;
path: .github/src/context.js
user: login: mikeharder
body: ```suggestion
    let issue_number;
```

Better to require each "if" block to set this explicitly to prevent errors.
created_at: 2025-03-12T17:34:30Z
author_association: MEMBER
diff_hunk: @@ -191,12 +192,6 @@ export async function extractInputs(github, context, core) {
           }
         }
       }
path: .github/src/context.js
user: login: mikeharder
body: ```suggestion
      }
      if (!issue_number) {
        core.info(`Could not find 'issue-number' artifact, which is required to associate the triggering workflow run with a PR`);
        issue_number = NaN;
      }
```
created_at: 2025-03-12T17:38:24Z
author_association: MEMBER
path: .github/src/sdk-types.js
user: login: mikeharder
body: Is there a benefit, either currently or planned, of defining these types in a separate file under .github/src?  If not, please move them into the existing JS file for the workflow.
created_at: 2025-03-12T17:40:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
+jobs:
+  process-check-run:
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
  sdk-breaking-change-labels:
```
created_at: 2025-03-12T17:41:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
+jobs:
+  process-check-run:
+    name: SDK Breaking Change Labels
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
    name: SDK Breaking Change Labels (Preview)
```
created_at: 2025-03-12T17:42:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
+jobs:
+  process-check-run:
+    name: SDK Breaking Change Labels
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            .github
+      
+      - name: Setup Node and install deps
+        uses: ./.github/actions/setup-node-install-deps
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: Do you actually need to run "npm ci" at the repo root, which this command does?  This is very slow so it should only be done when needed.  Looking at the JS code for your workflow, I don't believe you need this.
created_at: 2025-03-12T17:48:20Z
author_association: MEMBER
diff_hunk: @@ -20,6 +20,12 @@ export default async function updateLabels({ github, context, core }) {
     run_id = run_id || inputs.run_id;
   }
 
+  if (Number.isNaN(issue_number) || Number.isNaN(run_id)) {
+    core.info(`skipping run because issue_number or run_id is not a valid number. issue_numer: ${issue_number}, run_id: ${run_id}`);
+    return;
+  }
+  const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${issue_number}`;
+  core.info(`pull request url: ${pullRequestUrl}`);
path: .github/workflows/src/update-labels.js
user: login: mikeharder
body: ```suggestion
```

I think all this code can be deleted.  The decision on how to handle undefinded issue_number should be deferred as late as possible, until we know if it's necessary or not.
created_at: 2025-03-12T17:53:09Z
author_association: MEMBER
diff_hunk: @@ -20,6 +20,12 @@ export default async function updateLabels({ github, context, core }) {
     run_id = run_id || inputs.run_id;
   }
 
+  if (Number.isNaN(issue_number) || Number.isNaN(run_id)) {
+    core.info(`skipping run because issue_number or run_id is not a valid number. issue_numer: ${issue_number}, run_id: ${run_id}`);
+    return;
+  }
+  const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${issue_number}`;
+  core.info(`pull request url: ${pullRequestUrl}`);
path: .github/workflows/src/update-labels.js
user: login: raych1
body: Why not return as early as possible?
created_at: 2025-03-12T21:50:56Z
author_association: MEMBER
path: .github/src/sdk-types.js
user: login: raych1
body: This code will replace the duplicate one at [sdk-suppression code](https://github.com/Azure/azure-rest-api-specs/blob/main/eng/tools/sdk-suppressions/src/sdk.ts)
created_at: 2025-03-12T21:55:34Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: raych1
body: removed
created_at: 2025-03-12T21:58:15Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: raych1
body: As there are a few check runs in the 'Azure Pipeline' suite, I don't want the triggering needs to wait until all the check runs complete.
created_at: 2025-03-12T22:02:07Z
author_association: MEMBER
diff_hunk: @@ -20,6 +20,12 @@ export default async function updateLabels({ github, context, core }) {
     run_id = run_id || inputs.run_id;
   }
 
+  if (Number.isNaN(issue_number) || Number.isNaN(run_id)) {
+    core.info(`skipping run because issue_number or run_id is not a valid number. issue_numer: ${issue_number}, run_id: ${run_id}`);
+    return;
+  }
+  const pullRequestUrl = `https://github.com/${owner}/${repo}/pull/${issue_number}`;
+  core.info(`pull request url: ${pullRequestUrl}`);
path: .github/workflows/src/update-labels.js
user: login: raych1
body: Reported error in the later steps.
created_at: 2025-03-13T00:17:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,55 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        default: '38335408598'
+        type: string
+
+jobs:
+  process-check-run:
+    name: SDK Breaking Change Labels
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            .github
+      
+      - name: Setup Node and install deps
+        uses: ./.github/actions/setup-node-install-deps
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: raych1
body: I thought 'node settings' need it. I removed this step.
created_at: 2025-03-13T00:17:54Z
author_association: MEMBER
diff_hunk: @@ -103,6 +97,13 @@ export async function updateLabelsImpl({
   core.info(`labelsToAdd: ${JSON.stringify(labelsToAdd)}`);
   core.info(`labelsToRemove: ${JSON.stringify(labelsToRemove)}`);
 
+  if ((labelsToAdd.length > 0 || labelsToRemove.length > 0) && Number.isNaN(issue_number)) {
path: .github/workflows/src/update-labels.js
user: login: mikeharder
body: ```suggestion
  if ((labelsToAdd.length > 0 || labelsToRemove.length > 0) && !issue_number) {
```

Convention in JS is to use `!foo` to determine if a value is "falsy".  This includes things like `""`, `NaN`, `0`, `null`, and `undefined`.  Typically, you want to treat these all like "false", rather than specifically checking for `NaN`.
created_at: 2025-03-13T20:31:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+// @ts-check
+
+/**
+ * @typedef {'azure-sdk-for-go' | 'azure-sdk-for-java' | 'azure-sdk-for-js' | 'azure-sdk-for-net' | 'azure-sdk-for-python'} SdkName
+ */
+
+/**
+ * SDK labels mapping for breaking change labels
+ * @type {Object.<SdkName, {
+ *   breakingChange: string | undefined,
+ *   breakingChangeApproved: string | undefined,
+ *   breakingChangeSuppression: string | undefined,
+ *   breakingChangeSuppressionApproved: string | undefined
path: .github/src/sdk-types.js
user: login: mikeharder
body: ```suggestion
 *   breakingChange: string,
 *   breakingChangeApproved: string,
 *   breakingChangeSuppression: string,
 *   breakingChangeSuppressionApproved: string
```

Do these need to support undefined?  All keys appear to be defined in all objects below.
created_at: 2025-03-13T20:37:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        type: string
+
+permissions:
+  contents: read
+
+jobs:
+  sdk-breaking-change-labels:
+    name: SDK Breaking Change Labels (Preview)
+    runs-on: ubuntu-24.04
+    steps:
+      - uses: actions/checkout@v4
+        with:
+          sparse-checkout: |
+            .github
+      
+      - name: Setup Node and install deps
+        uses: ./.github/actions/setup-node-install-deps
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
```

You left a comment that you removed this, but it's still here.
created_at: 2025-03-13T20:39:39Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
+            method: 'GET',
+            headers: {
+              'Content-Type': 'application/json'
+            }
+          });
+          
+          if (response.ok) {
+            const artifacts = await response.json();
+            core.info(`Artifacts found: ${artifacts.count}`);
+            
+            // Process artifacts as needed
+            let addLabel = false;
+            let removeLabel = false;
+            let language = "";
+            for (const artifact of artifacts.value) {
+
+              // label artifact name example: 'spec-gen-sdk-azure-sdk-for-js-true'
+              if (artifact.name.startsWith("spec-gen-sdk_")) {
+                core.info(`Artifact: ${artifact.name}`);
+                if(artifact.name.endsWith("true")) {
+                  addLabel = true;
+                  language = artifact.name.split("_")[1];
+                } else if(artifact.name.endsWith("false")) {
+                  removeLabel = true;
+                  language = artifact.name.split("_")[1];
+                }
+              }
+            }
+
+            // Set label action and name based on the artifacts
+            if (addLabel) {
+              labelAction = LabelAction.Add;
+            } else if (removeLabel) {
+              labelAction = LabelAction.Remove;
+            }
+            if (language) {
+              labelName = sdkLabels[`${language}`].breakingChange;
+            }
+          } else {
+            core.error(`Failed to fetch artifacts: ${response.status}, ${response.statusText}`);
+            const errorText = await response.text();
+            core.error(`Error details: ${errorText}`);
+          }
+        } catch (apiError) {
+          core.error(`Error calling Azure DevOps API: ${apiError}`);
+        }
+      } else {
+        core.error(`Could not extract organization and project from URL: ${checkRun.details_url}`);
+      }
+    } else {
+      core.error(`Could not extract buildId from details_url: ${checkRun.details_url}`);
+    }
+  } else {
+    core.info('No details_url found in check run');
+  }
+  // Get the issue number from the check run
+  if (Number.isNaN(issue_number)) {
+    const { issueNumber } = await getIssueNumber({checkRun, core, github});
+    issue_number = issueNumber;
+  }
+
+  if (!labelAction) {
+    core.info('No label action found, defaulting to None');
+    labelAction = LabelAction.None;
+  }  
+  
+  return { labelName, labelAction, issueNumber: issue_number, state: labelAction !== LabelAction.None ? 'success' : 'neutral' };  
+}
+
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {import('github').CheckRun} params.checkRun - The check run object containing head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import('github-script').GitHub} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+async function getIssueNumber({checkRun, core, github})
+{
+  let issueNumber = NaN;
+
+  if (!checkRun.head_sha) {
+    core.info('No head_sha found in check run');
+    return { issueNumber };
+  }
+
+  core.info(`Searching for PRs with commit SHA: ${checkRun.head_sha}`);
+
+  try {
+    const searchResponse = await github.rest.search.issuesAndPullRequests({
+      q: `sha:${checkRun.head_sha} type:pr state:open`
+    });
+
+    const totalCount = searchResponse.data.total_count;
+    const itemsCount = searchResponse.data.items.length;
+
+    core.info(`Search results: ${totalCount} total matches, ${itemsCount} items returned`);
+
+    if (itemsCount > 0) {
+      const firstItem = searchResponse.data.items[0];
+      issueNumber = firstItem.number;
+      core.info(`Found the first matched PR #${issueNumber}: ${firstItem.html_url}`);
+
+      if (itemsCount > 1) {
+        core.warning(`Multiple PRs found for commit ${checkRun.head_sha}: ${searchResponse.data.items.map(item => `#${item.html_url}`).join(', ')}`);
+      }
+    } else {
+      core.info(`No open PRs found for commit ${checkRun.head_sha}`);
+    }
+  } catch (error) {
+    core.error(`${wfName}: Error searching for PRs with commit ${checkRun.head_sha}: ${error.message}`);
+    throw error;
+  }
+
+  return { issueNumber };
+}
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: Add trailing newline.  Your IDE might be removing this.
created_at: 2025-03-13T20:40:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: Please use the pattern defined here for extracting inputs:

https://github.com/Azure/azure-rest-api-specs/blob/main/.github/workflows/src/arm-auto-signoff-preview.js

Top-level function called by the workflow YAML, that extracts inputs and calls a helper method to do the actual work.  This makes our workflows more consistent, and allows you to test the business logic separately from the logic to extract the inputs.

Thus, I think a lot of the code below should be moved into context.js.  Any workflow that needs to trigger on a DevOps pipeline and extract inputs from it will need similar code, right?
created_at: 2025-03-13T20:47:17Z
author_association: MEMBER
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: Add unit tests to achieve 100% code coverage.  If a particular block of code is hard to test, I might be able to help, or we can suppress that bit of code from coverage if needed.
created_at: 2025-03-13T20:47:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: ```suggestion
    check_run_id: parseInt(check_run_id)
```

Default is base-10.
created_at: 2025-03-13T20:51:18Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: ```suggestion
  const checkRun = await github.rest.checks.get({
    owner: context.repo.owner,
    repo: context.repo.repo,
    check_run_id: parseInt(check_run_id, 10)
  }).data;
```

simplify
created_at: 2025-03-13T20:53:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: Is this available in the payload (and could be filtered in yaml instead of JS)?
created_at: 2025-03-13T20:53:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        type: string
+
+permissions:
+  contents: read
+
+jobs:
+  sdk-breaking-change-labels:
+    name: SDK Breaking Change Labels (Preview)
+    runs-on: ubuntu-24.04
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: mikeharder
body: ```suggestion
    if: contains(github.event.check_run.name, '(SDK Generation Job)') && github.event.check_run.check_suite.app.name == 'Azure Pipelines`
    runs-on: ubuntu-24.04
```

Filter as aggressively as possible based on the properties of `github.event` before allocating an agent.  Untested but I believe this should work.
created_at: 2025-03-13T20:57:44Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: Does this require authentication?  Have you tested it for private pipelines (repo specs-pr) as well as public?
created_at: 2025-03-13T20:59:53Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
+            method: 'GET',
+            headers: {
+              'Content-Type': 'application/json'
+            }
+          });
+          
+          if (response.ok) {
+            const artifacts = await response.json();
+            core.info(`Artifacts found: ${artifacts.count}`);
+            
+            // Process artifacts as needed
+            let addLabel = false;
+            let removeLabel = false;
+            let language = "";
+            for (const artifact of artifacts.value) {
+
+              // label artifact name example: 'spec-gen-sdk-azure-sdk-for-js-true'
+              if (artifact.name.startsWith("spec-gen-sdk_")) {
+                core.info(`Artifact: ${artifact.name}`);
+                if(artifact.name.endsWith("true")) {
+                  addLabel = true;
+                  language = artifact.name.split("_")[1];
+                } else if(artifact.name.endsWith("false")) {
+                  removeLabel = true;
+                  language = artifact.name.split("_")[1];
+                }
+              }
+            }
+
+            // Set label action and name based on the artifacts
+            if (addLabel) {
+              labelAction = LabelAction.Add;
+            } else if (removeLabel) {
+              labelAction = LabelAction.Remove;
+            }
+            if (language) {
+              labelName = sdkLabels[`${language}`].breakingChange;
+            }
+          } else {
+            core.error(`Failed to fetch artifacts: ${response.status}, ${response.statusText}`);
+            const errorText = await response.text();
+            core.error(`Error details: ${errorText}`);
+          }
+        } catch (apiError) {
+          core.error(`Error calling Azure DevOps API: ${apiError}`);
+        }
+      } else {
+        core.error(`Could not extract organization and project from URL: ${checkRun.details_url}`);
+      }
+    } else {
+      core.error(`Could not extract buildId from details_url: ${checkRun.details_url}`);
+    }
+  } else {
+    core.info('No details_url found in check run');
+  }
+  // Get the issue number from the check run
+  if (Number.isNaN(issue_number)) {
+    const { issueNumber } = await getIssueNumber({checkRun, core, github});
+    issue_number = issueNumber;
+  }
+
+  if (!labelAction) {
+    core.info('No label action found, defaulting to None');
+    labelAction = LabelAction.None;
+  }  
+  
+  return { labelName, labelAction, issueNumber: issue_number, state: labelAction !== LabelAction.None ? 'success' : 'neutral' };  
+}
+
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {import('github').CheckRun} params.checkRun - The check run object containing head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import('github-script').GitHub} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+async function getIssueNumber({checkRun, core, github})
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: mikeharder
body: I think this should definitely be a shared helper.
created_at: 2025-03-13T21:09:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        type: string
+
+permissions:
+  contents: read
+
+jobs:
+  sdk-breaking-change-labels:
+    name: SDK Breaking Change Labels (Preview)
+    runs-on: ubuntu-24.04
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: weshaggard
body: Yes if we can do that level of filtering we should.
created_at: 2025-03-13T22:27:29Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: weshaggard
body: It will need authentication for the private repo.
created_at: 2025-03-13T22:29:07Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: weshaggard
body: As we talked let's try to split on `_build` and take the first part to use in the other calls.
created_at: 2025-03-13T22:29:55Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: weshaggard
body: As we chatted if we can avoid listing the artifacts and can pull one by name we should do that.
created_at: 2025-03-13T22:30:13Z
author_association: MEMBER
diff_hunk: @@ -195,15 +195,30 @@ extends:
                 $optional_params
             displayName: 'Generate SDK'
 
-          - task: Powershell@2
+          - task: PowerShell@2
+            displayName: 'Output variables'
             inputs:
-              filePath: $(SdkRepoDirectory)/eng/common/scripts/Save-Package-Properties.ps1
-              arguments: >
-                -ServiceDirectory $(GeneratedSDK.ServiceName)
-                -OutDirectory $(GeneratedSDK.StagedArtifactsFolder)/PackageInfo
-              pwsh: true
-              workingDirectory: $(SdkRepoDirectory)
-            displayName: Dump Package properties
+              targetType: 'inline'
+              script: |
+                $artifactName = "$(AddBreakingChangeLabelArtifactName)"
+                $artifactPath = "$(System.DefaultWorkingDirectory)/$(AddBreakingChangeLabelArtifactPath)"
+                Write-Host "Artifact Name: $artifactName"
+                Write-Host "Artifact Path: $artifactPath"
+                if (Test-Path $artifactPath) {
+                  Get-ChildItem $artifactPath -Recurse
+                }
+
+          - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
+            parameters:
+              ArtifactName: $(AddBreakingChangeLabelArtifactName)
path: eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml
user: login: weshaggard
body: As we chatted lets only have one artifact that has a well-known name and not have the possibility of having multiple which can be confusing and require more downloading than needed.
created_at: 2025-03-13T22:31:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,50 @@
+// @ts-check
+
+/**
+ * @typedef {'azure-sdk-for-go' | 'azure-sdk-for-java' | 'azure-sdk-for-js' | 'azure-sdk-for-net' | 'azure-sdk-for-python'} SdkName
+ */
+
+/**
+ * SDK labels mapping for breaking change labels
+ * @type {Object.<SdkName, {
+ *   breakingChange: string | undefined,
+ *   breakingChangeApproved: string | undefined,
+ *   breakingChangeSuppression: string | undefined,
+ *   breakingChangeSuppressionApproved: string | undefined
path: .github/src/sdk-types.js
user: login: raych1
body: Yes, some languages don't have them defined, such as .NET, Java. 
created_at: 2025-03-18T15:06:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,57 @@
+name: SDK Breaking Change Labels (Preview)
+
+on:
+  check_run:
+    types: [completed]
+  workflow_dispatch:
+    inputs:
+      check_run_id:
+        description: 'Check run ID to process'
+        required: false
+        type: string
+
+permissions:
+  contents: read
+
+jobs:
+  sdk-breaking-change-labels:
+    name: SDK Breaking Change Labels (Preview)
+    runs-on: ubuntu-24.04
path: .github/workflows/sdk-breaking-change-labels.yaml
user: login: raych1
body: Updated.
created_at: 2025-03-18T15:07:25Z
author_association: MEMBER
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: Added tests with 100% coverage.
created_at: 2025-03-18T15:14:51Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: Made some input properties as optional in context.js and added common logic to there for check_run case.
created_at: 2025-03-18T15:16:09Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
+            method: 'GET',
+            headers: {
+              'Content-Type': 'application/json'
+            }
+          });
+          
+          if (response.ok) {
+            const artifacts = await response.json();
+            core.info(`Artifacts found: ${artifacts.count}`);
+            
+            // Process artifacts as needed
+            let addLabel = false;
+            let removeLabel = false;
+            let language = "";
+            for (const artifact of artifacts.value) {
+
+              // label artifact name example: 'spec-gen-sdk-azure-sdk-for-js-true'
+              if (artifact.name.startsWith("spec-gen-sdk_")) {
+                core.info(`Artifact: ${artifact.name}`);
+                if(artifact.name.endsWith("true")) {
+                  addLabel = true;
+                  language = artifact.name.split("_")[1];
+                } else if(artifact.name.endsWith("false")) {
+                  removeLabel = true;
+                  language = artifact.name.split("_")[1];
+                }
+              }
+            }
+
+            // Set label action and name based on the artifacts
+            if (addLabel) {
+              labelAction = LabelAction.Add;
+            } else if (removeLabel) {
+              labelAction = LabelAction.Remove;
+            }
+            if (language) {
+              labelName = sdkLabels[`${language}`].breakingChange;
+            }
+          } else {
+            core.error(`Failed to fetch artifacts: ${response.status}, ${response.statusText}`);
+            const errorText = await response.text();
+            core.error(`Error details: ${errorText}`);
+          }
+        } catch (apiError) {
+          core.error(`Error calling Azure DevOps API: ${apiError}`);
+        }
+      } else {
+        core.error(`Could not extract organization and project from URL: ${checkRun.details_url}`);
+      }
+    } else {
+      core.error(`Could not extract buildId from details_url: ${checkRun.details_url}`);
+    }
+  } else {
+    core.info('No details_url found in check run');
+  }
+  // Get the issue number from the check run
+  if (Number.isNaN(issue_number)) {
+    const { issueNumber } = await getIssueNumber({checkRun, core, github});
+    issue_number = issueNumber;
+  }
+
+  if (!labelAction) {
+    core.info('No label action found, defaulting to None');
+    labelAction = LabelAction.None;
+  }  
+  
+  return { labelName, labelAction, issueNumber: issue_number, state: labelAction !== LabelAction.None ? 'success' : 'neutral' };  
+}
+
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {import('github').CheckRun} params.checkRun - The check run object containing head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import('github-script').GitHub} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+async function getIssueNumber({checkRun, core, github})
+{
+  let issueNumber = NaN;
+
+  if (!checkRun.head_sha) {
+    core.info('No head_sha found in check run');
+    return { issueNumber };
+  }
+
+  core.info(`Searching for PRs with commit SHA: ${checkRun.head_sha}`);
+
+  try {
+    const searchResponse = await github.rest.search.issuesAndPullRequests({
+      q: `sha:${checkRun.head_sha} type:pr state:open`
+    });
+
+    const totalCount = searchResponse.data.total_count;
+    const itemsCount = searchResponse.data.items.length;
+
+    core.info(`Search results: ${totalCount} total matches, ${itemsCount} items returned`);
+
+    if (itemsCount > 0) {
+      const firstItem = searchResponse.data.items[0];
+      issueNumber = firstItem.number;
+      core.info(`Found the first matched PR #${issueNumber}: ${firstItem.html_url}`);
+
+      if (itemsCount > 1) {
+        core.warning(`Multiple PRs found for commit ${checkRun.head_sha}: ${searchResponse.data.items.map(item => `#${item.html_url}`).join(', ')}`);
+      }
+    } else {
+      core.info(`No open PRs found for commit ${checkRun.head_sha}`);
+    }
+  } catch (error) {
+    core.error(`${wfName}: Error searching for PRs with commit ${checkRun.head_sha}: ${error.message}`);
+    throw error;
+  }
+
+  return { issueNumber };
+}
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: Added.
created_at: 2025-03-18T15:17:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
+          core.info('Calling Azure DevOps API to list artifacts...');
+          
+          // Use Node.js fetch to call the API
+          const response = await fetch(apiUrl, {
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: This github action is used for public spec repo right now and will add auth later.
created_at: 2025-03-18T15:33:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: Updated and moved this part to context.js.
created_at: 2025-03-18T15:34:27Z
author_association: MEMBER
diff_hunk: @@ -195,15 +195,30 @@ extends:
                 $optional_params
             displayName: 'Generate SDK'
 
-          - task: Powershell@2
+          - task: PowerShell@2
+            displayName: 'Output variables'
             inputs:
-              filePath: $(SdkRepoDirectory)/eng/common/scripts/Save-Package-Properties.ps1
-              arguments: >
-                -ServiceDirectory $(GeneratedSDK.ServiceName)
-                -OutDirectory $(GeneratedSDK.StagedArtifactsFolder)/PackageInfo
-              pwsh: true
-              workingDirectory: $(SdkRepoDirectory)
-            displayName: Dump Package properties
+              targetType: 'inline'
+              script: |
+                $artifactName = "$(AddBreakingChangeLabelArtifactName)"
+                $artifactPath = "$(System.DefaultWorkingDirectory)/$(AddBreakingChangeLabelArtifactPath)"
+                Write-Host "Artifact Name: $artifactName"
+                Write-Host "Artifact Path: $artifactPath"
+                if (Test-Path $artifactPath) {
+                  Get-ChildItem $artifactPath -Recurse
+                }
+
+          - template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
+            parameters:
+              ArtifactName: $(AddBreakingChangeLabelArtifactName)
path: eng/pipelines/templates/stages/archetype-spec-gen-sdk.yml
user: login: raych1
body: Updated to use well-known artifact name then read the content of it to determine the language and label action.
created_at: 2025-03-18T15:35:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,186 @@
+// @ts-check
+import { sdkLabels } from "../../src/sdk-types.js";
+import { LabelAction } from "../../src/label.js";
+
+const wfName = 'sdk-breaking-change-labels';
+
+/**
+ * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments
+ * @returns {Promise<{labelName: string, labelAction: LabelAction, issueNumber: number, state: string}>}
+ */
+export async function getLabelAndAction({ github, context, core }) {
+  let issue_number = NaN;
+  let check_run_id = process.env.CHECK_RUN_ID || context.payload.check_run?.id;  
+  let labelAction;
+  let labelName = "";
+
+  // Determine check run ID to use (from manual input or hardcoded default)
+  if (!check_run_id) {
+    throw new Error('No check run ID provided');
+  }
+  core.info(`Fetching check run with ID: ${check_run_id}`);
+  
+  // First fetch the check run with specific ID
+  const checkRunData = await github.rest.checks.get({
+    owner: context.repo.owner,
+    repo: context.repo.repo,
+    check_run_id: parseInt(check_run_id, 10)
+  });
+  
+  // Use the check run data we retrieved instead of from the payload
+  const checkRun = checkRunData.data;
+  if (!checkRun) {
+    throw new Error('No check run data found in the API response');
+  }
+  core.info(`Check run name: ${checkRun.name}`);
+  if (!checkRun.name.includes("(SDK Generation Job)")) {
+    core.info(`Ignoring check run as it's not an SDK generation job`);
+    core.setOutput('result', 'neutral');
+    return { labelName, labelAction: LabelAction.None, issueNumber: issue_number, state: 'neutral' };
+  }
+
+  // Extract build ID from details_url if available
+  if (checkRun.details_url) {
+    core.info(`Details URL:, ${checkRun.details_url}`);
+    
+    // Extract buildId using regex
+    const buildIdMatch = checkRun.details_url.match(/buildId=(\d+)/);
+    if (buildIdMatch && buildIdMatch[1]) {
+      const buildId = buildIdMatch[1];
+      
+      // Extract organization and project from URL
+      const urlParts = new URL(checkRun.details_url);
+      const pathSegments = urlParts.pathname.split('/').filter(Boolean);
+      
+      if (pathSegments.length >= 2) {
+        const organization = pathSegments[0]; // e.g., azure-sdk
+        const project = pathSegments[1]; // e.g., fadad8ea-1e34-4d7a-9016-fd29b14e8b94
+
+        // Construct artifacts URL
+        const artifactsUrl = `https://dev.azure.com/${organization}/${project}/_build/results?buildId=${buildId}&view=artifacts`;
+        core.info(`Artifacts URL: ${artifactsUrl}`);
+        
+        // Call Azure DevOps REST API to list artifacts
+        try {          
+          // API URL to list artifacts
+          const apiUrl = `https://dev.azure.com/${organization}/${project}/_apis/build/builds/${buildId}/artifacts?api-version=7.0`;              
path: .github/workflows/src/sdk-breaking-change-labels.js
user: login: raych1
body: Updated to download specific artifact.
created_at: 2025-03-18T16:10:30Z
author_association: MEMBER
diff_hunk: @@ -12,3 +12,4 @@ export function createMockLogger() {
     isDebug: vi.fn().mockReturnValue(false),
   };
 }
+
path: .github/test/mocks.js
user: login: mikeharder
body: ```suggestion
```
created_at: 2025-03-18T17:16:34Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,8 @@
 {
   "name": "azure-rest-api-specs",
   "devDependencies": {
+    "@autorest/openapi-to-typespec": "0.10.12",
path: package.json
user: login: mikeharder
body: ```suggestion
    "@autorest/openapi-to-typespec": "0.10.13",
```

good to sort this correctly, but delete the duplicate below

created_at: 2025-03-20T18:02:43Z
author_association: MEMBER
diff_hunk: @@ -210,6 +210,36 @@ export async function extractInputs(github, context, core) {
       issue_number: issue_number,
       run_id: payload.workflow_run.id,
     };
+  } else if (context.eventName === "check_run") {
path: .github/workflows/src/context.js
user: login: mikeharder
body: ```suggestion
  } else if (context.eventName === "check_run" && context.payload.action === "completed") {
```

I'm not sure this is the exact property name, but I think this should be scoped to only "completed" events, until we need otherwise.
created_at: 2025-03-20T18:07:55Z
author_association: MEMBER
diff_hunk: @@ -210,6 +210,36 @@ export async function extractInputs(github, context, core) {
       issue_number: issue_number,
       run_id: payload.workflow_run.id,
     };
+  } else if (context.eventName === "check_run") {
+    let checkRun = context.payload.check_run;
+
+    // Extract the ADO build ID and project URL from the check run details URL
path: .github/workflows/src/context.js
user: login: mikeharder
body: What if the check_run didn't come from ADO?  I think we should handle this gracefully rather than throw.
created_at: 2025-03-20T18:08:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {String} params.head_sha - The head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+export async function getIssueNumber({ head_sha, core, github }) {
path: .github/workflows/src/issues.js
user: login: mikeharder
body: Could this be moved into context.js, and returned automatically as the value of "output.issue_number" instead of NaN?
created_at: 2025-03-20T18:10:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
path: .github/workflows/test/issues.test.js
user: login: mikeharder
body: Should this throw instead of returning NaN?  Is it ever valid to call without a head_sha?
created_at: 2025-03-20T18:27:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify appropriate message was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      "No head_sha found in check run",
+    );
path: .github/workflows/test/issues.test.js
user: login: mikeharder
body: I'd remove expectations about logging, it will be more work to update if we change the logs.  Logs should be flexible.
created_at: 2025-03-20T18:28:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify appropriate message was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      "No head_sha found in check run",
+    );
+  });
+
+  it("should handle multiple PRs for the same commit", async () => {
+    // Mock multiple PRs found
+    mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+      data: {
+        total_count: 2,
+        items: [
+          { number: 123, html_url: "https://github.com/pr/123" },
+          { number: 456, html_url: "https://github.com/pr/456" },
+        ],
+      },
+    });
+
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "abc",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result uses first PR
path: .github/workflows/test/issues.test.js
user: login: mikeharder
body: Why correct to use first PR?
created_at: 2025-03-20T18:29:13Z
author_association: MEMBER
diff_hunk: @@ -1,6 +1,8 @@
 {
   "name": "azure-rest-api-specs",
   "devDependencies": {
+    "@autorest/openapi-to-typespec": "0.10.12",
path: package.json
user: login: raych1
body: updated.
created_at: 2025-03-20T20:18:23Z
author_association: MEMBER
diff_hunk: @@ -210,6 +210,36 @@ export async function extractInputs(github, context, core) {
       issue_number: issue_number,
       run_id: payload.workflow_run.id,
     };
+  } else if (context.eventName === "check_run") {
+    let checkRun = context.payload.check_run;
+
+    // Extract the ADO build ID and project URL from the check run details URL
path: .github/workflows/src/context.js
user: login: raych1
body: At this point, we only have the check_run from ADO. We could enhance it when there are other run types.
created_at: 2025-03-20T20:21:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,52 @@
+/**
+ * Retrieves the PR number associated with a specific commit SHA
+ * @param {Object} params
+ * @param {String} params.head_sha - The head_sha
+ * @param {typeof import("@actions/core")} params.core - GitHub Actions core for logging
+ * @param {import("@octokit/core").Octokit & import("@octokit/plugin-rest-endpoint-methods/dist-types/types.js").Api} params.github - GitHub API client
+ * @returns {Promise<{issueNumber: number}>} - The PR number or NaN if not found
+ */
+export async function getIssueNumber({ head_sha, core, github }) {
path: .github/workflows/src/issues.js
user: login: raych1
body: As talked offline, the reason is that we want to save the call of searching issues if there are no label action to make.
created_at: 2025-03-20T20:22:34Z
author_association: MEMBER
diff_hunk: @@ -210,6 +210,36 @@ export async function extractInputs(github, context, core) {
       issue_number: issue_number,
       run_id: payload.workflow_run.id,
     };
+  } else if (context.eventName === "check_run") {
path: .github/workflows/src/context.js
user: login: raych1
body: Leave it to handle by each individual workflow template.
created_at: 2025-03-20T20:23:49Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify appropriate message was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      "No head_sha found in check run",
+    );
path: .github/workflows/test/issues.test.js
user: login: raych1
body: sounds good.
created_at: 2025-03-20T20:24:05Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,98 @@
+import { describe, expect, it, vi, beforeEach } from "vitest";
+import { getIssueNumber } from "../src/issues.js";
+import { createMockGithub, createMockCore } from "./mocks.js";
+
+const mockGithub = createMockGithub();
+const mockCore = createMockCore();
+
+describe("getIssueNumber", () => {
+  beforeEach(() => {
+    // Reset mocks
+    vi.clearAllMocks();
+  });
+  it("should return NaN when head_sha is missing", async () => {
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result
+    expect(result.issueNumber).toBeNaN();
+
+    // Verify appropriate message was logged
+    expect(mockCore.info).toHaveBeenCalledWith(
+      "No head_sha found in check run",
+    );
+  });
+
+  it("should handle multiple PRs for the same commit", async () => {
+    // Mock multiple PRs found
+    mockGithub.rest.search.issuesAndPullRequests.mockResolvedValue({
+      data: {
+        total_count: 2,
+        items: [
+          { number: 123, html_url: "https://github.com/pr/123" },
+          { number: 456, html_url: "https://github.com/pr/456" },
+        ],
+      },
+    });
+
+    // Call function
+    const result = await getIssueNumber({
+      head_sha: "abc",
+      github: mockGithub,
+      core: mockCore,
+    });
+
+    // Verify result uses first PR
path: .github/workflows/test/issues.test.js
user: login: raych1
body: Multiple PRs happens when users create multiple PRs based on the same commit sha. It's an uncommon user case and usually users should keep only one of them open. For now, just return the first PR and log warnings.  We could revisit this logic when there are more scenarios using this API in the future.
created_at: 2025-03-20T20:26:59Z
author_association: MEMBER

PR comments:
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-12T16:55:15Z
body: <h2>Next Steps to Merge</h2>Next steps that must be taken to merge this PR: <br/><ul><li>❌ The required check named <code>Protected Files</code> has failed. Refer to the check in the PR's 'Checks' tab for details on how to fix it and consult the <a href="https://aka.ms/ci-fix">aka.ms/ci-fix</a> guide</li></ul>
author_association: NONE
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-12T16:55:15Z
body: PR validation pipeline restarted successfully. If there is ApiView generated, it will be updated in this comment.
author_association: NONE
user: login: raych1
created_at: 2025-03-12T17:04:59Z
body: @mikeharder here are the test runs for `update-label` actions. I will check if test coverage for 'context.js' needs to be extended.
[Test run without issue_number](https://github.com/raych1/azure-rest-api-specs/actions/runs/13801334667/job/38604178253)
<img width="814" alt="image" src="https://github.com/user-attachments/assets/cfd25a49-3b0d-4474-a742-fb6de46c3a03" />

[Test run with issue_number](https://github.com/raych1/azure-rest-api-specs/actions/runs/13801448838/job/38604497653)
<img width="800" alt="image" src="https://github.com/user-attachments/assets/788c7eb8-248a-4d78-a1a6-f1557eec8b63" />

author_association: MEMBER
user: login: mikeharder
created_at: 2025-03-12T18:00:26Z
body: @raych1: Do you have an end-to-end examples of all the pipelines and workflows running together?  I'd like to trace how the information flows from devops to github.
author_association: MEMBER
user: login: raych1
created_at: 2025-03-12T23:52:09Z
body: > @raych1: Do you have an end-to-end examples of all the pipelines and workflows running together? I'd like to trace how the information flows from devops to github.

@mikeharder these are two PRs for the end to end testing.
[PR from forked repo](https://github.com/raych1/azure-rest-api-specs/pull/7)
[PR from same repo](https://github.com/raych1/azure-rest-api-specs/pull/11)
author_association: MEMBER
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-18T00:42:32Z
body: PR validation pipeline can not start as the pull request is not merged or mergeable - most likely it has merge conflicts.
author_association: NONE
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-18T15:04:00Z
body: PR validation pipeline can not start as the pull request is not merged or mergeable - most likely it has merge conflicts.
author_association: NONE
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-18T15:30:14Z
body: PR validation pipeline can not start as the pull request is not merged or mergeable - most likely it has merge conflicts.
author_association: NONE
user: login: openapi-pipeline-app[bot]
created_at: 2025-03-18T15:51:09Z
body: PR validation pipeline can not start as the pull request is not merged or mergeable - most likely it has merge conflicts.
author_association: NONE
user: login: raych1
created_at: 2025-03-18T17:06:05Z
body: @mikeharder all the comments were addressed, and could you have another review?
author_association: MEMBER
user: login: raych1
created_at: 2025-03-18T22:59:04Z
body: > Please fix linting issues first:
> 
> https://github.com/Azure/azure-rest-api-specs/actions/runs/13929427110/job/38982515798?pr=33158#step:4:16

lint and prettier issues have been fixed.
author_association: MEMBER
user: login: raych1
created_at: 2025-03-19T00:25:52Z
body: @mikeharder latest test run:
[PR from same repo](https://github.com/raych1/azure-rest-api-specs/pull/11#issuecomment-2734978531)
[PR from forked repo](https://github.com/raych1/azure-rest-api-specs/pull/7#issuecomment-2734978417)
author_association: MEMBER