Skip to content

Commit

Permalink
feat: migrate the changelog command (#21)
Browse files Browse the repository at this point in the history
* feat(changelog): add a changelog command to display the commits of the last 3 tags.

Also include a link to the ticket if a ticket_id is detected in the commit message.

---------

Co-authored-by: Grégoire Paris <[email protected]>
  • Loading branch information
M0nkeySan and greg0ire authored Jun 26, 2024
1 parent 58192c9 commit 066e1e2
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ SLACK_BOT_USER_O_AUTH_ACCESS_TOKEN=SLACK_BOT_USER_O_AUTH_ACCESS_TOKEN
SLACK_SIGNING_SECRET=SLACK_SIGNING_SECRET
EMAIL_DOMAINS=my-domain.com,ext.my-domain.com
GITLAB_URL=https://my-git.domain.com
TICKET_MANAGEMENT_URL_PATTERN=https://my-ticket-management.com/view/{ticketId}
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Here are the available commands:

| Command | Description |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/homer changelog` | Display changelogs, for any Gitlab project, between 2 release tags. |
| `/homer project add <project_name\|project_id>` | Add a Gitlab project to a channel. |
| `/homer project list` | List the Gitlab projects added to a channel. |
| `/homer project remove` | Remove a Gitlab project from a channel. |
Expand Down Expand Up @@ -213,6 +214,11 @@ Create a `.env` file containing the following variables:

The gitlab URL of your organization

- `TICKET_MANAGEMENT_URL_PATTERN`

The ticket management URL pattern for your organization, this is used to generate a link to the ticket in the changelog.
It must contain the `{ticketId}` matcher to be replaced by the ticket ID, for instance `https://my-ticket-management.com/view/{ticketId}`.

If you want Homer to connect to an **external PostgreSQL database**, you can set
the following variables:

Expand Down
181 changes: 181 additions & 0 deletions src/changelog/buildChangelogModalView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import type { Block, KnownBlock, View } from '@slack/types';
import { generateChangelog } from '@/changelog/utils/generateChangelog';
import { slackifyChangelog } from '@/changelog/utils/slackifyChangelog';
import { getProjectsByChannelId } from '@/core/services/data';
import { fetchProjectById, fetchProjectTags } from '@/core/services/gitlab';
import type { SlackOption } from '@/core/typings/SlackOption';

interface ChangelogModalData {
channelId?: string;
projectId?: number;
projectOptions?: SlackOption[];
releaseTagName?: string;
}

export async function buildChangelogModalView({
channelId,
projectId,
projectOptions,
releaseTagName,
}: ChangelogModalData): Promise<View> {
if (channelId !== undefined && projectOptions === undefined) {
const dataProjects = await getProjectsByChannelId(channelId);
const projects = await Promise.all(
dataProjects.map(async (dataProject) =>
fetchProjectById(Number(dataProject.projectId))
)
);

projectOptions = projects
.sort((a, b) =>
a.path_with_namespace.localeCompare(b.path_with_namespace)
)
.map((project) => ({
text: {
type: 'plain_text',
text: project.path_with_namespace,
},
value: project.id.toString(),
})) as SlackOption[];
}

if (!projectOptions || projectOptions.length === 0) {
throw new Error(
'No Gitlab project has been found on this channel :homer-stressed:'
);
}

if (projectId === undefined) {
projectId = parseInt(projectOptions[0].value, 10);
}

const tags = (await fetchProjectTags(projectId)).slice(0, 3);
const previousReleaseTagName = releaseTagName ?? tags[0]?.name;
const changelog = previousReleaseTagName
? await generateChangelog(projectId, previousReleaseTagName)
: '';

const previousReleaseOptions = tags.map(({ name }) => ({
text: {
type: 'plain_text',
text: name,
},
value: name,
})) as SlackOption[];

return {
type: 'modal',
callback_id: 'changelog-modal',
title: {
type: 'plain_text',
text: 'Changelog',
},
submit: {
type: 'plain_text',
text: 'Ok',
},
notify_on_close: false,
blocks: [
{
type: 'input',
block_id: 'changelog-project-block',
dispatch_action: true,
element: {
type: 'static_select',
action_id: 'changelog-select-project-action',
options: projectOptions,
initial_option: projectOptions?.[0],
placeholder: {
type: 'plain_text',
text: 'Select the project',
},
},
label: {
type: 'plain_text',
text: 'Project',
},
},
previousReleaseOptions.length > 0
? [
{
type: 'input',
block_id: 'changelog-release-tag-block',
dispatch_action: true,
element: {
type: 'static_select',
action_id: 'changelog-select-release-tag-action',
initial_option: previousReleaseOptions[0],
options: previousReleaseOptions,
placeholder: {
type: 'plain_text',
text: 'Select the previous release tag',
},
},
label: {
type: 'plain_text',
text: 'Previous release tag',
},
},
{
type: 'context',
block_id: 'changelog-release-tag-info-block',
elements: [
{
type: 'plain_text',
text: 'This should be changed only if the previous release has been aborted.',
},
],
},
]
: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Previous release tag*',
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: 'No previous release tag has been found.',
},
},
],
{
type: 'section',
block_id: 'changelog-preview-title-block',
text: {
type: 'mrkdwn',
text: '*Preview*',
},
},
{
type: 'section',
block_id: 'changelog-preview-block',
text: {
type: 'mrkdwn',
text: changelog
? slackifyChangelog(changelog)
: 'No change has been found.',
},
},
changelog && {
type: 'input',
block_id: 'changelog-markdown-block',
label: {
type: 'plain_text',
text: 'Markdown',
},
element: {
type: 'plain_text_input',
multiline: true,
initial_value: changelog,
},
},
]
.flat()
.filter(Boolean) as (KnownBlock | Block)[],
};
}
27 changes: 27 additions & 0 deletions src/changelog/changelogBlockActionsHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { logger } from '@/core/services/logger';
import type { BlockActionsPayload } from '@/core/typings/BlockActionPayload';
import { updateChangelog } from './utils/updateChangelog';
import { updateChangelogProject } from './utils/updateChangelogProject';

export async function changelogBlockActionsHandler(
payload: BlockActionsPayload
): Promise<void> {
await Promise.all(
payload.actions.map(async (action) => {
const { action_id } = action;

switch (action_id) {
case 'changelog-select-project-action':
return updateChangelogProject(payload);

case 'changelog-select-release-tag-action':
return updateChangelog(payload);

default:
logger.error(
new Error(`Unknown changelog block action: ${action_id}`)
);
}
})
);
}
27 changes: 27 additions & 0 deletions src/changelog/changelogRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Response } from 'express';
import { GENERIC_ERROR_MESSAGE, HTTP_STATUS_NO_CONTENT } from '@/constants';
import { slackBotWebClient } from '@/core/services/slack';
import type { SlackExpressRequest } from '@/core/typings/SlackSlashCommand';
import { buildChangelogModalView } from './buildChangelogModalView';

export async function changelogRequestHandler(
req: SlackExpressRequest,
res: Response
) {
const { channel_id, trigger_id, user_id } = req.body;

res.sendStatus(HTTP_STATUS_NO_CONTENT);

try {
await slackBotWebClient.views.open({
trigger_id,
view: await buildChangelogModalView({ channelId: channel_id }),
});
} catch (error) {
await slackBotWebClient.chat.postEphemeral({
channel: channel_id,
user: user_id,
text: error instanceof Error ? error.message : GENERIC_ERROR_MESSAGE,
});
}
}
Loading

0 comments on commit 066e1e2

Please sign in to comment.