From 0fc03b5fce9ab3ab170cd4aed6c140882a24da1e Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 12 Jun 2025 11:22:07 -0400 Subject: [PATCH] Reduce reliance on github action specific environment vars --- src/commands/fix/fix-env-helpers.mts | 75 ++++++---- src/commands/fix/git.mts | 206 +++++++++++++++++---------- src/commands/fix/npm-fix.mts | 2 +- src/commands/fix/open-pr.mts | 27 ---- src/commands/fix/pnpm-fix.mts | 2 +- src/constants.mts | 11 +- 6 files changed, 189 insertions(+), 134 deletions(-) diff --git a/src/commands/fix/fix-env-helpers.mts b/src/commands/fix/fix-env-helpers.mts index c3e344a38..ca712b637 100644 --- a/src/commands/fix/fix-env-helpers.mts +++ b/src/commands/fix/fix-env-helpers.mts @@ -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 { + // Lazily access constants.ENV.GITHUB_REPOSITORY. + const { GITHUB_REPOSITORY } = constants.ENV + if (!GITHUB_REPOSITORY) { + debugFn('miss: GITHUB_REPOSITORY env var') + } + 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 { 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( diff --git a/src/commands/fix/git.mts b/src/commands/fix/git.mts index 6e3d8f7f7..277afbf91 100644 --- a/src/commands/fix/git.mts +++ b/src/commands/fix/git.mts @@ -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 { + // 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( @@ -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, @@ -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( @@ -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( @@ -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 { @@ -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. @@ -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 { + 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, @@ -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) } } }), diff --git a/src/commands/fix/npm-fix.mts b/src/commands/fix/npm-fix.mts index 61bbd53ff..e6dfbe155 100644 --- a/src/commands/fix/npm-fix.mts +++ b/src/commands/fix/npm-fix.mts @@ -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 diff --git a/src/commands/fix/open-pr.mts b/src/commands/fix/open-pr.mts index f4e070f14..b578011d8 100644 --- a/src/commands/fix/open-pr.mts +++ b/src/commands/fix/open-pr.mts @@ -268,28 +268,6 @@ export async function enablePrAutoMerge({ return { enabled: false } } -export type GithubRepoInfo = { - owner: string - repo: string -} - -export function getGithubEnvRepoInfo(): GithubRepoInfo | null { - // Lazily access constants.ENV.GITHUB_REPOSITORY. - const { GITHUB_REPOSITORY } = constants.ENV - if (!GITHUB_REPOSITORY) { - debugFn('miss: GITHUB_REPOSITORY env var') - } - const ownerSlashRepo = GITHUB_REPOSITORY - const slashIndex = ownerSlashRepo.indexOf('/') - if (slashIndex === -1) { - return null - } - return { - owner: ownerSlashRepo.slice(0, slashIndex), - repo: ownerSlashRepo.slice(slashIndex + 1), - } -} - export type GetOpenSocketPrsOptions = { author?: string | undefined newVersion?: string | undefined @@ -470,11 +448,6 @@ export async function openPr( __proto__: null, ...options, } as OpenPrOptions - // Lazily access constants.ENV.GITHUB_ACTIONS. - if (!constants.ENV.GITHUB_ACTIONS) { - debugFn('miss: GITHUB_ACTIONS env var') - return null - } const purlObj = getPurlObject(purl) const octokit = getOctokit() try { diff --git a/src/commands/fix/pnpm-fix.mts b/src/commands/fix/pnpm-fix.mts index 71505613f..16ed19b28 100644 --- a/src/commands/fix/pnpm-fix.mts +++ b/src/commands/fix/pnpm-fix.mts @@ -143,7 +143,7 @@ export async function pnpmFix( spinner?.start() - const ciEnv = getCiEnv() + const ciEnv = await getCiEnv() const openPrs = ciEnv ? await getOpenPrsForEnvironment(ciEnv) : [] let count = 0 diff --git a/src/constants.mts b/src/constants.mts index 087f53799..f2573d4d1 100644 --- a/src/constants.mts +++ b/src/constants.mts @@ -46,7 +46,7 @@ type ENV = Remap< RegistryEnv & Readonly<{ DISABLE_GITHUB_CACHE: boolean - GITHUB_ACTIONS: boolean + GITHUB_BASE_REF: string GITHUB_REF_NAME: string GITHUB_REF_TYPE: string GITHUB_REPOSITORY: string @@ -232,10 +232,11 @@ const LAZY_ENV = () => { // Flag to disable using GitHub's workflow actions/cache. // https://github.com/actions/cache DISABLE_GITHUB_CACHE: envAsBoolean(env['DISABLE_GITHUB_CACHE']), - // Always set to true when GitHub Actions is running the workflow. This variable - // can be used to differentiate when tests are being run locally or by GitHub Actions. + // The name of the base ref or target branch of the pull request in a workflow + // run. This is only set when the event that triggers a workflow run is either + // pull_request or pull_request_target. For example, main. // https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#default-environment-variables - GITHUB_ACTIONS: envAsBoolean(env['GITHUB_ACTIONS']), + GITHUB_BASE_REF: envAsString(env['GITHUB_BASE_REF']), // The short ref name of the branch or tag that triggered the GitHub workflow // run. This value matches the branch or tag name shown on GitHub. For example, // feature-branch-1. For pull requests, the format is /merge. @@ -337,7 +338,7 @@ const LAZY_ENV = () => { // The git config user.email used by Socket CLI. SOCKET_CLI_GIT_USER_EMAIL: envAsString(env['SOCKET_CLI_GIT_USER_EMAIL']) || - `github-actions[bot]@users.noreply.github.com`, + 'github-actions[bot]@users.noreply.github.com', // The git config user.name used by Socket CLI. SOCKET_CLI_GIT_USER_NAME: envAsString(env['SOCKET_CLI_GIT_USER_NAME']) ||