Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
361f9f2
docs: design a stack that is green and deployable on generation
nghiatruongtps Sep 6, 2026
c4a1939
docs: plan the deployable stack, and fix what planning falsified
nghiatruongtps Sep 6, 2026
c61600f
feat: let an adapter declare the paths its health checks probe
nghiatruongtps Sep 6, 2026
78d4ea3
test: cover the readiness-path role gate through lint_adapters directly
nghiatruongtps Sep 6, 2026
188c9db
feat: serve the port compose publishes, on a route that exists
nghiatruongtps Sep 6, 2026
6a703ce
fix: fail post-generate if HealthModule wiring did not land
nghiatruongtps Sep 6, 2026
49ff1bf
feat: put an http server in the laravel images
nghiatruongtps Sep 6, 2026
b2f2d1b
feat: give a driver a seam for the app's connection environment
nghiatruongtps Sep 6, 2026
11534cd
fix: give lint's driver sourcing a real environment, and measure -P
nghiatruongtps Sep 6, 2026
c0042f4
feat: tell the application how to reach the service it was given
nghiatruongtps Sep 6, 2026
c3a249f
feat: let the released stack migrate its own schema
nghiatruongtps Sep 6, 2026
02a1a17
fix: give the nest migrate service a command the runtime image can run
nghiatruongtps Sep 6, 2026
e2e5ecd
feat: start the stack in ci and require it to answer
nghiatruongtps Sep 6, 2026
eaedb9a
fix: bind nextjs to every interface, and probe it on one that exists
nghiatruongtps Sep 6, 2026
0b59f6a
fix: a missing migrate service is a failure, not a green run
nghiatruongtps Sep 6, 2026
957aa4b
docs: record what a running stack changed about the seams
nghiatruongtps Sep 6, 2026
2f4e443
fix: reuse the nest readiness probe's PrismaClient instead of leaking…
nghiatruongtps Sep 6, 2026
c358af6
fix: require adapter health paths to hold a route, not just exist
nghiatruongtps Sep 6, 2026
0a4976d
test: prove lint_services' driver-function loop can fail
nghiatruongtps Sep 6, 2026
3b48bd3
fix: make the deploy gate generate real passwords, not changeme
nghiatruongtps Sep 6, 2026
bc55925
fix: skip the readiness curl for a driven adapter generated with --db…
nghiatruongtps Sep 6, 2026
0718e51
fix: assert absence of a literal password, not presence of an interpo…
nghiatruongtps Sep 6, 2026
766c111
fix: four small corrections from the final review
nghiatruongtps Sep 6, 2026
27d1df5
fix: correct two false claims about what is enforced
nghiatruongtps Sep 7, 2026
0c80012
test: prove the literal-password check can fail
nghiatruongtps Sep 7, 2026
7631df7
fix: anchor the password-key grep past leading whitespace
nghiatruongtps Sep 7, 2026
344b29b
fix: drop the redundant REDIS_PASSWORD line from redis's laravel driver
nghiatruongtps Sep 7, 2026
25f9f43
fix: satisfy Laravel Pint on the health route
nghiatruongtps Sep 7, 2026
a54ab32
fix: give deploy-check.sh its own git identity
nghiatruongtps Sep 7, 2026
27bb688
fix: give deploy-check.sh a GitHub owner and its own mise trust store
nghiatruongtps Sep 7, 2026
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
70 changes: 69 additions & 1 deletion .github/workflows/adapters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,73 @@ jobs:
ADAPTER: ${{ matrix.adapter }}
run: bats "tests/new-${ADAPTER}.bats"

deploy:
needs: discover
if: ${{ needs.discover.outputs.tier-a != '[]' }}
runs-on: ubuntu-latest
permissions:
contents: read
# a generation plus an image build plus a container start, per adapter.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
adapter: ${{ fromJson(needs.discover.outputs.tier-a) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
- run: corepack enable
- id: language
env:
ADAPTER: ${{ matrix.adapter }}
run: |
lang="$(grep '^ADAPTER_LANGUAGE=' "adapters/${ADAPTER}/adapter.env" | cut -d'"' -f2)"
echo "value=${lang}" >> "$GITHUB_OUTPUT"
- if: steps.language.outputs.value == 'php'
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
- env:
ADAPTER: ${{ matrix.adapter }}
run: ./scripts/deploy-check.sh "$ADAPTER"

deploy-tier-b:
needs: discover
if: ${{ needs.discover.outputs.tier-b != '[]' }}
runs-on: ubuntu-latest
permissions:
contents: read
# same per-adapter cost as deploy (generation + image build + container
# start) — tier b's timeout is longer than tier a's elsewhere in this
# file because those jobs run more tests per adapter, not because this
# one does.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
adapter: ${{ fromJson(needs.discover.outputs.tier-b) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4.3.0
- run: corepack enable
- id: language
env:
ADAPTER: ${{ matrix.adapter }}
run: |
lang="$(grep '^ADAPTER_LANGUAGE=' "adapters/${ADAPTER}/adapter.env" | cut -d'"' -f2)"
echo "value=${lang}" >> "$GITHUB_OUTPUT"
- if: steps.language.outputs.value == 'php'
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: "8.3"
- env:
ADAPTER: ${{ matrix.adapter }}
run: ./scripts/deploy-check.sh "$ADAPTER"

compose:
# expensive relative to the other checks here, so gated on the weekly
# schedule (not the nightly tier-a-only one) or a manual run
Expand Down Expand Up @@ -175,8 +242,9 @@ jobs:
if: >-
${{ always() && github.event_name == 'schedule' &&
(needs.smoke.result == 'failure' || needs.smoke-tier-b.result == 'failure' ||
needs.deploy.result == 'failure' || needs.deploy-tier-b.result == 'failure' ||
needs.compose.result == 'failure' || needs.services.result == 'failure') }}
needs: [smoke, smoke-tier-b, compose, services]
needs: [smoke, smoke-tier-b, deploy, deploy-tier-b, compose, services]
runs-on: ubuntu-latest
permissions:
issues: write
Expand Down
36 changes: 24 additions & 12 deletions adapters/laravel-api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,34 @@ COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-interaction \
--prefer-dist --optimize-autoloader

FROM php:8.3.21-fpm-alpine@sha256:d2170b0f8da574062b289566a05f25ab57173315a356c9f7519b5e444ae96dac AS runtime
# FrankenPHP, because php-fpm speaks FastCGI and this stack has no web server
# in front of it: nothing served HTTP at all, and compose published a dead
# port. Documented as a first-class server in laravel.com/docs/13.x/deployment,
# part of the PHP Foundation since May 2025, and what Laravel Cloud runs.
# Alpine specifically: services/mongodb/drivers/laravel.sh emits `apk add`.
FROM dunglas/frankenphp:1.12.7-php8.3-alpine@sha256:049b8d8356efceb93c91ed42866de890534310bcef4ad4dde902029e4a0d20c3 AS runtime
RUN docker-php-ext-install opcache
# @SERVICE_SETUP@
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY . .
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# :8080 is how frankenphp listens on an unprivileged port and declines to
# provision TLS, which belongs to whatever proxy this lands behind.
ENV SERVER_NAME=:8080
# Caddy writes here and will not start if it may not. The app tree needs the
# same: COPY runs as root, the server runs as www-data, and the first request
# that compiles a blade view or writes a log fails without this.
ENV XDG_CONFIG_HOME=/config XDG_DATA_HOME=/data
RUN mkdir -p /config /data \
&& chown -R www-data:www-data /config /data \
/var/www/storage /var/www/bootstrap/cache
USER www-data
EXPOSE 9000
# php-fpm speaks fastcgi on this port, not http, so the http probes the
# sibling adapters (nextjs, nestjs) use do not transplant here. the previous
# check, `php -r 'exit(0);'`, only proved the php cli starts — it can never
# fail while the container is actually broken, which is worse than no check
# at all: an orchestrator with no HEALTHCHECK knows it does not know, one
# with an always-green check believes it does. a real check needs php-fpm's
# ping.path probed with cgi-fcgi, which costs a pool config file and a
# package this image does not otherwise need; add both together if this is
# ever deployed behind something that polls container health.
CMD ["php-fpm"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/up || exit 1
# CMD replaces the base image's default args entirely rather than extending
# them, and those args are what point frankenphp at the Caddyfile that
# defines the :8080 site block; without them it starts only the admin API on
# 127.0.0.1:2019 and nothing ever listens on 8080.
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
15 changes: 14 additions & 1 deletion adapters/laravel-api/adapter.env
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,17 @@ ADAPTER_FAMILY="laravel"
# holding the same floor. See docs/decisions/0016.
ADAPTER_GENERATOR='composer create-project laravel/laravel:^13.0 "$APP_DIR" --no-interaction --prefer-dist'
# the skeleton ships phpunit and pint, but nothing for the check task
ADAPTER_POST_GENERATE='composer require --dev larastan/larastan phpstan/phpstan --no-interaction'
#
# The sed call wires routes/health.php (copied in by apply_adapter's
# directory loop, not this adapter's flat file list) into bootstrap/app.php's
# routing: laravel only auto-loads routes/web.php and routes/console.php, so
# a file dropped at routes/health.php with nothing pointing at it 404s
# forever. The grep pair after it is not optional — `s|health: '/up',|...|`
# silently no-ops if the skeleton ever reformats that line, leaving the
# route unregistered with no build failure, only a 404 discovered in
# production.
ADAPTER_POST_GENERATE='composer require --dev larastan/larastan phpstan/phpstan --no-interaction && sed -i "s|health: '"'"'/up'"'"',|health: '"'"'/up'"'"',\n then: function (): void {\n require __DIR__.'"'"'/../routes/health.php'"'"';\n },|" bootstrap/app.php && { grep -q "then: function (): void {" bootstrap/app.php && grep -q "require __DIR__.'"'"'/../routes/health.php'"'"';" bootstrap/app.php; } || { echo "post-generate: health route wiring missing from bootstrap/app.php — the laravel skeleton likely changed its withRouting shape; update the sed pattern in ADAPTER_POST_GENERATE to match" >&2; exit 1; }'
# /up ships with laravel since 11.x and deliberately touches nothing, which
# is why it cannot stand alone: a project with a wrong DATABASE_URL passes it.
ADAPTER_LIVENESS_PATH="/up"
ADAPTER_READINESS_PATH="/health/ready"
21 changes: 21 additions & 0 deletions adapters/laravel-api/routes/health.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Support\Facades\Route;

// Registered from bootstrap/app.php's withRouting(then: ...) — laravel loads
// only routes/web.php and routes/api.php on its own, so a file dropped here
// with nothing pointing at it would 404 forever.
//
// The probe is written by the selected service's driver: laravel has no
// provider-agnostic read, so a SQL connection gets `select 1` and mongodb
// gets a ping command. A project generated with --db none keeps the
// anchor's fallback and reports 503, because there is nothing here that
// could honestly report ready.
Route::get('/health/ready', function () {
try {
// @DB_PROBE@
throw new RuntimeException('no database is configured for this project');
} catch (Throwable $e) {
return response()->json(['status' => 'unavailable', 'reason' => $e->getMessage()], 503);
}
});
6 changes: 6 additions & 0 deletions adapters/laravel-inertia/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ storage/framework/cache
# other way — laravel refuses to start without it. For laravel-inertia either
# failure surfaces in vite's wayfinder plugin, several stages from the cause.
bootstrap/cache/*.php

# vite writes this and .gitignore excludes it, but a build context is not a
# git tree: without this line a developer who has run `npm run build` ships
# their host copy over the one the assets stage just built. Same shape as the
# bootstrap/cache manifests two lines up.
public/build
38 changes: 25 additions & 13 deletions adapters/laravel-inertia/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,35 @@ COPY --from=vendor /app/vendor ./vendor
COPY . .
RUN npm run build

FROM php:8.3.21-fpm-alpine@sha256:d2170b0f8da574062b289566a05f25ab57173315a356c9f7519b5e444ae96dac AS runtime
# FrankenPHP, because php-fpm speaks FastCGI and this stack has no web server
# in front of it: nothing served HTTP at all, and compose published a dead
# port. Documented as a first-class server in laravel.com/docs/13.x/deployment,
# part of the PHP Foundation since May 2025, and what Laravel Cloud runs.
# Alpine specifically: services/mongodb/drivers/laravel.sh emits `apk add`.
FROM dunglas/frankenphp:1.12.7-php8.3-alpine@sha256:049b8d8356efceb93c91ed42866de890534310bcef4ad4dde902029e4a0d20c3 AS runtime
RUN docker-php-ext-install opcache
# @SERVICE_SETUP@
WORKDIR /var/www
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
COPY . .
COPY --from=assets /app/public/build ./public/build
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# :8080 is how frankenphp listens on an unprivileged port and declines to
# provision TLS, which belongs to whatever proxy this lands behind.
ENV SERVER_NAME=:8080
# Caddy writes here and will not start if it may not. The app tree needs the
# same: COPY runs as root, the server runs as www-data, and the first request
# that compiles a blade view or writes a log fails without this.
ENV XDG_CONFIG_HOME=/config XDG_DATA_HOME=/data
RUN mkdir -p /config /data \
&& chown -R www-data:www-data /config /data \
/var/www/storage /var/www/bootstrap/cache
USER www-data
EXPOSE 9000
# php-fpm speaks fastcgi on this port, not http, so the http probes the
# sibling adapters (nextjs, nestjs) use do not transplant here. the obvious
# alternative, `php -r 'exit(0);'`, only proves the php cli starts — it can
# never fail while the container is actually broken, which is worse than no
# check at all: an orchestrator with no HEALTHCHECK knows it does not know,
# one with an always-green check believes it does. a real check needs
# php-fpm's ping.path probed with cgi-fcgi, which costs a pool config file
# and a package this image does not otherwise need; add both together if
# this is ever deployed behind something that polls container health.
CMD ["php-fpm"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/up || exit 1
# CMD replaces the base image's default args entirely rather than extending
# them, and those args are what point frankenphp at the Caddyfile that
# defines the :8080 site block; without them it starts only the admin API on
# 127.0.0.1:2019 and nothing ever listens on 8080.
CMD ["frankenphp", "run", "--config", "/etc/frankenphp/Caddyfile", "--adapter", "caddyfile"]
14 changes: 13 additions & 1 deletion adapters/laravel-inertia/adapter.env
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@ ADAPTER_GENERATOR='SHELL_VERBOSITY=-1 COMPOSER_PROCESS_TIMEOUT=900 composer crea
# dependabot file in the tree, and the kit's dependabot.yml sets
# `cooldown.default-days: 5`, which fails the security gate with exit 13 on
# the first pull request. Inert config whose only effect is a red check.
ADAPTER_POST_GENERATE='rm -rf .github'
# The sed call wires routes/health.php (copied in by apply_adapter's
# directory loop, not this adapter's flat file list) into bootstrap/app.php's
# routing: laravel only auto-loads routes/web.php and routes/console.php, so
# a file dropped at routes/health.php with nothing pointing at it 404s
# forever. The grep pair after it is not optional — `s|health: '/up',|...|`
# silently no-ops if the skeleton ever reformats that line, leaving the
# route unregistered with no build failure, only a 404 discovered in
# production.
ADAPTER_POST_GENERATE='rm -rf .github && sed -i "s|health: '"'"'/up'"'"',|health: '"'"'/up'"'"',\n then: function (): void {\n require __DIR__.'"'"'/../routes/health.php'"'"';\n },|" bootstrap/app.php && { grep -q "then: function (): void {" bootstrap/app.php && grep -q "require __DIR__.'"'"'/../routes/health.php'"'"';" bootstrap/app.php; } || { echo "post-generate: health route wiring missing from bootstrap/app.php — the laravel skeleton likely changed its withRouting shape; update the sed pattern in ADAPTER_POST_GENERATE to match" >&2; exit 1; }'
# /up ships with laravel since 11.x and deliberately touches nothing, which
# is why it cannot stand alone: a project with a wrong DATABASE_URL passes it.
ADAPTER_LIVENESS_PATH="/up"
ADAPTER_READINESS_PATH="/health/ready"
21 changes: 21 additions & 0 deletions adapters/laravel-inertia/routes/health.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

use Illuminate\Support\Facades\Route;

// Registered from bootstrap/app.php's withRouting(then: ...) — laravel loads
// only routes/web.php and routes/api.php on its own, so a file dropped here
// with nothing pointing at it would 404 forever.
//
// The probe is written by the selected service's driver: laravel has no
// provider-agnostic read, so a SQL connection gets `select 1` and mongodb
// gets a ping command. A project generated with --db none keeps the
// anchor's fallback and reports 503, because there is nothing here that
// could honestly report ready.
Route::get('/health/ready', function () {
try {
// @DB_PROBE@
throw new RuntimeException('no database is configured for this project');
} catch (Throwable $e) {
return response()->json(['status' => 'unavailable', 'reason' => $e->getMessage()], 503);
}
});
15 changes: 13 additions & 2 deletions adapters/nestjs/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@ WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
# `pris[m]a`, not `prisma`: a bracket glob that matches nothing copies
# nothing instead of failing the build, which a bare `prisma` would do on a
# --db none project that never ran the driver that creates this directory.
# Needed for the migrate service (see services/shared/nest.sh) to find its
# schema — schema.prisma is not itself an artifact `nest build` produces, so
# nothing else in this image carries it forward from the build context.
COPY --from=build /app/pris[m]a ./prisma
USER node
EXPOSE 3001
# 8080 because that is the container port common/compose.yaml publishes, and
# nothing rewrites it — an adapter listening anywhere else publishes a dead
# port. Nest reads PORT in main.ts's `app.listen(process.env.PORT ?? 3000)`.
ENV PORT=8080
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3001/health || exit 1
CMD wget -qO- http://localhost:8080/health/live || exit 1
CMD ["node", "dist/main.js"]
15 changes: 13 additions & 2 deletions adapters/nestjs/Dockerfile.workspace
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,19 @@ ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/apps/@APP_FILTER@/node_modules ./apps/@APP_FILTER@/node_modules
COPY --from=build /app/apps/@APP_FILTER@/dist ./apps/@APP_FILTER@/dist
# `pris[m]a`, not `prisma`: a bracket glob that matches nothing copies
# nothing instead of failing the build, which a bare `prisma` would do on a
# --db none project that never ran the driver that creates this directory.
# Needed for the migrate service (see services/shared/nest.sh) to find its
# schema — schema.prisma is not itself an artifact `nest build` produces, so
# nothing else in this image carries it forward from the build context.
COPY --from=build /app/apps/@APP_FILTER@/pris[m]a ./apps/@APP_FILTER@/prisma
USER node
EXPOSE 3001
# 8080 because that is the container port common/compose.yaml publishes, and
# nothing rewrites it — an adapter listening anywhere else publishes a dead
# port. Nest reads PORT in main.ts's `app.listen(process.env.PORT ?? 3000)`.
ENV PORT=8080
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3001/health || exit 1
CMD wget -qO- http://localhost:8080/health/live || exit 1
CMD ["node", "apps/@APP_FILTER@/dist/main.js"]
17 changes: 15 additions & 2 deletions adapters/nestjs/adapter.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,18 @@ ADAPTER_FAMILY="nest"
# generate with the new major and confirm `//apps/api:ci-unit` passes clean.
ADAPTER_GENERATOR='pnpm dlx @nestjs/cli@11 new "$APP_DIR" --package-manager pnpm --skip-git'
# main.ts's un-awaited bootstrap() trips --max-warnings 0, and the generator's
# own output is not prettier-formatted.
ADAPTER_POST_GENERATE='sed -i "s/^bootstrap();$/void bootstrap();/" src/main.ts && pnpm exec prettier --write .'
# own output is not prettier-formatted. Wiring HealthModule here too: the
# module lives in src/health/ (copied in by apply_adapter's directory loop,
# not this adapter's flat file list), and app.module.ts is the generator's
# own file, so it can only be edited after the generator has produced it.
# The grep pair after the two sed calls is not optional: `1i` always
# succeeds, and `s/imports: \[\]/…/` silently no-ops if the generator ever
# reformats that line, leaving HealthModule unregistered with no build or
# lint failure — only a 404 on /health/live, discovered by the HEALTHCHECK
# that quietly starts failing on every image.
ADAPTER_POST_GENERATE='sed -i "s/^bootstrap();$/void bootstrap();/" src/main.ts && sed -i "1i import { HealthModule } from '"'"'./health/health.module'"'"';" src/app.module.ts && sed -i "s/imports: \[\]/imports: [HealthModule]/" src/app.module.ts && { grep -q "import { HealthModule } from '"'"'./health/health.module'"'"';" src/app.module.ts && grep -q "imports: \[HealthModule\]" src/app.module.ts || { echo "post-generate: HealthModule wiring missing from src/app.module.ts; the nest generator likely changed its output format — update the sed patterns in ADAPTER_POST_GENERATE to match" >&2; exit 1; }; } && pnpm exec prettier --write .'
# The generator produces `/` returning Hello World and nothing else. Both of
# these are routes this adapter ships itself (src/health/), because the
# Dockerfile's HEALTHCHECK has been probing a /health that never existed.
ADAPTER_LIVENESS_PATH="/health/live"
ADAPTER_READINESS_PATH="/health/ready"
14 changes: 14 additions & 0 deletions adapters/nestjs/mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ run = "pnpm exec eslint \"src/**/*.ts\" \"test/**/*.ts\" --max-warnings 0"
# prisma, and this does nothing; a real prisma failure still fails.
run = "if [ -f prisma/schema.prisma ]; then pnpm exec prisma generate; fi"

[tasks.migrate]
# prisma's mongodb provider rejects `migrate deploy` outright — measured:
# `The "mongodb" provider is not supported with this command.` — and takes
# `db push` instead. Branching on the schema rather than on a recorded value
# keeps this true for a project whose provider changes.
run = """
if ! [ -f prisma/schema.prisma ]; then exit 0; fi
if grep -q 'provider *= *"mongodb"' prisma/schema.prisma; then
pnpm exec prisma db push --skip-generate
else
pnpm exec prisma migrate deploy
fi
"""

[tasks.check]
run = [{ task = ":prisma" }, "pnpm exec tsc --noEmit"]

Expand Down
Loading