Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
18 changes: 18 additions & 0 deletions .changeset/changesets-v3-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"changesets-gitlab": major
---

Drop support for Changesets v2 and Node < 22. Bump all `@changesets/*` dependencies to v3 and `@manypkg/get-packages` to v3.

Breaking changes:

- **Node engine requirement** bumped from `>=18.0.0` to `^22.11 || ^24 || >=26` to match Changesets v3
- **`@changesets/*` dependencies** bumped to v3 versions, which are ESM-only
- **`@manypkg/get-packages`** bumped to v3, changing the `Packages` and `Package` types (`tool` is now an object with `type` property, `root` renamed to `rootPackage`, `Package` now requires `relativeDir`)

Bug fixes for Changesets v3 compatibility:

- Handle the new `changeset version` exit code 1 when no unreleased changesets exist
- Detect published packages via the `CHANGESETS_OUTPUT` env var (NDJSON format), falling back to stdout "New tag:" parsing for Changesets v2
- Prevent creating empty release MRs when the version command produces no file changes — fall through to publish instead
- Use `ignoreReturnCode: true` on version and publish commands since v3 may exit non-zero in valid scenarios
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"license": "MIT",
"packageManager": "yarn@4.18.0",
"engines": {
"node": ">=18.0.0"
"node": "^22.11 || ^24 || >=26"
},
"bin": "lib/cli.js",
"main": "./lib/index.cjs",
Expand Down Expand Up @@ -64,14 +64,14 @@
"dependencies": {
"@actions/core": "^2.0.3",
"@actions/exec": "^1.1.1",
"@changesets/assemble-release-plan": "^6.0.6",
"@changesets/config": "^3.1.1",
"@changesets/errors": "^0.2.0",
"@changesets/parse": "^0.4.1",
"@changesets/pre": "^2.0.2",
"@changesets/read": "^0.6.3",
"@changesets/assemble-release-plan": "^7.0.0",
"@changesets/config": "^4.0.0",
"@changesets/errors": "^1.0.0",
"@changesets/parse": "^1.0.0",
"@changesets/pre": "^3.0.0",
"@changesets/read": "^1.0.0",
"@gitbeaker/rest": "^42.2.0",
"@manypkg/get-packages": "^1.1.3",
"@manypkg/get-packages": "^3.1.0",
"commander": "^13.1.0",
"dotenv": "^16.5.0",
"global-agent": "^3.0.0",
Expand All @@ -89,8 +89,8 @@
},
"devDependencies": {
"@1stg/common-config": "^13.0.1",
"@changesets/changelog-github": "^0.5.1",
"@changesets/cli": "^2.29.0",
"@changesets/changelog-github": "^1.0.0",
"@changesets/cli": "^3.0.0",
"@commitlint/cli": "^19.8.0",
"@pkgr/rollup": "^6.0.3",
"@types/global-agent": "^3.0.0",
Expand Down
4 changes: 2 additions & 2 deletions src/comment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ValidationError } from '@changesets/errors'
import { ExitError } from '@changesets/errors'
import type {
ComprehensiveRelease,
ReleasePlan,
Expand Down Expand Up @@ -302,7 +302,7 @@ export const comment = async () => {
changedFiles: packageChangedFiles,
cwdPrefix,
}).catch((err: unknown) => {
if (err instanceof ValidationError) {
if (err instanceof ExitError) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
errFromFetchingChangedFiles = `<details><summary>💥 An error occurred when fetching the changed packages and changesets in this MR</summary>\n\n\`\`\`\n${err.message}\n\`\`\`\n\n</details>\n`
} else {
console.error(err)
Expand Down
49 changes: 30 additions & 19 deletions src/get-changed-packages.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import fs from 'node:fs/promises'
import path from 'node:path'

import assembleReleasePlan from '@changesets/assemble-release-plan'
import { parse as parseConfig } from '@changesets/config'
import parseChangeset from '@changesets/parse'
import { assembleReleasePlan } from '@changesets/assemble-release-plan'
import { validateConfig as parseConfig } from '@changesets/config'
Comment thread
philibea marked this conversation as resolved.
import { parseChangesetFile as parseChangeset } from '@changesets/parse'
import type {
PackageJSON,
PreState,
NewChangeset,
WrittenConfig,
} from '@changesets/types'
import type { Packages, Tool } from '@manypkg/get-packages'
import type { Package, Packages, Tool } from '@manypkg/get-packages'
import micromatch from 'micromatch'
import { parse } from 'yaml'

Expand Down Expand Up @@ -50,13 +50,14 @@ export const getChangedPackages = async ({
}
}

async function getPackage(pkgPath: string) {
async function getPackage(pkgPath: string): Promise<Package> {
const jsonContent = await fetchJsonFile<PackageJSON>(
`${pkgPath}/package.json`,
)
return {
packageJson: jsonContent,
dir: pkgPath,
relativeDir: pkgPath,
}
}

Expand Down Expand Up @@ -109,11 +110,11 @@ export const getChangedPackages = async ({
)
}
}
let tool: { tool: Tool; globs: string[] } | undefined
let tool: { toolType: string; globs: string[] } | undefined

if (isPnpm) {
tool = {
tool: 'pnpm',
toolType: 'pnpm',
globs: (
parse(await fetchTextFile('pnpm-workspace.yaml')) as {
packages: string[]
Expand All @@ -125,27 +126,31 @@ export const getChangedPackages = async ({

if (rootPackageJsonContent.workspaces) {
tool = {
tool: 'yarn',
toolType: 'yarn',
globs: Array.isArray(rootPackageJsonContent.workspaces)
? rootPackageJsonContent.workspaces
: rootPackageJsonContent.workspaces.packages,
}
} else if (rootPackageJsonContent.bolt?.workspaces) {
tool = {
tool: 'bolt',
toolType: 'bolt',
globs: rootPackageJsonContent.bolt.workspaces,
}
}
}

const rootPackageJsonContent = await rootPackageJsonContentsPromise

const rootPackage: Package = {
dir: '/',
relativeDir: '.',
packageJson: rootPackageJsonContent,
}

const packages: Packages = {
root: {
dir: '/',
packageJson: rootPackageJsonContent,
},
tool: tool ? tool.tool : 'root',
rootDir: '/',
rootPackage,
tool: { type: tool ? tool.toolType : 'root' } as Tool,
packages: [],
}

Expand All @@ -160,16 +165,22 @@ export const getChangedPackages = async ({
const matches = micromatch(potentialWorkspaceDirectories, tool.globs)
packages.packages = await Promise.all(matches.map(dir => getPackage(dir)))
} else {
packages.packages.push(packages.root)
packages.packages.push(rootPackage)
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- https://github.com/microsoft/TypeScript/issues/9998
if (hasErrored) {
throw new Error('an error occurred when fetching files')
}

const config = await configPromise.then(rawConfig =>
parseConfig(rawConfig, packages),
)
const config = await configPromise.then(rawConfig => {
const result = parseConfig(rawConfig, packages)
if (result.errors) {
throw new Error(
`Failed to parse changeset config: ${result.errors.join(', ')}`,
)
}
return result.config
})

const releasePlan = assembleReleasePlan(
await Promise.all(changesetPromises),
Expand All @@ -179,7 +190,7 @@ export const getChangedPackages = async ({
)

return {
changedPackages: (packages.tool === 'root'
changedPackages: (packages.tool.type === 'root'
? packages.packages
: packages.packages.filter(pkg =>
changedFiles.some(
Expand Down
8 changes: 6 additions & 2 deletions src/git-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ export const push = async (
}

export const pushTags = async () => {
await exec('git', ['push', 'origin', '--tags'])
await exec('git', ['push', 'origin', '--tags'], {
ignoreReturnCode: true,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})
}

export const pushTag = async (tag: string) => {
await exec('git', ['push', 'origin', tag])
await exec('git', ['push', 'origin', tag], {
ignoreReturnCode: true,
})
}

export const switchToMaybeExistingBranch = async (branch: string) => {
Expand Down
153 changes: 94 additions & 59 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
export const main = async ({
published,
onlyChangesets,
// eslint-disable-next-line sonarjs/cognitive-complexity
}: MainCommandOptions = {}) => {
const { GITLAB_TOKEN, NPM_TOKEN } = env

Expand Down Expand Up @@ -67,69 +66,17 @@ export const main = async ({
return
}
case !hasChangesets && hasPublishScript: {
console.log(
'No changesets found, attempting to publish any unpublished packages to npm',
)

if (NPM_TOKEN) {
const userNpmrcPath = `${env.HOME}/.npmrc`
if (await fileExists(userNpmrcPath)) {
console.info('Found existing user .npmrc file')
const userNpmrcContent = await fs.readFile(userNpmrcPath, 'utf8')
const authLine = userNpmrcContent.split('\n').find(line => {
// check based on https://github.com/npm/cli/blob/8f8f71e4dd5ee66b3b17888faad5a7bf6c657eed/test/lib/adduser.js#L103-L105
return /^\s*\/\/registry\.npmjs\.org\/:[_-]authToken=/i.test(line)
})
if (authLine) {
console.info(
'Found existing auth token for the npm registry in the user .npmrc file',
)
} else {
console.info(
"Didn't find existing auth token for the npm registry in the user .npmrc file, creating one",
)
await fs.appendFile(
userNpmrcPath,
`\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n`,
)
}
} else {
console.info(
'No user .npmrc file found, creating one with NPM_TOKEN used as auth token',
)
await fs.writeFile(
userNpmrcPath,
`//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n`,
)
}
} else {
console.info(
'No NPM_TOKEN found - assuming trusted publishing or npm is already authenticated',
)
}

const result = await runPublish({
script: publishScript,
gitlabToken: GITLAB_TOKEN,
createGitlabReleases: !FALSY_VALUES.has(
getInput('create_gitlab_releases'),
),
await runPublishFlow({
publishScript,
published,
cwd,
GITLAB_TOKEN,
NPM_TOKEN,
})

if (result.published) {
setOutput('published', true)
setOutput('publishedPackages', result.publishedPackages)
exportVariable('PUBLISHED', true)
exportVariable('PUBLISHED_PACKAGES', result.publishedPackages)
if (published) {
execSync(published)
}
}
return
}
case hasChangesets: {
await runVersion({
const result = await runVersion({
script: getOptionalInput('version'),
gitlabToken: GITLAB_TOKEN,
mrTitle: getOptionalInput('title'),
Expand All @@ -142,6 +89,94 @@ export const main = async ({
if (onlyChangesets) {
execSync(onlyChangesets)
}
// If the version command produced no file changes (e.g. in Changesets v3
// when there are no unreleased changesets, or all packages are already
// at the target version), fall through to the publish flow instead of
// leaving the packages unpublished.
if (!result.hasChanges && hasPublishScript) {
console.log(
'Version command produced no changes, attempting to publish any unpublished packages to npm',
)
await runPublishFlow({
publishScript,
published,
cwd,
GITLAB_TOKEN,
NPM_TOKEN,
})
}
}
}
}

async function runPublishFlow({
publishScript,
published,
cwd,
GITLAB_TOKEN,
NPM_TOKEN,
}: {
publishScript: string
published?: string
cwd: string
GITLAB_TOKEN: string
NPM_TOKEN?: string
}) {
console.log(
'No changesets found, attempting to publish any unpublished packages to npm',
)

if (NPM_TOKEN) {
const userNpmrcPath = `${env.HOME}/.npmrc`
if (await fileExists(userNpmrcPath)) {
console.info('Found existing user .npmrc file')
const userNpmrcContent = await fs.readFile(userNpmrcPath, 'utf8')
const authLine = userNpmrcContent.split('\n').find(line => {
// check based on https://github.com/npm/cli/blob/8f8f71e4dd5ee66b3b17888faad5a7bf6c657eed/test/lib/adduser.js#L103-L105
return /^\s*\/\/registry\.npmjs\.org\/:[_-]authToken=/i.test(line)
})
if (authLine) {
console.info(
'Found existing auth token for the npm registry in the user .npmrc file',
)
} else {
console.info(
"Didn't find existing auth token for the npm registry in the user .npmrc file, creating one",
)
await fs.appendFile(
userNpmrcPath,
`\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n`,
)
}
} else {
console.info(
'No user .npmrc file found, creating one with NPM_TOKEN used as auth token',
)
await fs.writeFile(
userNpmrcPath,
`//registry.npmjs.org/:_authToken=${NPM_TOKEN}\n`,
)
}
} else {
console.info(
'No NPM_TOKEN found - assuming trusted publishing or npm is already authenticated',
)
}

const result = await runPublish({
script: publishScript,
gitlabToken: GITLAB_TOKEN,
createGitlabReleases: !FALSY_VALUES.has(getInput('create_gitlab_releases')),
cwd,
})

if (result.published) {
setOutput('published', true)
setOutput('publishedPackages', result.publishedPackages)
exportVariable('PUBLISHED', true)
exportVariable('PUBLISHED_PACKAGES', result.publishedPackages)
if (published) {
execSync(published)
}
}
}
Loading