Repository navigation
feat(website): result-aware signup box on the free tool pages #1933
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |