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
255 lines (253 loc) • 11 kB
JavaScript
import * as github from "@actions/github";
import c from "chalk";
import { GitProviderRoot } from "./gitProviderRoot.js";
import { getCurrentGitBranch, git, uxLog } from "../utils/index.js";
import { CONSTANTS } from "../../config/index.js";
export class GithubProvider extends GitProviderRoot {
octokit;
repoOwner;
repoName;
serverUrl;
workflow;
branch;
prNumber;
runId;
constructor() {
super();
const tokenName = process.env.CI_SFDX_HARDIS_GITHUB_TOKEN ? "CI_SFDX_HARDIS_GITHUB_TOKEN" : process.env.PAT ? "PAT" : "GITHUB_TOKEN";
const token = process.env[tokenName];
this.octokit = github.getOctokit(token || "");
this.repoOwner = github?.context?.repo?.owner || process.env.GITHUB_REPOSITORY_OWNER || null;
this.repoName = github?.context?.repo?.repo || process.env?.GITHUB_REPOSITORY?.split("/")[1] || null;
this.serverUrl = github?.context?.serverUrl || process.env.GITHUB_SERVER_URL || null;
this.workflow = github?.context?.workflow || process.env.GITHUB_WORKFLOW || null;
this.branch = github?.context?.ref || process.env.GITHUB_REF || null;
this.prNumber = github?.context?.payload?.pull_request?.number || (process.env.GITHUB_REF_NAME ? parseInt(process.env.GITHUB_REF_NAME.split("/")?.[0] || "0") : null);
this.runId = github?.context?.runId || process.env.GITHUB_RUN_ID || null;
}
getLabel() {
return "sfdx-hardis GitHub connector";
}
async getBranchDeploymentCheckId(gitBranch) {
let deploymentCheckId = null;
uxLog(this, c.grey("[GitHub Integration] Listing previously closed Pull Requests"));
const latestPullRequestsOnBranch = await this.octokit.rest.pulls.list({
owner: this.repoOwner || "",
repo: this.repoName || "",
state: "closed",
direction: "desc",
per_page: 10,
base: gitBranch,
});
if (latestPullRequestsOnBranch.data.length > 0) {
const latestPullRequest = latestPullRequestsOnBranch.data[0];
const latestPullRequestId = latestPullRequest.number;
deploymentCheckId = await this.getDeploymentIdFromPullRequest(latestPullRequestId, this.repoOwner || "", this.repoName || "", deploymentCheckId, latestPullRequest);
}
return deploymentCheckId;
}
async getPullRequestDeploymentCheckId() {
const pullRequestInfo = await this.getPullRequestInfo();
if (pullRequestInfo) {
return await this.getDeploymentIdFromPullRequest(pullRequestInfo.idNumber, this.repoOwner || "", this.repoName || "", null, pullRequestInfo);
}
return null;
}
async getDeploymentIdFromPullRequest(latestPullRequestId, repoOwner, repoName, deploymentCheckId, latestPullRequest) {
uxLog(this, c.grey(`[GitHub Integration] Listing comments for PR ${latestPullRequestId}`));
const existingComments = await this.octokit.rest.issues.listComments({
owner: repoOwner,
repo: repoName,
issue_number: latestPullRequestId,
});
for (const existingComment of existingComments.data) {
if ((existingComment.body || "").includes("<!-- sfdx-hardis deployment-id ")) {
const matches = /<!-- sfdx-hardis deployment-id (.*) -->/gm.exec(existingComment.body || "");
if (matches) {
deploymentCheckId = matches[1];
uxLog(this, c.gray(`Found deployment id ${deploymentCheckId} on PR #${latestPullRequestId} ${latestPullRequest.title}`));
break;
}
}
}
return deploymentCheckId;
}
// Returns current job URL
async getCurrentJobUrl() {
if (process.env.PIPELINE_JOB_URL) {
return process.env.PIPELINE_JOB_URL;
}
try {
if (this.repoOwner && this.repoName && this.serverUrl && this.runId) {
return `${this.serverUrl}/${this.repoOwner}/${this.repoName}/actions/runs/${this.runId}`;
}
}
catch (err) {
uxLog(this, c.yellow("[GitHub Integration]" + err.message));
}
if (process.env.GITHUB_JOB_URL) {
return process.env.GITHUB_JOB_URL;
}
return null;
}
// Returns current job URL
async getCurrentBranchUrl() {
try {
if (this.repoOwner && this.repoName && this.serverUrl && this.branch) {
return `${this.serverUrl}/${this.repoOwner}/${this.repoName}/tree/${this.branch}`;
}
}
catch (err) {
uxLog(this, c.yellow("[GitHub Integration]" + err.message));
}
return null;
}
// GitHub supports mermaid in PR markdown
async supportsMermaidInPrMarkdown() {
return true;
}
// Find pull request info
async getPullRequestInfo() {
// Case when PR is found in the context
if (this.prNumber !== null && this.repoOwner !== null) {
const pullRequest = await this.octokit.rest.pulls.get({
owner: this.repoOwner,
repo: this.repoName || "",
pull_number: this.prNumber,
});
// Add cross git provider properties used by sfdx-hardis
if (pullRequest) {
return this.completePullRequestInfo(pullRequest.data);
}
}
// Case when we find PRs from a commit
const sha = await git().revparse(["HEAD"]);
let graphQlRes = null;
try {
graphQlRes = await this.octokit.graphql(`
query associatedPRs($sha: String, $repo: String!, $owner: String!){
repository(name: $repo, owner: $owner) {
commit: object(expression: $sha) {
... on Commit {
associatedPullRequests(first:10){
edges{
node{
title
number
body
url
merged,
baseRef {
id
name
}
author {
login
}
}
}
}
}
}
}
}
`, {
sha: sha,
repo: this.repoName,
owner: this.repoOwner,
});
}
catch (error) {
uxLog(this, c.yellow(`[GitHub Integration] Error while calling GraphQL Api to list PR on commit ${sha}\n${error.message}`));
}
if (graphQlRes?.repository?.commit?.associatedPullRequests?.edges?.length > 0) {
const currentGitBranch = await getCurrentGitBranch();
const candidatePullRequests = graphQlRes.repository.commit.associatedPullRequests.edges.filter((pr) => pr.node.merged === true && pr.node.baseRef.name === currentGitBranch);
if (candidatePullRequests.length > 0) {
return this.completePullRequestInfo(candidatePullRequests[0].node);
}
}
uxLog(this, c.grey(`[GitHub Integration] Unable to find related Pull Request Info`));
return null;
}
// Posts a note on the merge request
async postPullRequestMessage(prMessage) {
if (this.repoName == null || this.prNumber == null) {
uxLog(this, c.grey("[GitHub Integration] No project and merge request, so no note posted..."));
return { posted: false, providerResult: { info: "No related pull request" } };
}
const githubJobUrl = await this.getCurrentJobUrl();
// Build note message
const messageKey = prMessage.messageKey + "-" + this.workflow + "-" + this.prNumber;
let messageBody = `**${prMessage.title || ""}**
${prMessage.message}
_Powered by [sfdx-hardis](${CONSTANTS.DOC_URL_ROOT}) from job [${this.workflow}](${githubJobUrl})_
<!-- 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(this, c.grey("[GitHub Integration] Listing comments of Pull Request..."));
const existingComments = await this.octokit.rest.issues.listComments({
owner: this.repoOwner || "",
repo: this.repoName,
issue_number: this.prNumber,
});
let existingCommentId = null;
for (const existingComment of existingComments.data) {
if (existingComment?.body?.includes(`<!-- sfdx-hardis message-key ${messageKey} -->`)) {
existingCommentId = existingComment.id;
}
}
// Create or update MR note
if (existingCommentId) {
// Update existing note
uxLog(this, c.grey("[GitHub Integration] Updating Pull Request Comment on GitHub..."));
const githubCommentEditResult = await this.octokit.rest.issues.updateComment({
owner: this.repoOwner || "",
repo: this.repoName,
issue_number: this.prNumber,
comment_id: existingCommentId,
body: messageBody,
});
const prResult = {
posted: githubCommentEditResult.data.id > 0,
providerResult: githubCommentEditResult,
};
return prResult;
}
else {
// Create new note if no existing not was found
uxLog(this, c.grey("[GitHub Integration] Adding Pull Request Comment on GitHub..."));
const githubCommentCreateResult = await this.octokit.rest.issues.createComment({
owner: this.repoOwner || "",
repo: this.repoName,
issue_number: this.prNumber,
body: messageBody,
});
const prResult = {
posted: githubCommentCreateResult.data.id > 0,
providerResult: githubCommentCreateResult,
};
return prResult;
}
}
completePullRequestInfo(prData) {
const prInfo = {
idNumber: prData?.number || 0,
idStr: String(prData.number || ""),
sourceBranch: (prData?.head?.ref || "").replace("refs/heads/", ""),
targetBranch: (prData?.base?.ref || "").replace("refs/heads/", ""),
title: prData?.title || "",
description: prData?.body || "",
authorName: prData?.user?.login || "",
webUrl: prData?.html_url || "",
providerInfo: prData,
customBehaviors: {}
};
return this.completeWithCustomBehaviors(prInfo);
}
}
//# sourceMappingURL=github.js.map