diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000000..78ff521e7778 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,64 @@ +# Rules for SDK release process and Spec readiness to generate and release SDK +- If a Spec project path is not available, prompt user to provide or select the Spec project root +- Verify whether spec project root is TypeSpec project. This guidelines to support spec to SDK release is currently + supported only for TypeSpec + - Follow the steps below for SDK release process from TypeSpec specification + +## SDK release process from TypeSpec + +### Pre-requisites +- User should have a GitHub account and should be logged in to GitHub account using GitHub CLI `gh auth login`. + +- Check the current branch name for the cloned GitHub repo. Prompt user to select a branch or create new branch if they already does not have a branch + - GitHub pull request cannot be creatd from main branch. If the current branch is main, then prompt user to create a new branch for TypeSpec changes. + - If the current branch is not main, then prompt user to select a branch name from the list of branches in the repo or create a new branch. + - If a branch needs to be created then prompt user to run command: `git checkout -b `. + - If a branch already exists and if it is not same as current branch then prompt user to run command `git checkout ` to switch to the branch. + +### Steps to generate and release SDK from TypeSpec API specification +- Step 1: Verify if the user has a TypeSpec project path for API specification. + - If not, then prompt user to create a TypeSpec for API specification. + - If user has a TypeSpec project path, then prompt user to provide TypeSpec project name and then verify if the path is a valid TypeSpec project. +- Step 2: Run TypeSpec validation and make sure there are no TypeSpec validation + - If there are any TypeSpec validation failures, then highlight the failures to fix them. +- Step 3: Show list of changed files in the repo and prompt user to confirm if the changes are correct. + - If the user confirms, then proceed with the next steps. + - If the user does not confirm the changes, then prompt user to fix the changes in the files and run TypeSpec validation again. +- Step 4: Verify user has a GitHub account and is logged in to GitHub account using GitHub CLI `gh auth login`. + - if GitHub login fails then prompt user to make sure to install GitHub CLI and login to GitHub account using `gh auth login` command. + - If user is logged in to GitHub account, then proceed with the next steps. +- Step 5: Create Pull request for changes. + - Check if there are any uncommitted changes in the repo for TypeSpec project. + - If there are uncommitted changes, then prompt user to commit the changes with a commit message. + - Prompt to run `git add ` command to add the changes to the staging area. + - Prompt to run `git commit -m ""` command to commit the changes. + - Push the changes to GitHup remote. Make sure that remote branch name is not "main" + - Prompt user to run `git push -u origin ` command to push the changes to GitHub remote. + - Next step is to create a pull request for the branch. This step will either creates a new pull request or find the existing pull request for the branch. + - Check the current branch name. Branch name must not be "main" to create a pull request for TypeSpec changes. + - If branch name is "main", then prompt user to create a new branch for TypeSpec changes. command: `git checkout -b `. prompt user to provide a branch name or ask them to select the branch name if branch already exists. + - Push the changes to the remote branch in GitHub repo. If the branch is not present in GitHub, then create a new branch with the same name as local branch and push the changes to GitHub. + - Generate a title and description based on the changes. Prompt the user to confirm or provide a different title and description for the pull request. + - Prompt user to select the the target branch for pull request. Default target branch is "main". + - Create pull requet for changes in TypeSpec project, target branch, title and descriptions +- Step 6: Get TypeSpec pull request details for current branch and show the details of the pull request. +- Step 7: Get pull request comments and check if there is any action item for the user. +- Step 8: Verify target lifecycle of API specification. + - Target lifecycle is the lifecycle of the API specification. Show below list of options and prompt user to select one of the options. + - Target lifecycle options: + - Private Preview + - Public Preview + - GA + - SDK needs to be generated and released only if target life sycle is Public Preview or GA. + - If target lifecycle is Private Preview, then inform user that SDK generation and release is not required for Private Preview. +- Step 9: Create release plan work item before generating the SDK or get a URL for existing release plan work item. + - Prompt user to provide the following details for the release plan work item: + - Service Tree ID for the Service + - Service Name + - Product Service tree ID for the Product + - Product Name + - Expected release timeline in Month and Year (e.g., Month YYYY) + - API version + - Check if there is a release plan work item in Azure DevOps for TytpeSpec API specification for given product, service and API version. + - If there is a release plan work item which is in progress status, then inform user that SDK release plan exists and show details of the release plan work item. + - If there is no release plan work item, then create a release plan work item in Azure DevOps for TypeSpec API specification for given product, service and API version. diff --git a/.github/package-lock.json b/.github/package-lock.json index 6b29afcbb949..14d3bdd70e5d 100644 --- a/.github/package-lock.json +++ b/.github/package-lock.json @@ -646,9 +646,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -721,9 +721,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", + "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", "dev": true, "license": "MIT", "engines": { @@ -1963,19 +1963,19 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", + "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", + "@eslint/config-array": "^0.20.0", "@eslint/config-helpers": "^0.2.0", "@eslint/core": "^0.12.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", + "@eslint/js": "9.24.0", "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -3311,9 +3311,9 @@ } }, "node_modules/vite": { - "version": "6.2.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz", - "integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==", + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.5.tgz", + "integrity": "sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/.github/workflows/_reusable-verify-run-status.yaml b/.github/workflows/_reusable-verify-run-status.yaml new file mode 100644 index 000000000000..4609526e78ef --- /dev/null +++ b/.github/workflows/_reusable-verify-run-status.yaml @@ -0,0 +1,42 @@ +# Prefix with "~" to sort last in Actions list +name: ~Templates - Verify Run Status + +on: + workflow_call: + inputs: + check_run_name: + description: Name of the check run to verify + required: true + type: string + workflow_name: + description: Name of the workflow to verify + required: true + type: string + +permissions: + checks: read + contents: read + +jobs: + check-run-status: + if: | + (github.event_name == 'workflow_run') || + (github.event_name == 'check_suite' && github.event.check_suite.app.name == 'openapi-pipeline-app') || + (github.event_name == 'check_run' && github.event.check_run.name == inputs.check_run_name) + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + .github + + - name: Verify matching status + uses: actions/github-script@v7 + with: + script: | + const { verifyRunStatus } = await import('${{ github.workspace }}/.github/workflows/src/verify-run-status.js'); + return await verifyRunStatus({ github, context, core }); + env: + CHECK_RUN_NAME: ${{ inputs.check_run_name }} + WORKFLOW_NAME: ${{ inputs.workflow_name }} diff --git a/.github/workflows/src/checks.js b/.github/workflows/src/checks.js new file mode 100644 index 000000000000..89121020281d --- /dev/null +++ b/.github/workflows/src/checks.js @@ -0,0 +1,25 @@ +import { PER_PAGE_MAX } from "./github.js"; + +/** + * @param {import('github-script').AsyncFunctionArguments['github']} github + * @param {{ + * owner: string; + * repo: string; + * ref: string; + * name?: string; + * status?: "queued" | "in_progress" | "completed"; + * }} params + * @returns {Promise} + */ +export async function listChecksForRef(github, { owner, repo, ref, name, status}) { + const options = { + owner, + repo, + ref, + ...(name && { check_name: name }), + ...(status && { status }), + per_page: PER_PAGE_MAX, + }; + + return await github.paginate(github.rest.checks.listForRef, options); +} diff --git a/.github/workflows/src/verify-run-status.js b/.github/workflows/src/verify-run-status.js new file mode 100644 index 000000000000..2a0770c46dfb --- /dev/null +++ b/.github/workflows/src/verify-run-status.js @@ -0,0 +1,180 @@ +import { extractInputs } from "./context.js"; +import { PER_PAGE_MAX } from "./github.js"; + +const SUPPORTED_EVENTS = ["workflow_run", "check_run", "check_suite"]; + +/** + * @typedef {import('@octokit/plugin-rest-endpoint-methods').RestEndpointMethodTypes} RestEndpointMethodTypes + * @typedef {RestEndpointMethodTypes["checks"]["listForRef"]["response"]["data"]["check_runs"]} CheckRuns + * @typedef {RestEndpointMethodTypes["actions"]["listWorkflowRunsForRepo"]["response"]["data"]["workflow_runs"]} WorkflowRuns + */ + +/* v8 ignore start */ +/** + * Given the name of a completed check run name and a completed workflow, verify + * that both have the same conclusion. If conclusions are different, fail the + * action. + * @param {import('github-script').AsyncFunctionArguments} AsyncFunctionArguments + */ +export async function verifyRunStatus({ github, context, core }) { + const checkRunName = process.env.CHECK_RUN_NAME; + if (!checkRunName) { + throw new Error("CHECK_RUN_NAME is not set"); + } + + const workflowName = process.env.WORKFLOW_NAME; + if (!workflowName) { + throw new Error("WORKFLOW_NAME is not set"); + } + + if (!SUPPORTED_EVENTS.some((e) => e === context.eventName)) { + throw new Error( + `Unsupported event: ${context.eventName}. Supported events: ${SUPPORTED_EVENTS.join(", ")}`, + ); + } + + if (context.eventName === "check_suite" && context.payload.check_suite.status !== "completed") { + core.setFailed(`Check suite ${context.payload.check_suite.app.name} is not completed. Cannot evaluate incomplete check suite.`); + return; + } + + return await verifyRunStatusImpl({ github, context, core , checkRunName, workflowName}); +} +/* v8 ignore stop */ + +/** + * @param {Object} params + * @param {import('github-script').AsyncFunctionArguments["github"]} params.github + * @param {import('github-script').AsyncFunctionArguments["context"]} params.context + * @param {import('github-script').AsyncFunctionArguments["core"]} params.core + * @param {string} params.checkRunName + * @param {string} params.workflowName + */ +export async function verifyRunStatusImpl({github, context, core, checkRunName, workflowName}) { + if (context.eventName == "check_run") { + const contextRunName = context.payload.check_run.name; + if (contextRunName !== checkRunName) { + core.setFailed(`Check run name (${contextRunName}) does not match input: ${checkRunName}. Ensure job is filtering by github.event.check_run.name.`); + return; + } + } + + const { head_sha } = await extractInputs(github, context, core); + + let checkRun; + if (context.eventName == "check_run") { + checkRun = context.payload.check_run; + } else { + const checkRuns = await getCheckRuns(github, context, checkRunName, head_sha); + if (checkRuns.length === 0) { + if (context.eventName === "check_suite") { + const message = `Could not locate check run ${checkRunName} in check suite ${context.payload.check_suite.app.name}. Ensure job is filtering by github.event.check_suite.app.name.`; + core.setFailed(message); + return; + } + + core.notice(`No completed check run with name: ${checkRunName}. Not enough information to judge success or failure. Ending with success status.`); + return; + } + + // Use the most recent check run + checkRun = checkRuns[0]; + } + + core.info( + `Check run name: ${checkRun.name}, conclusion: ${checkRun.conclusion}, URL: ${checkRun.html_url}`, + ); + core.debug(`Check run: ${JSON.stringify(checkRun)}`); + + let workflowRun; + if (context.eventName == "workflow_run") { + workflowRun = context.payload.workflow_run; + } else { + const workflowRuns = await getWorkflowRuns(github, context, workflowName, head_sha); + if (workflowRuns.length === 0) { + core.notice(`No completed workflow run with name: ${workflowName}. Not enough information to judge success or failure. Ending with success status.`); + return; + } + + // Use the most recent workflow run + workflowRun = workflowRuns[0]; + } + + core.info( + `Workflow run name: ${workflowRun.name}, conclusion: ${workflowRun.conclusion}, URL: ${workflowRun.html_url}`, + ); + core.debug(`Workflow run: ${JSON.stringify(workflowRun)}`); + + if (checkRun.conclusion !== workflowRun.conclusion) { + core.setFailed( + `Check run conclusion (${checkRun.conclusion}) does not match workflow run conclusion (${workflowRun.conclusion})`, + ); + return; + } + + core.notice(`Conclusions match for check run ${checkRunName} and workflow run ${workflowName}`); +} + +/** + * Returns the check with the given checkRunName for the given ref. + * @param {import('github-script').AsyncFunctionArguments['github']} github + * @param {import('github-script').AsyncFunctionArguments['context']} context + * @param {string} checkRunName + * @param {string} ref + * @returns {Promise} + */ +export async function getCheckRuns( + github, + context, + checkRunName, + ref, +) { + const result = await github.paginate(github.rest.checks.listForRef, { + ...context.repo, + ref: ref, + check_name: checkRunName, + status: "completed", + per_page: PER_PAGE_MAX, + }); + + // a and b will never be null because status is "completed" + /* v8 ignore next */ + return result.sort((a, b) => compareDatesDescending(a.completed_at || '', b.completed_at || '')); +} + +/** + * Returns the workflow run with the given workflowName for the given ref. + * @param {import('github-script').AsyncFunctionArguments['github']} github + * @param {import('github-script').AsyncFunctionArguments['context']} context + * @param {string} workflowName + * @param {string} ref + * @returns {Promise} + */ +export async function getWorkflowRuns( + github, + context, + workflowName, + ref, +) { + const result = await github.paginate( + github.rest.actions.listWorkflowRunsForRepo, + { + ...context.repo, + head_sha: ref, + status: "completed", + per_page: PER_PAGE_MAX, + }, + ); + + return result.filter((run) => run.name === workflowName).sort((a, b) => compareDatesDescending(a.updated_at, b.updated_at)); +} + +/** + * Compares two date strings in descending order. + * @param {string} a date string of the form "YYYY-MM-DDTHH:mm:ssZ" + * @param {string} b date string of the form "YYYY-MM-DDTHH:mm:ssZ" + * @returns + */ +export function compareDatesDescending(a, b) { + return new Date(b).getTime() - new Date(a).getTime(); +} \ No newline at end of file diff --git a/.github/workflows/src/workflows.js b/.github/workflows/src/workflows.js new file mode 100644 index 000000000000..139f388cf4e0 --- /dev/null +++ b/.github/workflows/src/workflows.js @@ -0,0 +1,23 @@ +import { PER_PAGE_MAX } from "./github.js"; + +/** + * @param {import('github-script').AsyncFunctionArguments['github']} github + * @param {{ + * owner: string; + * repo: string; + * head_sha?: string; + * status?: "completed" | "in_progress" | "queued"; + * event?: string; + * }} params + * @returns {Promise} + */ +export async function listWorkflowRunsForRepo(github, { owner, repo, head_sha, status, event}) { + return await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, + repo, + ...(head_sha && { head_sha }), + ...(status && { status }), + ...(event && { event }), + per_page: PER_PAGE_MAX, + }); +} diff --git a/.github/workflows/test/mocks.js b/.github/workflows/test/mocks.js index ed845f6f8d5b..797d21c7a541 100644 --- a/.github/workflows/test/mocks.js +++ b/.github/workflows/test/mocks.js @@ -48,12 +48,14 @@ export function createMockCore() { return { debug: vi.fn(console.debug), info: vi.fn(console.log), + notice: 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}')`), ), + setFailed: vi.fn((msg) => console.log(`setFailed('${msg}')`)), }; } diff --git a/.github/workflows/test/verify-run-status.test.js b/.github/workflows/test/verify-run-status.test.js new file mode 100644 index 000000000000..b08a6c31c4ac --- /dev/null +++ b/.github/workflows/test/verify-run-status.test.js @@ -0,0 +1,408 @@ +import { describe, expect, it, vi } from "vitest"; +import { + createMockGithub, + createMockContext, + createMockCore, +} from "./mocks.js"; +import { + getCheckRuns, + getWorkflowRuns, + verifyRunStatusImpl, +} from "../src/verify-run-status.js"; + +vi.mock("../src/context.js", () => { + return { + extractInputs: vi.fn().mockResolvedValue({ + head_sha: "head_sha", + }), + }; +}); + +describe("getCheckRuns", () => { + it("returns matching check_run", async () => { + const githubMock = createMockGithub(); + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [ + { + name: "checkRunName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const actual = await getCheckRuns( + githubMock, + createMockContext(), + createMockCore(), + "checkRunName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + }), + ]); + }); + + it("returns null when no check matches", async () => { + const githubMock = createMockGithub(); + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [], + }, + }); + + const actual = await getCheckRuns( + githubMock, + createMockContext(), + "checkRunName", + "head_sha", + ); + + expect(actual).toEqual([]); + }); + + it("throws when multiple checks match", async () => { + const githubMock = createMockGithub(); + const earlierDate = "2025-04-01T00:00:00Z"; + const laterDate = "2025-04-02T00:00:00Z"; + githubMock.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [ + { + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: earlierDate, + }, + { + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: laterDate, + }, + ], + }, + }); + + const actual = await await getCheckRuns( + githubMock, + createMockContext(), + "checkRunName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: laterDate, + }), + expect.objectContaining({ + name: "checkRunName", + status: "completed", + conclusion: "success", + completed_at: earlierDate, + }), + ]); + }); +}); + +describe("getWorkflowRuns", () => { + it("returns matching workflow_run", async () => { + const githubMock = createMockGithub(); + githubMock.rest.actions.listWorkflowRunsForRepo = vi + .fn() + .mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + name: "workflowName", + status: "completed", + conclusion: "success", + }), + ]); + }); + + it("returns null when no workflow matches", async () => { + const githubMock = createMockGithub(); + githubMock.rest.actions.listWorkflowRunsForRepo = vi + .fn() + .mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "otherWorkflowName", + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([]); + }); + + it("returns latest when multiple workflows match", async () => { + const githubMock = createMockGithub(); + const earlyDate = "2025-04-01T00:00:00Z"; + const laterDate = "2025-04-02T00:00:00Z"; + githubMock.rest.actions.listWorkflowRunsForRepo = vi + .fn() + .mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "success", + updated_at: earlyDate, + }, + { + name: "workflowName", + status: "completed", + conclusion: "success", + updated_at: laterDate, + }, + ], + }, + }); + + const actual = await getWorkflowRuns( + githubMock, + createMockContext(), + "workflowName", + "head_sha", + ); + + expect(actual).toEqual([ + expect.objectContaining({ + updated_at: laterDate, + }), + expect.objectContaining({ + updated_at: earlyDate, + }), + ]); + }); +}); + +describe("verifyRunStatusImpl", () => { + it("verifies status when check_run event fires", async () => { + const github = createMockGithub(); + github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const context = { + eventName: "check_run", + payload: { + check_run: { + name: "checkRunName", + conclusion: "success", + }, + }, + }; + + const core = createMockCore(); + + vi.stubEnv("CHECK_RUN_NAME", "checkRunName"); + vi.stubEnv("WORKFLOW_NAME", "workflowName"); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.notice).toHaveBeenCalledWith("Conclusions match for check run checkRunName and workflow run workflowName"); + }); + + it("verifies status when workflow_run event fires", async () => { + const github = createMockGithub(); + github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [ + { + name: "checkRunName", + status: "completed", + conclusion: "success", + }, + ], + }, + }); + + const context = { + eventName: "workflow_run", + payload: { + workflow_run: { + name: "workflowName", + conclusion: "success", + }, + }, + }; + + const core = createMockCore(); + + vi.stubEnv("CHECK_RUN_NAME", "checkRunName"); + vi.stubEnv("WORKFLOW_NAME", "workflowName"); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + + expect(core.setFailed).not.toHaveBeenCalled(); + }); + + it("returns early during workflow_run event when no matching check_run is found", async () => { + const github = createMockGithub(); + github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [], + }, + }); + + const context = { + eventName: "workflow_run", + payload: { + workflow_run: { + name: "workflowName", + conclusion: "success", + }, + }, + }; + const core = createMockCore(); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.notice).toHaveBeenCalledWith( + "No completed check run with name: checkRunName. Not enough information to judge success or failure. Ending with success status.", + ); + }); + + it("returns early during check_run event when no matching workflow_run is found", async () => { + const github = createMockGithub(); + github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [], + }, + }); + + const context = { + eventName: "check_run", + payload: { + check_run: { + name: "checkRunName", + conclusion: "success", + }, + }, + }; + const core = createMockCore(); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + expect(core.setFailed).not.toHaveBeenCalled(); + expect(core.notice).toHaveBeenCalledWith( + "No completed workflow run with name: workflowName. Not enough information to judge success or failure. Ending with success status.", + ); + }); + + it("returns early if event is check_run but does not match input name", async () => { + const github = createMockGithub(); + const context = { + eventName: "check_run", + payload: { + check_run: { + name: "checkRunName", + conclusion: "success", + }, + }, + }; + const core = createMockCore(); + await verifyRunStatusImpl({ github, context, core, checkRunName: "otherCheckRunName", workflowName: "workflowName" }); + expect(core.setFailed).toHaveBeenCalledWith( + "Check run name (checkRunName) does not match input: otherCheckRunName. Ensure job is filtering by github.event.check_run.name.", + ); + }); + + it("throws if check_run conclusion does not match workflow_run conclusion", async () => { + const github = createMockGithub(); + github.rest.actions.listWorkflowRunsForRepo = vi.fn().mockResolvedValue({ + data: { + workflow_runs: [ + { + name: "workflowName", + status: "completed", + conclusion: "failure", + }, + ], + }, + }); + + const context = { + eventName: "check_run", + payload: { + check_run: { + name: "checkRunName", + conclusion: "success", + }, + }, + }; + const core = createMockCore(); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + expect(core.setFailed).toHaveBeenCalledWith( + "Check run conclusion (success) does not match workflow run conclusion (failure)", + ); + }); + + it ("throws when in check_suite event but no check_run with name is found", async () => { + const github = createMockGithub(); + github.rest.checks.listForRef = vi.fn().mockResolvedValue({ + data: { + check_runs: [], + }, + }); + + const context = { + eventName: "check_suite", + payload: { + check_suite: { + app: { + name: "checkRunName", + }, + }, + }, + }; + const core = createMockCore(); + await verifyRunStatusImpl({ github, context, core, checkRunName: "checkRunName", workflowName: "workflowName" }); + expect(core.setFailed).toHaveBeenCalledWith( + "Could not locate check run checkRunName in check suite checkRunName. Ensure job is filtering by github.event.check_suite.app.name.", + ); + }); +}); \ No newline at end of file diff --git a/.github/workflows/watch-lintdiff.yaml b/.github/workflows/watch-lintdiff.yaml new file mode 100644 index 000000000000..a4c9f115430a --- /dev/null +++ b/.github/workflows/watch-lintdiff.yaml @@ -0,0 +1,25 @@ +# Use ~ to sort the workflow to the bottom of the list +name: "~Watch - LintDiff" + +on: + # check_suite is preferred over check_run to avoid triggering on all check + # runs. In some cases, check_run must be used in testing environments. + check_run: + types: completed + + workflow_run: + types: completed + workflows: + - "\\[TEST-IGNORE\\] Swagger LintDiff" + +permissions: + checks: read + contents: read + +jobs: + LintDiff: + name: Watch LintDiff + uses: ./.github/workflows/_reusable-verify-run-status.yaml + with: + check_run_name: "Swagger LintDiff" + workflow_name: "[TEST-IGNORE] Swagger LintDiff" diff --git a/specification/ai/Face/main.tsp b/specification/ai/Face/main.tsp index 8db754e34662..b06b8f002191 100644 --- a/specification/ai/Face/main.tsp +++ b/specification/ai/Face/main.tsp @@ -14,7 +14,7 @@ using TypeSpec.Rest; using TypeSpec.Versioning; @useAuth(KeyAuth | AADToken) -@service(#{ title: "Azure AI Face API" }) +@service(#{ title: "Azure AI Face APIs" }) @versioned(Versions) @server( "{endpoint}/face/{apiVersion}", diff --git a/specification/contosowidgetmanager/Contoso.WidgetManager/examples/2022-12-01/Widgets_GetWidgetSample.json b/specification/contosowidgetmanager/Contoso.WidgetManager/examples/2022-12-01/Widgets_GetWidgetSample.json index ecab18c65303..ccf33c894707 100644 --- a/specification/contosowidgetmanager/Contoso.WidgetManager/examples/2022-12-01/Widgets_GetWidgetSample.json +++ b/specification/contosowidgetmanager/Contoso.WidgetManager/examples/2022-12-01/Widgets_GetWidgetSample.json @@ -1,6 +1,6 @@ { "operationId": "Widgets_GetWidget", - "title": "Get widget by widget name.", + "title": "Get widget by widget name. Test change", "parameters": { "api-version": "2022-12-01", "widgetName": "searchbox" diff --git a/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml b/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml index 52a82b1e045a..ad992cd6097d 100644 --- a/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml +++ b/specification/contosowidgetmanager/Contoso.WidgetManager/tspconfig.yaml @@ -15,6 +15,7 @@ options: emitter-output-dir: "{project-root}/.." output-file: "{azure-resource-provider-folder}/{service-name}/{version-status}/{version}/widgets.json" "@azure-tools/typespec-python": + package-name: "azure-contoso-widgetmanager" package-dir: "azure-contoso-widgetmanager" namespace: "azure.contoso.widgetmanager" generate-test: true diff --git a/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/examples/Widgets_GetWidgetSample.json b/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/examples/Widgets_GetWidgetSample.json index ecab18c65303..ccf33c894707 100644 --- a/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/examples/Widgets_GetWidgetSample.json +++ b/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/examples/Widgets_GetWidgetSample.json @@ -1,6 +1,6 @@ { "operationId": "Widgets_GetWidget", - "title": "Get widget by widget name.", + "title": "Get widget by widget name. Test change", "parameters": { "api-version": "2022-12-01", "widgetName": "searchbox" diff --git a/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json b/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json index 34f77daf7126..17949c8e5d28 100644 --- a/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json +++ b/specification/contosowidgetmanager/data-plane/Azure.Contoso.WidgetManager/stable/2022-12-01/widgets.json @@ -115,7 +115,7 @@ } }, "x-ms-examples": { - "Get widget by widget name.": { + "Get widget by widget name. Test change": { "$ref": "./examples/Widgets_GetWidgetSample.json" } } diff --git a/specificationRepositoryConfiguration.json b/specificationRepositoryConfiguration.json index 0b2c1930d549..fbb51a455ca3 100644 --- a/specificationRepositoryConfiguration.json +++ b/specificationRepositoryConfiguration.json @@ -23,7 +23,8 @@ }, "azure-sdk-for-python": { "integrationRepository": "azure-sdk/azure-sdk-for-python", - "mainRepository": "Azure/azure-sdk-for-python" + "mainRepository": "Azure/azure-sdk-for-python", + "mainBranch": "fix-pipeline-2025-04-09-2" }, "azure-powershell": { "integrationRepository": "azure-sdk/azure-powershell",