Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
fe1dae8
fix: restore server error reporting
Producdevity Oct 30, 2026
9184240
fix: label Sentry releases and pin browser CI runner
Producdevity Oct 30, 2026
439d7dc
fix: disambiguate browser checks and preserve Sentry release fallback
Producdevity Oct 30, 2026
7e968d0
test: wait for failed games request before checking page shell
Producdevity Oct 31, 2026
512a5e8
ci: deploy branch images after all checks pass
Producdevity Oct 30, 2026
dbad987
fix: load server instrumentation from src and preserve error causes
Producdevity Oct 31, 2026
24b1418
ci: retain deployment verification through transient status failures
Producdevity Oct 31, 2026
9031b1c
Merge monitoring fixes into automatic deployment branch
Producdevity Oct 31, 2026
77f3b8a
fix: keep startup responsive and preserve server diagnostics
Producdevity Oct 31, 2026
5d97182
test: wait for report table before checking accessibility
Producdevity Oct 31, 2026
34cee27
ci: recheck release head and retain health verification failures
Producdevity Oct 31, 2026
d824c98
Merge reviewed monitoring fixes into automatic deployment branch
Producdevity Oct 31, 2026
b338e80
fix: share catalog loads during startup and concurrent lookups
Producdevity Oct 31, 2026
5292700
test: verify catalog outage recovery and tRPC route reporting
Producdevity Oct 31, 2026
8304c12
Merge catalog review fixes into automatic deployment branch
Producdevity Oct 31, 2026
6305fce
ci: disable persisted checkout credentials before running dependencies
Producdevity Oct 31, 2026
56b01c0
fix: isolate catalog refreshes and bound shared downloads
Producdevity Oct 31, 2026
2607966
Merge catalog lifecycle fixes into automatic deployment branch
Producdevity Oct 31, 2026
a0efe4b
docs: clarify release ordering when newer pushes arrive
Producdevity Oct 31, 2026
121d0e4
docs: scope branch checks to automatic deployments
Producdevity Oct 31, 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
40 changes: 36 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
DATABASE_URL: "postgresql://postgres:postgres@localhost:5432/emuready_test"
Expand Down Expand Up @@ -56,10 +56,15 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Test container build configuration
run: python3 -m unittest discover -s scripts -p container_build_config_test.py

- name: Test release safety checks
run: python3 -m unittest discover -s scripts -p container_deploy_test.py

- name: Setup pnpm
uses: pnpm/action-setup@v6

Expand All @@ -74,7 +79,7 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Run database migrations
run: pnpm exec prisma migrate deploy
run: pnpm db:migrate:deploy
Comment thread
Producdevity marked this conversation as resolved.
Resolved

- name: Generate Prisma client
run: pnpm db:generate
Expand Down Expand Up @@ -107,6 +112,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@v6
Expand All @@ -122,7 +129,7 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Run database migrations
run: pnpm exec prisma migrate deploy
run: pnpm db:migrate:deploy

- name: Generate Prisma client
run: pnpm db:generate
Expand Down Expand Up @@ -152,6 +159,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@v6
Expand All @@ -170,10 +179,33 @@ jobs:
run: pnpm clean

- name: Run database migrations
run: pnpm exec prisma migrate deploy
run: pnpm db:migrate:deploy

- name: Generate Prisma client
run: pnpm db:generate

- name: Build
run: pnpm build

- name: Verify compiled server instrumentation
run: |
node -e 'const hook = require("./.next/server/instrumentation.js"); if (typeof hook.register !== "function" || typeof hook.onRequestError !== "function") process.exit(1)'

browser-tests:
name: Browser tests
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
uses: ./.github/workflows/e2e-tests.yml
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
secrets: inherit

release:
name: Publish and deploy
if: github.event_name == 'push' && (github.ref == 'refs/heads/staging' || github.ref == 'refs/heads/master')
needs: [quality, unit-tests, build, browser-tests]
permissions:
contents: read
packages: write
uses: ./.github/workflows/container-image.yml
with:
publish: true
deploy: true
secrets: inherit
37 changes: 35 additions & 2 deletions .github/workflows/container-image.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
name: Container image

