Skip to content
Open
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
1 change: 0 additions & 1 deletion src/commands/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export const LOCAL_DOMAIN = 'local.factorial.dev'
export const LOCAL_AWS_PROFILE = 'development'
export const LOCAL_AWS_DEFAULT_REGION = 'eu-central-1'
export const CONDUCTOR_ECR_REGISTRY = '771567148620.dkr.ecr.eu-central-1.amazonaws.com'
export const BUNDLER_VERSION = '2.5.11'
export const PNPM_VERSION = '11.5.2'
export const PERSONAL_ENV_RC_PATH = path.join(REPO_PATH, '.envrc.personal')

Expand Down
96 changes: 65 additions & 31 deletions src/commands/steps/13-dev-environment.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
import { type SetupConfig } from '../../context/index.js'
import { getLibBuildFlags, isDarwin, isLinux } from '../../platform.js'
import { BUNDLER_VERSION, PNPM_VERSION, REPO_PATH } from '../constants.js'
import { getErrorMessage, sh, sudoSh, type ProgressCallback, type TaskResult } from '../helpers.js'
import { PNPM_VERSION, REPO_PATH } from '../constants.js'
import {
getErrorMessage,
sh,
shellEscape,
type ProgressCallback,
type TaskResult,
} from '../helpers.js'
import { readFile } from 'node:fs/promises'
import path from 'node:path'

async function getBundlerVersion(backendPath: string): Promise<string> {
const lockfile = await readFile(path.join(backendPath, 'Gemfile.lock'), 'utf-8')
const match = lockfile.match(/^BUNDLED WITH\r?\n\s+(\S+)/m)
if (!match?.[1]) {
throw new Error('Could not determine the Bundler version from backend/Gemfile.lock.')
}
return match[1]
}

/** Step 13: Setup development environment */
export async function runStep13(
config: SetupConfig,
onProgress: ProgressCallback
): Promise<TaskResult> {
const start = Date.now()
try {
const backendPath = path.join(REPO_PATH, 'backend')
const withRuntime = (command: string): string =>
config.versionManager === 'mise' ? `mise exec -- ${command}` : `asdf exec ${command}`

// 0. Install yarn/pnpm and run pnpm i
onProgress(0, 'Installing yarn and pnpm globally...')
await sh(`npm install --global yarn pnpm@${PNPM_VERSION}`, {
await sh(withRuntime(`npm install --global yarn pnpm@${PNPM_VERSION}`), {
cwd: REPO_PATH,
interactive: true,
check: true,
})

onProgress(1, 'Running pnpm install...')
await sh('pnpm i', { cwd: REPO_PATH, interactive: true })
await sh(withRuntime('pnpm i'), { cwd: REPO_PATH, interactive: true, check: true })

// 1. Install bundler + bundle install
onProgress(2, 'Installing bundler and running bundle install...')
const gemPath = (await sh('command -v gem')).stdout
const isUserGem = gemPath.includes(process.env.USER || '')
if (isUserGem) {
await sh(`gem install bundler -v "${BUNDLER_VERSION}"`, {
cwd: path.join(REPO_PATH, 'backend'),
})
} else {
// System gem requires elevated privileges
await sudoSh(`gem install bundler -v '${BUNDLER_VERSION}'`)
}
const bundlerVersion = await getBundlerVersion(backendPath)
await sh(withRuntime(`gem install bundler -v ${shellEscape(bundlerVersion)} --no-document`), {
cwd: backendPath,
check: true,
})

// mysql2 gem with library flags (platform-aware)
const buildFlags = await getLibBuildFlags((cmd) => sh(cmd))
Expand All @@ -52,40 +69,50 @@ export async function runStep13(
// Needed on all macOS (not just ARM) with MySQL 9.x which requires zstd.
if (isDarwin() || isLinux()) {
await sh(
`bundle config set --global build.mysql2 "--with-opt-dir=${buildFlags.optDir} --with-ldflags=${buildFlags.ldflags} --with-cppflags=${buildFlags.cppflags}"`,
{ cwd: path.join(REPO_PATH, 'backend'), check: true }
withRuntime(
`bundle config set --global build.mysql2 "--with-opt-dir=${buildFlags.optDir} --with-ldflags=${buildFlags.ldflags} --with-cppflags=${buildFlags.cppflags}"`
),
{ cwd: backendPath, check: true }
)
}

await sh(
`gem install mysql2 -- --with-opt-dir="${buildFlags.optDir}" --with-ldflags="${buildFlags.ldflags}" --with-cppflags="${buildFlags.cppflags}"`,
{ cwd: path.join(REPO_PATH, 'backend'), env: buildEnv, check: true }
withRuntime(
`gem install mysql2 -- --with-opt-dir="${buildFlags.optDir}" --with-ldflags="${buildFlags.ldflags}" --with-cppflags="${buildFlags.cppflags}"`
),
{ cwd: backendPath, env: buildEnv, check: true }
)

// tmuxinator (terminal multiplexer session manager)
await sh('gem install tmuxinator')
await sh(withRuntime('gem install tmuxinator'), { cwd: backendPath, check: true })

await sh('bundle install', {
cwd: path.join(REPO_PATH, 'backend'),
await sh(withRuntime('bundle install'), {
cwd: backendPath,
interactive: true,
env: buildEnv,
check: true,
})

// 2. Mobile + ATS deps
onProgress(3, 'Installing mobile and ATS dependencies...')
await sh('pnpm i', {
await sh(withRuntime('pnpm i'), {
cwd: path.join(REPO_PATH, 'mobile'),
interactive: true,
check: true,
})
await sh('yarn install', {
await sh(withRuntime('yarn install'), {
cwd: path.join(REPO_PATH, 'backend', 'components', 'ats'),
interactive: true,
check: true,
})

// 3. Shadowdog
onProgress(4, 'Running shadowdog...')
await sh('pnpm shadowdog', { cwd: REPO_PATH, interactive: true })
await sh(withRuntime('pnpm shadowdog'), {
cwd: REPO_PATH,
interactive: true,
check: true,
})

// 4. Docker compose — detect modern plugin vs legacy standalone
const composeCmd = await (async () => {
Expand Down Expand Up @@ -166,13 +193,15 @@ export async function runStep13(
if (config.restoreDb) {
onProgress(7, 'Restoring database from backup...')
await sh(
'bundle exec rails db:drop db:create db:seeds:restore db:migrate:with_data dev:enable_default_features db:test:prepare',
{ cwd: path.join(REPO_PATH, 'backend'), interactive: true, check: true }
withRuntime(
'bundle exec rails db:drop db:create db:seeds:restore db:migrate:with_data dev:enable_default_features db:test:prepare'
),
{ cwd: backendPath, interactive: true, check: true }
)
} else {
onProgress(7, 'Creating database...')
await sh('bundle exec rails db:create db:migrate db:test:prepare', {
cwd: path.join(REPO_PATH, 'backend'),
await sh(withRuntime('bundle exec rails db:create db:migrate db:test:prepare'), {
cwd: backendPath,
interactive: true,
check: true,
})
Expand All @@ -182,16 +211,21 @@ export async function runStep13(
// actually usable. Each check is fail-loud (`check: true`) so a failure here
// surfaces as a red task with a real error instead of a false ✓.
onProgress(7, 'Verifying environment...')
const backendCwd = path.join(REPO_PATH, 'backend')

// 7a. The mysql2 native extension actually loads (zstd/openssl linked OK),
// not just that `gem install` reported success.
await sh(`bundle exec ruby -e "require 'mysql2'"`, { cwd: backendCwd, check: true })
await sh(withRuntime(`bundle exec ruby -e "require 'mysql2'"`), {
cwd: backendPath,
check: true,
})

// 7b. The database is reachable AND has no pending migrations. This single
// rake task must connect to read schema_migrations, so it covers both
// "DB exists/reachable" and "schema is up to date" in one Rails boot.
await sh('bundle exec rails db:abort_if_pending_migrations', { cwd: backendCwd, check: true })
await sh(withRuntime('bundle exec rails db:abort_if_pending_migrations'), {
cwd: backendPath,
check: true,
})

// 8. Done — no editor or browser opened; the finished pane shows next steps

Expand Down