Skip to content

Commit ec55f6d

Browse files
committed
Add pr creation to npm fix as well
1 parent 11d1447 commit ec55f6d

File tree

9 files changed

+333
-283
lines changed

9 files changed

+333
-283
lines changed

src/commands/fix/npm-fix.ts

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { getManifestData } from '@socketsecurity/registry'
2+
import { logger } from '@socketsecurity/registry/lib/logger'
23
import { runScript } from '@socketsecurity/registry/lib/npm'
34
import {
45
fetchPackagePackument,
56
readPackageJson
67
} from '@socketsecurity/registry/lib/packages'
78

9+
import { openGitHubPullRequest } from './open-pr'
810
import constants from '../../constants'
911
import {
1012
Arborist,
@@ -14,18 +16,32 @@ import {
1416
import {
1517
findPackageNodes,
1618
getAlertsMapFromArborist,
17-
updateNode
18-
} from '../../utils/lockfile/package-lock-json'
19+
updateNode,
20+
updatePackageJsonFromNode
21+
} from '../../utils/arborist-helpers'
1922
import { getCveInfoByAlertsMap } from '../../utils/socket-package-alert'
2023

2124
import type { SafeNode } from '../../shadow/npm/arborist/lib/node'
2225
import type { EnvDetails } from '../../utils/package-environment'
2326
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
2427

25-
const { NPM } = constants
28+
const { CI, NPM } = constants
2629

27-
function isTopLevel(tree: SafeNode, node: SafeNode): boolean {
28-
return tree.children.get(node.name) === node
30+
type InstallOptions = {
31+
cwd?: string | undefined
32+
}
33+
34+
async function install(
35+
idealTree: SafeNode,
36+
options: InstallOptions
37+
): Promise<void> {
38+
const { cwd = process.cwd() } = {
39+
__proto__: null,
40+
...options
41+
} as InstallOptions
42+
const arb2 = new Arborist({ path: cwd })
43+
arb2.idealTree = idealTree
44+
await arb2.reify()
2945
}
3046

3147
type NpmFixOptions = {
@@ -78,9 +94,9 @@ export async function npmFix(
7894
for (const { 0: name, 1: infos } of infoByPkg) {
7995
const revertToIdealTree = arb.idealTree!
8096
arb.idealTree = null
97+
8198
// eslint-disable-next-line no-await-in-loop
8299
await arb.buildIdealTree()
83-
84100
const tree = arb.idealTree!
85101

86102
const hasUpgrade = !!getManifestData(NPM, name)
@@ -100,16 +116,14 @@ export async function npmFix(
100116
continue
101117
}
102118

103-
for (let i = 0, { length: nodesLength } = nodes; i < nodesLength; i += 1) {
104-
const node = nodes[i]!
105-
for (
106-
let j = 0, { length: infosLength } = infos;
107-
j < infosLength;
108-
j += 1
109-
) {
110-
const { firstPatchedVersionIdentifier, vulnerableVersionRange } =
111-
infos[j]!
119+
for (const node of nodes) {
120+
for (const {
121+
firstPatchedVersionIdentifier,
122+
vulnerableVersionRange
123+
} of infos) {
124+
spinner?.stop()
112125
const { version: oldVersion } = node
126+
const oldSpec = `${name}@${oldVersion}`
113127
if (
114128
updateNode(
115129
node,
@@ -118,45 +132,42 @@ export async function npmFix(
118132
firstPatchedVersionIdentifier
119133
)
120134
) {
135+
const targetVersion = node.package.version!
136+
const fixSpec = `${name}@^${targetVersion}`
121137
try {
138+
spinner?.start()
139+
spinner?.info(`Installing ${fixSpec}`)
140+
// eslint-disable-next-line no-await-in-loop
141+
await install(arb.idealTree!, { cwd })
122142
if (test) {
143+
spinner?.info(`Testing ${fixSpec}`)
123144
// eslint-disable-next-line no-await-in-loop
124145
await runScript(testScript, [], { spinner, stdio: 'ignore' })
125146
}
126-
127-
spinner?.info(`Patched ${name} ${oldVersion} -> ${node.version}`)
128-
129-
if (isTopLevel(tree, node)) {
130-
for (const depField of [
131-
'dependencies',
132-
'optionalDependencies',
133-
'peerDependencies'
134-
]) {
135-
const { content: pkgJson } = editablePkgJson
136-
const oldVersion = (pkgJson[depField] as any)?.[name]
137-
if (oldVersion) {
138-
const decorator = /^[~^]/.exec(oldVersion)?.[0] ?? ''
139-
;(pkgJson as any)[depField][name] =
140-
`${decorator}${node.version}`
141-
}
142-
}
147+
// Lazily access constants.ENV[CI].
148+
if (constants.ENV[CI]) {
149+
// eslint-disable-next-line no-await-in-loop
150+
await openGitHubPullRequest(name, targetVersion, cwd)
143151
}
152+
updatePackageJsonFromNode(editablePkgJson, tree, node)
144153
// eslint-disable-next-line no-await-in-loop
145154
await editablePkgJson.save()
155+
spinner?.info(`Fixed ${name}`)
146156
} catch {
147-
spinner?.error(`Reverting ${name} to ${oldVersion}`)
157+
spinner?.error(`Reverting ${fixSpec}`)
148158
arb.idealTree = revertToIdealTree
159+
// eslint-disable-next-line no-await-in-loop
160+
await install(arb.idealTree!, { cwd })
161+
spinner?.stop()
162+
logger.error(`Failed to fix ${oldSpec}`)
149163
}
150164
} else {
151-
spinner?.error(`Could not patch ${name} ${oldVersion}`)
165+
spinner?.stop()
166+
logger.error(`Could not patch ${oldSpec}`)
152167
}
153168
}
154169
}
155170
}
156171

157-
const arb2 = new Arborist({ path: cwd })
158-
arb2.idealTree = arb.idealTree
159-
await arb2.reify()
160-
161172
spinner?.stop()
162173
}

src/commands/fix/open-pr.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { Octokit } from '@octokit/rest'
2+
3+
import { logger } from '@socketsecurity/registry/lib/logger'
4+
import { spawn } from '@socketsecurity/registry/lib/spawn'
5+
6+
import constants from '../../constants'
7+
8+
const {
9+
GITHUB_ACTIONS,
10+
GITHUB_REF_NAME,
11+
GITHUB_REPOSITORY,
12+
SOCKET_SECURITY_GITHUB_PAT
13+
} = constants
14+
15+
async function branchExists(
16+
branch: string,
17+
cwd: string | undefined = process.cwd()
18+
): Promise<boolean> {
19+
try {
20+
await spawn(
21+
'git',
22+
['show-ref', '--verify', '--quiet', `refs/heads/${branch}`],
23+
{
24+
cwd,
25+
stdio: 'ignore'
26+
}
27+
)
28+
return true
29+
} catch {}
30+
return false
31+
}
32+
33+
async function checkoutBaseBranchIfAvailable(
34+
baseBranch: string,
35+
cwd: string | undefined = process.cwd()
36+
) {
37+
try {
38+
const currentBranch = (
39+
await spawn('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd })
40+
).stdout.trim()
41+
if (currentBranch === baseBranch) {
42+
logger.info(`Already on ${baseBranch}`)
43+
return
44+
}
45+
logger.info(`Switching branch from ${currentBranch} to ${baseBranch}...`)
46+
await spawn('git', ['checkout', baseBranch], { cwd })
47+
logger.info(`Checked out ${baseBranch}`)
48+
} catch {
49+
logger.warn(`Could not switch to ${baseBranch}. Proceeding with HEAD.`)
50+
}
51+
}
52+
53+
type GitHubRepoInfo = {
54+
owner: string
55+
repo: string
56+
}
57+
58+
function getGitHubRepoInfo(): GitHubRepoInfo {
59+
// Lazily access constants.ENV[GITHUB_REPOSITORY].
60+
const ownerSlashRepo = constants.ENV[GITHUB_REPOSITORY]
61+
const slashIndex = ownerSlashRepo.indexOf('/')
62+
if (slashIndex === -1) {
63+
throw new Error('GITHUB_REPOSITORY environment variable not set')
64+
}
65+
return {
66+
owner: ownerSlashRepo.slice(0, slashIndex),
67+
repo: ownerSlashRepo.slice(slashIndex + 1)
68+
}
69+
}
70+
71+
let _octokit: Octokit | undefined
72+
function getOctokit() {
73+
if (_octokit === undefined) {
74+
_octokit = new Octokit({
75+
// Lazily access constants.ENV[SOCKET_SECURITY_GITHUB_PAT].
76+
auth: constants.ENV[SOCKET_SECURITY_GITHUB_PAT]
77+
})
78+
}
79+
return _octokit
80+
}
81+
82+
export async function openGitHubPullRequest(
83+
name: string,
84+
targetVersion: string,
85+
cwd = process.cwd()
86+
) {
87+
// Lazily access constants.ENV[GITHUB_ACTIONS].
88+
if (constants.ENV[GITHUB_ACTIONS]) {
89+
// Lazily access constants.ENV[SOCKET_SECURITY_GITHUB_PAT].
90+
const pat = constants.ENV[SOCKET_SECURITY_GITHUB_PAT]
91+
if (!pat) {
92+
throw new Error('Missing SOCKET_SECURITY_GITHUB_PAT environment variable')
93+
}
94+
const baseBranch =
95+
// Lazily access constants.ENV[GITHUB_REF_NAME].
96+
constants.ENV[GITHUB_REF_NAME] ??
97+
// GitHub defaults to branch name "main"
98+
// 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
99+
'main'
100+
const branch = `socket-fix-${name}-${targetVersion.replace(/\./g, '-')}`
101+
const commitMsg = `chore: upgrade ${name} to ${targetVersion}`
102+
const { owner, repo } = getGitHubRepoInfo()
103+
const url = `https://x-access-token:${pat}@github.com/${owner}/${repo}`
104+
105+
await spawn('git', ['remote', 'set-url', 'origin', url], {
106+
cwd
107+
})
108+
109+
if (await branchExists(branch, cwd)) {
110+
logger.warn(`Branch "${branch}" already exists. Skipping creation.`)
111+
} else {
112+
await checkoutBaseBranchIfAvailable(baseBranch, cwd)
113+
await spawn('git', ['checkout', '-b', branch], { cwd })
114+
await spawn('git', ['add', 'package.json', 'pnpm-lock.yaml'], { cwd })
115+
await spawn('git', ['commit', '-m', commitMsg], { cwd })
116+
await spawn('git', ['push', '--set-upstream', 'origin', branch], { cwd })
117+
}
118+
const octokit = getOctokit()
119+
await octokit.pulls.create({
120+
owner,
121+
repo,
122+
title: commitMsg,
123+
head: branch,
124+
base: baseBranch,
125+
body: `[socket] Upgrade \`${name}\` to ${targetVersion}`
126+
})
127+
} else {
128+
throw new Error(
129+
'Unsupported CI platform or missing GITHUB_ACTIONS environment variable'
130+
)
131+
}
132+
}

0 commit comments

Comments
 (0)