From 02c352ef0271d3cb2af38772b5b1295325889314 Mon Sep 17 00:00:00 2001 From: Chris Book Date: Tue, 3 Mar 2026 12:54:35 -0500 Subject: [PATCH 1/4] feat: add an input to run at a subdirectory of the repo root - Allows for situations where the root of the npm workspace is not at the top level. --- README.md | 1 + src/comment.ts | 39 +++++++++++++++++++++++++++++-------- src/get-changed-packages.ts | 20 ++++++++++++------- src/main.ts | 7 ++++++- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 378a2dab..2808f6af 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ GitLab CI cli for [changesets](https://github.com/atlassian/changesets) like its - `INPUT_TARGET_BRANCH` -> The merge request target branch. Defaults to current branch - `INPUT_CREATE_GITLAB_RELEASES` - A boolean value to indicate whether to create Gitlab releases after publish or not. Default true. - `INPUT_LABELS` - A comma separated string of labels to be added to the version package Gitlab Merge request +- `INPUT_CWD` - A relative path from the repo root to the directory containing `package.json` and `.changeset/`. Use this when your npm/yarn workspace lives in a subdirectory of the git repo. Defaults to the repo root. ### Outputs diff --git a/src/comment.ts b/src/comment.ts index 1ea39016..40f4998a 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -1,3 +1,5 @@ +import path from 'node:path' + import { ValidationError } from '@changesets/errors' import type { ComprehensiveRelease, @@ -21,7 +23,12 @@ import * as context from './context.js' import { env } from './env.js' import { getChangedPackages } from './get-changed-packages.js' import type { LooseString } from './types.js' -import { getUsername, HTTP_STATUS_NOT_FOUND, TRUTHY_VALUES } from './utils.js' +import { + getOptionalInput, + getUsername, + HTTP_STATUS_NOT_FOUND, + TRUTHY_VALUES, +} from './utils.js' const generatedByBotNote = 'Generated By Changesets GitLab Bot' @@ -220,13 +227,15 @@ async function getNoteInfo( const hasChangesetBeenAdded = async ( changedFilesPromise: Promise, + changesetPrefix: string, ) => { const changedFiles = await changedFilesPromise return changedFiles.some(file => { return ( file.new_file && - /^\.changeset\/.+\.md$/.test(file.new_path) && - file.new_path !== '.changeset/README.md' + file.new_path.startsWith(changesetPrefix + '/') && + file.new_path.endsWith('.md') && + file.new_path !== changesetPrefix + '/README.md' ) }) } @@ -249,6 +258,12 @@ export const comment = async () => { return } + const cwdInput = getOptionalInput('cwd') + const changesetPrefix = cwdInput + ? `${cwdInput.replace(/\/$/, '')}/.changeset` + : '.changeset' + const absoluteCwd = path.resolve(process.cwd(), cwdInput ?? '.') + const api = createApi() let errFromFetchingChangedFiles = '' @@ -273,15 +288,23 @@ export const comment = async () => { return changes }) + const subdirPrefix = cwdInput ? cwdInput.replace(/\/$/, '') + '/' : '' + const packageChangedFiles = changedFilesPromise.then(changedFiles => + changedFiles.map(({ new_path }) => + subdirPrefix && new_path.startsWith(subdirPrefix) + ? new_path.slice(subdirPrefix.length) + : new_path, + ), + ) + const [noteInfo, hasChangeset, { changedPackages, releasePlan }] = await Promise.all([ getNoteInfo(api, mrIid, commentType), - hasChangesetBeenAdded(changedFilesPromise), + hasChangesetBeenAdded(changedFilesPromise, changesetPrefix), getChangedPackages({ - changedFiles: changedFilesPromise.then(changedFiles => - changedFiles.map(({ new_path }) => new_path), - ), + changedFiles: packageChangedFiles, api, + cwd: absoluteCwd, }).catch((err: unknown) => { if (err instanceof ValidationError) { errFromFetchingChangedFiles = `
💥 An error occurred when fetching the changed packages and changesets in this MR\n\n\`\`\`\n${err.message}\n\`\`\`\n\n
\n` @@ -295,7 +318,7 @@ export const comment = async () => { }), ] as const) - const newChangesetFileName = `.changeset/${humanId({ + const newChangesetFileName = `${changesetPrefix}/${humanId({ separator: '-', capitalize: false, })}.md` diff --git a/src/get-changed-packages.ts b/src/get-changed-packages.ts index 6aa7a29a..59b02126 100644 --- a/src/get-changed-packages.ts +++ b/src/get-changed-packages.ts @@ -23,9 +23,11 @@ function fetchFile(path: string) { export const getChangedPackages = async ({ changedFiles: changedFilesPromise, + cwd = process.cwd(), }: { changedFiles: Promise | string[] api: Gitlab + cwd?: string // eslint-disable-next-line sonarjs/cognitive-complexity }) => { let hasErrored = false @@ -53,7 +55,7 @@ export const getChangedPackages = async ({ async function getPackage(pkgPath: string) { const jsonContent = await fetchJsonFile( - pkgPath + '/package.json', + nodePath.join(cwd, pkgPath, 'package.json'), ) return { packageJson: jsonContent, @@ -72,10 +74,12 @@ export const getChangedPackages = async ({ workspaces?: string[] } } - >('package.json') - const configPromise = fetchJsonFile('.changeset/config.json') + >(nodePath.join(cwd, 'package.json')) + const configPromise = fetchJsonFile( + nodePath.join(cwd, '.changeset/config.json'), + ) - const tree = await getAllFiles(process.cwd()) + const tree = await getAllFiles(cwd) let preStatePromise: Promise | undefined const changesetPromises: Array> = [] @@ -90,7 +94,7 @@ export const getChangedPackages = async ({ } else if (item === 'pnpm-workspace.yaml') { isPnpm = true } else if (item === '.changeset/pre.json') { - preStatePromise = fetchJsonFile('.changeset/pre.json') + preStatePromise = fetchJsonFile(nodePath.join(cwd, '.changeset/pre.json')) } else if ( item !== '.changeset/README.md' && item.startsWith('.changeset') && @@ -103,7 +107,7 @@ export const getChangedPackages = async ({ } const id = res[1] changesetPromises.push( - fetchTextFile(item).then(text => ({ + fetchTextFile(nodePath.join(cwd, item)).then(text => ({ ...parseChangeset(text), id, })), @@ -116,7 +120,9 @@ export const getChangedPackages = async ({ tool = { tool: 'pnpm', globs: ( - parse(await fetchTextFile('pnpm-workspace.yaml')) as { + parse( + await fetchTextFile(nodePath.join(cwd, 'pnpm-workspace.yaml')), + ) as { packages: string[] } ).packages, diff --git a/src/main.ts b/src/main.ts index 5eabf022..883150d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,5 @@ import fs from 'node:fs' +import path from 'node:path' import { URL } from 'node:url' import { getInput, setFailed, setOutput, exportVariable } from '@actions/core' @@ -50,7 +51,9 @@ export const main = async ({ ) } - const { changesets } = await readChangesetState() + const cwd = path.resolve(process.cwd(), getOptionalInput('cwd') ?? '.') + + const { changesets } = await readChangesetState(cwd) const publishScript = getInput('publish') const hasChangesets = changesets.length > 0 @@ -88,6 +91,7 @@ export const main = async ({ createGitlabReleases: !FALSY_VALUES.has( getInput('create_gitlab_releases'), ), + cwd, }) if (result.published) { @@ -110,6 +114,7 @@ export const main = async ({ commitMessage: getOptionalInput('commit'), removeSourceBranch: getInput('remove_source_branch') === 'true', hasPublishScript, + cwd, }) if (onlyChangesets) { execSync(onlyChangesets) From 67170aad4cccd15615edd8da79e4e0404e9d3635 Mon Sep 17 00:00:00 2001 From: bookchris Date: Wed, 4 Mar 2026 11:58:13 -0500 Subject: [PATCH 2/4] Add changeset for minor change. --- .changeset/dry-spoons-own.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/dry-spoons-own.md diff --git a/.changeset/dry-spoons-own.md b/.changeset/dry-spoons-own.md new file mode 100644 index 00000000..d5be5f19 --- /dev/null +++ b/.changeset/dry-spoons-own.md @@ -0,0 +1,5 @@ +--- +"changesets-gitlab": minor +--- + +feat: add an input to run at a subdirectory of the repo root From 97d58e76cc0b71b261869640b886d39a2c563063 Mon Sep 17 00:00:00 2001 From: Chris Book Date: Mon, 9 Mar 2026 09:48:29 -0400 Subject: [PATCH 3/4] fix: address input normalization comment --- src/comment.ts | 12 +++++------- src/main.ts | 3 ++- src/utils.ts | 12 ++++++++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/comment.ts b/src/comment.ts index 40f4998a..051774fc 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -24,9 +24,9 @@ import { env } from './env.js' import { getChangedPackages } from './get-changed-packages.js' import type { LooseString } from './types.js' import { - getOptionalInput, getUsername, HTTP_STATUS_NOT_FOUND, + getCwdInput, TRUTHY_VALUES, } from './utils.js' @@ -258,11 +258,9 @@ export const comment = async () => { return } - const cwdInput = getOptionalInput('cwd') - const changesetPrefix = cwdInput - ? `${cwdInput.replace(/\/$/, '')}/.changeset` - : '.changeset' - const absoluteCwd = path.resolve(process.cwd(), cwdInput ?? '.') + const cwdRel = getCwdInput() + const changesetPrefix = cwdRel ? `${cwdRel}/.changeset` : '.changeset' + const absoluteCwd = path.resolve(process.cwd(), cwdRel || '.') const api = createApi() @@ -288,7 +286,7 @@ export const comment = async () => { return changes }) - const subdirPrefix = cwdInput ? cwdInput.replace(/\/$/, '') + '/' : '' + const subdirPrefix = cwdRel ? `${cwdRel}/` : '' const packageChangedFiles = changedFilesPromise.then(changedFiles => changedFiles.map(({ new_path }) => subdirPrefix && new_path.startsWith(subdirPrefix) diff --git a/src/main.ts b/src/main.ts index 883150d7..e2c8ccef 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ import { FALSY_VALUES, getOptionalInput, getUsername, + getCwdInput, TRUTHY_VALUES, } from './utils.js' @@ -51,7 +52,7 @@ export const main = async ({ ) } - const cwd = path.resolve(process.cwd(), getOptionalInput('cwd') ?? '.') + const cwd = path.resolve(process.cwd(), getCwdInput() || '.') const { changesets } = await readChangesetState(cwd) diff --git a/src/utils.ts b/src/utils.ts index cabea43b..22b6c298 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -161,6 +161,18 @@ export const execSync = (command: string) => export const getOptionalInput = (name: string) => getInput(name) || undefined +export const getCwdInput = (): string => { + const input = getOptionalInput('cwd') + if (!input) { + return '' + } + const normalized = input.replace(/^\.\//, '').replace(/\/$/, '') + if (path.isAbsolute(normalized) || normalized.split('/').includes('..')) { + throw new Error(`Invalid cwd input: "${input}"`) + } + return normalized === '.' ? '' : normalized +} + // eslint-disable-next-line sonarjs/function-return-type export const getUsername = (api: Gitlab) => { return ( From 0720e405da2efecbc9cf392560b87ce2581a98c5 Mon Sep 17 00:00:00 2001 From: Chris Book Date: Mon, 9 Mar 2026 12:13:54 -0400 Subject: [PATCH 4/4] fix: address package filtering comments --- src/comment.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/comment.ts b/src/comment.ts index 051774fc..2a1ed28f 100644 --- a/src/comment.ts +++ b/src/comment.ts @@ -288,11 +288,13 @@ export const comment = async () => { const subdirPrefix = cwdRel ? `${cwdRel}/` : '' const packageChangedFiles = changedFilesPromise.then(changedFiles => - changedFiles.map(({ new_path }) => - subdirPrefix && new_path.startsWith(subdirPrefix) - ? new_path.slice(subdirPrefix.length) - : new_path, - ), + changedFiles + .filter( + ({ new_path }) => !subdirPrefix || new_path.startsWith(subdirPrefix), + ) + .map(({ new_path }) => + subdirPrefix ? new_path.slice(subdirPrefix.length) : new_path, + ), ) const [noteInfo, hasChangeset, { changedPackages, releasePlan }] =