Commit Graph

663 Commits

Author SHA1 Message Date
Teknium
cdce56faf2 plugin-catalog CI: enforce rule 17 — scan pinned entries for unscannable payloads
The admission gate ran the install scanner (dangerous fails) but nothing refused
a compiled binary, committed bytecode, or an archive hiding either — the shapes
the scanner cannot read. This adds that enforcement to pinned-source-validate.

- scripts/scan_plugin_payloads.py (stdlib-only, so the no-install validator and
  third-party CI can run it): flags compiled binaries by magic header (ELF,
  Mach-O, PE/MZ, WebAssembly — not by extension, which a rename defeats),
  .pyc/.pyo bytecode (incl. sourceless/hash-forged caches that run instead of
  the .py beside them), and archives whose members are not reviewable source.
  Plain-text source bundles and inert assets (fonts, images) pass.
- plugin-catalog-ci.yml: after `hermes plugins validate`, each changed entry is
  scanned; a finding fails it. An entry may list `attested_binaries:` paths,
  each verified with `gh attestation verify --repo <repo> --source-digest <sha>`
  against the pinned commit; verified paths are dropped before the scan, any
  other binary still fails. Repo-controlled filenames are newline-stripped in
  the log so none can forge a workflow command.
- validate_plugin_catalog.py: `attested_binaries` is a known key, shape-checked
  as a list of relative paths.
- Tests: unit tests for the detector and two new workflow-driven bypass cases
  (bundled .exe, committed .pyc) in the confinement harness, which runs the real
  `run:` block.

