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
4 changes: 3 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,16 @@
"test": "run -p lint test:unit test:types"
},
"dependencies": {
"@conventional-changelog/git-client": "^3.0.1",
"@conventional-changelog/git-client": "^3.1.0",
"@conventional-changelog/template": "^1.2.0",
"@simple-libs/child-process-utils": "^2.0.0",
"@simple-libs/hosted-git-info": "^2.0.0",
"@simple-libs/stream-utils": "^2.0.0",
"conventional-changelog": "^8.0.0",
"conventional-changelog-conventionalcommits": "^10.2.0",
"conventional-changelog-preset-loader": "^6.0.0",
"conventional-commits-filter": "^6.0.1",
"conventional-commits-parser": "^7.0.1",
"conventional-recommended-bump": "^12.0.0",
"semver": "^7.5.2"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/project/monorepo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,8 @@ export abstract class MonorepoProject extends Project {
}[] = []
const baseVersion = await this.manifest.getVersion()
const tagPrefix = await this.getTagPrefix('')
const firstRelease = !await this.gitClient.getLastSemverTag({
prefix: tagPrefix
const firstRelease = !await this.getLastReleaseTag({
tagPrefix
})
let hasBump = false
let fixedVersion: string | undefined
Expand Down
49 changes: 34 additions & 15 deletions packages/core/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,17 +176,31 @@ export abstract class Project {
}

/**
* Get maintenance branch refs to create after a major version release.
* @param options - The options to use for detecting tags and formatting branches.
* @returns Maintenance branch refs.
* Get the last release tag for the project.
* @param options - The options to use for getting the tag.
* @returns The last release tag, `null` if not found.
*/
async getMaintenanceBranches(options: ProjectReleaseOptions = {}): Promise<ProjectMaintenanceBranch[]> {
async getLastReleaseTag(options: ProjectReleaseOptions = {}): Promise<string | null> {
const { tagPrefix } = options
const {
gitClient,
manifest
} = this

return await gitClient.getLastSemverTag({
path: manifest.projectPath,
prefix: tagPrefix
})
}

/**
* Get maintenance branch refs to create after a major version release.
* @param options - The options to use for detecting tags and formatting branches.
* @returns Maintenance branch refs.
*/
async getMaintenanceBranches(options: ProjectReleaseOptions = {}): Promise<ProjectMaintenanceBranch[]> {
const { manifest } = this
const { tagPrefix = 'v' } = options
const { projectPath } = manifest
const version = await manifest.getVersion()
const currentVersion = semver.valid(version)

Expand All @@ -195,9 +209,8 @@ export abstract class Project {
}

const [release] = await this.getReleaseData(options)
const previousTag = release?.previousTag || await gitClient.getLastSemverTag({
path: projectPath,
prefix: tagPrefix
const previousTag = release?.previousTag || await this.getLastReleaseTag({
tagPrefix
})

if (previousTag) {
Expand Down Expand Up @@ -252,11 +265,12 @@ export abstract class Project {
return forcedVersion
}

const lastReleaseTag = await this.getLastReleaseTag({
tagPrefix
})

if (typeof firstRelease === 'undefined') {
firstRelease = !await gitClient.getLastSemverTag({
path: projectPath,
prefix: tagPrefix
})
firstRelease = !lastReleaseTag
}

const version = baseVersion || await manifest.getVersion()
Expand All @@ -275,7 +289,7 @@ export abstract class Project {
.commits({
path: projectPath
})
.tag({
.tag(lastReleaseTag || {
prefix: tagPrefix
})
.bump()
Expand Down Expand Up @@ -347,17 +361,22 @@ export abstract class Project {
}

if (!skipChangelog) {
const lastReleaseTag = await this.getLastReleaseTag({
tagPrefix
}) || undefined
const notes = new ConventionalChangelog(gitClient)
.loadPreset(preset, _ => import(_))
.commits({
path: projectPath
path: projectPath,
from: lastReleaseTag
})
.tags({
prefix: tagPrefix
})
.readRepository()
.context({
version: nextVersion
version: nextVersion,
previousTag: lastReleaseTag
})
.writer({
preamblePartial
Expand Down
114 changes: 114 additions & 0 deletions packages/node-gha/src/project.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { join } from 'path'
import fs from 'fs/promises'
import {
describe,
expect,
it
} from 'vitest'
import {
createDirectory,
dummyCommit,
forkProject,
packageJsonProject
} from 'test'
import { NodeGhaProject } from './project.js'

describe('node-gha', () => {
describe('project', () => {
describe('getLastReleaseTag', () => {
it('should find last release tag despite bundled release commits', async () => {
const { cwd, run } = await forkProject('node-gha-last-release-tag', packageJsonProject({
name: 'node-gha-last-release-tag-project',
version: '2.0.0',
files: [
'dist'
]
}))
const remote = await createDirectory('node-gha-last-release-tag-remote')
const project = new NodeGhaProject({
path: join(cwd, 'package.json')
})

await run([
({ git }) => git.exec('-C', remote, 'init', '--bare'),
({ git }) => git.exec('remote', 'set-url', 'origin', remote),
({ cwd }) => fs.writeFile(
join(cwd, 'package.json'),
JSON.stringify({
name: 'node-gha-last-release-tag-project',
version: '3.0.0',
files: [
'dist'
]
})
),
({ git }) => git.add('package.json'),
({ git }) => git.commit({
message: 'chore(release): 3.0.0'
}),
({ cwd }) => fs.mkdir(join(cwd, 'dist'), {
recursive: true
}),
({ cwd }) => fs.writeFile(join(cwd, 'dist/index.js'), 'built\n')
])

await project.publish()

await run([ctx => dummyCommit(ctx, 'fix')])

expect(await project.getLastReleaseTag()).toBe('v3.0.0')

await project.bump()

const [versionUpdate] = project.versionUpdates

expect(versionUpdate.to).toBe('3.0.1')
expect(versionUpdate.notes).toContain('commit message for fix')
expect(versionUpdate.notes).not.toContain('commit message for feat')
})

it('should find last release tag for a maintenance branch', async () => {
const { cwd, run } = await forkProject('node-gha-maintenance-tag', packageJsonProject({
name: 'node-gha-maintenance-tag-project',
version: '2.0.0',
files: [
'dist'
]
}))
const remote = await createDirectory('node-gha-maintenance-tag-remote')
const project = new NodeGhaProject({
path: join(cwd, 'package.json')
})

await run([
({ git }) => git.exec('-C', remote, 'init', '--bare'),
({ git }) => git.exec('remote', 'set-url', 'origin', remote),
({ cwd }) => fs.writeFile(
join(cwd, 'package.json'),
JSON.stringify({
name: 'node-gha-maintenance-tag-project',
version: '3.0.0',
files: [
'dist'
]
})
),
({ git }) => git.add('package.json'),
({ git }) => git.commit({
message: 'chore(release): 3.0.0'
}),
({ cwd }) => fs.mkdir(join(cwd, 'dist'), {
recursive: true
}),
({ cwd }) => fs.writeFile(join(cwd, 'dist/index.js'), 'built\n')
])

await project.publish()

await run([({ git }) => git.exec('checkout', '-b', 'v2-maintenance', 'v2.0.0')])

expect(await project.getLastReleaseTag()).toBe('v2.0.0')
})
})
})
})
39 changes: 39 additions & 0 deletions packages/node-gha/src/project.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
type PackageJsonProjectOptions,
type ProjectBumpOptions,
type ProjectReleaseOptions,
PackageJsonProject
} from '@simple-release/core'
import {
Expand All @@ -18,6 +19,44 @@ export type NodeGhaProjectPublishOptions = PublishOptions
* A Node.js GitHub Actions project that publishes version refs to git branches and tags.
*/
export class NodeGhaProject extends PackageJsonProject {
private async isReachableFromHead(ref: string) {
try {
await this.gitClient.exec('merge-base', '--is-ancestor', ref, 'HEAD')
return true
} catch {
return false
}
}

/**
* Get the last release tag for the project.
* GitHub Action release tags point to bundled commits in the release branches
* and are not reachable from the source branch, so all refs are walked
* and tags are matched to the current branch by their parent commits.
* @param options - The options to use for getting the tag.
* @returns The last release tag, `null` if not found.
*/
override async getLastReleaseTag(options: ProjectReleaseOptions = {}) {
const { tagPrefix = 'v' } = options
const tags = this.gitClient.getSemverTags({
prefix: tagPrefix,
all: true
})

for await (const tag of tags) {
// A bundled release commit is amended from the release commit in the source branch,
// so the tag itself or its parent should be reachable from the current branch.
if (
await this.isReachableFromHead(tag)
|| await this.isReachableFromHead(`${tag}^`)
) {
return tag
}
}

return null
}

override async publish(options: NodeGhaProjectPublishOptions = {}): Promise<void> {
if (options.skip) {
options.logger?.info('Skipping publish')
Expand Down
2 changes: 1 addition & 1 deletion packages/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"test": "run -p lint test:types"
},
"dependencies": {
"@conventional-changelog/git-client": "^3.0.1",
"@conventional-changelog/git-client": "^3.1.0",
"@simple-libs/stream-utils": "^2.0.0",
"@types/node": "^22.0.0"
}
Expand Down
Loading