All files / packages/sds-release-manager/src/services git-service.ts

100% Statements 41/41
100% Branches 12/12
100% Functions 12/12
100% Lines 41/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134                                                  8x 8x 8x 8x 8x       9x       26x       2x     2x 1x   1x       1x 1x                 1x 1x       3x     3x 3x 1x     2x     2x 2x 2x 1x     1x               1x 1x 1x           1x       1x     1x       1x 1x       2x     2x 2x 1x 1x 1x             1x 1x      
import { DEFAULT_PIN_CATALOG_FILE, DEFAULT_PIN_VERSION_FILE } from '../config.js';
import type { Logger } from '../utils/logger.js';
import type { ApplyPinUpdateInput, PinBlock } from '../utils/pin-files.js';
import type { ISshService } from './ssh-service.js';
 
export type { ApplyPinUpdateInput, PinBlock } from '../utils/pin-files.js';
 
export interface IGitService {
  verifyConnectivity(): Promise<string>;
  fetchAndCheckoutTarget(remote: string, targetBranch: string): Promise<void>;
  createBranch(newBranch: string): Promise<void>;
  readSldsScsPin(): Promise<PinBlock>;
  applyPinUpdate(input: ApplyPinUpdateInput): Promise<void>;
  stagePinFiles(): Promise<string>;
  commit(message: string): Promise<void>;
  ensureGitcorePushUrl(remote: string, rel: string): Promise<void>;
  push(remote: string, branch: string): Promise<void>;
}
 
/**
 * Git operations for the Bazel pin bump on a remote Workspace checkout.
 * Kept for Workspace-backed commands; bump-bazel-pin uses gitcore-service instead.
 */
export class GitService implements IGitService {
  constructor(
    private readonly ssh: ISshService,
    private readonly corePath: string,
    private readonly logger: Logger,
    private readonly pinVersionFile = DEFAULT_PIN_VERSION_FILE,
    private readonly pinCatalogFile = DEFAULT_PIN_CATALOG_FILE,
  ) {}
 
  private cd(cmd: string): string {
    return `cd ${this.shellQuote(this.corePath)} && ${cmd}`;
  }
 
  private shellQuote(s: string): string {
    return "'" + s.replace(/'/g, "'\\''") + "'";
  }
 
  async verifyConnectivity(): Promise<string> {
    const { stdout } = await this.ssh.runRemote(
      `echo ok && git -C ${this.shellQuote(this.corePath)} remote -v`,
    );
    if (!stdout.includes('ok')) {
      throw new Error('Workspace connectivity check failed');
    }
    return stdout;
  }
 
  async fetchAndCheckoutTarget(remote: string, targetBranch: string): Promise<void> {
    this.logger.step(`Fetch and checkout ${remote}/${targetBranch}`);
    await this.ssh.runRemote(
      this.cd(
        `git fetch ${this.shellQuote(remote)} ${this.shellQuote(targetBranch)} && ` +
          `git checkout -B ${this.shellQuote(targetBranch)} FETCH_HEAD`,
      ),
    );
  }
 
  async createBranch(newBranch: string): Promise<void> {
    this.logger.step(`Create branch ${newBranch}`);
    await this.ssh.runRemote(this.cd(`git checkout -b ${this.shellQuote(newBranch)}`));
  }
 
  async readSldsScsPin(): Promise<PinBlock> {
    const { stdout: versionOut } = await this.ssh.runRemote(
      `grep '_SLDS_SCS_VERSION' ${this.shellQuote(`${this.corePath}/${this.pinVersionFile}`)}`,
    );
    const versionMatch = versionOut.match(/_SLDS_SCS_VERSION\s*=\s*"([^"]+)"/);
    if (!versionMatch) {
      throw new Error(`Could not parse _SLDS_SCS_VERSION from remote:\n${versionOut}`);
    }
 
    const { stdout: blockOut } = await this.ssh.runRemote(
      `grep -A8 -B2 org_sfs_slds_scs ${this.shellQuote(`${this.corePath}/${this.pinCatalogFile}`)}`,
    );
    const sha256Match = blockOut.match(/artifact_sha256\s*=\s*"([^"]+)"/);
    const sha1Match = blockOut.match(/artifact_sha1\s*=\s*"([^"]+)"/);
    if (!sha256Match || !sha1Match) {
      throw new Error(`Could not parse slds-scs SHAs from remote:\n${blockOut}`);
    }
 
    return {
      version: versionMatch[1],
      sha256: sha256Match[1],
      sha1: sha1Match[1],
    };
  }
 
  async applyPinUpdate(input: ApplyPinUpdateInput): Promise<void> {
    this.logger.step(`Update pin ${input.oldVersion} → ${input.newVersion}`);
    const versionSed = `sed -i 's|_SLDS_SCS_VERSION = "${input.oldVersion}"|_SLDS_SCS_VERSION = "${input.newVersion}"|' ${this.pinVersionFile}`;
    const catalogSed = [
      `sed -i 's|artifact = "org.sfs:slds-scs:jar:${input.oldVersion}"|artifact = "org.sfs:slds-scs:jar:${input.newVersion}"|' ${this.pinCatalogFile}`,
      `sed -i 's|artifact_sha256 = "${input.oldSha256}"|artifact_sha256 = "${input.newSha256}"|' ${this.pinCatalogFile}`,
      `sed -i 's|artifact_sha1 = "${input.oldSha1}"|artifact_sha1 = "${input.newSha1}"|' ${this.pinCatalogFile}`,
    ].join(' && ');
 
    await this.ssh.runRemote(this.cd(`${versionSed} && ${catalogSed}`));
  }
 
  async stagePinFiles(): Promise<string> {
    const { stdout } = await this.ssh.runRemote(
      this.cd(`git add ${this.pinVersionFile} ${this.pinCatalogFile} && git diff --cached`),
    );
    return stdout;
  }
 
  async commit(message: string): Promise<void> {
    const escaped = message.replace(/'/g, `'\\''`);
    await this.ssh.runRemote(this.cd(`git commit -m '${escaped}'`));
  }
 
  async ensureGitcorePushUrl(remote: string, rel: string): Promise<void> {
    const { stdout } = await this.ssh.runRemote(
      this.cd(`git remote get-url --push ${this.shellQuote(remote)}`),
    );
    const pushUrl = stdout.trim();
    if (pushUrl.includes('gitcore-cache')) {
      const gitcoreUrl = `https://gitcore.soma.salesforce.com/core-2206/core-${rel}-public.git`;
      this.logger.warn(`Push URL is cache host; setting push URL to ${gitcoreUrl}`);
      await this.ssh.runRemote(
        this.cd(`git remote set-url --push ${this.shellQuote(remote)} ${this.shellQuote(gitcoreUrl)}`),
      );
    }
  }
 
  async push(remote: string, branch: string): Promise<void> {
    this.logger.step(`Push ${branch} to ${remote}`);
    await this.ssh.runRemote(this.cd(`git push -u ${this.shellQuote(remote)} ${this.shellQuote(branch)}`));
  }
}