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: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,15 @@ production releases. Its production build command is
`bun run release:cloudflare:build` and its deploy command is
`bun run release:cloudflare:deploy`; both fetch their environment from Infisical
before invoking the underlying release stages. Cloudflare's build image runs
coverage, types, lint, and the Worker build; its deploy stage then waits for the
coverage, types, lint, the Worker build and isolated workerd runtime smoke checks; its deploy stage then waits for the
same commit's required GitHub application/browser and database checks. A failed,
cancelled, missing, or timed-out GitHub check prevents migrations and deployment.
The only Cloudflare build secret is a read-only, production-scoped
`INFISICAL_TOKEN`; application and database secrets remain in Infisical. Preview
builds are disabled so pull requests and forks cannot receive production
credentials or run production migrations.
After deployment, public HTTP probes verify the dynamic API, homepage, login and 404 response.
Next.js remains on patched 16.2 releases until OpenNext supports the newer preview-props manifest.
The production trigger pins `BUN_VERSION=1.2.21` to match the repository and
GitHub Actions coverage runtime.
The self-hosted Infisical API domain is public configuration and is pinned in
Expand Down
152 changes: 43 additions & 109 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@
"test:all": "bun scripts/run-test-suite.mjs",
"test:coverage": "bun scripts/run-test-suite.mjs --coverage-only",
"test:stress": "bun test --rerun-each=20",
"test:worker": "bun scripts/test-worker-smoke.mjs",
"test:smoke": "bun scripts/test-site-smoke.mjs",
"test:db": "bun scripts/run-supabase-tests.mjs",
"test:roadmap-votes": "bun test tests/roadmap-vote-lifecycle.test.ts",
"db:migrate:production": "bun scripts/migrate-production.mjs",
"release:build": "bun run test:all",
"release:deploy": "bun run db:migrate:production && wrangler deploy",
"release:deploy": "bun run db:migrate:production && wrangler deploy && bun scripts/test-worker-smoke.mjs --live",
"release:cloudflare:build": "node_modules/@infisical/cli/bin/infisical run --silent --domain=https://infisical.onestack.cloud/api --token=\"$INFISICAL_TOKEN\" --projectId=15892171-18cf-4a7f-b0c3-ad4291a4b395 --env=prod -- bun scripts/run-test-suite.mjs --skip-browser-smoke",
"release:cloudflare:deploy": "node_modules/@infisical/cli/bin/infisical run --silent --domain=https://infisical.onestack.cloud/api --token=\"$INFISICAL_TOKEN\" --projectId=15892171-18cf-4a7f-b0c3-ad4291a4b395 --env=prod --command 'bun scripts/wait-for-github-release-gate.mjs && bun run release:deploy'",
"lint": "eslint .",
Expand Down Expand Up @@ -71,8 +72,8 @@
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "~16.4.2",
"next": "~16.4.2",
"eslint-config-next": "~16.2.12",
"next": "~16.2.12",
"postgres": "3.4.7",
"puppeteer": "^24.22.0",
"supabase": "2.101.0",
Expand Down
12 changes: 11 additions & 1 deletion renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,15 @@
},
"npm": {
"enabled": false
}
},
"packageRules": [
{
"description": "Keep Next on OpenNext-compatible 16.2 patches until preview-props support is released: opennextjs/opennextjs-cloudflare#1355",
"matchPackageNames": [
"next",
"eslint-config-next"
],
"allowedVersions": ">=16.2.3 <16.3.0"
}
]
}
1 change: 1 addition & 0 deletions scripts/run-test-suite.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ if (!process.argv.includes('--coverage-only')) {
run('TypeScript', 'bunx', ['tsc', '--noEmit'])
run('lint', 'bun', ['run', 'lint'])
run('production Cloudflare Worker build', 'bun', ['run', 'build:worker'])
run('Cloudflare Worker runtime smoke tests', 'bun', ['scripts/test-worker-smoke.mjs'])
if (process.argv.includes('--skip-browser-smoke')) {
console.log('\n[tests] browser smoke delegated to the required GitHub release check')
} else {
Expand Down
96 changes: 96 additions & 0 deletions scripts/test-worker-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env node
import { spawn } from 'node:child_process'
import { mkdtempSync, readFileSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve, join } from 'node:path'
import net from 'node:net'
import http from 'node:http'
import ts from 'typescript'

const live = process.argv.includes('--live')
let child
let directory
let stub
let output = ''
async function availablePort() {
const server = net.createServer()
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve))
const port = server.address().port
await new Promise((resolve) => server.close(resolve))
return port
}
try {
let baseUrl = 'https://linear.gratis'
if (!live) {
const parsed = ts.parseConfigFileTextToJson('wrangler.jsonc', readFileSync('wrangler.jsonc', 'utf8'))
if (parsed.error) throw new Error('Cannot parse Wrangler configuration')
const config = parsed.config
stub = http.createServer((request, response) => {
response.writeHead(401, { 'Content-Type': 'application/json' })
response.end(JSON.stringify({ message: 'No smoke-test session' }))
})
await new Promise((resolve) => stub.listen(0, '127.0.0.1', resolve))
directory = mkdtempSync(join(tmpdir(), 'linear-worker-smoke-'))
const configPath = join(directory, 'wrangler.json')
// A separate config directory prevents loading local production credentials.
writeFileSync(configPath, JSON.stringify({
name: 'linear-gratis-smoke',
main: resolve(config.main),
compatibility_date: config.compatibility_date,
compatibility_flags: config.compatibility_flags,
assets: { ...config.assets, directory: resolve(config.assets.directory) },
vars: {
NEXT_PUBLIC_SUPABASE_URL: `http://127.0.0.1:${stub.address().port}`,
NEXT_PUBLIC_SUPABASE_ANON_KEY: 'smoke-test-anon-key',
SUPABASE_SERVICE_ROLE_KEY: 'smoke-test-service-role-key',
NEXT_PUBLIC_APP_DOMAIN: 'localhost',
ENCRYPTION_KEY: 'smoke-test-encryption-key',
ACCESS_COOKIE_SECRET: 'smoke-test-cookie-secret',
},
}))
const port = await availablePort()
baseUrl = `http://localhost:${port}`
child = spawn('bunx', ['wrangler', 'dev', '--config', configPath, '--local', '--ip', '127.0.0.1', '--port', String(port)], {
env: { ...process.env, CI: 'true', WRANGLER_SEND_METRICS: 'false' },
stdio: ['ignore', 'pipe', 'pipe'],
})
child.on('error', (error) => { output += error.message })
for (const stream of [child.stdout, child.stderr]) {
stream.on('data', (data) => { output = (output + data.toString()).slice(-8000) })
}
}
for (const [route, status, content] of [
['/api/hello', 200, 'Hello World'],
['/', 200, '<html'],
['/login', 200, '<html'],
['/__worker_smoke_missing__', 404, '<html'],
]) {
let response
const deadline = Date.now() + 30000
while (!response) {
if (child && child.exitCode !== null) throw new Error(`Worker exited (${child.exitCode})\n${output}`)
try {
response = await fetch(new URL(route, baseUrl), { signal: AbortSignal.timeout(10000) })
} catch (error) {
if (Date.now() >= deadline) throw error
await new Promise((resolve) => setTimeout(resolve, 250))
}
}
const body = await response.text()
if (response.status !== status || !body.includes(content)) {
throw new Error(`${route}: expected ${status} and ${content}, got ${response.status}: ${body.slice(0, 200)}\n${live ? '' : output}`)
}
console.log(`[worker-smoke] ${route}: ${response.status}`)
}
} finally {
if (child && child.exitCode === null) {
child.kill('SIGTERM')
await Promise.race([
new Promise((resolve) => child.once('exit', resolve)),
new Promise((resolve) => setTimeout(resolve, 3000)),
])
if (child.exitCode === null) child.kill('SIGKILL')
}
if (stub) await new Promise((resolve) => stub.close(resolve))
if (directory) rmSync(directory, { recursive: true, force: true })
}
4 changes: 2 additions & 2 deletions tests/codebase-contracts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ describe('secret-management contracts', () => {

assert.equal(packageJson.scripts?.['release:build'], 'bun run test:all')
assert.match(packageJson.scripts?.['release:deploy'] ?? '',
/^bun run db:migrate:production && wrangler deploy$/,
'pending migrations must complete before Worker deployment')
/^bun run db:migrate:production && wrangler deploy && bun scripts\/test-worker-smoke\.mjs --live$/,
'migrations must precede deployment and public runtime probes must follow it')

for (const script of ['release:cloudflare:build', 'release:cloudflare:deploy']) {
const command = packageJson.scripts?.[script] ?? ''
Expand Down
Loading