v1.5.8 was tagged and produced no image. The job died at step 2 of 5:
::error::Error response from daemon:
Get "https://ghcr.io/v2/": denied: denied
The credential is fine. Measured on the forge over 7 days, luxfi ran 365
ghcr logins with the same org secret and 27 failed — 7.4% — across 23 repos
and a dozen runners, and the same secret succeeded on either side of every
burst, including two hours after one. Three readings were tested and all
three are wrong:
expired token the same secret succeeded at 16:27 today, hours after
failures at 20:53 and 21:18 yesterday
sick runner failures spread over 12 runners; the worst has 3 of 7,
a clean one has 12 of 12
throttling no correlation with concurrency — 7.4% at one login per
minute, 10.9% at two, 0% at four and five
So it is an intermittent rejection from ghcr, and the only defect on our
side is that nothing retries it. docker/login-action does not, and the job
dies before the build, so each occurrence costs a release silently: a tag
with no image and no failed push to show for it. 27 of those in a week.
Five attempts over 75s, backing off. Exhausting all five is a different
failure than the one this covers, and says so rather than pretending.
--password-stdin, so the token is never an argument and never lands in a
process list or a log — docker/login-action's own behaviour, kept.
Same shape belongs in the other 22 luxfi repos that publish this way;
this one is the repo with a release waiting on it.
Co-authored-by: Hanzo Dev <[email protected]>
Explorer
Single-binary Lux explorer: chain indexer, GraphQL engine, and SPA frontend
in one Go process. Imports luxfi/indexer
and luxfi/graph as libraries; embeds the
luxfi/explore Next.js build via
go:embed.
ghcr.io/luxfi/explorer
-> indexes one or more EVM chains (luxfi/indexer)
-> exposes per-chain GraphQL (luxfi/graph)
-> serves embedded SPA + REST API (luxfi/explore)
Routes
| Path | Purpose |
|---|---|
/ |
SPA (embedded; SPA-routing fallback) |
/envs.js |
Runtime config window.ENV = {...} |
/icon.svg, /logo.svg |
Per-host brand assets (disk override) |
/health |
Service health |
/v1/indexer/* |
Default chain explorer API |
/v1/indexer/{slug}/* |
Per-chain explorer API |
/v1/explorer/{slug}/* |
Legacy alias for /v1/indexer/{slug} |
/v1/graph/{slug}/{subgraph}/graphql |
Per-chain, per-subgraph GraphQL |
/v1/explorer/admin/chains[/{slug}] |
Runtime registry CRUD |
/v1/explorer/realtime |
WebSocket realtime hub |
/v1/explorer/realtime/stats |
Realtime stats |
Quick Start
docker run -p 8090:8090 \
-v $(pwd)/chains.example.yaml:/etc/explorer/chains.yaml:ro \
-v explorer-data:/data \
ghcr.io/luxfi/explorer:latest \
--config /etc/explorer/chains.yaml
Or with compose:
docker compose up
Configuration
chains.yaml is the single source of truth. Every aspect of the deploy --
chains, branding, GraphQL subgraphs, network switcher links -- is per-chain
customizable. See chains.example.yaml for the full
schema.
data_dir: /data
http_addr: :8090
brand_default:
name: Lux Explorer
coin: LUX
accent_color: "#5cf"
networks:
- { label: Mainnet, domain: explorer.lux.network, chain_id: 96369 }
chains:
- slug: cchain
name: Lux C-Chain
chain_id: 96369
type: evm
rpc: https://api.lux.network/v1/bc/C/rpc
coin: LUX
enabled: true
default: true
indexer:
poll_interval: 30s
graph:
enabled: true
subgraphs:
- { name: amm, schema: amm, enabled: true }
brand:
name: Lux C-Chain
accent_color: "#5cf"
$ENV_VAR substitution in rpc: / ws: lets one file template every
network -- mount it read-only and inject secrets via env vars.
Per-Network Customization
The same image powers every deploy. To rebrand for a different network:
- Mount a different
chains.yaml. - Optionally mount
icon.svg/logo.svgat the path referenced bychains.yaml-- they are read from disk per-request, not embedded. - Set environment variables to fill the
${VAR}placeholders.
No image rebuild needed. The SPA reads chain list and brand at runtime from
/envs.js.
Runtime Registry
Chains can be added, updated, and removed at runtime without restart:
# Add
curl -X POST http://localhost:8090/v1/explorer/admin/chains \
-H 'Content-Type: application/json' \
-d '{"slug":"new","name":"New","chain_id":123,"rpc":"http://node:8545","type":"evm"}'
# List
curl http://localhost:8090/v1/explorer/admin/chains
# Remove
curl -X DELETE http://localhost:8090/v1/explorer/admin/chains/new
Adding a chain spawns its indexer + (optional) graph workers; removing cancels them and frees the routes.
mDNS Discovery
Set EXPLORER_MDNS=true to auto-register chains advertised by local Lux
nodes (_luxd._tcp, _zood._tcp, _hanzod._tcp, _parsd._tcp). The
explorer queries info.getChains on the discovered node and registers each
tracked chain. mDNS-sourced chains never override config-sourced entries.
Storage Layout
/data/
cchain/
query/indexer.db (SQLite WAL, indexer)
{zapdb}/ (KV store)
graph/
amm/graph.db (SQLite WAL, per-subgraph)
zoo/
...
Each chain (and each subgraph) owns an isolated SQLite + KV store. WAL is
optionally streamed to S3 with PQ encryption when REPLICATE_S3_ENDPOINT
is set -- see luxfi/indexer/daemon.
Local Build
# Sibling layout: ~/work/lux/{indexer,graph,explorer}
cd ~/work/lux/explorer
go build -o explorer .
go.mod uses local replace directives pointing at ../indexer and
../graph. Production CI clones all three repos into the same layout
inside the Docker build.
Endpoints (curl examples)
curl http://localhost:8090/health
curl http://localhost:8090/v1/indexer/cchain/blocks/latest
curl -X POST http://localhost:8090/v1/graph/cchain/amm/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ swaps(first:5) { id amount0 amount1 } }"}'
Related
luxfi/indexer-- chain indexer libraryluxfi/graph-- per-chain GraphQL libraryluxfi/explore-- Next.js SPA