diff --git a/.github/workflows/release_to_azure_devops.yaml b/.github/workflows/release_to_azure_devops.yaml index 6896ac5..6a40da2 100644 --- a/.github/workflows/release_to_azure_devops.yaml +++ b/.github/workflows/release_to_azure_devops.yaml @@ -66,20 +66,8 @@ jobs: run: node ../../.github/workflows/update-task-version.js ${{ env.EXT_VERSION }} task.json working-directory: src/format-check - - name: Clean dev dependencies - run: npm prune --production - working-directory: src/format-check - - - name: Remove .test.ts files - run: find . -type f -name "*.test.ts" -delete - working-directory: src/format-check - - - name: Remove common no-code directories from node_modules - run: find . -type d \( -name "docs" -o -name "doc" -o -name "test" -o -name "examples" \) -exec rm -rf {} + - working-directory: src/format-check - - - name: Compile TypeScript - run: tsc + - name: Build task bundle + run: npm run build working-directory: src/format-check - name: Install tfx-cli diff --git a/.gitignore b/.gitignore index b855ed7..4060fe1 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ yarn-debug.log* yarn-error.log* /dist +dist \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c2ca063..85fa935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2578,9 +2578,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", "dev": true, "license": "MIT", "dependencies": { @@ -3225,9 +3225,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -4513,9 +4513,9 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4526,9 +4526,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/format-check/package-lock.json b/src/format-check/package-lock.json index a323833..1375072 100644 --- a/src/format-check/package-lock.json +++ b/src/format-check/package-lock.json @@ -14,6 +14,7 @@ "@types/jest": "^29.5.14", "@types/node": "^22.15.21", "@types/node-fetch": "^2.6.12", + "@vercel/ncc": "^0.38.4", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", "ts-jest": "^29.3.4", @@ -947,6 +948,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@vercel/ncc": { + "version": "0.38.4", + "resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.4.tgz", + "integrity": "sha512-8LwjnlP39s08C08J5NstzriPvW1SP8Zfpp1BvC2sI35kPeZnHfxVkCwu4/+Wodgnd60UtT1n8K8zw+Mp7J9JmQ==", + "dev": true, + "license": "MIT", + "bin": { + "ncc": "dist/ncc/cli.js" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "dev": true, @@ -3358,9 +3369,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/src/format-check/package.json b/src/format-check/package.json index e0883c6..1868255 100644 --- a/src/format-check/package.json +++ b/src/format-check/package.json @@ -9,13 +9,15 @@ "@types/jest": "^29.5.14", "@types/node": "^22.15.21", "@types/node-fetch": "^2.6.12", + "@vercel/ncc": "^0.38.4", "jest": "^29.7.0", "jest-fetch-mock": "^3.0.3", "ts-jest": "^29.3.4", "typescript": "^5.8.3" }, "scripts": { - "test": "jest" + "test": "jest", + "build": "ncc build scripts/main.ts -o dist" }, "jest": { "preset": "ts-jest", diff --git a/src/format-check/scripts/format-check.js b/src/format-check/scripts/format-check.js new file mode 100644 index 0000000..ec23b83 --- /dev/null +++ b/src/format-check/scripts/format-check.js @@ -0,0 +1,493 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.runFormatCheck = runFormatCheck; +exports.getChangedFilesInPR = getChangedFilesInPR; +exports.updatePullRequestThreads = updatePullRequestThreads; +exports.markResolvedThreadsAsClosed = markResolvedThreadsAsClosed; +exports.getStatusDescription = getStatusDescription; +exports.setPullRequestStatusAndDetermineShouldFailTask = setPullRequestStatusAndDetermineShouldFailTask; +var gi = __importStar(require("azure-devops-node-api/interfaces/GitInterfaces")); +var pull_request_file_change_1 = require("./types/pull-request-file-change"); +var pull_request_service_1 = require("./services/pull-request-service"); +var format_check_runner_1 = require("./services/format-check-runner"); +var path_normalizer_1 = require("./utils/path-normalizer"); +var commentPreamble = '[DotNetFormatTask][Automated]'; +/** + * The `runFormatCheck` is an asynchronous function that performs a formatting check on certain code files and updates pull request status accordingly. + * + * The function follows these main steps: + * 1. Initializes and starts a format check runner with appropriate file paths provided via Settings. + * 2. Generates annotated reports for the files checked. + * 3. If the scope is set to Pull Request, it retrieves the list of files involved in the Pull Request and updates the reports accordingly. + * 4. Updates the Pull Request comment threads based on the resulting check report. + * 5. Sets the final Pull Request status based on the check outcome and determines if the task should fail. + * + * @async + * @function runFormatCheck + * @param {Settings} settings - Settings object containing parameters needed for the format check runner and Pull Request Service. + * @returns {Promise} - Promise resolving to a boolean indicating if the task should fail due to formatting errors. + */ +function runFormatCheck(settings) { + return __awaiter(this, void 0, void 0, function () { + var pullRequestService, runner, reports, annotatedReports, changedInPR_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, (0, pull_request_service_1.getPullRequestService)(settings)]; + case 1: + pullRequestService = _a.sent(); + if (!settings.Parameters.statusCheck) return [3 /*break*/, 3]; + return [4 /*yield*/, pullRequestService.updatePullRequestStatus(gi.GitStatusState.Pending, getStatusDescription)]; + case 2: + _a.sent(); + _a.label = 3; + case 3: + runner = new format_check_runner_1.FormatCheckRunner(settings.Parameters.solutionPath, settings.Parameters.includePath, settings.Parameters.excludePath); + return [4 /*yield*/, runner.runFormatCheck()]; + case 4: + reports = _a.sent(); + annotatedReports = reports.map(function (r) { + return __assign(__assign({}, r), { FilePath: new path_normalizer_1.PathNormalizer(settings).normalizeFilePath(r.FilePath), commitId: '', changeType: gi.VersionControlChangeType.None }); + }); + if (!settings.Parameters.scopeToPullRequest) return [3 /*break*/, 6]; + console.log("Scoping issues to files part of the Pull Request."); + return [4 /*yield*/, getChangedFilesInPR(pullRequestService, settings)]; + case 5: + changedInPR_1 = _a.sent(); + annotatedReports = annotatedReports.map(function (report) { + var change = changedInPR_1.find(function (c) { return c.FilePath === report.FilePath; }); + if (change) { + report.commitId = change.CommitId; + report.changeType = change.changeType; + } + return report; + }).filter(function (x) { + var include = changedInPR_1.some(function (c) { return c.FilePath === x.FilePath; }); + if (include) { + console.log("\u2714 Include file: ".concat(x.FilePath)); + } + else { + console.log("\u274C Exclude file: ".concat(x.FilePath)); + } + return include; + }); + // Filter format issues to only include those in changed lines + annotatedReports = annotatedReports.map(function (report) { + var change = changedInPR_1.find(function (c) { return c.FilePath === report.FilePath; }); + if (change && change.lineChanges.length > 0) { + report.FileChanges = report.FileChanges.filter(function (changeData) { + return change.lineChanges.includes(changeData.LineNumber); + }); + } + return report; + }).filter(function (report) { return report.FileChanges.length > 0; }); + _a.label = 6; + case 6: + // Update the Pull Request comment threads based on the format check reports + return [4 /*yield*/, updatePullRequestThreads(pullRequestService, annotatedReports)]; + case 7: + // Update the Pull Request comment threads based on the format check reports + _a.sent(); + return [4 /*yield*/, setPullRequestStatusAndDetermineShouldFailTask(pullRequestService, annotatedReports.length > 0, settings.Parameters.failOnFormattingErrors, settings.Parameters.statusCheck)]; + case 8: + // Set final Pull Request status and determine if the task should fail + return [2 /*return*/, _a.sent()]; + } + }); + }); +} +/** + * The `getChangedFilesInPR` function retrieves + * all pull request file changes with respect to their commits it handles. + * + * @param {PullRequestService} pullRequestUtils - An instance of PullRequestService to interact with Pull Request API + * @param {Settings} settings + * @return {Promise} - Returns a Promise that resolves to an array of file changes. + * Each record denotes a path, commit id and change type in a file as a result of a commit. + * + * @description + * Function operates asynchronously, because of the nature of its service calls to GitService and PullRequestService. + */ +function getChangedFilesInPR(pullRequestUtils, settings) { + return __awaiter(this, void 0, void 0, function () { + var pullRequestChanges, maxRetries, attempt, error_1, files, _loop_1, _i, _a, change; + var _b, _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + console.log("Getting the PR commits..."); + maxRetries = 3; + attempt = 0; + _d.label = 1; + case 1: + if (!(attempt < maxRetries)) return [3 /*break*/, 8]; + _d.label = 2; + case 2: + _d.trys.push([2, 4, , 7]); + return [4 /*yield*/, pullRequestUtils.getPullRequestChanges()]; + case 3: + // Due to intermittent issues with Azure DevOps API (ECONNRESET), we implement a retry mechanism + pullRequestChanges = _d.sent(); + return [3 /*break*/, 8]; + case 4: + error_1 = _d.sent(); + attempt++; + console.warn("Attempt ".concat(attempt, " to getPullRequestChanges failed: ").concat(error_1)); + if (!(attempt < maxRetries)) return [3 /*break*/, 6]; + return [4 /*yield*/, new Promise(function (res) { return setTimeout(res, 1000 * attempt); })]; + case 5: + _d.sent(); + _d.label = 6; + case 6: return [3 /*break*/, 7]; + case 7: return [3 /*break*/, 1]; + case 8: + if (!pullRequestChanges) { + throw new Error("Pull request changes could not be retrieved."); + } + files = []; + _loop_1 = function (change) { + if (change.item.path == undefined) { + console.warn("Warning: File path is undefined for commit id " + ((_b = change.item) === null || _b === void 0 ? void 0 : _b.commitId)); + return "continue"; + } + var normalizedPath = new path_normalizer_1.PathNormalizer(settings).normalizeFilePath(change.item.path); + // Extract line changes if available + var lineChanges = []; + if (change) { + // Check if the change has a 'changes' property and handle it + if (change.changes && Array.isArray(change.changes)) { + for (var _e = 0, _f = change.changes; _e < _f.length; _e++) { + var fileChange = _f[_e]; + if (fileChange.addedLines && Array.isArray(fileChange.addedLines)) { + fileChange.addedLines.forEach(function (line) { + if (line && line.lineNumber > 0) { + lineChanges.push(line.lineNumber); + } + }); + } + if (fileChange.removedLines && Array.isArray(fileChange.removedLines)) { + fileChange.removedLines.forEach(function (line) { + if (line && line.lineNumber > 0) { + lineChanges.push(line.lineNumber); + } + }); + } + } + } + } + files.push(new pull_request_file_change_1.PullRequestFileChange(normalizedPath, (_c = change.item) === null || _c === void 0 ? void 0 : _c.commitId, change.changeType, lineChanges)); + }; + for (_i = 0, _a = pullRequestChanges; _i < _a.length; _i++) { + change = _a[_i]; + _loop_1(change); + } + console.log("All changed files considered to be part of this Pull Request: "); + files.forEach(function (file) { + console.log("".concat(file.FilePath, " - ").concat(file.changeType, " - ").concat(file.CommitId)); + if (file.lineChanges.length > 0) { + console.log(" Changed lines: ".concat(file.lineChanges.join(', '))); + } + }); + return [2 /*return*/, files]; + } + }); + }); +} +/** + * The `updatePullRequestThreads` function updates the pull request comment threads based on given format check reports. + * + * @param {PullRequestService} pullRequestService - An instance of PullRequestService to interact with the Pull Request API. + * @param {AnnotatedReports} reports - AnnotatedReports object containing information about every commit that contains formatting issues. + * + * @returns {Promise} - Returns a Promise that will be fulfilled after all the pull request comment threads have been updated. + * + * @description + * This function operates asynchronously due to the nature of its service calls to the PullRequestService. + * It fetches existing threads and for each report in `reports`, it generates a content for comments which keeps the track of active issues. + * If there is an existing thread for the active issue, it updates that thread; if not, it creates a new thread for the issue. + * Lastly, it also closes threads for resolved issues. + */ +function updatePullRequestThreads(pullRequestService, reports) { + return __awaiter(this, void 0, void 0, function () { + var existingThreads, activeIssuesContent, _i, reports_1, report, _loop_2, _a, _b, change; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + console.log("Fetching existing threads."); + return [4 /*yield*/, pullRequestService.getThreads()]; + case 1: + existingThreads = _c.sent(); + console.log("Completed fetching existing threads."); + activeIssuesContent = []; + _i = 0, reports_1 = reports; + _c.label = 2; + case 2: + if (!(_i < reports_1.length)) return [3 /*break*/, 7]; + report = reports_1[_i]; + _loop_2 = function (change) { + var content, existingThread, comment, thread, thread; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + content = "".concat(commentPreamble, " ").concat(change.DiagnosticId, ": ").concat(change.FormatDescription, " on line ").concat(change.LineNumber, ", position ").concat(change.CharNumber); + activeIssuesContent.push(content); // Keep track of active issues + existingThread = existingThreads.find(function (thread) { var _a; return (_a = thread.comments) === null || _a === void 0 ? void 0 : _a.some(function (comment) { return comment.content === content; }); }); + comment = { + content: content, + commentType: gi.CommentType.Text + }; + if (!existingThread) return [3 /*break*/, 2]; + console.log("Updating existing thread."); + if (!existingThread.id) { + throw new Error("Existing thread id is not set."); + } + thread = { + status: gi.CommentThreadStatus.Active, + lastUpdatedTime: new Date() + }; + return [4 /*yield*/, pullRequestService.updateThread(thread, existingThread.id)]; + case 1: + _d.sent(); + return [3 /*break*/, 4]; + case 2: + if (report.changeType === gi.VersionControlChangeType.Delete) { + console.log("Skipping creating thread for deleted file ".concat(report.FilePath, ".")); + return [2 /*return*/, "continue"]; + } + console.log("\uD83D\uDCDD Creating new thread for file ".concat(report.FilePath, ".")); + thread = { + comments: [comment], + status: gi.CommentThreadStatus.Active, + threadContext: { + filePath: report.FilePath, + rightFileStart: { line: change.LineNumber, offset: change.CharNumber }, + rightFileEnd: { line: change.LineNumber, offset: change.CharNumber + 1 } + } + }; + return [4 /*yield*/, pullRequestService.createThread(thread)]; + case 3: + _d.sent(); + _d.label = 4; + case 4: return [2 /*return*/]; + } + }); + }; + _a = 0, _b = report.FileChanges; + _c.label = 3; + case 3: + if (!(_a < _b.length)) return [3 /*break*/, 6]; + change = _b[_a]; + return [5 /*yield**/, _loop_2(change)]; + case 4: + _c.sent(); + _c.label = 5; + case 5: + _a++; + return [3 /*break*/, 3]; + case 6: + _i++; + return [3 /*break*/, 2]; + case 7: + // Close threads for resolved issues + return [4 /*yield*/, markResolvedThreadsAsClosed(pullRequestService, existingThreads, activeIssuesContent)]; + case 8: + // Close threads for resolved issues + _c.sent(); + return [2 /*return*/]; + } + }); + }); +} +/** + * Asynchronously marks threads that were previously marked as resolved now as closed. + * + * @param {PullRequestService} pullRequestService - An instance of PullRequestService to interact with the Pull Request API. + * @param {gi.GitPullRequestCommentThread[]} existingThreads - The current set of existing pull request comment threads. + * @param {string[]} activeIssuesContent - The content of comments that have active issues. + * + * @description + * Iterates over the existing threads filtered for those starting with a specific preamble. + * If the existing thread status is set to closed, it is ignored. Else, for each existing thread, its content is compared with the + * contents of active issues. If a particular threads content does not match, it is considered as resolved and hence marked as closed. + */ +function markResolvedThreadsAsClosed(pullRequestService, existingThreads, activeIssuesContent) { + return __awaiter(this, void 0, void 0, function () { + var _i, _a, existingThread, threadContent, closedThread; + var _b, _c; + return __generator(this, function (_d) { + switch (_d.label) { + case 0: + _i = 0, _a = existingThreads.filter(function (thread) { var _a; return (_a = thread.comments) === null || _a === void 0 ? void 0 : _a.some(function (comment) { var _a; return (_a = comment.content) === null || _a === void 0 ? void 0 : _a.startsWith(commentPreamble); }); }); + _d.label = 1; + case 1: + if (!(_i < _a.length)) return [3 /*break*/, 4]; + existingThread = _a[_i]; + console.log("Processing the existing thread for file ".concat((_b = existingThread.threadContext) === null || _b === void 0 ? void 0 : _b.filePath, ".")); + if (existingThread.status === gi.CommentThreadStatus.Closed) { + return [3 /*break*/, 3]; + } + threadContent = (_c = existingThread.comments[0]) === null || _c === void 0 ? void 0 : _c.content; + if (!(threadContent && !activeIssuesContent.includes(threadContent))) return [3 /*break*/, 3]; + console.log("🔒 Closing resolved thread."); + if (!existingThread.id) { + throw new Error("Existing thread id is not set."); + } + closedThread = { + status: gi.CommentThreadStatus.Closed + }; + return [4 /*yield*/, pullRequestService.updateThread(closedThread, existingThread.id)]; + case 2: + _d.sent(); + _d.label = 3; + case 3: + _i++; + return [3 /*break*/, 1]; + case 4: return [2 /*return*/]; + } + }); + }); +} +/** + * Function `getStatusDescription` provides human-reader friendly status messages based on git status state. + * @param {gi.GitStatusState} status - determines a state of the Git. + * @returns {string} - corresponding message to the Git state. + */ +function getStatusDescription(status) { + switch (status) { + case gi.GitStatusState.Pending: + return "Format check is running"; + case gi.GitStatusState.Failed: + return "Formatting errors found"; + case gi.GitStatusState.Error: + return "Formatting task failed with an error."; + default: + return "No formatting errors found"; + } +} +/** + * An asynchronous function named `setPullRequestStatusAndDetermineShouldFailTask` is used for setting the status for pull requests and determining if the task should fail or not. + * + * @param {PullRequestService} pullRequestService - An instance of PullRequestService which is used to handle interactions with the Pull Request API. + * @param {boolean} formatIssuesExist - It denotes whether issues related with code format exist or not. + * @param {boolean} failOnFormattingErrors - It signifies whether the formatting errors should cause the function to fail. + * @param {boolean} setStatusCheck - It signifies whether the status check is to be set or not. + * @returns {Promise} - The function returns a promise that resolves to a boolean indicating if the task should fail based on the code format. + * + * @description + * This function makes use of console variable for logging and PullRequestService for setting the pull request status. In case of formatting errors and when set to fail on such errors, + * the function logs a failure message and subsequently sets the pull request status to 'Failed'. If there are no formatting issues or the function is not set to fail on such issues, + * the successful log message is printed and the pull request status is set to 'Succeeded'. + */ +function setPullRequestStatusAndDetermineShouldFailTask(pullRequestService, formatIssuesExist, failOnFormattingErrors, setStatusCheck) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!formatIssuesExist) return [3 /*break*/, 3]; + if (!setStatusCheck) return [3 /*break*/, 2]; + return [4 /*yield*/, pullRequestService.updatePullRequestStatus(gi.GitStatusState.Failed, getStatusDescription)]; + case 1: + _a.sent(); + _a.label = 2; + case 2: + if (failOnFormattingErrors) { + console.log("##vso[task.complete result=Failed;]Code format is incorrect."); + return [2 /*return*/, true]; + } + else { + console.log("##vso[task.complete result=Succeeded;]Code format is incorrect."); + return [2 /*return*/, false]; + } + return [3 /*break*/, 6]; + case 3: + console.log("##vso[task.complete result=Succeeded;]Code format is correct."); + if (!setStatusCheck) return [3 /*break*/, 5]; + return [4 /*yield*/, pullRequestService.updatePullRequestStatus(gi.GitStatusState.Succeeded, getStatusDescription)]; + case 4: + _a.sent(); + _a.label = 5; + case 5: return [2 /*return*/, false]; + case 6: return [2 /*return*/]; + } + }); + }); +} diff --git a/src/format-check/scripts/format-check.test.js b/src/format-check/scripts/format-check.test.js new file mode 100644 index 0000000..436fe29 --- /dev/null +++ b/src/format-check/scripts/format-check.test.js @@ -0,0 +1,422 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +jest.mock('fs'); +jest.mock('child_process'); +jest.mock('process'); +jest.mock('console', function () { return ({ + error: jest.fn(), +}); }); +jest.mock('node-fetch', function () { return jest.fn(function () { return Promise.resolve({ + json: function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, ({ + changes: [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: "somefile.ts", + commitId: "commit123" + }, + changes: [ + { + addedLines: [{ lineNumber: 10 }, { lineNumber: 11 }], + removedLines: [{ lineNumber: 5 }] + } + ] + } + ] + })]; + }); + }); }, +}); }); }); +var gi = __importStar(require("azure-devops-node-api/interfaces/GitInterfaces")); +var globals_1 = require("@jest/globals"); +var pull_request_file_change_1 = require("./types/pull-request-file-change"); +var format_check_1 = require("./format-check"); +var base_git_api_service_1 = require("./services/base-git-api-service"); +var fs_1 = __importDefault(require("fs")); +var crypto_1 = require("crypto"); +var child_process = __importStar(require("child_process")); +(0, globals_1.describe)('getChangedFilesInPR', function () { + // mock PullRequestService + var pullRequestServiceMock = { + getPullRequestChanges: jest.fn() + }; + var settingsMock = { + Environment: { + sourcesDirectory: "/path/to" + } + }; + (0, globals_1.beforeEach)(function () { + jest.resetAllMocks(); + // mock console + global.console.warn = jest.fn(); + }); + (0, globals_1.it)('should return the list of changed files in a PR', function () { return __awaiter(void 0, void 0, void 0, function () { + var changesListMock, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + changesListMock = [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: 'path/to/test.ts', + commitId: 'commit123' + } + } + ]; + pullRequestServiceMock.getPullRequestChanges.mockReturnValue(changesListMock); + return [4 /*yield*/, (0, format_check_1.getChangedFilesInPR)(pullRequestServiceMock, settingsMock)]; + case 1: + result = _a.sent(); + // Assert + (0, globals_1.expect)(result).toHaveLength(1); + (0, globals_1.expect)(result).toEqual([new pull_request_file_change_1.PullRequestFileChange('path/to/test.ts', 'commit123', gi.VersionControlChangeType.Edit, [])]); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should extract line changes from pull request changes', function () { return __awaiter(void 0, void 0, void 0, function () { + var changesListMock, result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + changesListMock = [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: 'path/to/test.ts', + commitId: 'commit123' + }, + changes: [ + { + addedLines: [{ lineNumber: 10 }, { lineNumber: 11 }], + removedLines: [{ lineNumber: 5 }] + } + ] + } + ]; + pullRequestServiceMock.getPullRequestChanges.mockReturnValue(changesListMock); + return [4 /*yield*/, (0, format_check_1.getChangedFilesInPR)(pullRequestServiceMock, settingsMock)]; + case 1: + result = _a.sent(); + // Assert + (0, globals_1.expect)(result).toHaveLength(1); + (0, globals_1.expect)(result[0].FilePath).toEqual('path/to/test.ts'); + // Sort the lineChanges array to ensure consistent order + (0, globals_1.expect)(result[0].lineChanges.sort(function (a, b) { return a - b; })).toEqual([5, 10, 11].sort(function (a, b) { return a - b; })); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should console warn if a change does not have a path', function () { return __awaiter(void 0, void 0, void 0, function () { + var changesListMock; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + changesListMock = [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: undefined, + commitId: 'commit123' + } + } + ]; + pullRequestServiceMock.getPullRequestChanges.mockReturnValue(changesListMock); + // Act + return [4 /*yield*/, (0, format_check_1.getChangedFilesInPR)(pullRequestServiceMock, settingsMock)]; + case 1: + // Act + _a.sent(); + // Assert + (0, globals_1.expect)(console.warn).toHaveBeenCalledWith("Warning: File path is undefined for commit id commit123"); + return [2 /*return*/]; + } + }); + }); }); +}); +(0, globals_1.describe)('runFormatCheck', function () { + var mockGitApi; + var mockSettings = { + Environment: { + orgUrl: "https://some-url/", + repoId: "rrrrr", + projectId: "ppppp", + pullRequestId: 123, + token: "some-token-secret", + sourcesDirectory: '/src', + pullRequestSourceCommit: '123123132123', + pullRequestTargetBranch: '/refs/heads/main' + }, + Parameters: { + statusCheck: true, + statusCheckContext: { + genre: "formatting", + name: "dotnet format check" + }, + scopeToPullRequest: true, + failOnFormattingErrors: false, + solutionPath: 'test', + includePath: 'test', + excludePath: 'test' + } + }; + (0, globals_1.beforeEach)(function () { + jest.resetAllMocks(); + mockGitApi = { + updatePullRequest: jest.fn(), + getPullRequestIterations: jest.fn(), + getPullRequest: jest.fn(), + getPullRequestById: jest.fn(), + createPullRequestStatus: jest.fn(), + getThreads: jest.fn(), + createThread: jest.fn(), + updateThread: jest.fn() + }; + }); + (0, globals_1.it)('should update PullRequest status and return false if there are format errors', function () { return __awaiter(void 0, void 0, void 0, function () { + var document, mockReport, mockGitCommitDiffs, _a, firstArg, mockReportWithMultipleIssues_1, _b, threadArgs; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: + jest.spyOn(base_git_api_service_1.BaseGitApiService, 'getGitApi').mockReturnValue(Promise.resolve(mockGitApi)); + fs_1.default.existsSync.mockReturnValue(true); // Mock return value + jest.spyOn(child_process, 'execSync').mockReturnValue(Buffer.from('')); + document = { + Id: (0, crypto_1.randomUUID)(), + ProjectId: { + Id: (0, crypto_1.randomUUID)() + } + }; + mockReport = [ + { + DocumentId: document, + FileName: "somefile.ts", + FilePath: "/src/somefile.ts", + FileChanges: [ + { + CharNumber: 1, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 2, + FormatDescription: "some error" + } + ] + }, + { + DocumentId: document, + FileName: "file-not-in-pr.ts", + FilePath: "/src/file-not-in-pr.ts", + FileChanges: [ + { + CharNumber: 5, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 1, + FormatDescription: "some error not relevant" + } + ] + } + ]; + fs_1.default.readFileSync.mockImplementation(function () { return JSON.stringify(mockReport); }); + fs_1.default.unlinkSync.mockImplementation(function () { }); + fs_1.default.existsSync.mockImplementation(function () { return true; }); + mockGitApi.updatePullRequest.mockImplementation(jest.fn()); + mockGitApi.getPullRequestIterations.mockReturnValueOnce([ + { + id: 1, + description: 'Initial commit', + author: { + displayName: 'Mock Author', + id: 'mock_author_123', + }, + updatedAt: new Date() + } + ]); + mockGitApi.getPullRequestIterations.mockReturnValueOnce([ + { + id: 1, + description: 'Initial commit', + author: { + displayName: 'Mock Author', + id: 'mock_author_123', + }, + updatedAt: new Date() + } + ]); + mockGitApi.getPullRequestById.mockReturnValue(Promise.resolve({ + status: 'active', + createdBy: { + displayName: 'Test User', + id: 'test_user_123', + }, + creationDate: new Date() + })); + mockGitCommitDiffs = { + changes: [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: "/somefile.ts", + commitId: (0, crypto_1.randomUUID)() + }, + changes: [ + { + addedLines: [{ lineNumber: 10 }, { lineNumber: 11 }], + removedLines: [{ lineNumber: 5 }] + } + ] + } + ] + }; + jest.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, + status: 200, + json: function () { return Promise.resolve(mockGitCommitDiffs); }, + headers: new Headers({ 'Content-Type': 'application/json' }) + }); + mockGitApi.getThreads.mockReturnValue(Promise.resolve([ + { + id: 1, + comments: [ + { + content: '[DotNetFormatTask][Automated] Test comment', + } + ] + } + ])); + _a = globals_1.expect; + return [4 /*yield*/, (0, format_check_1.runFormatCheck)(mockSettings)]; + case 1: + _a.apply(void 0, [_c.sent()]).toEqual(false); + if (!(mockGitApi.createThread.mock.calls.length > 0)) return [3 /*break*/, 3]; + firstArg = mockGitApi.createThread.mock.calls[0][0]; + // expect the first call to be for '/src/somefile.ts' + (0, globals_1.expect)(firstArg).toMatchObject({ + threadContext: globals_1.expect.objectContaining({ + filePath: globals_1.expect.stringContaining('/somefile.ts') + }) + }); + // Loop through each call to ensure none of them contain '/src/some-file-not-in-pr.ts' + mockGitApi.createThread.mock.calls.forEach(function (call) { + var firstArg = call[0]; + (0, globals_1.expect)(firstArg).not.toMatchObject({ + threadContext: globals_1.expect.objectContaining({ + filePath: globals_1.expect.stringContaining('/src/file-not-in-pr.ts') + }) + }); + }); + mockReportWithMultipleIssues_1 = [ + { + DocumentId: document, + FileName: "somefile.ts", + FilePath: "/src/somefile.ts", + FileChanges: [ + { + CharNumber: 1, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 2, // Not in changed lines + FormatDescription: "some error" + }, + { + CharNumber: 1, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 10, // In changed lines + FormatDescription: "some error in changed line" + } + ] + } + ]; + fs_1.default.readFileSync.mockImplementation(function () { return JSON.stringify(mockReportWithMultipleIssues_1); }); + // Reset mocks + mockGitApi.createThread.mockReset(); + mockGitApi.updateThread.mockReset(); + _b = globals_1.expect; + return [4 /*yield*/, (0, format_check_1.runFormatCheck)(mockSettings)]; + case 2: + _b.apply(void 0, [_c.sent()]).toEqual(true); // Should return true because there is a formatting error in a changed line + // Verify that only the issue on line 10 (a changed line) was reported + (0, globals_1.expect)(mockGitApi.createThread).toHaveBeenCalled(); + threadArgs = mockGitApi.createThread.mock.calls[0][0]; + if (threadArgs.comments) { + (0, globals_1.expect)(threadArgs.comments[0].content).toContain("LineNumber: 10"); + (0, globals_1.expect)(threadArgs.comments[0].content).not.toContain("LineNumber: 2"); + } + _c.label = 3; + case 3: return [2 /*return*/]; + } + }); + }); }); +}); diff --git a/src/format-check/scripts/main.js b/src/format-check/scripts/main.js new file mode 100644 index 0000000..a502393 --- /dev/null +++ b/src/format-check/scripts/main.js @@ -0,0 +1,78 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var format_check_1 = require("./format-check"); +var base_git_api_service_1 = require("./services/base-git-api-service"); +var settings_1 = require("./types/settings"); +var dotenv_1 = __importDefault(require("dotenv")); +dotenv_1.default.config(); +function main() { + return __awaiter(this, void 0, void 0, function () { + var settings, shouldFail; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + settings = (0, settings_1.getSettings)(); + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.init(settings)]; + case 1: + _a.sent(); + return [4 /*yield*/, (0, format_check_1.runFormatCheck)(settings)]; + case 2: + shouldFail = _a.sent(); + // Exit the program based on the format check outcome + if (shouldFail) { + console.log("Format check task failed."); + process.exit(1); + } + else { + console.log("Format check task succeeded."); + process.exit(0); + } + return [2 /*return*/]; + } + }); + }); +} +// Call these functions in your main function and do your own error handling +main().catch(function (error) { + console.error(error); + process.exit(1); +}); diff --git a/src/format-check/scripts/services/base-git-api-service.js b/src/format-check/scripts/services/base-git-api-service.js new file mode 100644 index 0000000..d3acbae --- /dev/null +++ b/src/format-check/scripts/services/base-git-api-service.js @@ -0,0 +1,128 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.BaseGitApiService = void 0; +var azdev = __importStar(require("azure-devops-node-api")); +var BaseGitApiService = /** @class */ (function () { + function BaseGitApiService() { + } + BaseGitApiService.init = function (settings) { + return __awaiter(this, void 0, void 0, function () { + var authHandler, connection, _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (this.GitApiPromise) + return [2 /*return*/]; + console.log("Creating personal access token handler."); + authHandler = azdev.getPersonalAccessTokenHandler(settings.Parameters.token); + console.log("Creating TFS connection."); + connection = new azdev.WebApi(settings.Environment.orgUrl, authHandler); + console.log("Getting Git API."); + this.GitApiPromise = connection.getGitApi(); + _a = this; + return [4 /*yield*/, this.GitApiPromise]; + case 1: + _a.GitApi = _b.sent(); + return [2 /*return*/]; + } + }); + }); + }; + BaseGitApiService.getGitApi = function () { + return __awaiter(this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + if (this.GitApiPromise === null) { + throw new Error('call BaseGitApiService.init() first'); + } + if (!(this.GitApi === null)) return [3 /*break*/, 2]; + _a = this; + return [4 /*yield*/, this.GitApiPromise]; + case 1: + _a.GitApi = _b.sent(); + _b.label = 2; + case 2: return [2 /*return*/, this.GitApi]; + } + }); + }); + }; + BaseGitApiService.reset = function () { + this.GitApi = null; + this.GitApiPromise = null; + }; + BaseGitApiService.GitApi = null; + BaseGitApiService.GitApiPromise = null; + return BaseGitApiService; +}()); +exports.BaseGitApiService = BaseGitApiService; diff --git a/src/format-check/scripts/services/base-git-api-service.test.js b/src/format-check/scripts/services/base-git-api-service.test.js new file mode 100644 index 0000000..e2c014c --- /dev/null +++ b/src/format-check/scripts/services/base-git-api-service.test.js @@ -0,0 +1,201 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var azdev = __importStar(require("azure-devops-node-api")); +var globals_1 = require("@jest/globals"); +var base_git_api_service_1 = require("./base-git-api-service"); +var crypto_1 = require("crypto"); +// Explicitly cast to jest.Mock +var MockedWebApi = azdev.WebApi; +var MockedGetPersonalAccessTokenHandler = azdev.getPersonalAccessTokenHandler; +globals_1.jest.mock('azure-devops-node-api'); +(0, globals_1.describe)('BaseGitApiService', function () { + var mockSettings; + (0, globals_1.beforeEach)(function () { + mockSettings = { + Environment: { + orgUrl: 'mockOrgUrl', + repoId: 'mockRepoId', + projectId: 'mockProjectId', + pullRequestId: 1, + token: 'mockToken', + sourcesDirectory: '/src', + pullRequestSourceCommit: (0, crypto_1.randomUUID)(), + pullRequestTargetBranch: '/refs/heads/main' + }, + Parameters: { + solutionPath: 'mockSolutionPath', + includePath: 'mockIncludePath', + excludePath: 'mockExcludePath', + statusCheck: true, + failOnFormattingErrors: false, + statusCheckContext: { + name: 'mockName', + genre: 'mockGenre' + }, + scopeToPullRequest: true, + token: 'mockToken' + } + }; + globals_1.jest.clearAllMocks(); + MockedWebApi.mockClear(); + base_git_api_service_1.BaseGitApiService.reset(); + }); + (0, globals_1.it)('should initialize Azure DevOps Git API', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockGetGitApi, mockWebApiInstance; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockGetGitApi = globals_1.jest.fn(); + mockWebApiInstance = { getGitApi: mockGetGitApi }; + MockedGetPersonalAccessTokenHandler.mockReturnValue({}); + MockedWebApi.mockImplementation(function () { return mockWebApiInstance; }); + // Calling the function + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.init(mockSettings)]; + case 1: + // Calling the function + _a.sent(); + // Assertions + (0, globals_1.expect)(MockedGetPersonalAccessTokenHandler).toHaveBeenCalledWith(mockSettings.Parameters.token); + (0, globals_1.expect)(MockedWebApi).toHaveBeenCalledWith(mockSettings.Environment.orgUrl, {}); + (0, globals_1.expect)(mockGetGitApi).toHaveBeenCalledTimes(1); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should return the initialized Git API instance', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockGitApiInstance, returnedGitApi; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockGitApiInstance = { + baseUrl: "https://mockurl" + }; + base_git_api_service_1.BaseGitApiService.GitApiPromise = Promise.resolve(mockGitApiInstance); + base_git_api_service_1.BaseGitApiService.GitApi = mockGitApiInstance; + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.getGitApi()]; + case 1: + returnedGitApi = _a.sent(); + // Assertions + (0, globals_1.expect)(returnedGitApi).toBe(mockGitApiInstance); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should throw error if init fails', function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + MockedWebApi.mockImplementation(function () { + return { + getGitApi: function () { return Promise.reject(new Error('Initialization failed')); } + }; + }); + return [4 /*yield*/, (0, globals_1.expect)(base_git_api_service_1.BaseGitApiService.init(mockSettings)).rejects.toThrow('Initialization failed')]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should throw an error if GitApi is not initialized', function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + base_git_api_service_1.BaseGitApiService.GitApi = null; + return [4 /*yield*/, (0, globals_1.expect)(base_git_api_service_1.BaseGitApiService.getGitApi()).rejects.toStrictEqual(new Error('call BaseGitApiService.init() first'))]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should not re-initialize if already initialized', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockGetGitApi, mockWebApiInstance; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + console.log(" ===================== it('should not re-initialize if already initialized', async () => { ================"); + mockGetGitApi = globals_1.jest.fn(function () { return Promise.resolve(); }); + mockWebApiInstance = { getGitApi: mockGetGitApi }; + MockedWebApi.mockImplementation(function () { return mockWebApiInstance; }); + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.init(mockSettings)]; + case 1: + _a.sent(); + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.init(mockSettings)]; + case 2: + _a.sent(); + (0, globals_1.expect)(mockGetGitApi).toHaveBeenCalledTimes(1); + return [2 /*return*/]; + } + }); + }); }); +}); diff --git a/src/format-check/scripts/services/format-check-runner.js b/src/format-check/scripts/services/format-check-runner.js new file mode 100644 index 0000000..0c1e897 --- /dev/null +++ b/src/format-check/scripts/services/format-check-runner.js @@ -0,0 +1,188 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FormatCheckRunner = void 0; +var child_process_1 = require("child_process"); +var fs = __importStar(require("fs")); +/** + * @class FormatCheckRunner + * A utility class to run .NET format checks on a given solution. + */ +var FormatCheckRunner = /** @class */ (function () { + /** + * Initializes a new instance of FormatCheckRunner. + * + * @param {string} solutionPath - The path to the .NET solution file. + * @param {string} includePath - Files to include in the format check. + * @param {string} excludePath - Files to exclude from the format check. + * + * @example + * const runner = new FormatCheckRunner("./solution.sln", "./include", "./exclude"); + */ + function FormatCheckRunner(solutionPath, includePath, excludePath) { + this.solutionPath = solutionPath; + this.includePath = includePath; + this.excludePath = excludePath; + this.reportPath = "format-report.json"; + if (!fs.existsSync(solutionPath)) { + console.error("Solution file at solutionPath does not exist."); + process.exit(1); + } + if (fs.existsSync(this.reportPath)) { + fs.unlinkSync(this.reportPath); + console.log("Successfully deleted the existing report file."); + } + } + /** + * Runs the .NET format check command and returns a report. + * + * @returns {Promise} - The formatting error report. + * + * @example + * const report = await runner.runFormatCheck(); + * + * @throws Error when the command fails. + */ + FormatCheckRunner.prototype.runFormatCheck = function () { + return __awaiter(this, void 0, void 0, function () { + var formatCmd, dotnetFormatVersion, error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + _a.trys.push([0, 2, , 3]); + formatCmd = this.getCommandString(); + console.log("Running dotnet format command. (".concat(formatCmd, ")")); + dotnetFormatVersion = (0, child_process_1.execSync)("dotnet format --version", { + encoding: "utf8", + }); + console.log("Using dotnet format version ".concat(dotnetFormatVersion)); + try { + (0, child_process_1.execSync)(formatCmd, { stdio: ["inherit", "inherit", "pipe"] }); + } + catch (error) { + this.handleDotnetFormatError(error); + } + console.log("Dotnet format command completed."); + return [4 /*yield*/, this.retrieveErrorReport()]; + case 1: return [2 /*return*/, _a.sent()]; + case 2: + error_1 = _a.sent(); + console.error("Dotnet format task failed with error ".concat(error_1)); + process.exit(1); + return [3 /*break*/, 3]; + case 3: return [2 /*return*/]; + } + }); + }); + }; + /** + * @private + * Generates the .NET format command string based on the given parameters. + * + * @returns {string} - The generated command string. + */ + FormatCheckRunner.prototype.getCommandString = function () { + return "dotnet format ".concat(this.solutionPath, " --verify-no-changes --verbosity diagnostic --report ").concat(this.reportPath, " ").concat(this.includePath ? "--include ".concat(this.includePath) : "", " ").concat(this.excludePath ? "--exclude ".concat(this.excludePath) : ""); + }; + /** + * @private + * Handles errors thrown by the `dotnet format` command. + * + * @param {any} error - The error to handle. + */ + FormatCheckRunner.prototype.handleDotnetFormatError = function (error) { + console.error("Dotnet format command failed with error ".concat(error)); + if ("stderr" in error && error.stderr) { + console.error("stderr output:", error.stderr.toString()); + } + if (!fs.existsSync(this.reportPath)) { + // Format command has failed. No report was generated. + console.error("No report found at reportPath."); + process.exit(1); + } + }; + /** + * @private + * Retrieves the formatting error report generated by the `dotnet format` command. + * + * @returns {Promise} - The formatting error report. + */ + FormatCheckRunner.prototype.retrieveErrorReport = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + console.log("Loading error report."); + return [2 /*return*/, JSON.parse(fs.readFileSync(this.reportPath, "utf8"))]; + }); + }); + }; + return FormatCheckRunner; +}()); +exports.FormatCheckRunner = FormatCheckRunner; diff --git a/src/format-check/scripts/services/format-check-runner.test.js b/src/format-check/scripts/services/format-check-runner.test.js new file mode 100644 index 0000000..625e4a9 --- /dev/null +++ b/src/format-check/scripts/services/format-check-runner.test.js @@ -0,0 +1,237 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var format_check_runner_1 = require("./format-check-runner"); +var child_process_1 = require("child_process"); +var fs = __importStar(require("fs")); +var globals_1 = require("@jest/globals"); +var crypto_1 = require("crypto"); +globals_1.jest.mock('child_process'); +var mockExecSync = globals_1.jest.mocked(child_process_1.execSync); +globals_1.jest.mock('process'); +globals_1.jest.mock('console', function () { return ({ + error: globals_1.jest.fn(), +}); }); +globals_1.jest.mock('fs'); +var mockUnlinkSync = globals_1.jest.mocked(fs.unlinkSync); +var mockExistsSync = globals_1.jest.mocked(fs.existsSync); +(0, globals_1.describe)('FormatCheckRunner', function () { + var runner; + var exitSpy; + var exitCode = 0; + (0, globals_1.beforeEach)(function () { + globals_1.jest.clearAllMocks(); + exitSpy = globals_1.jest.spyOn(process, 'exit').mockImplementation(function (code) { + exitCode = code === null ? 0 : (typeof code === 'number' ? code : 0); + return undefined; + }); + }); + (0, globals_1.afterEach)(function () { + // Clean up the spy + exitSpy.mockRestore(); + }); + (0, globals_1.it)('should construct without errors', function () { + fs.existsSync.mockReturnValue(true); // Mock return value + runner = new format_check_runner_1.FormatCheckRunner('./solution.sln', './include', './exclude'); + (0, globals_1.expect)(runner).toBeInstanceOf(format_check_runner_1.FormatCheckRunner); + }); + (0, globals_1.it)('should fail construction if solution does not exist', function () { + fs.existsSync.mockReturnValue(false); + // Mock process.exit before the test + var exitSpy = globals_1.jest.spyOn(process, 'exit').mockImplementation((function () { + })); + new format_check_runner_1.FormatCheckRunner('./solution.sln', './include', './exclude'); + // Check if process.exit has been called with code 1 + (0, globals_1.expect)(exitSpy).toHaveBeenCalledWith(1); + // Restore process.exit to its original function after the test + exitSpy.mockRestore(); + }); + (0, globals_1.it)('should run format check successfully', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockReport, report; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockReport = [ + { + DocumentId: { + Id: (0, crypto_1.randomUUID)(), + ProjectId: { + Id: (0, crypto_1.randomUUID)() + } + }, + FileName: "somefile.ts", + FilePath: "/src/somefile.ts", + FileChanges: [ + { + CharNumber: 1, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 2, + FormatDescription: "some error" + } + ] + } + ]; + fs.existsSync.mockReturnValue(true); // Mock return value + fs.readFileSync.mockReturnValue(JSON.stringify(mockReport)); + runner = new format_check_runner_1.FormatCheckRunner('./solution.sln', './include', './exclude'); + return [4 /*yield*/, runner.runFormatCheck()]; + case 1: + report = _a.sent(); + (0, globals_1.expect)(report).toEqual(mockReport); + (0, globals_1.expect)(child_process_1.execSync).toHaveBeenCalled(); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should handle dotnet format crashes', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockError; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockError = new Error('Dotnet format crashed'); + mockExecSync.mockImplementation((function (command) { + if (command.startsWith('dotnet format')) { + throw mockError; + } + return Buffer.from('mock buffer content', 'utf-8'); + })); + mockExistsSync.mockReturnValue(false); + runner = new format_check_runner_1.FormatCheckRunner('./solution.sln', './include', './exclude'); + return [4 /*yield*/, runner.runFormatCheck()]; + case 1: + _a.sent(); + (0, globals_1.expect)(exitCode).toBe(1); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should handle dotnet format errors', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockReport; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockReport = [ + { + DocumentId: { + Id: (0, crypto_1.randomUUID)(), + ProjectId: { + Id: (0, crypto_1.randomUUID)() + } + }, + FileName: "somefile.ts", + FilePath: "/src/somefile.ts", + FileChanges: [ + { + CharNumber: 1, + DiagnosticId: (0, crypto_1.randomUUID)(), + LineNumber: 2, + FormatDescription: "some error" + } + ] + } + ]; + fs.readFileSync.mockReturnValue(JSON.stringify(mockReport)); + child_process_1.execSync + .mockImplementationOnce(function () { + }) + .mockImplementationOnce(function () { + }); + fs.existsSync.mockReturnValueOnce(true); + mockUnlinkSync.mockImplementation((function (path) { + if (path.toString().endsWith('format-report.json')) { + // do nothing + } + else { + throw new Error("not mocked"); + } + })); + runner = new format_check_runner_1.FormatCheckRunner('./solution.sln', './include', './exclude'); + return [4 /*yield*/, (0, globals_1.expect)(runner.runFormatCheck()).resolves.toEqual([{ + "DocumentId": { + "Id": mockReport[0].DocumentId.Id, + "ProjectId": { "Id": mockReport[0].DocumentId.ProjectId.Id } + }, + "FileChanges": [{ + "CharNumber": 1, + "DiagnosticId": mockReport[0].FileChanges[0].DiagnosticId, + "FormatDescription": "some error", + "LineNumber": 2 + }], + "FileName": "somefile.ts", + "FilePath": "/src/somefile.ts" + }])]; + case 1: + _a.sent(); + return [2 /*return*/]; + } + }); + }); }); +}); diff --git a/src/format-check/scripts/services/pull-request-service.js b/src/format-check/scripts/services/pull-request-service.js new file mode 100644 index 0000000..2deb551 --- /dev/null +++ b/src/format-check/scripts/services/pull-request-service.js @@ -0,0 +1,350 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PullRequestService = void 0; +exports.getPullRequestService = getPullRequestService; +var gi = __importStar(require("azure-devops-node-api/interfaces/GitInterfaces")); +var base_git_api_service_1 = require("./base-git-api-service"); +var node_fetch_1 = __importDefault(require("node-fetch")); +/** + * PullRequestService class is a service that offers methods to interact with pull requests. + * Its purpose is to help in performing operations related to pull requests like obtaining pull request commits, + * updating pull request statuses and working with threads in pull requests. + * + * @class + * @public + * + * @property {IGitApi} gitApi - An instance of IGitApi to interact with Azure DevOps Git API. + * @property {Settings} settings - An instance of Settings, containing project-specific parameters and environment variables. + * + * @method constructor(gitApi, settings) - Initializes a new instance of the PullRequestService class. + * @method async updatePullRequestStatus(status, getStatusDescription) - Asynchronously updates the pull request status. + * @method private async getLastPullRequestIteration() - Asynchronously obtains the last pull request iteration. + * @method async getPullRequestChanges() - Asynchronously fetches and returns the changes made in a pull request. + * @method async getThreads() - Asynchronously gets the threads related to a pull request. + * @method async updateThread(commentThread, existingThreadId) - Asynchronously updates a specific thread of a pull request. + * @method async createThread(thread) - Asynchronously creates a new thread in a pull request. + */ +var PullRequestService = /** @class */ (function () { + /** + * Constructor for PullRequestService. + * + * @param {IGitApi} gitApi - An instance of IGitApi to interact with Azure DevOps Git API. + * @param {Settings} settings - An instance of Settings to use project-specific parameters and environment variables. + */ + function PullRequestService(gitApi, settings) { + this.gitApi = gitApi; + this.settings = settings; + } + /** + * Async method that updates the pull request status. + * + * @method updatePullRequestStatus + * @public + * @async + * + * @param {gi.GitStatusState} status - The status enum of GitStatusState, representing the status of the pull request. + * @param {Function} getStatusDescription - A function that returns the description of the GitStatusState status. + * + * This method first gets the last pull request iteration ID, then creates a GitPullRequestStatus object. + * Finally, it calls the `createPullRequestStatus` method from the `gitApi` to update the pull request status. + * + * @returns {Promise} A promise that resolves when the pull request status has been updated. + */ + PullRequestService.prototype.updatePullRequestStatus = function (status, getStatusDescription) { + return __awaiter(this, void 0, void 0, function () { + var logMsg, iterationId, prStatus; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + if (!this.settings.Parameters.statusCheck) { + console.warn("updatePullRequestStatus called to set status check, but statusCheck task parameter is false"); + return [2 /*return*/]; + } + if (!this.settings.Parameters.statusCheckContext) { + throw new Error("statusCheckContext is not set in the settings"); + } + logMsg = "Setting status check '".concat(this.settings.Parameters.statusCheckContext.genre, "\\").concat(this.settings.Parameters.statusCheckContext.name, "' to: ").concat(gi.GitStatusState[status]); + console.log(logMsg); + return [4 /*yield*/, this.getLastPullRequestIteration()]; + case 1: + iterationId = _a.sent(); + prStatus = { + context: this.settings.Parameters.statusCheckContext, + state: status, + description: getStatusDescription(status), + iterationId: iterationId, + }; + return [4 /*yield*/, this.gitApi.createPullRequestStatus(prStatus, this.settings.Environment.repoId, this.settings.Environment.pullRequestId)]; + case 2: + _a.sent(); + return [2 /*return*/]; + } + }); + }); + }; + /** + * Asynchronously fetches and returns the changes made in a pull request. + * + * @method getPullRequestChanges + * @public + * @async + * + * This method first fetches an instance of the pull request with the help of the `gitApi.getPullRequest` method, + * using repository id, pull request id, and project id derived from `settings.Environment`. + * Then, it fetches the differences between the base version (source branch) and the target version (target branch) + * of the pull request using the `gitApi.getCommitDiffs` method. + * + * The `baseVersion`, `baseVersionOptions`, `baseVersionType`, `targetVersion`, `targetVersionOptions`, + * and `targetVersionType` parameters for `gitApi.getCommitDiffs` method are set using the properties of + * the obtained pull request instance. + * + * @returns {Promise} A promise that fulfills with an array of GitChange objects that represents the changes + * made in the pull request. If there are no changes, an empty array is returned. + */ + PullRequestService.prototype.getPullRequestChanges = function () { + return __awaiter(this, void 0, void 0, function () { + var pr, sourceRefName, targetRefName, token, encodedToken, url, response, commitDiffs, error_1; + var _a, _b; + return __generator(this, function (_c) { + switch (_c.label) { + case 0: return [4 /*yield*/, this.gitApi.getPullRequestById(this.settings.Environment.pullRequestId, this.settings.Environment.projectId)]; + case 1: + pr = _c.sent(); + sourceRefName = (_a = pr.sourceRefName) === null || _a === void 0 ? void 0 : _a.replace('refs/heads/', ''); + targetRefName = (_b = pr.targetRefName) === null || _b === void 0 ? void 0 : _b.replace('refs/heads/', ''); + console.log("Checking for file changes between ".concat(sourceRefName, " and ").concat(targetRefName)); + token = this.settings.Parameters.token; + encodedToken = Buffer.from(":".concat(token)).toString('base64'); + url = "".concat(this.settings.Environment.orgUrl).concat(this.settings.Environment.projectId, "/") + + "_apis/git/repositories/".concat(this.settings.Environment.repoId, "/diffs/commits") + + "?api-version=4.1&baseVersion=".concat(targetRefName, "&targetVersion=").concat(sourceRefName) + + "&targetVersionType=branch&baseVersionType=branch&diffCommonCommit=false&inlineChangedLines=true"; + console.log("Fetching ".concat(url)); + return [4 /*yield*/, (0, node_fetch_1.default)(url, { + headers: { + 'Authorization': "Basic ".concat(encodedToken) + } + })]; + case 2: + response = _c.sent(); + _c.label = 3; + case 3: + _c.trys.push([3, 5, , 6]); + return [4 /*yield*/, response.json()]; + case 4: + commitDiffs = (_c.sent()); + return [3 /*break*/, 6]; + case 5: + error_1 = _c.sent(); + console.error("Error parsing JSON response:", error_1); + return [2 /*return*/, []]; + case 6: return [2 /*return*/, (commitDiffs === null || commitDiffs === void 0 ? void 0 : commitDiffs.changes) || []]; + } + }); + }); + }; + /** + * Asynchronously fetches and returns discussion threads related to a specific pull request. + * + * @method getThreads + * @public + * @async + * + * This method uses the `gitApi` instance to call the `getThreads` function where it fetches all discussion threads for + * a specific pull request in the Azure DevOps Git API. The repository id, pull request id, and project id derived from + * the properties of `settings.EnvVars` specified in the Settings instance are used to target the correct pull request. + * Fetching pull request threads can be beneficial when wanting to analyze the discussions and comments related to a pull + * request. + * + * @returns {Promise} A promise that fulfills with an array of + * GitPullRequestCommentThread objects representing all the threads involved in the pull request discussion. Each + * GitPullRequestCommentThread object encapsulates information regarding a single thread including the comments, + * the status of the thread, and so on. + */ + PullRequestService.prototype.getThreads = function () { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.gitApi.getThreads(this.settings.Environment.repoId, this.settings.Environment.pullRequestId, this.settings.Environment.projectId)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + /** + * Asynchronously updates a specific thread of a pull request. + * + * @method updateThread + * @public + * @async + * + * @param {GitPullRequestCommentThread} commentThread - The comment thread object containing the modifications. + * @param {number} existingThreadId - The ID of the existing thread to be updated. + * + * This method leverages the `gitApi` instance to interact with the Azure DevOps Git API's `updateThread` method. + * It uses the repository id, pull request id, and project id from `settings.EnvVars` to pinpoint the desired pull request. + * This is used when modifications are required in a specific discussion thread concerning a pull request. + * + * @returns {Promise} A promise that fulfills with the updated GitPullRequestCommentThread + * object once the thread update operation completes. + */ + PullRequestService.prototype.updateThread = function (commentThread, existingThreadId) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.gitApi.updateThread(commentThread, this.settings.Environment.repoId, this.settings.Environment.pullRequestId, existingThreadId, this.settings.Environment.projectId)]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + /** + * Asynchronously creates a new discussion thread in a specific pull request. + * + * @method createThread + * @public + * @async + * + * @param {GitPullRequestCommentThread} thread - The comment thread object to be created in the pull request. + * + * This method employs the `gitApi` instance to call the Azure DevOps Git API's `createThread` method. + * It uses attributes like repository id, pull request id, and project id from `settings.EnvVars` to + * target the intended pull request. Typically, a new thread could be created to start a discussion about + * some aspect of the pull request. + * + * @returns {Promise} A promise that fulfills with the newly created + * GitPullRequestCommentThread object after the thread creation operation is done. + */ + PullRequestService.prototype.createThread = function (thread) { + return __awaiter(this, void 0, void 0, function () { + return __generator(this, function (_a) { + return [2 /*return*/, this.gitApi.createThread(thread, this.settings.Environment.repoId, this.settings.Environment.pullRequestId, this.settings.Environment.projectId)]; + }); + }); + }; + /** + * Private asynchronous method that retrieves the last pull request iteration's ID. + * + * @private + * @async + * + * @method getLastPullRequestIteration + * + * This method internally queries the Azure DevOps Git API to fetch all iterations of a pull request + * by utilizing project-specific parameters and environment variables provided in the Settings instance. + * From the received data, it retrieves and returns the ID of the last pull request iteration. + * + * @returns {Promise} A promise that fulfills with the ID of the last iteration of the pull request. + */ + PullRequestService.prototype.getLastPullRequestIteration = function () { + return __awaiter(this, void 0, void 0, function () { + var pullRequestIterations, lastIteration; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, this.gitApi.getPullRequestIterations(this.settings.Environment.repoId, this.settings.Environment.pullRequestId, this.settings.Environment.projectId, true)]; + case 1: + pullRequestIterations = _a.sent(); + lastIteration = pullRequestIterations.pop(); + if (!(lastIteration === null || lastIteration === void 0 ? void 0 : lastIteration.id)) { + throw new Error("Last PullRequest Iteration ID not set"); + } + return [2 /*return*/, lastIteration.id]; + } + }); + }); + }; + return PullRequestService; +}()); +exports.PullRequestService = PullRequestService; +/** + * The async function `getPullRequestService` initializes the BaseGitApiService with the specified settings and then returns + * a new instance of the PullRequestService with the BaseGitApiService's GitApi and the specified settings. + * + * @param {Settings} settings - An instance of Settings containing project-specific parameters and environment variables. + * @returns {Promise} A promise that resolves to a new instance of PullRequestService. + */ +function getPullRequestService(settings) { + return __awaiter(this, void 0, void 0, function () { + var _a; + return __generator(this, function (_b) { + switch (_b.label) { + case 0: + _a = PullRequestService.bind; + return [4 /*yield*/, base_git_api_service_1.BaseGitApiService.getGitApi()]; + case 1: return [2 /*return*/, new (_a.apply(PullRequestService, [void 0, _b.sent(), settings]))()]; + } + }); + }); +} diff --git a/src/format-check/scripts/services/pull-request-service.test.js b/src/format-check/scripts/services/pull-request-service.test.js new file mode 100644 index 0000000..ea95101 --- /dev/null +++ b/src/format-check/scripts/services/pull-request-service.test.js @@ -0,0 +1,321 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); + return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (g && (g = 0, op[0] && (_ = 0)), _) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var node_fetch_1 = __importDefault(require("node-fetch")); +jest.mock('node-fetch', function () { return jest.fn(); }); +jest.mock('./base-git-api-service', function () { + return { + BaseGitApiService: { + getGitApi: jest.fn().mockReturnValue({}), + }, + }; +}); +var pull_request_service_1 = require("./pull-request-service"); +var gi = __importStar(require("azure-devops-node-api/interfaces/GitInterfaces")); +var globals_1 = require("@jest/globals"); +var crypto_1 = require("crypto"); +(0, globals_1.describe)('PullRequestService', function () { + var mockGitApi; + var settings; + var service; + (0, globals_1.beforeEach)(function () { + mockGitApi = { + getChanges: jest.fn(), + createThread: jest.fn(), + updateThread: jest.fn(), + getThreads: jest.fn(), + getPullRequestById: jest.fn(), + createPullRequestStatus: jest.fn(), + getPullRequestIterations: jest.fn() + }; + settings = { + Environment: { + orgUrl: 'https://mockOrgUrl/', + repoId: 'mockRepoId', + projectId: 'mockProjectId', + pullRequestId: 1, + token: 'mockToken', + sourcesDirectory: '/src', + pullRequestSourceCommit: (0, crypto_1.randomUUID)(), + pullRequestTargetBranch: '/refs/heads/main' + }, + Parameters: { + solutionPath: 'mockSolutionPath', + includePath: 'mockIncludePath', + excludePath: 'mockExcludePath', + statusCheck: true, + failOnFormattingErrors: false, + statusCheckContext: { + name: 'mockName', + genre: 'mockGenre' + }, + scopeToPullRequest: true, + token: 'mockToken' + } + }; + service = new pull_request_service_1.PullRequestService(mockGitApi, settings); + }); + (0, globals_1.it)('should updatePullRequestStatus correctly', function () { return __awaiter(void 0, void 0, void 0, function () { + var status, descriptionFunc; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + status = gi.GitStatusState.Succeeded; + descriptionFunc = function (_status) { return 'description'; }; + mockGitApi.getPullRequestIterations.mockReturnValue([ + { + id: 1, + description: 'Initial commit', + author: { + displayName: 'Mock Author', + id: 'mock_author_123', + }, + updatedAt: new Date(), + }, + { + id: 2, + description: 'Updated commit', + author: { + displayName: 'Mock Author', + id: 'mock_author_123', + }, + updatedAt: new Date(), + } + ]); + return [4 /*yield*/, service.updatePullRequestStatus(status, descriptionFunc)]; + case 1: + _a.sent(); + (0, globals_1.expect)(mockGitApi.createPullRequestStatus).toHaveBeenCalled(); // Add more assertions based on your logic + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should getPullRequestChanges correctly', function () { return __awaiter(void 0, void 0, void 0, function () { + var mockReturnValue, changes; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + mockGitApi.getPullRequestById.mockReturnValue({ + pullRequestId: settings.Environment.pullRequestId, + repository: { + id: settings.Environment.repoId + }, + sourceRefName: 'refs/heads/feature/test', + targetRefName: 'targetRef', + }); + mockReturnValue = { + changeCounts: { + edit: 2, + add: 3, + delete: 1 + }, + changes: [ + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: '/path1', + }, + }, + { + changeType: gi.VersionControlChangeType.Edit, + item: { + path: '/path2', + }, + }, + { + changeType: gi.VersionControlChangeType.Add, + item: { + path: '/path3', + }, + }, + { + changeType: gi.VersionControlChangeType.Add, + item: { + path: '/path4', + }, + }, + { + changeType: gi.VersionControlChangeType.Add, + item: { + path: '/path5', + }, + }, + { + changeType: gi.VersionControlChangeType.Delete, + item: { + path: '/path6', + }, + }, + ], + diffCommonCommit: { + commitId: 'mockCommitId', + }, + }; + node_fetch_1.default.mockResolvedValueOnce({ + status: 200, + json: function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) { + return [2 /*return*/, mockReturnValue]; + }); }); }, + }); + return [4 /*yield*/, service.getPullRequestChanges()]; + case 1: + changes = _a.sent(); + (0, globals_1.expect)(changes).toEqual(mockReturnValue.changes); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should getThreads correctly', function () { return __awaiter(void 0, void 0, void 0, function () { + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, service.getThreads()]; + case 1: + _a.sent(); + (0, globals_1.expect)(mockGitApi.getThreads).toHaveBeenCalled(); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should updateThread correctly', function () { return __awaiter(void 0, void 0, void 0, function () { + var thread, existingThreadId; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + thread = { /* populate this */}; + existingThreadId = 1; + return [4 /*yield*/, service.updateThread(thread, existingThreadId)]; + case 1: + _a.sent(); + (0, globals_1.expect)(mockGitApi.updateThread).toHaveBeenCalled(); + return [2 /*return*/]; + } + }); + }); }); + (0, globals_1.it)('should createThread correctly', function () { return __awaiter(void 0, void 0, void 0, function () { + var thread; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + thread = { /* populate this */}; + return [4 /*yield*/, service.createThread(thread)]; + case 1: + _a.sent(); + (0, globals_1.expect)(mockGitApi.createThread).toHaveBeenCalled(); + return [2 /*return*/]; + } + }); + }); }); +}); +(0, globals_1.describe)('getPullRequestService function', function () { + var settings; + (0, globals_1.beforeEach)(function () { + settings = { + Environment: { + orgUrl: 'mockOrgUrl', + repoId: 'mockRepoId', + projectId: 'mockProjectId', + pullRequestId: 1, + token: 'mockToken', + sourcesDirectory: '/src', + pullRequestSourceCommit: (0, crypto_1.randomUUID)(), + pullRequestTargetBranch: '/refs/heads/main' + }, + Parameters: { + solutionPath: 'mockSolutionPath', + includePath: 'mockIncludePath', + excludePath: 'mockExcludePath', + statusCheck: true, + failOnFormattingErrors: false, + statusCheckContext: { + name: 'mockName', + genre: 'mockGenre' + }, + scopeToPullRequest: true, + token: 'mockToken' + } + }; + }); + (0, globals_1.it)('should return a new PullRequestService instance', function () { return __awaiter(void 0, void 0, void 0, function () { + var result; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, (0, pull_request_service_1.getPullRequestService)(settings)]; + case 1: + result = _a.sent(); + (0, globals_1.expect)(result).toBeInstanceOf(pull_request_service_1.PullRequestService); + return [2 /*return*/]; + } + }); + }); }); +}); diff --git a/src/format-check/scripts/types/annotated-report.js b/src/format-check/scripts/types/annotated-report.js new file mode 100644 index 0000000..06af690 --- /dev/null +++ b/src/format-check/scripts/types/annotated-report.js @@ -0,0 +1,36 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AnnotatedReport = void 0; +var format_report_1 = require("./format-report"); +/** + * @class AnnotatedReport + * Extends FormatReport to include version control information. + * + * @extends {FormatReport} + */ +var AnnotatedReport = /** @class */ (function (_super) { + __extends(AnnotatedReport, _super); + function AnnotatedReport(commitId, changeType) { + var _this = _super.call(this) || this; + _this.commitId = commitId; + _this.changeType = changeType; + return _this; + } + return AnnotatedReport; +}(format_report_1.FormatReport)); +exports.AnnotatedReport = AnnotatedReport; diff --git a/src/format-check/scripts/types/environment.js b/src/format-check/scripts/types/environment.js new file mode 100644 index 0000000..50c56b7 --- /dev/null +++ b/src/format-check/scripts/types/environment.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Environment = void 0; +/** +* Environment class parameters. +* @property {string | undefined} orgUrl - The organization URL. +* @property {string | undefined} repoId - The repository ID. +* @property {string | undefined} projectId - The project ID. +* @property {number | undefined} pullRequestId - The pull request ID. +* @property {string | undefined} token - The access token used for authentication. +*/ +var Environment = /** @class */ (function () { + function Environment() { + } + return Environment; +}()); +exports.Environment = Environment; diff --git a/src/format-check/scripts/types/format-report.js b/src/format-check/scripts/types/format-report.js new file mode 100644 index 0000000..148bce8 --- /dev/null +++ b/src/format-check/scripts/types/format-report.js @@ -0,0 +1,42 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.FormatReports = exports.FormatReport = void 0; +/** + * @class FormatReport + * A class to represent the report of a single file's formatting issues. + */ +var FormatReport = /** @class */ (function () { + function FormatReport() { + } + return FormatReport; +}()); +exports.FormatReport = FormatReport; +/** +* @class FormatReports +* An array-like class to hold multiple FormatReport instances. +* +* @extends {Array} +*/ +var FormatReports = /** @class */ (function (_super) { + __extends(FormatReports, _super); + function FormatReports() { + return _super !== null && _super.apply(this, arguments) || this; + } + return FormatReports; +}(Array)); +exports.FormatReports = FormatReports; diff --git a/src/format-check/scripts/types/parameters.js b/src/format-check/scripts/types/parameters.js new file mode 100644 index 0000000..00d7e0c --- /dev/null +++ b/src/format-check/scripts/types/parameters.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Parameters = void 0; +/** + * The Parameters class models the attributes for configuration settings. + * + * @class + * + * @param {string | undefined} solutionPath - The path of the solution to check. Can be undefined. + * @param {string | undefined} excludePath - Path to exclude from the files to check. Can be undefined. + * @param {boolean} failOnFormattingErrors - Whether to fail the process on formatting errors. + * @param {string | undefined} includePath - Path to include for files to check. Can be undefined. + * @param {boolean} scopeToPullRequest - Whether the operation scope is limited to thr pull request or not. + * @param {boolean} statusCheck - Whether to set the status check or not. + * @param {Object} statusCheckContext - Context for the status check comprising genre and name attributes. + * @param {string | undefined} token - Authentication token. Can be undefined. + */ +var Parameters = /** @class */ (function () { + function Parameters() { + } + return Parameters; +}()); +exports.Parameters = Parameters; diff --git a/src/format-check/scripts/types/pull-request-file-change.js b/src/format-check/scripts/types/pull-request-file-change.js new file mode 100644 index 0000000..690ef7f --- /dev/null +++ b/src/format-check/scripts/types/pull-request-file-change.js @@ -0,0 +1,47 @@ +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PullRequestFileChanges = exports.PullRequestFileChange = void 0; +/** + * @class PullRequestFileChange + * A class to represent individual file changes within a Pull Request. + */ +var PullRequestFileChange = /** @class */ (function () { + function PullRequestFileChange(FilePath, CommitId, changeType, lineChanges) { + if (lineChanges === void 0) { lineChanges = []; } + this.FilePath = FilePath; + this.CommitId = CommitId; + this.changeType = changeType; + this.lineChanges = lineChanges; + } + return PullRequestFileChange; +}()); +exports.PullRequestFileChange = PullRequestFileChange; +/** + * @class PullRequestFileChanges + * An array-like class to hold multiple PullRequestFileChange instances. + * + * @extends {Array} + */ +var PullRequestFileChanges = /** @class */ (function (_super) { + __extends(PullRequestFileChanges, _super); + function PullRequestFileChanges() { + return _super !== null && _super.apply(this, arguments) || this; + } + return PullRequestFileChanges; +}(Array)); +exports.PullRequestFileChanges = PullRequestFileChanges; diff --git a/src/format-check/scripts/types/settings.js b/src/format-check/scripts/types/settings.js new file mode 100644 index 0000000..9fa05d0 --- /dev/null +++ b/src/format-check/scripts/types/settings.js @@ -0,0 +1,78 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.getSettings = void 0; +/** + * Returns an object of type `Settings` populated from environment variables. + * + * @function getSettings + * @returns {Settings} - An object that holds various environmental variables and parameters. + * + * @throws {Error} Will exit the process if `SYSTEM_PULLREQUEST_PULLREQUESTID` or `SolutionPath` is not set. + */ +var getSettings = function () { + var _a, _b, _c; + var pullRequestId = parseInt(process.env.SYSTEM_PULLREQUEST_PULLREQUESTID, 10); + if (!pullRequestId) { + console.log("Not a PR build. Skipping."); + process.exit(0); + } + var settings = { + Environment: { + orgUrl: process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI || (function () { + throw new Error("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI is not set."); + })(), + repoId: process.env.BUILD_REPOSITORY_ID || (function () { + throw new Error("BUILD_REPOSITORY_ID is not set."); + })(), + projectId: process.env.SYSTEM_TEAMPROJECTID || (function () { + throw new Error("SYSTEM_TEAMPROJECTID is not set."); + })(), + pullRequestId: pullRequestId, + token: process.env.SYSTEM_ACCESSTOKEN, + sourcesDirectory: process.env.BUILD_SOURCESDIRECTORY || (function () { + throw new Error("BUILD_SOURCESDIRECTORY is not set."); + })(), + pullRequestSourceCommit: process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID || (function () { + throw new Error("SYSTEM_PULLREQUEST_SOURCECOMMITID is not set."); + })(), + pullRequestTargetBranch: process.env.SYSTEM_PULLREQUEST_TARGETBRANCH || (function () { + throw new Error("SYSTEM_PULLREQUEST_TARGETBRANCH is not set."); + })(), + }, + Parameters: { + solutionPath: process.env.INPUT_SOLUTIONPATH, + includePath: process.env.INPUT_INCLUDEPATH, + excludePath: process.env.INPUT_EXCLUDEPATH, + statusCheck: process.env.INPUT_STATUSCHECK === 'true', + failOnFormattingErrors: process.env.INPUT_FAILONFORMATTINGERRORS === 'true', + statusCheckContext: process.env.INPUT_STATUSCHECKNAME && process.env.INPUT_STATUSCHECKGENRE ? { + name: process.env.INPUT_STATUSCHECKNAME, + genre: process.env.INPUT_STATUSCHECKGENRE, + } : undefined, + scopeToPullRequest: process.env.INPUT_SCOPETOPULLREQUEST === 'true', + token: process.env.INPUT_PAT || process.env.SYSTEM_ACCESSTOKEN || (function () { + throw new Error("Token is not set."); + })() + } + }; + console.log('task input parameters:'); + console.log("Solution Path: ".concat(settings.Parameters.solutionPath)); + console.log("Include Path: ".concat(settings.Parameters.includePath)); + console.log("Exclude Path: ".concat(settings.Parameters.excludePath)); + console.log("Status Check: ".concat(settings.Parameters.statusCheck)); + console.log("Fail On Formatting Errors: ".concat(settings.Parameters.failOnFormattingErrors)); + console.log("Status Check Name: ".concat((_a = settings.Parameters.statusCheckContext) === null || _a === void 0 ? void 0 : _a.name)); + console.log("Status Check Genre: ".concat((_b = settings.Parameters.statusCheckContext) === null || _b === void 0 ? void 0 : _b.genre)); + console.log("Scope To Pull Request: ".concat(settings.Parameters.scopeToPullRequest)); + console.log("OrgUrl: ".concat(settings.Environment.orgUrl)); + console.log("RepoId: ".concat(settings.Environment.repoId)); + console.log("ProjectId: ".concat(settings.Environment.projectId)); + console.log("PullRequestId: ".concat(settings.Environment.pullRequestId)); + console.log("Sources Directory: ".concat(settings.Environment.sourcesDirectory)); + if (!((_c = settings.Parameters.solutionPath) === null || _c === void 0 ? void 0 : _c.trim())) { + console.error("SolutionPath is not set."); + process.exit(1); + } + return settings; +}; +exports.getSettings = getSettings; diff --git a/src/format-check/scripts/types/settings.test.js b/src/format-check/scripts/types/settings.test.js new file mode 100644 index 0000000..e37043a --- /dev/null +++ b/src/format-check/scripts/types/settings.test.js @@ -0,0 +1,84 @@ +"use strict"; +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +var settings_1 = require("./settings"); // Adjust the import to match your actual file layout +var globals_1 = require("@jest/globals"); +(0, globals_1.describe)('getSettings', function () { + var consoleLogSpy; + var originalEnv; + (0, globals_1.beforeAll)(function () { + // Save the original process.env + originalEnv = __assign({}, process.env); + }); + (0, globals_1.beforeEach)(function () { + consoleLogSpy = globals_1.jest.spyOn(console, 'log').mockImplementation(function (_message) { + var _options = []; + for (var _i = 1; _i < arguments.length; _i++) { + _options[_i - 1] = arguments[_i]; + } + }); + }); + (0, globals_1.afterEach)(function () { + // Restore the original process.env and console.log + process.env = __assign({}, originalEnv); + consoleLogSpy.mockRestore(); + }); + (0, globals_1.it)('should not log the token', function () { + // Mock necessary environment variables + process.env.BUILD_REPOSITORY_ID = 'repoId'; + process.env.BUILD_SOURCESDIRECTORY = '/src'; + process.env.SYSTEM_PULLREQUEST_PULLREQUESTID = '123'; + process.env.SYSTEM_PULLREQUEST_SOURCECOMMITID = '123123-1231230123123-13123'; + process.env.SYSTEM_PULLREQUEST_TARGETBRANCH = '/refs/heads/main'; + process.env.SYSTEM_TEAMPROJECTID = 'projectId'; + process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI = 'uri'; + process.env.INPUT_SOLUTIONPATH = 'solutionPath'; + process.env.INPUT_INCLUDEPATH = '.*'; + process.env.INPUT_EXCLUDEPATH = '.*.test.ts'; + process.env.INPUT_STATUSCHECK = 'true'; + process.env.INPUT_STATUSCHECKNAME = 'code formatting'; + process.env.INPUT_STATUSCHECKGENRE = 'formatting check'; + process.env.INPUT_FAILONFORMATTINGERRORS = 'true'; + process.env.INPUT_SCOPETOPULLREQUEST = 'true'; + process.env.SYSTEM_ACCESSTOKEN = 'token'; + process.env.INPUT_PAT = 'token'; + var settings = (0, settings_1.getSettings)(); + // Verify token is not logged + (0, globals_1.expect)(consoleLogSpy).not.toHaveBeenCalledWith(globals_1.expect.stringContaining('token')); + (0, globals_1.expect)(settings).toEqual({ + Environment: { + orgUrl: 'uri', + repoId: 'repoId', + projectId: 'projectId', + pullRequestId: 123, + token: 'token', + sourcesDirectory: '/src', + pullRequestSourceCommit: '123123-1231230123123-13123', + pullRequestTargetBranch: '/refs/heads/main' + }, + Parameters: { + excludePath: '.*.test.ts', + failOnFormattingErrors: true, + includePath: '.*', + scopeToPullRequest: true, + solutionPath: 'solutionPath', + statusCheck: true, + statusCheckContext: { + genre: 'formatting check', + name: 'code formatting' + }, + token: 'token' + } + }); + }); +}); diff --git a/src/format-check/scripts/utils/path-normalizer.js b/src/format-check/scripts/utils/path-normalizer.js new file mode 100644 index 0000000..14833b6 --- /dev/null +++ b/src/format-check/scripts/utils/path-normalizer.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.PathNormalizer = void 0; +/** + * PathNormalizer is a class for path normalization tasks. It uses the settings object for its behavioural config. + * The main task is to convert an absolute file path to a relative path from the project's source directory. + * + * @class + * @property {Settings} settings - An object of Type Settings necessary for the working of PathNormalizer. + * @constructor + * @param {Settings} settings - Configurable settings used in path normalization tasks. + * @method + * public normalizeFilePath(filePath: string): string - Takes an absolute file path and normalizes it to be relative to the + * source directory specified in the `settings` object. + */ +var PathNormalizer = /** @class */ (function () { + function PathNormalizer(settings) { + this.settings = settings; + } + /** + * Takes a file path as input and returns a normalized version of the path, relative to the project's source directory. + * + * @public + * @param {string} filePath - The absolute file path to normalise. + * @returns {string} Normalized version of the supplied file path, relative to the project's source directory. + */ + PathNormalizer.prototype.normalizeFilePath = function (filePath) { + // remove trailing slash from sourcesDirectory + var pathToStrip = this.settings.Environment.sourcesDirectory.replace(/\/$/, ''); + // remove the path to the source directory from the file path + var relativeToSourceDir = filePath.replace("".concat(pathToStrip), ''); + // replace \ with / for windows + return relativeToSourceDir.replace(/\\/g, '/'); + }; + return PathNormalizer; +}()); +exports.PathNormalizer = PathNormalizer; diff --git a/src/format-check/scripts/utils/path-normalizer.test.js b/src/format-check/scripts/utils/path-normalizer.test.js new file mode 100644 index 0000000..6af0953 --- /dev/null +++ b/src/format-check/scripts/utils/path-normalizer.test.js @@ -0,0 +1,43 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +var path_normalizer_1 = require("./path-normalizer"); +var globals_1 = require("@jest/globals"); +(0, globals_1.describe)('PathNormalizer', function () { + var originalBuildSourcesDirectory; + (0, globals_1.beforeEach)(function () { + // Save the original environment variable + originalBuildSourcesDirectory = process.env.BUILD_SOURCESDIRECTORY; + }); + (0, globals_1.afterEach)(function () { + // Restore the original environment variable + process.env.BUILD_SOURCESDIRECTORY = originalBuildSourcesDirectory; + }); + (0, globals_1.it)('should normalize file path by removing Settings.Environment.sourcesDirectory', function () { + // Arrange + var mockSettings = { + Environment: { + sourcesDirectory: '/source' + } + }; + var filePath = '/source/folder/file.txt'; + var normalizer = new path_normalizer_1.PathNormalizer(mockSettings); + // Act + var normalized = normalizer.normalizeFilePath(filePath); + // Assert + (0, globals_1.expect)(normalized).toBe('/folder/file.txt'); + }); + (0, globals_1.it)('should normalize file path and always keep a /', function () { + // Arrange + var mockSettings = { + Environment: { + sourcesDirectory: '/source/' + } + }; + var filePath = '/source/folder/file.txt'; + var normalizer = new path_normalizer_1.PathNormalizer(mockSettings); + // Act + var normalized = normalizer.normalizeFilePath(filePath); + // Assert + (0, globals_1.expect)(normalized).toBe('/folder/file.txt'); + }); +}); diff --git a/src/format-check/task.json b/src/format-check/task.json index 5e8c6e8..58ec126 100644 --- a/src/format-check/task.json +++ b/src/format-check/task.json @@ -8,7 +8,7 @@ "runsOn": ["Agent", "DeploymentGroup"], "execution": { "Node10": { - "target": "scripts/main.js", + "target": "dist/index.js", "inputs": { "SolutionPath": "INPUT_SOLUTIONPATH", "IncludePath": "INPUT_INCLUDEPATH", @@ -21,7 +21,7 @@ } }, "Node20_1": { - "target": "scripts/main.js", + "target": "dist/index.js", "inputs": { "SolutionPath": "INPUT_SOLUTIONPATH", "IncludePath": "INPUT_INCLUDEPATH", diff --git a/src/format-check/tsconfig.json b/src/format-check/tsconfig.json index ae0760e..8dca4a6 100644 --- a/src/format-check/tsconfig.json +++ b/src/format-check/tsconfig.json @@ -1,9 +1,12 @@ { "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", "types": ["node", "jest"], "typeRoots": ["node_modules/@types"], "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": true + "strict": true, + "skipLibCheck": true } } \ No newline at end of file diff --git a/src/vss-extension.json b/src/vss-extension.json index 7f287c1..b53b351 100644 --- a/src/vss-extension.json +++ b/src/vss-extension.json @@ -31,11 +31,7 @@ }, "files": [ { - "path": "format-check/node_modules", - "addressable": true - }, - { - "path": "format-check/scripts", + "path": "format-check/dist", "addressable": true }, {