fix(icons): commit generated icons; installs and regular builds never render
User installs failed with 'resvg-py is missing' because the web/desktop source builds rendered icons on whatever python was on PATH. The default brand outputs are now committed; source_build, apps/desktop build.mjs and the npm/docusaurus pre-hooks consume them directly. Flavored release bundles (canary/commit) still render into their own product dir. icons-freshness-check now regenerates and fails on any byte diff.
26
.github/workflows/icons-freshness-check.yml
vendored
@@ -1,13 +1,11 @@
|
||||
name: Icon Assets Freshness
|
||||
|
||||
# Guard for the icon pipeline. The sources of truth are the nous-girl SVGs
|
||||
# (assets/nous-girl-*.svg) and the per-platform backgrounds
|
||||
# (assets/backgrounds/*.svg). Generated outputs are NOT committed — every
|
||||
# consuming pipeline regenerates them via scripts/generate-icons.mjs — so
|
||||
# this lane regenerates everything from a fresh checkout and runs --check,
|
||||
# which asserts structural invariants (sizes, transparent squircle corners,
|
||||
# container frame sets). A source change can never land broken. See
|
||||
# scripts/generate_icons.py.
|
||||
# The committed icon outputs must match what scripts/generate_icons.py
|
||||
# renders from assets/nous-girl-*.svg + assets/backgrounds/*.svg. Regular
|
||||
# builds and user installs consume the committed files and never render, so
|
||||
# this lane is what keeps them honest: regenerate, structural --check, then
|
||||
# fail on any byte difference. Fix a red run with
|
||||
# `node scripts/generate-icons.mjs` on a Hermes runtime interpreter and commit.
|
||||
#
|
||||
# Byte-determinism note: resvg renders the same SVG to identical bytes on any
|
||||
# host for pure-shape artwork. If the master ever contains <text>, font
|
||||
@@ -22,7 +20,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Regenerate icons + structural check
|
||||
name: Regenerate icons + diff against committed
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
@@ -36,7 +34,15 @@ jobs:
|
||||
cache-python: true
|
||||
prune-python-cache: true
|
||||
|
||||
- name: Regenerate every target, then verify structure
|
||||
- name: Regenerate every target, verify structure, diff against committed
|
||||
env:
|
||||
HERMES_PAYLOAD_TAG: ''
|
||||
HERMES_BUILD_COMMIT: ''
|
||||
run: |
|
||||
node scripts/generate-icons.mjs
|
||||
node scripts/generate-icons.mjs --check
|
||||
if ! git diff --exit-code --stat || [ -n "$(git status --porcelain --untracked-files=all)" ]; then
|
||||
git status --porcelain --untracked-files=all
|
||||
echo "::error::Committed icons are stale. Run 'node scripts/generate-icons.mjs' and commit the result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
14
.github/workflows/rust-tests.yml
vendored
@@ -38,20 +38,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
# tauri::generate_context! embeds the app icons, which are generated,
|
||||
# not committed. The generator runs on a Hermes runtime interpreter; []
|
||||
# exports one as HERMES_PYTHON.
|
||||
- uses: ./.github/actions/setup-pm
|
||||
with:
|
||||
toolchain: all
|
||||
extras: '[]'
|
||||
cache-python: true
|
||||
prune-python-cache: true
|
||||
|
||||
- name: Generate icons
|
||||
working-directory: .
|
||||
run: node scripts/generate-icons.mjs
|
||||
|
||||
# Tauri links against the system webkit2gtk on Linux, so the crate does
|
||||
# not compile without these even for `cargo test --lib`.
|
||||
- name: Install Tauri system dependencies
|
||||
|
||||
28
.gitignore
vendored
@@ -318,32 +318,4 @@ native/fts5_cjk/*.so
|
||||
# PM lockfile advisory lock; kept in place so waiting writers share its inode
|
||||
/pm/.lock.json.lock
|
||||
|
||||
# Generated icon assets (scripts/generate_icons.py). Regenerated on demand by
|
||||
# every consuming pipeline; sources of truth: assets/nous-girl-*.svg + assets/backgrounds/.
|
||||
assets/icon-master.svg
|
||||
assets/icon-master-dark.svg
|
||||
apps/desktop/assets/icon.png
|
||||
apps/desktop/assets/icon.ico
|
||||
apps/desktop/assets/icon.icns
|
||||
apps/desktop/assets/icon-dark.png
|
||||
apps/desktop/assets/icon-dark.ico
|
||||
apps/desktop/assets/icon-dark.icns
|
||||
apps/desktop/assets/appx/*.png
|
||||
apps/desktop/public/apple-touch-icon.png
|
||||
apps/desktop/public/nous-girl.png
|
||||
apps/desktop/public/nous-girl-dark.png
|
||||
apps/desktop/.dist-build*
|
||||
apps/bootstrap-installer/src-tauri/icons/*.png
|
||||
apps/bootstrap-installer/src-tauri/icons/icon.ico
|
||||
apps/bootstrap-installer/src-tauri/icons/icon.icns
|
||||
apps/bootstrap-installer/public/nous-girl.png
|
||||
website/static/img/logo.png
|
||||
website/static/img/logo-dark.png
|
||||
website/static/img/nous-logo.png
|
||||
website/static/img/nous-logo-dark.png
|
||||
website/static/img/favicon-16x16.png
|
||||
website/static/img/favicon-32x32.png
|
||||
website/static/img/apple-touch-icon.png
|
||||
website/static/img/favicon.ico
|
||||
website/static/img/favicon.svg
|
||||
web/public/favicon.ico
|
||||
|
||||
@@ -197,7 +197,8 @@ Native desktop dependencies can also require the platform build toolchain.
|
||||
Logos and icons are generated from `assets/nous-girl-*.svg` and
|
||||
`assets/backgrounds/`. `node scripts/generate-icons.mjs` renders them with the
|
||||
Hermes runtime Python (`HERMES_PYTHON`, else `python` on PATH): Pillow and
|
||||
resvg-py are core dependencies. Do not commit generated PNG/ICO/ICNS outputs.
|
||||
resvg-py are core dependencies. Generated outputs are committed and CI fails if
|
||||
they are stale; rerun the generator and commit after changing any source SVG.
|
||||
|
||||
### Run tests
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 5175",
|
||||
"prebuild": "node ../../scripts/generate-icons.mjs --source ../.. --out ../..",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
|
||||
BIN
apps/bootstrap-installer/public/nous-girl.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/bootstrap-installer/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
apps/bootstrap-installer/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
apps/bootstrap-installer/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
apps/bootstrap-installer/src-tauri/icons/icon.icns
Normal file
BIN
apps/bootstrap-installer/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
apps/desktop/assets/appx/Square150x150Logo-dark.png
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
BIN
apps/desktop/assets/appx/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
apps/desktop/assets/appx/Square44x44Logo-dark.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
apps/desktop/assets/appx/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
apps/desktop/assets/appx/StoreLogo-dark.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
apps/desktop/assets/appx/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
apps/desktop/assets/appx/Wide310x150Logo-dark.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
apps/desktop/assets/appx/Wide310x150Logo.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
apps/desktop/assets/icon-dark.icns
Normal file
BIN
apps/desktop/assets/icon-dark.ico
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
apps/desktop/assets/icon-dark.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
apps/desktop/assets/icon.icns
Normal file
BIN
apps/desktop/assets/icon.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
apps/desktop/assets/icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
@@ -19,7 +19,7 @@
|
||||
"clean:e2e": "tsc --build tsconfig.e2e.json --clean",
|
||||
"clean:renderer": "tsc --build tsconfig.json --clean ",
|
||||
"clean:electron": "tsc --build tsconfig.electron.json --clean",
|
||||
"dev": "node ../../scripts/generate-icons.mjs && concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"",
|
||||
"dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev",
|
||||
"dev:mock": "node ../../tests-js/scripts/mock-server.ts",
|
||||
"dev:renderer": "node scripts/assert-root-install.mjs && npm run clean:renderer && vite --host 127.0.0.1 --port 5174",
|
||||
|
||||
BIN
apps/desktop/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
apps/desktop/public/nous-girl-dark.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/desktop/public/nous-girl.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
@@ -3,19 +3,15 @@ import { execFileSync } from 'node:child_process'
|
||||
import { cpSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
import { generateIcons } from '../../../scripts/generate-icons.mjs'
|
||||
import { isMain, repoRoot } from '../../../scripts/build/frontend-common.mjs'
|
||||
|
||||
export function buildSourceDesktop({ source = repoRoot, icons, run = execFileSync, generate = generateIcons } = {}) {
|
||||
export function buildSourceDesktop({ source = repoRoot, icons, run = execFileSync } = {}) {
|
||||
source = resolve(source)
|
||||
const app = join(source, 'apps/desktop')
|
||||
const step = (script, args = []) => run(process.execPath, [join(source, script), ...args], { cwd: app, stdio: 'inherit' })
|
||||
step('apps/desktop/scripts/assert-root-install.mjs')
|
||||
if (!icons) {
|
||||
if (generate(['--source', source, '--out', source]) !== 0) throw new Error('Icon preparation failed')
|
||||
icons = source
|
||||
}
|
||||
icons = resolve(icons)
|
||||
// Default-brand icons are committed; only flavored release builds pass --icons.
|
||||
icons = resolve(icons ?? source)
|
||||
if (icons !== source) {
|
||||
// electron-builder consumes packaging artwork in the workspace. Copy the
|
||||
// prepared pixels; do not create another Python environment to redraw them.
|
||||
|
||||
7
assets/icon-master-dark.svg
Normal file
|
After Width: | Height: | Size: 135 KiB |
7
assets/icon-master.svg
Normal file
|
After Width: | Height: | Size: 114 KiB |
@@ -85,9 +85,8 @@ def build_source_tui(project_root: Path, *, env: dict) -> None:
|
||||
|
||||
|
||||
def build_source_web(project_root: Path, *, env: dict, icons: Path | None = None) -> None:
|
||||
if icons is None:
|
||||
icons = project_root
|
||||
run_source_script(project_root, "scripts/generate-icons.mjs", env=env, label="Generating icons")
|
||||
# Default-brand icons are committed; installs never render them.
|
||||
icons = icons or project_root
|
||||
run_source_script(project_root, "scripts/build/web.mjs", "--source", str(project_root),
|
||||
"--icons", str(icons), "--out", str(project_root / "hermes_cli/web_dist"), env=env,
|
||||
label="Building the web UI")
|
||||
|
||||
@@ -36,13 +36,9 @@ She renders in front of the border, clipped only to the outer rounded silhouette
|
||||
Only nodes near her bottom edge extend to the border; the fitted face and hair stay fixed.
|
||||
Standalone wordmarks remain centered and have no border.
|
||||
|
||||
GENERATED OUTPUTS ARE NOT COMMITTED. Everything this script writes is
|
||||
gitignored and regenerated on demand by the consuming pipelines (website
|
||||
prebuild, desktop prebuild/predev, installer prebuild, web prebuild — via
|
||||
scripts/generate-icons.mjs). The freshness lane (icons-freshness-check.yml)
|
||||
runs --check, which regenerates in memory and asserts structural invariants
|
||||
(sizes, transparency, container frame sets) — there are no committed bytes to
|
||||
byte-compare against.
|
||||
GENERATED OUTPUTS ARE COMMITTED. Regular builds and installs consume them and
|
||||
never render; flavored release bundles (canary/commit) render to a product dir.
|
||||
icons-freshness-check.yml regenerates, runs --check, and fails on any diff.
|
||||
|
||||
Rendering: resvg (resvg-py) for SVG -> PNG fidelity at every size.
|
||||
Containers: Pillow for multi-size .ico and .icns.
|
||||
|
||||
@@ -15,7 +15,6 @@ test('desktop development composition reuses prepared icon pixels instead of pro
|
||||
put(join(input.icons, 'apps/desktop/assets/icon.ico'), 'prepared packaging icon')
|
||||
const commands = []
|
||||
buildSourceDesktop({ source: input.source, icons: input.icons,
|
||||
generate: () => { throw new Error('prepared icons must not regenerate') },
|
||||
run: (command, args) => commands.push([command, ...args]),
|
||||
})
|
||||
expect(readFileSync(join(input.source, 'apps/desktop/assets/icon.ico'), 'utf8')).toBe('prepared packaging icon')
|
||||
|
||||
@@ -167,7 +167,7 @@ def source_products(source_checkout):
|
||||
encoding="utf-8",
|
||||
)
|
||||
for script, step, output in [
|
||||
("generate-icons.mjs", "icons", "web/public/favicon.ico"),
|
||||
("generate-icons.mjs", "icons", "never-rendered-at-install"),
|
||||
("build/tui.mjs", "tui", "ui-tui/dist/entry.js"),
|
||||
("build/web.mjs", "web", "hermes_cli/web_dist/index.html"),
|
||||
]:
|
||||
@@ -188,6 +188,9 @@ def source_products(source_checkout):
|
||||
"if (!process.argv.includes('--icons')) await import('../generate-icons.mjs');\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
# Default-brand icons are committed; updates consume them without rendering.
|
||||
(root / "web/public").mkdir(parents=True, exist_ok=True)
|
||||
(root / "web/public/favicon.ico").write_bytes(b"committed icon")
|
||||
return root, acquired
|
||||
|
||||
|
||||
@@ -252,7 +255,7 @@ def test_update_builds_selected_products_after_one_union_preparation(source_prod
|
||||
app.write_text("previous app")
|
||||
build_update_products(root, desktop=desktop)
|
||||
steps = [event["step"] for event in _events(root)]
|
||||
assert steps == ["deps", "tui", "icons", "web"] + (["desktop"] if desktop else [])
|
||||
assert steps == ["deps", "tui", "web"] + (["desktop"] if desktop else [])
|
||||
assert acquired == ["npm"]
|
||||
assert (root / "ui-tui/dist/entry.js").read_text() == "tui"
|
||||
assert (root / "hermes_cli/web_dist/index.html").read_text() == "web"
|
||||
@@ -264,7 +267,7 @@ def test_update_builds_selected_products_after_one_union_preparation(source_prod
|
||||
|
||||
|
||||
@pytest.mark.platforms("linux")
|
||||
@pytest.mark.parametrize("step", ["tui", "icons", "web", "desktop"])
|
||||
@pytest.mark.parametrize("step", ["tui", "web", "desktop"])
|
||||
def test_update_failure_raises_without_retries_or_replacing_live_app(source_products, step):
|
||||
from hermes_cli.source_build import build_update_products
|
||||
|
||||
@@ -277,7 +280,7 @@ def test_update_failure_raises_without_retries_or_replacing_live_app(source_prod
|
||||
build_update_products(root, desktop=True)
|
||||
assert app.read_text() == "previous app"
|
||||
assert not list((root / "apps/desktop").glob(".staging-*"))
|
||||
order = ["deps", "tui", "icons", "web", "desktop"]
|
||||
order = ["deps", "tui", "web", "desktop"]
|
||||
assert [event["step"] for event in _events(root)] == order[:order.index(step) + 1]
|
||||
assert acquired == ["npm"]
|
||||
assert not (Path(os.environ["HERMES_HOME"]) / "desktop-build-stamp.json").exists()
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "node ../scripts/generate-icons.mjs --source .. --out ..",
|
||||
"dev": "vite",
|
||||
"prebuild": "node ../scripts/generate-icons.mjs --source .. --out ..",
|
||||
"build": "node ../scripts/build/web.mjs",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
|
||||
BIN
web/public/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
@@ -33,7 +33,6 @@ const repoRoot = resolve(websiteDir, "..");
|
||||
const extractScript = join(scriptDir, "extract-skills.py");
|
||||
const llmsScript = join(scriptDir, "generate-llms-txt.py");
|
||||
const cronBlueprintsScript = join(scriptDir, "extract-automation-blueprints.py");
|
||||
const iconGenScript = join(repoRoot, "scripts", "generate-icons.mjs");
|
||||
const pluginsScript = join(scriptDir, "extract-plugins.py");
|
||||
const pluginStarsScript = join(scriptDir, "fetch-plugin-stars.py");
|
||||
const outputFile = join(websiteDir, "static", "api", "skills.json");
|
||||
@@ -125,19 +124,7 @@ async function ensureUnifiedIndex() {
|
||||
}
|
||||
}
|
||||
|
||||
// 0) Icon assets — the navbar logo, favicons, and apple-touch-icon are
|
||||
// generated (not committed). This must fail loudly: a docs build without the
|
||||
// logo ships a broken navbar.
|
||||
console.log("[prebuild] generating icon assets…");
|
||||
{
|
||||
const r = spawnSync("node", [iconGenScript, "--source", repoRoot, "--out", repoRoot], { stdio: "inherit", cwd: repoRoot });
|
||||
if (r.status !== 0) {
|
||||
console.error("[prebuild] icon generation failed — it needs a Hermes runtime Python (HERMES_PYTHON); see the output above");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 0b) Pull unified index if we don't have a fresh one.
|
||||
// 0) Pull unified index if we don't have a fresh one.
|
||||
await ensureUnifiedIndex();
|
||||
|
||||
// 1) skills.json — required for the Skills Hub page.
|
||||
|
||||
BIN
website/static/img/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
website/static/img/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 691 B |
BIN
website/static/img/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
website/static/img/favicon.ico
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
7
website/static/img/favicon.svg
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
website/static/img/logo-dark.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
website/static/img/logo.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
website/static/img/nous-logo-dark.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
website/static/img/nous-logo.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |