Skip to content

fix: add Prysm v7 config keys to the mainnet chain config - #26

Merged
maciejwitowski merged 2 commits into
mainfrom
fix/prysm-v7-config-keys
Sep 14, 2026
Merged

maciejwitowski merged 2 commits into
mainfrom
fix/prysm-v7-config-keys

Conversation

@inf-1oop

@inf-1oop inf-1oop commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

This PR carries two changes to the mainnet chain config:

  1. Prysm v7 config keys — four keys the config never overrode, each of which breaks a v7 node.
  2. The deposit contract switch — mainnet moves to a new deposit contract at block 10,229,958.

They are together because both touch networks/mainnet/config.yml and both land in the same operator upgrade.

⚠️ Part 2 changes the beacon node image. Part 1 on its own is backward compatible with Prysm v5.1.0; part 2 is not. See Compatibility below.


Part 1 — Prysm v7 config keys

Problem

networks/mainnet/config.yml declares PRESET_BASE: mainnet, so every key it does not define is inherited from Ethereum mainnet. Prysm v7 added three settings this config never overrides, and each inherited default breaks a Vana node in a different way — two of them silently.

Key Inherited value Failure
FULU_FORK_VERSION / GLOAS_FORK_VERSION 0x06000000 / 0x07000000 Node refuses to start: version 0x06000000 for fork fulu in config vana conflicts with existing config named=mainnet
SLOT_DURATION_MS 12000 Preferred over SECONDS_PER_SLOT for all slot timing → 12s/slot arithmetic on a 6s chain → FATAL invalid payload timestamp
BLOB_SCHEDULE Ethereum BPO forks at epochs 412672 / 419072 Gossip allowlist truncated → every gossip topic refused

On BLOB_SCHEDULE specifically

A BPO entry only receives a distinct fork digest when its epoch is at or after FULU_FORK_EPOCH. Fulu is disabled here, so Ethereum's BPO entries instead reuse the preceding fork's digest — and since 412672/419072 fall between Bellatrix (epoch 2) and Capella/Deneb (503609), they carry Bellatrix's digest.

Prysm's allTopics() walks the fork schedule terminating on next.ForkDigest != last.ForkDigest, so the repeated digest halts the walk before Capella/Deneb. The allowlist ends up with 221 topics and no 78ba1733 (the live Deneb digest), so pubsub refuses everything with topic is not allowed by the subscription filter.

This one is the nastiest to diagnose: the node still starts and follows the chain by range sync, so it looks alive. It just participates in nothing — no gossip, and a sync distance that drifts instead of sitting at 0.

Verification

Tested on a live Vana mainnet node running Prysm v7.1.8:

  • Before: FATAL at startup, then (after the fork keys) FATAL invalid payload timestamp, then (after SLOT_DURATION_MS) a running-but-gossip-dead node with sync distance drifting 2→17 and block arrival at 7–13s.
  • After all three: sync distance 0 sustained, head advancing 2 slots / 12s (correct 6s cadence), 0 subscription-filter rejections, 0 payload-timestamp errors, 17 peers.

Consensus-safety checks:

  • genesis_validators_root unchanged: 0x7a2ee1f3a4332b8a3229efd4cba0903049bc8b1e7cd1fe46052e320cf3f6d184
  • Fork unchanged: current_version 0x20000093, head block version deneb
  • Fulu/Gloas do not enter the fork schedule (far-future epochs are truncated), so the resulting schedule is still exactly Phase0 → Deneb and the Deneb digest is still 0x78ba1733
  • MaxBlobsPerBlockAtEpoch() at the current epoch is 6 both before and after — the inherited BPO entries were superseded by Deneb, so emptying the schedule changes no blob limit

Part 2 — Deposit contract switch

Mainnet moves to a new deposit contract at block 10,229,958.

Current contract (from the switch block onward) 0xB98aafa6684aef18AEf518772F01F5aE0DA5eA4C
Retired contract (holds the history before it) 0x17BbE91c315Bf14f38F6D35052a827cadfFe184e
Switch block 10229958

Why this does not break the deposit trie

The new contract was deployed at block 10,099,568 already seeded with the retired contract's final state, so the merkle index continues across the boundary instead of restarting at 0. Read from both contracts on chain:

Retired (at block 9,475,268) New (today)
get_deposit_count() 32 32
get_deposit_root() 0x60dff8f6…172401 0x60dff8f6…172401

Below the switch block a node scans only the retired contract; at and above it, only the current one. A log batch that straddles the boundary is split into two queries, one per contract. A node syncing from genesis therefore reconstructs exactly the deposit history it always did, and no node resyncs.

This depends on a patch to Prysm (vana-com/prysm#1), released as v7.1.8-vega.1, which adds the two config keys and the split scan.

Compatibility

This is the part that is not backward compatible. The beacon node moves to Vana's build of Prysm, ghcr.io/vana-com/prysm-beacon-chain:v7.1.8-vega.1, pinned by a new BEACON_IMAGE variable. Upstream Prysm cannot run against this config: it does not know the two new keys, and it refuses to start at all once DEPOSIT_CONTRACT_ADDRESS no longer matches the address recorded in its own database.

Only the beacon node is forked. The validator and prysmctl stay on upstream Prysm, moving from v5.1.0 to v7.1.8 — the release the Vana build is based on — so the two components no longer sit four minor versions apart. The switch is beacon-node work; the upstream validator reads this config file fine. Both v5.1.0 and v7.1.8 were run against it and report the two new keys as unknown fields in the same non-fatal message they already emit for FULU_FORK_VERSION and BLOB_SCHEDULE, with every recognised field still applied — UnmarshalStrict populates what it recognises before collecting the strict errors, and the loader logs and continues.

For part 1 alone, the earlier claim still holds: Prysm v5.1.0 logs the new keys as unknown fields and continues normally.

Operator impact

Upgrading is an image change:

docker compose pull beacon validator
docker compose up -d beacon validator
  • The beacon service passes --clear-deposit-contract. A node's database records the deposit contract address it last ran with and refuses to start when the configured one differs (database contract is 0x17bbe91c... but tried to run with 0xb98aafa6...). The flag clears that record and the migration marker, then writes the address again from the config on the same start. All other data is kept — this is not a resync. It is on by default so operators don't have to edit docker-compose.yml; the cost is that the marker is cleared on every start, so the check that refuses a changed switch block does not fire. The comment in the file says to drop it once the fleet is past the switch.
  • A node that has already scanned past the switch block rewinds its deposit log scan once, logging Rewinding deposit log scan.
  • DEPOSIT_CONTRACT_ADDRESS in .env moves to the new contract — it is where submit-deposits sends deposits, and the retired contract no longer accepts them.
  • PRYSM_VERSION crosses two major versions for the validator, so operators should back up the validator database (slashing protection) first.

Verification

Rehearsed end to end on a 4-stack shadowfork devnet, all 8 phases, zero failures:

  • The seeded contract is a byte-identical continuation — count, root, and all 32 branch slots matched the retired contract.
  • A rolling upgrade v5.1.0 → patched, one stack at a time, while the other three kept producing. No keys moved, no node retired; 31 of 34 slots produced across the cutover, finality never stalled, and the deposit-log scan came back clean on all 4 stacks.
  • The boundary-split query fired as designed on a batch straddling the switch block.
  • The chain then walked Deneb → Electra → Fulu with incorrect merkle index: 0 and Could not process deposit log: 0, confirming the switch does not disturb the later EIP-6110 transition.

Against the published image with the config in this PR: it starts and reaches genesis loading, and each validation guard was confirmed to fire by deliberately breaking it (switch block 0, deployment block above the switch, retired equal to current, retired as the zero address). Every image tag the compose file now references was confirmed to exist.

Open question: the pre-switch window

The switch block is roughly 9 days out, but the new contract is already live and accepting deposits — its only gate is a pubkey allowlist (DepositContract: publicKey not allowed), not a block height.

That leaves a ~130,000-block window in which a deposit from an allowlisted key would land below the switch block, where no node scans the new contract. The result would be an index gap and nodes halting deposit processing. The devnet rehearsal avoided this by setting the switch block to the new contract's deployment block, which leaves no window.

Two ways to close it, for reviewers to decide:

  1. Set DEPOSIT_CONTRACT_SWITCH_BLOCK: 10099568 (the deployment block) — strictly safe, since the contract could not emit before it existed.
  2. Keep 10229958 and hold the allowlist empty until then, as an operational guarantee.

Separately worth noting: the retired contract now reverts on get_deposit_count() and get_deposit_root() — it answered 32 at block 9,475,268 and reverts by block 10,000,000. Since the config still points at it today, that is the likely source of any Unable to process past deposit contract logs errors currently seen on mainnet. The switch resolves it, and the Vana build additionally makes an unreadable deposit count non-fatal.


Not included: networks/moksha/config.yml

Moksha is missing all four v7 keys too and will hit the same class of problem. I have deliberately left it out because I could not test it, and it is not a copy-paste of this change:

  • Its fork-version numbering differs (ELECTRA_FORK_VERSION: 0x60000038, not the 0x2000009x series), so Fulu/Gloas versions need a deliberate choice.
  • All its forks are at epoch 0, so the inherited BPO entries land after Deneb rather than before it — the topic-walk truncation may not bite the same way.

Moksha also has no deposit contract switch of its own.

Worth a follow-up from someone who can run a Moksha node.

🤖 Generated with Claude Code

inf-1oop and others added 2 commits August 20, 2026 13:27
networks/mainnet/config.yml sets PRESET_BASE: mainnet, so every key it does not
define is inherited from Ethereum mainnet. Prysm v7 added three settings that
Vana's config never overrides, and each inherited default breaks a Vana node in
a different way:

- FULU/GLOAS fork versions inherit 0x06000000/0x07000000, which collide with
  Ethereum's own registered config, so the node refuses to start.
- SLOT_DURATION_MS inherits 12000 and takes precedence over SECONDS_PER_SLOT,
  so the node does 12s/slot arithmetic on a 6s chain and dies with
  "invalid payload timestamp".
- BLOB_SCHEDULE inherits Ethereum's BPO forks at epochs 412672/419072, which
  reuse the preceding fork's digest because Fulu is disabled. That halts
  Prysm's allTopics() fork-schedule walk, leaving Capella/Deneb out of the
  gossip allowlist so every gossip topic is refused.

Verified on a live mainnet node running Prysm v7.1.8: sync distance 0, fork
0x20000093 (Deneb), no gossip rejections. genesis_validators_root and the Deneb
fork digest are unchanged. Also verified backward compatible with Prysm v5.1.0,
which logs the new keys as unknown fields and continues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mainnet moves to a new deposit contract, 0xB98aafa6684aef18AEf518772F01F5aE0DA5eA4C,
at block 10,229,958. The new contract was deployed at block 10,099,568 already seeded
with the retired contract's final state -- deposit count 32 and root
0x60dff8f6a92d68799e7653acdcefa253a1c2603b4dd071d8265491b465172401 -- so the deposit
index and merkle tree continue across the boundary and no node rebuilds its deposit
history. Both values were read from the two contracts on chain.

networks/mainnet/config.yml gains RETIRED_DEPOSIT_CONTRACT_ADDRESS and
DEPOSIT_CONTRACT_SWITCH_BLOCK, and DEPOSIT_CONTRACT_ADDRESS becomes the new contract.
Below the switch block a node scans only the retired contract, at and above it only the
current one, so a node syncing from genesis still reconstructs the same trie.

The beacon node moves to Vana's build of Prysm, ghcr.io/vana-com/prysm-beacon-chain,
because upstream Prysm does not know the two new keys and refuses to start once the
configured deposit contract differs from the one recorded in its database. This is the
one change here that is not backward compatible, and BEACON_IMAGE pins it separately
from PRYSM_VERSION.

Only the beacon node is forked. The validator and prysmctl stay upstream and move to
v7.1.8, the release the Vana build is based on, so the two no longer sit four minor
versions apart. The switch is beacon-node work and the upstream validator loads this
config unchanged: both v5.1.0 and v7.1.8 were run against it and report the two new keys
as unknown fields in the same non-fatal message they already emit for FULU_FORK_VERSION
and BLOB_SCHEDULE, with every recognised field still applied. Every image tag the compose
file now references was confirmed to exist.

The beacon service passes --clear-deposit-contract so that an operator upgrading across
the switch does not have to edit docker-compose.yml: it clears the recorded contract
address and the recorded migration marker, then writes the address again from the config
on the same start, keeping all other data. The cost is that the marker is cleared on
every start, so the check that refuses a switch block different from the one already
migrated for does not fire; the comment says to drop the flag once the fleet is past the
switch.

.env DEPOSIT_CONTRACT_ADDRESS moves to the new contract too -- it is where
submit-deposits sends deposits, and the retired contract no longer accepts them.

Verified against ghcr.io/vana-com/prysm-beacon-chain:v7.1.8-vega.1: the shipped config
starts and reaches genesis loading, and each validation guard was confirmed to fire by
breaking it (switch block 0, deployment block above the switch, retired equal to current,
retired as the zero address).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maciejwitowski
maciejwitowski merged commit a409872 into main Sep 14, 2026
4 checks passed
@maciejwitowski
maciejwitowski deleted the fix/prysm-v7-config-keys branch September 14, 2026 10:35
inf-1oop added a commit that referenced this pull request Sep 14, 2026
## Problem

`docker-compose.yml` passes
`--beacon-rest-api-provider=${EXTERNAL_IP}:${RPC_PORT}`, which renders
as `0.0.0.0:4000`. That value is wrong three ways:

| | |
|---|---|
| **No scheme** | A REST endpoint is a URL. The gRPC provider on the
line above is a dial target and must *not* have one — they look alike
but are parsed by different code. |
| **Wrong port** | `4000` is the gRPC port. The HTTP API is on `3500`
(`--grpc-gateway-port`, now an alias for `--http-port`). |
| **Silently inert on v5** | Which is why nobody noticed. |

## Why it is breaking now, and not before

On Prysm **v5.1.0** the REST API is enabled only by the explicit
boolean, so the provider flag was dead config and the validator used
gRPC:

```go
// v5.1.0  config/features/config.go:296
if ctx.Bool(EnableBeaconRESTApi.Name) {
```

**v7.1.8** also enables it *implicitly* whenever the provider flag is
set:

```go
// v7.1.8  config/features/config.go:372
if ctx.Bool(EnableBeaconRESTApi.Name) || ctx.IsSet(validatorflags.BeaconRESTApiProviderFlag.Name) {
```

So the value stops being ignored and starts deciding how the validator
reaches the beacon.

**#26 moved `PRYSM_VERSION` from `v5.1.0` to `v7.1.8` and is already
merged**, so this is live on `main` today. Any operator who pulls `main`
and restarts their validator loses attestations.

## Reproduction

Against `gcr.io/prysmaticlabs/prysm/validator:v7.1.8` with the value
`main` currently produces, repeating every 12 seconds:

```
WARN  flags: Enabled feature flag enable-beacon-rest-api=...
INFO  rest: Initialized REST connection provider count=1 endpoints=[[invalid endpoint]]
ERROR beacon-api: failed to get health of node error=failed to create request for
      endpoint [invalid endpoint]: parse "0.0.0.0:4000/eth/v1/node/health":
      first path segment in URL cannot contain colon  url=0.0.0.0:4000
WARN  fallback: No responsive beacon node found tried=[[invalid endpoint]]
```

The same run on `v5.1.0` initialises no REST provider at all. Note the
gRPC provider *is* still constructed (`endpoints=[beacon:4000]`) — the
validator does not discard it, but the health check runs over REST,
fails to build a request, and the fallback layer concludes no beacon is
responsive. Net effect: zero duties.

## Fix

```diff
- --beacon-rest-api-provider=${EXTERNAL_IP}:${RPC_PORT:-4000}
+ --beacon-rest-api-provider=http://beacon:${GRPC_GATEWAY_PORT:-3500}
```

Verified on `v7.1.8` with isolated datadirs per container, so no
bolt-lock contention between runs:

| Config | REST endpoint | REST feature | parse errors in 40s |
|---|---|---|---|
| `0.0.0.0:4000` (current `main`) | `[invalid endpoint]` | on | **4** |
| `http://beacon:3500` (this PR) | `http://beacon:3500` | on | **0** |
| flag dropped entirely | `http://127.0.0.1:3500` (default, unused) |
off | 0 |

## Note on the approach

Dropping the flag was the other option: it restores v5.1.0's behaviour
exactly, since the feature flag then stays off and gRPC carries the
duties. Repointing it instead puts the validator on the standardised
Beacon API, which is where Prysm is heading and which works against any
consensus client — but it is a **transport change, not only a syntax
fix**, and deserves a validator-in-the-loop test before the fleet moves.

What is verified here is that the endpoint parses and initialises
correctly. What is **not** verified is that duties actually flow over
REST end to end — that needs a live beacon and real keys.

## Scope

`docker-compose.yml` only. The Caddyfile is untouched; public endpoint
exposure was handled separately in #27.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
maciejwitowski added a commit that referenced this pull request Sep 14, 2026
`npx semantic-release@25.0.9` pinned only the top package. semantic-release
loads its plugins from its own node_modules tree, and every dependency
below it (release-notes-generator ^14.1.0, conventional-changelog-writer
^8.0.0, the GitHub plugin's Octokit stack) resolved fresh on each run with
no lockfile. Any new npm release in that tree reached a job that holds a
contents:write token, unreviewed.

Move the toolchain into .github/release/package.json with a committed
package-lock.json: semantic-release 25.0.9, commit-analyzer 13.0.1,
release-notes-generator 14.1.1, github 12.0.9, conventionalcommits preset
9.3.1. The job installs it with `npm ci --ignore-scripts` (no package in the
tree has an install script) and runs that binary from the repo root. It
lives under .github/ so the operator checkout gets no root package.json.
Node is pinned to 24, the version this was verified on.

Verified on Node 24 against main at 47db176, running the two workflow steps
as written with the full .releaserc and a GitHub token, `--dry-run --no-ci`,
no root node_modules: next version 2.3.2, notes render Bug Fixes with #26,
#27 and #28.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dqz4KWjLzxNEJuea75Avb7
inf-1oop added a commit that referenced this pull request Sep 16, 2026
## `main` currently ships a validator that cannot start

#26 set `PRYSM_VERSION=v7.1.8`, but the validator command still passes
`--db-backup-output-dir`, which Prysm **removed** in v7:

```
Incorrect Usage: flag provided but not defined: -db-backup-output-dir
```

The container then crash-loops. This is not latent — it affects anyone
who pulls `main` and starts a validator today. I hit it for real
upgrading `contango-validator-1`, where it cost ~2.5 minutes of missed
duties.

Reproduced against `gcr.io/prysmaticlabs/prysm/validator:v7.1.8` with
the exact flag set `main` renders:

| flag set | result on v7.1.8 |
|---|---|
| `main` today | `Incorrect Usage: flag provided but not defined:
-db-backup-output-dir` |
| this PR | `INFO main: Prysm Validator started
version=Prysm/v7.1.8/51b5a75...` |

## Three fixes, all in `docker-compose.yml`

### 1. Drop the two dead backup flags

`--db-backup-output-dir` is gone in v7. `--enable-db-backup-webhook`
goes with it — v7 keeps it only in `config/features/deprecated_flags.go`
as a hidden no-op, so it logs `is deprecated and has no effect` on every
start and does nothing.

### 2. `validator-backup` actually backs something up

The service POSTs to the validator's `/db/backup` webhook — which is
precisely what the deprecated flag used to enable. **The call returns
success while backing up nothing.**

That matters more than a normal dead feature: the README tells operators
to run it before upgrading, and **slashing protection history lives in
that database**. A backup step that silently no-ops is worse than no
backup step, because it buys false confidence before the one operation
where you need the file.

It now runs `slashing-protection-history export`, which writes the
standard **EIP-3076** file that any client can import.

One constraint, spelled out in a comment in the file: it must run with
the validator **stopped**, because `validator.db` is a bolt database
with a single-writer lock.

```bash
docker compose stop validator
docker compose --profile backup run --rm validator-backup
docker compose --profile node --profile validator --profile init up -d validator
```

### 3. Restart policy for `geth`, `beacon`, `validator`

Only `stats` and `caddy` used the `x-restart` anchor. Without it, an OOM
kill or crash stops attesting until a human notices — an operator fleet
has already hit this, with a geth OOM-killed during an archive sync
sitting dead for two days while the node still reported "up".

## Verification

- `main`'s rendered flag set fails immediately on v7.1.8 with the error
above
- this PR's flag set reaches `Prysm Validator started`
- `validator slashing-protection-history export` exists in v7.1.8 and
accepts `--slashing-protection-export-dir`
- `docker compose config` clean across the `node`, `validator`, `init`
and `backup` profiles
- the running upgrade on `contango-validator-1` (canonical validator
v7.1.8 + `v7.1.8-vega.1` beacon) has been performing all duties —
attestations, aggregates, sync committee messages and contributions, and
block proposals — for 4+ hours

## Deliberately not in this PR

These came out of the same upgrade but need testing rather than review,
so they belong to the validator rollout session:

- **`--enable-doppelganger`** is absent from the repo. It is a real
anti-double-signing control and operators are hand-rolling it in
overrides. Shipping it costs ~2 epochs of missed attestations per start,
which deserves a deliberate decision.
- **The override trap.** Compose *replaces* `command:` rather than
merging, so an operator override restating the validator command
silently bypasses every future fix here — #28's REST fix was in the repo
and inert on the node for exactly this reason. Fixes 3 (and
doppelganger) reduce the *reason* to keep an override at all; the README
should also state the rule.
- **v5→v7 renamed log strings** (`Submitted new sync message` →
`Submitted sync committee messages`). Any alerting that greps validator
logs goes quiet and looks healthy. This fooled my own checks twice.
- **No rollback path.** A v7→v5 validator downgrade is unsupported and
untested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
maciejwitowski added a commit that referenced this pull request Sep 24, 2026
## Problem

The Release workflow fails at `generateNotes` on every push that should
cut a release. No GitHub release has been published since v2.3.1
(2026-08-03), so #27, #26 and #28 are on `main` with no release. And
v2.3.1 itself shipped with an empty body.

| Run | Commit | Result |
|---|---|---|
| 2026-08-03 | ffd802d #21 | success, but v2.3.1 notes are the version
header only |
| 2026-08-12 → 08-19 | chore/ci commits | success (no release due, notes
step never ran) |
| 2026-09-04 | a4cb188 #27 | failure |
| 2026-09-14 | a409872 #26 | failure |
| 2026-09-14 | 47db176 #28 | failure |

```
Failed step "generateNotes" of plugin "@semantic-release/release-notes-generator"
Error: Missing helper: "conventional-changelog-conventionalcommits requires conventional-changelog-writer@9 or newer ..."
```

## Cause

The workflow installed the release tooling unpinned, with no lockfile:

- `npx semantic-release` resolves to 25.0.9. Its
`@semantic-release/release-notes-generator` 14.1.1 (the latest) depends
on `conventional-changelog-writer ^8`.
- `npm install --no-save conventional-changelog-conventionalcommits`
resolves to the latest preset. Preset **v10** targets writer 9:
- 10.0.0–10.3.0 (from 2026-06-26): no error, but writer 8 renders only
the version header. That is why the [v2.3.1
release](https://github.com/vana-com/vana/releases/tag/v2.3.1) has no
notes, while v2.3.0 (January, preset 9.x) does.
- 10.4.0 (2026-08-18): added an explicit guard, which turned the silent
empty output into the hard failure above.

Pinning only the top-level packages does not close this.
semantic-release loads its plugins from its own `node_modules` tree, and
every range below it (`release-notes-generator ^14.1.0`,
`conventional-changelog-writer ^8.0.0`, the GitHub plugin's Octokit
stack) resolves fresh on each run. The job holds a `contents: write`
token, so any new npm release in that tree runs with it, unreviewed.

## Fix

- New `.github/release/package.json` + committed `package-lock.json` for
the toolchain:

  | Package | Version |
  |---|---|
  | `semantic-release` | 25.0.9 |
  | `@semantic-release/commit-analyzer` | 13.0.1 |
  | `@semantic-release/release-notes-generator` | 14.1.1 |
  | `@semantic-release/github` | 12.0.9 |
| `conventional-changelog-conventionalcommits` | 9.3.1, the last major
built for writer 8 |

- The job installs it with `npm ci --ignore-scripts --prefix
.github/release` (no package in the tree has an install script) and runs
`.github/release/node_modules/.bin/semantic-release` from the repo root,
so `.releaserc.yaml` is still the config.
- It lives under `.github/` so the operator checkout gets no root
`package.json`. `.gitignore` covers its `node_modules`.
- Node pinned to `24`, the version this was verified on.
- A comment in the workflow says when to bump the preset: once the notes
generator ships writer 9.

## Verification

Ran the two workflow steps as written, on Node 24 / npm 11, on a
checkout of `main` at 47db176 with no root `node_modules`, the full
`.releaserc.yaml` and a GitHub token, `--dry-run --no-ci`:

- `npm ci` installs one deduped copy of each package; every plugin and
the preset resolve from `.github/release/node_modules`.
- GitHub `verifyConditions` passes, next version **2.3.2**, notes render
`### Bug Fixes` with #26, #27, #28, same format as v2.3.0.

Preset comparison (same command pair, earlier run):

| Preset | Result |
|---|---|
| 9.3.1 | notes render Bug Fixes |
| 10.3.0 | notes are the header only |
| 10.4.0 | fails with the same "Missing helper" error as CI |

Cosmetic: #26's commit body contains the word "closes" before
"compatibility", so its line gets a bogus `closes #compatibility` link.
Harmless; editing the release body after publish fixes it if wanted.

## Heads-up on merging

Merging this triggers the workflow on `main`, which will **publish
v2.3.2** as a public GitHub release containing #26, #27 and #28 (Prysm
v7 config keys, the deposit contract switch config, the validator REST
fix). The GitHub plugin also comments "included in version 2.3.2" on
those PRs and labels them `released`, as it did on #21 for v2.3.1.
Anyone watching this repo's releases gets notified, so merge when that
announcement is fine to go out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01Dqz4KWjLzxNEJuea75Avb7

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 2.4.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants