The dep-version-gate ruleset requires a team review for package
manifests, eslint configs, and workflow files. If the autofix patch
contains one of these files, the bot PR waits for that review and
auto-merge stops. The patch step now excludes them, so a bot PR
never gates itself. The eslint check in typecheck.yml still reports
their lint errors.
Hermes now pins its toolchain to Node 26 everywhere. Every path that
installs, accepts, heals, or upgrades a Node runtime moves from the old
22-default / `^20.19 || >=22.12` floor to a single rule: Node >=26.
Installers:
- scripts/install.sh — NODE_VERSION=26; node_satisfies_build() collapses
the two-branch Vite floor to `major >= 26`; user-facing messages updated.
- scripts/install.ps1 — $NodeVersion=26; Test-NodeVersionOk likewise;
winget fallback switches OpenJS.NodeJS.LTS -> OpenJS.NodeJS (26 is
Current, not LTS — the LTS manifest would reinstall a too-old Node).
- Dockerfile — node_source stage node:22-bookworm-slim -> node:26 (digest
pinned, amd64 sha256:9e6f...bf73).
- nix/ was already on nodejs_26 (lib.nix, npm-12-0-2.nix); the checks.nix
wrapper check ratchets from `>= 20` to `>= 26`.
Heal/upgrade paths:
- scripts/lib/node-bootstrap.sh — HERMES_NODE_TARGET_MAJOR default 22->26
and HERMES_NODE_MIN_VERSION default 20->26, so heal_managed_node,
_nb_install_bundled_node, and the fnm/proto/nvm/brew rungs all target 26
and stop accepting an on-PATH Node below it. Both remain env-overridable.
- hermes_constants.py — _HERMES_NODE_TARGET_MAJOR fallback 22->26, which
drives the Windows heal path's latest-v26.x download.
Version gates:
- package.json engines.node >=20 -> >=26; apps/desktop engines
`^20.19.0 || >=22.12.0` -> `>=26.0.0`.
- CI setup-node: all five workflows 22 -> 26.
- Docs describing Hermes's own toolchain updated (windows-native, docker,
acp, nix-setup, contributing). Skill docs describing third-party tools'
own requirements are untouched.
Termux still installs via `pkg install nodejs` best-effort (nodejs.org
ships no Android tarballs); that path was never version-gated.
Verified: bash -n on both shell scripts, PowerShell AST parse of
install.ps1, latest-v26.x index resolves (node-v26.5.1), and the install
test suite — 18 tests across the 5 install/runtime test files — passes.
* fix(ci): publish inline E2E evidence
Upload bounded screenshot evidence from E2E, then publish validated images
from a trusted workflow_run job to commit-pinned branches in the evidence repo.
Wait briefly for the live CI review comment marker before publishing, so
GitHub's read-after-write delay cannot leave an orphaned evidence branch.
* fix(ci): isolate privileged credentials from PR jobs
Keep App private keys and Docker Hub credentials out of PR-controlled
workflows. Use protected environments for trusted publishing and a public
repository variable for the App client ID.
* fix(ci): attach E2E evidence with restricted bot session
Replace the App-backed evidence repository publisher with gh-image uploads
from a dedicated bot session in the gh-image environment.
* fix(ci): publish validated E2E evidence from forks
Let the trusted default-branch publisher handle bounded, validated evidence
artifacts from fork PR CI without checking out or executing fork code.
Composite actions cannot access the secrets context — the runner's
template engine rejects secrets.* references at load time with
'Unrecognized named-value: secrets'.
Move APP_ID and APP_PRIVATE_KEY from direct secrets.* references inside
the composite action to inputs passed by each calling workflow. The
fallback logic (GITHUB_TOKEN when APP_ID is empty, for fork PRs) stays
in the composite action's check step.
Replace the long-lived fine-grained PAT (AUTOFIX_BOT_PAT) with short-lived
(1-hour) installation access tokens minted via a new get-app-token composite
action wrapping actions/create-github-app-token@v3.2.0.
The PAT was used in 13 spots across 8 workflow files for gh CLI / GitHub API
calls. The per-repo GITHUB_TOKEN (1,000 req/hr) was getting rate-limited when
multiple workflows fire concurrently (deploy-site, skills-index, ci-timings,
supply-chain-audit, js-autofix). App installation tokens get 5,000 req/hr
per installation and are scoped to the App's permissions, not a user account.
New composite action: .github/actions/get-app-token/
- Wraps actions/create-github-app-token@bcd2ba49 (v3.2.0, SHA-pinned)
- Reads APP_ID + APP_PRIVATE_KEY repo secrets
- Outputs a 1hr installation token via steps.app-token.outputs.token
Requires two new repo secrets (set after creating the GitHub App):
- APP_ID: the App's numeric ID
- APP_PRIVATE_KEY: the PEM private key
App installation permissions needed:
contents: write (js-autofix push, pypi release upload)
pull-requests: write (js-autofix PR create/merge, supply-chain comment)
issues: write (skills-index-freshness issue creation)
actions: write (skills-index workflow trigger)
workflows: write (skills-index triggers deploy-site.yml)
The AUTOFIX_BOT_PAT secret can be deleted once CI passes on this PR.
The comment in js-autofix.yml noting that PAT pushes trigger downstream
workflows is updated — App tokens have the same property (they are not
GITHUB_TOKEN), so the concurrency-cancel loop logic is unchanged.
generate-patch now emits a has-fixes job output (true/false).
apply-patch is gated on it via `if: needs.generate-patch.outputs.has-fixes == 'true'`,
so when `npm run fix` produces no diff, the privileged job is skipped
entirely — no runner allocation, no redundant checkout/download/push/PR.
When the bot PR auto-merges, main moves — which the poll loop detects
as 'main moved' and tries to close the PR. But the PR is already
merged, so gh pr close fails with an error.
Fix: when main moves, re-check PR state before closing. If it's
already MERGED, exit cleanly. Also make gh pr close non-fatal (|| true)
as a belt-and-suspenders guard against the same race on the other
close paths.
The js-autofix workflow used 'gh pr create --json number --jq .number'
to capture the PR number, but 'gh pr create' doesn't support --json.
Extract the PR number from the URL that 'gh pr create' prints instead.
* fix(js): never format package-lock.json
prettier and eslint should never touch package-lock.json. main has a
repo rule requiring team approval when lockfiles change, so an autofix
PR touching it would hang waiting for review.
- Add .prettierignore at repo root
- Add '**/package-lock.json' to eslint shared config ignores
* fix(ci): js-autofix pushes via PR instead of direct push to main
Main now has repository rules requiring pull requests + required status
checks ("All required checks pass"), so the workflow's direct push to
main is rejected with GH013 every time eslint --fix produces changes.
Switch apply-patch to push to a dedicated bot/js-autofix branch, create
or update a PR, and enable auto-merge (squash). The PR auto-merges once
CI passes. If CI fails or main moves, the PR is auto-closed and the
branch deleted — the next run re-applies on the current state.
The two-job security split is preserved:
- generate-patch stays unprivileged (contents: read only) — it runs npm
on an ephemeral runner with zero push permissions.
- apply-patch (contents: write + pull-requests: write) still never runs
npm, never installs anything, never executes repo code — it applies
the trusted patch artifact and delivers it via PR.