Skip to content

Commit 90f3fd2

Browse files
Feature/add weekly financial report schedule (#116)
New Features * Automatic weekly financial report scheduling — runs Tuesdays at 1:00 PM ET, timezone-aware, non-overlapping with a 1-day catch-up window; scheduling now runs during startup before the worker begins. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Automatic weekly financial report schedule is checked and created at startup with race-condition protection. * Server/client address for workflow service is now configurable for startup connections. * **Refactor** * Centralized logging into a shared logger for consistent operational logs and startup flow. * **Chores** * CI code-quality scan updated to set organization and enable verbose logging. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 622ed8a commit 90f3fd2

7 files changed

Lines changed: 169 additions & 6 deletions

File tree

.github/workflows/code-quality.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@ jobs:
2222
sonarqube:
2323
name: SonarQube
2424
runs-on: ubuntu-latest
25+
if: vars.SONAR_DISABLE_CI != 'true'
2526
steps:
2627
- name: Checkout code
2728
uses: actions/checkout@v4
2829
with:
29-
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
30+
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
3031
- name: Install dependencies
3132
run: cd workers/main && npm install
3233
- name: Run tests with coverage
3334
run: cd workers/main && npm run coverage
3435
- name: Run SonarQube scan
3536
uses: SonarSource/sonarqube-scan-action@v6
37+
with:
38+
args: >
39+
-Dsonar.organization=automatization-bot
40+
-Dsonar.verbose=true
3641
env:
3742
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
3843

sonar-project.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
sonar.projectKey=speedandfunction_automatization
2-
sonar.organization=speedandfunction
2+
sonar.organization=automatization-bot
33
sonar.javascript.lcov.reportPaths=workers/main/coverage/lcov.info
44
sonar.exclusions=**/*.test.ts,**/src/dist/**
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Client } from '@temporalio/client';
2+
3+
import { logger } from '../logger';
4+
import { workerConfig } from './worker';
5+
6+
const SCHEDULE_ID = 'weekly-financial-report-schedule';
7+
8+
/**
9+
* Checks if an error is a "not found" error
10+
*/
11+
function validateIsScheduleNotFoundError(error: unknown): boolean {
12+
return (
13+
(error as { code?: number }).code === 5 ||
14+
(error instanceof Error &&
15+
error.message.toLowerCase().includes('not found'))
16+
);
17+
}
18+
19+
/**
20+
* Checks if schedule exists, returns true if it exists
21+
*/
22+
async function validateScheduleExists(client: Client): Promise<boolean> {
23+
try {
24+
const scheduleHandle = client.schedule.getHandle(SCHEDULE_ID);
25+
26+
await scheduleHandle.describe();
27+
logger.info(`Schedule ${SCHEDULE_ID} already exists, skipping creation`);
28+
29+
return true;
30+
} catch (error) {
31+
if (!validateIsScheduleNotFoundError(error)) {
32+
throw error;
33+
}
34+
logger.info(`Schedule ${SCHEDULE_ID} not found, creating new schedule`);
35+
36+
return false;
37+
}
38+
}
39+
40+
/**
41+
* Creates schedule with race condition protection
42+
*/
43+
async function createScheduleWithRaceProtection(client: Client): Promise<void> {
44+
try {
45+
await client.schedule.create({
46+
scheduleId: SCHEDULE_ID,
47+
spec: {
48+
cronExpressions: ['0 13 * * 2'],
49+
timezone: 'America/New_York',
50+
},
51+
action: {
52+
type: 'startWorkflow',
53+
workflowType: 'weeklyFinancialReportsWorkflow',
54+
taskQueue: workerConfig.taskQueue,
55+
workflowId: `weekly-financial-report-scheduled`,
56+
},
57+
policies: {
58+
overlap: 'SKIP',
59+
catchupWindow: '1 day',
60+
},
61+
});
62+
63+
logger.info(
64+
`Successfully created schedule ${SCHEDULE_ID} for weekly financial reports`,
65+
);
66+
} catch (createError) {
67+
// Handle race condition: schedule was created by another worker
68+
const isAlreadyExists =
69+
(createError as { code?: number }).code === 6 ||
70+
(createError instanceof Error &&
71+
(createError.message.toLowerCase().includes('already exists') ||
72+
createError.message.toLowerCase().includes('already running')));
73+
74+
if (isAlreadyExists) {
75+
logger.info(
76+
`Schedule ${SCHEDULE_ID} already exists (created by another worker), treating as success`,
77+
);
78+
79+
return;
80+
}
81+
82+
throw createError;
83+
}
84+
}
85+
86+
/**
87+
* Sets up the weekly financial report schedule
88+
* Schedule runs every Tuesday at 1 PM America/New_York time (EST/EDT)
89+
* @param client - Temporal client instance
90+
*/
91+
export async function setupWeeklyReportSchedule(client: Client): Promise<void> {
92+
try {
93+
const isScheduleExists = await validateScheduleExists(client);
94+
95+
if (isScheduleExists) {
96+
return;
97+
}
98+
99+
await createScheduleWithRaceProtection(client);
100+
} catch (error) {
101+
logger.error(
102+
`Failed to setup schedule ${SCHEDULE_ID}: ${error instanceof Error ? error.message : String(error)}`,
103+
);
104+
throw error;
105+
}
106+
}
107+
108+
/**
109+
* Schedule configuration exported for documentation and testing
110+
*/
111+
export const scheduleConfig = {
112+
scheduleId: SCHEDULE_ID,
113+
cronExpression: '0 13 * * 2',
114+
timezone: 'America/New_York',
115+
description: 'Runs every Tuesday at 1 PM EST/EDT',
116+
} as const;

workers/main/src/configs/temporal.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,26 @@ import { z } from 'zod';
33

44
const DEFAULT_TEMPORAL_ADDRESS = 'temporal:7233';
55

6+
/**
7+
* Temporal connection configuration
8+
* Used by both workers and clients to connect to Temporal server
9+
*/
610
export const temporalConfig: NativeConnectionOptions = {
711
address: process.env.TEMPORAL_ADDRESS || DEFAULT_TEMPORAL_ADDRESS,
812
};
913

1014
export const temporalSchema = z.object({
1115
TEMPORAL_ADDRESS: z.string().default(DEFAULT_TEMPORAL_ADDRESS),
1216
});
17+
18+
/**
19+
* Schedule Configuration Documentation
20+
*
21+
* Weekly Financial Report Schedule:
22+
* The schedule is automatically created/verified when the worker starts.
23+
*
24+
* For schedule configuration details (schedule ID, cron expression, timezone, etc.),
25+
* see the exported `scheduleConfig` object in ./schedules.ts
26+
*
27+
* Implementation: ./schedules.ts
28+
*/

workers/main/src/index.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22

3-
import { handleRunError, logger } from './index';
3+
import { handleRunError } from './index';
4+
import { logger } from './logger';
45

56
describe('handleRunError', () => {
67
let processExitSpy: ReturnType<typeof vi.spyOn>;

workers/main/src/index.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { DefaultLogger, NativeConnection, Worker } from '@temporalio/worker';
1+
import { Client, Connection } from '@temporalio/client';
2+
import { NativeConnection, Worker } from '@temporalio/worker';
23

34
import * as activities from './activities';
45
import { validateEnv } from './common/utils';
6+
import { setupWeeklyReportSchedule } from './configs/schedules';
57
import { temporalConfig } from './configs/temporal';
68
import { workerConfig } from './configs/worker';
7-
8-
export const logger = new DefaultLogger('ERROR');
9+
import { logger } from './logger';
910

1011
validateEnv();
1112

@@ -25,6 +26,22 @@ export async function createWorker(connection: NativeConnection) {
2526
}
2627

2728
export async function run(): Promise<void> {
29+
// Setup weekly report schedule before starting worker
30+
const clientConnection = await Connection.connect(temporalConfig);
31+
32+
try {
33+
const client = new Client({ connection: clientConnection });
34+
35+
await setupWeeklyReportSchedule(client);
36+
} catch (err) {
37+
logger.error(
38+
`Failed to setup schedule: ${err instanceof Error ? err.message : String(err)}`,
39+
);
40+
} finally {
41+
await clientConnection.close();
42+
}
43+
44+
// Create and run worker
2845
const connection = await createConnection();
2946

3047
try {

workers/main/src/logger.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { DefaultLogger } from '@temporalio/worker';
2+
3+
/**
4+
* Shared logger instance for the worker
5+
* Using INFO level to capture important operational messages
6+
* including schedule setup, errors, and warnings
7+
*/
8+
export const logger = new DefaultLogger('INFO');

0 commit comments

Comments
 (0)