Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
102 changes: 98 additions & 4 deletions .github/workflows/on-push-trigger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:
permissions:
contents: read
actions: write
pull-requests: read

jobs:
trigger-deploy-workflow:
Expand All @@ -16,15 +17,108 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Get PR Number
id: get-pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --state merged --search "${{ github.sha }}" --json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "No PR found for this commit"
echo "pr_number=" >> $GITHUB_OUTPUT
else
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "Found PR #$PR_NUMBER"
fi

- name: Get PR Labels
id: get-labels
if: steps.get-pr.outputs.pr_number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LABELS=$(gh pr view ${{ steps.get-pr.outputs.pr_number }} --json labels --jq '.labels[].name')
echo "Labels found:"
echo "$LABELS"
echo "labels<<EOF" >> $GITHUB_OUTPUT
echo "$LABELS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

- name: Determine Services to Deploy
id: determine-services
env:
LABELS: ${{ steps.get-labels.outputs.labels || '' }}
run: |
DEPLOY_API=false
DEPLOY_WORKER=false
DEPLOY_WS=false
DEPLOY_WEBHOOK=false
SKIP_DEPLOYMENT=false

# Check if only CI/CD label exists (standalone)
LABEL_COUNT=$(echo "$LABELS" | grep -v '^$' | wc -l | tr -d ' ')
if [ "$LABEL_COUNT" = "1" ] && echo "$LABELS" | grep -q "CI/CD"; then
echo "Only CI/CD label found, skipping deployment"
SKIP_DEPLOYMENT=true
elif echo "$LABELS" | grep -q "CI/CD"; then
echo "CI/CD label found with other labels, continuing with deployment"
fi

# Check for service-specific labels only if not skipping
if [ "$SKIP_DEPLOYMENT" = "false" ]; then
if echo "$LABELS" | grep -q "@novu/api-service"; then
DEPLOY_API=true
echo "Found @novu/api-service label"
fi

if echo "$LABELS" | grep -q "@novu/worker"; then
DEPLOY_WORKER=true
echo "Found @novu/worker label"
fi

if echo "$LABELS" | grep -q "@novu/ws"; then
DEPLOY_WS=true
echo "Found @novu/ws label"
fi

if echo "$LABELS" | grep -q "@novu/webhook"; then
DEPLOY_WEBHOOK=true
echo "Found @novu/webhook label"
fi

# If no service labels found, deploy api and worker by default
if [ "$DEPLOY_API" = "false" ] && [ "$DEPLOY_WORKER" = "false" ] && [ "$DEPLOY_WS" = "false" ] && [ "$DEPLOY_WEBHOOK" = "false" ]; then
echo "No service labels found, deploying api and worker by default"
DEPLOY_API=true
DEPLOY_WORKER=true
fi
fi

echo "skip_deployment=$SKIP_DEPLOYMENT" >> $GITHUB_OUTPUT
echo "deploy_api=$DEPLOY_API" >> $GITHUB_OUTPUT
echo "deploy_worker=$DEPLOY_WORKER" >> $GITHUB_OUTPUT
echo "deploy_ws=$DEPLOY_WS" >> $GITHUB_OUTPUT
echo "deploy_webhook=$DEPLOY_WEBHOOK" >> $GITHUB_OUTPUT

echo "Final deployment configuration:"
echo " Skip: $SKIP_DEPLOYMENT"
echo " API: $DEPLOY_API"
echo " Worker: $DEPLOY_WORKER"
echo " WS: $DEPLOY_WS"
echo " Webhook: $DEPLOY_WEBHOOK"

- name: Trigger Deploy Workflow via GitHub CLI
if: steps.determine-services.outputs.skip_deployment == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh workflow run "deploy.yml" \
--ref next \
-f environment=staging \
-f deploy_api=true \
-f deploy_worker=true \
-f deploy_ws=true \
-f deploy_webhook=true
-f deploy_api=${{ steps.determine-services.outputs.deploy_api }} \
-f deploy_worker=${{ steps.determine-services.outputs.deploy_worker }} \
-f deploy_ws=${{ steps.determine-services.outputs.deploy_ws }} \
-f deploy_webhook=${{ steps.determine-services.outputs.deploy_webhook }}
32 changes: 26 additions & 6 deletions apps/api/src/app/inbox/e2e/mark-notification-as.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u

it('should update the read status', async () => {
const { body, status } = await updateNotification({ id: message._id, status: 'read' });
const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -158,7 +158,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u

const { body, status } = await updateNotification({ id: message._id, status: 'unread' });

const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -177,7 +177,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u
it('should update the archived status', async () => {
const { body, status } = await updateNotification({ id: message._id, status: 'archive' });

const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -202,7 +202,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u

const { body, status } = await updateNotification({ id: message._id, status: 'unarchive' });

const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -226,7 +226,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u
body: { snoozeUntil },
});

const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand Down Expand Up @@ -254,7 +254,7 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u
// Then unsnooze it
const { body, status } = await updateNotification({ id: message._id, status: 'unsnooze' });

const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -267,4 +267,24 @@ describe('Mark Notification As - /inbox/notifications/:id/{read,unread,archive,u
expect(body.data.isSnoozed).to.be.false;
expect(body.data.snoozedUntil).to.be.undefined;
});

it('should return workflow and to fields populated', async () => {
const { body, status } = await updateNotification({ id: message._id, status: 'read' });

expect(status).to.equal(200);
expect(body.data.workflow).to.exist;
expect(body.data.workflow.id).to.equal(String(template._id));
expect(body.data.workflow.identifier).to.equal(template.triggers?.[0]?.identifier);
expect(body.data.workflow.name).to.equal(template.name);
expect(body.data.workflow.critical).to.equal(template.critical);
expect(body.data.workflow.tags).to.deep.equal(template.tags);
expect(body.data.workflow.severity).to.exist;

expect(body.data.to).to.exist;
expect(body.data.to.id).to.equal(subscriber?._id ? String(subscriber._id) : '');
expect(body.data.to.subscriberId).to.equal(subscriber?.subscriberId ?? '');
expect(body.data.to.firstName).to.equal(subscriber?.firstName);
expect(body.data.to.lastName).to.equal(subscriber?.lastName);
expect(body.data.to.avatar).to.equal(subscriber?.avatar);
});
});
28 changes: 26 additions & 2 deletions apps/api/src/app/inbox/e2e/update-notification-action.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ describe('Update Notification Action - /inbox/notifications/:id/{complete/revert
action: 'complete',
actionType: ButtonTypeEnum.PRIMARY,
});
const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -219,7 +219,7 @@ describe('Update Notification Action - /inbox/notifications/:id/{complete/revert
action: 'complete',
actionType: ButtonTypeEnum.SECONDARY,
});
const updatedMessage = (await messageRepository.findOne({
const updatedMessage = (await messageRepository.findOneForInbox({
_environmentId: session.environment._id,
_subscriberId: subscriber?._id ?? '',
_templateId: template._id,
Expand All @@ -230,4 +230,28 @@ describe('Update Notification Action - /inbox/notifications/:id/{complete/revert
expect(body.data.primaryAction.isCompleted).to.be.false;
expect(body.data.secondaryAction.isCompleted).to.be.true;
});

it('should return workflow and to fields populated', async () => {
const { body, status } = await updateNotificationAction({
id: message._id,
action: 'complete',
actionType: ButtonTypeEnum.PRIMARY,
});

expect(status).to.equal(200);
expect(body.data.workflow).to.exist;
expect(body.data.workflow.id).to.equal(String(template._id));
expect(body.data.workflow.identifier).to.equal(template.triggers?.[0]?.identifier);
expect(body.data.workflow.name).to.equal(template.name);
expect(body.data.workflow.critical).to.equal(template.critical);
expect(body.data.workflow.tags).to.deep.equal(template.tags);
expect(body.data.workflow.severity).to.exist;

expect(body.data.to).to.exist;
expect(body.data.to.id).to.equal(subscriber?._id ? String(subscriber._id) : '');
expect(body.data.to.subscriberId).to.equal(subscriber?.subscriberId ?? '');
expect(body.data.to.firstName).to.equal(subscriber?.firstName);
expect(body.data.to.lastName).to.equal(subscriber?.lastName);
expect(body.data.to.avatar).to.equal(subscriber?.avatar);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('MarkNotificationAs', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(undefined);
messageRepositoryMock.findOneForInbox.resolves(undefined);

try {
await updateNotification.execute(command);
Expand All @@ -104,8 +104,8 @@ describe('MarkNotificationAs', () => {
const updatedMessageMock = { ...mockMessage, read: true };

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.onFirstCall().resolves(mockMessage);
messageRepositoryMock.findOne.onSecondCall().resolves(updatedMessageMock);
messageRepositoryMock.findOneForInbox.onFirstCall().resolves(mockMessage);
messageRepositoryMock.findOneForInbox.onSecondCall().resolves(updatedMessageMock);
markManyNotificationsAsMock.execute.resolves();

const updatedMessage = await updateNotification.execute(command);
Expand Down Expand Up @@ -136,7 +136,8 @@ describe('MarkNotificationAs', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(mockMessage);
messageRepositoryMock.findOneForInbox.onFirstCall().resolves(mockMessage);
messageRepositoryMock.findOneForInbox.onSecondCall().resolves(mockMessage);

await updateNotification.execute(command);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class MarkNotificationAs {
throw new BadRequestException(`Subscriber with id: ${command.subscriberId} is not found.`);
}

const message = await this.messageRepository.findOne({
const message = await this.messageRepository.findOneForInbox({
_environmentId: command.environmentId,
_subscriberId: subscriber._id,
_id: command.notificationId,
Expand Down Expand Up @@ -62,7 +62,7 @@ export class MarkNotificationAs {
});

return mapToDto(
(await this.messageRepository.findOne({
(await this.messageRepository.findOneForInbox({
_environmentId: command.environmentId,
_id: command.notificationId,
})) as MessageEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ describe('UpdateNotificationAction', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(undefined);
messageRepositoryMock.findOneForInbox.resolves(undefined);

try {
await updateNotificationAction.execute(command);
Expand All @@ -126,7 +126,7 @@ describe('UpdateNotificationAction', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(mockMessage);
messageRepositoryMock.findOneForInbox.resolves(mockMessage);

try {
await updateNotificationAction.execute(command);
Expand All @@ -147,7 +147,7 @@ describe('UpdateNotificationAction', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(mockMessage);
messageRepositoryMock.findOneForInbox.resolves(mockMessage);

try {
await updateNotificationAction.execute(command);
Expand Down Expand Up @@ -179,8 +179,8 @@ describe('UpdateNotificationAction', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.onFirstCall().resolves(mockMessageWithButtons);
messageRepositoryMock.findOne.onSecondCall().resolves(updatedMessageWithButtonsMock);
messageRepositoryMock.findOneForInbox.onFirstCall().resolves(mockMessageWithButtons);
messageRepositoryMock.findOneForInbox.onSecondCall().resolves(updatedMessageWithButtonsMock);
messageRepositoryMock.updateActionStatus.resolves();

const updatedMessage = await updateNotificationAction.execute(command);
Expand Down Expand Up @@ -211,7 +211,8 @@ describe('UpdateNotificationAction', () => {
};

getSubscriberMock.execute.resolves(mockSubscriber);
messageRepositoryMock.findOne.resolves(mockMessageWithButtons);
messageRepositoryMock.findOneForInbox.onFirstCall().resolves(mockMessageWithButtons);
messageRepositoryMock.findOneForInbox.onSecondCall().resolves(mockMessageWithButtons);
messageRepositoryMock.updateActionStatus.resolves();

await updateNotificationAction.execute(command);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class UpdateNotificationAction {
throw new BadRequestException(`Subscriber with id: ${command.subscriberId} is not found.`);
}

const message = await this.messageRepository.findOne({
const message = await this.messageRepository.findOneForInbox({
_environmentId: command.environmentId,
_subscriberId: subscriber._id,
_id: command.notificationId,
Expand Down Expand Up @@ -67,7 +67,7 @@ export class UpdateNotificationAction {
});

return mapToDto(
(await this.messageRepository.findOne({
(await this.messageRepository.findOneForInbox({
_environmentId: command.environmentId,
_id: command.notificationId,
})) as MessageEntity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { SubscriberPreferenceChannels } from '../../shared/dtos/preference-channels';
import { SubscriberPreferenceOverrideDto } from '../../subscribers/dtos';
Expand All @@ -22,4 +22,10 @@ export class SubscriberWorkflowPreferenceDto {
@ApiProperty({ description: 'Workflow information', type: SubscriberPreferencesWorkflowInfoDto })
@Type(() => SubscriberPreferencesWorkflowInfoDto)
workflow: SubscriberPreferencesWorkflowInfoDto;

@ApiPropertyOptional({
description:
'Timestamp when the subscriber last updated their preference. Only present if subscriber explicitly set preferences.',
})
updatedAt?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export class GetSubscriberPreferences {
enabled: preference.enabled,
channels: preference.channels,
overrides: preference.overrides,
updatedAt: preference.updatedAt,
workflow: {
slug: buildSlug(template.name, ShortIsPrefixEnum.WORKFLOW, template._id),
identifier: template.triggers[0].identifier,
Expand Down
Loading
Loading