From 766a8ff5b6cb9cff4c34dfc287df6be0f0ab9b05 Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Mon, 13 Jul 2026 15:07:00 +1000 Subject: [PATCH 1/5] fix(docker): copy css.d.ts into app build stage to fix nightly build The TypeScript 6 upgrade (#2617) added ambient 'declare module *.css' declarations in packages/app/css.d.ts to satisfy TS2882 for side-effect stylesheet imports in pages/_app.tsx. Both Dockerfiles copied mdx.d.ts but not css.d.ts, so next build failed type-checking inside the container, breaking the App, All-in-One, and Local nightly image builds. Also drops a stray 'export' on getAlertWindowStart (only used in-file) so the pre-commit knip check passes. --- .changeset/fix-nightly-css-dts-docker.md | 9 +++++++++ docker/hyperdx/Dockerfile | 2 +- packages/api/src/tasks/checkAlerts/index.ts | 2 +- packages/app/Dockerfile | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/fix-nightly-css-dts-docker.md diff --git a/.changeset/fix-nightly-css-dts-docker.md b/.changeset/fix-nightly-css-dts-docker.md new file mode 100644 index 0000000000..a1eb0d8763 --- /dev/null +++ b/.changeset/fix-nightly-css-dts-docker.md @@ -0,0 +1,9 @@ +--- +'@hyperdx/app': patch +--- + +Copy `css.d.ts` into the Docker build stage so the app image compiles. The +TypeScript 6 upgrade added ambient `declare module '*.css'` declarations in +`css.d.ts` to satisfy TS2882 for side-effect stylesheet imports, but the +Dockerfiles only copied `mdx.d.ts`, so `next build` failed inside the +container. diff --git a/docker/hyperdx/Dockerfile b/docker/hyperdx/Dockerfile index fdf8bd696e..bb1e2bb1df 100644 --- a/docker/hyperdx/Dockerfile +++ b/docker/hyperdx/Dockerfile @@ -52,7 +52,7 @@ COPY .yarnrc.yml yarn.lock package.json nx.json .prettierrc .prettierignore ./ts # full source is copied in the build stages that need it. COPY ./packages/common-utils/package.json ./packages/common-utils/package.json COPY ./packages/api/jest.config.js ./packages/api/tsconfig.json ./packages/api/tsconfig.build.json ./packages/api/package.json ./packages/api/ -COPY ./packages/app/jest.config.js ./packages/app/tsconfig.json ./packages/app/tsconfig.build.json ./packages/app/package.json ./packages/app/next.config.mjs ./packages/app/mdx.d.ts ./packages/app/eslint.config.mjs ./packages/app/ +COPY ./packages/app/jest.config.js ./packages/app/tsconfig.json ./packages/app/tsconfig.build.json ./packages/app/package.json ./packages/app/next.config.mjs ./packages/app/mdx.d.ts ./packages/app/css.d.ts ./packages/app/eslint.config.mjs ./packages/app/ # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat diff --git a/packages/api/src/tasks/checkAlerts/index.ts b/packages/api/src/tasks/checkAlerts/index.ts index 82f15525df..96606dcaa8 100644 --- a/packages/api/src/tasks/checkAlerts/index.ts +++ b/packages/api/src/tasks/checkAlerts/index.ts @@ -330,7 +330,7 @@ export const getScheduledWindowStart = ( * that history fetched up-front (see getConsecutiveWindowHistories) lines up * exactly with the window processAlert evaluates. */ -export const getAlertWindowStart = (alert: IAlert, now: Date): Date => { +const getAlertWindowStart = (alert: IAlert, now: Date): Date => { const windowSizeInMins = ms(alert.interval) / 60000; const scheduleStartAt = normalizeScheduleStartAt({ alertId: alert.id, diff --git a/packages/app/Dockerfile b/packages/app/Dockerfile index 56edc5f284..eafc9c923f 100644 --- a/packages/app/Dockerfile +++ b/packages/app/Dockerfile @@ -9,7 +9,7 @@ COPY .yarnrc.yml yarn.lock package.json nx.json .prettierrc .prettierignore ./ts # Only copy package.json for workspace resolution during yarn install; # full source is copied in the build stages that need it. COPY ./packages/common-utils/package.json ./packages/common-utils/package.json -COPY ./packages/app/jest.config.js ./packages/app/tsconfig.json ./packages/app/tsconfig.build.json ./packages/app/package.json ./packages/app/next.config.mjs ./packages/app/mdx.d.ts ./packages/app/eslint.config.mjs ./packages/app/ +COPY ./packages/app/jest.config.js ./packages/app/tsconfig.json ./packages/app/tsconfig.build.json ./packages/app/package.json ./packages/app/next.config.mjs ./packages/app/mdx.d.ts ./packages/app/css.d.ts ./packages/app/eslint.config.mjs ./packages/app/ RUN --mount=type=cache,target=/root/.yarn/berry/cache,id=yarn-cache \ yarn install --mode=skip-build && yarn cache clean From 223d23ba45e63a1e0750806a8b3c087e41463e01 Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Mon, 13 Jul 2026 15:14:15 +1000 Subject: [PATCH 2/5] ci: build release Docker images on PRs (conditional, no push) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A source-level type check can't catch failures that only surface inside the Docker image — like the css.d.ts declaration file that existed in the repo but was never COPYed into the build stage, which broke every nightly image build. Add a Docker Build workflow that builds each released image with push:false so these are caught in PR review. Builds are conditional on changed paths (via tj-actions/changed-files, matching the existing otel-collector jobs in main.yml) so unrelated PRs skip them: - OTel Collector image: docker/otel-collector or packages/otel-collector - App/prod image: api/app/common-utils/docker-hyperdx or root deps - All-in-one image: the above plus clickhouse/otel bundled-service inputs Single-arch (amd64) and warmed from the nightly gha cache scopes to keep runtimes low; build errors are arch-independent. --- .github/workflows/docker-build.yml | 147 +++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 .github/workflows/docker-build.yml diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000000..4d1d7fc7b7 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,147 @@ +name: Docker Build +# Verifies the release Docker images actually build on PRs, without pushing. +# A source-level type check (`make ci-lint`) cannot catch build failures that +# only surface inside the image — e.g. a declaration file that exists in the +# repo but was never COPYed into the Docker build stage. Each image is built +# only when files that affect it change, so unrelated PRs stay fast. +on: + pull_request: + branches: [main] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +permissions: + contents: read +jobs: + # OTel Collector image (Go build via OCB) — docker/otel-collector/Dockerfile. + otel-collector-image: + name: Build OTel Collector Image + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v47.0.6 + with: + files: | + docker/otel-collector/** + packages/otel-collector/** + - name: Setup Docker Buildx + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/setup-buildx-action@v3 + - name: Build (no push) + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/otel-collector/Dockerfile + target: prod + platforms: linux/amd64 + push: false + cache-from: | + type=gha,scope=otel-collector-nightly-amd64 + type=gha,scope=docker-build-pr-otel-collector + cache-to: type=gha,mode=max,scope=docker-build-pr-otel-collector + + # App/prod image (API + App + common-utils Node build) — docker/hyperdx target prod. + # This is the image the nightly css.d.ts failure surfaced in; the same builder + # stage backs the all-in-one targets below. + app-image: + name: Build App Image + runs-on: ubuntu-24.04 + timeout-minutes: 25 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v47.0.6 + with: + files: | + packages/api/** + packages/app/** + packages/common-utils/** + docker/hyperdx/** + package.json + yarn.lock + .yarn/** + tsconfig.base.json + nx.json + - name: Setup Docker Buildx + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/setup-buildx-action@v3 + - name: Build (no push) + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/hyperdx/Dockerfile + target: prod + platforms: linux/amd64 + push: false + build-contexts: | + hyperdx=./docker/hyperdx + api=./packages/api + app=./packages/app + build-args: | + CODE_VERSION=pr-${{ github.event.pull_request.number }} + cache-from: | + type=gha,scope=app-nightly-amd64 + type=gha,scope=docker-build-pr-app + cache-to: type=gha,mode=max,scope=docker-build-pr-app + + # All-in-one image (adds ClickHouse, MongoDB, OTel Collector to the app build) + # — docker/hyperdx target all-in-one-auth. Building the auth target also + # exercises the shared all-in-one-base, so no-auth need not be built + # separately. Gated on the app inputs plus the bundled-service inputs. + all-in-one-image: + name: Build All-in-One Image + runs-on: ubuntu-24.04 + timeout-minutes: 40 + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v47.0.6 + with: + files: | + packages/api/** + packages/app/** + packages/common-utils/** + packages/otel-collector/** + docker/hyperdx/** + docker/clickhouse/** + docker/otel-collector/** + package.json + yarn.lock + .yarn/** + tsconfig.base.json + nx.json + - name: Setup Docker Buildx + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/setup-buildx-action@v3 + - name: Build (no push) + if: steps.changed-files.outputs.any_changed == 'true' + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/hyperdx/Dockerfile + target: all-in-one-auth + platforms: linux/amd64 + push: false + build-contexts: | + clickhouse=./docker/clickhouse + otel-collector=./docker/otel-collector + hyperdx=./docker/hyperdx + api=./packages/api + app=./packages/app + build-args: | + CODE_VERSION=pr-${{ github.event.pull_request.number }} + cache-from: | + type=gha,scope=all-in-one-nightly-amd64 + type=gha,scope=docker-build-pr-all-in-one + cache-to: type=gha,mode=max,scope=docker-build-pr-all-in-one From 44631818d5178f11108bb187faf8547bee4c2c14 Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Mon, 13 Jul 2026 16:14:06 +1000 Subject: [PATCH 3/5] ci: stop rebuilding the app inside the all-in-one docker check The all-in-one target does COPY --from=prod /app /app, so it already builds the full App/prod image. With the app paths in both filters, every app change ran both jobs and built the Node app twice (~8 wasted minutes). Gate the all-in-one job only on the incremental bundling inputs (ClickHouse / OTel / the shared Dockerfile); api/app/common-utils/deps are covered by the App Image job and can't affect the bundling layers. --- .github/workflows/docker-build.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 4d1d7fc7b7..3a4ff8e591 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -96,7 +96,14 @@ jobs: # All-in-one image (adds ClickHouse, MongoDB, OTel Collector to the app build) # — docker/hyperdx target all-in-one-auth. Building the auth target also # exercises the shared all-in-one-base, so no-auth need not be built - # separately. Gated on the app inputs plus the bundled-service inputs. + # separately. + # + # This target's base does `COPY --from=prod /app /app`, so it already + # rebuilds the entire App/prod image. To avoid duplicating that ~8-minute + # Node build on every app change, this job is gated ONLY on the incremental + # bundling inputs (ClickHouse / OTel / the shared Dockerfile) — NOT on + # api/app/common-utils/deps. Those are covered by the App Image job above, + # and they can't break the bundling layers, which only COPY the prod output. all-in-one-image: name: Build All-in-One Image runs-on: ubuntu-24.04 @@ -109,18 +116,10 @@ jobs: uses: tj-actions/changed-files@v47.0.6 with: files: | - packages/api/** - packages/app/** - packages/common-utils/** packages/otel-collector/** docker/hyperdx/** docker/clickhouse/** docker/otel-collector/** - package.json - yarn.lock - .yarn/** - tsconfig.base.json - nx.json - name: Setup Docker Buildx if: steps.changed-files.outputs.any_changed == 'true' uses: docker/setup-buildx-action@v3 From e428cf114e0bd6ba82f0834a4d23b3a9dd31b2a2 Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Mon, 13 Jul 2026 16:31:40 +1000 Subject: [PATCH 4/5] ci: cover missing build inputs and fix workflow_dispatch in docker-build Add .yarnrc.yml, .prettierrc, .prettierignore to the app-image changed-files gate and .vex/** to the all-in-one gate, so a PR touching only those Dockerfile inputs still runs the build check. Make workflow_dispatch force-build all three jobs (changed-files finds nothing without a PR payload) and fall back to github.run_id for CODE_VERSION so it is never a bare 'pr-'. --- .github/workflows/docker-build.yml | 32 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 3a4ff8e591..d193fc17a0 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -30,10 +30,14 @@ jobs: docker/otel-collector/** packages/otel-collector/** - name: Setup Docker Buildx - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/setup-buildx-action@v3 - name: Build (no push) - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/build-push-action@v6 with: context: . @@ -68,13 +72,20 @@ jobs: package.json yarn.lock .yarn/** + .yarnrc.yml + .prettierrc + .prettierignore tsconfig.base.json nx.json - name: Setup Docker Buildx - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/setup-buildx-action@v3 - name: Build (no push) - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/build-push-action@v6 with: context: . @@ -87,7 +98,7 @@ jobs: api=./packages/api app=./packages/app build-args: | - CODE_VERSION=pr-${{ github.event.pull_request.number }} + CODE_VERSION=pr-${{ github.event.pull_request.number || github.run_id }} cache-from: | type=gha,scope=app-nightly-amd64 type=gha,scope=docker-build-pr-app @@ -120,11 +131,16 @@ jobs: docker/hyperdx/** docker/clickhouse/** docker/otel-collector/** + .vex/** - name: Setup Docker Buildx - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/setup-buildx-action@v3 - name: Build (no push) - if: steps.changed-files.outputs.any_changed == 'true' + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' uses: docker/build-push-action@v6 with: context: . @@ -139,7 +155,7 @@ jobs: api=./packages/api app=./packages/app build-args: | - CODE_VERSION=pr-${{ github.event.pull_request.number }} + CODE_VERSION=pr-${{ github.event.pull_request.number || github.run_id }} cache-from: | type=gha,scope=all-in-one-nightly-amd64 type=gha,scope=docker-build-pr-all-in-one From 5e069cbf83f4a4cc427c3437815e0ba78dbaf36d Mon Sep 17 00:00:00 2001 From: Jordan Simonovski Date: Mon, 13 Jul 2026 16:39:30 +1000 Subject: [PATCH 5/5] ci: build all-in-one-noauth (Local) image in docker-build The all-in-one job only built the auth target, but noauth is a sibling off all-in-one-base with its own entry-script COPY layer. A break there (gated under docker/hyperdx/**, so the job runs) would pass the PR check and fail the nightly Local build. Build both siblings; the base is reused from buildx's local cache so noauth only adds its one COPY layer. --- .github/workflows/docker-build.yml | 37 ++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index d193fc17a0..b2c11a052c 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -105,9 +105,13 @@ jobs: cache-to: type=gha,mode=max,scope=docker-build-pr-app # All-in-one image (adds ClickHouse, MongoDB, OTel Collector to the app build) - # — docker/hyperdx target all-in-one-auth. Building the auth target also - # exercises the shared all-in-one-base, so no-auth need not be built - # separately. + # — docker/hyperdx targets all-in-one-auth AND all-in-one-noauth (the Local + # image). The two are siblings off the shared all-in-one-base, each with its + # own final COPY of an entry script (entry.local.auth.sh / .noauth.sh), so + # building auth alone would NOT catch a break in the noauth-specific layer — + # which is what surfaced in the nightly. Both are built here; the base is + # built once and reused from buildx's local cache for the second target, so + # the noauth build only adds its one tiny COPY layer. # # This target's base does `COPY --from=prod /app /app`, so it already # rebuilds the entire App/prod image. To avoid duplicating that ~8-minute @@ -137,7 +141,7 @@ jobs: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'workflow_dispatch' uses: docker/setup-buildx-action@v3 - - name: Build (no push) + - name: Build all-in-one-auth (no push) if: steps.changed-files.outputs.any_changed == 'true' || github.event_name == 'workflow_dispatch' @@ -160,3 +164,28 @@ jobs: type=gha,scope=all-in-one-nightly-amd64 type=gha,scope=docker-build-pr-all-in-one cache-to: type=gha,mode=max,scope=docker-build-pr-all-in-one + # noauth is a sibling of auth off the shared all-in-one-base; the base is + # already in buildx's local cache from the step above, so this only builds + # the final noauth COPY layer. + - name: Build all-in-one-noauth / Local (no push) + if: + steps.changed-files.outputs.any_changed == 'true' || github.event_name + == 'workflow_dispatch' + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/hyperdx/Dockerfile + target: all-in-one-noauth + platforms: linux/amd64 + push: false + build-contexts: | + clickhouse=./docker/clickhouse + otel-collector=./docker/otel-collector + hyperdx=./docker/hyperdx + api=./packages/api + app=./packages/app + build-args: | + CODE_VERSION=pr-${{ github.event.pull_request.number || github.run_id }} + cache-from: | + type=gha,scope=all-in-one-nightly-amd64 + type=gha,scope=docker-build-pr-all-in-one