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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,10 @@ RUST_LOG=buzz_relay=debug,buzz_datastore=info,buzz_db=debug,buzz_auth=debug,buzz
# app launch while keeping the current identity and relay data.
# VITE_BUZZ_FORCE_FRESH_ONBOARDING=true

# Protected internal builds only: selects the module graph that contains the
# default-off Bestie experiment. Official OSS builds must leave this unset.
# VITE_BUZZ_BESTIE=1

# ── Subscription & filtering ─────────────────────────────────────────────────
# Subscribe mode: "mentions" (default), "all", or "config" (rule-based).
# BUZZ_ACP_SUBSCRIBE=mentions
Expand Down
36 changes: 35 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ jobs:
name: Desktop
runs-on: ubuntu-latest
timeout-minutes: 5
needs: [changes, desktop-core, desktop-smoke-e2e]
needs: [changes, desktop-core, desktop-smoke-e2e, desktop-windows-build]
if: always() && (github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true')
permissions:
contents: read
Expand All @@ -319,6 +319,10 @@ jobs:
echo "Desktop Smoke E2E shards finished with: ${{ needs.desktop-smoke-e2e.result }}"
exit 1
fi
if [ "${{ needs.desktop-windows-build.result }}" != "success" ]; then
echo "Desktop Windows Build finished with: ${{ needs.desktop-windows-build.result }}"
exit 1
fi
echo "Desktop jobs passed"

desktop-e2e-relay:
Expand Down Expand Up @@ -1121,6 +1125,36 @@ jobs:
-p git-credential-nostr \
-p git-sign-nostr

desktop-windows-build:
name: Desktop Windows Build
runs-on: windows-latest
timeout-minutes: 20
needs: [changes]
if: github.event_name == 'push' || needs.changes.outputs.desktop == 'true' || needs.changes.outputs.desktop-rust == 'true' || needs.changes.outputs.rust == 'true'
permissions:
contents: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 24.14.1
package-manager-cache: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: 11.4.0
- name: Install desktop dependencies
shell: bash
run: pnpm install --frozen-lockfile
- name: Build both protected-feature selections
shell: pwsh
run: |
Remove-Item Env:VITE_BUZZ_BESTIE -ErrorAction SilentlyContinue
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$env:VITE_BUZZ_BESTIE = "1"
pnpm -C desktop build
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

windows-rust:
name: Windows Rust (x86_64-pc-windows-msvc)
runs-on: windows-latest
Expand Down
6 changes: 3 additions & 3 deletions desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc && node ./scripts/build-protected-feature-artifacts.mjs",
"build:e2e": "tsc && vite build --mode e2e",
"typecheck": "tsc --noEmit",
"check:file-sizes": "node ./scripts/check-file-sizes.mjs",
Expand All @@ -16,13 +16,13 @@
"format": "biome format --write .",
"test": "node --import ./test-loader.mjs --experimental-strip-types --test \"src/**/*.test.mjs\"",
"preview": "vite preview",
"tauri": "tauri",
"tauri": "node ./scripts/tauri-command.mjs",
"test:e2e": "pnpm build:e2e && playwright test",
"test:e2e:smoke": "pnpm build:e2e && playwright test --project=smoke",
"test:e2e:integration": "pnpm build:e2e && playwright test --project=integration",
"test:e2e:release-smoke": "pnpm build:e2e && playwright test --config=playwright.release-smoke.config.ts",
"test:e2e:report": "playwright show-report",
"tauri:build": "tauri build"
"tauri:build": "node ./scripts/tauri-command.mjs build"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
Expand Down
151 changes: 151 additions & 0 deletions desktop/scripts/build-protected-feature-artifacts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { spawnSync } from "node:child_process";
import {
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
} from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { loadEnv } from "vite";

const desktopRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const vitePackageJsonPath = fileURLToPath(
import.meta.resolve("vite/package.json"),
);
const vitePackage = JSON.parse(readFileSync(vitePackageJsonPath, "utf8"));
const viteEntrypoint = path.resolve(
path.dirname(vitePackageJsonPath),
vitePackage.bin.vite,
);

function buildVariant({ internal, output }) {
const env = {
...process.env,
// Pin both children explicitly. Deleting the OSS value lets Vite reload
// `=1` from .env.local or a mode-specific env file.
VITE_BUZZ_BESTIE: internal ? "1" : "0",
};

const result = spawnSync(
process.execPath,
[viteEntrypoint, "build", "--outDir", output, "--emptyOutDir"],
{
cwd: desktopRoot,
env,
stdio: "inherit",
},
);
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(
`${internal ? "internal" : "OSS"} desktop build failed with status ${result.status}`,
);
}
}

function emittedText(root) {
const chunks = [];
const visit = (candidate) => {
const stat = statSync(candidate);
if (stat.isDirectory()) {
for (const child of readdirSync(candidate)) {
visit(path.join(candidate, child));
}
return;
}
if (/\.(?:css|html|js|json)$/u.test(candidate)) {
chunks.push(readFileSync(candidate, "utf8"));
}
};
visit(root);
return chunks.join("\n");
}

export function assertArtifactContract({ ossOutput, internalOutput }) {
const ossText = emittedText(ossOutput);
const internalText = emittedText(internalOutput);
const protectedContent = /\bbestie\b|chief of staff|builtin:bestie/iu;
const internalManifestMarker =
"Try a personal agent that is always close at hand";

if (protectedContent.test(ossText)) {
throw new Error(
"Official OSS desktop artifact contains protected Bestie/Chief content",
);
}
if (!internalText.includes(internalManifestMarker)) {
throw new Error(
"Protected internal desktop artifact is missing the Bestie manifest",
);
}
}

/** Resolve the requested output with the same precedence used by Vite config. */
export function selectInternalVariant({ processEnv, modeEnv }) {
return (processEnv.VITE_BUZZ_BESTIE ?? modeEnv.VITE_BUZZ_BESTIE) === "1";
}

/** Build and inspect both graphs, leaving the requested variant in dist. */
export function buildArtifactMatrix({
selectedInternalVariant,
selectedOutput,
alternateOutput,
build = buildVariant,
}) {
// Build the unselected variant outside dist first, then leave the requested
// variant in dist for Vite/Tauri's ordinary packaging contract.
build({
internal: !selectedInternalVariant,
output: alternateOutput,
});
build({
internal: selectedInternalVariant,
output: selectedOutput,
});

assertArtifactContract({
ossOutput: selectedInternalVariant ? alternateOutput : selectedOutput,
internalOutput: selectedInternalVariant ? selectedOutput : alternateOutput,
});
}

function main() {
const selectedInternalVariant = selectInternalVariant({
processEnv: process.env,
modeEnv: loadEnv("production", desktopRoot, ""),
});
const scratchRoot = mkdtempSync(
path.join(tmpdir(), "buzz-protected-feature-artifacts-"),
);
const selectedOutput = process.env.BUZZ_PROTECTED_BUILD_OUTPUT
? path.resolve(process.env.BUZZ_PROTECTED_BUILD_OUTPUT)
: path.join(desktopRoot, "dist");
const alternateOutput = path.join(scratchRoot, "alternate");

try {
buildArtifactMatrix({
selectedInternalVariant,
selectedOutput,
alternateOutput,
});
} finally {
rmSync(scratchRoot, { recursive: true, force: true });
}

console.log(
`Protected feature artifact matrix passed; dist contains the ${selectedInternalVariant ? "internal" : "OSS"} variant.`,
);
}

if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
main();
}
63 changes: 63 additions & 0 deletions desktop/scripts/tauri-command.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";

const desktopRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
const tauriPackageJsonPath = fileURLToPath(
import.meta.resolve("@tauri-apps/cli/package.json"),
);
const tauriPackage = JSON.parse(readFileSync(tauriPackageJsonPath, "utf8"));
const defaultTauriEntrypoint = path.resolve(
path.dirname(tauriPackageJsonPath),
tauriPackage.bin.tauri,
);

function runTauri(args, options = {}) {
const entrypoint =
process.env.BUZZ_TAURI_CLI_ENTRYPOINT ?? defaultTauriEntrypoint;
const result = spawnSync(process.execPath, [entrypoint, ...args], {
cwd: desktopRoot,
env: { ...process.env, ...options.env },
stdio: "inherit",
});
if (result.error) throw result.error;
return result.status ?? 1;
}

export function runTauriCommand(args) {
if (args[0] !== "build") return runTauri(args);

// Tauri runs beforeBuildCommand and then consumes frontendDist. Give the
// entire invocation a private directory so concurrent OSS/internal packages
// cannot replace one another's assets between those two operations.
const invocationRoot = mkdtempSync(
path.join(tmpdir(), "buzz-tauri-package-assets-"),
);
const frontendDist = path.join(invocationRoot, "dist");
const outputOverride = JSON.stringify({ build: { frontendDist } });

try {
const delimiterIndex = args.indexOf("--");
const configIndex = delimiterIndex === -1 ? args.length : delimiterIndex;
const tauriArgs = [...args];
tauriArgs.splice(configIndex, 0, "--config", outputOverride);
return runTauri(tauriArgs, {
env: { BUZZ_PROTECTED_BUILD_OUTPUT: frontendDist },
});
} finally {
rmSync(invocationRoot, { recursive: true, force: true });
}
}

if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
process.exitCode = runTauriCommand(process.argv.slice(2));
}
9 changes: 6 additions & 3 deletions desktop/src/features/settings/ui/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
canManageCommunityMembers,
shouldWarnMissingMembershipSnapshot,
} from "@/shared/api/relayMembers";
import { getFeature } from "@/shared/features/manifest";
import {
getFeature,
resolveEnabled,
useFeatureSnapshot,
} from "@/shared/features/useFeatureEnabled";
} from "@/shared/features";
import { topChromeBackdrop } from "@/shared/layout/chromeLayout";
import { cn } from "@/shared/lib/cn";
import {
Expand Down Expand Up @@ -137,7 +137,10 @@ export function SettingsView({
// stable and renders unconditionally (fail-open).
if (s.featureGate) {
const feature = getFeature(s.featureGate);
if (feature && !resolveEnabled(s.featureGate, featureState)) {
if (
feature &&
!resolveEnabled(s.featureGate, featureState, feature.defaultEnabled)
) {
return false;
}
}
Expand Down
Loading
Loading