Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 51 additions & 24 deletions src/commands/fix/fix-env-helpers.mts
Original file line number Diff line number Diff line change
@@ -1,41 +1,68 @@
import { createSocketBranchParser, getBaseGitBranch } from './git.mts'
import { getGithubEnvRepoInfo, getOpenSocketPrs } from './open-pr.mts'
import { debugFn } from '@socketsecurity/registry/lib/debug'

import {
createSocketBranchParser,
getBaseGitBranch,
gitRepoInfo,
} from './git.mts'
import { getOpenSocketPrs } from './open-pr.mts'
import constants from '../../constants.mts'

import type { SocketBranchParser } from './git.mts'
import type { GithubRepoInfo, PrMatch } from './open-pr.mts'
import type { RepoInfo, SocketBranchParser } from './git.mts'
import type { PrMatch } from './open-pr.mts'

async function getEnvRepoInfo(
cwd?: string | undefined,
): Promise<RepoInfo | null> {
// Lazily access constants.ENV.GITHUB_REPOSITORY.
const { GITHUB_REPOSITORY } = constants.ENV
if (!GITHUB_REPOSITORY) {
debugFn('miss: GITHUB_REPOSITORY env var')
}
Comment on lines +19 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The debug statement logs when GITHUB_REPOSITORY is missing but the function continues execution with a potentially undefined value. Since the next line uses ownerSlashRepo without checking if it's defined, consider adding a return null after the debug call to fail early and prevent attempting to parse an undefined string.

Suggested change
if (!GITHUB_REPOSITORY) {
debugFn('miss: GITHUB_REPOSITORY env var')
}
if (!GITHUB_REPOSITORY) {
debugFn('miss: GITHUB_REPOSITORY env var')
return null
}

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

const ownerSlashRepo = GITHUB_REPOSITORY
const slashIndex = ownerSlashRepo.indexOf('/')
if (slashIndex !== -1) {
return {
owner: ownerSlashRepo.slice(0, slashIndex),
repo: ownerSlashRepo.slice(slashIndex + 1),
}
}
return await gitRepoInfo(cwd)
}

export interface CiEnv {
gitEmail: string
gitUser: string
githubToken: string
repoInfo: GithubRepoInfo
repoInfo: RepoInfo
baseBranch: string
branchParser: SocketBranchParser
}

