Skip to content

feat(website): result-aware signup box on the free tool pages #1933

feat(website): result-aware signup box on the free tool pages

feat(website): result-aware signup box on the free tool pages #1933

Workflow file for this run

name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
# Explicit permissions for security best practices
permissions:
contents: read
# Every DB-backed job below hits the same Neon branch (TEST_DATABASE_URL is a
# single secret, and resolve-branch.mjs only isolates worktrees, never CI). Two
# concurrent runs of this workflow — e.g. two pushes minutes apart — execute
# their beforeAll/afterAll fixture windows on that one database at once, so one
# run's cleanup deletes the deterministic fixture (test-onboarding-org-1,
# contacts-analytics-db-c-active-1) another run's suite is still reading. That
# surfaces as FK 23503. Repo-level serialisation per ref prevents the overlap;
# this is the same mechanism migrate.yml and deploy-api.yml already use.
concurrency:
group: test-db-${{ github.ref }}
cancel-in-progress: false
# Runner allocation is by wall-clock position, not by job kind. This workflow
# finishes when test-web finishes (~366s on Blacksmith 4vCPU). Only test-web
# stays on Blacksmith; everything else runs on GitHub-hosted runners, which
# are free on this public repo and ran 1.2-1.8x slower than Blacksmith for
# the jobs that moved.
#
# The constraint is Blacksmith's free tier: 3,000 2vCPU-minutes a month, and a
# 4vCPU minute costs two. Over 2026-09-05..10-05 (~156 Test runs) test-web
# used ~1,036 4vCPU-minutes (~2,070 of the 3,000). test-api (~676) and
# selfhost-smoke (~337) on top took the total to ~4,100, so both moved off.
#
# Before moving a job onto Blacksmith, check it against test-web's duration —
# if it finishes well inside that, paying for it buys no PR feedback time —
# and check the monthly minutes still fit the free tier.
jobs:
# Every job here is guarded to the upstream repo — forks lack the test
# secrets, and the Blacksmith jobs also lack the runners, so unguarded jobs
# fail or queue forever on their pushes and sync PRs. The ubuntu-latest jobs
# that need no secrets (test, test-scripts, test-packages, baseline, lint)
# could now drop this guard and give fork PRs real CI for free — that is a
# deliberate choice about running contributor code, not an oversight.
test:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.x, 24.x]
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build dependencies
run: pnpm --filter "@wraps.dev/cli^..." build
- name: Run type checking
run: pnpm --dir packages/cli typecheck
- name: Run tests
run: pnpm --dir packages/cli test -- --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
if: matrix.node-version == '24.x'
with:
files: ./packages/cli/coverage/coverage-final.json
flags: cli
token: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true
test-api:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build workspace deps
run: pnpm --filter "@wraps/api^..." build
- name: Typecheck API
run: pnpm turbo run typecheck --filter=@wraps/api
- name: Run API tests
run: pnpm --filter @wraps/api test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
BETTER_AUTH_SECRET: test-secret-key-for-testing-only
NEXT_PUBLIC_APP_URL: http://localhost:3000
CORS_ORIGIN: http://localhost:3000
test-auth:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build auth workspace dependencies
run: pnpm --filter "@wraps/auth^..." build
- name: Run auth type checking
run: pnpm --filter @wraps/auth typecheck
- name: Run auth tests
run: pnpm --filter @wraps/auth test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
BETTER_AUTH_SECRET: test-secret-key-for-testing-only
NEXT_PUBLIC_APP_URL: http://localhost:3000
CORS_ORIGIN: http://localhost:3000
STRIPE_SECRET_KEY: sk_test_fake_key_for_ci_only
STRIPE_WEBHOOK_SECRET: whsec_fake_secret_for_ci_only
STRIPE_GROWTH_PRICE_ID: price_fake_growth_for_ci_only
- name: Run email-send tests
run: pnpm --filter @wraps/email-send test
test-web:
if: github.repository == 'wraps-team/wraps'
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build web workspace dependencies
run: pnpm --filter "@wraps/web^..." build
- name: Run web type checking
run: pnpm --filter @wraps/web typecheck
- name: Run web tests
run: pnpm --filter @wraps/web test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
BETTER_AUTH_SECRET: test-secret-key-for-testing-only
NEXT_PUBLIC_APP_URL: http://localhost:3000
# resolveApiBaseUrl() deliberately has no fallback — a self-hosted
# deployment must never point unsubscribe links at the Wraps API.
# Test sends that build those links throw without this.
NEXT_PUBLIC_API_URL: http://localhost:3001
CORS_ORIGIN: http://localhost:3000
test-db:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check migration snapshot integrity
run: pnpm --filter @wraps/db exec drizzle-kit check
# Self-hosted installs get schema changes only through migrations
# (scripts/selfhost/upgrade.ts). A schema edit with no generated
# migration passes every test here, since the test DB is db:push-managed,
# and ships a column self-hosters never receive. Offline: generate reads
# the schema and the snapshots, not a database.
- name: Every schema change has a migration
run: |
pnpm --filter @wraps/db exec drizzle-kit generate --name ci-drift-probe
drift=$(git status --porcelain packages/db/src/migrations)
if [ -n "$drift" ]; then
echo "::error::packages/db schema changed with no migration. Run 'pnpm --filter @wraps/db db:generate' and commit the result."
echo "$drift"
exit 1
fi
- name: Run db tests
run: pnpm --filter @wraps/db test
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
# scripts/ sits outside the pnpm workspace, so turbo's `pnpm test` never
# reaches it. Without this job the selfhost deploy/upgrade/migrate/reroute
# tests protect nothing in CI.
test-scripts:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build workspace deps
run: pnpm --filter "@wraps.dev/cli^..." build
# verify-opennext-build.test.ts reads infra/.sst/platform/src/components/
# aws/nextjs.ts at module load. .sst is gitignored, so a fresh runner only
# has it after `sst install` materialises the platform. selfhost-smoke runs
# this step too, but that job does not run these unit tests.
#
# sst install downloads a version-pinned pulumi binary from GitHub
# releases at CI runtime, which can fail transiently. The materialisation
# is idempotent, so retry it with backoff.
- name: Materialise the SST platform (sst install)
working-directory: infra
run: |
for attempt in 1 2 3; do
if ../node_modules/.bin/sst install --config selfhost.config.ts --stage production --print-logs; then
exit 0
fi
echo "sst install failed (attempt $attempt/3), retrying"
sleep "$((attempt * 15))"
done
echo "::error::sst install failed after 3 attempts"
exit 1
- name: Run selfhost script tests
run: pnpm test:scripts
# These ten packages have test scripts that `turbo run test` runs locally
# but no CI job ran until this one. Between them they hold the
# agent-enforcer Lambda's security tests, the unsubscribe-token signing
# suite, and the website's pricing-copy regression net. None needs a
# database or any secret. Keep the filter list in sync with the
# architecture test that guards it.
test-packages:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js 24.x
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build workspace deps
run: pnpm turbo run build --filter=@wraps/core --filter=@wraps/db --filter=@wraps.dev/email-check
- name: Run package tests
run: |
pnpm turbo run test \
--filter=wraps-website \
--filter=@wraps/ai \
--filter=@wraps/analytics \
--filter=@wraps/observability \
--filter=@wraps/core \
--filter=@wraps/email \
--filter=@wraps.dev/email-check \
--filter=@wraps.dev/pulumi \
--filter=mail-audit \
--filter=@wraps/template-render \
--filter=@wraps/unsubscribe-token \
--filter=@wraps.dev/cdk \
--filter=wraps-templates
baseline:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
- name: Install dependencies
run: pnpm install --frozen-lockfile
# baseline/architecture.test.ts is not part of any workspace package, so
# no other job runs it. The scan below is the other half of
# `pnpm test:baseline`, run separately here so it can be changed-only.
- name: Run architecture tests
run: pnpm test:architecture
- name: Run baseline (changed files only)
if: github.event_name == 'pull_request'
run: pnpm exec baseline scan --format github --changed-only .
- name: Run baseline (full scan)
if: github.event_name == 'push'
run: pnpm exec baseline scan --format github .
lint:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm check:errors
- name: Run design-system lint ceilings
run: pnpm check:design
# Clean-clone smoke test for the self-hosted deploy path. A CI runner IS a
# fresh customer clone — this job proves the path a fork customer follows
# actually works without any maintainer-machine state (stale gitignored
# dist/, hoisted binaries, lockfile drift). No AWS credentials needed: every
# step stops before touching AWS. See project_selfhost_fresh_clone_fixes —
# six shipped bugs were only visible from a clean clone.
selfhost-smoke:
if: github.repository == 'wraps-team/wraps'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v4
- name: Setup pnpm
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- name: Setup Node.js
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 24.x
cache: 'pnpm'
- name: Install dependencies (frozen, as CI deploys do)
run: pnpm install --frozen-lockfile
- name: Lockfile is stable on a plain install (as the docs instruct)
run: |
pnpm install
git diff --exit-code pnpm-lock.yaml || {
echo "::error::A plain 'pnpm install' rewrote pnpm-lock.yaml — a workspace package is referenced by the lockfile but not shipped in git (gitignored dir matched by a workspace glob?). Customers following the docs get a permanently dirty tree."
exit 1
}
- name: Build workspace packages (what selfhost:deploy/upgrade run first)
run: pnpm selfhost:build-deps
- name: Dist artifacts the SST bundle imports exist
run: |
for f in packages/db/dist/index.js packages/core/dist/index.js packages/email-check/dist/index.js; do
[ -f "$f" ] || { echo "::error::$f missing after selfhost:build-deps — the SST esbuild bundle will fail with 'Could not resolve' on customer machines"; exit 1; }
done
# SST's bundler injects the aws/sst globals into every source file
# EXCEPT paths containing ".sst" (its platform dir check is a naive
# substring match). A config named e.g. selfhost.sst.config.ts silently
# gets no globals and dies at deploy with "aws is not defined".
- name: SST config filename must not contain ".sst"
run: |
if ls infra/*.ts | grep -q "\.sst"; then
echo "::error::An infra config filename contains '.sst' — SST's InjectGlobals plugin skips such paths, so aws/sst globals will be undefined at deploy time"
exit 1
fi
# Same bounded retry as test-scripts: the pinned pulumi download can
# fail transiently.
- name: SST config builds from zero (sst install)
working-directory: infra
run: |
for attempt in 1 2 3; do
if ../node_modules/.bin/sst install --config selfhost.config.ts --stage production --print-logs; then
exit 0
fi
echo "sst install failed (attempt $attempt/3), retrying"
sleep "$((attempt * 15))"
done
echo "::error::sst install failed after 3 attempts"
exit 1
# Full run() evaluation — the phase sst install never reaches (it only
# evaluates app()). Real config + real SST platform components + real
# @pulumi/aws; only the Pulumi engine is mocked (pulumi's official
# setMocks API). The negative control proves the harness can still
# detect the missing-globals failure, so it can't rot into always-pass.
- name: SST config run() evaluates (mock engine)
run: |
node scripts/selfhost/eval-config.mjs
node scripts/selfhost/eval-config.mjs negative
- name: Upgrade script boots and fails with its own guard (not a crash)
run: |
set +e
output=$(pnpm exec tsx scripts/selfhost/upgrade.ts 2>&1)
status=$?
set -e
echo "$output"
if [ $status -eq 0 ]; then
echo "::error::upgrade.ts exited 0 with no .env.selfhost — the guard is broken"
exit 1
fi
echo "$output" | grep -q ".env.selfhost not found" || {
echo "::error::upgrade.ts failed with something other than its .env.selfhost guard — likely a broken import (missing root dep or dist artifact)"
exit 1
}
# The one step that actually builds the dashboard the way a customer's
# deploy does. sst.aws.Nextjs does not build Next itself — it shells out
# to OpenNext, which reimplements Next's server output for Lambda and
# tracks Next's internals minor by minor. Every step above stops at
# config evaluation, and no job in this workflow runs a Next production
# build of apps/web at all, so without this a Next bump OpenNext cannot
# adapt to merges green and first fails inside a customer's AWS account
# mid-deploy. The script derives the OpenNext version from the platform
# `sst install` just materialised, so it follows an sst bump on its own.
# The build below is ~2 min and CPU-bound, which puts selfhost-smoke on
# this workflow's critical path on every push. Only ~46% of recent
# commits can affect it, so scope it to the paths that can.
#
# `paths:` is workflow-level and this workflow has eleven other jobs, so
# the decision has to happen here. It uses the compare API rather than
# git so the checkout can stay shallow: `compare/BASE...HEAD` is
# merge-base semantics, which is what a PR wants and what a multi-commit
# push needs (github.event.before, not HEAD^).
#
# Fail open. Every path that cannot resolve a base — a new branch, a
# force push, an API blip — runs the build. A guard that quietly stops
# guarding is worse than one that costs two minutes.
- name: Does this change affect the OpenNext build?
id: scope
env:
GH_TOKEN: ${{ github.token }}
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
set -uo pipefail
run_it() { echo "build=true" >> "$GITHUB_OUTPUT"; echo "$1"; exit 0; }
case "$BASE" in
""|0000000000000000000000000000000000000000) run_it "no comparison base: building" ;;
esac
files=$(gh api "repos/$GITHUB_REPOSITORY/compare/$BASE...$HEAD" \
--paginate --jq '.files[].filename' 2>/dev/null) \
|| run_it "could not read the diff from the API: building"
[ -n "$files" ] || run_it "API returned no file list: building"
# apps/web plus its workspace dependency closure — keep in sync with
# `pnpm --filter "@wraps/web^..." list`, which the sibling test
# verify-opennext-build.test.ts asserts against this list. next is
# pinned in pnpm-workspace.yaml's `overrides:`, and the sst version
# in the root package.json decides which OpenNext runs, so a bump to
# either changes what this step is testing.
if echo "$files" | grep -qE \
'^(apps/web/|packages/(ai|analytics|auth|core|db|email|email-check|email-send|template-render|ui|unsubscribe-token)/|infra/selfhost\.config\.ts$|scripts/selfhost/verify-opennext-build\.mjs$|package\.json$|pnpm-lock\.yaml$|pnpm-workspace\.yaml$|\.github/workflows/test\.yml$)'
then
echo "build=true" >> "$GITHUB_OUTPUT"
echo "OpenNext-relevant paths changed: building"
else
echo "build=false" >> "$GITHUB_OUTPUT"
echo "nothing OpenNext-relevant changed: skipping the build"
fi
- name: OpenNext can build and serve apps/web on this Next version
if: steps.scope.outputs.build == 'true'
run: node scripts/selfhost/verify-opennext-build.mjs