diff --git a/.github/shared/cmd/api-doc-preview.js b/.github/shared/cmd/api-doc-preview.js new file mode 100755 index 000000000000..d3ba8a05a372 --- /dev/null +++ b/.github/shared/cmd/api-doc-preview.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node +// @ts-check + +import { mkdir, writeFile } from "fs/promises"; +import { dirname, join, resolve } from "path"; +import { fileURLToPath } from "url"; +import { parseArgs } from "util"; + +import { getChangedFilesStatuses, swagger } from "../src/changed-files.js"; + +import { + getSwaggersToProcess, + indexMd, + mappingJSONTemplate, + repoJSONTemplate, +} from "../src/doc-preview.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +function usage() { + console.log(`Usage: +npx api-doc-preview --output + +parameters: + --output Directory to write documentation artifacts to. + --build-id Build ID, used in the documentation index. Defaults to BUILD_BUILDID environment variable. + --spec-repo-name Name of the repository containing the swagger files of the form /. Defaults to BUILD_REPOSITORY_NAME environment variable. + --spec-repo-pr-number PR number of the repository containing the swagger files. Defaults to SYSTEM_PULLREQUEST_PULLREQUESTNUMBER environment variable. + --spec-repo-root Root path of the repository containing the swagger files. Defaults to the root of the repository containing this script.`); +} + +const { + values: { + output: outputDir, + "build-id": buildId, + "spec-repo-name": specRepoName, + "spec-repo-pr-number": specRepoPrNumber, + "spec-repo-root": specRepoRoot, + }, +} = parseArgs({ + options: { + output: { type: "string", default: "" }, + "build-id": { + type: "string", + default: process.env.BUILD_BUILDID || "", + }, + "spec-repo-name": { + type: "string", + default: process.env.BUILD_REPOSITORY_NAME || "", + }, + "spec-repo-pr-number": { + type: "string", + default: process.env.SYSTEM_PULLREQUEST_PULLREQUESTNUMBER || "", + }, + "spec-repo-root": { + type: "string", + default: resolve(__dirname, "../../../"), + }, + }, + allowPositionals: false, +}); + +let validArgs = true; + +if (!outputDir) { + console.log(`Missing required parameter --output. Value given: ${outputDir || ""}`); + validArgs = false; +} + +if (!specRepoName) { + console.log( + `Missing required parameter --spec-repo-name. Value given: ${specRepoName || ""}`, + ); + validArgs = false; +} + +if (!specRepoPrNumber) { + console.log( + `Missing required parameter --spec-repo-pr-number. Value given: ${specRepoPrNumber || ""}`, + ); + validArgs = false; +} + +if (!specRepoRoot) { + console.log(`Invalid parameter --spec-repo-root. Value given: ${specRepoRoot || ""}`); + validArgs = false; +} + +if (!validArgs) { + usage(); + process.exit(1); +} + +// Get selected version and swaggers to process + +const changedFileStatuses = await getChangedFilesStatuses({ + cwd: specRepoRoot, + paths: ["specification"], +}); + +// Exclude deleted files as they are not relevant for generating documentation. +const changedFiles = [ + ...changedFileStatuses.additions, + ...changedFileStatuses.modifications, + // Current names of renamed files are interesting, previous names are not. + ...changedFileStatuses.renames.map((r) => r.to), +]; +console.log(`Found ${changedFiles.length} relevant changed files in ${specRepoRoot}`); +console.log("Changed files:"); +changedFiles.forEach((file) => console.log(` - ${file}`)); +const swaggerPaths = changedFiles.filter(swagger); + +if (swaggerPaths.length === 0) { + console.log("No eligible swagger files found. No documentation artifacts will be written."); + process.exit(0); +} + +const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(swaggerPaths); + +const repoName = specRepoName; +const prNumber = specRepoPrNumber; + +await mkdir(outputDir, { recursive: true }); +await writeFile( + join(outputDir, "repo.json"), + JSON.stringify(repoJSONTemplate(repoName, prNumber), null, 2), +); +await writeFile( + join(outputDir, "mapping.json"), + JSON.stringify(mappingJSONTemplate(swaggersToProcess), null, 2), +); +await writeFile(join(outputDir, "index.md"), indexMd(buildId, repoName, prNumber)); +console.log(`Documentation preview artifacts written to ${outputDir}`); + +console.log(`Doc preview for API version ${selectedVersion} includes:`); +swaggersToProcess.forEach((swagger) => console.log(` - ${swagger}`)); +console.log(`Artifacts written to: ${outputDir}`); diff --git a/.github/shared/package-lock.json b/.github/shared/package-lock.json index 308e5e1b9ad2..af2795689f9d 100644 --- a/.github/shared/package-lock.json +++ b/.github/shared/package-lock.json @@ -13,6 +13,7 @@ "simple-git": "^3.27.0" }, "bin": { + "api-doc-preview": "cmd/api-doc-preview.js", "spec-model": "cmd/spec-model.js" }, "devDependencies": { diff --git a/.github/shared/package.json b/.github/shared/package.json index 52f96ba59d1a..b82abb44c7aa 100644 --- a/.github/shared/package.json +++ b/.github/shared/package.json @@ -23,7 +23,8 @@ "./test/examples": "./test/examples.js" }, "bin": { - "spec-model": "./cmd/spec-model.js" + "spec-model": "./cmd/spec-model.js", + "api-doc-preview": "./cmd/api-doc-preview.js" }, "_comments": { "dependencies": "Runtime dependencies must be kept to an absolute minimum for performance, ideally with no transitive dependencies", diff --git a/.github/shared/src/changed-files.js b/.github/shared/src/changed-files.js index 5641a95e64ed..4561c394116f 100644 --- a/.github/shared/src/changed-files.js +++ b/.github/shared/src/changed-files.js @@ -8,6 +8,8 @@ import { includesFolder } from "./path.js"; debug.enable("simple-git"); /** + * Get a list of changed files in a git repository + * * @param {Object} [options] * @param {string} [options.baseCommitish] Default: "HEAD^". * @param {string} [options.cwd] Current working directory. Default: process.cwd(). @@ -32,7 +34,6 @@ export async function getChangedFiles(options = {}) { const result = await simpleGit(cwd).diff(["--name-only", baseCommitish, headCommitish, ...paths]); const files = result.trim().split("\n"); - logger?.info("Changed Files:"); for (const file of files) { logger?.info(` ${file}`); @@ -43,6 +44,10 @@ export async function getChangedFiles(options = {}) { } /** + * Get a list of changed files in a git repository with statuses for additions, + * modifications, deletions, and renames. Warning: rename behavior can vary + * based on the git client's configuration of diff.renames. + * * @param {Object} [options] * @param {string} [options.baseCommitish] Default: "HEAD^". * @param {string} [options.cwd] Current working directory. Default: process.cwd(). @@ -210,6 +215,14 @@ export function typespec(file) { ); } +/** + * @param {string} [file] + * @returns {boolean} + */ +export function quickstartTemplate(file) { + return typeof file === "string" && json(file) && file.includes("/quickstart-templates/"); +} + /** * @param {string} [file] * @returns {boolean} @@ -220,6 +233,7 @@ export function swagger(file) { json(file) && (dataPlane(file) || resourceManager(file)) && !example(file) && + !quickstartTemplate(file) && !scenario(file) ); } diff --git a/.github/shared/src/doc-preview.js b/.github/shared/src/doc-preview.js new file mode 100644 index 000000000000..1ac67eb7ee36 --- /dev/null +++ b/.github/shared/src/doc-preview.js @@ -0,0 +1,167 @@ +// @ts-check +const DOCS_NAMESPACE = "_swagger_specs"; +const SPEC_FILE_REGEX = + "(specification/)+(.*)/(resourcemanager|resource-manager|dataplane|data-plane|control-plane)/(.*)/(preview|stable|privatepreview)/(.*?)/(example)?(.*)"; + +/** + * @typedef {Object} SwaggerFileMetadata + * @property {string} path + * @property {string} serviceName + * @property {string} serviceType + * @property {string} resourceProvider + * @property {string} releaseState + * @property {string} apiVersion + * @property {string} fileName + */ + +/** + * @typedef {Object} RepoJSONTemplate + * @property {Object[]} repo + * @property {string} repo[].url + * @property {string} repo[].prNumber + * @property {string} repo[].name + */ + +/** + * @typedef {Object} MappingJSONStructure + * @property {string} target_api_root_dir + * @property {boolean} enable_markdown_fragment + * @property {string} markdown_fragment_folder + * @property {boolean} use_yaml_toc + * @property {boolean} formalize_url + * @property {string[]} version_list + * @property {Object[]} organizations + * @property {string} organizations[].index + * @property {string} organizations[].default_toc_title + * @property {string} organizations[].version + * @property {Object[]} organizations[].services + * @property {string} organizations[].services[].toc_title + * @property {string} organizations[].services[].url_group + * @property {Object[]} organizations[].services[].swagger_files + * @property {string} organizations[].services[].swagger_files[].source + */ + +/** + * Extract swagger file metadata from path. + * @param {string} specPath + * @returns {SwaggerFileMetadata} + */ +export function parseSwaggerFilePath(specPath) { + const m = specPath.match(SPEC_FILE_REGEX); + if (!m) { + throw new Error(`Path "${specPath}" does not match expected swagger file pattern.`); + } + const [path, , serviceName, serviceType, resourceProvider, releaseState, apiVersion, , fileName] = + m; + return { + path, + serviceName, + serviceType, + resourceProvider, + releaseState, + apiVersion, + fileName, + }; +} + +/** + * @param {string} repoName + * @param {string} prNumber + * @returns {object} + */ +export function repoJSONTemplate(repoName, prNumber) { + return { + repo: [ + { + url: `https://github.com/${repoName}`, + prNumber: prNumber, + name: DOCS_NAMESPACE, + }, + ], + }; +} + +/** + * @param {string[]} files + * @returns {MappingJSONStructure} + */ +export function mappingJSONTemplate(files) { + return { + target_api_root_dir: "structured", + enable_markdown_fragment: true, + markdown_fragment_folder: "authored", + use_yaml_toc: true, + formalize_url: true, + version_list: ["default"], + organizations: [ + { + index: "index.md", + default_toc_title: "Getting Started", + version: "default", + services: [ + { + toc_title: "Documentation Preview", + url_group: "documentation-preview", + swagger_files: files.map((source) => ({ + source: `${DOCS_NAMESPACE}/${source}`, + })), + }, + ], + }, + ], + }; +} + +/** + * @param {string} buildId + * @param {string} repoName + * @param {string} prNumber + * @returns {string} + */ +export function indexMd(buildId, repoName, prNumber) { + return `# Documentation Preview for swagger pipeline build #${buildId} + +Welcome to documentation preview for ${repoName}/pull/${prNumber} +created via the swagger pipeline. + +Your documentation may be viewed in the menu on the left hand side. + +If you have issues around documentation generation, please feel free to contact +us in the [Docs Support Teams Channel](https://aka.ms/ci-fix/api-docs-help)`; +} + +/** + * Given a list of changed swagger files, select an API version and a list of + * swagger files in that API version to process. + * @param {string[]} swaggerFiles + **/ +export function getSwaggersToProcess(swaggerFiles) { + const swaggerFileObjs = swaggerFiles.map(parseSwaggerFilePath); + + const versions = swaggerFileObjs.map((obj) => obj.apiVersion).filter(Boolean); + if (versions.length === 0) { + throw new Error("No new API versions found in eligible swagger files."); + } + const uniqueVersions = Array.from(new Set(versions)); + + let selectedVersion; + if (uniqueVersions.length === 1) { + selectedVersion = uniqueVersions[0]; + console.log(`Single API version found: ${selectedVersion}`); + } else { + // This sorting logic is ported from the original code which sorts only the + // strings and doesn't attempt to parse versions for more semantically-aware + // sorting. + const sortedVersions = [...uniqueVersions].sort(); + selectedVersion = sortedVersions[sortedVersions.length - 1]; + console.log( + `Multiple API versions found: ${JSON.stringify(sortedVersions)}. Selected version: ${selectedVersion}`, + ); + } + + const swaggersToProcess = swaggerFileObjs + .filter((obj) => obj.apiVersion === selectedVersion) + .map((obj) => obj.path); + + return { selectedVersion, swaggersToProcess }; +} diff --git a/.github/shared/test/changed-files.test.js b/.github/shared/test/changed-files.test.js index 92bd84b827db..e98dadff5d9d 100644 --- a/.github/shared/test/changed-files.test.js +++ b/.github/shared/test/changed-files.test.js @@ -16,6 +16,7 @@ import { getChangedFiles, getChangedFilesStatuses, json, + quickstartTemplate, readme, resourceManager, scenario, @@ -62,6 +63,7 @@ describe("changedFiles", () => { "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", "specification/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "specification/compute/quickstart-templates/swagger.json", ]; const filesResolved = files.map((f) => resolve(f)); @@ -77,6 +79,7 @@ describe("changedFiles", () => { "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/contoso.json", "specification/contosowidgetmanager/resource-manager/Microsoft.Contoso/stable/2021-11-01/examples/Employees_Get.json", "specification/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", + "specification/compute/quickstart-templates/swagger.json", ]; expect(files.filter(json)).toEqual(expected); @@ -140,6 +143,12 @@ describe("changedFiles", () => { expect(filesResolved.filter(example)).toEqual(expected.map((f) => resolve(f))); }); + it("filter:quickstartTemplate", () => { + const expected = ["specification/compute/quickstart-templates/swagger.json"]; + + expect(files.filter(quickstartTemplate)).toEqual(expected); + }); + it("filter:scenarios", () => { const expected = [ "not-spec/contosowidgetmanager/Contoso.Management/scenarios/2021-11-01/Employees_Get.json", diff --git a/.github/shared/test/doc-preview.test.js b/.github/shared/test/doc-preview.test.js new file mode 100644 index 000000000000..b0425c4f8b36 --- /dev/null +++ b/.github/shared/test/doc-preview.test.js @@ -0,0 +1,140 @@ +// @ts-check + +import { describe, expect, test } from "vitest"; +import { + getSwaggersToProcess, + indexMd, + mappingJSONTemplate, + parseSwaggerFilePath, + repoJSONTemplate, +} from "../src/doc-preview.js"; + +describe("parseSwaggerFilePath", () => { + test("throws null when given invalid path", () => { + expect(() => parseSwaggerFilePath("invalid/path/to/swagger.json")).toThrow(); + }); + + test("parses valid swagger file path", () => { + const path = + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json"; + const result = parseSwaggerFilePath(path); + + expect(result).toEqual({ + path: path, + serviceName: "batch", + serviceType: "data-plane", + resourceProvider: "Azure.Batch", + releaseState: "preview", + apiVersion: "2024-07-01.20.0", + fileName: "BatchService.json", + }); + }); +}); + +describe("getSwaggersToProcess", () => { + test("throws when swagger paths do not properly parse", () => { + expect(() => getSwaggersToProcess(["specification/inscrutable/path/swagger.json"])).toThrow(); + }); + + test("returns swaggers to process for valid files", () => { + const files = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + ]; + + const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(files); + + expect(selectedVersion).toEqual("2024-07-01.20.0"); + expect(swaggersToProcess).toEqual(files); + }); + + test("selects the latest version from multiple files with multiple versions", () => { + const files = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + "specification/batch/data-plane/Azure.Batch/preview/2025-06-01/BatchService.json", + ]; + + const { selectedVersion, swaggersToProcess } = getSwaggersToProcess(files); + + expect(selectedVersion).toEqual("2025-06-01"); + expect(swaggersToProcess).toEqual([files[1]]); + }); +}); + +describe("repoJSONTemplate", () => { + test("matches snapshot", () => { + const actual = repoJSONTemplate("test-repo", "1234"); + + expect(actual).toMatchInlineSnapshot(` + { + "repo": [ + { + "name": "_swagger_specs", + "prNumber": "1234", + "url": "https://github.com/test-repo", + }, + ], + } + `); + }); +}); + +describe("mappingJSONTemplate", () => { + test("matches snapshot", () => { + const swaggers = [ + "specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + ]; + const actual = mappingJSONTemplate(swaggers); + + expect(actual).toMatchInlineSnapshot(` + { + "enable_markdown_fragment": true, + "formalize_url": true, + "markdown_fragment_folder": "authored", + "organizations": [ + { + "default_toc_title": "Getting Started", + "index": "index.md", + "services": [ + { + "swagger_files": [ + { + "source": "_swagger_specs/specification/batch/data-plane/Azure.Batch/preview/2024-07-01.20.0/BatchService.json", + }, + ], + "toc_title": "Documentation Preview", + "url_group": "documentation-preview", + }, + ], + "version": "default", + }, + ], + "target_api_root_dir": "structured", + "use_yaml_toc": true, + "version_list": [ + "default", + ], + } + `); + }); +}); + +describe("indexMd", () => { + test("matches snapshot", () => { + const buildId = "build-123"; + const repoName = "test-repo"; + const prNumber = "1234"; + const actual = indexMd(buildId, repoName, prNumber); + + expect(actual).toMatchInlineSnapshot(` + "# Documentation Preview for swagger pipeline build #build-123 + + Welcome to documentation preview for test-repo/pull/1234 + created via the swagger pipeline. + + Your documentation may be viewed in the menu on the left hand side. + + If you have issues around documentation generation, please feel free to contact + us in the [Docs Support Teams Channel](https://aka.ms/ci-fix/api-docs-help)" + `); + }); +}); diff --git a/eng/pipelines/swagger-api-doc-preview.yml b/eng/pipelines/swagger-api-doc-preview.yml new file mode 100644 index 000000000000..8174542fd87f --- /dev/null +++ b/eng/pipelines/swagger-api-doc-preview.yml @@ -0,0 +1,227 @@ +trigger: none +pr: + paths: + include: + # Trigger for files that will result in a doc preview build + - specification/** + + # Smoke test on changed files + - eng/pipelines/swagger-api-doc-preview.yml + - eng/pipelines/templates/steps/set-sha-check.yml + - .github/shared/src/doc-preview.js + - .github/shared/cmd/api-doc-preview.js + +jobs: + - job: SwaggerApiDocPreview + + pool: + name: $(LINUXPOOL) + vmImage: $(LINUXVMIMAGE) + + variables: + - template: /eng/pipelines/templates/variables/globals.yml + - template: /eng/pipelines/templates/variables/image.yml + + - name: BranchName + value: preview/$(Build.Repository.Name)/pr/$(System.PullRequest.PullRequestNumber)/build/$(Build.BuildId)/attempt/$(System.JobAttempt) + + - name: CurrentBuildUrl + value: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId) + + - name: StatusName + value: 'Swagger ApiDocPreview' + + steps: + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + State: pending + TargetUrl: $(CurrentBuildUrl) + Description: 'Starting docs build' + Context: $(StatusName) + + - checkout: self + # Fetch depth required to get list of changed files + fetchDepth: 2 + + - template: /eng/common/pipelines/templates/steps/sparse-checkout.yml + parameters: + SkipCheckoutNone: true + TokenToUseForAuth: $(azuresdk-github-pat) + # Path does not need to be set because sparse-checkout.yml already + # checks out files in the repo root + Repositories: + - Name: MicrosoftDocs/AzureRestPreview + Commitish: main + WorkingDirectory: AzureRestPreview + + - template: /eng/pipelines/templates/steps/npm-install.yml + parameters: + WorkingDirectory: .github/shared + + - script: cmd/api-doc-preview.js --output ../../AzureRestPreview + displayName: Generate Swagger API documentation preview + workingDirectory: .github/shared + + - template: /eng/common/pipelines/templates/steps/git-push-changes.yml + parameters: + WorkingDirectory: AzureRestPreview + BaseRepoOwner: MicrosoftDocs + TargetRepoOwner: MicrosoftDocs + TargetRepoName: AzureRestPreview + BaseRepoBranch: $(BranchName) + CommitMsg: | + Update API Doc Preview + Build: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId) + PR: $(System.PullRequest.SourceRepositoryURI)/pull/$(System.PullRequest.PullRequestId) + + - task: AzureCLI@2 + displayName: Start docs build + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + inputs: + azureSubscription: msdocs-apidrop-connection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $buildStartRaw = az pipelines build queue ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --definition-id "8157" ` + --variables 'params={"target_repo":{"url":"https://github.com/MicrosoftDocs/AzureRestPreview","branch":"$(BranchName)"}, "source_of_truth": "code"}' + $buildStartRaw | Set-Content buildstart.json + $buildStart = $buildStartRaw | ConvertFrom-Json + Write-Host "Build started at https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + Condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + State: pending + TargetUrl: $(CurrentBuildUrl) + Description: 'Waiting for docs build to finish' + Context: $(StatusName) + + - task: AzureCLI@2 + displayName: Wait for docs build to finish + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + # Retry on failure to handle transient issues or builds that run longer + # than the token used by az CLI is valid + retryCountOnTaskFailure: 3 + inputs: + azureSubscription: msdocs-apidrop-connection + scriptType: pscore + scriptLocation: inlineScript + inlineScript: | + $buildStart = Get-Content buildstart.json | ConvertFrom-Json + + Write-Host "Waiting for build to finish: https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + # Timeout in 10 minutes to avoid infinite waiting + $start = Get-Date + while (((Get-Date) - $start).TotalMinutes -lt 10) { + $runStatusRaw = az pipelines runs show ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --id "$($buildStart.id)" + + if ($LASTEXITCODE) { + Write-Host "Failed to get run status" + Write-Host "Exit code: $LASTEXITCODE" + Write-Host "Output: $runStatusRaw" + exit 1 + } + + $runStatus = $runStatusRaw | ConvertFrom-Json + Write-Host "Run status: $($runStatus.status)" + + if ($runStatus.status -eq "completed") { + break; + } + Start-Sleep -Seconds 10 + } + + Write-Host "Build completed with status: $($runStatus.result)" + Write-Host "Build logs: https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + Write-Host "Downloading artifact..." + $artifactDownloadRaw = az pipelines runs artifact download ` + --organization "https://dev.azure.com/apidrop/" ` + --project "Content CI" ` + --run-id "$($buildStart.id)" ` + --artifact-name "report" ` + --path "./" + + if ($LASTEXITCODE) { + Write-Host "Failed to download artifact" + Write-Host "Exit code: $LASTEXITCODE" + Write-Host "Output: $artifactDownloadRaw" + exit 1 + } + + Write-Host "Artifact downloaded successfully" + + - pwsh: | + # Read the report.json file downloaded from the docs build artifact + $reportRaw = Get-Content -Path "./report.json" -Raw + $report = $reportRaw | ConvertFrom-Json -AsHashtable + + # Get build ID from buildstart.json (set during the "Start docs build" step) + $buildStart = Get-Content buildstart.json | ConvertFrom-Json + $buildLink = "https://dev.azure.com/apidrop/Content%20CI/_build/results?buildId=$($buildStart.id)" + + if ($report.status -ne "Succeeded") { + Write-Host "Docs build failed with status: $($report.status)" + Write-Host "Report:" + Write-Host $reportRaw + + Write-Host "##vso[task.setvariable variable=CheckUrl]${buildLink}" + Write-Host "##vso[task.setvariable variable=CheckDescription]Docs build failed (click to see pipeline logs)" + Write-Host "##vso[task.setvariable variable=CheckState]failure" + + # Docs build failed, but this job should not fail unless it + # encounters an unexpected error. The check status will be set in + # the next task. + exit 0 + } + + Write-Host "Docs build succeeded with status: $($report.status)" + + $docsPreviewUrl = "https://review.learn.microsoft.com/en-us/rest/api/azure-rest-preview/?branch=$([System.Web.HttpUtility]::UrlEncode($report.branch))&view=azure-rest-preview" + Write-Host "##vso[task.setvariable variable=CheckUrl]$docsPreviewUrl" + Write-Host "##vso[task.setvariable variable=CheckDescription]Docs build succeeded (click to see preview)" + Write-Host "##vso[task.setvariable variable=CheckState]success" + Write-Host "Docs preview URL: $docsPreviewUrl" + + exit 0 + displayName: Interpret docs build results + condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + + # Sets check status from docs build using $(CheckUrl), $(CheckState), and + # $(CheckDescription) variables set by api-doc-preview-interpret.js. + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status from docs build + Condition: and(succeeded(), eq(variables['HasChanges'], 'true')) + State: $(CheckState) + TargetUrl: $(CheckUrl) + Description: $(CheckDescription) + Context: $(StatusName) + + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status for no-op + Condition: and(succeeded(), ne(variables['HasChanges'], 'true')) + State: success + TargetUrl: $(CurrentBuildUrl) + Description: No files changed require docs build + Context: $(StatusName) + + # In the event of a failure in this job (not the docs build job), set the + # PR status to failed and link to the current build. + - template: /eng/pipelines/templates/steps/set-sha-check.yml + parameters: + DisplayName: Set PR status for job failure + # Only run if a previous step in the job failed + Condition: failed() + State: failure + TargetUrl: $(CurrentBuildUrl) + Description: 'Orchestration build failed (click to see logs)' + Context: $(StatusName) diff --git a/eng/pipelines/templates/steps/npm-install.yml b/eng/pipelines/templates/steps/npm-install.yml index c64de780bc12..399d58b558fa 100644 --- a/eng/pipelines/templates/steps/npm-install.yml +++ b/eng/pipelines/templates/steps/npm-install.yml @@ -2,6 +2,9 @@ parameters: - name: NodeVersion type: string default: $(NodeVersion) + - name: WorkingDirectory + type: string + default: $(Build.SourcesDirectory) steps: - template: /eng/pipelines/templates/steps/use-node-version.yml @@ -10,7 +13,9 @@ steps: - script: npm ci displayName: npm ci + workingDirectory: ${{ parameters.WorkingDirectory }} - script: npm ls -a || true displayName: npm ls -a condition: succeededOrFailed() + workingDirectory: ${{ parameters.WorkingDirectory }} diff --git a/eng/pipelines/templates/steps/set-sha-check.yml b/eng/pipelines/templates/steps/set-sha-check.yml new file mode 100644 index 000000000000..ae5845922de0 --- /dev/null +++ b/eng/pipelines/templates/steps/set-sha-check.yml @@ -0,0 +1,64 @@ +# Create a "status" check for a SHA (inferred from PR by default) in a GitHub +# repository. By default this uses the azure-sdk account. This might not work +# for required checks where a source must be configured. + +parameters: + - name: Sha + type: string + default: $(System.PullRequest.SourceCommitId) + + - name: RepositoryName + type: string + default: $(Build.Repository.Name) + + - name: State + type: string + default: 'pending' + # Valid values: + # - error + # - failure + # - pending + # - success + + - name: TargetUrl + type: string + default: '' + + - name: Description + type: string + default: '' + + - name: Context + type: string + default: default context + + - name: Condition + type: string + default: succeeded() + + - name: DisplayName + type: string + default: 'Set PR status' + + - name: GitHubToken + type: string + default: $(azuresdk-github-pat) + +steps: + - bash: | + echo "Repository Name: ${{ parameters.RepositoryName }}" + echo "Commit ID: ${{ parameters.Sha }}" + echo "State: ${{ parameters.State }}" + echo "Target URL: ${{ parameters.TargetUrl }}" + echo "Description: ${{ parameters.Description }}" + echo "Context: ${{ parameters.Context }}" + + gh api repos/${{ parameters.RepositoryName }}/statuses/${{ parameters.Sha }} \ + -f state='${{ parameters.State }}' \ + -f target_url='${{ parameters.TargetUrl }}' \ + -f description='${{ parameters.Description }}' \ + -f context='${{ parameters.Context }}' + displayName: ${{ parameters.DisplayName }} + condition: ${{ parameters.Condition }} + env: + GH_TOKEN: ${{ parameters.GitHubToken }} diff --git a/package-lock.json b/package-lock.json index 097d5ed5e6f8..ee7ac1bb1e6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,6 +51,7 @@ "simple-git": "^3.27.0" }, "bin": { + "api-doc-preview": "cmd/api-doc-preview.js", "spec-model": "cmd/spec-model.js" }, "devDependencies": {