export function getCiEnv(): CiEnv | null {
export async function getCiEnv(): Promise<CiEnv | null> {
const gitEmail = constants.ENV.SOCKET_CLI_GIT_USER_EMAIL
const gitUser = constants.ENV.SOCKET_CLI_GIT_USER_NAME
const githubToken = constants.ENV.SOCKET_CLI_GITHUB_TOKEN
const isCi = !!(
constants.ENV.CI &&
constants.ENV.GITHUB_ACTIONS &&
constants.ENV.GITHUB_REPOSITORY &&
gitEmail &&
gitUser &&
githubToken
)
return isCi
? {
gitEmail,
gitUser,
githubToken,
repoInfo: getGithubEnvRepoInfo()!,
baseBranch: getBaseGitBranch(),
branchParser: createSocketBranchParser(),
}
: null
const isCi = !!(constants.ENV.CI && gitEmail && gitUser && githubToken)
if (!isCi) {
return null
}
const baseBranch = await getBaseGitBranch()
if (!baseBranch) {
return null
}
const repoInfo = await getEnvRepoInfo()
if (!repoInfo) {
return null
}
return {
gitEmail,
gitUser,
githubToken,
repoInfo,
baseBranch,
branchParser: createSocketBranchParser(),
}
}

export async function getOpenPrsForEnvironment(
Expand Down
206 changes: 130 additions & 76 deletions src/commands/fix/git.mts
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,77 @@ function formatBranchName(name: string): string {
return name.replace(/[^-a-zA-Z0-9/._-]+/g, '+')
}

export function getBaseGitBranch(): string {
// Lazily access constants.ENV.GITHUB_REF_NAME.
return (
constants.ENV.GITHUB_REF_NAME ||
// GitHub defaults to branch name "main"
// https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches#about-the-default-branch
'main'
)
export type SocketBranchParser = (
branch: string,
) => SocketBranchParseResult | null

export type SocketBranchParseResult = {
fullName: string
newVersion: string
type: string
workspace: string
version: string
}

export function getSocketBranchPurlTypeComponent(
purl: string | PackageURL | SocketArtifact,
): string {
const purlObj = getPurlObject(purl)
return formatBranchName(purlObj.type)
export type SocketBranchPatternOptions = {
newVersion?: string | undefined
purl?: string | undefined
workspace?: string | undefined
}

export function createSocketBranchParser(
options?: SocketBranchPatternOptions | undefined,
): SocketBranchParser {
const pattern = getSocketBranchPattern(options)
return function parse(branch: string): SocketBranchParseResult | null {
const match = pattern.exec(branch) as
| [string, string, string, string, string, string]
| null
if (!match) {
return null
}
const {
1: type,
2: workspace,
3: fullName,
4: version,
5: newVersion,
} = match
return {
fullName,
newVersion: semver.coerce(newVersion.replaceAll('+', '.'))?.version,
type,
workspace,
version: semver.coerce(version.replaceAll('+', '.'))?.version,
} as SocketBranchParseResult
}
}

export async function getBaseGitBranch(cwd = process.cwd()): Promise<string> {
// Lazily access constants.ENV properties.
const { GITHUB_BASE_REF, GITHUB_REF_NAME, GITHUB_REF_TYPE } = constants.ENV
// 1. In a pull request, this is always the base branch.
if (GITHUB_BASE_REF) {
return GITHUB_BASE_REF
}
// 2. If it's a branch (not a tag), GITHUB_REF_TYPE should be 'branch'.
if (GITHUB_REF_TYPE === 'branch' && GITHUB_REF_NAME) {
return GITHUB_REF_NAME
}
// 3. Try to resolve the default remote branch using 'git remote show origin'.
// This handles detached HEADs or workflows triggered by tags/releases.
try {
const stdout = (
await spawn('git', ['remote', 'show', 'origin'], { cwd })
).stdout.trim()
const match = /(?<=HEAD branch: ).+/.exec(stdout)
if (match?.[0]) {
return match[0].trim()
}
} catch {}
// GitHub defaults to branch name "main"
// https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-branches#about-the-default-branch
return 'main'
}

export function getSocketBranchFullNameComponent(
Expand All @@ -58,23 +114,6 @@ export function getSocketBranchFullNameComponent(
return `${fmtMaybeNamespace}${formatBranchName(purlObj.name)}`
}

export function getSocketBranchPackageVersionComponent(
version: string | PackageURL | SocketArtifact,
): string {
const purlObj = getPurlObject(
typeof version === 'string' && !version.startsWith('pkg:')
? PackageURL.fromString(`pkg:unknown/unknown@${version}`)
: version,
)
return formatBranchName(purlObj.version!)
}

export function getSocketBranchWorkspaceComponent(
workspace: string | undefined,
): string {
return workspace ? formatBranchName(workspace) : 'root'
}

export function getSocketBranchName(
purl: string | PackageURL | SocketArtifact,
newVersion: string,
Expand All @@ -89,10 +128,15 @@ export function getSocketBranchName(
return `socket/${fmtType}/${fmtWorkspace}/${fmtFullName}_${fmtVersion}_${fmtNewVersion}`
}

export type SocketBranchPatternOptions = {
newVersion?: string | undefined
purl?: string | undefined
workspace?: string | undefined
export function getSocketBranchPackageVersionComponent(
version: string | PackageURL | SocketArtifact,
): string {
const purlObj = getPurlObject(
typeof version === 'string' && !version.startsWith('pkg:')
? PackageURL.fromString(`pkg:unknown/unknown@${version}`)
: version,
)
return formatBranchName(purlObj.version!)
}

export function getSocketBranchPattern(
Expand Down Expand Up @@ -124,54 +168,27 @@ export function getSocketBranchPattern(
)
}

export type SocketBranchParser = (
branch: string,
) => SocketBranchParseResult | null

export type SocketBranchParseResult = {
fullName: string
newVersion: string
type: string
workspace: string
version: string
export function getSocketBranchPurlTypeComponent(
purl: string | PackageURL | SocketArtifact,
): string {
const purlObj = getPurlObject(purl)
return formatBranchName(purlObj.type)
}

export function createSocketBranchParser(
options?: SocketBranchPatternOptions | undefined,
): SocketBranchParser {
const pattern = getSocketBranchPattern(options)
return function parse(branch: string): SocketBranchParseResult | null {
const match = pattern.exec(branch) as
| [string, string, string, string, string, string]
| null
if (!match) {
return null
}
const {
1: type,
2: workspace,
3: fullName,
4: version,
5: newVersion,
} = match
return {
fullName,
newVersion: semver.coerce(newVersion.replaceAll('+', '.'))?.version,
type,
workspace,
version: semver.coerce(version.replaceAll('+', '.'))?.version,
} as SocketBranchParseResult
}
export function getSocketBranchWorkspaceComponent(
workspace: string | undefined,
): string {
return workspace ? formatBranchName(workspace) : 'root'
}

export function getSocketPullRequestTitle(
export function getSocketCommitMessage(
purl: string | PackageURL | SocketArtifact,
newVersion: string,
workspace?: string | undefined,
): string {
const purlObj = getPurlObject(purl)
const fullName = getPkgFullNameFromPurl(purlObj)
return `Bump ${fullName} from ${purlObj.version} to ${newVersion}${workspace ? ` in ${workspace}` : ''}`
return `socket: Bump ${fullName} from ${purlObj.version} to ${newVersion}${workspace ? ` in ${workspace}` : ''}`
}

export function getSocketPullRequestBody(
Expand All @@ -185,14 +202,14 @@ export function getSocketPullRequestBody(
return `Bump [${fullName}](${pkgOverviewUrl}) from ${purlObj.version} to ${newVersion}${workspace ? ` in ${workspace}` : ''}.`
}

export function getSocketCommitMessage(
export function getSocketPullRequestTitle(
purl: string | PackageURL | SocketArtifact,
newVersion: string,
workspace?: string | undefined,
): string {
const purlObj = getPurlObject(purl)
const fullName = getPkgFullNameFromPurl(purlObj)
return `socket: Bump ${fullName} from ${purlObj.version} to ${newVersion}${workspace ? ` in ${workspace}` : ''}`
return `Bump ${fullName} from ${purlObj.version} to ${newVersion}${workspace ? ` in ${workspace}` : ''}`
}

export async function gitCleanFdx(cwd = process.cwd()): Promise<void> {
Expand Down Expand Up @@ -227,7 +244,10 @@ export async function gitCreateAndPushBranch(
)
return true
} catch (e) {
debugFn('catch: unexpected\n', e)
debugFn(
`catch: git push --force --set-upstream origin ${branch} failed\n`,
e,
)
}
try {
// Will throw with exit code 1 if branch does not exist.
Expand All @@ -236,6 +256,40 @@ export async function gitCreateAndPushBranch(
return false
}

export type RepoInfo = {
owner: string
repo: string
}

export async function gitRepoInfo(
cwd = process.cwd(),
): Promise<RepoInfo | null> {
try {
const remoteUrl = (
await spawn('git', ['remote', 'get-url', 'origin'], { cwd })
).stdout.trim()
// 1. Handle SSH-style, e.g. git@github.com:owner/repo.git
const sshMatch = /^git@[^:]+:([^/]+)\/(.+?)(?:\.git)?$/.exec(remoteUrl)
if (sshMatch) {
return { owner: sshMatch[1]!, repo: sshMatch[2]! }
}
// 2. Handle HTTPS/URL-style, e.g. https://github.com/owner/repo.git
try {
const parsed = new URL(remoteUrl)
const segments = parsed.pathname.split('/')
const owner = segments.at(-2)
const repo = segments.at(-1)?.replace(/\.git$/, '')
if (owner && repo) {
return { owner, repo }
}
} catch {}
debugFn('git: unmatched git remote URL format', remoteUrl)
} catch (e) {
debugFn('catch: git remote get-url origin failed\n', e)
}
return null
}

export async function gitEnsureIdentity(
name: string,
email: string,
Expand All @@ -260,7 +314,7 @@ export async function gitEnsureIdentity(
try {
await spawn('git', ['config', prop, value], stdioIgnoreOptions)
} catch (e) {
debugFn('catch: unexpected\n', e)
debugFn(`catch: git config ${prop} ${value} failed\n`, e)
}
}
}),
Expand Down
2 changes: 1 addition & 1 deletion src/commands/fix/npm-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export async function npmFix(

spinner?.start()

const ciEnv = getCiEnv()
const ciEnv = await getCiEnv()
const openPrs = ciEnv ? await getOpenPrsForEnvironment(ciEnv) : []

let count = 0
Expand Down
Loading