mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-01 03:52:17 +03:00
Compare commits
7 Commits
docs/opena
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b342c1a361 | ||
|
|
550541c175 | ||
|
|
4febee9415 | ||
|
|
9dc6500ebd | ||
|
|
2af28c4e2c | ||
|
|
6f1f1668dd | ||
|
|
8aa3f1e5ab |
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -976,7 +976,11 @@ jobs:
|
|||||||
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
||||||
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
|
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
|
||||||
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
||||||
timeout-minutes: 20
|
# 30, not 20 (2026-08-29): the informational Codecov upload below hung for the rest of
|
||||||
|
# the budget on two consecutive main runs (33207760653, 33215115341); the job ended
|
||||||
|
# `cancelled` and dragged the whole run's conclusion to `cancelled` although every
|
||||||
|
# blocking job was green. The upload step now has its own ceiling; this is headroom.
|
||||||
|
timeout-minutes: 30
|
||||||
needs: test-unit
|
needs: test-unit
|
||||||
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
||||||
env:
|
env:
|
||||||
@@ -1055,6 +1059,10 @@ jobs:
|
|||||||
# (if-no-files-found: warn) — Sonar consumes the same file.
|
# (if-no-files-found: warn) — Sonar consumes the same file.
|
||||||
- name: Upload coverage to Codecov (informational)
|
- name: Upload coverage to Codecov (informational)
|
||||||
if: always()
|
if: always()
|
||||||
|
# Informational means informational: its own ceiling and continue-on-error, so a
|
||||||
|
# stalled upload can neither eat the job's budget nor turn a green job cancelled.
|
||||||
|
timeout-minutes: 5
|
||||||
|
continue-on-error: true
|
||||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||||
with:
|
with:
|
||||||
files: coverage/lcov.info
|
files: coverage/lcov.info
|
||||||
|
|||||||
3
.github/workflows/docker-publish.yml
vendored
3
.github/workflows/docker-publish.yml
vendored
@@ -485,6 +485,9 @@ jobs:
|
|||||||
severity: CRITICAL
|
severity: CRITICAL
|
||||||
ignore-unfixed: true
|
ignore-unfixed: true
|
||||||
exit-code: "1"
|
exit-code: "1"
|
||||||
|
# Explicit: the advisory scan above already points at it, and the blocking
|
||||||
|
# gate must honour the same accepted-risk list (#12084).
|
||||||
|
trivyignores: .trivyignore
|
||||||
|
|
||||||
- name: Upload Trivy SARIF to Security tab
|
- name: Upload Trivy SARIF to Security tab
|
||||||
if: needs.prepare.outputs.version != 'main'
|
if: needs.prepare.outputs.version != 'main'
|
||||||
|
|||||||
27
.github/workflows/electron-release.yml
vendored
27
.github/workflows/electron-release.yml
vendored
@@ -4,12 +4,21 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
# A dispatch builds the ref it is dispatched ON (`gh workflow run … --ref v3.8.50` rebuilds
|
||||||
|
# that tag; `--ref main` builds the repaired line). The ref is deliberately NOT an input:
|
||||||
|
# CodeQL flags an input-controlled checkout next to the npm cache on the default branch as
|
||||||
|
# cache poisoning (actions/cache-poisoning/poisonable-step), and `github.ref` is trusted.
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
description: "Release version (e.g., v1.6.8)"
|
description: "Release version (e.g., v1.6.8)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
publish_npm:
|
||||||
|
description: "Also run the npm publish leg (turn off when re-attaching desktop assets to a release whose npm package already shipped)"
|
||||||
|
required: false
|
||||||
|
default: true
|
||||||
|
type: boolean
|
||||||
|
|
||||||
# Least-privilege default: read-only at the top level; each job grants the writes it
|
# Least-privilege default: read-only at the top level; each job grants the writes it
|
||||||
# needs (build/release upload assets, publish-npm forwards npm provenance / packages
|
# needs (build/release upload assets, publish-npm forwards npm provenance / packages
|
||||||
@@ -404,7 +413,14 @@ jobs:
|
|||||||
tag_name: ${{ needs.validate.outputs.version }}
|
tag_name: ${{ needs.validate.outputs.version }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: false
|
||||||
generate_release_notes: true
|
# NEVER. Phase 3 of the release flow creates the GitHub Release with the curated
|
||||||
|
# notes seconds after pushing the tag, so by the time this step runs (1-2 h of
|
||||||
|
# builds later) the body already exists — and `true` APPENDS GitHub's
|
||||||
|
# auto-generated "What's Changed" block to it (v3.8.48 shipped that way; the
|
||||||
|
# v3.8.50 re-attach dispatch added +1,416 chars to a 121 KB body, run
|
||||||
|
# 33238093090). A curated body sits ~3 KB under the 125,000-char cap, so the
|
||||||
|
# append can also turn this step RED and leave the release with no assets.
|
||||||
|
generate_release_notes: false
|
||||||
fail_on_unmatched_files: false
|
fail_on_unmatched_files: false
|
||||||
files: |
|
files: |
|
||||||
release-assets/*.dmg
|
release-assets/*.dmg
|
||||||
@@ -462,11 +478,20 @@ jobs:
|
|||||||
publish-npm:
|
publish-npm:
|
||||||
name: Publish to npm
|
name: Publish to npm
|
||||||
needs: [validate, release]
|
needs: [validate, release]
|
||||||
|
# A re-dispatch that only re-attaches desktop assets must not publish the npm package again.
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' || inputs.publish_npm }}
|
||||||
permissions:
|
permissions:
|
||||||
# Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose
|
# Must be `write`, not `read`: this job calls the reusable npm-publish.yml whose
|
||||||
# `publish` job needs `contents: write` (gh release upload — attach the SBOM, #3874).
|
# `publish` job needs `contents: write` (gh release upload — attach the SBOM, #3874).
|
||||||
# A reusable workflow's job cannot request more permission than the caller grants,
|
# A reusable workflow's job cannot request more permission than the caller grants,
|
||||||
# so a `read` here makes GitHub reject the run at startup (startup_failure).
|
# so a `read` here makes GitHub reject the run at startup (startup_failure).
|
||||||
|
#
|
||||||
|
# `actions: read` for the same reason: the called `publish` job downloads the next-build
|
||||||
|
# artefact and requests it. v3.8.50 (run 33005490476) died at startup with "The nested
|
||||||
|
# job 'publish' is requesting 'actions: read', but is only allowed 'actions: none'" — and
|
||||||
|
# because `release` lives in this same workflow, the tag shipped with ZERO assets. Keep
|
||||||
|
# this block a superset of every job's permissions in npm-publish.yml.
|
||||||
|
actions: read
|
||||||
contents: write
|
contents: write
|
||||||
id-token: write # npm provenance (forwarded to the reusable workflow)
|
id-token: write # npm provenance (forwarded to the reusable workflow)
|
||||||
packages: write # publish to npm.pkg.github.com
|
packages: write # publish to npm.pkg.github.com
|
||||||
|
|||||||
39
.github/workflows/nightly-release-green.yml
vendored
39
.github/workflows/nightly-release-green.yml
vendored
@@ -196,6 +196,26 @@ jobs:
|
|||||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Close tracking issue when the branch is green again
|
||||||
|
if: steps.validate.outputs.exit == '0'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
TARGET: ${{ steps.branch.outputs.target }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# The open/update step above is the UPWARD half of the loop; without this
|
||||||
|
# step a stale "not green" issue outlives the fix and every base-green check
|
||||||
|
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||||||
|
TITLE="🔴 Release branch not green: ${TARGET}"
|
||||||
|
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||||||
|
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||||||
|
--comment "✅ \`${TARGET}\` is release-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||||||
|
echo "Closed issue #$EXISTING"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload report artifact
|
- name: Upload report artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
@@ -294,6 +314,25 @@ jobs:
|
|||||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Close tracking issue when the branch is green again
|
||||||
|
if: steps.validate.outputs.exit == '0'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# The open/update step above is the UPWARD half of the loop; without this
|
||||||
|
# step a stale "not green" issue outlives the fix and every base-green check
|
||||||
|
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||||||
|
TITLE="🔴 main branch not green"
|
||||||
|
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||||||
|
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||||
|
if [ -n "$EXISTING" ]; then
|
||||||
|
gh issue close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||||||
|
--comment "✅ \`main\` is main-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||||||
|
echo "Closed issue #$EXISTING"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload report artifact
|
- name: Upload report artifact
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
|
|||||||
15
.github/workflows/npm-publish.yml
vendored
15
.github/workflows/npm-publish.yml
vendored
@@ -273,11 +273,20 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Attach SBOM to GitHub Release
|
- name: Attach SBOM to GitHub Release
|
||||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'release'
|
# Not only on the `release` event: the v3.8.50 package shipped through a
|
||||||
|
# workflow_dispatch (staged publish, 11 attempts) and this step was skipped, so the
|
||||||
|
# GitHub Release carried no SBOM until it was attached by hand from the run's
|
||||||
|
# `sbom-npm` artifact. Attach whenever a release for the published tag exists.
|
||||||
|
if: steps.resolve.outputs.skip != 'true' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.event_name == 'release' && github.ref_name || format('v{0}', inputs.version) }}
|
||||||
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
|
run: |
|
||||||
|
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||||
|
echo "::notice::no GitHub Release for $TAG yet — SBOM stays on the sbom-npm workflow artifact"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
gh release upload "$TAG" sbom-npm.cdx.json --repo "$GITHUB_REPOSITORY" --clobber
|
||||||
|
|
||||||
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
|
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
|
||||||
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
|
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
|
||||||
|
|||||||
5
.github/workflows/scorecard.yml
vendored
5
.github/workflows/scorecard.yml
vendored
@@ -4,12 +4,15 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "27 7 * * 1"
|
- cron: "27 7 * * 1"
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
# Scorecard only accepts the DEFAULT branch — here the active release/vX.Y.Z,
|
||||||
|
# not `main`. The job below guards on it so a push to any other branch skips.
|
||||||
|
branches: ["main", "release/**"]
|
||||||
|
|
||||||
permissions: read-all
|
permissions: read-all
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analysis:
|
analysis:
|
||||||
|
if: ${{ github.event_name != 'push' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||||
name: Scorecard analysis
|
name: Scorecard analysis
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
10
.trivyignore
10
.trivyignore
@@ -19,4 +19,12 @@
|
|||||||
# Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a
|
# Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a
|
||||||
# patched base / bump the dep) over suppressing. Stale entries are debt.
|
# patched base / bump the dep) over suppressing. Stale entries are debt.
|
||||||
#
|
#
|
||||||
# (No accepted-risk suppressions at present — ignore-unfixed covers the noise.)
|
# CVE-2025-68121 — Go stdlib crypto/tls (session-resumption certificate validation)
|
||||||
|
# inside the PREBUILT bogdanfinn/tls-client v1.15.1 .so that tls-client-node's
|
||||||
|
# postinstall downloads (built with go 1.24.1; fixed in 1.24.13). No upstream
|
||||||
|
# rebuild exists (v1.15.1 is still the latest release) and nothing in this repo
|
||||||
|
# can bump it. The binary is only loaded by the browser-TLS web-provider
|
||||||
|
# executors (claude-web / grok-web / lmarena / perplexity-web / notion-web),
|
||||||
|
# whose handshakes go through utls. Tracking issue: #12084. Revisit at the next
|
||||||
|
# tls-client release or base-image bump and BEFORE the v3.8.51 tag (2026-09-15).
|
||||||
|
CVE-2025-68121
|
||||||
|
|||||||
36
Dockerfile
36
Dockerfile
@@ -184,19 +184,29 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
|
|||||||
# silently leaving no standalone bundle. Next derives the worker count from
|
# silently leaving no standalone bundle. Next derives the worker count from
|
||||||
# CIRCLE_NODE_TOTAL (workers = N-1). (#10060)
|
# CIRCLE_NODE_TOTAL (workers = N-1). (#10060)
|
||||||
#
|
#
|
||||||
# Lowered 8 → 3 (7 workers → 2). Every page-data worker inherits NODE_OPTIONS
|
# Lowered 8 → 3 (7 workers → 2) in #11419, then 3 → 2 (2 workers → 1) in #7518.
|
||||||
# above, so the ceiling is per PROCESS, not per build: 7 workers on a 16 GB
|
# Every page-data worker inherits NODE_OPTIONS above, so the ceiling is per
|
||||||
# GitHub runner (ubuntu-24.04 / ubuntu-24.04-arm, 4 vCPU) exhausted the host and
|
# PROCESS, not per build: 7 workers on a 16 GB GitHub runner (ubuntu-24.04 /
|
||||||
# buildkit failed the whole step with `ResourceExhausted: ... cannot allocate
|
# ubuntu-24.04-arm, 4 vCPU) exhausted the host and buildkit failed the whole
|
||||||
# memory`. The compile phase always finished ("✓ Compiled successfully in
|
# step with `ResourceExhausted: ... cannot allocate memory`. The compile phase
|
||||||
# 4.2min"); the kernel killed the build right after "Collecting page data using
|
# always finished ("✓ Compiled successfully in 4.2min"); the kernel killed the
|
||||||
# 7 workers". It was intermittent for a while and went 100% on 2026-08-22, which
|
# build right after "Collecting page data using N workers".
|
||||||
# is what a threshold being crossed by ordinary codebase growth looks like.
|
#
|
||||||
# tests/unit/docker-build-memory-budget.test.ts does the arithmetic and fails if
|
# #11419's first fix (8 → 3) modeled the per-worker peak as an INFERENCE
|
||||||
# either knob is raised past what a 16 GB runner holds. 2 workers also stops
|
# (2560 MB, guessed from "7 workers didn't fit") and assumed the parent
|
||||||
# oversubscribing the runner's 4 vCPU, which 7 did. Override for a big builder:
|
# process's RSS tracked the V8 heap ceiling. Both assumptions were wrong: a
|
||||||
# `--build-arg OMNIROUTE_BUILD_WORKERS=8`.
|
# live VPS reproduction (issue #7518, dmesg OOM-killer report) measured the
|
||||||
ARG OMNIROUTE_BUILD_WORKERS=3
|
# real per-process RSS directly at ~4.5 GB, independent of the NODE_OPTIONS
|
||||||
|
# heap flag (Turbopack itself is native/Rust, outside the V8 heap) — and it
|
||||||
|
# applies to the parent process too, not just workers. 2 workers (3 processes
|
||||||
|
# × 4.5 GB = 13.5 GB) still didn't fit the 12.288 GB (75%) budget on a 16 GB
|
||||||
|
# runner, matching the still-live publish failures after #11419 merged. 1
|
||||||
|
# worker (2 processes × 4.5 GB = 9 GB) fits with headroom to spare.
|
||||||
|
# tests/unit/docker-build-memory-budget.test.ts does the arithmetic against
|
||||||
|
# the measured figure and fails if either knob is raised past what a 16 GB
|
||||||
|
# runner holds. Override for a big builder: `--build-arg
|
||||||
|
# OMNIROUTE_BUILD_WORKERS=8`.
|
||||||
|
ARG OMNIROUTE_BUILD_WORKERS=2
|
||||||
ENV CIRCLE_NODE_TOTAL=${OMNIROUTE_BUILD_WORKERS}
|
ENV CIRCLE_NODE_TOTAL=${OMNIROUTE_BUILD_WORKERS}
|
||||||
|
|
||||||
COPY . ./
|
COPY . ./
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
||||||
FROM oven/bun:1.3.14-slim AS base
|
FROM oven/bun:1.4.0-slim AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
@@ -34,8 +34,10 @@ RUN if [ -f "node_modules/tls-client-node/scripts/postinstall.js" ]; then \
|
|||||||
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
|
bun node_modules/tls-client-node/scripts/postinstall.js || true; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Disable Turbopack for Bun builder stage (Turbopack V8 internal worker bindings require Node)
|
# Turbopack is supported on Bun 1.4+ (Next 16.3); override via
|
||||||
ENV OMNIROUTE_USE_TURBOPACK=0
|
# --build-arg OMNIROUTE_USE_TURBOPACK=0 to force the webpack fallback.
|
||||||
|
ARG OMNIROUTE_USE_TURBOPACK=1
|
||||||
|
ENV OMNIROUTE_USE_TURBOPACK=${OMNIROUTE_USE_TURBOPACK}
|
||||||
|
|
||||||
ARG OMNIROUTE_BASE_PATH=""
|
ARG OMNIROUTE_BASE_PATH=""
|
||||||
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
||||||
@@ -46,6 +48,33 @@ ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED
|
|||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Cap the Next.js build heap and page-data worker pool inside the Bun image the
|
||||||
|
# same way the node Dockerfile does (#10060/#11419/#7518). Without these knobs
|
||||||
|
# Next falls back to its defaults: worker pool = os.cpus()-1 (3 on the 4-vCPU
|
||||||
|
# GitHub runner) and an 8 GB V8 heap ceiling per process. 4+ V8 processes at
|
||||||
|
# multi-GB each blow past the 16 GB runner, the cgroup OOM killer SIGKILLs a
|
||||||
|
# build worker mid-compile, and buildx fails the step with `ResourceExhausted:
|
||||||
|
# ... cannot allocate memory` — every Bun image published on main since the -bun
|
||||||
|
# targets landed (#11709, #11039).
|
||||||
|
#
|
||||||
|
# The per-process peak is a MEASURED ~4.5 GB RSS (dmesg OOM-killer report,
|
||||||
|
# #7518), independent of NODE_OPTIONS — Turbopack is native/Rust and compiles
|
||||||
|
# outside the V8 heap — and it applies to the parent process too, so 2 page-data
|
||||||
|
# workers (3 processes × 4.5 GB ≈ 13.5 GB) do not fit the 12.288 GB (75%)
|
||||||
|
# budget either. Both images therefore default to OMNIROUTE_BUILD_WORKERS=2
|
||||||
|
# (1 page-data worker): 2 processes × 4.5 GB ≈ 9 GB fits with headroom (#11663).
|
||||||
|
# The default Turbopack path keeps the compile outside the V8 heap, but the
|
||||||
|
# guards must hold for the webpack fallback (OMNIROUTE_USE_TURBOPACK=0) too, so
|
||||||
|
# they are wired exactly like the node image.
|
||||||
|
#
|
||||||
|
# NODE_OPTIONS propagates to the spawned `next build` child and its workers
|
||||||
|
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env), so the
|
||||||
|
# ceiling is per PROCESS, not per build.
|
||||||
|
ARG OMNIROUTE_BUILD_MEMORY_MB=6144
|
||||||
|
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
|
||||||
|
ARG OMNIROUTE_BUILD_WORKERS=2
|
||||||
|
ENV CIRCLE_NODE_TOTAL=${OMNIROUTE_BUILD_WORKERS}
|
||||||
|
|
||||||
# Bun native Next.js build execution
|
# Bun native Next.js build execution
|
||||||
RUN bun run --quiet build
|
RUN bun run --quiet build
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
- **fix(docker):** bump the Bun image to 1.4.0, enable Turbopack on Bun, and port the node image's build memory guards so the `-bun` container builds fit the 16 GB GitHub runner instead of dying with `cannot allocate memory` ([#11719](https://github.com/diegosouzapw/OmniRoute/pull/11719)). Both images now default `OMNIROUTE_BUILD_WORKERS` to `2` (1 page-data worker) against the measured ~4.5 GB per-process RSS budget (#7518/#11663).
|
||||||
1
changelog.d/fixes/v3850-electron-lockfile-build-ref.md
Normal file
1
changelog.d/fixes/v3850-electron-lockfile-build-ref.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Electron release: `electron/package-lock.json` regained the optional `electron-builder-squirrel-windows` subtree (13 entries) that `npm ci` had been refusing as out of sync — the Linux desktop leg died on it — and `electron-release.yml` gained a `build_ref` dispatch input so a release whose tag was cut with the broken lock can have its assets rebuilt from the repaired line
|
||||||
1
changelog.d/fixes/v3850-electron-release-assets.md
Normal file
1
changelog.d/fixes/v3850-electron-release-assets.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- Electron release workflow: the `publish-npm` job now grants `actions: read` to the reusable `npm-publish.yml` it calls (its `publish` job requests it), which is what made GitHub refuse the whole v3.8.50 run at startup and ship the release with zero desktop assets; a `workflow_dispatch` now builds the requested tag instead of the dispatching branch and can skip the npm leg (`publish_npm=false`) when only re-attaching assets
|
||||||
1
changelog.d/fixes/v3850-sbom-attach-on-dispatch.md
Normal file
1
changelog.d/fixes/v3850-sbom-attach-on-dispatch.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
- npm publish workflow: the CycloneDX SBOM is attached to the GitHub Release on `workflow_dispatch` publishes too (when a release for the tag exists), not only on the `release` event — v3.8.50 shipped through a staged dispatch and its release carried no SBOM until it was attached by hand from the run's `sbom-npm` artifact
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
- `Coverage` job on `ci.yml`: the informational Codecov upload gets its own 5-minute ceiling and `continue-on-error`, and the job budget grows from 20 to 30 minutes (the 8-shard c8 merge alone takes ~10) — a stalled upload no longer ends the job `cancelled` and drags a fully green `main` run's conclusion down with it
|
||||||
@@ -226,16 +226,22 @@ Three build args control what the `builder` stage costs. They are build-time onl
|
|||||||
| --------------------------- | ------- | ----------------------------------------------------------------------------------- |
|
| --------------------------- | ------- | ----------------------------------------------------------------------------------- |
|
||||||
| `OMNIROUTE_USE_TURBOPACK` | `1` | `0` builds with webpack instead. Lower peak memory, slower. |
|
| `OMNIROUTE_USE_TURBOPACK` | `1` | `0` builds with webpack instead. Lower peak memory, slower. |
|
||||||
| `OMNIROUTE_BUILD_MEMORY_MB` | `6144` | V8 heap ceiling (`--max-old-space-size`) for the spawned `next build`. |
|
| `OMNIROUTE_BUILD_MEMORY_MB` | `6144` | V8 heap ceiling (`--max-old-space-size`) for the spawned `next build`. |
|
||||||
| `OMNIROUTE_BUILD_WORKERS` | `3` | Feeds `CIRCLE_NODE_TOTAL`; Next derives `workers = N - 1` for page-data collection. |
|
| `OMNIROUTE_BUILD_WORKERS` | `2` | Feeds `CIRCLE_NODE_TOTAL`; Next derives `workers = N - 1` for page-data collection. |
|
||||||
|
|
||||||
`OMNIROUTE_BUILD_WORKERS` is the one to raise on a big builder and the one to
|
`OMNIROUTE_BUILD_WORKERS` is the one to raise on a big builder and the one to
|
||||||
suspect when a constrained build dies **after** `✓ Compiled successfully`. Each
|
suspect when a constrained build dies **after** `✓ Compiled successfully`. Each
|
||||||
page-data worker is its own process and inherits `NODE_OPTIONS`, so the heap
|
page-data worker is its own process, and so is the parent `next build` itself;
|
||||||
ceiling is per process, not per build: the default of `3` (→ 2 workers) is sized
|
a live VPS reproduction (issue #7518) measured each process's peak RSS at
|
||||||
for the 16 GB / 4 vCPU GitHub-hosted runners the publish pipeline uses. At `8`
|
~4.5 GB independent of the `NODE_OPTIONS` heap flag (Turbopack compiles in
|
||||||
(→ 7 workers) that runner ran out of memory and buildkit failed the step with
|
native/Rust memory outside the V8 heap). The default of `2` (→ 1 worker, 2
|
||||||
`ResourceExhausted: ... cannot allocate memory`. `tests/unit/docker-build-memory-budget.test.ts`
|
processes total) is sized for the 16 GB / 4 vCPU GitHub-hosted runners the
|
||||||
does the arithmetic and fails if either knob outgrows the runner.
|
publish pipeline uses. At `8` (→ 7 workers) that runner ran out of memory and
|
||||||
|
buildkit failed the step with `ResourceExhausted: ... cannot allocate memory`;
|
||||||
|
`3` (→ 2 workers) still didn't fit once the per-process RSS was measured
|
||||||
|
directly instead of inferred. `tests/unit/docker-build-memory-budget.test.ts`
|
||||||
|
does the arithmetic against the measured figure and fails if either knob
|
||||||
|
outgrows the runner. Both images (node and Bun) share these defaults; the Bun
|
||||||
|
image's are set in `Dockerfile.bun` (Turbopack on Bun 1.4+, `#11719`).
|
||||||
|
|
||||||
Turbopack compiles in native Rust memory that lives **outside** the V8 heap, so
|
Turbopack compiles in native Rust memory that lives **outside** the V8 heap, so
|
||||||
`OMNIROUTE_BUILD_MEMORY_MB` does not bound it. On a host with a memory ceiling the
|
`OMNIROUTE_BUILD_MEMORY_MB` does not bound it. On a host with a memory ceiling the
|
||||||
|
|||||||
195
electron/package-lock.json
generated
195
electron/package-lock.json
generated
@@ -297,6 +297,45 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@electron/windows-sign": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@electron/windows-sign/-/windows-sign-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cross-dirname": "^0.1.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"fs-extra": "^11.1.1",
|
||||||
|
"minimist": "^1.2.8",
|
||||||
|
"postject": "^1.0.0-alpha.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"electron-windows-sign": "bin/electron-windows-sign.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@electron/windows-sign/node_modules/fs-extra": {
|
||||||
|
"version": "11.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.0.tgz",
|
||||||
|
"integrity": "sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.2.0",
|
||||||
|
"jsonfile": "^6.0.1",
|
||||||
|
"universalify": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/fs-minipass": {
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
@@ -1091,6 +1130,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-dirname": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-dirname/-/cross-dirname-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -1411,6 +1459,19 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-builder-squirrel-windows": {
|
||||||
|
"version": "26.15.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-builder-squirrel-windows/-/electron-builder-squirrel-windows-26.15.3.tgz",
|
||||||
|
"integrity": "sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"app-builder-lib": "26.15.3",
|
||||||
|
"builder-util": "26.15.3",
|
||||||
|
"electron-winstaller": "5.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-publish": {
|
"node_modules/electron-publish": {
|
||||||
"version": "26.15.3",
|
"version": "26.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-26.15.3.tgz",
|
||||||
@@ -1445,6 +1506,66 @@
|
|||||||
"tiny-typed-emitter": "^2.1.0"
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-winstaller": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@electron/asar": "^3.2.1",
|
||||||
|
"debug": "^4.1.1",
|
||||||
|
"fs-extra": "^7.0.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"temp": "^0.9.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@electron/windows-sign": "^1.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/fs-extra": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"graceful-fs": "^4.1.2",
|
||||||
|
"jsonfile": "^4.0.0",
|
||||||
|
"universalify": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6 <7 || >=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/jsonfile": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"optionalDependencies": {
|
||||||
|
"graceful-fs": "^4.1.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-winstaller/node_modules/universalify": {
|
||||||
|
"version": "0.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
|
||||||
|
"integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
@@ -2359,6 +2480,20 @@
|
|||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "0.5.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||||
|
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "bin/cmd.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -2622,6 +2757,36 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postject": {
|
||||||
|
"version": "1.0.0-alpha.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/postject/-/postject-1.0.0-alpha.6.tgz",
|
||||||
|
"integrity": "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "^9.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"postject": "dist/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/postject/node_modules/commander": {
|
||||||
|
"version": "9.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz",
|
||||||
|
"integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.20.0 || >=14"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proc-log": {
|
"node_modules/proc-log": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz",
|
||||||
@@ -2816,6 +2981,21 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rimraf": {
|
||||||
|
"version": "2.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
|
||||||
|
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
|
||||||
|
"deprecated": "Rimraf versions prior to v4 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/roarr": {
|
"node_modules/roarr": {
|
||||||
"version": "2.15.4",
|
"version": "2.15.4",
|
||||||
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz",
|
||||||
@@ -3071,6 +3251,21 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/temp": {
|
||||||
|
"version": "0.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
|
||||||
|
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"mkdirp": "^0.5.1",
|
||||||
|
"rimraf": "~2.6.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/temp-file": {
|
"node_modules/temp-file": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.4.0.tgz",
|
||||||
|
|||||||
@@ -131,9 +131,15 @@ function runNextBuild() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
|
export function resolveNextBuildBundlerFlag(baseEnv = process.env) {
|
||||||
// Turbopack is the default on Node.js; on Bun or when explicitly disabled (=0),
|
// Turbopack is the default; OMNIROUTE_USE_TURBOPACK=0 is the documented escape hatch
|
||||||
// use Webpack (--webpack) to avoid Turbopack V8 internal worker API mismatches.
|
// to webpack (Windows, native-binding trouble, RAM-constrained machines — #6409, and
|
||||||
if (process.versions.bun || baseEnv.OMNIROUTE_USE_TURBOPACK === "0") {
|
// docs/reference/ENVIRONMENT.md). The choice is env-only ON PURPOSE: the variable is
|
||||||
|
// the operator's control and CI sets it explicitly, so sniffing the runtime here would
|
||||||
|
// silently override an operator who asked for Turbopack. Bun 1.4+ supports Turbopack's
|
||||||
|
// V8 worker bindings (#11471), so the historical `process.versions.bun` → `--webpack`
|
||||||
|
// hardcode is gone; the `OMNIROUTE_USE_TURBOPACK=0` fallback remains for Bun < 1.4
|
||||||
|
// images built with the webpack path.
|
||||||
|
if (baseEnv.OMNIROUTE_USE_TURBOPACK === "0") {
|
||||||
return "--webpack";
|
return "--webpack";
|
||||||
}
|
}
|
||||||
return "--turbopack";
|
return "--turbopack";
|
||||||
|
|||||||
@@ -123,6 +123,48 @@ function discoverPackagedExecutable() {
|
|||||||
throw new Error(`Packaged Electron smoke check does not support ${platform()}.`);
|
throw new Error(`Packaged Electron smoke check does not support ${platform()}.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The packaged app opens SQLite lazily: `/login` (the readiness URL) never touches the
|
||||||
|
* database, so a smoke that only waits for readiness sees no `[DB]` line at all. After
|
||||||
|
* readiness the smoke requests a DB-backed endpoint and waits for evidence that the
|
||||||
|
* database opened. The primary open path does NOT print "[DB] Driver: ..." (only the
|
||||||
|
* recovery path and the sql.js fallback do), so the evidence is any `[DB]`/`[Migration]`
|
||||||
|
* startup line — and the #7592 guard below rejects the fallback's own line explicitly.
|
||||||
|
*/
|
||||||
|
export const DB_TOUCH_PATH = "/api/monitoring/health";
|
||||||
|
export const DB_OPEN_EVIDENCE_PATTERN =
|
||||||
|
/\[DB\] (Driver: |SQLite database ready|Added [^\n]* column|Changing cache_size|cache_size changed)|\[Migration\] (Applied|Pre-migration backup)/;
|
||||||
|
|
||||||
|
export async function waitForDatabaseOpen(getLogs, { timeoutMs = 15_000, pollMs = 250 } = {}) {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
while (Date.now() - startedAt < timeoutMs) {
|
||||||
|
const logs = getLogs();
|
||||||
|
assertNoFatalLogs(logs);
|
||||||
|
if (DB_OPEN_EVIDENCE_PATTERN.test(logs)) return logs;
|
||||||
|
await sleep(pollMs);
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
`Packaged Electron app logged no [DB]/[Migration] startup line within ${timeoutMs}ms of ` +
|
||||||
|
`touching ${DB_TOUCH_PATH} — the database never opened, so the SQLite driver cannot be verified.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDatabaseForSmoke({ logs, smokeUrl }) {
|
||||||
|
const touchUrl = new URL(DB_TOUCH_PATH, smokeUrl).toString();
|
||||||
|
try {
|
||||||
|
const response = await fetchWithTimeout(touchUrl, 5_000);
|
||||||
|
console.log(
|
||||||
|
`[electron-smoke] touched ${touchUrl} (HTTP ${response.status}) to open the database`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(
|
||||||
|
`[electron-smoke] touching ${touchUrl} failed (${error instanceof Error ? error.message : String(error)}) — waiting for the database anyway`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await waitForDatabaseOpen(() => logs.value);
|
||||||
|
console.log("[electron-smoke] database opened");
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchWithTimeout(url, timeoutMs) {
|
async function fetchWithTimeout(url, timeoutMs) {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
@@ -450,8 +492,13 @@ export function assertNativeDriverSelected(logs) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The primary open path prints no "[DB] Driver: ..." line at all (only the recovery path and
|
||||||
|
// the sql.js fallback do), so a database that demonstrably opened WITHOUT the fallback's own
|
||||||
|
// line is the native driver — that is exactly what #7592 guards.
|
||||||
|
if (DB_OPEN_EVIDENCE_PATTERN.test(logs)) return;
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Packaged Electron app logs contain no '[DB] Driver: ...' line — cannot confirm which SQLite " +
|
"Packaged Electron app logs show no database activity at all — cannot confirm which SQLite " +
|
||||||
"driver loaded."
|
"driver loaded."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -506,7 +553,14 @@ async function waitForReady({ logs, smokeUrl, timeoutMs, settleMs, exitState })
|
|||||||
* by the single-launch path and the cold-restart (two-launch) path so both
|
* by the single-launch path and the cold-restart (two-launch) path so both
|
||||||
* exercise identical spawn/readiness/shutdown behavior.
|
* exercise identical spawn/readiness/shutdown behavior.
|
||||||
*/
|
*/
|
||||||
async function launchAndCollectLogs({ appExecutable, smokeUrl, dataDir, timeoutMs, settleMs, streamLogs }) {
|
async function launchAndCollectLogs({
|
||||||
|
appExecutable,
|
||||||
|
smokeUrl,
|
||||||
|
dataDir,
|
||||||
|
timeoutMs,
|
||||||
|
settleMs,
|
||||||
|
streamLogs,
|
||||||
|
}) {
|
||||||
const smokeEnv = buildSmokeEnv({ dataDir });
|
const smokeEnv = buildSmokeEnv({ dataDir });
|
||||||
await assertPortIsFree(smokeUrl);
|
await assertPortIsFree(smokeUrl);
|
||||||
await ensureSmokeEnvDirs(smokeEnv, dataDir);
|
await ensureSmokeEnvDirs(smokeEnv, dataDir);
|
||||||
@@ -538,6 +592,8 @@ async function launchAndCollectLogs({ appExecutable, smokeUrl, dataDir, timeoutM
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await waitForReady({ logs, smokeUrl, timeoutMs, settleMs, exitState });
|
await waitForReady({ logs, smokeUrl, timeoutMs, settleMs, exitState });
|
||||||
|
// Outside waitForReady on purpose: a missing database is a verdict, not a readiness retry.
|
||||||
|
await openDatabaseForSmoke({ logs, smokeUrl });
|
||||||
return logs.value;
|
return logs.value;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!streamLogs) {
|
if (!streamLogs) {
|
||||||
@@ -568,7 +624,14 @@ async function main() {
|
|||||||
!process.env.ELECTRON_SMOKE_DATA_DIR && process.env.ELECTRON_SMOKE_KEEP_DATA !== "1";
|
!process.env.ELECTRON_SMOKE_DATA_DIR && process.env.ELECTRON_SMOKE_KEEP_DATA !== "1";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await launchAndCollectLogs({ appExecutable, smokeUrl, dataDir, timeoutMs, settleMs, streamLogs });
|
await launchAndCollectLogs({
|
||||||
|
appExecutable,
|
||||||
|
smokeUrl,
|
||||||
|
dataDir,
|
||||||
|
timeoutMs,
|
||||||
|
settleMs,
|
||||||
|
streamLogs,
|
||||||
|
});
|
||||||
|
|
||||||
if (!coldRestart) return;
|
if (!coldRestart) return;
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ describe("Protocol clients E2E", () => {
|
|||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({ a2aEnabled: true }),
|
body: JSON.stringify({ a2aEnabled: true }),
|
||||||
});
|
});
|
||||||
expect([200, 401]).toContain(response.status);
|
expect([200, 401, 403]).toContain(response.status);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
@@ -134,7 +134,7 @@ describe("Protocol clients E2E", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const auditRes = await apiFetch("/api/mcp/audit?limit=50&tool=omniroute_get_health");
|
const auditRes = await apiFetch("/api/mcp/audit?limit=50&tool=omniroute_get_health");
|
||||||
expect([200, 401]).toContain(auditRes.status);
|
expect([200, 401, 403]).toContain(auditRes.status);
|
||||||
if (auditRes.status === 200) {
|
if (auditRes.status === 200) {
|
||||||
expect(auditRes.ok).toBe(true);
|
expect(auditRes.ok).toBe(true);
|
||||||
const auditJson = (await auditRes.json()) as any;
|
const auditJson = (await auditRes.json()) as any;
|
||||||
|
|||||||
19
tests/unit/build/resolve-next-build-bundler-flag.test.mjs
Normal file
19
tests/unit/build/resolve-next-build-bundler-flag.test.mjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { resolveNextBuildBundlerFlag } from "../../../scripts/build/build-next-isolated.mjs";
|
||||||
|
|
||||||
|
test("resolveNextBuildBundlerFlag returns --turbopack by default", () => {
|
||||||
|
const flag = resolveNextBuildBundlerFlag({});
|
||||||
|
assert.equal(flag, "--turbopack");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveNextBuildBundlerFlag returns --webpack when OMNIROUTE_USE_TURBOPACK is '0'", () => {
|
||||||
|
const flag = resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "0" });
|
||||||
|
assert.equal(flag, "--webpack");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("resolveNextBuildBundlerFlag returns --turbopack when OMNIROUTE_USE_TURBOPACK is '1'", () => {
|
||||||
|
const flag = resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "1" });
|
||||||
|
assert.equal(flag, "--turbopack");
|
||||||
|
});
|
||||||
@@ -88,13 +88,37 @@ test("createSyncDriverFactory prefers better-sqlite3 when running under Node", (
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resolveNextBuildBundlerFlag automatically disables Turbopack and uses Webpack under Bun", async () => {
|
// `OMNIROUTE_USE_TURBOPACK` is the operator's only control over the bundler:
|
||||||
|
// Turbopack is the default and `0` is the documented escape hatch (webpack), taken
|
||||||
|
// for Windows / native-binding trouble / RAM-constrained machines — see
|
||||||
|
// docs/reference/ENVIRONMENT.md and #6409. Nothing sniffs the runtime, so pin that:
|
||||||
|
// a hidden override would silently ignore an explicit `=1` from an operator who set
|
||||||
|
// it on purpose (CI does, in build.yml / ci.yml / quality.yml). Bun 1.4+ supports
|
||||||
|
// Turbopack's V8 worker bindings (#11471), so the historical bun-only webpack
|
||||||
|
// forced path is gone from the Bun image as well.
|
||||||
|
test("resolveNextBuildBundlerFlag is decided by OMNIROUTE_USE_TURBOPACK alone, not by the runtime", async () => {
|
||||||
|
const buildIsolated = await import("../../scripts/build/build-next-isolated.mjs");
|
||||||
const originalBun = process.versions.bun;
|
const originalBun = process.versions.bun;
|
||||||
try {
|
try {
|
||||||
(process.versions as Record<string, string>).bun = "1.1.20";
|
for (const bun of [undefined, "1.1.20", "1.3.14"]) {
|
||||||
const buildIsolated = await import("../../scripts/build/build-next-isolated.mjs");
|
if (bun === undefined) {
|
||||||
assert.equal(buildIsolated.resolveNextBuildBundlerFlag({}), "--webpack");
|
delete (process.versions as Record<string, string | undefined>).bun;
|
||||||
assert.equal(buildIsolated.resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "1" }), "--webpack");
|
} else {
|
||||||
|
(process.versions as Record<string, string>).bun = bun;
|
||||||
|
}
|
||||||
|
const where = `bun=${bun ?? "absent"}`;
|
||||||
|
assert.equal(buildIsolated.resolveNextBuildBundlerFlag({}), "--turbopack", where);
|
||||||
|
assert.equal(
|
||||||
|
buildIsolated.resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "1" }),
|
||||||
|
"--turbopack",
|
||||||
|
where
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
buildIsolated.resolveNextBuildBundlerFlag({ OMNIROUTE_USE_TURBOPACK: "0" }),
|
||||||
|
"--webpack",
|
||||||
|
where
|
||||||
|
);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (originalBun === undefined) {
|
if (originalBun === undefined) {
|
||||||
delete (process.versions as Record<string, string | undefined>).bun;
|
delete (process.versions as Record<string, string | undefined>).bun;
|
||||||
|
|||||||
@@ -4,76 +4,109 @@ import { readFileSync } from "node:fs";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
// The Docker publish workflow builds on GitHub-hosted runners (ubuntu-24.04 and
|
// The Docker publish workflow builds on GitHub-hosted runners (ubuntu-24.04 and
|
||||||
// ubuntu-24.04-arm): 4 vCPU, 16 GB RAM. Every Next page-data worker is its own
|
// ubuntu-24.04-arm): 4 vCPU, 16 GB RAM. Every Next page-data worker AND the
|
||||||
// process and inherits NODE_OPTIONS, so the V8 ceiling is per PROCESS: the
|
// parent `next build` process are separate OS processes, so the budget has to
|
||||||
// build's worst case is roughly `workers × OMNIROUTE_BUILD_MEMORY_MB`.
|
// cover all of them, not just the workers. Both images (node and Bun) run the
|
||||||
|
// same build-next-isolated.mjs pipeline on the same runners; the Bun image runs
|
||||||
|
// Turbopack by default on Bun 1.4+ (#11471) and the webpack fallback
|
||||||
|
// (OMNIROUTE_USE_TURBOPACK=0) keeps memory in V8, so the guards must hold for
|
||||||
|
// both bundlers on both images (#11709).
|
||||||
//
|
//
|
||||||
// With 7 workers × 6144 MB the runner ran out and buildkit failed the step with
|
// With 7 workers × 6144 MB the runner ran out and buildkit failed the step with
|
||||||
// `ResourceExhausted: ... cannot allocate memory`, right after "Collecting page
|
// `ResourceExhausted: ... cannot allocate memory`, right after "Collecting page
|
||||||
// data using 7 workers" — every Docker publish since 2026-08-22 23:14 UTC.
|
// data using 7 workers" — every Docker publish since 2026-08-22 23:14 UTC.
|
||||||
|
// Lowering to 2 workers (#10060 / PR #11419) was not enough: it modeled the
|
||||||
|
// per-process peak as an INFERENCE (`WORKER_PEAK_MB = 2560`, derived only from
|
||||||
|
// "7 workers didn't fit") and assumed the parent process tracked the V8 heap
|
||||||
|
// ceiling (`OMNIROUTE_BUILD_MEMORY_MB`) rather than its own RSS. The owner's
|
||||||
|
// live VPS reproduction (issue #7518, dmesg OOM-killer report, 2026-08-24)
|
||||||
|
// measured the real number directly: `next-build (v16) ... anon-rss:4522744kB`
|
||||||
|
// (~4.5 GB) per process, independent of the NODE_OPTIONS heap flag — Turbopack
|
||||||
|
// itself is native/Rust and compiles outside the V8 heap. With 2 workers that
|
||||||
|
// keeps the publish pipeline failing at "Collecting page data using 2 workers"
|
||||||
|
// (run 32907937950, 2026-08-25).
|
||||||
//
|
//
|
||||||
// This pins the budget so raising either knob has to be a deliberate change
|
// This pins the budget on the MEASURED figure, applied uniformly to every
|
||||||
// that re-does the arithmetic, not a one-line bump that silently reds the
|
// process (parent + workers) and to both images, so raising the worker count
|
||||||
// publish pipeline again.
|
// has to be a deliberate change that re-does the arithmetic, not a one-line
|
||||||
|
// bump that silently reds the publish pipeline again.
|
||||||
|
|
||||||
const RUNNER_MEMORY_MB = 16 * 1024;
|
const RUNNER_MEMORY_MB = 16 * 1024;
|
||||||
// Leave room for buildkit, the snapshotter and page cache.
|
// Leave room for buildkit, the snapshotter and page cache.
|
||||||
const HEADROOM_FRACTION = 0.75;
|
const HEADROOM_FRACTION = 0.75;
|
||||||
// Planning figure for one page-data worker's peak RSS. It is an INFERENCE, not
|
// Measured (not inferred) peak RSS for a single Next/Turbopack build process —
|
||||||
// a measurement: 7 workers did not fit in 16 GB alongside the parent, which
|
// parent or page-data worker alike — from the dmesg OOM-killer report above.
|
||||||
// puts the per-worker peak somewhere north of ~1.8 GB. 2.5 GB is that bound
|
// If a future build OOMs again, re-measure via dmesg before raising this
|
||||||
// rounded up, so the budget below stays conservative. If a future build OOMs
|
// number — do not weaken the budget with another guess.
|
||||||
// again with a worker count this test accepts, raise this number — do not
|
const MEASURED_PROCESS_RSS_MB = 4500;
|
||||||
// weaken the budget.
|
|
||||||
const WORKER_PEAK_MB = 2560;
|
|
||||||
|
|
||||||
const dockerfile = readFileSync(
|
const DOCKERFILES = [
|
||||||
fileURLToPath(new URL("../../Dockerfile", import.meta.url)),
|
{ label: "Dockerfile", raw: readFileSync(fileURLToPath(new URL("../../Dockerfile", import.meta.url)), "utf8") },
|
||||||
"utf8"
|
{ label: "Dockerfile.bun", raw: readFileSync(fileURLToPath(new URL("../../Dockerfile.bun", import.meta.url)), "utf8") },
|
||||||
);
|
];
|
||||||
|
|
||||||
function readArgDefault(name: string): number {
|
function readArgDefault(name: string, label: string): number {
|
||||||
const match = dockerfile.match(new RegExp(`^ARG ${name}=(\\d+)$`, "m"));
|
const raw = DOCKERFILES.find((entry) => entry.label === label)!.raw;
|
||||||
assert.ok(match, `Dockerfile no longer declares ARG ${name}`);
|
const match = raw.match(new RegExp(`^ARG ${name}=(\\d+)$`, "m"));
|
||||||
|
assert.ok(match, `${label} no longer declares ARG ${name}`);
|
||||||
return Number(match![1]);
|
return Number(match![1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
test("the Docker build's worker pool is derived from OMNIROUTE_BUILD_WORKERS", () => {
|
for (const { label } of DOCKERFILES) {
|
||||||
// assert.ok(boolean), not assert.match — a failing assert.match dumps the
|
test(`the ${label} build's worker pool is derived from OMNIROUTE_BUILD_WORKERS`, () => {
|
||||||
// whole Dockerfile into the report.
|
// assert.ok(boolean), not assert.match — a failing assert.match dumps the
|
||||||
assert.ok(
|
// whole Dockerfile into the report.
|
||||||
/^ENV CIRCLE_NODE_TOTAL=\$\{OMNIROUTE_BUILD_WORKERS\}$/m.test(dockerfile),
|
const raw = DOCKERFILES.find((entry) => entry.label === label)!.raw;
|
||||||
"CIRCLE_NODE_TOTAL must stay wired to the build arg so a big builder can raise it"
|
assert.ok(
|
||||||
);
|
/^ENV CIRCLE_NODE_TOTAL=\$\{OMNIROUTE_BUILD_WORKERS\}$/m.test(raw),
|
||||||
assert.ok(
|
`${label}: CIRCLE_NODE_TOTAL must stay wired to the build arg so a big builder can raise it`
|
||||||
/^ENV NODE_OPTIONS="--max-old-space-size=\$\{OMNIROUTE_BUILD_MEMORY_MB\}"$/m.test(dockerfile),
|
);
|
||||||
"the build heap ceiling must stay wired to OMNIROUTE_BUILD_MEMORY_MB"
|
assert.ok(
|
||||||
);
|
/^ENV NODE_OPTIONS="--max-old-space-size=\$\{OMNIROUTE_BUILD_MEMORY_MB\}"$/m.test(raw),
|
||||||
});
|
`${label}: the build heap ceiling must stay wired to OMNIROUTE_BUILD_MEMORY_MB`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("worker count × per-process heap fits a 16 GB GitHub runner", () => {
|
test(`worker count × measured per-process RSS fits a 16 GB GitHub runner (${label})`, () => {
|
||||||
const workerPool = readArgDefault("OMNIROUTE_BUILD_WORKERS");
|
const workerPool = readArgDefault("OMNIROUTE_BUILD_WORKERS", label);
|
||||||
const heapMb = readArgDefault("OMNIROUTE_BUILD_MEMORY_MB");
|
|
||||||
|
|
||||||
// Next derives `workers = CIRCLE_NODE_TOTAL - 1`.
|
// Next derives `workers = CIRCLE_NODE_TOTAL - 1`.
|
||||||
const workers = workerPool - 1;
|
const workers = workerPool - 1;
|
||||||
assert.ok(workers >= 1, `CIRCLE_NODE_TOTAL=${workerPool} leaves no build workers`);
|
assert.ok(workers >= 1, `${label}: CIRCLE_NODE_TOTAL=${workerPool} leaves no build workers`);
|
||||||
|
|
||||||
// The parent `next build` process is the one that genuinely needs the raised
|
// Every process — the parent `next build` process AND each page-data
|
||||||
// ceiling (the webpack/turbopack production pass, #4076); the workers are
|
// worker — is budgeted at the measured per-process RSS floor (see the file
|
||||||
// budgeted at their inferred peak instead.
|
// banner comment). The V8 heap ceiling (OMNIROUTE_BUILD_MEMORY_MB) bounds
|
||||||
const worstCaseMb = heapMb + workers * WORKER_PEAK_MB;
|
// JS allocations but not Turbopack's native/Rust memory, so it cannot stand
|
||||||
const budgetMb = RUNNER_MEMORY_MB * HEADROOM_FRACTION;
|
// in for the parent process's real RSS.
|
||||||
assert.ok(
|
const processes = workers + 1;
|
||||||
worstCaseMb <= budgetMb,
|
const worstCaseMb = processes * MEASURED_PROCESS_RSS_MB;
|
||||||
`parent ${heapMb} MB + ${workers} workers × ${WORKER_PEAK_MB} MB = ${worstCaseMb} MB ` +
|
const budgetMb = RUNNER_MEMORY_MB * HEADROOM_FRACTION;
|
||||||
`exceeds the ${budgetMb} MB budget on a ${RUNNER_MEMORY_MB} MB runner — the Docker ` +
|
assert.ok(
|
||||||
`publish step dies with "ResourceExhausted: cannot allocate memory" during page-data ` +
|
worstCaseMb <= budgetMb,
|
||||||
`collection`
|
`${label}: ${processes} processes (1 parent + ${workers} workers) × ${MEASURED_PROCESS_RSS_MB} MB ` +
|
||||||
);
|
`measured RSS = ${worstCaseMb} MB exceeds the ${budgetMb} MB budget on a ` +
|
||||||
});
|
`${RUNNER_MEMORY_MB} MB runner — the Docker publish step dies with "ResourceExhausted: ` +
|
||||||
|
`cannot allocate memory" during page-data collection`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("the worker pool does not oversubscribe the runner's 4 vCPU", () => {
|
test(`both images default to OMNIROUTE_BUILD_WORKERS=2 (1 page-data worker) (${label})`, () => {
|
||||||
const workers = readArgDefault("OMNIROUTE_BUILD_WORKERS") - 1;
|
// At OMNIROUTE_BUILD_WORKERS=2 → CIRCLE_NODE_TOTAL=2 → Next derives 1
|
||||||
assert.ok(workers <= 4, `${workers} workers oversubscribe a 4 vCPU runner`);
|
// page-data worker, so 2 processes (parent + worker) × ~4.5 GB ≈ 9 GB fit
|
||||||
});
|
// the 12.288 GB (75%) budget on a 16 GB runner with headroom. At =3 → 2
|
||||||
|
// workers → 3 processes × 4.5 GB ≈ 13.5 GB, which exceeds it (measured
|
||||||
|
// per-process RSS, #7518). Raising this default must re-do the budget
|
||||||
|
// arithmetic and stay green on the test above (#11663).
|
||||||
|
assert.equal(
|
||||||
|
readArgDefault("OMNIROUTE_BUILD_WORKERS", label),
|
||||||
|
2,
|
||||||
|
`${label}: OMNIROUTE_BUILD_WORKERS must stay 2 (1 page-data worker)`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`the worker pool does not oversubscribe the runner's 4 vCPU (${label})`, () => {
|
||||||
|
const workers = readArgDefault("OMNIROUTE_BUILD_WORKERS", label) - 1;
|
||||||
|
assert.ok(workers <= 4, `${workers} workers oversubscribe a 4 vCPU runner`);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
FATAL_LOG_PATTERNS,
|
FATAL_LOG_PATTERNS,
|
||||||
LINUX_EXECUTABLE_NAMES,
|
LINUX_EXECUTABLE_NAMES,
|
||||||
stopApp,
|
stopApp,
|
||||||
|
waitForDatabaseOpen,
|
||||||
|
DB_TOUCH_PATH,
|
||||||
} from "../../scripts/dev/smoke-electron-packaged.mjs";
|
} from "../../scripts/dev/smoke-electron-packaged.mjs";
|
||||||
|
|
||||||
test("electron smoke discovers the default Linux executable name", () => {
|
test("electron smoke discovers the default Linux executable name", () => {
|
||||||
@@ -94,6 +96,46 @@ test("electron smoke flags a cold-restart fallback to the sql.js WASM driver", (
|
|||||||
test("electron smoke flags startup logs missing any driver selection line", () => {
|
test("electron smoke flags startup logs missing any driver selection line", () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => assertNativeDriverSelected("[electron] [server] listening on 20128"),
|
() => assertNativeDriverSelected("[electron] [server] listening on 20128"),
|
||||||
/no '\[DB\] Driver: \.\.\.' line/
|
/no database activity/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("electron smoke waits for database-open evidence after touching a DB-backed endpoint", async () => {
|
||||||
|
assert.equal(DB_TOUCH_PATH, "/api/monitoring/health");
|
||||||
|
let logs = "[electron] [Server] [STARTUP] ready\n";
|
||||||
|
setTimeout(() => {
|
||||||
|
logs += "[electron] [Server] [DB] Added usage_history.combo_strategy column\n";
|
||||||
|
}, 60);
|
||||||
|
const seen = await waitForDatabaseOpen(() => logs, { timeoutMs: 2_000, pollMs: 20 });
|
||||||
|
assert.match(seen, /\[DB\] Added/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("electron smoke fails clearly when the database never opens", async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
waitForDatabaseOpen(() => "[electron] [Server] [STARTUP] ready\n", {
|
||||||
|
timeoutMs: 120,
|
||||||
|
pollMs: 20,
|
||||||
|
}),
|
||||||
|
/logged no \[DB\]\/\[Migration\] startup line within 120ms/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("electron smoke driver guard: native line, DB evidence and sql.js fallback", () => {
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
assertNativeDriverSelected("[DB] Driver: better-sqlite3 | file: /tmp/x/storage.sqlite\n")
|
||||||
|
);
|
||||||
|
assert.doesNotThrow(() =>
|
||||||
|
assertNativeDriverSelected(
|
||||||
|
"[electron] [Server] [DB] Added call_logs.session_tag column\n[electron] [Server] [Migration] Applied: 046_database_settings\n"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertNativeDriverSelected("[DB] Driver: sql.js | file: /tmp/x/storage.sqlite\n"),
|
||||||
|
/sql\.js \(WASM\) driver/
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertNativeDriverSelected("[STARTUP] nothing here\n"),
|
||||||
|
/no database activity/
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user