From cad1f8b2a7ecd5a7b65f0a759f4f60a4cb032edf Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 6 Oct 2026 11:13:42 +0200 Subject: [PATCH 1/2] chore(release): land 1.5.0 on v1.x 1.5.0 is tagged and published, but the publish run could not move v1.x to its bump commit after branch protection landed. This applies the same version and changelog change so v1.x matches what shipped. --- CHANGELOG.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7ee785a8..79ef8107d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/package.json b/package.json index 40e301ce2..1010203a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.4.2", + "version": "1.5.0", "description": "CLI for Socket.dev", "contentPolicy": { "class": "dual-use" From 4e711525c598284ca9b477320cfa5ce25bd64839 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 6 Oct 2026 11:13:50 +0200 Subject: [PATCH 2/2] fix(release): anchor the changelog to squash-landed releases v1.x only accepts squash merges, so a release bump landed through a PR gets a new SHA and its tag stays on a commit outside the release line. That is already true for 1.4.2 and 1.5.0. Without this, the next bump anchors to v1.4.1, re-counts their commits, and derives a minor. Tagged versions that are not reachable now resolve to the first-parent commit that set package.json to that version, and the changelog range starts there. --- scripts/release/bump.mts | 2 +- scripts/release/history.mts | 65 ++++++++++++++++++++++++--- test/release-history.test.mts | 83 ++++++++++++++++++++++++++++++++--- 3 files changed, 138 insertions(+), 12 deletions(-) diff --git a/scripts/release/bump.mts b/scripts/release/bump.mts index 42efd0801..05a400340 100644 --- a/scripts/release/bump.mts +++ b/scripts/release/bump.mts @@ -134,7 +134,7 @@ async function main(): Promise { `${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, diff --git a/scripts/release/history.mts b/scripts/release/history.mts index 64424b4dd..cc0fed0e7 100644 --- a/scripts/release/history.mts +++ b/scripts/release/history.mts @@ -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[] } @@ -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> { + const landed = new Map() + 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, @@ -39,13 +86,21 @@ 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, } @@ -53,9 +108,9 @@ export async function readReleaseHistory( export async function readReleaseCommits( cwd: string, - anchorTag: string | undefined, + anchorRef: string | undefined, ): Promise { - const range = anchorTag ? `${anchorTag}..HEAD` : 'HEAD' + const range = anchorRef ? `${anchorRef}..HEAD` : 'HEAD' return await readReleaseGit(cwd, [ 'log', range, diff --git a/test/release-history.test.mts b/test/release-history.test.mts index b2b2c3261..d91e6fbb5 100644 --- a/test/release-history.test.mts +++ b/test/release-history.test.mts @@ -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(() => { @@ -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( @@ -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({ @@ -122,6 +146,53 @@ 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') @@ -129,13 +200,13 @@ describe('release history and version reservations', () => { 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]) })