UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

353 lines (351 loc) • 17 kB
import { Gitlab } from "@gitbeaker/rest"; import c from "chalk"; import { Agent as HttpsAgent } from "https"; import { getCurrentGitBranch, git, uxLog } from "../utils/index.js"; import { GitProviderRoot } from "./gitProviderRoot.js"; import { CONSTANTS } from "../../config/index.js"; export class GitlabProvider extends GitProviderRoot { gitlabApi; serverUrl; token; constructor() { super(); // Gitlab URL is always provided by default CI variables this.serverUrl = process.env.CI_SERVER_URL || ""; // It's better to have a project token defined in a CI_SFDX_HARDIS_GITLAB_TOKEN variable, to have the rights to act on Pull Requests this.token = process.env.CI_SFDX_HARDIS_GITLAB_TOKEN || process.env.ACCESS_TOKEN || ""; const gitlabConfig = { host: this.serverUrl, token: this.token, }; if (process.env.GITLAB_API_REJECT_UNAUTHORIZED === "false") { gitlabConfig.agent = new HttpsAgent({ rejectUnauthorized: false }); } this.gitlabApi = new Gitlab(gitlabConfig); } getLabel() { return "sfdx-hardis Gitlab connector"; } // Returns current job URL async getCurrentJobUrl() { if (process.env.PIPELINE_JOB_URL) { return process.env.PIPELINE_JOB_URL; } if (process.env.CI_JOB_URL) { return process.env.CI_JOB_URL; } return null; } // Returns current job URL async getCurrentBranchUrl() { if (process.env.CI_PROJECT_URL && process.env.CI_COMMIT_REF_NAME) return `${process.env.CI_PROJECT_URL}/-/tree/${process.env.CI_COMMIT_REF_NAME}`; return null; } // Gitlab supports mermaid in PR markdown async supportsMermaidInPrMarkdown() { return true; } // Find pull request info async getPullRequestInfo() { // Case when MR is found in the context const projectId = process.env.CI_PROJECT_ID || null; const mrNumber = process.env.CI_MERGE_REQUEST_IID || null; if (mrNumber !== null) { const mergeRequests = await this.gitlabApi.MergeRequests.all({ projectId: projectId || "", iids: [parseInt(mrNumber)], }); if (mergeRequests.length > 0) { const mergeRequest = mergeRequests[0]; return this.completePullRequestInfo(mergeRequest); } } // Case when we find MR from a commit const sha = await git().revparse(["HEAD"]); // Fetch recent merged MRs and pick the one whose merge commit SHA matches the current HEAD let allMergedMRs = []; try { // Prefer the commit-level endpoint (more efficient) if available: // GET /projects/:id/repository/commits/:sha/merge_requests // This returns merge requests related to the commit directly. try { const commitMrs = await this.gitlabApi.Commits.allMergeRequests(projectId || "", sha); if (Array.isArray(commitMrs) && commitMrs.length > 0) { allMergedMRs = commitMrs; } } catch (err) { // Some GitLab instances or gitbeaker versions may not expose this helper -> fall back below uxLog("log", this, c.grey(`[Gitlab Integration] Commit-level MR lookup not available or failed: ${String(err)}. Falling back to filtered MR list.`)); } // Fallback: fetch merged MRs but narrow the scope to be performant if (allMergedMRs.length === 0) { // try to limit by the current branch (CI variable or local git) const currentBranch = process.env.CI_COMMIT_REF_NAME || (await getCurrentGitBranch()); allMergedMRs = await this.gitlabApi.MergeRequests.all({ projectId: projectId || "", state: "merged", // prefer filtering by targetBranch to reduce results; if unknown, omit the filter ...(currentBranch ? { targetBranch: currentBranch } : {}), orderBy: "updated_at", sort: "desc", perPage: 100, maxPages: 1, }); } } catch (err) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Error fetching merged MRs: ${String(err)}`)); // as a last resort try a small unfiltered query to avoid huge responses try { allMergedMRs = await this.gitlabApi.MergeRequests.all({ projectId: projectId || "", state: "merged", perPage: 10, maxPages: 1, orderBy: "updated_at", sort: "desc", }); } catch (innerErr) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Fallback query failed: ${String(innerErr)}`)); allMergedMRs = []; } } const matchedMr = allMergedMRs.find((mr) => { const mergeSha = mr.mergeCommitSha || mr.merge_commit_sha; return mergeSha === sha; }); const latestMergeRequestsOnBranch = matchedMr ? [matchedMr] : []; if (latestMergeRequestsOnBranch.length > 0) { const currentGitBranch = await getCurrentGitBranch(); const candidateMergeRequests = latestMergeRequestsOnBranch.filter((pr) => pr.target_branch === currentGitBranch); if (candidateMergeRequests.length > 0) { return this.completePullRequestInfo(candidateMergeRequests[0]); } } uxLog("log", this, c.grey(`[Gitlab Integration] Unable to find related Merge Request Info`)); return null; } async getBranchDeploymentCheckId(gitBranch) { let deploymentCheckId = null; const projectId = process.env.CI_PROJECT_ID || null; const latestMergeRequestsOnBranch = await this.gitlabApi.MergeRequests.all({ projectId: projectId || "", state: "merged", sort: "desc", targetBranch: gitBranch, }); if (latestMergeRequestsOnBranch.length > 0) { const latestMergeRequest = latestMergeRequestsOnBranch[0]; const latestMergeRequestId = latestMergeRequest.iid; deploymentCheckId = await this.getDeploymentIdFromPullRequest(projectId || "", latestMergeRequestId, deploymentCheckId, this.completePullRequestInfo(latestMergeRequest)); } return deploymentCheckId; } async getPullRequestDeploymentCheckId() { const pullRequestInfo = await this.getPullRequestInfo(); if (pullRequestInfo) { const projectId = process.env.CI_PROJECT_ID || null; return await this.getDeploymentIdFromPullRequest(projectId || "", pullRequestInfo.idNumber, null, pullRequestInfo); } return null; } async getDeploymentIdFromPullRequest(projectId, latestMergeRequestId, deploymentCheckId, latestMergeRequest) { const existingNotes = await this.gitlabApi.MergeRequestNotes.all(projectId, latestMergeRequestId); for (const existingNote of existingNotes) { if (existingNote.body.includes("<!-- sfdx-hardis deployment-id ")) { const matches = /<!-- sfdx-hardis deployment-id (.*) -->/gm.exec(existingNote.body); if (matches) { deploymentCheckId = matches[1]; uxLog("error", this, c.grey(`Found deployment id ${deploymentCheckId} on MR #${latestMergeRequestId} ${latestMergeRequest.title}`)); break; } } } return deploymentCheckId; } // Posts a note on the merge request async postPullRequestMessage(prMessage) { // Get CI variables const prInfo = await this.getPullRequestInfo(); const projectId = process.env.CI_PROJECT_ID || null; const mergeRequestIdRaw = process.env.CI_MERGE_REQUEST_IID || process.env.CI_MERGE_REQUEST_ID || prInfo?.idStr || null; const mergeRequestId = mergeRequestIdRaw ? parseInt(String(mergeRequestIdRaw), 10) : NaN; if (projectId == null || !Number.isFinite(mergeRequestId)) { uxLog("log", this, c.grey("[Gitlab integration] No project and merge request, so no note posted...")); return { posted: false, providerResult: { info: "No related merge request" } }; } const gitlabCiJobName = process.env.CI_JOB_NAME; const gitlabCIJobUrl = process.env.CI_JOB_URL; // Build note message const messageKey = prMessage.messageKey + "-" + gitlabCiJobName + "-" + mergeRequestId; let messageBody = `## ${prMessage.title || ""} ${prMessage.message} _Powered by [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT}) from job [${gitlabCiJobName}](${gitlabCIJobUrl})_ <!-- sfdx-hardis message-key ${messageKey} --> `; // Add deployment id if present if (globalThis.pullRequestDeploymentId) { messageBody += `\n<!-- sfdx-hardis deployment-id ${globalThis.pullRequestDeploymentId} -->`; } // Check for existing note from a previous run uxLog("log", this, c.grey("[Gitlab integration] Listing Notes of Merge Request...")); const existingNotes = await this.gitlabApi.MergeRequestNotes.all(projectId, mergeRequestId); let existingNoteId = null; for (const existingNote of existingNotes) { if (existingNote.body.includes(`<!-- sfdx-hardis message-key ${messageKey} -->`)) { existingNoteId = existingNote.id; } } // Create or update MR note if (existingNoteId) { // Update existing note uxLog("log", this, c.grey("[Gitlab integration] Updating Merge Request Note on Gitlab...")); const gitlabEditNoteResult = await this.gitlabApi.MergeRequestNotes.edit(projectId, mergeRequestId, existingNoteId, { body: messageBody }); const prResult = { posted: gitlabEditNoteResult.id > 0, providerResult: gitlabEditNoteResult, }; return prResult; } else { // Create new note if no existing not was found uxLog("log", this, c.grey("[Gitlab integration] Adding Merge Request Note on Gitlab...")); const gitlabPostNoteResult = await this.gitlabApi.MergeRequestNotes.create(projectId, mergeRequestId, messageBody); const prResult = { posted: gitlabPostNoteResult.id > 0, providerResult: gitlabPostNoteResult, }; return prResult; } } async listPullRequestsInBranchSinceLastMerge(currentBranchName, targetBranchName, childBranchesNames) { if (!this.gitlabApi) { return []; } try { // Get project ID from the API configuration const projectId = process.env.CI_PROJECT_ID || process.env.CI_PROJECT_PATH; if (!projectId) { uxLog("warning", this, c.yellow("[Gitlab Integration] CI_PROJECT_ID or CI_PROJECT_PATH environment variable is required")); return []; } // Step 1: Find the last merged MR from currentBranch to targetBranch uxLog("log", this, c.grey(`[Gitlab Integration] Finding last merged MR from ${currentBranchName} to ${targetBranchName}`)); const lastMergeToTarget = await this.findLastMergedMR(currentBranchName, targetBranchName, projectId); // Step 2: Get all commits in currentBranch since that merge (or all if no previous merge) const commitsSinceLastMerge = await this.getCommitsSinceLastMerge(currentBranchName, lastMergeToTarget, projectId); if (commitsSinceLastMerge.length === 0) { return []; } // Create a Set of commit SHAs for fast lookup const commitSHAs = new Set(commitsSinceLastMerge.map((c) => c.id)); // Step 3: Get all merged MRs targeting currentBranch and child branches (parallelized) const allBranches = [currentBranchName, ...childBranchesNames]; const mrPromises = allBranches.map(async (branchName) => { try { const mergedMRs = await this.gitlabApi.MergeRequests.all({ projectId, targetBranch: branchName, state: "merged", perPage: 100, }); uxLog("log", this, c.grey(`[Gitlab Integration] Fetching merged MRs for branch ${branchName}`)); return mergedMRs; } catch (err) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Error fetching merged MRs for branch ${branchName}: ${String(err)}`)); return []; } }); const mrResults = await Promise.all(mrPromises); const allMergedMRs = mrResults.flat(); // Step 4: Filter MRs whose merge commit SHA is in our commit list const relevantMRs = allMergedMRs.filter((mr) => { // Check if the merge commit SHA is in our commits const mergeCommitSha = mr.mergeCommitSha || mr.merge_commit_sha; if (mergeCommitSha && commitSHAs.has(mergeCommitSha)) { return true; } // Also check if the MR's SHA (last commit before merge) is in our commits if (mr.sha && commitSHAs.has(mr.sha)) { return true; } return false; }); // Step 5: Remove duplicates (same MR might be found through different branches) const uniqueMRsMap = new Map(); for (const mr of relevantMRs) { if (mr.iid && !uniqueMRsMap.has(mr.iid)) { uniqueMRsMap.set(mr.iid, mr); } } const uniqueMRs = Array.from(uniqueMRsMap.values()); // Step 6: Convert to CommonPullRequestInfo return uniqueMRs.map((mr) => this.completePullRequestInfo(mr)); } catch (err) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Error in listPullRequestsInBranchSinceLastMerge: ${String(err)}\n${err instanceof Error ? err.stack : ""}`)); return []; } } async findLastMergedMR(sourceBranch, targetBranch, projectId) { try { const mergedMRs = await this.gitlabApi.MergeRequests.all({ projectId, sourceBranch, targetBranch, state: "merged", orderBy: "updated_at", sort: "desc", perPage: 1, maxPages: 1, }); return mergedMRs.length > 0 ? mergedMRs[0] : null; } catch (err) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Error finding last merged MR from ${sourceBranch} to ${targetBranch}: ${String(err)}`)); return null; } } async getCommitsSinceLastMerge(branchName, lastMerge, projectId) { try { const options = { refName: branchName, perPage: 100, }; // If there was a previous merge, get commits since that merge commit if (lastMerge) { const mergeCommitSha = lastMerge.mergeCommitSha || lastMerge.merge_commit_sha; if (mergeCommitSha) { // Get commits since the merge commit options.since = lastMerge.mergedAt || lastMerge.merged_at; } } const commits = await this.gitlabApi.Commits.all(projectId, options); return commits || []; } catch (err) { uxLog("warning", this, c.yellow(`[Gitlab Integration] Error fetching commits for branch ${branchName}: ${String(err)}`)); return []; } } completePullRequestInfo(prData) { const prInfo = { idNumber: prData?.iid || prData?.id || 0, idStr: String(prData?.iid || prData?.id || ""), sourceBranch: (prData?.source_branch || "").replace("refs/heads/", ""), targetBranch: (prData?.target_branch || "").replace("refs/heads/", ""), title: prData?.title || "", description: prData?.description || "", authorName: prData?.author?.name || "", webUrl: prData?.web_url || "", providerInfo: prData, customBehaviors: {} }; return this.completeWithCustomBehaviors(prInfo); } } //# sourceMappingURL=gitlab.js.map