Skip to content

Commit f201a38

Browse files
authored
Merge branch 'main' into feat/blocks-by-range-long-range-sync
2 parents 41bcebf + 9a56f54 commit f201a38

33 files changed

Lines changed: 1674 additions & 228 deletions
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Counts Rust lines of code in the ethlambda workspace via cargo-warloc and
4+
produces report files for Slack, Telegram, and the GitHub Actions step summary.
5+
6+
`cargo warloc` reports per-file `main`/`tests` line counts using a Rust AST
7+
parser, so inline `#[cfg(test)]` blocks are correctly classified as test code.
8+
9+
Inputs (optional):
10+
loc_report.json.old Previous run's report. Used to compute deltas.
11+
12+
Outputs:
13+
loc_report.json Machine-readable report for caching.
14+
loc_report_slack.json Slack Block Kit payload (daily).
15+
loc_report_telegram.txt Telegram HTML body (weekly).
16+
loc_report_github.txt Plain-text block for the workflow step summary.
17+
"""
18+
19+
from __future__ import annotations
20+
21+
import html
22+
import json
23+
import os
24+
import subprocess
25+
from datetime import datetime, timezone
26+
from pathlib import Path
27+
28+
29+
# Crates whose entire contents are test infrastructure and should never
30+
# appear in the "no tests" totals or per-crate listing.
31+
TEST_ONLY_CRATES = frozenset({
32+
"crates/common/test-fixtures",
33+
})
34+
35+
# Individual files that are test infrastructure but live next to production
36+
# code inside an otherwise-production crate. Their lines are folded into the
37+
# owning crate's `tests` bucket.
38+
TEST_ONLY_FILES = frozenset({
39+
"crates/net/rpc/src/test_driver.rs",
40+
})
41+
42+
43+
def _run(cmd: list[str]) -> str:
44+
return subprocess.check_output(cmd, text=True)
45+
46+
47+
def warloc_by_file() -> dict:
48+
return json.loads(_run(["cargo", "warloc", "--by-file", "-o", "json"]))
49+
50+
51+
def workspace_crates() -> list[str]:
52+
md = json.loads(_run(["cargo", "metadata", "--no-deps", "--format-version", "1"]))
53+
cwd = os.getcwd() + "/"
54+
crates = []
55+
for pkg in md["packages"]:
56+
path = pkg["manifest_path"][: -len("/Cargo.toml")]
57+
if path.startswith(cwd):
58+
path = path[len(cwd):]
59+
crates.append(path)
60+
# Sort longest first so longest-prefix match wins when grouping files.
61+
crates.sort(key=len, reverse=True)
62+
return crates
63+
64+
65+
def group_by_crate(by_file: dict, crates: list[str]) -> dict[str, dict[str, int]]:
66+
buckets = {c: {"main": 0, "tests": 0} for c in crates}
67+
for raw_path, stats in by_file["files"].items():
68+
path = raw_path[2:] if raw_path.startswith("./") else raw_path
69+
owner = next((c for c in crates if path.startswith(c + "/")), None)
70+
if owner is None:
71+
continue
72+
is_test_only = owner in TEST_ONLY_CRATES or path in TEST_ONLY_FILES
73+
if is_test_only:
74+
# All lines from this file/crate count as tests.
75+
buckets[owner]["tests"] += stats["main"]["code"] + stats["tests"]["code"]
76+
else:
77+
buckets[owner]["main"] += stats["main"]["code"]
78+
buckets[owner]["tests"] += stats["tests"]["code"]
79+
return buckets
80+
81+
82+
def format_diff(cur: int, old: int) -> str:
83+
if cur > old:
84+
return f"(+{cur - old})"
85+
if cur < old:
86+
return f"(-{old - cur})"
87+
return ""
88+
89+
90+
def main() -> None:
91+
by_file = warloc_by_file()
92+
crates = workspace_crates()
93+
buckets = group_by_crate(by_file, crates)
94+
95+
rows = [
96+
{"path": c, "main": b["main"], "tests": b["tests"]}
97+
for c, b in buckets.items()
98+
]
99+
rows.sort(key=lambda r: -r["main"])
100+
101+
total_main = sum(r["main"] for r in rows)
102+
total_tests = sum(r["tests"] for r in rows)
103+
total_with_tests = total_main + total_tests
104+
105+
new_report = {
106+
"total_main": total_main,
107+
"total_tests": total_tests,
108+
"total_with_tests": total_with_tests,
109+
"crates": rows,
110+
}
111+
Path("loc_report.json").write_text(json.dumps(new_report))
112+
113+
# Resolve previous values (default = current → blank deltas on first run).
114+
old_path = Path("loc_report.json.old")
115+
if old_path.exists():
116+
old = json.loads(old_path.read_text())
117+
old_main = old.get("total_main", total_main)
118+
old_with = old.get("total_with_tests", total_with_tests)
119+
old_crates = {c["path"]: c["main"] for c in old.get("crates", [])}
120+
else:
121+
old_main = total_main
122+
old_with = total_with_tests
123+
old_crates = {r["path"]: r["main"] for r in rows}
124+
125+
main_diff = format_diff(total_main, old_main)
126+
with_diff = format_diff(total_with_tests, old_with)
127+
128+
sha = os.environ.get("GITHUB_SHA") or _run(["git", "rev-parse", "HEAD"]).strip()
129+
short = sha[:7]
130+
date_utc = datetime.now(timezone.utc).strftime("%Y-%m-%d")
131+
132+
per_crate = []
133+
for r in rows:
134+
# Test-only crates fold their lines into the tests bucket and have
135+
# main == 0; skip them in the per-crate "no tests" listing.
136+
if r["main"] == 0:
137+
continue
138+
old_loc = old_crates.get(r["path"], r["main"])
139+
per_crate.append({
140+
"path": r["path"],
141+
"loc": r["main"],
142+
"diff": format_diff(r["main"], old_loc),
143+
})
144+
145+
# --- GitHub step summary -------------------------------------------------
146+
gh_lines = [
147+
"```",
148+
f"ethlambda lines of code ({date_utc}, {short})",
149+
"============================================",
150+
"",
151+
"Per-crate (no tests)",
152+
"--------------------",
153+
]
154+
gh_lines += [f"{r['path']}: {r['loc']} {r['diff']}".rstrip() for r in per_crate]
155+
gh_lines += [
156+
"",
157+
f"Total Rust LoC (no tests): {total_main} {main_diff}".rstrip(),
158+
f"Total Rust LoC (with tests): {total_with_tests} {with_diff}".rstrip(),
159+
"```",
160+
]
161+
Path("loc_report_github.txt").write_text("\n".join(gh_lines) + "\n")
162+
163+
# --- Slack Block Kit ------------------------------------------------------
164+
per_crate_slack = "\n".join(
165+
f"*{r['path']}*: {r['loc']} {r['diff']}".rstrip() for r in per_crate
166+
)
167+
totals_slack = (
168+
f"*Total (no tests):* {total_main} {main_diff}".rstrip()
169+
+ "\n"
170+
+ f"*Total (with tests):* {total_with_tests} {with_diff}".rstrip()
171+
)
172+
slack_payload = {
173+
"blocks": [
174+
{"type": "header",
175+
"text": {"type": "plain_text", "text": "Daily ethlambda LoC Report"}},
176+
{"type": "section",
177+
"text": {"type": "mrkdwn",
178+
"text": f"_Date:_ {date_utc} • _Commit:_ `{short}`"}},
179+
{"type": "divider"},
180+
{"type": "header",
181+
"text": {"type": "plain_text", "text": "Per-crate (no tests)"}},
182+
{"type": "section",
183+
"text": {"type": "mrkdwn", "text": per_crate_slack}},
184+
{"type": "divider"},
185+
{"type": "section",
186+
"text": {"type": "mrkdwn", "text": totals_slack}},
187+
]
188+
}
189+
Path("loc_report_slack.json").write_text(json.dumps(slack_payload))
190+
191+
# --- Telegram (HTML parse mode) ------------------------------------------
192+
def esc(s: str) -> str:
193+
return html.escape(s, quote=False)
194+
195+
tg_lines = [
196+
"<b>Weekly ethlambda LoC Report</b>",
197+
f"Date: {date_utc} • Commit: <code>{esc(short)}</code>",
198+
"",
199+
"<b>Per-crate (no tests)</b>",
200+
]
201+
tg_lines += [
202+
f"<b>{esc(r['path'])}</b>: {r['loc']} {r['diff']}".rstrip()
203+
for r in per_crate
204+
]
205+
tg_lines += [
206+
"",
207+
f"<b>Total Rust LoC (no tests):</b> {total_main} {main_diff}".rstrip(),
208+
f"<b>Total Rust LoC (with tests):</b> {total_with_tests} {with_diff}".rstrip(),
209+
]
210+
Path("loc_report_telegram.txt").write_text("\n".join(tg_lines) + "\n")
211+
212+
213+
if __name__ == "__main__":
214+
main()

.github/scripts/publish_slack.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env bash
2+
#
3+
# POSTs a Slack Block Kit payload to an incoming webhook.
4+
#
5+
# Required env:
6+
# SLACK_WEBHOOK Incoming-webhook URL. Read from the env (not argv) so it
7+
# doesn't leak into the process list.
8+
#
9+
# Usage: publish_slack.sh <payload_file>
10+
11+
set -euo pipefail
12+
13+
PAYLOAD_FILE="${1:?payload file required}"
14+
15+
if [[ -z "${SLACK_WEBHOOK:-}" ]]; then
16+
echo "::error::SLACK_WEBHOOK resolved to an empty value — check the secret configured for this trigger (scheduled vs manual)"
17+
exit 1
18+
fi
19+
20+
curl --fail-with-body -X POST "$SLACK_WEBHOOK" \
21+
-H 'Content-Type: application/json; charset=utf-8' \
22+
--data @"$PAYLOAD_FILE"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
#
3+
# POSTs the contents of a file as an HTML-formatted Telegram message.
4+
#
5+
# Required env:
6+
# TELEGRAM_BOT_TOKEN Bot token used to authenticate the request.
7+
# TELEGRAM_ETHLAMBDA_CHAT_ID Destination chat ID.
8+
#
9+
# Usage: publish_telegram.sh <message_file>
10+
11+
set -euo pipefail
12+
13+
MESSAGE_FILE="${1:?message file required}"
14+
15+
if [[ -z "${TELEGRAM_BOT_TOKEN:-}" ]]; then
16+
echo "::error::TELEGRAM_BOT_TOKEN secret is not set — skipping Telegram post"
17+
exit 1
18+
fi
19+
20+
if [[ -z "${TELEGRAM_ETHLAMBDA_CHAT_ID:-}" ]]; then
21+
echo "::error::TELEGRAM_ETHLAMBDA_CHAT_ID resolved to an empty value — check that the appropriate secret is configured for this trigger (scheduled vs manual)"
22+
exit 1
23+
fi
24+
25+
curl --fail-with-body -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
26+
-d chat_id="$TELEGRAM_ETHLAMBDA_CHAT_ID" \
27+
-d parse_mode=HTML \
28+
--data-urlencode text="$(cat "$MESSAGE_FILE")"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
name: Daily Lines of Code Report
2+
3+
on:
4+
schedule:
5+
# Every day at UTC midnight (Slack daily, Telegram on Monday only)
6+
- cron: "0 0 * * *"
7+
workflow_dispatch:
8+
inputs:
9+
target:
10+
description: "Where to post (test channel/chat or prod)"
11+
required: true
12+
default: "test"
13+
type: choice
14+
options:
15+
- test
16+
- prod
17+
post_telegram:
18+
description: "Also post to Telegram on this manual run"
19+
required: false
20+
default: false
21+
type: boolean
22+
23+
permissions:
24+
contents: read
25+
actions: write
26+
27+
env:
28+
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
29+
CARGO_NET_RETRY: "10"
30+
31+
jobs:
32+
loc:
33+
name: Count ethlambda LoC and publish report
34+
runs-on: ubuntu-latest
35+
steps:
36+
- name: Checkout sources
37+
uses: actions/checkout@v6
38+
39+
- name: Setup Rust
40+
uses: dtolnay/rust-toolchain@master
41+
with:
42+
toolchain: "1.92.0"
43+
44+
- name: Setup cache
45+
uses: Swatinem/rust-cache@v2
46+
47+
- name: Install cargo-warloc
48+
run: cargo install cargo-warloc --locked --version 0.1.1
49+
50+
- name: Restore previous LoC report
51+
id: cache-loc-report
52+
uses: actions/cache/restore@v5
53+
with:
54+
path: loc_report.json
55+
key: loc-report-${{ github.ref_name }}-${{ github.run_id }}
56+
restore-keys: |
57+
loc-report-${{ github.ref_name }}-
58+
59+
- name: Stash previous report as .old for delta computation
60+
if: steps.cache-loc-report.outputs.cache-hit != ''
61+
run: mv loc_report.json loc_report.json.old
62+
63+
- name: Generate LoC report
64+
run: python3 .github/scripts/generate_loc_report.py
65+
66+
- name: Save new LoC report to cache
67+
if: success()
68+
uses: actions/cache/save@v5
69+
with:
70+
path: loc_report.json
71+
key: loc-report-${{ github.ref_name }}-${{ github.run_id }}
72+
73+
- name: Post results to workflow summary
74+
run: cat loc_report_github.txt >> "$GITHUB_STEP_SUMMARY"
75+
76+
- name: Post to Slack
77+
env:
78+
SLACK_WEBHOOK: >-
79+
${{ (github.event_name == 'schedule' || inputs.target == 'prod')
80+
&& secrets.ETHLAMBDA_GENERAL_SLACK_WEBHOOK
81+
|| secrets.ETHLAMBDA_TEST_SLACK_WEBHOOK }}
82+
run: bash .github/scripts/publish_slack.sh loc_report_slack.json
83+
84+
- name: Post to Telegram (weekly, or manual opt-in)
85+
env:
86+
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
87+
TELEGRAM_ETHLAMBDA_CHAT_ID: >-
88+
${{ (github.event_name == 'schedule' || inputs.target == 'prod')
89+
&& secrets.TELEGRAM_ETHLAMBDA_CHAT_ID
90+
|| secrets.TELEGRAM_ETHLAMBDA_TEST_CHAT_ID }}
91+
run: |
92+
# Scheduled runs only post to Telegram on Monday (UTC).
93+
# Manual runs require post_telegram=true to opt in.
94+
if [[ "${{ github.event_name }}" == "schedule" ]]; then
95+
day_of_week=$(date -u +%u) # 1=Monday .. 7=Sunday
96+
if [[ "$day_of_week" != "1" ]]; then
97+
echo "Skipping Telegram post (scheduled run, only sent on Monday)"
98+
exit 0
99+
fi
100+
elif [[ "${{ inputs.post_telegram }}" != "true" ]]; then
101+
echo "Skipping Telegram post (manual run, post_telegram not enabled)"
102+
exit 0
103+
fi
104+
bash .github/scripts/publish_telegram.sh loc_report_telegram.txt

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,8 @@ The RPC crate runs **two independent Axum servers** on separate ports, allowing
294294
- `GET /lean/v0/checkpoints/justified` — justified checkpoint (JSON)
295295
- `GET /lean/v0/fork_choice` — fork choice tree (JSON)
296296
- `GET /lean/v0/fork_choice/ui` — interactive D3.js visualization
297+
- `GET /lean/v0/blocks/{block_id}` — block as JSON; `block_id` is a `0x`-prefixed 32-byte hex root or a decimal slot
298+
- `GET /lean/v0/blocks/{block_id}/header` — block header as JSON
297299
- Requires `Store` access
298300

299301
### Metrics Server (`:5054`)

CONTRIBUTING.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,11 @@ If you discover a security vulnerability, **do not open a public issue**. Instea
144144

145145
## Communication
146146

147-
- **Telegram**: [ethlambda group](https://t.me/ethlambda_client) — questions, discussion, coordination
148-
- **X (Twitter)**: Follow [@ethlambda_lean](https://twitter.com/ethlambda_lean) for updates
149-
- **GitHub Issues**: Bugs, feature requests, and technical discussion
147+
- **Telegram**: [ethlambda group](https://t.me/ethlambda_client), where we post daily updates; drop by to ask questions or chat about anything Lean-related.
148+
- **X (Twitter)**: [@ethlambda_lean](https://twitter.com/ethlambda_lean) for occasional updates.
149+
- **Weekly community call**: every Friday, streamed live on [@class_lambda](https://x.com/class_lambda); the call link is posted on Telegram beforehand.
150+
- **GitHub Issues**: bugs, feature requests, and technical discussion.
151+
- **Ecosystem coordination**: the [PQ Interop calls](https://github.com/ethereum/pm/issues?q=is%3Aissue+%22PQ+Interop%22+in%3Atitle) on `ethereum/pm` cover cross-client Lean Ethereum work and related updates; the meeting links are posted on each issue.
150152

151153
## License
152154

0 commit comments

Comments
 (0)