diff --git a/.github/actions/deploy-to-vercel-preview/action.yml b/.github/actions/deploy-to-vercel-preview/action.yml index faa7907e9..535a7350f 100644 --- a/.github/actions/deploy-to-vercel-preview/action.yml +++ b/.github/actions/deploy-to-vercel-preview/action.yml @@ -25,6 +25,6 @@ runs: steps: - id: run name: Deploy - working-directory: ${{ github.action_path }} - run: python3 src/main.py + working-directory: ${{ github.workspace }} + run: python3 ${{ github.action_path }}/src/main.py shell: bash diff --git a/.github/actions/deploy-to-vercel-production/action.yml b/.github/actions/deploy-to-vercel-production/action.yml index 67f515307..55255bc15 100644 --- a/.github/actions/deploy-to-vercel-production/action.yml +++ b/.github/actions/deploy-to-vercel-production/action.yml @@ -25,6 +25,6 @@ runs: steps: - id: run name: Deploy - working-directory: ${{ github.action_path }} - run: python3 src/main.py + working-directory: ${{ github.workspace }} + run: python3 ${{ github.action_path }}/src/main.py shell: bash diff --git a/.github/actions/download-build-artifact/action.yml b/.github/actions/download-build-artifact/action.yml index 1288568cb..a6689cfc8 100644 --- a/.github/actions/download-build-artifact/action.yml +++ b/.github/actions/download-build-artifact/action.yml @@ -13,8 +13,8 @@ runs: using: composite steps: - name: Download and extract - working-directory: ${{ github.action_path }} - run: python3 src/main.py + working-directory: ${{ github.workspace }} + run: python3 ${{ github.action_path }}/src/main.py shell: bash env: INPUT_TOKEN: ${{ inputs.token }} diff --git a/.husky/prepare.js b/.husky/prepare.js index f89aca6a5..30416d9fa 100644 --- a/.husky/prepare.js +++ b/.husky/prepare.js @@ -8,30 +8,28 @@ import { existsSync } from 'node:fs' import { join } from 'node:path' import { execSync } from 'node:child_process' +import { isCI } from '../src/lib/config/environmentServer' const projectRoot = process.cwd() -const gitDirectory = join(projectRoot, '.git') - -const isCi = process.env.CI === '1' || process.env.CI === 'true' -const isProduction = process.env.NODE_ENV === 'production' -// In production installs, package managers commonly omit devDependencies. -// Husky lives in devDependencies, so running it would fail the install. -if (isCi || isProduction) { - console.warn(`✅ Skipping Husky install: CI=${String(process.env.CI ?? '')} NODE_ENV=${String(process.env.NODE_ENV ?? '')}`) +/** Don't need Husky in CI environment */ +if (isCI()) { + console.warn(`✅ Skipping Husky install in CI environment`) process.exit(0) } +const gitDirectory = join(projectRoot, '.git') + if (!existsSync(gitDirectory)) { - console.warn(`✅ Skipping Husky install: missing .git directory at ${gitDirectory}`) - process.exit(0) + console.error(`❌ Skipping Husky install: missing .git directory at ${gitDirectory}`) + process.exit(1) } const huskyBin = join(projectRoot, 'node_modules', '.bin', process.platform === 'win32' ? 'husky.cmd' : 'husky') if (!existsSync(huskyBin)) { - console.warn(`✅ Skipping Husky install: missing husky binary at ${huskyBin}`) - process.exit(0) + console.error(`❌ Skipping Husky install: missing husky binary at ${huskyBin}`) + process.exit(1) } try { diff --git a/_TODO.md b/_TODO.md index 992e8597a..bff952415 100644 --- a/_TODO.md +++ b/_TODO.md @@ -117,46 +117,3 @@ Add Upstash Search as a Vercel Marketplace Integration. ### "Add to Calendar" button Google Calendar, Apple Calendar, Microsoft Outlook and Teams, and generate iCal/ics files (for all other calendars and cases). - -## Troubleshooting deploy workflow issues - -`https://github.com/add2cal/add-to-calendar-button` -`https://add-to-calendar-button.com/` - -Here are the most plausible causes, ranked, with evidence from your repo: - -1. Composite action inputs aren't being exported to `INPUT_*` for `run:` steps (so `actions_toolkit` can't read them) - -- Your passing action `check-prerequisites-and-locate-build-artifact` manually exports inputs into env: (e.g. `INPUT_GITHUB_TOKEN: ${{ inputs.github-token }}`). - -- The failing action `download-build-artifact` (and also `create-github-deployment-preview`, `mark-deployment-in-progress`, `update-deployment-status`) does not export any `INPUT_*` env vars. - -- If the runner doesn't auto-populate `INPUT_*` for composite `run:` steps (or changed behavior), then `core.get_input(...)` will think the input is missing and throw exactly what you're seeing. - -2. `actions_toolkit` is looking for `INPUT_GITHUB_TOKEN` (underscore), but only `INPUT_GITHUB-TOKEN` (dash) exists (or vice versa) - -- The fact that action.yml explicitly sets `INPUT_GITHUB_TOKEN` is a big hint that at least one of your toolchains expects the underscore variant. - -- If the runner provides only the dashed form but the toolkit reads only the underscored form (or the opposite), you'll get "Input required and not supplied" even though the workflow shows the with: value. - -3. The deploy workflow is executing main's actions, not the branch you think you fixed - -- `deployment-preview.yml` checks out ref: main. For a `workflow_run` trigger, that makes it easy to end up running local actions from main even if you "fixed it" elsewhere previously. - -- Symptom: you "keep fixing" but the preview run keeps behaving like an older version. - -4. The `"with: github-token: ***"` line can be misleading if the expression resolves empty - -- `github.token` should exist, but edge cases (permissions mis-specified, workflow context differences, or job-level permission not applied the way you expect for `workflow_run`) can produce a token that's unusable or empty. - -- This would be rarer than (1)/(2), but it still manifests as "required input missing". - -5. The error text is from the library's normalization, not a literal requested key - -- You don't have any repo code that calls `get_input("GITHUB-TOKEN")` literally; everything calls `get_input_compat("github-token", required=True)`. - -- Many toolkits uppercase / normalize the input name when reporting it, so `GITHUB-TOKEN` in the error can still correspond to `github-token` as defined. - -If you want a single "most likely" call: based on the repo evidence, (1) is the top suspect — it explains why the prereq action works (it exports env) and the next composite Python action fails (it doesn't). - - diff --git a/src/actions/_environment/siteUrlActions.ts b/src/actions/_environment/siteUrlActions.ts index dbaea9613..7964be56e 100644 --- a/src/actions/_environment/siteUrlActions.ts +++ b/src/actions/_environment/siteUrlActions.ts @@ -1,14 +1,12 @@ -import packageJson from '../../../package.json' with { type: 'json' } import { isVercel } from './environmentActions' import { getOptionalEnv } from '@lib/config/environmentServer' const devServerPort = getOptionalEnv('DEV_SERVER_PORT')?.trim() const resolvedDevServerPort = devServerPort && devServerPort.length > 0 ? devServerPort : '4321' -const { domain } = packageJson export const getSiteUrl = (): string => { - if (isVercel() && domain) { - return `https://${domain}` + if (isVercel()) { + return `https://www.webstackbuilders.com` } return `http://localhost:${resolvedDevServerPort}` diff --git a/src/actions/_utils/rateLimit.ts b/src/actions/_utils/rateLimit.ts index 5424aa67f..ef78e8a30 100644 --- a/src/actions/_utils/rateLimit.ts +++ b/src/actions/_utils/rateLimit.ts @@ -1,5 +1,5 @@ import { isDbError } from 'astro:db' -import { isDev, isTest } from '@actions/_environment/environmentActions' +import { isProd } from '@actions/_environment/environmentActions' import { withRateLimitWindow } from '@actions/_utils/rateLimitStore' export type RateLimiter = { @@ -46,7 +46,7 @@ export async function checkRateLimit( } export function checkContactRateLimit(ipFingerprint: string): boolean { - if (isDev() || isTest()) { + if (!isProd()) { return true } @@ -77,7 +77,8 @@ async function applyRateLimit( config: RateLimiterConfig, identifier: string, ): Promise<{ success: boolean; reset: number | undefined }> { - if (isDev() || isTest()) { + /** Testing helper */ + if (!isProd()) { return { success: true, reset: Date.now() + config.windowMs, diff --git a/src/actions/contact/responder.ts b/src/actions/contact/responder.ts index f366ce1b7..63c9da44f 100644 --- a/src/actions/contact/responder.ts +++ b/src/actions/contact/responder.ts @@ -5,7 +5,7 @@ import { v4 as uuidv4, validate as uuidValidate } from 'uuid' import { ActionError, defineAction } from 'astro:actions' import { checkContactRateLimit } from '@actions/_utils/rateLimit' import { buildRequestFingerprint, createRateLimitIdentifier } from '@actions/_utils/requestContext' -import { getPrivacyPolicyVersion, getResendApiKey, isDev, isTest } from '@actions/_environment/environmentActions' +import { getPrivacyPolicyVersion, getResendApiKey, isProd } from '@actions/_environment/environmentActions' import { createConsentRecord } from '@actions/gdpr/domain/consentStore' type ContactFormData = { @@ -132,7 +132,7 @@ ${fields.join('\n')} } async function sendEmail(emailData: EmailData, files: FileAttachment[]): Promise { - if (isTest() || isDev()) { + if (!isProd()) { return } diff --git a/src/actions/gdpr/_dsarVerificationEmails.ts b/src/actions/gdpr/_dsarVerificationEmails.ts index 266b6609c..e96727e4f 100644 --- a/src/actions/gdpr/_dsarVerificationEmails.ts +++ b/src/actions/gdpr/_dsarVerificationEmails.ts @@ -1,7 +1,7 @@ import { Resend } from 'resend' import { dsarVerificationEmailHtml } from '@content/email/dsar.html' import { dsarVerificationEmailText } from '@content/email/dsar.text' -import { getResendApiKey, isDev, isTest } from '@actions/_environment/environmentActions' +import { getResendApiKey, isProd } from '@actions/_environment/environmentActions' import { getSiteUrl } from '@actions/_environment/siteUrlActions' import { ActionsFunctionError } from '@actions/_errors/ActionsFunctionError' @@ -10,7 +10,8 @@ export async function sendDsarVerificationEmail( token: string, requestType: 'ACCESS' | 'DELETE', ): Promise { - if (isDev() || isTest()) { + /** Testing helper */ + if (!isProd()) { console.log('[DEV/TEST MODE] DSAR verification email would be sent:', { email, token, requestType }) return } diff --git a/src/actions/newsletter/entities.ts b/src/actions/newsletter/entities.ts index a29a2d640..0a7fc238c 100644 --- a/src/actions/newsletter/entities.ts +++ b/src/actions/newsletter/entities.ts @@ -1,5 +1,5 @@ import { Resend } from 'resend' -import { getResendApiKey, isDev, isTest } from '@actions/_environment/environmentActions' +import { getResendApiKey, isProd } from '@actions/_environment/environmentActions' import { getSiteUrl } from '@actions/_environment/siteUrlActions' import { ActionsFunctionError } from '@actions/_errors/ActionsFunctionError' @@ -172,7 +172,8 @@ export async function sendConfirmationEmail(email: string, token: string, firstN const confirmUrl = `${siteUrl}/newsletter/confirm/${token}` const expiresIn = '24 hours' - if (isDev() || isTest()) { + /** Testing helper */ + if (!isProd()) { console.log('[DEV/TEST MODE] Newsletter confirmation email would be sent:', { email, token }) return } @@ -217,7 +218,8 @@ export async function sendConfirmationEmail(email: string, token: string, firstN } export async function sendWelcomeEmail(email: string, firstName?: string): Promise { - if (isDev() || isTest()) { + /** Testing helper */ + if (!isProd()) { console.log('[DEV/TEST MODE] Newsletter welcome email would be sent:', { email }) return } diff --git a/src/actions/newsletter/responder.ts b/src/actions/newsletter/responder.ts index 4d941d5db..3ac2dd11f 100644 --- a/src/actions/newsletter/responder.ts +++ b/src/actions/newsletter/responder.ts @@ -2,7 +2,7 @@ import emailValidator from 'email-validator' import { v4 as uuidv4, validate as uuidValidate } from 'uuid' import { ActionError, defineAction } from 'astro:actions' import { z } from 'astro:schema' -import { getConvertkitApiKey, getPrivacyPolicyVersion, isDev, isTest } from '@actions/_environment/environmentActions' +import { getConvertkitApiKey, getPrivacyPolicyVersion, isProd } from '@actions/_environment/environmentActions' import { checkRateLimit, rateLimiters } from '@actions/_utils/rateLimit' import { buildRequestFingerprint, createRateLimitIdentifier } from '@actions/_utils/requestContext' import { createConsentRecord, markConsentRecordsVerified } from '@actions/gdpr/domain/consentStore' @@ -55,7 +55,8 @@ function validateEmail(email: string): string { } export async function subscribeToConvertKit(data: NewsletterFormData): Promise { - if (isDev() || isTest()) { + /** Testing helper */ + if (!isProd()) { console.log('[DEV/TEST MODE] Newsletter subscription would be created:', { email: data.email }) return { subscriber: { diff --git a/src/components/Avatar/server/index.ts b/src/components/Avatar/server/index.ts index d149816fb..47b50b9af 100644 --- a/src/components/Avatar/server/index.ts +++ b/src/components/Avatar/server/index.ts @@ -15,7 +15,6 @@ */ import type { ImageMetadata } from 'astro' -import { isDev } from '@lib/config/environmentServer' import type { AvatarMap } from './@types' import { loadAvatarModules } from './avatarImports' @@ -76,7 +75,7 @@ class AvatarManagerClass { if (filename && imageData) { // Deep freeze the image metadata to prevent modifications map[filename] = Object.freeze({ ...imageData }) - } else if (isDev()) { + } else { console.warn(`[AvatarManager] Failed to process avatar at path: ${path}`) } } @@ -84,9 +83,7 @@ class AvatarManagerClass { // Freeze the map to prevent modifications this.avatarMap = Object.freeze(map) - if (isDev()) { - console.log(`[AvatarManager] Initialized with ${Object.keys(this.avatarMap).length} avatars`) - } + console.log(`[AvatarManager] Initialized with ${Object.keys(this.avatarMap).length} avatars`) } /** diff --git a/src/components/WebMentions/server/index.ts b/src/components/WebMentions/server/index.ts index ba4ef9e68..ca1ed1d89 100644 --- a/src/components/WebMentions/server/index.ts +++ b/src/components/WebMentions/server/index.ts @@ -5,14 +5,14 @@ * @see https://github.com/aaronpk/webmention.io */ import { WEBMENTION_IO_TOKEN } from 'astro:env/server' -import { isDev } from '@lib/config/environmentServer' +import { isProd } from '@lib/config/environmentServer' import type { Webmention, WebmentionResponse } from '@components/WebMentions/@types' import sanitizeHtml from 'sanitize-html' const allowedTypes = new Set(['mention-of', 'in-reply-to', 'like-of', 'repost-of']) const PLACEHOLDER_TOKENS = new Set(['', 'updateme', 'your_api_token_here']) const SUCCESS_CACHE_TTL_MS = 5 * 60 * 1000 -const FAILURE_RETRY_COOLDOWN_MS = isDev() ? 60 * 1000 : 5 * 60 * 1000 +const FAILURE_RETRY_COOLDOWN_MS = isProd() ? 5 * 60 * 1000 : 60 * 1000 const LOG_THROTTLE_WINDOW_MS = 60 * 1000 const FETCH_TIMEOUT_MS = 10_000 diff --git a/src/components/scripts/sentry/__tests__/helpers.spec.ts b/src/components/scripts/sentry/__tests__/helpers.spec.ts index e50290981..106d25f6f 100644 --- a/src/components/scripts/sentry/__tests__/helpers.spec.ts +++ b/src/components/scripts/sentry/__tests__/helpers.spec.ts @@ -5,13 +5,13 @@ const mockScope = { setContext: vi.fn(), } -const isDevMock = vi.hoisted(() => vi.fn(() => false)) +const isProdMock = vi.hoisted(() => vi.fn(() => true)) const getConsentSnapshotMock = vi.hoisted(() => vi.fn(() => ({ analytics: true, }))) vi.mock('@components/scripts/utils/environmentClient', () => ({ - isDev: isDevMock, + isProd: isProdMock, })) vi.mock('@components/scripts/store/consent', () => ({ @@ -36,14 +36,14 @@ const createHint = (): Parameters[1] => ({}) as Parame describe('sentry helpers', () => { beforeEach(() => { vi.clearAllMocks() - isDevMock.mockReturnValue(false) + isProdMock.mockReturnValue(false) getConsentSnapshotMock.mockReturnValue({ analytics: true }) consoleLogSpy.mockClear() }) describe('beforeSendHandler', () => { - it('skips sending events in development', () => { - isDevMock.mockReturnValue(true) + it('skips sending events outside prod', () => { + isProdMock.mockReturnValue(false) const event = createEvent() const result = beforeSendHandler(event, createHint()) @@ -53,6 +53,7 @@ describe('sentry helpers', () => { }) it('returns event unchanged when analytics consent exists', () => { + isProdMock.mockReturnValue(true) getConsentSnapshotMock.mockReturnValue({ analytics: true }) const event = createEvent() @@ -65,6 +66,7 @@ describe('sentry helpers', () => { }) it('scrubs PII when analytics consent is missing', () => { + isProdMock.mockReturnValue(true) getConsentSnapshotMock.mockReturnValue({ analytics: false }) const event = createEvent() diff --git a/src/components/scripts/sentry/helpers.ts b/src/components/scripts/sentry/helpers.ts index fbad23fa7..7e1ed5080 100644 --- a/src/components/scripts/sentry/helpers.ts +++ b/src/components/scripts/sentry/helpers.ts @@ -1,5 +1,5 @@ import { getCurrentScope, type BrowserOptions } from '@sentry/browser' -import { isDev } from '@components/scripts/utils/environmentClient' +import { isProd } from '@components/scripts/utils/environmentClient' import { getConsentSnapshot } from '@components/scripts/store/consent' type BeforeSendHandler = NonNullable @@ -8,7 +8,7 @@ type BeforeSendHandler = NonNullable * Applies consent-aware filtering to Sentry events before they are sent. */ export const beforeSendHandler: BeforeSendHandler = (event, _hint) => { - if (isDev()) { + if (!isProd()) { return null } @@ -31,7 +31,8 @@ export const beforeSendHandler: BeforeSendHandler = (event, _hint) => { } /** - * Sets Sentry scope context when consent changes to keep telemetry aligned with user preferences. + * Sets Sentry scope context when consent changes to keep + * telemetry aligned with user preferences. */ export function updateConsentContext(hasAnalyticsConsent: boolean): void { const scope = getCurrentScope() diff --git a/src/components/scripts/utils/__tests__/siteUrlClient.spec.ts b/src/components/scripts/utils/__tests__/siteUrlClient.spec.ts index 89ff50c2e..3ffd6d029 100644 --- a/src/components/scripts/utils/__tests__/siteUrlClient.spec.ts +++ b/src/components/scripts/utils/__tests__/siteUrlClient.spec.ts @@ -36,10 +36,8 @@ describe('getSiteUrl', () => { const url = getSiteUrl() - expect(url).toBe('https://demo.webstackbuilders.com') - expect(consoleSpy).toHaveBeenCalledWith( - 'Using production environment with domain from astro config: demo.webstackbuilders.com' - ) + expect(url).toBe('https://www.webstackbuilders.com') + expect(consoleSpy).not.toHaveBeenCalled() }) it('falls back to localhost when running locally or during E2E', () => { diff --git a/src/components/scripts/utils/environmentClient.ts b/src/components/scripts/utils/environmentClient.ts index 2e3360715..88a11b3b0 100644 --- a/src/components/scripts/utils/environmentClient.ts +++ b/src/components/scripts/utils/environmentClient.ts @@ -51,7 +51,7 @@ export const isDev = () => { } export const isProd = () => { - return import.meta.env.MODE === 'production' && !isUnitTest() + return import.meta.env.MODE === 'production' } /** diff --git a/src/components/scripts/utils/siteUrlClient.ts b/src/components/scripts/utils/siteUrlClient.ts index f195c10f6..6d262e052 100644 --- a/src/components/scripts/utils/siteUrlClient.ts +++ b/src/components/scripts/utils/siteUrlClient.ts @@ -7,10 +7,7 @@ import { isProd, isE2eTest } from '@components/scripts/utils/environmentClient' export const getSiteUrl = () => { if (isProd() && !isE2eTest()) { - console.log( - `Using production environment with domain from astro config: ${import.meta.env.SITE}` - ) - return `https://${import.meta.env.SITE}` + return `https://www.webstackbuilders.com` } else { console.log(`Using development environment on port ${DEV_SERVER_PORT}.`) return `http://localhost:${DEV_SERVER_PORT}` diff --git a/src/lib/config/__tests__/siteUrlServer.spec.ts b/src/lib/config/__tests__/siteUrlServer.spec.ts index 376c8398c..29dff102c 100644 --- a/src/lib/config/__tests__/siteUrlServer.spec.ts +++ b/src/lib/config/__tests__/siteUrlServer.spec.ts @@ -2,23 +2,13 @@ import { describe, it, expect, vi, afterEach } from 'vitest' const originalDevServerPort = process.env['DEV_SERVER_PORT'] -type ImportOptions = { - domain?: string | undefined -} - // Reload the module with a fresh environment snapshot and mocked dependencies. -const importSiteUrlServer = async (options: ImportOptions = {}) => { - const hasDomainOverride = Object.prototype.hasOwnProperty.call(options, 'domain') - const domain = hasDomainOverride ? options.domain : 'webstackbuilders.com' +const importSiteUrlServer = async () => { vi.resetModules() const isVercelMock = vi.fn(() => false) vi.doMock('../environmentServer', () => ({ isVercel: isVercelMock, })) - vi.doMock('../../../../package.json', () => ({ - domain, - default: { domain }, - })) const module = await import('../siteUrlServer') return { getSiteUrl: module.getSiteUrl, isVercelMock } } @@ -33,10 +23,16 @@ afterEach(() => { }) describe('getSiteUrl', () => { - it('returns the production domain when Vercel runtime is detected', async () => { + it('throws a BuildError when running on Vercel', async () => { const { getSiteUrl, isVercelMock } = await importSiteUrlServer() + const { BuildError } = await import('../../errors/BuildError') isVercelMock.mockReturnValue(true) - expect(getSiteUrl()).toBe('https://webstackbuilders.com') + + const invokeGetSiteUrl = () => getSiteUrl() + expect(invokeGetSiteUrl).toThrowError(BuildError) + expect(invokeGetSiteUrl).toThrowError( + '❌ Build runs on GitHub, so this build-time getSiteUrl() function should never be called on Vercel.' + ) }) it('uses the provided DEV_SERVER_PORT when not running on Vercel', async () => { @@ -59,14 +55,4 @@ describe('getSiteUrl', () => { isVercelMock.mockReturnValue(false) expect(getSiteUrl()).toBe('http://localhost:4321') }) - - it('throws a BuildError when running on Vercel without a configured domain', async () => { - const { getSiteUrl, isVercelMock } = await importSiteUrlServer({ domain: undefined }) - const { BuildError } = await import('@lib/errors/BuildError') - isVercelMock.mockReturnValue(true) - - const invokeGetSiteUrl = () => getSiteUrl() - expect(invokeGetSiteUrl).toThrowError(BuildError) - expect(invokeGetSiteUrl).toThrowError('Domain is required in package.json for Vercel production environment.') - }) }) diff --git a/src/lib/config/environmentServer.ts b/src/lib/config/environmentServer.ts index 261c54597..6f56ad7e5 100644 --- a/src/lib/config/environmentServer.ts +++ b/src/lib/config/environmentServer.ts @@ -26,11 +26,9 @@ export const isTest = () => { return isUnitTest() || isE2eTest() } -/** - * Production behaviors include functionality like using real API keys. - * The only environment we want that behavior is during the production - * build on Vercel. - */ +export const isCI = () => { + return isGitHub() || isVercel() +} export const isDev = () => { return !isVercel() @@ -64,10 +62,6 @@ export const isVercel = () => { return !!process.env['VERCEL'] } -export const isCI = () => { - return isGitHub() || isVercel() -} - /** * This method is only intended to be called from astro.config.ts * @throws {BuildError} If SENTRY_AUTH_TOKEN is not set diff --git a/src/lib/config/siteUrlServer.ts b/src/lib/config/siteUrlServer.ts index 7bc016003..682688864 100644 --- a/src/lib/config/siteUrlServer.ts +++ b/src/lib/config/siteUrlServer.ts @@ -1,22 +1,16 @@ /** * Server-side method to determine correct URL */ -import packageJson from '../../../package.json' with { type: 'json' } import { BuildError } from '../errors/BuildError' import { isVercel } from './environmentServer' -const { domain } = packageJson const devServerPort = process.env['DEV_SERVER_PORT']?.trim() const resolvedDevServerPort = devServerPort && devServerPort.length > 0 ? devServerPort : '4321' /** Called from astro.config.ts to determine "site" config key */ export const getSiteUrl = (): string => { - if (isVercel() && domain) { - return `https://${domain}` - } - - if (isVercel() && !domain) { - throw new BuildError('❌ Domain is required in package.json for Vercel production environment.') + if (isVercel()) { + throw new BuildError('❌ Build runs on GitHub, so this build-time getSiteUrl() function should never be called on Vercel.') } return `http://localhost:${resolvedDevServerPort}` diff --git a/src/lib/helpers/breadcrumbTitleLengthRefinement.ts b/src/lib/helpers/breadcrumbTitleLengthRefinement.ts index 3d5dade33..1755e6e8a 100644 --- a/src/lib/helpers/breadcrumbTitleLengthRefinement.ts +++ b/src/lib/helpers/breadcrumbTitleLengthRefinement.ts @@ -1,11 +1,9 @@ import { z } from 'astro:content' -import { isProd } from '@lib/config/environmentServer' const MAX_BREADCRUMB_TITLE_LENGTH = 55 const loggedBreadcrumbTitleWarnings = new Set() export const warnOnBreadcrumbTitleLength = (title: string, collectionName: string): void => { - if (!isProd()) return if (title.length <= MAX_BREADCRUMB_TITLE_LENGTH) return const warningKey = `${collectionName}:${title}` diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts index 8541f771e..89e9955ca 100644 --- a/src/lib/logger/index.ts +++ b/src/lib/logger/index.ts @@ -2,8 +2,8 @@ * Simple logger for development mode and testing * In production, errors are sent to Sentry instead */ -import { isDev, isTest } from '@lib/config/environmentServer' -const shouldLog = isDev() || isTest() +import { isProd } from '@lib/config/environmentServer' +const shouldLog = !isProd() export const logger = { /** diff --git a/src/pages/api/_utils/environment/environmentApi.ts b/src/pages/api/_utils/environment/environmentApi.ts index d12e58530..9aaf07dba 100644 --- a/src/pages/api/_utils/environment/environmentApi.ts +++ b/src/pages/api/_utils/environment/environmentApi.ts @@ -6,13 +6,11 @@ * functions with process.env. */ import { ApiFunctionError } from '../errors/ApiFunctionError' -import { isUnitTest } from '@lib/config/environmentServer' export { isCI, isE2eTest, isGitHub, isTest, - isUnitTest, isVercel, } from '@lib/config/environmentServer' @@ -27,7 +25,7 @@ export const isDev = () => { } export const isProd = () => { - return import.meta.env.MODE === 'production' && !isUnitTest() + return import.meta.env.MODE === 'production' } /** diff --git a/src/pages/api/_utils/environment/siteUrlApi.ts b/src/pages/api/_utils/environment/siteUrlApi.ts index daba5c732..d3a18005a 100644 --- a/src/pages/api/_utils/environment/siteUrlApi.ts +++ b/src/pages/api/_utils/environment/siteUrlApi.ts @@ -1,18 +1,15 @@ /** - * Server-side method to determine correct URL + * Method to determine correct site URL in serverless functions (API routes) */ -import packageJson from '../../../../../package.json' with { type: 'json' } import { isVercel } from './environmentApi' const devServerPort = process.env['DEV_SERVER_PORT']?.trim() const resolvedDevServerPort = devServerPort && devServerPort.length > 0 ? devServerPort : '4321' -const { domain } = packageJson -/** Called from astro.config.ts to determine "site" config key */ export const getSiteUrl = (): string => { - if (isVercel() && domain) { - return `https://${domain}` + if (isVercel()) { + return `https://www.webstackbuilders.com` } return `http://localhost:${resolvedDevServerPort}` diff --git a/src/pages/api/_utils/sentry/__tests__/index.spec.ts b/src/pages/api/_utils/sentry/__tests__/index.spec.ts index 1728a1e3a..71ee9a265 100644 --- a/src/pages/api/_utils/sentry/__tests__/index.spec.ts +++ b/src/pages/api/_utils/sentry/__tests__/index.spec.ts @@ -6,7 +6,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest' const sentryInitMock = vi.fn() const envMocks = vi.hoisted(() => ({ isProd: vi.fn(() => false), - isDev: vi.fn(() => false), getSentryDsn: vi.fn(() => 'https://public@example.ingest.sentry.io/1'), getPackageRelease: vi.fn(() => 'pkg@1.0.0'), })) @@ -22,9 +21,7 @@ describe('ensureApiSentry', () => { vi.resetModules() vi.clearAllMocks() envMocks.isProd.mockReset() - envMocks.isDev.mockReset() envMocks.isProd.mockReturnValue(false) - envMocks.isDev.mockReturnValue(false) }) it('skips initialization outside production', async () => { @@ -38,7 +35,6 @@ describe('ensureApiSentry', () => { it('initializes once when running in production', async () => { envMocks.isProd.mockReturnValue(true) - envMocks.isDev.mockReturnValue(false) const module = await import('@pages/api/_utils/sentry') @@ -54,17 +50,16 @@ describe('ensureApiSentry', () => { expect(sentryInitMock).toHaveBeenCalledTimes(1) }) - it('drops events when dev flag is true', async () => { + it('drops events when prod flag is false', async () => { envMocks.isProd.mockReturnValue(true) - envMocks.isDev.mockReturnValue(true) const module = await import('@pages/api/_utils/sentry') const config = sentryInitMock.mock.calls[0]![0] const event = {} - expect(config.beforeSend(event as any)).toBeNull() - envMocks.isDev.mockReturnValue(false) expect(config.beforeSend(event as any)).toBe(event) + envMocks.isProd.mockReturnValue(false) + expect(config.beforeSend(event as any)).toBeNull() module.ensureApiSentry() expect(sentryInitMock).toHaveBeenCalledTimes(1) diff --git a/src/pages/api/_utils/sentry/index.ts b/src/pages/api/_utils/sentry/index.ts index b72dabd65..6f284ca68 100644 --- a/src/pages/api/_utils/sentry/index.ts +++ b/src/pages/api/_utils/sentry/index.ts @@ -2,18 +2,13 @@ import { init as sentryInit } from '@sentry/astro' import { getPackageRelease, getSentryDsn, - isDev, isProd } from '@pages/api/_utils/environment' let initialized = false export function ensureApiSentry(): void { - if (initialized) { - return - } - - if (!isProd()) { + if (!isProd() || initialized) { return } @@ -26,7 +21,7 @@ export function ensureApiSentry(): void { attachStacktrace: true, maxBreadcrumbs: 100, beforeSend(event) { - if (isDev()) { + if (!isProd()) { return null } return event diff --git a/src/pages/articles/[...slug].astro b/src/pages/articles/[...slug].astro index 9bc594ab7..fb7754310 100644 --- a/src/pages/articles/[...slug].astro +++ b/src/pages/articles/[...slug].astro @@ -1,7 +1,7 @@ --- import { type CollectionEntry, getCollection, getEntry } from 'astro:content' import { Picture } from 'astro:assets' -import { isDev, isProd } from '@lib/config/environmentServer' +import { isDev } from '@lib/config/environmentServer' import MarkdownLayout from '@layouts/MarkdownLayout.astro' import Shares from '@components/Social/Shares/index.astro' import Carousel from '@components/Carousel/index.astro' @@ -14,7 +14,7 @@ export interface Props { /** Generate static paths for all articles */ export async function getStaticPaths() { const articleEntries = await getCollection('articles', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) return articleEntries.map(article => ({ params: { slug: article.id }, diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro index 10022329e..ccc21fcda 100644 --- a/src/pages/articles/index.astro +++ b/src/pages/articles/index.astro @@ -1,11 +1,11 @@ --- import { getCollection } from 'astro:content' import { Picture } from 'astro:assets' -import { isDev, isProd } from '@lib/config/environmentServer' +import { isDev } from '@lib/config/environmentServer' import PageLayout from '@layouts/PageLayout.astro' const allArticles = await getCollection('articles', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) /** Sort by publish date (newest first) */ diff --git a/src/pages/downloads/[...slug].astro b/src/pages/downloads/[...slug].astro index d5fc50e84..adc3db00d 100644 --- a/src/pages/downloads/[...slug].astro +++ b/src/pages/downloads/[...slug].astro @@ -1,7 +1,7 @@ --- import { type CollectionEntry, getCollection } from 'astro:content' import { Picture } from 'astro:assets' -import { isDev, isProd } from '@lib/config/environmentServer' +import { isDev } from '@lib/config/environmentServer' import MarkdownLayout from '@layouts/MarkdownLayout.astro' import DownloadForm from '@components/Forms/Download/index.astro' @@ -12,7 +12,7 @@ export interface Props { /** Generate static paths for all downloads */ export async function getStaticPaths() { const downloadEntries = await getCollection('downloads', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) return downloadEntries.map(download => ({ params: { slug: download.id }, diff --git a/src/pages/tags/[tag].astro b/src/pages/tags/[tag].astro index b40c3d754..dd2cc6d5b 100644 --- a/src/pages/tags/[tag].astro +++ b/src/pages/tags/[tag].astro @@ -1,7 +1,7 @@ --- import { type CollectionEntry, getCollection, render } from 'astro:content' import { Picture } from 'astro:assets' -import { isDev, isProd } from '@lib/config/environmentServer' +import { isDev } from '@lib/config/environmentServer' import BaseLayout from '@layouts/BaseLayout.astro' export interface Params { @@ -23,17 +23,17 @@ export async function getStaticPaths() { // Get all content collections with tags const allArticles = await getCollection('articles', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) const allCaseStudies = await getCollection('caseStudies', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) const allServices = await getCollection('services', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) const allDownloads = await getCollection('downloads', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) // Combine all content and extract unique tags diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro index 8e9224cc6..6b2ebc658 100644 --- a/src/pages/tags/index.astro +++ b/src/pages/tags/index.astro @@ -1,20 +1,20 @@ --- import { getCollection } from 'astro:content' import { Picture } from 'astro:assets' -import { isDev, isProd } from '@lib/config/environmentServer' +import { isDev } from '@lib/config/environmentServer' import BaseLayout from '@layouts/BaseLayout.astro' // Get all content collections with tags const allArticles = await getCollection('articles', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) const allCaseStudies = await getCollection('caseStudies', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) const allServices = await getCollection('services', ({ data }) => { - return isDev() || (isProd() && data.isDraft !== true) + return isDev() || data.isDraft !== true }) // Combine all content and sort by publish date (newest first) diff --git a/test/unit/helpers/__tests__/breadcrumbTitleLengthRefinement.spec.ts b/test/unit/helpers/__tests__/breadcrumbTitleLengthRefinement.spec.ts index 6c42a9de8..e317c8bf0 100644 --- a/test/unit/helpers/__tests__/breadcrumbTitleLengthRefinement.spec.ts +++ b/test/unit/helpers/__tests__/breadcrumbTitleLengthRefinement.spec.ts @@ -17,15 +17,6 @@ afterEach(() => { }) describe('warnOnBreadcrumbTitleLength', () => { - it('does not log warnings outside production', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { warnOnBreadcrumbTitleLength } = await loadHelpersModule(false) - - warnOnBreadcrumbTitleLength(LONG_TITLE, 'articles') - - expect(warnSpy).not.toHaveBeenCalled() - }) - it('does not log warnings for titles within the limit', async () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const { warnOnBreadcrumbTitleLength } = await loadHelpersModule(true)