From fa8b574855cede0969fe5fe9ed3700607e29491c Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 24 Feb 2026 13:20:59 -0500 Subject: [PATCH 1/6] Add zod dependency to package.json and package-lock.json --- package-lock.json | 4 ++-- package.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5fcb69e..2aa550d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,8 @@ "react-router": "5.3.4", "react-router-dom": "5.3.4", "uuid": "13.0.0", - "yup": "1.7.1" + "yup": "1.7.1", + "zod": "4.3.6" }, "devDependencies": { "@capacitor/cli": "8.1.0", @@ -14670,7 +14671,6 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 112cbd7..14789fa 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,8 @@ "react-router": "5.3.4", "react-router-dom": "5.3.4", "uuid": "13.0.0", - "yup": "1.7.1" + "yup": "1.7.1", + "zod": "4.3.6" }, "devDependencies": { "@capacitor/cli": "8.1.0", From dcc5874e6b0abe4d7080645d0aba7dfc20e88e06 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 24 Feb 2026 13:21:09 -0500 Subject: [PATCH 2/6] Add Zod schema validation for application configuration --- src/common/utils/__tests__/config.test.ts | 406 ++++++++++++++++++++++ src/common/utils/config.ts | 71 ++++ 2 files changed, 477 insertions(+) create mode 100644 src/common/utils/__tests__/config.test.ts create mode 100644 src/common/utils/config.ts diff --git a/src/common/utils/__tests__/config.test.ts b/src/common/utils/__tests__/config.test.ts new file mode 100644 index 0000000..13b32af --- /dev/null +++ b/src/common/utils/__tests__/config.test.ts @@ -0,0 +1,406 @@ +import { describe, expect, it } from 'vitest'; + +import { configSchema } from '../config'; + +describe('config', () => { + describe('configSchema validation', () => { + it('should validate environment object with all valid values', () => { + // ARRANGE + const validEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(validEnv); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.VITE_BUILD_COMMIT_SHA).toBe('abc123def456'); + expect(result.data.VITE_BUILD_ENV_CODE).toBe('dev'); + expect(result.data.VITE_BASE_URL_API).toBe('https://api.example.com'); + expect(result.data.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe(5000); + } + }); + + it('should fail validation when required environment variables are missing', () => { + // ARRANGE + const incompleteEnv = { + VITE_BUILD_DATE: '2026-02-11', + // Missing other required variables + }; + + // ACT + const result = configSchema.safeParse(incompleteEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_BUILD_DATE is invalid', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: 'invalid-date', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_BUILD_TIME is invalid', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: 'invalid-time', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_BUILD_TS is invalid datetime', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: 'invalid-datetime', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_BASE_URL_API is not a valid URL', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'not-a-valid-url', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should coerce string numbers to integers', () => { + // ARRANGE + const envWithStringNumbers = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: '99', + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: '3', + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: '3000', + }; + + // ACT + const result = configSchema.safeParse(envWithStringNumbers); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(typeof result.data.VITE_BUILD_WORKFLOW_RUN_NUMBER).toBe('number'); + expect(result.data.VITE_BUILD_WORKFLOW_RUN_NUMBER).toBe(99); + expect(typeof result.data.VITE_BUILD_WORKFLOW_RUN_ATTEMPT).toBe('number'); + expect(result.data.VITE_BUILD_WORKFLOW_RUN_ATTEMPT).toBe(3); + expect(typeof result.data.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe('number'); + expect(result.data.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe(3000); + } + }); + + it('should use default value for VITE_TOAST_AUTO_DISMISS_MILLIS when not provided', () => { + // ARRANGE + const envWithoutToastDefault = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + // VITE_TOAST_AUTO_DISMISS_MILLIS is not provided + }; + + // ACT + const result = configSchema.safeParse(envWithoutToastDefault); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe(5000); + } + }); + + it('should fail validation when VITE_BUILD_WORKFLOW_RUN_NUMBER is negative', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: -1, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_BUILD_WORKFLOW_RUN_ATTEMPT is negative', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: -5, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should fail validation when VITE_TOAST_AUTO_DISMISS_MILLIS is negative', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: -1000, + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + }); + + it('should validate all required properties are present in parsed config', () => { + // ARRANGE + const validEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(validEnv); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toHaveProperty('VITE_BUILD_DATE'); + expect(result.data).toHaveProperty('VITE_BUILD_TIME'); + expect(result.data).toHaveProperty('VITE_BUILD_TS'); + expect(result.data).toHaveProperty('VITE_BUILD_COMMIT_SHA'); + expect(result.data).toHaveProperty('VITE_BUILD_ENV_CODE'); + expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_NAME'); + expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_RUN_NUMBER'); + expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_RUN_ATTEMPT'); + expect(result.data).toHaveProperty('VITE_BASE_URL_API'); + expect(result.data).toHaveProperty('VITE_TOAST_AUTO_DISMISS_MILLIS'); + } + }); + + it('should provide validation errors for invalid inputs', () => { + // ARRANGE + const invalidEnv = { + VITE_BUILD_DATE: 'invalid', + VITE_BUILD_TIME: 'invalid', + VITE_BUILD_TS: 'invalid', + VITE_BUILD_COMMIT_SHA: '', + VITE_BUILD_ENV_CODE: '', + VITE_BUILD_WORKFLOW_NAME: '', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 'not-a-number', + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 'not-a-number', + VITE_BASE_URL_API: 'invalid-url', + VITE_TOAST_AUTO_DISMISS_MILLIS: 'not-a-number', + }; + + // ACT + const result = configSchema.safeParse(invalidEnv); + + // ASSERT + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues.length).toBeGreaterThan(0); + } + }); + + it('should accept valid ISO 8601 datetime format with timezone', () => { + // ARRANGE + const validEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(validEnv); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.VITE_BUILD_TS).toBe('2026-02-11T14:30:00Z'); + } + }); + + it('should validate workflow run number and attempt are non-negative integers', () => { + // ARRANGE + const validEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 0, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 0, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 0, + }; + + // ACT + const result = configSchema.safeParse(validEnv); + + // ASSERT + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.VITE_BUILD_WORKFLOW_RUN_NUMBER).toBe(0); + expect(result.data.VITE_BUILD_WORKFLOW_RUN_ATTEMPT).toBe(0); + expect(result.data.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe(0); + } + }); + }); + + describe('Config type', () => { + it('should have correct type structure', () => { + // ARRANGE + const validEnv = { + VITE_BUILD_DATE: '2026-02-11', + VITE_BUILD_TIME: '14:30:00', + VITE_BUILD_TS: '2026-02-11T14:30:00Z', + VITE_BUILD_COMMIT_SHA: 'abc123def456', + VITE_BUILD_ENV_CODE: 'dev', + VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, + VITE_BASE_URL_API: 'https://api.example.com', + VITE_TOAST_AUTO_DISMISS_MILLIS: 5000, + }; + + // ACT + const result = configSchema.safeParse(validEnv); + + // ASSERT - Verify that the type matches the expected structure + expect(result.success).toBe(true); + if (result.success) { + const config = result.data; + expect(typeof config.VITE_BUILD_DATE).toBe('string'); + expect(typeof config.VITE_BUILD_TIME).toBe('string'); + expect(typeof config.VITE_BUILD_TS).toBe('string'); + expect(typeof config.VITE_BUILD_COMMIT_SHA).toBe('string'); + expect(typeof config.VITE_BUILD_ENV_CODE).toBe('string'); + expect(typeof config.VITE_BUILD_WORKFLOW_NAME).toBe('string'); + expect(typeof config.VITE_BUILD_WORKFLOW_RUN_NUMBER).toBe('number'); + expect(typeof config.VITE_BUILD_WORKFLOW_RUN_ATTEMPT).toBe('number'); + expect(typeof config.VITE_BASE_URL_API).toBe('string'); + expect(typeof config.VITE_TOAST_AUTO_DISMISS_MILLIS).toBe('number'); + } + }); + }); +}); diff --git a/src/common/utils/config.ts b/src/common/utils/config.ts new file mode 100644 index 0000000..7adf7f5 --- /dev/null +++ b/src/common/utils/config.ts @@ -0,0 +1,71 @@ +/** + * Application configuration utility using Zod schema validation. + * Validates and exports environment variables as a type-safe config object. + */ + +import { z } from 'zod'; + +/** + * Zod schema for environment variables validation. + * All environment variables must be prefixed with VITE_ for Vite compatibility. + * @see https://vite.dev/guide/env-and-mode + */ +export const configSchema = z.object({ + /** Build configuration */ + VITE_BUILD_DATE: z.iso.date().describe('The date and time when the application was built'), + VITE_BUILD_TIME: z.iso.time().describe('The time when the application was built'), + VITE_BUILD_TS: z.iso.datetime().describe('The timestamp when the application was built'), + VITE_BUILD_COMMIT_SHA: z.string().describe('The Git commit SHA of the build'), + VITE_BUILD_ENV_CODE: z.string().describe('The environment code for the build (e.g., local, dev, qa, prd)'), + VITE_BUILD_WORKFLOW_NAME: z.string().describe('The name of the CI/CD workflow that produced the build'), + VITE_BUILD_WORKFLOW_RUN_NUMBER: z.coerce + .number() + .int() + .nonnegative() + .describe('The run number of the CI/CD workflow that produced the build'), + VITE_BUILD_WORKFLOW_RUN_ATTEMPT: z.coerce + .number() + .int() + .nonnegative() + .describe('The attempt number of the CI/CD workflow run that produced the build'), + /** API configuration */ + VITE_BASE_URL_API: z.url().describe('Base URL for REST API service'), + /** Application configuration */ + VITE_TOAST_AUTO_DISMISS_MILLIS: z.coerce + .number() + .int() + .nonnegative() + .default(5000) + .describe('Duration in milliseconds before toast notifications auto-dismiss'), +}); + +/** + * Type inference for the configuration object + */ +export type Config = z.infer; + +/** + * Parse and validate environment variables + */ +const parseConfig = (): Config => { + console.debug('Parsing environment variables with Zod schema validation...'); + try { + // Parse environment variables using Zod schema + const parsed = configSchema.parse(import.meta.env); + + return parsed; + } catch (error) { + if (error instanceof z.ZodError) { + const validationIssues = error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '); + console.error(`Environment variable validation failed with the following issues: ${validationIssues}`); + throw new Error(`Invalid configuration. Details: ${validationIssues}`); + } + throw error; + } +}; + +/** + * Validated and type-safe configuration object. + * This is the main export that should be used throughout the application. + */ +export const config = parseConfig(); From a3aaa53934fcaa60cf57443113953061e8bf643d Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 24 Feb 2026 13:21:19 -0500 Subject: [PATCH 3/6] Update environment configuration scripts to use UTC for build timestamps --- .github/workflows/deploy-dev.yml | 6 +++--- .github/workflows/deploy-prod.yml | 6 +++--- .github/workflows/deploy-qa.yml | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index b6826af..62fa428 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -48,9 +48,9 @@ jobs: - name: Create Environment Configuration run: | echo "${{ env.ENV_FILE }}" > .env - echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env - echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env - echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env + echo "VITE_BUILD_DATE=$(date -u +'%Y-%m-%d')" >> .env + echo "VITE_BUILD_TIME=$(date -u +'%H:%M:%S')" >> .env + echo "VITE_BUILD_TS=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> .env echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env echo "VITE_BUILD_WORKFLOW_RUNNER=GitHub Actions" >> .env diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 1b552f2..22fe9b4 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -48,9 +48,9 @@ jobs: - name: Create Environment Configuration run: | echo "${{ env.ENV_FILE }}" > .env - echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env - echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env - echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env + echo "VITE_BUILD_DATE=$(date -u +'%Y-%m-%d')" >> .env + echo "VITE_BUILD_TIME=$(date -u +'%H:%M:%S')" >> .env + echo "VITE_BUILD_TS=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> .env echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env echo "VITE_BUILD_WORKFLOW_RUNNER=GitHub Actions" >> .env diff --git a/.github/workflows/deploy-qa.yml b/.github/workflows/deploy-qa.yml index 308d0b2..917b314 100644 --- a/.github/workflows/deploy-qa.yml +++ b/.github/workflows/deploy-qa.yml @@ -47,9 +47,9 @@ jobs: - name: Create Environment Configuration run: | echo "${{ env.ENV_FILE }}" > .env - echo "VITE_BUILD_DATE=$(date +'%Y-%m-%d')" >> .env - echo "VITE_BUILD_TIME=$(date +'%H:%M:%S%z')" >> .env - echo "VITE_BUILD_TS=$(date +'%Y-%m-%dT%H:%M:%S%z')" >> .env + echo "VITE_BUILD_DATE=$(date -u +'%Y-%m-%d')" >> .env + echo "VITE_BUILD_TIME=$(date -u +'%H:%M:%S')" >> .env + echo "VITE_BUILD_TS=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> .env echo "VITE_BUILD_COMMIT_SHA=${{ github.sha }}" >> .env echo "VITE_BUILD_ENV_CODE=${{ env.AWS_ENV_CODE }}" >> .env echo "VITE_BUILD_WORKFLOW_RUNNER=GitHub Actions" >> .env From 217fbdc0acd0578ead204e6702e7277f06b42fbf Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 24 Feb 2026 13:33:43 -0500 Subject: [PATCH 4/6] Refactor application to remove ConfigContext and related components, replacing usage with Zod schema validation for configuration management --- src/App.tsx | 29 ++++---- src/common/hooks/__tests__/useConfig.test.ts | 23 ------- src/common/hooks/useConfig.ts | 16 ----- src/common/providers/ConfigContext.ts | 23 ------- src/common/providers/ConfigProvider.tsx | 66 ------------------- .../__tests__/ConfigProvider.test.tsx | 46 ------------- src/pages/Account/AccountPage.tsx | 3 +- .../Diagnostics/BuildDiagnostics.tsx | 3 +- src/pages/Auth/SignIn/api/useSignIn.ts | 3 +- src/pages/Users/api/useCreateUser.ts | 3 +- src/pages/Users/api/useDeleteUser.ts | 3 +- src/pages/Users/api/useGetUser.ts | 3 +- src/pages/Users/api/useGetUsers.ts | 3 +- src/pages/Users/api/useUpdateUser.ts | 3 +- src/test/wrappers/WithAllProviders.tsx | 25 ++++--- 15 files changed, 32 insertions(+), 220 deletions(-) delete mode 100644 src/common/hooks/__tests__/useConfig.test.ts delete mode 100644 src/common/hooks/useConfig.ts delete mode 100644 src/common/providers/ConfigContext.ts delete mode 100644 src/common/providers/ConfigProvider.tsx delete mode 100644 src/common/providers/__tests__/ConfigProvider.test.tsx diff --git a/src/App.tsx b/src/App.tsx index b2cb6bc..ca6bbbd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,6 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { ErrorBoundary } from 'react-error-boundary'; import ErrorPage from 'common/components/Error/ErrorPage'; -import ConfigContextProvider from './common/providers/ConfigProvider'; import { queryClient } from 'common/utils/query-client'; import AuthProvider from 'common/providers/AuthProvider'; import AxiosProvider from 'common/providers/AxiosProvider'; @@ -25,21 +24,19 @@ setupIonicReact(); const App = () => ( - - - - - - - - - - - - - - - + + + + + + + + + + + + + ); diff --git a/src/common/hooks/__tests__/useConfig.test.ts b/src/common/hooks/__tests__/useConfig.test.ts deleted file mode 100644 index 6014ff3..0000000 --- a/src/common/hooks/__tests__/useConfig.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { renderHook as renderHookWithoutWrapper } from '@testing-library/react'; - -import { renderHook, waitFor } from 'test/test-utils'; - -import { useConfig } from '../useConfig'; - -describe('useConfig', () => { - it('should return the context', async () => { - // ARRANGE - const { result } = renderHook(() => useConfig()); - await waitFor(() => expect(result.current).not.toBeNull()); - - // ASSERT - expect(result.current).toBeDefined(); - expect(result.current.VITE_BUILD_ENV_CODE).toBe('test'); - }); - - it('should throw error when not within provider', () => { - // ASSERT - expect(() => renderHookWithoutWrapper(() => useConfig())).toThrow(/hook must be used within/); - }); -}); diff --git a/src/common/hooks/useConfig.ts b/src/common/hooks/useConfig.ts deleted file mode 100644 index 47841f0..0000000 --- a/src/common/hooks/useConfig.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useContext } from 'react'; - -import { Config, ConfigContext } from 'common/providers/ConfigContext'; - -/** - * The `useConfig` hook returns the current `ConfigContext` value. - * @returns {Config} The current `ConfigContext` value, `Config`. - */ -export const useConfig = (): Config => { - const context = useContext(ConfigContext); - if (!context) { - throw new Error('useConfig hook must be used within a ConfigContextProvider'); - } - - return context; -}; diff --git a/src/common/providers/ConfigContext.ts b/src/common/providers/ConfigContext.ts deleted file mode 100644 index 1614fbd..0000000 --- a/src/common/providers/ConfigContext.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createContext } from 'react'; - -/** - * The application configuration. The `value` provided by the `ConfigContext`. - */ -export interface Config { - VITE_BASE_URL_API: string; - VITE_BUILD_DATE: string; - VITE_BUILD_TIME: string; - VITE_BUILD_TS: string; - VITE_BUILD_COMMIT_SHA: string; - VITE_BUILD_ENV_CODE: string; - VITE_BUILD_WORKFLOW_RUNNER: string; - VITE_BUILD_WORKFLOW_NAME: string; - VITE_BUILD_WORKFLOW_RUN_NUMBER: number; - VITE_BUILD_WORKFLOW_RUN_ATTEMPT: number; - VITE_TOAST_AUTO_DISMISS_MILLIS: number; -} - -/** - * The `ConfigContext` instance. - */ -export const ConfigContext = createContext(undefined); diff --git a/src/common/providers/ConfigProvider.tsx b/src/common/providers/ConfigProvider.tsx deleted file mode 100644 index 39ff435..0000000 --- a/src/common/providers/ConfigProvider.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { PropsWithChildren, useEffect, useState } from 'react'; -import { ObjectSchema, ValidationError, number, object, string } from 'yup'; -import { useTranslation } from 'react-i18next'; - -import { Config, ConfigContext } from './ConfigContext'; - -/** - * The `ConfigContextProvider` React component creates, maintains, and provides - * access to the `ConfigContext` value. - * Validates the React application configuration values from `import.meta.env`. - * Throws an `Error` when the configuration is invalid, preventing application - * startup. - * @param {PropsWithChildren} props - Component properties, `PropsWithChildren`. - * @returns {JSX.Element} JSX - */ -const ConfigContextProvider = ({ children }: PropsWithChildren) => { - const { t } = useTranslation(); - const [isReady, setIsReady] = useState(false); - const [config, setConfig] = useState(); - - /** - * The configuration validation schema. - * @see {@link https://github.com/jquense/yup | Yup} - */ - const configSchema: ObjectSchema = object({ - VITE_BASE_URL_API: string() - .url() - .required(({ path }) => t('validation.required-path', { path })), - VITE_BUILD_DATE: string().default('1970-01-01'), - VITE_BUILD_TIME: string().default('00:00:00'), - VITE_BUILD_TS: string().default('1970-01-01T00:00:00+0000'), - VITE_BUILD_COMMIT_SHA: string().default('local'), - VITE_BUILD_ENV_CODE: string().default('local'), - VITE_BUILD_WORKFLOW_RUNNER: string().default('local'), - VITE_BUILD_WORKFLOW_NAME: string().default('local'), - VITE_BUILD_WORKFLOW_RUN_NUMBER: number().default(1), - VITE_BUILD_WORKFLOW_RUN_ATTEMPT: number().default(-1), - VITE_TOAST_AUTO_DISMISS_MILLIS: number().default(5000), - }); - - useEffect(() => { - try { - const validatedConfig = configSchema.validateSync(import.meta.env, { - abortEarly: false, - stripUnknown: true, - }); - setConfig(validatedConfig); - setIsReady(true); - } catch (err) { - if (err instanceof ValidationError) { - throw new Error( - `${t('error-configuration-validation')}. ${err.errors.reduce((msg, error) => `${msg} ${error}`)}`, - ); - } - if (err instanceof Error) throw new Error(`${t('error-configuration')}. ${err.message}`); - throw err; - } - - // disabling eslint rule because hook is only called on mount and unmount, and we don't want to add dependencies that would cause it to run more than once - /* eslint-disable react-hooks/exhaustive-deps */ - }, []); - - return {isReady && <>{children}}; -}; - -export default ConfigContextProvider; diff --git a/src/common/providers/__tests__/ConfigProvider.test.tsx b/src/common/providers/__tests__/ConfigProvider.test.tsx deleted file mode 100644 index c993022..0000000 --- a/src/common/providers/__tests__/ConfigProvider.test.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; - -import { render, screen } from 'test/test-utils'; - -import ConfigContextProvider from '../ConfigProvider'; - -describe('ConfigProvider', () => { - it('should render successfully', async () => { - // ARRANGE - render( - -
-
, - ); - await screen.findByTestId('provider-config'); - - // ASSERT - expect(screen.getByTestId('provider-config')).toBeDefined(); - }); -}); - -describe.skip('ConfigProvider error', () => { - const originalEnv = process.env; - - beforeAll(() => { - process.env = { NODE_ENV: 'test', PUBLIC_URL: 'localhost' }; - }); - - afterAll(() => { - process.env = originalEnv; - }); - - it('should throw configuration validation error', () => { - // ARRANGE - function renderContextProvider() { - render( - -
-
, - ); - } - - // ASSERT - expect(renderContextProvider).toThrow(/is a required field/); - }); -}); diff --git a/src/pages/Account/AccountPage.tsx b/src/pages/Account/AccountPage.tsx index 87fd8f9..9b01409 100644 --- a/src/pages/Account/AccountPage.tsx +++ b/src/pages/Account/AccountPage.tsx @@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { PropsWithTestId } from 'common/components/types'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import ProgressProvider from 'common/providers/ProgressProvider'; import Header from 'common/components/Header/Header'; import SettingsForm from './components/Settings/SettingsForm'; @@ -29,7 +29,6 @@ import List from 'common/components/List/List'; */ const AccountPage = ({ testid = 'page-account' }: PropsWithTestId) => { const [diagnosticsCount, setDiagnosticsCount] = useState(0); - const config = useConfig(); const router = useIonRouter(); const { t } = useTranslation(); diff --git a/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx b/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx index 7276b11..54bd389 100644 --- a/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx +++ b/src/pages/Account/components/Diagnostics/BuildDiagnostics.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next'; import { BaseComponentProps } from 'common/components/types'; import List from 'common/components/List/List'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; /** * The `BuildDiagnostics` component displays application diagnostic information @@ -16,7 +16,6 @@ import { useConfig } from 'common/hooks/useConfig'; * @returns {JSX.Element} JSX */ const BuildDiagnostics = ({ className, testid = 'diagnostics-build' }: BaseComponentProps) => { - const config = useConfig(); const { t } = useTranslation(); return ( diff --git a/src/pages/Auth/SignIn/api/useSignIn.ts b/src/pages/Auth/SignIn/api/useSignIn.ts index 7ff8bdd..0132324 100644 --- a/src/pages/Auth/SignIn/api/useSignIn.ts +++ b/src/pages/Auth/SignIn/api/useSignIn.ts @@ -7,7 +7,7 @@ import { UserTokens } from 'common/models/auth'; import { QueryKey, StorageKey } from 'common/utils/constants'; import storage from 'common/utils/storage'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; /** * An API hook which authenticates a `User`. @@ -16,7 +16,6 @@ import { useConfig } from 'common/hooks/useConfig'; export const useSignIn = () => { const queryClient = useQueryClient(); const axios = useAxios(); - const config = useConfig(); /** * Authenticates a user. diff --git a/src/pages/Users/api/useCreateUser.ts b/src/pages/Users/api/useCreateUser.ts index e04cb27..9f38167 100644 --- a/src/pages/Users/api/useCreateUser.ts +++ b/src/pages/Users/api/useCreateUser.ts @@ -1,7 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import { User } from 'common/models/user'; import { QueryKey } from 'common/utils/constants'; @@ -28,7 +28,6 @@ export type CreateUserVariables = { export const useCreateUser = () => { const axios = useAxios(); const queryClient = useQueryClient(); - const config = useConfig(); /** * Create a `User`. diff --git a/src/pages/Users/api/useDeleteUser.ts b/src/pages/Users/api/useDeleteUser.ts index 611bd51..4eea828 100644 --- a/src/pages/Users/api/useDeleteUser.ts +++ b/src/pages/Users/api/useDeleteUser.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import reject from 'lodash/reject'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import { User } from 'common/models/user'; import { QueryKey } from 'common/utils/constants'; @@ -25,7 +25,6 @@ export type DeleteUserVariables = { export const useDeleteUser = () => { const axios = useAxios(); const queryClient = useQueryClient(); - const config = useConfig(); /** * Delete a `User`. diff --git a/src/pages/Users/api/useGetUser.ts b/src/pages/Users/api/useGetUser.ts index 36183e7..24219b8 100644 --- a/src/pages/Users/api/useGetUser.ts +++ b/src/pages/Users/api/useGetUser.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import { User } from 'common/models/user'; import { QueryKey } from 'common/utils/constants'; @@ -20,7 +20,6 @@ interface UseGetUserProps { */ export const useGetUser = ({ userId }: UseGetUserProps) => { const axios = useAxios(); - const config = useConfig(); const getUser = async (): Promise => { const response = await axios.request({ diff --git a/src/pages/Users/api/useGetUsers.ts b/src/pages/Users/api/useGetUsers.ts index 29c7920..952081c 100644 --- a/src/pages/Users/api/useGetUsers.ts +++ b/src/pages/Users/api/useGetUsers.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import { User } from 'common/models/user'; import { QueryKey } from 'common/utils/constants'; @@ -11,7 +11,6 @@ import { QueryKey } from 'common/utils/constants'; */ export const useGetUsers = () => { const axios = useAxios(); - const config = useConfig(); const getUsers = async (): Promise => { const response = await axios.request({ diff --git a/src/pages/Users/api/useUpdateUser.ts b/src/pages/Users/api/useUpdateUser.ts index 418bd1b..cf05474 100644 --- a/src/pages/Users/api/useUpdateUser.ts +++ b/src/pages/Users/api/useUpdateUser.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import reject from 'lodash/reject'; import { useAxios } from 'common/hooks/useAxios'; -import { useConfig } from 'common/hooks/useConfig'; +import { config } from 'common/utils/config'; import { User } from 'common/models/user'; import { QueryKey } from 'common/utils/constants'; @@ -25,7 +25,6 @@ export type UpdateUserVariables = { export const useUpdateUser = () => { const axios = useAxios(); const queryClient = useQueryClient(); - const config = useConfig(); /** * Update a `User`. diff --git a/src/test/wrappers/WithAllProviders.tsx b/src/test/wrappers/WithAllProviders.tsx index 6f48497..8bf857f 100644 --- a/src/test/wrappers/WithAllProviders.tsx +++ b/src/test/wrappers/WithAllProviders.tsx @@ -3,7 +3,6 @@ import { MemoryRouter } from 'react-router'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from 'test/query-client'; -import ConfigContextProvider from 'common/providers/ConfigProvider'; import ToastProvider from 'common/providers/ToastProvider'; import AxiosProvider from 'common/providers/AxiosProvider'; import AuthProvider from 'common/providers/AuthProvider'; @@ -11,19 +10,17 @@ import ScrollProvider from 'common/providers/ScrollProvider'; const WithAllProviders = ({ children }: PropsWithChildren) => { return ( - - - - - - - {children} - - - - - - + + + + + + {children} + + + + + ); }; From cc120fea867038ed552901fb0434444e9b73e6fc Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Tue, 24 Feb 2026 13:36:40 -0500 Subject: [PATCH 5/6] Add VITE_BUILD_WORKFLOW_RUNNER to config schema and tests --- src/common/utils/__tests__/config.test.ts | 17 +++++++++++++++++ src/common/utils/config.ts | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/common/utils/__tests__/config.test.ts b/src/common/utils/__tests__/config.test.ts index 13b32af..227c0d3 100644 --- a/src/common/utils/__tests__/config.test.ts +++ b/src/common/utils/__tests__/config.test.ts @@ -13,6 +13,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -55,6 +56,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -77,6 +79,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -99,6 +102,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -121,6 +125,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'not-a-valid-url', @@ -143,6 +148,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: '99', VITE_BUILD_WORKFLOW_RUN_ATTEMPT: '3', VITE_BASE_URL_API: 'https://api.example.com', @@ -173,6 +179,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -198,6 +205,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: -1, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -220,6 +228,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: -5, VITE_BASE_URL_API: 'https://api.example.com', @@ -242,6 +251,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -264,6 +274,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -282,6 +293,7 @@ describe('config', () => { expect(result.data).toHaveProperty('VITE_BUILD_COMMIT_SHA'); expect(result.data).toHaveProperty('VITE_BUILD_ENV_CODE'); expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_NAME'); + expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_RUNNER'); expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_RUN_NUMBER'); expect(result.data).toHaveProperty('VITE_BUILD_WORKFLOW_RUN_ATTEMPT'); expect(result.data).toHaveProperty('VITE_BASE_URL_API'); @@ -298,6 +310,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: '', VITE_BUILD_ENV_CODE: '', VITE_BUILD_WORKFLOW_NAME: '', + VITE_BUILD_WORKFLOW_RUNNER: '', VITE_BUILD_WORKFLOW_RUN_NUMBER: 'not-a-number', VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 'not-a-number', VITE_BASE_URL_API: 'invalid-url', @@ -323,6 +336,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -348,6 +362,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 0, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 0, VITE_BASE_URL_API: 'https://api.example.com', @@ -377,6 +392,7 @@ describe('config', () => { VITE_BUILD_COMMIT_SHA: 'abc123def456', VITE_BUILD_ENV_CODE: 'dev', VITE_BUILD_WORKFLOW_NAME: 'CI/CD Pipeline', + VITE_BUILD_WORKFLOW_RUNNER: 'GitHub Actions', VITE_BUILD_WORKFLOW_RUN_NUMBER: 42, VITE_BUILD_WORKFLOW_RUN_ATTEMPT: 1, VITE_BASE_URL_API: 'https://api.example.com', @@ -396,6 +412,7 @@ describe('config', () => { expect(typeof config.VITE_BUILD_COMMIT_SHA).toBe('string'); expect(typeof config.VITE_BUILD_ENV_CODE).toBe('string'); expect(typeof config.VITE_BUILD_WORKFLOW_NAME).toBe('string'); + expect(typeof config.VITE_BUILD_WORKFLOW_RUNNER).toBe('string'); expect(typeof config.VITE_BUILD_WORKFLOW_RUN_NUMBER).toBe('number'); expect(typeof config.VITE_BUILD_WORKFLOW_RUN_ATTEMPT).toBe('number'); expect(typeof config.VITE_BASE_URL_API).toBe('string'); diff --git a/src/common/utils/config.ts b/src/common/utils/config.ts index 7adf7f5..37d4907 100644 --- a/src/common/utils/config.ts +++ b/src/common/utils/config.ts @@ -18,6 +18,7 @@ export const configSchema = z.object({ VITE_BUILD_COMMIT_SHA: z.string().describe('The Git commit SHA of the build'), VITE_BUILD_ENV_CODE: z.string().describe('The environment code for the build (e.g., local, dev, qa, prd)'), VITE_BUILD_WORKFLOW_NAME: z.string().describe('The name of the CI/CD workflow that produced the build'), + VITE_BUILD_WORKFLOW_RUNNER: z.string().describe('The runner of the CI/CD workflow that produced the build'), VITE_BUILD_WORKFLOW_RUN_NUMBER: z.coerce .number() .int() @@ -48,7 +49,6 @@ export type Config = z.infer; * Parse and validate environment variables */ const parseConfig = (): Config => { - console.debug('Parsing environment variables with Zod schema validation...'); try { // Parse environment variables using Zod schema const parsed = configSchema.parse(import.meta.env); From 208c4d8ed6bfae629715008ce2daa4ab43ec9662 Mon Sep 17 00:00:00 2001 From: Matthew Warman Date: Wed, 25 Feb 2026 05:27:41 -0500 Subject: [PATCH 6/6] Add setup instructions for environment configuration in README.md --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 2c5b047..72e23f1 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,33 @@ The application is configured using Environment Variables. Because single-page a After project installation and before running the application locally, create the following `.env` files in the project base directory. Learn more in the official [Vite guide for environment variables and modes](https://vitejs.dev/guide/env-and-mode.html). +#### Setup + +1. **Copy the example configuration file:** + + ```bash + cp .env.example .env + ``` + +2. **Update variables for your environment:** + + ```env + VITE_BASE_URL_API=https://your-api.example.com + VITE_TOAST_AUTO_DISMISS_MILLIS=5000 + ``` + +3. **Build information** (typically set by CI/CD pipeline): + ```env + VITE_BUILD_DATE=2026-02-10 + VITE_BUILD_TIME=14:30:00 + VITE_BUILD_TS=2026-02-10T14:30:00Z + VITE_BUILD_COMMIT_SHA=abc123def456 + VITE_BUILD_ENV_CODE=dev + VITE_BUILD_WORKFLOW_NAME=Build + VITE_BUILD_WORKFLOW_RUN_NUMBER=42 + VITE_BUILD_WORKFLOW_RUN_ATTEMPT=1 + ``` + #### `.env.local` The `.env.local` configuration file provides the configuration values when the application is started on a developer's local machine.