Full-content re-sweep of all 543 pins: only tabro carries a native binary (it is
delisted in the companion PR). Three entries (crawl4ai, hermes-vision-inline,
mermail) commit __pycache__/*.pyc build litter and would fail rule 17 on their
next pin bump — pre-existing, not touched here.
2026-10-11 03:03:39 -07:00
Teknium
c152ba5a82 docs(ci): prompt-surface changes need one core approval, not two
The prompt-surface-review ruleset was meant to require one other reviewer;
it is now set to one hermes-agent-core approval. CODEOWNERS, the CI comment,
the staleness-test message and the developer docs said two.
2026-10-11 02:26:46 -07:00
Teknium
ae3598c7cd feat(ci): reviewers see every change to the system prompt and tool schemas
A one-line code change can rewrite what every session sends to the model -- a
guidance constant moving tiers, a toolset gaining a tool, a dynamic rewriter
editing a description -- without the diff showing it. The rendered prompt
surface is now committed and reviewed like code.

- scripts/ci/prompt_surface.py renders the system prompt
  (build_system_prompt_parts) and the agent's tools[] through the production
  code for nine surfaces (cli x claude/gpt/gemini/hermes, cli in a code
  workspace, desktop, telegram, discord, cron) into
  tests/fixtures/prompt_surface/. A fresh home in a child process; clock,
  host OS, network, tool availability, env probe and machine paths pinned,
  so a laptop and CI produce the same bytes. Each tool schema is stored once,
  with a per-surface copy only where a surface serves a different schema.
- tests/ci/test_prompt_surface.py fails when the committed snapshot is stale,
  so a surface change must carry the regenerated snapshot in the same PR.
- prompt-surface-diff.yml (advisory, PR-only, stdlib-only, no PR code runs)
  posts per-surface token deltas, prompt hunks and per-tool
  description/param changes into the unified review comment.
- CODEOWNERS requests hermes-agent-core on the snapshot; a new
  prompt-surface-review ruleset requires two of their approvals.
- Docs: prompt-assembly.md (how it works), contributing.md (what to do when
  the test fails).
2026-10-11 01:21:18 -07:00
Maciej Lisiewski
c098e0b80c ci: checkout once for docker and shell linting 2026-10-10 23:58:48 +02:00
Maciej Lisiewski
d68bb0c9ae ci: make ruff strict the default 2026-10-10 15:20:14 +02:00
Maciej Lisiewski
bfd01ac80d ci: use new, stricter ruff config 2026-10-10 15:20:14 +02:00
ethernet
8b497e2216 ci(docker): pin Python 3.14 in the publish job too
publish runs the same install-stamp and boundary scripts on the same
runner matrix, including arm64 whose system python3 is 3.10 and lacks
datetime.UTC. Without this, PR checks pass and main-push publish breaks.
2026-10-09 13:33:20 -04:00
ethernet
369be700e7 ci(docker): pin Python 3.14 in the build and test image job
The install stamp and profile boundary scripts ran on whatever python3
the runner image shipped. Set it up explicitly with actions/setup-python.
2026-10-09 13:29:27 -04:00
ethernet
25a096f317 fix(release): treat any missing stable baseline as no upgrade arm
A release that skipped bundles never writes the stable baseline pointer,
so its absence is normal at any point, not only before the first bundle
release. Drop the final-tag scan. A 404 on the default pointer now always
plans no upgrade arm. A supplied baseline-manifest that does not exist
still blocks, because that is a bad input.
2026-10-09 01:41:35 -04:00
ethernet
4e23d79de4 fix(release): let the first bundle release pass without an upgrade baseline
No stable release has shipped signed packages yet. Every final tag so far
skipped bundles or predates the claim pipeline. So
releases/stable/release-candidates.json does not exist, and the three
"Pin actual OLD and NEW signed packages" jobs refused with "No published
stable package baseline".

transitions now treats a 404 as a first release only when no final stable
tag records a candidate manifest and no baseline-manifest input was given.
It still validates the receipt, emits empty matrices and baseline=none.
The packaged upgrade arms skip on that output. The gate accepts their skip
only when their own planner succeeded with baseline=none, so a missing
baseline after a real bundle release still blocks.
2026-10-09 01:37:59 -04:00
Teknium
1744a19e0d ci(desktop): re-smoke an already-built package without rebuilding it
The native smoke only ran inside desktop-bundled-release, and it checked out
its test scripts at the same commit as the artifact it fetched. Changing a
test therefore meant a new commit, and a new commit meant a full signed
build (about an hour per platform) just to rerun the smoke.

Add workflow_dispatch: `sha` picks the staged artifact to fetch, and the
scripts come from the ref the dispatch runs on. Release callers are
unchanged.
2026-10-08 20:20:55 -07:00
Teknium
ffcd3621a9 ci: E2E suites run only on release builds
Pull requests and pushes to main no longer run any E2E suite: ci.yaml's detect gate
forces every e2e* lane off unless the run is a stable-release run (inputs.release) or a
manual dispatch, whatever the diff, the update-path classifier or the run-e2e label
selects. That covers the Python e2e and e2e-upgrade jobs, the Windows E2E and Windows
install + update journey in tests-os, and the Desktop core/update suites.

install-e2e.yml drops its pull_request (path-filtered `pr` route) and 12-hourly
schedule triggers; it runs from the stable-release gate (workflow_call) or by dispatch.
live-providers drops its nightly schedule and keeps the release-tag push + dispatch.

Why: a one-row change to left_core_migration.py held a PR for 40 minutes on the
Windows install + update E2E; the suites cost most of the larger-runner minutes and
the release gate already runs all of them strictly.
2026-10-08 19:48:31 -07:00
ethernet
c20541d09a Revert "feat(release): promote Docker stable/latest from a published receipt tag"
This reverts commit cbe5e53e28.
2026-10-08 10:42:02 -04:00
Teknium
cbe5e53e28 feat(release): promote Docker stable/latest from a published receipt tag
The ordered publication pass moves nousresearch/hermes-agent:stable/latest
only after it reads the R2 stable channel head. When that read fails
(v0.21.6: "Channel repository authority mismatch"), the release publishes
and the aliases never move.

`python -m scripts.releases.docker promote --version X.Y.Z` reads the
published vX.Y.Z receipt tag and runs the existing promote_stable with the
digest it binds (slim checked against the receipt, desktop from its own
attempt tag). docker-promote-stable.yml dispatches it with the Docker Hub
credentials only CI holds.
2026-10-08 05:06:16 -07:00
Teknium
818c13be1d fix(release): give the Nix release build its commit and push Docker images under the attempt ref
Two stable-release failures on rc.3-v0.21.6 (run 37753841932):

- nix: the stamped release source is a `path:` flake with no git metadata,
  so self.rev is null, install-stamp.json carries no commit and the packaged
  `hermes --version` reports "unknown". Pass the commit as `?rev=$GITHUB_SHA`
  (verified on Nix 2.35: path:<tree>?rev=<sha> sets self.rev).
- docker: the versioned images were pushed under the final vX.Y.Z, which does
  not exist until publish. rc.1 created `:v0.21.6`, and every later attempt hit
  "versioned Docker manifest differs" against it. scripts/releases/docker.py
  (published_digest, promote_stable) already reads the attempt-ref tag, so pass
  claim-tag to both docker.yml calls.
2026-10-08 03:40:51 -07:00
Teknium
0240fa4a84 fix(release): read the Hermes version line in the Nix check and accept rc.N attempt refs
Two stable-release identity checks broke on the first pipeline cuts of v0.21.6:

- nix.yml "Verify release package runtime identity" took the LAST token of the
  whole `hermes --version` output. That output ends with the OpenAI SDK line, so
  the check compared 0.21.6 against 2.24.0 and failed every release. Parse the
  first line ("Hermes Agent v<version> ...") instead.
- msix-shared.mjs releaseEpoch() still required the retired `vX.Y.Z-rc` claim
  name. Attempt refs are `rc.<N>-vX.Y.Z` (versioning.parse_attempt_ref), so the
  Windows universal bundle threw "Stable claim tag differs from its payload tag".
  Accept exactly `rc.<N>-<tag>`; a different version (rc.3-v0.27.10 for v0.27.1)
  still refuses.
2026-10-08 01:58:48 -07:00
ethernet
8cb42b9ee3 Update .github/workflows/nix.yml
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-10-07 22:47:10 -07:00
ethernet
5631d6680f ci(release): also build the desktop package from the stamped source
desktop wraps the default package, so building it in the release
identity step proves the stamped tree builds the Electron app too.
2026-10-07 22:47:10 -07:00
ethernet
08dfac427e fix(release): build the default flake package for the Nix identity check
The release workflow built release-source#hermes-agent, but the flake
only exposes packages.<system>.default (plus minimal/messaging/etc.),
so the step failed with 'does not provide attribute'.
2026-10-07 22:47:10 -07:00
alt-glitch
7ab657a06b ci(wine2e): run the long-paths-off PM proof even when an earlier lane fails
The PM long-path steps do not use anything the earlier live lanes set up.
Gating them on those lanes hid the proof whenever an unrelated lane went
red. The test step still requires the long-paths-off step to succeed, so
it never runs with the runner image's setting.
2026-10-07 06:40:39 +05:30
alt-glitch
af60718b03 test(pm): live Windows proof for PM with LongPathsEnabled=0 (#130242)
Drives a real Store through scratch, the bundled-copy copytree, tree_digest,
publish and removal over a tree with files and a directory beyond MAX_PATH,
then checks the facts record and composed child env carry no \\?\ prefix.
The test first asserts RtlAreLongPathsEnabled() is false so it cannot pass
as a false proof. The wine2e lane sets LongPathsEnabled=0 in a separate,
final step and selects the test with -m 'platforms and integration'.
2026-10-07 06:40:39 +05:30
Teknium
9f8d7bf604 ci: route the ZIP recovery module and the SSH gate's corpus test like their siblings
`hermes_cli/_early_recovery_zip.py` (split out of `_early_recovery.py` on the
commit-point PR) is imported by the update path, so a change to it must start
the update pipeline and install E2E suites. Both lists and the install-e2e path
filter now match the `_early_recovery` prefix, which also routes any later
sibling. `remote-lifecycle-v2-marker.test.ts` reads the update-marker corpus
and is now a listed consumer; `remote-update-marker-programs.test.ts` exists in
this tree, so its sibling-PR allowance is gone.
2026-10-06 13:02:14 -07:00
Teknium
448add284a ci: the Windows update E2E fails when the crash journey ran no cells
run_tests.sh runs one pytest per file and reports a file whose tests were
all deselected (pytest exit 5) or all skipped as passed. 8ce4ce35fc2 made
the -m selection and platforms('windows') marker a tests/ci check, but any
other zero-execution path (the HERMES_E2E_WINDOWS_INSTALL opt-in lost from
the step env, a new skip condition) still ran zero crash cells green.

The crash journey now writes crash-cells-ran.txt (each journey step and
ok/failed) into HERMES_E2E_ARTIFACTS once it has run, and a new required
step "Every crash cell ran" after the run step throws when the manifest
is missing. The manifest is uploaded with the other artifacts.

Test (tests/ci, every PR): the step exists after the run step, is neither
disabled nor advisory (workflow_steps.required), reads the same
HERMES_E2E_ARTIFACTS and names the module's manifest constant; and
_record_cells_ran writes exactly the steps that ran, nothing without the
env. Red on the start workflow (no step) and with the step made
continue-on-error.

Review 5411223855 (F52: collected > 0 / executed-cells assertion).
2026-10-06 13:02:14 -07:00
Teknium
e9f7772602 ci(e2e): size the Windows update E2E file budget for the crash-cell journey
test_crash_cells.py runs one module-scoped journey (install, the shared
gateway phase, six crash cells) inside a single file. It took 1,750-2,090 s
on normal runners (87% of the 2,400 s per-file budget) and was killed at
2,400 s on a runner that was uniformly 1.3-1.8x slower (run 37243653456:
32 passed, 0 failed, crash cells SIGKILL'd mid-journey). Raise the per-file
budget to 3,600 s and the job to 75 min so runner variance is not a verdict.
2026-10-06 13:02:14 -07:00
Teknium
259fe73b0b fix(ci): route update dependencies and reject selected consumer skips
Route the exercised subprocess and provider-migration dependencies to both
real-update journeys. Keep backup routing under an explicit dispatch test.
Use the aggregate's existing detect outputs to reject skipped or missing
update consumers while retaining legitimate unselected PR skips.

Verified with the real classifier and aggregate subprocesses against
b9dec76a042, composite ff5c8f1a64b, and fixed code. Canonical runner:
571 passed, 0 failed, 1 native-Windows skip across CI and marker corpora.

(cherry picked from commit a082ca3c62ed49382f173a07882d4280d0ba7c9c)
(cherry picked from commit ed9f2028256b8b1d6af81ef97cd472f30d2cb042)
(cherry picked from commit 6d5cc740b6a75d476802222c2f4754855a5712b0)
2026-10-06 13:02:14 -07:00
Teknium
53847b7d41 test(e2e): strict acceptance mode makes a batch's known gaps hard failures
known_failure XFAILs a gap while its fix is an open PR, which is right
on a PR and wrong on the acceptance run of the integrated batch: there
it accepts exactly the gap the batch claims to close (the final
composite's Windows run passed with `test_crash_cells.py (3✓ 1xf)`).

HERMES_E2E_STRICT_ACCEPTANCE=<owner>[,<owner>] (or 1 = every gap) makes
the matching known_failure / known_gate blocks re-raise the gap's own
assertion. It is set only from the `strict_acceptance` dispatch input of
ci.yaml (threaded through tests.yml / tests-os.yml to every E2E step)
and of windows-install-update-e2e.yml; pull requests never set it.
run_tests.sh forwards it through its env -i allowlist.

Red on 8cc78279a36: test_pending_fixes_strict.py (a real pytest process
per mode) — the batch-owned gap XFAILs under strict mode.

(cherry picked from commit ca1e7e749a61263181e89aa687dec366d95fda18)
(cherry picked from commit 7768bf3ef8e15fb7f228ebad619d6a9a85ad4100)
2026-10-06 13:02:14 -07:00
Teknium
5a36d5ac34 ci: route update owners by the import graph and dispatch every set lane
The classifier missed modules the update entry points import
(_subprocess_compat, desktop_build_lock, memory_provider_migration, the
scripts/build compilers) and never sent source_build/source_completion to
the Desktop update suite. ci.yaml gated tests-os on `python` alone, so a
TypeScript-only change to the Electron marker/result readers set
desktop_updater and still never reached the Windows hand-off tests.

- classify_changes.py: _UPDATE_ENTRY_POINTS / _DESKTOP_BUILD_ENTRY_POINTS,
  an owner list of what they import, and an explicit _SHARED_HUBS set.
- tests/ci/test_update_ci_routing.py reads the entry points' real imports
  (AST incl. lazy imports; ESM import/step() closure of the build scripts)
  and fails until each is routed or is a real hub (>= 25 importers).
- It also replays the REAL classifier's output through the composite
  action, ci.yaml detect outputs, job if:/with:, the called workflows' job
  if: and the rendered Windows step, for every combination of the gated
  lanes (tests/ci/_gha_expr.py models the expression subset and raises on
  anything else).
- ci.yaml: tests runs for python|e2e|e2e_upgrade; tests-os for
  python|desktop_updater|e2e|e2e_upgrade.

Red on 8cc78279a36: 30 of the new routing tests fail (R15 table cells,
16 lane combinations, ownership/closure tests).

(cherry picked from commit 3b17b5d2ad8081534bbeefcf5d8334cd79a7e6fd)
(cherry picked from commit 594f8b0c4ba32324da922d5289b0932299ffe7a9)
2026-10-06 13:02:14 -07:00
Teknium
008479c7ea ci: make the Desktop update E2E required and drop a dead wine2e nodeid
e2e-desktop-update ran with continue-on-error, so a PR touching the
Desktop update path could merge on a red "Update now" hand-off. It has
passed every main-push shard on record (48/48 over the last 12 main CI
runs), so it now gates the merge whenever the e2e_desktop_update lane
fires, like the Linux e2e-upgrade shards and the Windows install +
update journey already do for e2e_upgrade (skipped = success otherwise).

windows-venv-e2e.yml listed
tests/hermes_cli/test_runtime_repair.py::TestWindowsRuntimeSelfLock, a
file that never existed on main; run_tests.sh dropped it silently. The
in-use-runtime replacement it meant to prove now lives in pm and is
tests/pm/test_held_entry_windows_live.py.

Tests pin both: every real-update suite reaches all-checks-pass, is
gated on its lane and is never continue-on-error; every path the
wine2e lane runs is an existing test file.

(cherry picked from commit f4791428341afaf67cc53775653dde197591fa4f)
(cherry picked from commit 5d3a4b505fe5c3858a6bb7f3554c97eb272231ca)
2026-10-06 13:02:14 -07:00
Teknium
3f524a2459 ci: version the required check so CI greens from before a new required job stop counting
The protect-main ruleset requires a check by NAME, and a green check run on a
PR head never expires. After the code-health ratchet joined the required set,
32 PRs whose last CI run predated it merged on their old "All required checks
pass" green without the ratchet ever running; 20 of them would have been
blocked (62 findings, incl. 15 new blind `except Exception`).

Renaming the gate to "All required checks pass (v2)" and pointing the ruleset
at the new name voids every older green: each open PR needs one fresh CI run,
which now includes the ratchet, before it can merge, whoever merges it.
The ci.yaml comment documents the bump as the standard step whenever the
required set gains a job every PR must pass.

live_comment.py keeps both names in its infra list (old runs still render),
and a test pins that the gate's current display name never shows as a job.
2026-10-06 03:57:34 -07:00
Teknium
93c9360a8d ci: markdown under pm/ no longer builds six PM bundles
pm-bundle, install-e2e and termux-verify trigger on pm/**, so adding pm/AGENTS.md
(and every later edit to it) queued darwin-x64 / win32-arm64 bundle builds for a doc
change. Nothing reads .md files under pm/, so exclude them from the path filters.
2026-10-04 23:05:42 -07:00
Teknium
e1ec584b57 feat(ci): cap every AGENTS.md chain below the context budget it is truncated at
scripts/ci/check_agents_md_size.py (in scripts/check and lint.yml): root <= 12k, every
AGENTS.md plus its ancestors <= 30k (the joined root->cwd block on a 128k window, minus
the per-file labels), every area file <= _MAX_HINT_CHARS read from the source so the two
cannot drift. On origin/main it reports the root and all 11 root-to-area chains over.
2026-10-04 23:05:42 -07:00
teknium1
cf0755f947 fix(code-health): occurrence provenance, alias-aware rules, strict file cap (round 4)
Fourth review round (F11-F18, N1-N4, kvnloo, Adolanium):

- Hits are old only where the base occurrence survives: a line the comment- and
  whitespace-blind line diff keeps (blame semantics), or anything inside a unit whose
  body is unchanged. An identical violation moved past unrelated code is new; removing
  an allow comment or re-indenting is not. Hit fingerprints exclude comments (N4).
- Inline allows cover their own line; only a comment-only line covers the next (N3).
- Rules read a copy of the AST with import aliases resolved, so `sp.run`,
  `from subprocess import run as execute`, aliased threading/asyncio/load_config are
  seen (F12); deadline helpers must resolve to the real asyncio API, and
  communicate()/wait() are judged only on receivers holding a spawned process (F13);
  infinite deadlines are unbounded (F14).
- HERMES_* writes introduce a name too (N1); bool(os.environ[...]) is HX010 (N2);
  HX009 needs the isinstance subject to come from gather(return_exceptions=True).
- Recursive self-references are normalised out of body hashes, so a recursive rename
  keeps its cap, py and ts (F15). Decorators belong to their function's scope and
  classes are matchable units, so their hits follow proven moves (F16).
- An over-cap file may not grow (F17, Teknium: strict improvement-only).
- A file the checker cannot parse or measure (ast/ruff/TS parse error) fails.
- scripts/check: --staged always lints an index snapshot (ignored files are invisible
  to git status, not to guards) (F18); create the --prefix dir for Git for Windows
  (F11); --only builds just the selected jobs, so it needs no ruff for non-ruff checks.
- CODEOWNERS covers scripts/check + scripts/code_health; frontend-only PRs skip the
  Python-only lint jobs again and run only the ratchet.
2026-10-04 21:29:39 -07:00
teknium1
ca682666b8 ci: one command runs every blocking lint check, locally and in CI
`python scripts/check` runs ruff, the Windows-footgun/shebang/no-tmp/
config-writer/OS-marker guards and the code-health ratchet in parallel
with CI's pinned ruff (read from pyproject's dev group; a different local
ruff is never used silently, `uvx ruff@<pin>` is the fallback).
`--install-hook` makes it the pre-commit hook; the hook skips branches
that predate scripts/check, since hooks are shared by every worktree.

lint.yml gains the blocking `code health ratchet` job: it compares the PR
merge commit with its first parent (the base branch tip), so a PR is held
to main as it is now. The lint lane also runs for frontend-only changes
because the ratchet covers TypeScript, and the existing ruff installs read
the same pin.
2026-10-04 21:29:39 -07:00
teknium1
1a69f42275 ci(plugin-catalog): full blobless checkout so Hermes resolves its version
pinned-source-validate checked out depth 2. Hermes resolves its own
version from release tags merged into HEAD, so the shallow checkout
reported 'unknown' and plugins that gate on the host version refused to
load (omh, #122578: requires Hermes >=0.21.1). A blobless full clone
carries the commit graph and tags for about the cost of the shallow one.
2026-10-04 12:33:42 -07:00
ethernet
1af98f58ba Merge pull request #131726 from NousResearch/ethie/codeowners-ci-review
ci: replace the ci-reviewed label gate with CODEOWNERS
2026-10-02 18:49:46 -04:00
ethernet
059efe8b29 ci: replace the ci-reviewed label gate with CODEOWNERS
Path-based review (workflows, composite actions, eslint/prettier config,
the MCP catalog, install-hook files) moves to .github/CODEOWNERS. GitHub
requests and enforces that review, so CI no longer classifies those paths,
checks a label, or reruns failed jobs when the label is added.

Removes review-labels.yml, emit_review_status.py, the rerun-review-labels
job, and the ci_review / ci_review_files / mcp_catalog classifier outputs.
The run-e2e rerun job in label-rerun.yml stays.

The supply-chain scan keeps its content patterns (base64 decode + exec,
obfuscated subprocess), which CODEOWNERS cannot express. It fails on its
own again, as it did before #68833 routed it through the label gate. Its
.pth and install-hook path checks are dropped in favour of CODEOWNERS.
2026-10-02 15:22:49 -04:00
ethernet
8c34d2ecc5 fix(release): let the win32-bundle candidate call assemble the universal bundle
stable-release.yml runs `jobs: win32-bundle` as its own call after the
per-arch candidate calls. In that call validate selects only win32-bundle, so
assemble-win32-bundle's gate (win32-x64/win32-arm64 selected AND their build
jobs succeeded) was always false and the job skipped, which failed the stable
bundle phase with `assemble-win32-bundle=skipped`.

A build selected in the same call must still succeed; an unselected one is
fetched from the R2 handoff, as the assemble steps already do.
2026-10-02 15:10:43 -04:00
ethernet
0217a2a324 feat(icons): custom drive artwork as the volume icon of both installer DMGs
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.

Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
2026-10-02 10:33:44 -04:00
ethernet
383916b845 feat(icons): layered macOS 26 icon for the bootstrap installer and the DMGs
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.

The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
2026-10-02 10:33:44 -04:00
ethernet
26960b1afb build(desktop): compile the layered icon to Assets.car on macOS 26 hosts
electron-builder (app-builder-lib 27.0.0-alpha.6) compiles a `.icon` given as
`mac.icon` into Contents/Resources/Assets.car and sets CFBundleIconName, but
only with actool >= 26 (Xcode 26); on any other host it throws and the build
fails. scripts/mac-icon.cjs picks the Icon Composer package when that actool
is present and the legacy .icns otherwise, so a dev Mac on Xcode 16 keeps
building the app it always built.

The release runners keep Xcode 16.4 selected although 26.x is installed, so
the darwin job now selects the newest Xcode 26 via DEVELOPER_DIR and asserts
the actool version before packaging; a silent fallback cannot ship from CI.

electron-builder's `.icon` path also replaces the bundled icon.icns with
actool's 256px fallback. macOS <= 15 shows that file, so after-pack restores
our 16..1024 assets/icon.icns.
2026-10-02 10:33:43 -04:00
teknium1
093f8d4480 ci: run the Python suite and the macOS lane as two slices each
The full suite was the last job to finish in 33 of 43 CI runs since it
moved from a 96-core to one 32-core runner (p50 666 s). That runner is
~90% packed, so only more machines help, and one bigger box is poor
value: on 96 cores the same files took 25-50% longer each and the suite
ran ~1.8x faster for 3x the price. Two 32-core slices scale linearly
(simulated from per-file CI timings: 262 s / 252 s vs 514 s on one box).

macOS (free 3-core runner, ~7.5 min) would become the slowest required
job once Linux splits, so it runs as two slices on the same free runner.

--slice now partitions from the checkout alone (sorted round-robin):
each slice job restores its own duration cache, and a main push saving
a newer cache between the two restores made timed LPT compute different
partitions per job, silently dropping or double-running files.
Round-robin lands within 5 s of timed LPT at this suite size. Each slice
saves its own cache key on main; the runner keeps the restored entries
for files outside its slice, so the cache stays complete.
2026-10-01 23:49:20 -07:00
teknium1
013b776ab0 ci(label-rerun): never rerun a CI run whose PR head has moved
Adding ci-reviewed starts a job that waits for the PR's CI run, then reruns
its failed jobs. A push during the wait cancels that run, the waiter sees
"completed", reruns it, and the rerun rejoins the per-PR concurrency group
and cancels the new head's run. The PR is left testing a stale commit (seen
on #131236 and #131246). Skip the rerun when the head moved: the new run's
own event already carries the label. Same guard on the run-e2e job.
2026-10-01 23:49:01 -07:00
teknium1
b909c01d07 fix(ci): scope the catalog data-only guard to entry PRs; diff the merge commit shallowly
- Run the data-only guard once, inside the changed-entries step of the
  pinned-source job, and only when an entry YAML changed. README, policy,
  docs and CI PRs that touch plugin-catalog/ without an entry no longer go
  red.
- Allow contributors/emails/** beside plugin-catalog/**: the attribution
  fix we push onto catalog PR branches is data the gate never executes
  (29 of the last 134 merged catalog PRs carried one).
- Diff GitHub's merge commit against its first parent with fetch-depth: 2
  instead of a full-history clone plus merge-base. The structural job,
  which only validates the whole directory, goes back to a shallow
  checkout with no diff at all.
- `subdir:` with no value means the repo root in both the parse step and
  the structural validator, matching how the runtime loader has always
  read it.
2026-10-01 10:46:45 -07:00
beardthelion
7e52bd0fa9 fix(ci): close remaining pin-gate escapes found in review
- Scan the whole clone for escaping symlinks, not just PLUGIN_DIR:
  find does not descend into a symlinked directory, so an allowed
  in-clone link (plugin/ext -> ../lib) hid a nested escape.
- Require catalog admission PRs to be data-only in both jobs; the
  gate's enforcement code resolves from the PR checkout, so a diff
  that also touches tooling could neuter the gate vetting it.
- persist-credentials: false on both checkouts: `hermes plugins
  validate` runs pinned-repo code and must not see the job token.
- Broaden the self-updater patterns: releases/download and codeload
  fetches, and the Node fs write sinks (writeFileSync, renameSync,
  copyFileSync, cpSync, createWriteStream).
2026-10-01 10:46:45 -07:00
teknium1
eb9cb6c8e1 fix(ci): make the -I shadow guard load-bearing; widen clone timeout to 600s
Follow-up to the admission-gate confinement:

- tests/ci: test_parser_imports_cannot_be_shadowed_by_checkout_files planted
  `re.py`, but `re` is imported during interpreter startup, so a cwd copy never
  shadows it and the test passed even with `-I` stripped from the workflow.
  Plant `shlex.py` instead (the parser is its first importer): with a shadow
  whose quote() swallows the rejection, the gate returns rc 0 / PASS without
  `-I` and rc 1 with it, so the test now actually guards the fix.
- workflow: `timeout 120 git clone` -> 600. The clone is deliberately full
  (the pin may not be the tip) and listed repos exceed 700 MB
  (hermes-jackal-verified 1.43 GB, hindsight 741 MB); 120s would false-fail a
  legitimate re-pin on a slow runner while 600s still bounds a hostile URL
  well under the 30-minute job cap.

Attribution: beardthelion@users.noreply.github.com is already in the legacy
AUTHOR_MAP, so no mapping file is needed.
2026-10-01 10:46:45 -07:00
beardthelion
8444e9eb18 fix(ci): confine plugin-catalog admission gate to the pinned clone
The pinned-source job joined a PR-controlled subdir onto the clone root
with no validation, accepted file:// repos and ref names as the "pin",
and followed committed symlinks out of the clone, so the gate could
validate attacker-planted files instead of the declared commit's
contents. Harden the step end to end:

- validate repo (https://, whitespace-free), sha (40-hex, commit object),
  and subdir (plain relative path) in the parser before any use
- run the parser under python3 -I against installed ruamel.yaml so the
  PR checkout cannot shadow its imports, capture parser output, and
  reset shell variables per entry so a dead parser cannot replay the
  previous entry's fields
- confine PLUGIN_DIR and every symlink beneath it to the clone root
- include renamed and typechanged entry files in the changed set
- feed entries over fd 3 and keep plugin-executed commands off stdin so
  validate cannot drain the remaining list
- NUL-delimit both self-updater grep stages and sanitize the echoed
  filename list so crafted names cannot forge workflow commands
- mirror the same repo/subdir rules in the runtime catalog loader and
  the site extractor, and use \Z anchors so a trailing newline cannot
  split the verdict between gates
2026-10-01 10:46:45 -07:00
Maciej Lisiewski
7239625ae1 ci: reduce HERMES_TEST_WORKERS to 16 for Windows x64/arm64, default for macos 2026-09-30 16:34:52 -04:00
Maciej Lisiewski
eb46d8bf35 ci: bump HERMES_TEST_WORKERS to match earlier runner size change 2026-09-30 16:34:52 -04:00
Maciej Lisiewski
1188c9c397 ci: Update Github actions versions, use same versions everywhere 2026-09-30 16:34:52 -04:00
Siddharth Balyan
09681e7959 fix(release): --branding stable works before stable has a channel record (#129541)
`--branding stable` copies the identity from the published stable channel
record on R2, and refused when there was none. Production has no
releases/channels/stable.json yet: the channel pipeline merged after the
last stable release (v2026.9.24), and no stable release has published
through it since. So every stable-branded channel build failed at
allocation ("none is published"), including the first real dispatch.

When the stable record does not exist, use product_identity(), the
identity channel_publish already holds every stable release to. A stable
record without a head still refuses, so once the first stable release
publishes, its record is the only authority again and this path stops
running by itself.

The allocation step needs node for product_identity(), so the workflow's
identity-reader setup also runs for branding == 'stable'.
2026-10-01 01:30:24 +05:30