changesets-gitlab
Version:
[](https://github.com/un-ts/changesets-gitlab/actions/workflows/ci.yml?query=branch%3Amain) [![CodeRabbit Pull Request Revie
228 lines (210 loc) • 10 kB
JavaScript
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