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
34 changes: 34 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ jobs:
--set "*.args.GIT_COMMIT=${{ github.sha }}" \
--set "*.args.BASE_DOMAIN=testplanit.com"

- name: Smoke-test workers image (AMD64)
run: |
VERSION="${{ github.ref_name }}"
VERSION_NUM="${VERSION#v}"
IMAGE="ghcr.io/${REPO_LC}:${VERSION_NUM}-workers-amd64"
docker pull "$IMAGE"
# Fail fast if any worker entrypoint's require graph is broken
# (e.g. a missing native dep or a stripped package — the failure
# mode from the 2026-04-22 next/headers incident).
docker run --rm --entrypoint node "$IMAGE" ./scripts/smoke-test-workers.js

# Build ARM64 images natively on macOS arm64 runner (using Colima)
build-arm64:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
Expand Down Expand Up @@ -146,6 +157,17 @@ jobs:
--set "*.args.GIT_COMMIT=${{ github.sha }}" \
--set "*.args.BASE_DOMAIN=testplanit.com"

- name: Smoke-test workers image (ARM64)
run: |
VERSION="${{ github.ref_name }}"
VERSION_NUM="${VERSION#v}"
IMAGE="ghcr.io/${REPO_LC}:${VERSION_NUM}-workers-arm64"
docker pull "$IMAGE"
# Fail fast if any worker entrypoint's require graph is broken
# (e.g. a missing native dep or a stripped package — the failure
# mode from the 2026-04-22 next/headers incident).
docker run --rm --entrypoint node "$IMAGE" ./scripts/smoke-test-workers.js

# Merge architecture-specific images into multi-arch manifests
merge-manifests:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
Expand Down Expand Up @@ -259,6 +281,12 @@ jobs:
--set "*.args.GIT_COMMIT=${{ github.sha }}" \
--set "*.args.BASE_DOMAIN=testplanit.com"

- name: Smoke-test workers image (AMD64)
run: |
IMAGE="ghcr.io/${REPO_LC}:${{ github.event.inputs.tag }}-workers-amd64"
docker pull "$IMAGE"
docker run --rm --entrypoint node "$IMAGE" ./scripts/smoke-test-workers.js

# Manual build: ARM64 images natively on macOS arm64 runner (using Colima)
docker-manual-arm64:
if: github.event_name == 'workflow_dispatch'
Expand Down Expand Up @@ -314,6 +342,12 @@ jobs:
--set "*.args.GIT_COMMIT=${{ github.sha }}" \
--set "*.args.BASE_DOMAIN=testplanit.com"

- name: Smoke-test workers image (ARM64)
run: |
IMAGE="ghcr.io/${REPO_LC}:${{ github.event.inputs.tag }}-workers-arm64"
docker pull "$IMAGE"
docker run --rm --entrypoint node "$IMAGE" ./scripts/smoke-test-workers.js

# Manual build: Merge architecture-specific images into multi-arch manifests
docker-manual-merge:
if: github.event_name == 'workflow_dispatch'
Expand Down
76 changes: 76 additions & 0 deletions testplanit/scripts/smoke-test-workers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/usr/bin/env node
/**
* Smoke-test: verify every worker's compiled entry point loads cleanly
* inside the workers Docker image.
*
* Background: on 2026-04-22 the multitenant-workers deploy crashlooped
* at startup because a module imported by testmoImportWorker had a
* top-level `import "next/headers"` — but the workers image strips
* Next.js from node_modules to save ~900MB. `docker build` never
* surfaces this: the build only compiles files, never loads them at
* runtime. This script closes that gap.
*
* What it does: `require()` each compiled worker entry so Node's CJS
* resolver walks the full import graph. Any missing/unresolved module
* throws synchronously and fails this script. Only require-time errors
* count; the workers' connection attempts to Valkey/Postgres that fire
* from the main-guard (`typeof import.meta === "undefined"` branch)
* happen asynchronously and we exit(0) before they escape, so we
* don't need a live Valkey/Postgres in CI.
*
* Keep the list in sync with ecosystem.config.js + scripts/build-workers.js.
*/

const path = require("path");

// Worker entrypoints — must match scripts/build-workers.js entryPoints.
const WORKERS = [
"notificationWorker",
"emailWorker",
"forecastWorker",
"syncWorker",
"testmoImportWorker",
"elasticsearchReindexWorker",
"auditLogWorker",
"autoTagWorker",
"budgetAlertWorker",
"repoCacheWorker",
"copyMoveWorker",
"duplicateScanWorker",
"magicSelectWorker",
"stepSequenceScanWorker",
"generateFromUrlWorker",
];

const entryPoints = [
...WORKERS.map((name) => ({ name, file: `dist/workers/${name}.js` })),
{ name: "scheduler", file: "dist/scheduler.js" },
];

let failed = 0;
for (const { name, file } of entryPoints) {
const abs = path.resolve(process.cwd(), file);
try {
require(abs);
console.log(`✓ ${name}`);
} catch (err) {
failed += 1;
console.error(`✗ ${name}: ${err && err.message ? err.message : err}`);
if (err && err.stack) {
console.error(err.stack);
}
}
}

if (failed > 0) {
console.error(`\n${failed} worker entrypoint(s) failed to load.`);
// Exit on the next tick so pending error logs flush before we bail.
process.exit(1);
}

console.log(
`\nAll ${entryPoints.length} worker entrypoints loaded successfully.`
);
// Force exit to short-circuit any async startup work (BullMQ workers
// keep the event loop alive once new Worker(...) has been called).
process.exit(0);
Loading