Skip to content

fix: unblock the nchat desktop build (nine layers) - #181

Merged
acamarata merged 10 commits into
mainfrom
fix/ui-dts-ts5101
Sep 15, 2026
Merged

acamarata merged 10 commits into
mainfrom
fix/ui-dts-ts5101

Conversation

@acamarata

@acamarata acamarata commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

Correction to this PR's original claim

I opened this saying the TypeScript 6 / tsup baseUrl injection was the root cause. That was wrong. The TS6 fix is real and still needed — tsup@8.5.1 injects baseUrl into the dts worker's tsconfig and TypeScript 6 rejects it with TS5101 — but it was one layer of nine, and not the important one.

Fixing the trigger is what proved it wrong. This build had never completed in CI on any platform, so each defect was invisible until the one before it cleared.

The nine layers

# Failure Fix
1 every check on this PR reported SKIPPED — no paths-filter covers root packages/ desktop workflows trigger on packages/**
2 Rollup failed to resolve import "@nself/observability" check out the sibling nself-org/packages repo
3 ERR_PNPM_OUTDATED_LOCKFILE … specifiers in the lockfile ({}) regenerate the lockfile; 16 sibling importers
4 'tsc' is not recognized, then "i18next" unresolved one --filter "!@nself-chat/web"
5 error[E0599]: no method named get_webview_window use tauri::Manager
6 bundled fine, then failed to decode secret key updater artifacts off for non-tag builds
7 RC2175: icon.ico is not in 3.00 format real icons — all five were placeholders
8 failed to run command security import export APPLE_* only when non-empty
9 macOS x64 leg never scheduled macos-13 is retired → macos-15-intel

Layer 2 is the actual answer to "why has this been broken for weeks": pnpm-workspace.yaml globs '../packages/@nself/*' — the nself-org/packages repo as a sibling directory — and no nchat workflow has ever checked it out. That layout exists only on a developer's machine.

Layer 7 deserves naming: icon.ico and icon.icns were 70-byte 1×1 PNGs, and the three PNGs were solid black — one distinct colour across every pixel. Regenerated from the brand artwork already committed at .github/wiki/brand/icons/icon-1k.png, so nothing is designed here.

Proven

Linux went green on 80e806f5 — the first successful nchat desktop build, producing .deb, .rpm and .AppImage.

To stop paying one CI round per layer, I reproduced CI's steps locally in a sibling layout and got the whole JS half green in one pass:

pnpm install --frozen-lockfile --filter "!@nself-chat/web"     ok (1581 resolved)
pnpm --filter ui --filter core --filter state --filter config build   ok
cd desktop && pnpm build   →  521 modules, 427.81 kB │ gzip: 130.85 kB

The Rust half was deliberately not built locally: swap was at 87–89% and this machine has a recorded kernel panic from cargo plus swap exhaustion.

Lockfile blast radius

24 other nchat workflows install from this lockfile and none check out the sibling. Verified both scopes, before the rebase and again after regenerating on top of #182/#183:

sibling present Scope: all 27 workspace projects, exit 0
sibling absent Scope: all 11 workspace projects, exit 0

The glob matches nothing when the directory is absent, so the extra importers are out of scope.

One deliberate trade-off

The sibling checkout is pinned to 8a5cc53, not tracking main. A floating sibling and a frozen lockfile contradict each other — the next push to nself-org/packages would red every desktop build. Cost: the sha and the lockfile must be bumped together by hand.

Owner-gated, not fixed here

  • TAURI_SIGNING_PRIVATE_KEY / _PASSWORD do not exist, so a tag build still cannot sign an updater artifact.
  • plugins.updater.pubkey is "", so a client could not verify a signature anyway.
  • No APPLE_* secrets, so macOS releases will be unsigned.

Logged as OWNER-ACTIONS §68. Nothing is broken for users today because no signed release has ever shipped.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

PR Checks Summary

Check Status
Lint & Format ✅ success
Type Check ✅ success
Unit Tests ✅ success
Build ✅ success

All checks passed! Ready for review.

@acamarata acamarata changed the title fix(build): unblock the workspace package build under TypeScript 6 fix: unblock the nchat desktop build (TS6 + sibling packages + dead triggers) Sep 14, 2026
@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

Dependency Review

The following issues were found:

  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ✅ 0 package(s) with unknown licenses.
  • ⚠️ 37 packages with OpenSSF Scorecard issues.

View full job summary

@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

1 similar comment
@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

acamarata added a commit that referenced this pull request Sep 14, 2026
* ci: make PR checks actually run on shared-package changes

Every job in pr-checks.yml is gated behind a dorny/paths-filter, and none of
the filters covered the repo's root packages/ directory. A PR touching only
packages/ therefore skipped Lint, Type Check, Unit Tests and Build Check.
Measured on PR #181, which changes two tsconfigs under packages/: 12 of its
checks reported SKIPPED and nothing compiled the code it changed.

- packages: also match packages/**, pnpm-workspace.yaml, pnpm-lock.yaml
- web: matched frontend/apps/web/**, which does not exist; the web surface
  is the root web/ directory

The mobile: and desktop: filters matched four paths that are absent from the
repo -- frontend/platforms/{capacitor,react-native,electron,tauri}. The real
directories are frontend/platforms/mobile and desktop/. Those two filters could
never match, so Mobile Build Check and Desktop Build Check have never produced
a verdict; had they matched, both would have failed, because each cds into one
of the missing directories.

Both jobs duplicate a dedicated workflow that already targets the real path:
build-capacitor.yml builds frontend/platforms/mobile, and desktop-linux.yml,
desktop-macos.yml and desktop-windows.yml build desktop/. Removing the two dead
jobs drops no coverage that existed.

Still uncovered and deliberately not papered over here: frontend-vite/ matches
no filter, but every job in this workflow runs with working-directory: frontend,
so adding a filter for it would run the wrong app's build and report a green
that means nothing. It needs its own job.

* ci: cover root packages/ in ci.yml's filter too

ci.yml runs on push to main and gates its jobs with the same paths-filter shape
as pr-checks.yml, including the same 'packages:' entry that only matched
frontend/. A push touching only the shared packages/ directory skipped lint,
type-check, test and build on main as well as on PRs.

frontend/pnpm-workspace.yaml does not exist; the workspace manifest is at the
repo root.
@acamarata acamarata changed the title fix: unblock the nchat desktop build (TS6 + sibling packages + dead triggers) fix: unblock the nchat desktop build (nine layers) Sep 14, 2026
@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

2 similar comments
@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

The "Build workspace packages" step fails, and with it Desktop — Linux,
Desktop — Windows and Desktop — macOS. Those desktop builds have not succeeded
on any run examined back to 2026-08-03. None of them is a required check on main
(required: Gitleaks, CodeQL Analysis), which is why six weeks passed unnoticed.

Two independent TypeScript 6 breakages in the same step.

1. packages/ui — TS5101, and it is NOT our config.

    error TS5101: Option 'baseUrl' is deprecated and will stop functioning in
    TypeScript 7.0

No tsconfig in this repo sets baseUrl. `tsc --showConfig -p
packages/ui/tsconfig.json` shows none, and plain `tsc --noEmit -p
packages/ui/tsconfig.json` passes clean — the ESM and CJS builds pass too, and
only the declaration build dies.

tsup injects it. tsup 8.5.1, dist/rollup.js:6837:

    baseUrl: compilerOptions.baseUrl || ".",

It sets baseUrl unconditionally on the options it hands its dts worker,
defaulting to "." when the project has none. TypeScript 6 makes that an error,
so every tsup declaration build fails regardless of what the project says.

8.5.1 is the newest tsup published, so there is no upgrade that drops the
injection. `ignoreDeprecations: "6.0"` is the suppression TypeScript's own TS5101
message prescribes, it is scoped to that one deprecation, and type checking is
otherwise unchanged. Five packages across nself-org already carry it for the same
reason (plugins-pro's @nself/sentry, and four in the packages repo).

TIME-BOXED: this stops working at TypeScript 7.0, where baseUrl is removed rather
than deprecated. The real fix is upstream in tsup or replacing its dts step.
Re-check when a tsup newer than 8.5.1 ships.

2. packages/state — TS5011.

    error TS5011: The common source directory of 'tsconfig.json' is './src'.
    The 'rootDir' setting must be explicitly set

TypeScript 6 requires rootDir to be explicit. packages/core and packages/config
already declare `"rootDir": "src"`; packages/state was the only one of the four
missing it. Added, matching its siblings.

Verified by running the exact CI command in a checkout staged the way CI stages
it — WITHOUT the ../packages/@nself/* sibling, which no nchat workflow clones:

    pnpm --filter "@nself-chat/ui" --filter "@nself-chat/core" \
         --filter "@nself-chat/state" --filter "@nself-chat/config" build
    exit 0

Before this change the same command exits 1 on TS5101, and 2 on TS5011 once
TS5101 is cleared.

Both tsconfigs stay strict JSON with no comments, matching every other tsconfig
in this repo.
desktop/ depends on @nself-chat/ui and @nself-chat/state via workspace:*,
so its Tauri build compiles whatever those packages emit. The three desktop
workflows only triggered on 'desktop/**', which meant a change to the shared
packages could break the desktop build without any PR ever running it.

That is how this PR's own fix had no evidence: every check on it was SKIPPED.
Add packages/** plus the lockfile and workspace manifest to the PR triggers.
With the trigger fixed in the previous commit, the desktop builds ran for the
first time and failed immediately:

  [vite]: Rollup failed to resolve import "@nself/observability"
          from desktop/src/main.tsx

pnpm-workspace.yaml globs '../packages/@nself/*' -- the nself-org/packages repo
as a SIBLING directory of this one. desktop/package.json depends on five of them
(auth-core, graphql-client, i18n, observability, ui) as workspace:*, and no nchat
workflow has ever checked that repo out. The layout only exists on a developer's
machine, so the desktop build could not have succeeded in CI at any point.

nclaw and ntask already solve this; the variant used here is nsentry's, which
checks out into a subdirectory and MOVEs it to the sibling path rather than
symlinking, because pnpm writes relative node_modules links that resolve against
the physical path and dangle through a symlink.

Also drop a stale 'nchat/' prefix from the Linux and macOS artifact and bundle
paths. Their build steps run with working-directory: desktop, so the bundles land
under desktop/src-tauri/target, not nchat/desktop/src-tauri/target -- Windows
already had this right. Both are downstream of a build that has never succeeded,
so neither had ever been exercised: the macOS bundle-size gate would have exited
1 with "no DMG found" and the Linux upload would have matched no files.
With the sibling repo checked out, the desktop build got one step further and
then failed on the lockfile:

  ERR_PNPM_OUTDATED_LOCKFILE  Cannot install with "frozen-lockfile" because
  pnpm-lock.yaml is not up to date with packages/@nself/errors/package.json
  specifiers in the lockfile ({}) don't match specs in package.json

pnpm-lock.yaml had 11 importers and none for '../packages/@nself/*', even though
pnpm-workspace.yaml globs them. It had only ever been generated with the sibling
absent. Regenerated against nself-org/packages@8a5cc53: 16 importers added.

Verified both scopes locally, because 24 other workflows install from this same
lockfile and none of them check the sibling out:

  sibling present -> "Scope: all 27 workspace projects", frozen install exit 0
  sibling absent  -> "Scope: all 11 workspace projects", frozen install exit 0

The glob simply matches nothing when the directory is not there, so the extra
importers are out of scope and the other workflows are unaffected. The only
resolution removed is third-party-web@0.29.2, an optional lighthouse transitive;
everything else in the diff is additive.

Pin the sibling checkout to that same sha rather than tracking main. A floating
sibling and a frozen lockfile contradict each other: the next push to
nself-org/packages would red every desktop build. The cost is that the sha and
the lockfile have to be bumped together, by hand -- Dependabot does not track a
checkout ref.
With the lockfile fixed, the Windows install step passed and the next one failed:

  packages/ui build: 'tsup' is not recognized as an internal or external command
  packages/config build: 'tsc' is not recognized as an internal or external command
  WARN Local package.json exists, but node_modules missing, did you mean to install?

The install is scoped on Windows to keep frontend's test-only native dep `canvas`
out of the graph (it has no Windows prebuilt and fails node-gyp on windows-2022).
But the scope it used, `...@nself-chat/desktop`, does not cover all four packages
the very next step builds, so ui/core/state/config never got a node_modules and
their build tools were absent.

Name the four explicitly instead of widening the filter, so the canvas exclusion
keeps working. The invariant is that the install scope must be a superset of the
build scope.

Linux and macOS install unscoped, so neither is affected.
Third failure in the same class, so stop enumerating and invert the filter.

The Windows install was scoped with positive filters, and each time the build
got further it hit another project that had been left out of the install graph:

  packages/{ui,core,state,config}   no node_modules -> "'tsc' is not recognized",
                                    "'tsup' is not recognized"
  ../packages/@nself/i18n           package resolved, its dependency i18next did
                                    not -> [vite]: Rollup failed to resolve
                                    import "i18next" from .../i18n/src/provider.tsx

@nself/i18n exports ./src/index.ts, so vite compiles its TypeScript source and
needs its real dependencies installed, not just a link to the package.

The exclusion was always the load-bearing part: shamefully-hoist=true hoists
every in-scope project's deps to the root node_modules, so the positive filter
never stopped pnpm resolving frontend's test-only native dep `canvas` -- dropping
@nself-chat/web from the graph is what skips it. `canvas` has no Windows prebuilt
and fails node-gyp on windows-2022.

So keep only the negative filter. Every workspace project except web is a
superset of anything this build can reach, and the canvas exclusion is unchanged.
With the JS half of the desktop build finally green, the Rust half compiled for
the first time and failed:

  error[E0599]: no method named `app_handle` found for reference `&tauri::Window`
    --> src/lib.rs:72:32
  error[E0599]: no method named `get_webview_window` found for struct `AppHandle<R>`
    --> src/lib.rs:86:51
  help: trait `Manager` which provides `app_handle` is implemented but not in scope

Both are trait methods on tauri::Manager, not inherent methods, so the trait has
to be in scope. lib.rs imported Emitter, Listener and WindowEvent but not Manager.

Only lib.rs was affected, which is why there were exactly two errors: tray.rs and
menu.rs use the same methods and already import Manager.

This has been latent for as long as the desktop build has been broken. Nothing
ever compiled this crate in CI, so no gate could report it.
The Linux desktop build now compiles, links and bundles. It produced all three
artifacts and then failed on the very last action:

  Finished 3 bundles at:
      .../bundle/deb/nChat_0.0.0_amd64.deb
      .../bundle/rpm/nChat-0.0.0-1.x86_64.rpm
      .../bundle/appimage/nChat_0.0.0_amd64.AppImage
  failed to decode secret key: incorrect updater private key password:
  Missing comment in secret key

tauri.conf.json sets createUpdaterArtifacts: true, so Tauri signs an updater
artifact on every build. There is no TAURI_SIGNING_PRIVATE_KEY in this repo or in
the org, so that step cannot succeed on any branch, and never could.

Disable updater artifacts for anything that is not a tag build. A PR has no
business signing a release artifact. Tag builds are untouched: they still set the
signing env and still produce and sign updater artifacts, so the release path is
unchanged and no gate is weakened.

The override is written to a file rather than passed inline after --config. An
unquoted {"bundle":{...}} argument loses its double quotes to the shell and Tauri
receives invalid JSON.

Two things this does NOT fix, both owner-gated and worth surfacing:
  - TAURI_SIGNING_PRIVATE_KEY / _PASSWORD are not set anywhere, so a tag build
    will hit this same error until they are provisioned.
  - tauri.conf.json has plugins.updater.pubkey: "", so the updater could not
    verify a signature even if one were produced.
The Windows build got all the way to the resource compiler and died there:

  resource.rc(26) : error RC2175 : resource file
    desktop/src-tauri/icons/icon.ico is not in 3.00 format
  thread 'main' panicked at tauri-winres-0.3.6/src/lib.rs:543:14:
  called `Result::unwrap()` on an `Err` value:
  Failed("RC.EXE failed to compile specified resource file")

RC.EXE was right. icon.ico was 70 bytes and not an ICO at all — a 1x1 pixel PNG
with an .ico extension. icon.icns was the same 70-byte 1x1 PNG, so the macOS
bundle carried a placeholder too. The three PNGs were the correct dimensions and
solid black: exactly one distinct colour across every pixel.

Nothing had ever caught this because the desktop build had never reached the
resource compiler on any platform.

Regenerated all five from the brand artwork already committed in this repo,
.github/wiki/brand/icons/icon-1k.png (1024x1024, real artwork). No new asset is
introduced and nothing is designed here — this is a derivation from the project's
own icon.

  icon.ico          70 B  ->  101 KB, 7 resolutions 16..256
  icon.icns         70 B  ->  1.4 MB, built with iconutil from a full iconset
  32x32.png        solid black -> 680 colours
  128x128.png      solid black -> 4088 colours
  128x128@2x.png   solid black -> 8565 colours

For reference, nclaw ships a 361 KB multi-resolution ICO and ntask a 33 KB one.
nchat was the outlier.
…hat exists

Two separate macOS failures, neither previously reachable because the build had
never got this far.

1. Code signing. The aarch64 leg built, then died bundling:

     failed to bundle project failed codesign application:
     failed to run command security import: failed to import keychain certificate

   This repo has ZERO APPLE_* secrets (checked by name). `${{ secrets.X }}` for an
   unset secret yields a variable that is DEFINED AND EMPTY, and tauri-bundler does
   not read that as "no identity" — it runs `security import` on an empty
   certificate and fails. The variable has to be genuinely ABSENT. Export each one
   only when its secret actually holds a value. nclaw hit the identical trap and
   documents it in its own desktop-macos.yml.

   A PR now produces an unsigned .app/.dmg, which is the correct outcome. A tag
   build with the secrets present signs exactly as before.

2. Runner. The x64 leg asked for macos-13 and was never scheduled — queued for
   over 90 minutes on one run and cancelled on another, while the aarch64 leg on
   macos-14 ran fine. macos-13 is retired. nclaw builds x86_64-apple-darwin on
   macos-15-intel and succeeded there today, so use the same label.

The env vars are renamed to *_SECRET and re-exported under their real names inside
the step, since a workflow cannot conditionally omit an `env:` key.
@github-actions

Copy link
Copy Markdown

Security Scan Results: PASSED

Scanner Critical High
SAST 0 0
Dependencies 0 0

@github-actions

Copy link
Copy Markdown

Coverage after merging fix/ui-dts-ts5101 into main will be

30.09%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src
   middleware.ts0%0%0%0%100, 100, 108, 116–117, 117–118, 122–123, 123–124, 128–129, 129–130, 133, 141, 143–144, 144, 144, 144–145, 151–152, 152–154, 154, 154, 154–156, 156–157, 157, 157, 16, 162, 164, 172, 179–181, 196, 198, 198, 198, 200, 204, 204, 21, 210, 210, 210, 210, 210, 210, 217, 217, 220, 230, 233, 236–240, 246, 246–247, 254, 256, 26, 262–263, 266, 266–267, 272–273, 278, 282, 282, 282, 282, 282, 284, 288, 290, 290–292, 295, 295–296, 303, 306, 306, 306, 306, 306, 306, 312–313, 313, 313, 317–318, 318, 318–319, 322, 322, 324, 324–325, 325, 325, 325, 325, 331–332, 334–335, 339, 339, 341, 341–345, 349, 349, 349, 349–350, 350, 352–355, 360–361, 365–366, 372, 40, 45, 50, 55, 60, 66–67, 67–68, 70, 70, 70, 78, 85, 92, 99
src/app
   error.tsx0%100%0%0%16–17, 19, 3–5
   layout.tsx0%0%0%0%10, 107, 123, 147, 150, 2, 20, 20, 20, 22, 22, 22, 226, 226, 24, 3–5, 8
   loading.tsx0%100%0%0%1, 7
   not-found.tsx0%100%0%0%1, 7
   page.tsx0%0%0%0%3, 35–38, 4, 40, 40, 40, 42–43, 43, 46, 46–48, 5, 52, 52, 52, 52, 52, 54, 54–55, 57, 6, 60–61, 65, 7, 73
src/app/(authenticated)/bookmarks
   page.tsx0%100%0%0%10–12, 14, 9
src/app/activity
   page.tsx0%0%0%0%10, 113, 113, 134, 142, 142, 147, 16, 162, 162, 162, 167, 167, 167, 17, 171, 18, 184, 186, 186, 189, 191, 191, 191, 191–193, 198, 205, 205, 210, 210, 222, 229, 229, 58, 74, 77, 80, 80–81, 9, 91–92
src/app/admin
   loading.tsx0%0%0%0%1, 104, 2–3, 38, 42, 42, 77, 9, 91
   page.tsx0%0%0%0%100–101, 103–104, 104, 104, 104, 104–105, 109–111, 111, 111, 111, 111–112, 115, 115, 115, 115, 115–116, 17–19, 199, 20–21, 239, 248, 248, 251, 251, 257, 261, 261, 28–29, 3, 30–31, 36, 39, 4, 43, 5, 52, 6–7, 90, 97–99
src/app/admin/advanced
   page.tsx0%0%0%0%106–110, 112–113, 113, 113, 113, 113–114, 118, 118, 118, 118, 118–119, 14–15, 25–26, 33–38, 392, 394, 41–45, 48, 77
src/app/admin/analytics
   page.tsx0%0%0%0%100, 100, 100, 100, 100, 103–107, 117, 124–125, 125, 130, 133, 133, 133–135, 135–137, 141, 141, 141–143, 143–145, 15, 154–159, 16, 160, 160, 163, 167–168, 168, 168–169, 169, 169, 17, 171, 171, 171, 173, 177, 18, 181–182, 182–183, 191, 193, 196, 196, 196–197, 200, 200, 208–209, 209–210, 216, 219, 219, 224–226, 226, 226–228, 228, 228, 230, 25, 256–257, 259, 26, 263, 265–266, 269, 271–279, 282, 282, 3, 313, 33, 331, 331, 34–36, 4, 41, 44, 49, 5, 52, 57, 60, 65, 68, 73, 76, 81, 84, 88, 96–98
src/app/admin/audit
   page.tsx0%0%0%0%116–117, 119–120, 123, 123, 123, 123, 123, 123, 125, 127, 129–130, 132, 134, 136, 138, 140, 143–144, 147, 149, 149, 149, 151, 151, 155, 163, 163, 171, 171, 177, 177, 189, 19, 196–198, 206, 209, 21, 211–212, 215–216, 216, 216, 216, 216–217, 22, 222–223, 225–229, 23, 233–234, 236–240, 243–244, 247–250, 252–255, 258, 258, 258, 258, 258–259, 263–267, 28, 289, 289, 291, 291, 299, 30, 303, 305, 305, 307, 307, 31, 322, 322, 43, 50, 59–60, 67, 7, 73, 79, 8, 80, 88, 9
src/app/admin/audit/security
   page.tsx0%0%0%0%10, 114, 12, 122, 13, 136, 136, 136, 140, 148, 148, 148, 15,

@acamarata
acamarata merged commit c049085 into main Sep 15, 2026
45 checks passed
@acamarata
acamarata deleted the fix/ui-dts-ts5101 branch September 15, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant