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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]
## [1.5.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.5.0) - 2026-10-06

### Added
- `socket fix --allow-overrides` fixes a vulnerability that a parent package's version range blocks by writing an override or resolution that forces the fixed version under that parent, in npm, pnpm, Yarn Berry and Rush projects.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "socket",
"version": "1.4.2",
"version": "1.5.0",
"description": "CLI for Socket.dev",
"contentPolicy": {
"class": "dual-use"
Expand Down
2 changes: 1 addition & 1 deletion scripts/release/bump.mts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async function main(): Promise<void> {
`${history.tagVersions.length} landed release tag(s); ` +
`${history.reservedVersions.length} reserved tag(s); manifest ${manifestVersion}.`,
)
const commitsRaw = await readReleaseCommits(rootPath, history.anchorTag)
const commitsRaw = await readReleaseCommits(rootPath, history.anchorRef)
const commits = parseConventionalCommits(commitsRaw)
const derived = deriveNextVersion({
commits,
Expand Down
65 changes: 60 additions & 5 deletions scripts/release/history.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import {
const execFile = promisify(execFileCallback)

export interface ReleaseHistory {
readonly anchorTag: string | undefined
// Where the changelog range starts: the newest landed release's tag, or the
// commit that landed it when the tag sits off the release line.
readonly anchorRef: string | undefined
readonly reservedVersions: readonly string[]
readonly tagVersions: readonly string[]
}
Expand All @@ -27,6 +29,51 @@ async function readReleaseGit(
return stdout
}

/**
* Map each tagged version that is not reachable from HEAD to the first-parent
* commit that set package.json to it. A squash merge lands a release bump
* under a new SHA, which leaves the tag on a commit outside the release line.
*/
async function findSquashLandedReleases(
cwd: string,
unreachableTags: readonly string[],
): Promise<Map<string, string>> {
const landed = new Map<string, string>()
const wanted = new Map(
unreachableTags.map(tag => [tag.replace(/^v/, ''), tag]),
)
if (!wanted.size) {
return landed
}
const shas = (
await readReleaseGit(cwd, [
'log',
'--first-parent',
'--format=%H',
'-G',
'"version":',
'HEAD',
'--',
'package.json',
])
)
.split('\n')
.filter(Boolean)
for (const sha of shas) {
// eslint-disable-next-line no-await-in-loop
const manifest = await readReleaseGit(cwd, ['show', `${sha}:package.json`])
const version = (JSON.parse(manifest) as { version?: string }).version
const tag = version ? wanted.get(version) : undefined
if (tag && !landed.has(tag)) {
landed.set(tag, sha)
if (landed.size === wanted.size) {
break
}
}
}
return landed
}

export async function readReleaseHistory(
cwd: string,
manifestVersion: string,
Expand All @@ -39,23 +86,31 @@ export async function readReleaseHistory(
allTags.trim().split('\n'),
manifestVersion,
)
const tagVersions = releaseVersionsForLine(
const reachableTags = releaseVersionsForLine(
landedTags.trim().split('\n'),
manifestVersion,
)
const squashLanded = await findSquashLandedReleases(
cwd,
reservedVersions.filter(tag => !reachableTags.includes(tag)),
)
const tagVersions = [...reachableTags, ...squashLanded.keys()]
const anchorVersion = maxReleaseVersion(tagVersions)
const anchorTag = anchorVersion ? `v${anchorVersion}` : undefined
return {
anchorTag: anchorVersion ? `v${anchorVersion}` : undefined,
anchorRef: anchorTag
? (squashLanded.get(anchorTag) ?? anchorTag)
: undefined,
reservedVersions,
tagVersions,
}
}

export async function readReleaseCommits(
cwd: string,
anchorTag: string | undefined,
anchorRef: string | undefined,
): Promise<string> {
const range = anchorTag ? `${anchorTag}..HEAD` : 'HEAD'
const range = anchorRef ? `${anchorRef}..HEAD` : 'HEAD'
return await readReleaseGit(cwd, [
'log',
range,
Expand Down
83 changes: 77 additions & 6 deletions test/release-history.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,31 @@ function createReleaseRepository() {
subject,
])
}
return { commit, cwd, git }
// A commit whose tree holds a package.json at `version`.
function commitManifest(
subject: string,
version: string,
parent?: string,
): string {
const blob = execFileSync('git', ['hash-object', '-w', '--stdin'], {
cwd,
encoding: 'utf8',
input: `{\n "name": "fixture",\n "version": "${version}"\n}\n`,
}).trim()
const manifestTree = execFileSync('git', ['mktree'], {
cwd,
encoding: 'utf8',
input: `100644 blob ${blob}\tpackage.json\n`,
}).trim()
return git([
'commit-tree',
manifestTree,
...(parent ? ['-p', parent] : []),
'-m',
subject,
])
}
return { commit, commitManifest, cwd, git }
}

afterEach(() => {
Expand All @@ -73,12 +97,12 @@ describe('release history and version reservations', () => {
'1.1.11-prerelease',
)
expect(history).toEqual({
anchorTag: 'v1.1.10',
anchorRef: 'v1.1.10',
reservedVersions: ['v1.1.10', 'v1.1.11', 'v1.1.12'],
tagVersions: ['v1.1.10'],
})
const commits = parseConventionalCommits(
await readReleaseCommits(repository.cwd, history.anchorTag),
await readReleaseCommits(repository.cwd, history.anchorRef),
)
expect(commits.map(commit => commit.hash)).toEqual([head])
expect(
Expand Down Expand Up @@ -110,7 +134,7 @@ describe('release history and version reservations', () => {
'1.1.11-prerelease',
)
const commits = parseConventionalCommits(
await readReleaseCommits(repository.cwd, history.anchorTag),
await readReleaseCommits(repository.cwd, history.anchorRef),
)
expect(
deriveNextVersion({
Expand All @@ -122,20 +146,67 @@ describe('release history and version reservations', () => {
).toMatchObject({ base: '1.1.10', level: 'minor', version: '1.2.2' })
})

it('anchors to the squash commit that landed a release tagged off the line', async () => {
const repository = createReleaseRepository()
const base = repository.commitManifest('chore(release): 1.4.1', '1.4.1')
repository.git(['tag', 'v1.4.1', base])
const fix = repository.commitManifest(
'fix(cli): handle timeouts',
'1.4.1',
base,
)
const bump = repository.commitManifest(
'chore(release): 1.4.2',
'1.4.2',
fix,
)
repository.git(['tag', 'v1.4.2', bump])
const squash = repository.commitManifest(
'chore(release): 1.4.2 (#12)',
'1.4.2',
fix,
)
const head = repository.commitManifest(
'fix(cli): quote paths',
'1.4.2',
squash,
)
repository.git(['update-ref', 'refs/heads/fixture-release', head])
const history = await readReleaseHistory(repository.cwd, '1.4.2')
expect(history).toEqual({
anchorRef: squash,
reservedVersions: ['v1.4.1', 'v1.4.2'],
tagVersions: ['v1.4.1', 'v1.4.2'],
})
const commits = parseConventionalCommits(
await readReleaseCommits(repository.cwd, history.anchorRef),
)
expect(commits.map(commit => commit.hash)).toEqual([head])
expect(
deriveNextVersion({
commits,
manifestVersion: '1.4.2',
publishedVersion: '1.4.2',
reservedVersions: history.reservedVersions,
tagVersions: history.tagVersions,
}),
).toMatchObject({ base: '1.4.2', level: 'patch', version: '1.4.3' })
})

it('uses the complete history for an untagged release line', async () => {
const repository = createReleaseRepository()
const head = repository.commit('feat(cli): add initial command')
repository.git(['update-ref', 'refs/heads/fixture-release', head])
repository.git(['tag', 'v2.0.0', head])
const history = await readReleaseHistory(repository.cwd, '1.0.0-prerelease')
expect(history).toEqual({
anchorTag: undefined,
anchorRef: undefined,
reservedVersions: [],
tagVersions: [],
})
expect(
parseConventionalCommits(
await readReleaseCommits(repository.cwd, history.anchorTag),
await readReleaseCommits(repository.cwd, history.anchorRef),
).map(commit => commit.hash),
).toEqual([head])
})
Expand Down