on:
workflow_call:
inputs:
publish:
type: boolean
default: false
deploy:
type: boolean
default: false
workflow_dispatch:
inputs:
publish:
Expand All @@ -20,7 +28,7 @@ jobs:
image:
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/staging'
runs-on: ubuntu-latest
timeout-minutes: 45
timeout-minutes: 70
environment: ${{ github.ref == 'refs/heads/master' && 'container-production' || 'container-staging' }}
env:
DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/emuready_build
Expand All @@ -45,6 +53,18 @@ jobs:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Check release target and database compatibility
if: inputs.deploy
env:
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
COOLIFY_APPLICATION_UUID: ${{ vars.COOLIFY_APPLICATION_UUID }}
CONTAINER_APPROVED_SCHEMA_DIGEST: ${{ vars.CONTAINER_APPROVED_SCHEMA_DIGEST }}
PUBLISH_IMAGE: ${{ inputs.publish }}
run: |
test "$PUBLISH_IMAGE" = true
python3 scripts/container_deploy.py --preflight

- uses: pnpm/action-setup@v6

Expand Down Expand Up @@ -84,9 +104,12 @@ jobs:
"${build_args[@]}" --tag "$image:$tag" .
test "$(docker image inspect "$image:$tag" --format '{{.Architecture}}')" = amd64
test "$(docker image inspect "$image:$tag" --format '{{.Config.User}}')" = nextjs
docker run --rm --network none --entrypoint node "$image:$tag" \
-e 'const hook = require("./.next/server/instrumentation.js"); if (typeof hook.register !== "function" || typeof hook.onRequestError !== "function") process.exit(1)'
printf 'name=%s\ntag=%s\n' "$image" "$tag" >> "$GITHUB_OUTPUT"

- name: Publish image
id: publish
if: inputs.publish
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -98,4 +121,14 @@ jobs:
docker push "$IMAGE:$TAG"
digest=$(docker buildx imagetools inspect "$IMAGE:$TAG" --format '{{json .Manifest}}' | jq -er '.digest')
[[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]
printf 'Deploy this immutable image through Coolify Docker Image:\n\n`%s`\n' "$IMAGE@$digest" >> "$GITHUB_STEP_SUMMARY"
printf 'image=%s\n' "$IMAGE@$digest" >> "$GITHUB_OUTPUT"
printf 'Published immutable image:\n\n %s\n' "$IMAGE@$digest" >> "$GITHUB_STEP_SUMMARY"

- name: Deploy and verify release
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
if: inputs.deploy
env:
COOLIFY_TOKEN: ${{ secrets.COOLIFY_TOKEN }}
COOLIFY_APPLICATION_UUID: ${{ vars.COOLIFY_APPLICATION_UUID }}
CONTAINER_APPROVED_SCHEMA_DIGEST: ${{ vars.CONTAINER_APPROVED_SCHEMA_DIGEST }}
RELEASE_IMAGE: ${{ steps.publish.outputs.image }}
run: python3 scripts/container_deploy.py --image "$RELEASE_IMAGE"
30 changes: 6 additions & 24 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
name: E2E Tests

on:
workflow_call:
workflow_dispatch:
pull_request:
paths:
- "src/**"
- "tests/**"
- "playwright.config.ts"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"
push:
branches: [master, staging]
paths:
- "src/**"
- "tests/**"
- "playwright.config.ts"
- "package.json"
- "pnpm-lock.yaml"
- "pnpm-workspace.yaml"

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
e2e-tests:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
timeout-minutes: 30

services:
Expand Down Expand Up @@ -72,6 +52,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup pnpm
uses: pnpm/action-setup@v6
Expand All @@ -87,13 +69,13 @@ jobs:
run: pnpm install --frozen-lockfile

- name: Run database migrations
run: pnpm exec prisma migrate deploy
run: pnpm db:migrate:deploy

- name: Generate Prisma client
run: pnpm db:generate

- name: Seed test database
run: pnpm exec prisma db seed
run: pnpm db:seed

- name: Build application
run: pnpm build
Expand Down
14 changes: 8 additions & 6 deletions docs/SELF_HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ EmuReady runs as a standalone Next.js container behind Coolify and Cloudflare. S
- Set `BUILD_DATABASE_URL` to a migrated, disposable Postgres database because Prisma TypedSQL generation introspects the schema. Never use production or a restored production backup for this. Apply the existing migrations to an empty database; seeds are unnecessary.
- Optionally set `BUILD_DATABASE_DIRECT_URL` for that same disposable database. It defaults to `BUILD_DATABASE_URL`. The builder maps these names to Prisma's `DATABASE_URL` and `DATABASE_DIRECT_URL` only for `pnpm build`, and fails if `BUILD_DATABASE_URL` is missing.
- Keep the real `DATABASE_URL`, `DATABASE_DIRECT_URL`, and other runtime-only secrets, such as `CLERK_SECRET_KEY`, out of the build phase. Backups and environment files must remain excluded from the Docker context.
- For a release containing migrations, build the `migrator` target from the same commit and run it with `DATABASE_DIRECT_URL` before deploying the `app` image.
- For a release containing migrations, verify a fresh backup and obtain explicit approval, then build the `migrator` target from the same commit and run it with `DATABASE_DIRECT_URL` before deploying the `app` image.

Use the manual [Container image workflow](container-image.md) to build and publish
images on GitHub runners, then deploy their immutable digests through Coolify's
Docker Image build pack. Disable source auto-deployment to keep builds off the
application VPS. Image publication does not deploy or apply hosted migrations.
CI calls the [Container image workflow](container-image.md) after quality and
browser checks pass, then deploys its immutable digest through Coolify's Docker
Image build pack. `staging` and `master` select separate GitHub environments and
Coolify applications. Keep Coolify source auto-deployment disabled so builds stay
on GitHub runners. Manual image publication remains available; hosted migrations
require a separate approved operation.

## Coolify application

- Use the Docker Image build pack with the verified immutable digest and exposed port `3000`.
- Set runtime `APP_VERSION` to the exact source commit. The image workflow sets `NEXT_BUILD_ID` while building.
- Use the image's embedded `APP_VERSION`; disable Coolify runtime overrides for `APP_VERSION` and `NEXT_BUILD_ID`. The image workflow supplies the exact source commit during the build.
- Use `/api/health/ready` for deployment health checks and `/api/health/live` for process liveness.

## Production configuration
Expand Down
69 changes: 58 additions & 11 deletions docs/container-image.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
# Container images

The manual **Container image** workflow builds on GitHub runners and optionally
publishes to private GHCR. It does not deploy or change DNS.
Merges into `staging` and `master` run CI, including browser tests. Once all jobs
pass, CI calls **Container image** to build on GitHub runners, publish to private
GHCR, and deploy the immutable digest through Coolify. `staging` deploys to
staging; `master` deploys to production. DNS and hosted databases are not changed.

Manual dispatch on `master` or `staging` builds and optionally publishes an image, with `publish`
defaulting to false. Automatic deployment is called from CI after all checks pass.
For automatic deployments, the branch head is checked before building and
immediately before selecting the image. If it has advanced at either check, the release stops.
GitHub and Coolify do not share an atomic revision lock: a merge after that check
can briefly leave the earlier revision running until the next release finishes.
A running release is not cancelled by a newer push, so Coolify verification can
finish after image selection and deployment have started. The next release waits
for it and must pass its own CI and deployment checks.

Authenticated browser CI runs for repository branches and their pull requests.
Fork pull requests skip that job because GitHub does not provide repository
secrets to them; browser tests still run after an accepted merge into `staging`
or `master`, before publication or deployment.

Create a GitHub Actions environment **variable** named `CONTAINER_PUBLIC_BUILD_ENV`
containing a JSON object in each environment below. The workflow reads it through
Expand All @@ -19,14 +36,39 @@ application secrets: the workflow migrates an empty PostgreSQL 15 database on th
Prisma generation. Public settings are baked into the image and require a rebuild
when changed.

After the exact commit passes CI, dispatch the workflow on the corresponding
branch with `publish` enabled. Before deployment, check the target database's
applied migrations against that commit. This workflow migrates only its disposable
database and publishes only the `app` target; the application does not migrate on
startup. If migrations are pending, stop until a fresh backup is verified and the
user explicitly approves applying them. Build the same-commit `migrator` target
off the application VPS and apply the migrations as described in
[Self-hosting](SELF_HOSTING.md#build-and-release-contract) before deploying the app.
Each environment also needs a `COOLIFY_APPLICATION_UUID` variable and a
`COOLIFY_TOKEN` secret. Use a team-scoped token with `read`, `write`, and `deploy`
permissions. It inspects the non-sensitive application configuration, selects the
image digest and source commit, triggers the deployment and reads its status.
It does not need `root`, `read:sensitive` or `write:sensitive`. Store it only in
the matching branch-restricted GitHub environment and rotate it before expiry.

The deployment script refuses source build packs, the wrong branch/image
repository, missing readiness checks and pre/post deployment commands. Disable
runtime overrides for `APP_VERSION` and `NEXT_BUILD_ID` in Coolify: the image
already embeds its source commit. The public `/api/health` version and readiness
must match after Coolify finishes. A queued request alone is not success.

The schema and migration files are compared with the current public deployment's
source commit. Any difference blocks deployment unless the exact schema digest
has been approved after migration verification. The live version must identify
an exact source commit; an unknown baseline must be investigated before release.
This workflow migrates only its
disposable runner database; the application does not migrate on startup.

For a schema change, verify a fresh backup and obtain explicit approval before
applying hosted migrations. Build the same-commit `migrator` target off the
application VPS and follow
[Self-hosting](SELF_HOSTING.md#build-and-release-contract). After the migration
is verified, set the environment variable `CONTAINER_APPROVED_SCHEMA_DIGEST` to
the exact digest printed by:

```sh
GITHUB_SHA="$(git rev-parse HEAD)" python3 scripts/container_deploy.py --schema-digest
```

This approval matches only that schema/migration file set; it does not authorize
future changes. Never set it before the backup, approval and migration checks.

Before deployment, configure GHCR authentication for the Docker account used by
Coolify on every deployment server. Use a pull-only credential with `read:packages`
Expand All @@ -38,7 +80,12 @@ Deploy that immutable digest through Coolify's Docker Image build pack.
Preserve runtime credentials, domains, labels, port 3000, health checks,
certificate mounts and persistent storage. Disable
source auto-deployment so pushes cannot start builds on the application VPS.
Set runtime `APP_VERSION` to the image's exact source commit.
The image supplies `APP_VERSION` from the exact build commit.

If a release fails, inspect its deployment UUID and workflow logs before retrying.
Coolify keeps the previous container when the new container fails readiness.
The workflow prints the previous image selection for manual rollback; it does
not apply database changes or initiate an extra rollback deployment.

Verify image identity, readiness, TLS, canonical URLs, authentication and static
assets before switching public DNS. Keep the existing Vercel deployments and
Expand Down
15 changes: 0 additions & 15 deletions instrumentation.ts

This file was deleted.

17 changes: 1 addition & 16 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,32 +324,17 @@ const withBundleAnalyzer = NextBundleAnalyzer({
const analyzedConfig = withBundleAnalyzer(nextConfig)

const sentryBuildOptions = {
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/build/

org: 'hexelnet',
project: 'emuready',

// Only print logs for uploading source maps in CI
release: { name: nextBuildId || undefined },
silent: !process.env.CI,

// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,

sourcemaps: {
disable: !isVercelBuild,
},

// Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
// tunnelRoute: "/monitoring",

bundleSizeOptimizations: {
excludeDebugStatements: true,
},

telemetry: false,
}

Expand Down
Loading
Loading