From a94ec6286791dcbbe5408dfbef71be74e8e1f2c7 Mon Sep 17 00:00:00 2001 From: jdalton Date: Wed, 9 Apr 2025 20:15:06 -0600 Subject: [PATCH] Enable auto-merge and range styles --- .dep-stats.json | 5 ++- package-lock.json | 2 ++ package.json | 2 ++ src/commands/fix/cmd-fix.test.ts | 4 ++- src/commands/fix/cmd-fix.ts | 14 ++++++-- src/commands/fix/npm-fix.ts | 46 ++++++++++++++------------ src/commands/fix/open-pr.ts | 42 +++++++++++++++++++++-- src/commands/fix/pnpm-fix.ts | 45 ++++++++++++++----------- src/commands/fix/run-fix.ts | 53 ++++++++--------------------- src/commands/fix/shared.ts | 57 ++++++++++++++++++++++++++++++++ src/commands/fix/types.ts | 30 +++++++++++++++-- src/utils/arborist-helpers.ts | 8 +++-- 12 files changed, 216 insertions(+), 92 deletions(-) create mode 100644 src/commands/fix/shared.ts diff --git a/.dep-stats.json b/.dep-stats.json index 8a08a71c9..04c0acfb6 100644 --- a/.dep-stats.json +++ b/.dep-stats.json @@ -32,7 +32,10 @@ "yargs-parser": "21.1.1", "yoctocolors-cjs": "2.1.2" }, - "devDependencies": {}, + "devDependencies": { + "@octokit/openapi-types": "^24.2.0", + "@octokit/types": "^13.10.0" + }, "esm": { "@octokit/auth-token": "^5.0.0", "@octokit/core": "^6.1.4", diff --git a/package-lock.json b/package-lock.json index 8830bd984..0a3a601cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,6 +60,8 @@ "@cyclonedx/cdxgen": "^11.2.3", "@eslint/compat": "^1.2.8", "@eslint/js": "^9.24.0", + "@octokit/openapi-types": "^24.2.0", + "@octokit/types": "^13.10.0", "@rollup/plugin-commonjs": "28.0.3", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", diff --git a/package.json b/package.json index 5fcd2c120..c4965b7b6 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,8 @@ "@cyclonedx/cdxgen": "^11.2.3", "@eslint/compat": "^1.2.8", "@eslint/js": "^9.24.0", + "@octokit/openapi-types": "^24.2.0", + "@octokit/types": "^13.10.0", "@rollup/plugin-commonjs": "28.0.3", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", diff --git a/src/commands/fix/cmd-fix.test.ts b/src/commands/fix/cmd-fix.test.ts index 229d68b74..c1861e37b 100644 --- a/src/commands/fix/cmd-fix.test.ts +++ b/src/commands/fix/cmd-fix.test.ts @@ -24,6 +24,8 @@ describe('socket fix', async () => { $ socket fix Options + --autoMerge Enable auto-merge for pull requests that Socket opens. + See GitHub documentation (\\u200bhttps://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository\\u200b) for managing auto-merge for pull requests in your repository. --dryRun Do input validation for a command and exit 0 when input is ok --help Print this help --rangeStyle Define how updated dependency versions should be written in package.json. @@ -34,7 +36,7 @@ describe('socket fix', async () => { *\\x09pin - Use the exact version (e.g. 1.2.3) *\\x09preserve - Retain the existing version range as-is *\\x09tilde - Use ~ range for patch/minor updates (e.g. ~1.2.3) - --test Very the fix by running unit tests + --test Verify the fix by running unit tests --testScript The test script to run for each fix attempt" ` ) diff --git a/src/commands/fix/cmd-fix.ts b/src/commands/fix/cmd-fix.ts index b6bdf7dbb..7260b734f 100644 --- a/src/commands/fix/cmd-fix.ts +++ b/src/commands/fix/cmd-fix.ts @@ -1,10 +1,11 @@ import { stripIndent } from 'common-tags' +import terminalLink from 'terminal-link' import { joinOr } from '@socketsecurity/registry/lib/arrays' import { logger } from '@socketsecurity/registry/lib/logger' import { runFix } from './run-fix' -import { RangeStyles } from './types' +import { RangeStyles } from './shared' import constants from '../../constants' import { commonFlags } from '../../flags' import { handleBadInput } from '../../utils/handle-bad-input' @@ -22,6 +23,14 @@ const config: CliCommandConfig = { hidden: true, flags: { ...commonFlags, + autoMerge: { + type: 'boolean', + default: true, + description: `Enable auto-merge for pull requests that Socket opens.\n See ${terminalLink( + 'GitHub documentation', + 'https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository' + )} for managing auto-merge for pull requests in your repository.` + }, rangeStyle: { type: 'string', default: 'preserve', @@ -39,7 +48,7 @@ const config: CliCommandConfig = { test: { type: 'boolean', default: true, - description: 'Very the fix by running unit tests' + description: 'Verify the fix by running unit tests' }, testScript: { type: 'string', @@ -93,6 +102,7 @@ async function run( const { spinner } = constants await runFix({ + autoMerge: Boolean(cli.flags['autoMerge']), spinner, rangeStyle: (cli.flags['rangeStyle'] ?? undefined) as | RangeStyle diff --git a/src/commands/fix/npm-fix.ts b/src/commands/fix/npm-fix.ts index a16dc24dd..c80ed7861 100644 --- a/src/commands/fix/npm-fix.ts +++ b/src/commands/fix/npm-fix.ts @@ -6,7 +6,8 @@ import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { openGitHubPullRequest } from './open-pr' +import { enableAutoMerge, openGitHubPullRequest } from './open-pr' +import { NormalizedFixOptions } from './types' import constants from '../../constants' import { Arborist, @@ -22,11 +23,9 @@ import { } from '../../utils/arborist-helpers' import { getCveInfoByAlertsMap } from '../../utils/socket-package-alert' -import type { RangeStyle } from './types' import type { SafeNode } from '../../shadow/npm/arborist/lib/node' import type { EnvDetails } from '../../utils/package-environment' import type { PackageJson } from '@socketsecurity/registry/lib/packages' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' const { CI, NPM } = constants @@ -47,25 +46,17 @@ async function install( await arb2.reify() } -type NpmFixOptions = { - cwd?: string | undefined - rangeStyle?: RangeStyle | undefined - spinner?: Spinner | undefined - test?: boolean | undefined - testScript?: string | undefined -} - export async function npmFix( _pkgEnvDetails: EnvDetails, - options?: NpmFixOptions | undefined -) { - const { - cwd = process.cwd(), + { + autoMerge, + cwd, + rangeStyle, spinner, - test = false, - testScript = 'test' - } = { __proto__: null, ...options } as NpmFixOptions - + test, + testScript + }: NormalizedFixOptions +) { spinner?.start() const arb = new SafeArborist({ @@ -158,7 +149,12 @@ export async function npmFix( let saved = false let installed = false try { - updatePackageJsonFromNode(editablePkgJson, arb.idealTree!, node) + updatePackageJsonFromNode( + editablePkgJson, + arb.idealTree!, + node, + rangeStyle + ) // eslint-disable-next-line no-await-in-loop await editablePkgJson.save() saved = true @@ -177,7 +173,15 @@ export async function npmFix( // Lazily access constants.ENV[CI]. if (constants.ENV[CI]) { // eslint-disable-next-line no-await-in-loop - await openGitHubPullRequest(name, targetVersion, cwd) + const prResponse = await openGitHubPullRequest( + name, + targetVersion, + cwd + ) + if (autoMerge) { + // eslint-disable-next-line no-await-in-loop + await enableAutoMerge(prResponse.data) + } } } catch { spinner?.error(`Reverting ${fixSpec}`) diff --git a/src/commands/fix/open-pr.ts b/src/commands/fix/open-pr.ts index 94830f617..f10e47960 100644 --- a/src/commands/fix/open-pr.ts +++ b/src/commands/fix/open-pr.ts @@ -5,6 +5,11 @@ import { spawn } from '@socketsecurity/registry/lib/spawn' import constants from '../../constants' +import type { components } from '@octokit/openapi-types' +import type { OctokitResponse } from '@octokit/types' + +type PullsCreateResponseData = components['schemas']['pull-request'] + const { GITHUB_ACTIONS, GITHUB_REF_NAME, @@ -79,11 +84,44 @@ function getOctokit() { return _octokit } +export async function enableAutoMerge( + prResponseData: PullsCreateResponseData +): Promise { + const octokit = getOctokit() + const { node_id: prId, number: prNumber } = prResponseData + + try { + await octokit.graphql( + ` + mutation EnableAutoMerge($pullRequestId: ID!) { + enablePullRequestAutoMerge(input: { + pullRequestId: $pullRequestId, + mergeMethod: SQUASH + }) { + pullRequest { + number + autoMergeRequest { + enabledAt + } + } + } + } + `, + { + pullRequestId: prId + } + ) + logger.info(`Auto-merge enabled for PR #${prNumber}`) + } catch (e) { + logger.error(`Failed to enable auto-merge for PR #${prNumber}:`, e) + } +} + export async function openGitHubPullRequest( name: string, targetVersion: string, cwd = process.cwd() -) { +): Promise> { // Lazily access constants.ENV[GITHUB_ACTIONS]. if (constants.ENV[GITHUB_ACTIONS]) { // Lazily access constants.ENV[SOCKET_SECURITY_GITHUB_PAT]. @@ -116,7 +154,7 @@ export async function openGitHubPullRequest( await spawn('git', ['push', '--set-upstream', 'origin', branch], { cwd }) } const octokit = getOctokit() - await octokit.pulls.create({ + return await octokit.pulls.create({ owner, repo, title: commitMsg, diff --git a/src/commands/fix/pnpm-fix.ts b/src/commands/fix/pnpm-fix.ts index 1454e089d..ec8a825b6 100644 --- a/src/commands/fix/pnpm-fix.ts +++ b/src/commands/fix/pnpm-fix.ts @@ -8,7 +8,7 @@ import { readPackageJson } from '@socketsecurity/registry/lib/packages' -import { openGitHubPullRequest } from './open-pr' +import { enableAutoMerge, openGitHubPullRequest } from './open-pr' import constants from '../../constants' import { SAFE_ARBORIST_REIFY_OPTIONS_OVERRIDES, @@ -24,7 +24,7 @@ import { getAlertsMapFromPnpmLockfile } from '../../utils/pnpm-lock-yaml' import { getCveInfoByAlertsMap } from '../../utils/socket-package-alert' import { runAgentInstall } from '../optimize/run-agent' -import type { RangeStyle } from './types' +import type { NormalizedFixOptions } from './types' import type { StringKeyValueObject } from '../../types' import type { EnvDetails } from '../../utils/package-environment' import type { PackageJson } from '@socketsecurity/registry/lib/packages' @@ -48,25 +48,17 @@ async function install( }) } -type PnpmFixOptions = { - cwd?: string | undefined - rangeStyle?: RangeStyle | undefined - spinner?: Spinner | undefined - test?: boolean | undefined - testScript?: string | undefined -} - export async function pnpmFix( pkgEnvDetails: EnvDetails, - options?: PnpmFixOptions -) { - const { - cwd = process.cwd(), + { + autoMerge, + cwd, + rangeStyle, spinner, - test = false, - testScript = 'test' - } = { __proto__: null, ...options } as PnpmFixOptions - + test, + testScript + }: NormalizedFixOptions +) { const lockfile = await readWantedLockfile(cwd, { ignoreIncompatible: false }) if (!lockfile) { return @@ -185,7 +177,12 @@ export async function pnpmFix( let installed = false try { editablePkgJson.update(updateData) - updatePackageJsonFromNode(editablePkgJson, arb.actualTree!, node) + updatePackageJsonFromNode( + editablePkgJson, + arb.actualTree!, + node, + rangeStyle + ) // eslint-disable-next-line no-await-in-loop await editablePkgJson.save() saved = true @@ -206,7 +203,15 @@ export async function pnpmFix( // Lazily access constants.ENV[CI]. if (constants.ENV[CI]) { // eslint-disable-next-line no-await-in-loop - await openGitHubPullRequest(name, targetVersion, cwd) + const prResponse = await openGitHubPullRequest( + name, + targetVersion, + cwd + ) + if (autoMerge) { + // eslint-disable-next-line no-await-in-loop + await enableAutoMerge(prResponse.data) + } } } catch { spinner?.error(`Reverting ${fixSpec}`) diff --git a/src/commands/fix/run-fix.ts b/src/commands/fix/run-fix.ts index 2b0e13178..61f674fda 100644 --- a/src/commands/fix/run-fix.ts +++ b/src/commands/fix/run-fix.ts @@ -2,60 +2,33 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { npmFix } from './npm-fix' import { pnpmFix } from './pnpm-fix' +import { assignDefaultFixOptions } from './shared' import constants from '../../constants' import { detectAndValidatePackageEnvironment } from '../../utils/package-environment' -import type { RangeStyle } from './types' -import type { Spinner } from '@socketsecurity/registry/lib/spinner' +import type { FixOptions } from './types' const { NPM, PNPM } = constants const CMD_NAME = 'socket fix' -type RunFixOptions = { - cwd?: string | undefined - rangeStyle?: RangeStyle | undefined - spinner?: Spinner | undefined - test?: boolean | undefined - testScript?: string | undefined -} - -export async function runFix({ - cwd = process.cwd(), - rangeStyle, - spinner, - test = false, - testScript = 'test' -}: RunFixOptions) { - const pkgEnvDetails = await detectAndValidatePackageEnvironment(cwd, { +export async function runFix(options_: FixOptions) { + const options = assignDefaultFixOptions({ + __proto__: null, + ...options_ + } as FixOptions) + const pkgEnvDetails = await detectAndValidatePackageEnvironment(options.cwd, { cmdName: CMD_NAME, logger }) if (!pkgEnvDetails) { - spinner?.stop() return } logger.info(`Fixing packages for ${pkgEnvDetails.agent}`) - switch (pkgEnvDetails.agent) { - case NPM: { - await npmFix(pkgEnvDetails, { - rangeStyle, - spinner, - test, - testScript - }) - break - } - case PNPM: { - await pnpmFix(pkgEnvDetails, { - rangeStyle, - spinner, - test, - testScript - }) - break - } + const { agent } = pkgEnvDetails + if (agent === NPM) { + await npmFix(pkgEnvDetails, options) + } else if (agent === PNPM) { + await pnpmFix(pkgEnvDetails, options) } - spinner?.stop() - // spinner.successAndStop('Socket.dev fix successful') } diff --git a/src/commands/fix/shared.ts b/src/commands/fix/shared.ts new file mode 100644 index 000000000..2e12f3460 --- /dev/null +++ b/src/commands/fix/shared.ts @@ -0,0 +1,57 @@ +import semver from 'semver' + +import type { FixOptions, NormalizedFixOptions, RangeStyle } from './types' + +export function assignDefaultFixOptions( + options: FixOptions +): NormalizedFixOptions { + if (options.autoMerge === undefined) { + options.autoMerge = false + } + if (options.cwd === undefined) { + options.cwd = process.cwd() + } + if (options.rangeStyle === undefined) { + options.rangeStyle = 'preserve' + } + if (options.test === undefined) { + options.test = !!options.testScript + } + if (options.testScript === undefined) { + options.testScript = 'test' + } + return options as NormalizedFixOptions +} + +export function applyRange( + refRange: string, + version: string, + style: RangeStyle = 'preserve' +): string { + switch (style) { + case 'caret': + return `^${version}` + case 'gt': + return `>${version}` + case 'gte': + return `>=${version}` + case 'lt': + return `<${version}` + case 'lte': + return `<=${version}` + case 'preserve': { + const comparators = [...new semver.Range(refRange).set].flat() + const { length } = comparators + return !length || length > 1 + ? version + : `${comparators[0]!.operator}${version}` + } + case 'tilde': + return `~${version}` + case 'pin': + default: + return version + } +} + +export const RangeStyles = ['caret', 'gt', 'lt', 'pin', 'preserve', 'tilde'] diff --git a/src/commands/fix/types.ts b/src/commands/fix/types.ts index f75819c8d..1926d4015 100644 --- a/src/commands/fix/types.ts +++ b/src/commands/fix/types.ts @@ -1,3 +1,29 @@ -export type RangeStyle = 'caret' | 'gt' | 'lt' | 'pin' | 'preserve' | 'tilde' +import type { Spinner } from '@socketsecurity/registry/lib/spinner' -export const RangeStyles = ['caret', 'gt', 'lt', 'pin', 'preserve', 'tilde'] +type StripUndefined = { + [K in keyof T]-?: Exclude +} + +export type RangeStyle = + | 'caret' + | 'gt' + | 'gte' + | 'lt' + | 'lte' + | 'pin' + | 'preserve' + | 'tilde' + +export type FixOptions = { + autoMerge?: boolean | undefined + cwd?: string | undefined + rangeStyle?: RangeStyle | undefined + spinner?: Spinner | undefined + test?: boolean | undefined + testScript?: string | undefined +} + +export type NormalizedFixOptions = StripUndefined< + Required> +> & + Pick diff --git a/src/utils/arborist-helpers.ts b/src/utils/arborist-helpers.ts index 80b48da7d..8bce79f77 100644 --- a/src/utils/arborist-helpers.ts +++ b/src/utils/arborist-helpers.ts @@ -17,8 +17,10 @@ import { Edge } from '../shadow/npm/arborist/lib/edge' import { getPublicToken, setupSdk } from '../utils/sdk' import { CompactSocketArtifact } from './alert/artifact' import { addArtifactToAlertsMap } from './socket-package-alert' +import { applyRange } from '../commands/fix/shared' import type { AlertIncludeFilter, AlertsByPkgId } from './socket-package-alert' +import type { RangeStyle } from '../commands/fix/types' import type { Diff } from '../shadow/npm/arborist/lib/arborist/types' import type { SafeEdge } from '../shadow/npm/arborist/lib/edge' import type { SafeNode } from '../shadow/npm/arborist/lib/node' @@ -404,7 +406,8 @@ export function updateNode( export function updatePackageJsonFromNode( editablePkgJson: EditablePackageJson, tree: SafeNode, - node: SafeNode + node: SafeNode, + rangeStyle?: RangeStyle | undefined ) { if (isTopLevel(tree, node)) { const { name, version } = node @@ -419,11 +422,10 @@ export function updatePackageJsonFromNode( if (oldValue) { const oldVersion = oldValue[name] if (oldVersion) { - const rangeDecorator = /^[~^]/.exec(oldVersion)?.[0] ?? '' editablePkgJson.update({ [depField]: { ...oldValue, - [name]: `${rangeDecorator}${version}` + [name]: applyRange(oldVersion, version, rangeStyle) } }) }