diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index a8aa1d4..8f71dd8 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -22,17 +22,22 @@ jobs: sonarqube: name: SonarQube runs-on: ubuntu-latest + if: vars.SONAR_DISABLE_CI != 'true' steps: - name: Checkout code uses: actions/checkout@v4 with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Install dependencies run: cd workers/main && npm install - name: Run tests with coverage run: cd workers/main && npm run coverage - name: Run SonarQube scan uses: SonarSource/sonarqube-scan-action@v6 + with: + args: > + -Dsonar.organization=automatization-bot + -Dsonar.verbose=true env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/sonar-project.properties b/sonar-project.properties index ec62924..a698ce2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,4 +1,4 @@ sonar.projectKey=speedandfunction_automatization -sonar.organization=speedandfunction +sonar.organization=automatization-bot sonar.javascript.lcov.reportPaths=workers/main/coverage/lcov.info sonar.exclusions=**/*.test.ts,**/src/dist/** diff --git a/workers/main/src/configs/schedules.ts b/workers/main/src/configs/schedules.ts new file mode 100644 index 0000000..c47b4d7 --- /dev/null +++ b/workers/main/src/configs/schedules.ts @@ -0,0 +1,116 @@ +import { Client } from '@temporalio/client'; + +import { logger } from '../logger'; +import { workerConfig } from './worker'; + +const SCHEDULE_ID = 'weekly-financial-report-schedule'; + +/** + * Checks if an error is a "not found" error + */ +function validateIsScheduleNotFoundError(error: unknown): boolean { + return ( + (error as { code?: number }).code === 5 || + (error instanceof Error && + error.message.toLowerCase().includes('not found')) + ); +} + +/** + * Checks if schedule exists, returns true if it exists + */ +async function validateScheduleExists(client: Client): Promise { + try { + const scheduleHandle = client.schedule.getHandle(SCHEDULE_ID); + + await scheduleHandle.describe(); + logger.info(`Schedule ${SCHEDULE_ID} already exists, skipping creation`); + + return true; + } catch (error) { + if (!validateIsScheduleNotFoundError(error)) { + throw error; + } + logger.info(`Schedule ${SCHEDULE_ID} not found, creating new schedule`); + + return false; + } +} + +/** + * Creates schedule with race condition protection + */ +async function createScheduleWithRaceProtection(client: Client): Promise { + try { + await client.schedule.create({ + scheduleId: SCHEDULE_ID, + spec: { + cronExpressions: ['0 13 * * 2'], + timezone: 'America/New_York', + }, + action: { + type: 'startWorkflow', + workflowType: 'weeklyFinancialReportsWorkflow', + taskQueue: workerConfig.taskQueue, + workflowId: `weekly-financial-report-scheduled`, + }, + policies: { + overlap: 'SKIP', + catchupWindow: '1 day', + }, + }); + + logger.info( + `Successfully created schedule ${SCHEDULE_ID} for weekly financial reports`, + ); + } catch (createError) { + // Handle race condition: schedule was created by another worker + const isAlreadyExists = + (createError as { code?: number }).code === 6 || + (createError instanceof Error && + (createError.message.toLowerCase().includes('already exists') || + createError.message.toLowerCase().includes('already running'))); + + if (isAlreadyExists) { + logger.info( + `Schedule ${SCHEDULE_ID} already exists (created by another worker), treating as success`, + ); + + return; + } + + throw createError; + } +} + +/** + * Sets up the weekly financial report schedule + * Schedule runs every Tuesday at 1 PM America/New_York time (EST/EDT) + * @param client - Temporal client instance + */ +export async function setupWeeklyReportSchedule(client: Client): Promise { + try { + const isScheduleExists = await validateScheduleExists(client); + + if (isScheduleExists) { + return; + } + + await createScheduleWithRaceProtection(client); + } catch (error) { + logger.error( + `Failed to setup schedule ${SCHEDULE_ID}: ${error instanceof Error ? error.message : String(error)}`, + ); + throw error; + } +} + +/** + * Schedule configuration exported for documentation and testing + */ +export const scheduleConfig = { + scheduleId: SCHEDULE_ID, + cronExpression: '0 13 * * 2', + timezone: 'America/New_York', + description: 'Runs every Tuesday at 1 PM EST/EDT', +} as const; diff --git a/workers/main/src/configs/temporal.ts b/workers/main/src/configs/temporal.ts index 5058a0d..6f4967d 100644 --- a/workers/main/src/configs/temporal.ts +++ b/workers/main/src/configs/temporal.ts @@ -3,6 +3,10 @@ import { z } from 'zod'; const DEFAULT_TEMPORAL_ADDRESS = 'temporal:7233'; +/** + * Temporal connection configuration + * Used by both workers and clients to connect to Temporal server + */ export const temporalConfig: NativeConnectionOptions = { address: process.env.TEMPORAL_ADDRESS || DEFAULT_TEMPORAL_ADDRESS, }; @@ -10,3 +14,15 @@ export const temporalConfig: NativeConnectionOptions = { export const temporalSchema = z.object({ TEMPORAL_ADDRESS: z.string().default(DEFAULT_TEMPORAL_ADDRESS), }); + +/** + * Schedule Configuration Documentation + * + * Weekly Financial Report Schedule: + * The schedule is automatically created/verified when the worker starts. + * + * For schedule configuration details (schedule ID, cron expression, timezone, etc.), + * see the exported `scheduleConfig` object in ./schedules.ts + * + * Implementation: ./schedules.ts + */ diff --git a/workers/main/src/index.test.ts b/workers/main/src/index.test.ts index 4f5af83..c22d686 100644 --- a/workers/main/src/index.test.ts +++ b/workers/main/src/index.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { handleRunError, logger } from './index'; +import { handleRunError } from './index'; +import { logger } from './logger'; describe('handleRunError', () => { let processExitSpy: ReturnType; diff --git a/workers/main/src/index.ts b/workers/main/src/index.ts index 5aa8523..1124d8f 100644 --- a/workers/main/src/index.ts +++ b/workers/main/src/index.ts @@ -1,11 +1,12 @@ -import { DefaultLogger, NativeConnection, Worker } from '@temporalio/worker'; +import { Client, Connection } from '@temporalio/client'; +import { NativeConnection, Worker } from '@temporalio/worker'; import * as activities from './activities'; import { validateEnv } from './common/utils'; +import { setupWeeklyReportSchedule } from './configs/schedules'; import { temporalConfig } from './configs/temporal'; import { workerConfig } from './configs/worker'; - -export const logger = new DefaultLogger('ERROR'); +import { logger } from './logger'; validateEnv(); @@ -25,6 +26,22 @@ export async function createWorker(connection: NativeConnection) { } export async function run(): Promise { + // Setup weekly report schedule before starting worker + const clientConnection = await Connection.connect(temporalConfig); + + try { + const client = new Client({ connection: clientConnection }); + + await setupWeeklyReportSchedule(client); + } catch (err) { + logger.error( + `Failed to setup schedule: ${err instanceof Error ? err.message : String(err)}`, + ); + } finally { + await clientConnection.close(); + } + + // Create and run worker const connection = await createConnection(); try { diff --git a/workers/main/src/logger.ts b/workers/main/src/logger.ts new file mode 100644 index 0000000..fd3afbd --- /dev/null +++ b/workers/main/src/logger.ts @@ -0,0 +1,8 @@ +import { DefaultLogger } from '@temporalio/worker'; + +/** + * Shared logger instance for the worker + * Using INFO level to capture important operational messages + * including schedule setup, errors, and warnings + */ +export const logger = new DefaultLogger('INFO');