UNPKG

changesets-gitlab

Version:

[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/un-ts/changesets-gitlab/ci.yml?branch=main)](https://github.com/un-ts/changesets-gitlab/actions/workflows/ci.yml?query=branch%3Amain) [![CodeRabbit Pull Request Revie

228 lines (210 loc) 10 kB
import { ValidationError } from '@changesets/errors'; import { GitbeakerRequestError, } from '@gitbeaker/rest'; import { humanId } from 'human-id'; import { markdownTable } from 'markdown-table'; import { createApi } from "./api.js"; import * as context from './context.js'; import { env } from './env.js'; import { getChangedPackages } from './get-changed-packages.js'; import { getUsername, HTTP_STATUS_NOT_FOUND, TRUTHY_VALUES } from './utils.js'; const generatedByBotNote = 'Generated By Changesets GitLab Bot'; const getReleasePlanMessage = (releasePlan) => { if (!releasePlan) { return ''; } const publishableReleases = releasePlan.releases.filter((x) => x.type !== 'none'); const table = markdownTable([ ['Name', 'Type'], ...publishableReleases.map(x => [ x.name, { major: 'Major', minor: 'Minor', patch: 'Patch', }[x.type], ]), ]); return `<details><summary>This MR includes ${releasePlan.changesets.length > 0 ? `changesets to release ${publishableReleases.length === 1 ? '1 package' : `${publishableReleases.length} packages`}` : 'no changesets'}</summary> ${publishableReleases.length > 0 ? table : "When changesets are added to this MR, you'll see the packages that this MR includes changesets for and the associated semver types"} </details>`; }; const customLinks = env.GITLAB_COMMENT_CUSTOM_LINKS?.trim(); const ADD_CHANGESET_URL_PLACEHOLDER_REGEXP = /\{\{\s*addChangesetUrl\s*\}\}/; const getAbsentMessage = (commitSha, addChangesetUrl, newChangesetTemplateFallback, releasePlan) => `### ⚠️ No Changeset found Latest commit: ${commitSha} Merging this MR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.** ${getReleasePlanMessage(releasePlan)} ${customLinks ? customLinks.replace(ADD_CHANGESET_URL_PLACEHOLDER_REGEXP, addChangesetUrl) : `[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). [Click here if you're a maintainer who wants to add a changeset to this MR](${addChangesetUrl})`} ${newChangesetTemplateFallback} __${generatedByBotNote}__ `; const getApproveMessage = (commitSha, addChangesetUrl, newChangesetTemplateFallback, releasePlan) => `### 🦋 Changeset detected Latest commit: ${commitSha} **The changes in this MR will be included in the next version bump.** ${getReleasePlanMessage(releasePlan)} ${customLinks ? customLinks.replace(ADD_CHANGESET_URL_PLACEHOLDER_REGEXP, addChangesetUrl) : `Not sure what this means? [Click here to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md). [Click here if you're a maintainer who wants to add another changeset to this MR](${addChangesetUrl})`} ${newChangesetTemplateFallback} __${generatedByBotNote}__ `; const getNewChangesetTemplate = (changedPackages, title) => `--- ${changedPackages.map(x => `"${x}": patch`).join('\n')} --- ${title} `; const isMrNote = (discussionOrNote) => 'noteable_type' in discussionOrNote && discussionOrNote.noteable_type === 'MergeRequest'; const RANDOM_BOT_NAME_PATTERN = /^((?:project|group)_\d+_bot\w*)_[\da-z]+$/i; const isChangesetBotNote = (note, username, random) => (note.author.username === username || (random && RANDOM_BOT_NAME_PATTERN.exec(note.author.username)?.[1] === username)) && note.body.includes(generatedByBotNote); async function getNoteInfo(api, mrIid, commentType, random) { const discussionOrNotes = await (commentType === 'discussion' ? api.MergeRequestDiscussions.all(context.projectId, mrIid) : api.MergeRequestNotes.all(context.projectId, +mrIid)); const username = await getUsername(api); for (const discussionOrNote of discussionOrNotes) { if (isMrNote(discussionOrNote)) { if (isChangesetBotNote(discussionOrNote, username, random)) { return { noteId: discussionOrNote.id, }; } continue; } if (!discussionOrNote.notes) { continue; } const changesetBotNote = discussionOrNote.notes.find(note => isChangesetBotNote(note, username)); if (changesetBotNote) { return { discussionId: discussionOrNote.id, noteId: changesetBotNote.id, }; } } return random ? null : getNoteInfo(api, mrIid, commentType, true); } const hasChangesetBeenAdded = async (changedFilesPromise) => { const changedFiles = await changedFilesPromise; return changedFiles.some(file => { return (file.new_file && /^\.changeset\/.+\.md$/.test(file.new_path) && file.new_path !== '.changeset/README.md'); }); }; export const comment = async () => { const mrBranch = env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME; if (!mrBranch) { console.warn('[changesets-gitlab:comment] It should only be used on MR'); return; } const { CI_MERGE_REQUEST_IID: mrIid, GITLAB_COMMENT_TYPE: commentType, GITLAB_ADD_CHANGESET_MESSAGE: commitMessage, } = env; if (mrBranch.startsWith('changeset-release')) { return; } const api = createApi(); let errFromFetchingChangedFiles = ''; try { const latestCommitSha = env.CI_MERGE_REQUEST_SOURCE_BRANCH_SHA; const changedFilesPromise = api.MergeRequests.allDiffs(context.projectId, mrIid).catch(async (err) => { if (!(err instanceof GitbeakerRequestError) || err.cause?.response.status !== HTTP_STATUS_NOT_FOUND) { throw err; } const { changes } = await api.MergeRequests.showChanges(context.projectId, mrIid); return changes; }); const [noteInfo, hasChangeset, { changedPackages, releasePlan }] = await Promise.all([ getNoteInfo(api, mrIid, commentType), hasChangesetBeenAdded(changedFilesPromise), getChangedPackages({ changedFiles: changedFilesPromise.then(changedFiles => changedFiles.map(({ new_path }) => new_path)), api, }).catch((err) => { if (err instanceof ValidationError) { 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); } return { changedPackages: ['@fake-scope/fake-pkg'], releasePlan: null, }; }), ]); const newChangesetFileName = `.changeset/${humanId({ separator: '-', capitalize: false, })}.md`; const newChangesetTemplate = getNewChangesetTemplate(changedPackages, env.CI_MERGE_REQUEST_TITLE); const addChangesetUrl = `${env.CI_MERGE_REQUEST_PROJECT_URL}/-/new/${mrBranch}?file_name=${newChangesetFileName}&file=${encodeURIComponent(newChangesetTemplate)}${commitMessage ? '&commit_message=' + encodeURIComponent(commitMessage) : ''}`; const newChangesetTemplateFallback = ` If the above link doesn't fill the changeset template file name and content which is [a known regression on GitLab >= 16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/532221), you can copy and paste the following template into ${newChangesetFileName} instead: \`\`\`yaml ${newChangesetTemplate} \`\`\` `.trim(); const prComment = (hasChangeset ? getApproveMessage(latestCommitSha, addChangesetUrl, newChangesetTemplateFallback, releasePlan) : getAbsentMessage(latestCommitSha, addChangesetUrl, newChangesetTemplateFallback, releasePlan)) + errFromFetchingChangedFiles; switch (commentType) { case 'discussion': { if (noteInfo) { if (hasChangeset && TRUTHY_VALUES.has(env.GITLAB_COMMENT_DISCUSSION_AUTO_RESOLVE || '1')) { await api.MergeRequestDiscussions.resolve(context.projectId, mrIid, noteInfo.discussionId, true); } return api.MergeRequestDiscussions.editNote(context.projectId, mrIid, noteInfo.discussionId, noteInfo.noteId, { body: prComment, }); } return api.MergeRequestDiscussions.create(context.projectId, mrIid, prComment); } case 'note': { if (noteInfo) { return api.MergeRequestNotes.edit(context.projectId, mrIid, noteInfo.noteId, { body: prComment }); } return api.MergeRequestNotes.create(context.projectId, mrIid, prComment); } default: { throw new Error(`Invalid comment type "${commentType}", should be "discussion" or "note"`); } } } catch (err) { if (err instanceof GitbeakerRequestError && err.cause) { const { description, request, response } = err.cause; console.error(description); try { console.error('request:', await request.text()); } catch { console.error("The error's request could not be used as plain text"); } try { console.error('response:', await response.text()); } catch { console.error("The error's response could not be used as plain text"); } } throw err; } }; //# sourceMappingURL=comment.js.map