Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/deploy-to-vercel-preview/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .github/actions/deploy-to-vercel-production/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .github/actions/download-build-artifact/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
22 changes: 10 additions & 12 deletions .husky/prepare.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
43 changes: 0 additions & 43 deletions _TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).


6 changes: 2 additions & 4 deletions src/actions/_environment/siteUrlActions.ts
Original file line number Diff line number Diff line change
@@ -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}`
Expand Down
7 changes: 4 additions & 3 deletions src/actions/_utils/rateLimit.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -46,7 +46,7 @@ export async function checkRateLimit(
}

export function checkContactRateLimit(ipFingerprint: string): boolean {
if (isDev() || isTest()) {
if (!isProd()) {
return true
}

Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/actions/contact/responder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -132,7 +132,7 @@ ${fields.join('\n')}
}

async function sendEmail(emailData: EmailData, files: FileAttachment[]): Promise<void> {
if (isTest() || isDev()) {
if (!isProd()) {
return
}

Expand Down
5 changes: 3 additions & 2 deletions src/actions/gdpr/_dsarVerificationEmails.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -10,7 +10,8 @@ export async function sendDsarVerificationEmail(
token: string,
requestType: 'ACCESS' | 'DELETE',
): Promise<void> {
if (isDev() || isTest()) {
/** Testing helper */
if (!isProd()) {
console.log('[DEV/TEST MODE] DSAR verification email would be sent:', { email, token, requestType })
return
}
Expand Down
8 changes: 5 additions & 3 deletions src/actions/newsletter/entities.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -217,7 +218,8 @@ export async function sendConfirmationEmail(email: string, token: string, firstN
}

export async function sendWelcomeEmail(email: string, firstName?: string): Promise<void> {
if (isDev() || isTest()) {
/** Testing helper */
if (!isProd()) {
console.log('[DEV/TEST MODE] Newsletter welcome email would be sent:', { email })
return
}
Expand Down
5 changes: 3 additions & 2 deletions src/actions/newsletter/responder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -55,7 +55,8 @@ function validateEmail(email: string): string {
}

export async function subscribeToConvertKit(data: NewsletterFormData): Promise<ConvertKitResponse> {
if (isDev() || isTest()) {
/** Testing helper */
if (!isProd()) {
console.log('[DEV/TEST MODE] Newsletter subscription would be created:', { email: data.email })
return {
subscriber: {
Expand Down
7 changes: 2 additions & 5 deletions src/components/Avatar/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/

import type { ImageMetadata } from 'astro'
import { isDev } from '@lib/config/environmentServer'
import type { AvatarMap } from './@types'
import { loadAvatarModules } from './avatarImports'

Expand Down Expand Up @@ -76,17 +75,15 @@ 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}`)
}
}

// 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`)
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/components/WebMentions/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 7 additions & 5 deletions src/components/scripts/sentry/__tests__/helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand All @@ -36,14 +36,14 @@ const createHint = (): Parameters<typeof beforeSendHandler>[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())
Expand All @@ -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()
Expand All @@ -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()
Expand Down
7 changes: 4 additions & 3 deletions src/components/scripts/sentry/helpers.ts
Original file line number Diff line number Diff line change
@@ -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<BrowserOptions['beforeSend']>
Expand All @@ -8,7 +8,7 @@ type BeforeSendHandler = NonNullable<BrowserOptions['beforeSend']>
* Applies consent-aware filtering to Sentry events before they are sent.
*/
export const beforeSendHandler: BeforeSendHandler = (event, _hint) => {
if (isDev()) {
if (!isProd()) {
return null
}

Expand All @@ -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()
Expand Down
6 changes: 2 additions & 4 deletions src/components/scripts/utils/__tests__/siteUrlClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/components/scripts/utils/environmentClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const isDev = () => {
}

export const isProd = () => {
return import.meta.env.MODE === 'production' && !isUnitTest()
return import.meta.env.MODE === 'production'
}

/**
Expand Down
5 changes: 1 addition & 4 deletions src/components/scripts/utils/siteUrlClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`
Expand Down
Loading
Loading