Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 210 additions & 0 deletions .github/scripts/release-body.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env python3

"""Generate the GitHub release body listing compiled and republished guest programs."""

import argparse
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from subprocess import DEVNULL, PIPE, STDOUT

ROOT = Path(__file__).resolve().parents[2]
TARGET_DIR = ROOT / "target"

REPOSITORY = "eth-act/ere-guests"
GUEST_PREFIX = "stateless-validator-"
COMPILED_ELS = ("ethrex", "reth")
ZKVMS = ("airbender", "openvm", "risc0", "sp1", "zisk")

TABLE_HEADER = (
"| EL | EL Version | zkVM | zkVM Version | Target | ELF | Program VK |",
"| --- | --- | --- | --- | --- | --- | --- |",
)


@dataclass(frozen=True)
class Guest:
"""A guest program keyed by execution layer and zkVM, with display versions."""

el: str
el_version: str
zkvm: str
zkvm_version: str
source_url: str | None = None


def parse_args() -> argparse.Namespace:
"""Parses the command-line arguments."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--tag", required=True)
parser.add_argument("--artifacts-dir", type=Path, default=Path("artifacts"))
parser.add_argument(
"--artifact-registry", type=Path, default=Path("artifact-registry.json")
)
return parser.parse_args()


def run_command(
args: list[str], stdout: int = DEVNULL, stderr: int = PIPE
) -> subprocess.CompletedProcess:
"""Runs `args` from ROOT, raising RuntimeError with captured output on failure."""
try:
proc = subprocess.run(args, cwd=ROOT, stdout=stdout, stderr=stderr, text=True)
except OSError as error:
raise RuntimeError(f"`{' '.join(args)}` could not run: {error}") from error
if proc.returncode != 0:
detail = (proc.stderr or proc.stdout or "").strip()
raise RuntimeError(f"`{' '.join(args)}` failed ({proc.returncode})\n{detail}")
return proc


def read_ere_version() -> str:
"""Returns the Ere version pinned by git tag in the workspace Cargo.toml."""
cargo_toml = (ROOT / "Cargo.toml").read_text()
match = re.search(r'eth-act/ere"[^}]*?\btag\s*=\s*"(v[^"]+)"', cargo_toml)
if not match:
raise RuntimeError('no `eth-act/ere` git `tag = "vX.Y.Z"` found in Cargo.toml')
return match.group(1)


def read_compiled_el_version(el: str) -> str:
"""Returns the EL version from the `stateless-validator-{el}` build script."""
crate = f"{GUEST_PREFIX}{el}"
run_command(["cargo", "clean", "--package", crate])
proc = run_command(["cargo", "build", "-vv", "--package", crate], PIPE, STDOUT)
match = re.search(r"cargo:rustc-env=EL_VERSION=(\S+)", proc.stdout)
if not match:
raise RuntimeError(f"EL_VERSION not emitted by `{crate}` build script")
return match.group(1)


def read_zkvm_versions() -> dict[str, str]:
"""Returns zkVM SDK versions parsed from the `ere-catalog` build-script output."""
version_file = "zkvm_sdk_version_impl.rs"
run_command(["cargo", "build", "--package", "ere-catalog"])
outputs = (TARGET_DIR / "debug" / "build").glob(f"ere-catalog-*/out/{version_file}")
latest = max(outputs, key=lambda path: path.stat().st_mtime, default=None)
if latest is None:
raise RuntimeError(f"{version_file} not found after building ere-catalog")
versions = re.findall(r'Self::(\w+)\s*=>\s*"([^"]+)"', latest.read_text())
if not versions:
raise RuntimeError(f"failed to parse zkVM versions from {version_file}")
return {zkvm.lower(): version for zkvm, version in versions}


def read_elf_word_size(elf_path: Path) -> int:
"""Returns the ELF word size (32 or 64) reported by `file`."""
proc = run_command(["file", str(elf_path)], PIPE, STDOUT)
if "32-bit" in proc.stdout:
return 32
if "64-bit" in proc.stdout:
return 64
raise RuntimeError(f"cannot determine ELF class from `file`: {proc.stdout.strip()}")


def render_row(guest: Guest, artifacts_dir: Path, release_url: str) -> str | None:
"""Renders `guest` as a Markdown table row, or None when its ELF or VK is absent."""
elf = f"{GUEST_PREFIX}{guest.el}-{guest.zkvm}.elf"
vk = f"{GUEST_PREFIX}{guest.el}-{guest.zkvm}.vk"
elf_path = artifacts_dir / elf
vk_path = artifacts_dir / vk
if not (elf_path.is_file() and vk_path.is_file()):
return None

target = f"riscv{read_elf_word_size(elf_path)}im"
elf_cell = f"[Link]({release_url}/{elf})"
if guest.source_url:
elf_cell += f" / [Source]({guest.source_url})"
return (
f"| `{guest.el}` | `{guest.el_version}` "
f"| `{guest.zkvm}` | `{guest.zkvm_version}` "
f"| `{target}` | {elf_cell} | [Link]({release_url}/{vk}) |"
)


def compiled_guests() -> list[Guest]:
"""Returns the COMPILED_ELS x ZKVMS guests, with versions from the build scripts."""
el_versions = {el: read_compiled_el_version(el) for el in COMPILED_ELS}
zkvm_versions = read_zkvm_versions()
return [
Guest(el, el_versions[el], zkvm, zkvm_versions[zkvm])
for el in COMPILED_ELS
for zkvm in ZKVMS
]


def republished_guests(artifact_registry: Path) -> list[Guest]:
"""Returns the guests listed in the artifact registry, ordered by key."""
registry = json.loads(artifact_registry.read_text())["stateless-validator-elf"]
guests = []
for key, entry in sorted(registry.items()):
el, zkvm = key.rsplit("-", 1)
guests.append(
Guest(el, entry["el_version"], zkvm, entry["zkvm_version"], entry["url"])
)
return guests


def compiled_rows(artifacts_dir: Path, release_url: str) -> list[str]:
"""Returns rows for compiled guests, skipping any whose artifacts are absent."""
rendered = (
render_row(guest, artifacts_dir, release_url) for guest in compiled_guests()
)
return [row for row in rendered if row is not None]


def republished_rows(
artifacts_dir: Path, artifact_registry: Path, release_url: str
) -> list[str]:
"""Returns rows for registry guests, requiring every artifact to be present."""
rows = []
for guest in republished_guests(artifact_registry):
row = render_row(guest, artifacts_dir, release_url)
if row is None:
raise RuntimeError(
f"republished guest {GUEST_PREFIX}{guest.el}-{guest.zkvm}.elf is missing"
)
rows.append(row)
return rows


def render_release_body(tag: str, artifacts_dir: Path, artifact_registry: Path) -> str:
"""Builds the Markdown release body for `tag`."""
release_url = f"https://github.com/{REPOSITORY}/releases/download/{tag}"

ere_version = read_ere_version()
compiled = compiled_rows(artifacts_dir, release_url)
republished = republished_rows(artifacts_dir, artifact_registry, release_url)

body = [
"## Compiled guest programs",
"",
f"Built with Ere compiler version: `{ere_version}`.",
"",
*TABLE_HEADER,
*compiled,
]
if republished:
body += [
"",
"## Republished guest programs",
"",
*TABLE_HEADER,
*republished,
]
return "\n".join(body)


def main() -> None:
args = parse_args()
print(render_release_body(args.tag, args.artifacts_dir, args.artifact_registry))


if __name__ == "__main__":
try:
main()
except RuntimeError as error:
sys.exit(str(error))
89 changes: 31 additions & 58 deletions .github/workflows/compile-and-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,26 @@ permissions:

jobs:
compile:
name: Compile ${{ matrix.guest }}-${{ matrix.zkvm }}
name: Compile stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}
runs-on: ubuntu-latest
outputs:
ere_tag: ${{ steps.get_ere_tag.outputs.ere_tag }}
strategy:
fail-fast: false
matrix:
guest:
- empty
- panic
- stateless-validator-ethrex
- stateless-validator-reth
el:
- ethrex
- reth
zkvm:
- airbender
- openvm
- risc0
- sp1
- zisk
exclude:
- guest: stateless-validator-ethrex
- el: ethrex
zkvm: airbender
- guest: stateless-validator-ethrex
- el: ethrex
zkvm: openvm
steps:
- name: Checkout code
Expand Down Expand Up @@ -70,9 +68,9 @@ jobs:
-v $PWD/output:/output \
ghcr.io/eth-act/ere/ere-compiler-${{ matrix.zkvm }}:${{ steps.get_ere_tag.outputs.ere_tag }} \
--compiler-kind rust-customized \
--guest-dir /ere-guests/bin/${{ matrix.guest }}/${{ matrix.zkvm }} \
--guest-dir /ere-guests/bin/stateless-validator-${{ matrix.el }}/${{ matrix.zkvm }} \
--output-dir /output \
--elf-name ${{ matrix.guest }}-${{ matrix.zkvm }}.elf
--elf-name stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}.elf

- name: Compile ZisK guest with feature cycle-scope enabled
if: ${{ matrix.zkvm == 'zisk' }}
Expand All @@ -84,9 +82,9 @@ jobs:
-v $PWD/output:/output \
ghcr.io/eth-act/ere/ere-compiler-${{ matrix.zkvm }}:${{ steps.get_ere_tag.outputs.ere_tag }} \
--compiler-kind rust-customized \
--guest-dir /ere-guests/bin/${{ matrix.guest }}/${{ matrix.zkvm }} \
--guest-dir /ere-guests/bin/stateless-validator-${{ matrix.el }}/${{ matrix.zkvm }} \
--output-dir /output \
--elf-name ${{ matrix.guest }}-${{ matrix.zkvm }}-profiling.elf \
--elf-name stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}-profiling.elf \
-- \
--features cycle-scope

Expand All @@ -96,17 +94,17 @@ jobs:
-e RUST_LOG=info \
-v $PWD/output:/output \
ghcr.io/eth-act/ere/ere-server-${{ matrix.zkvm }}:${{ steps.get_ere_tag.outputs.ere_tag }} \
--elf-path /output/${{ matrix.guest }}-${{ matrix.zkvm }}.elf \
--elf-path /output/stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}.elf \
keygen \
--program-vk-path /output/${{ matrix.guest }}-${{ matrix.zkvm }}.vk
--program-vk-path /output/stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}.vk

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.guest }}-${{ matrix.zkvm }}
name: stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}
path: |
output/${{ matrix.guest }}-${{ matrix.zkvm }}*.elf
output/${{ matrix.guest }}-${{ matrix.zkvm }}.vk
output/stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}*.elf
output/stateless-validator-${{ matrix.el }}-${{ matrix.zkvm }}.vk
if-no-files-found: error

check-git-tag:
Expand All @@ -131,6 +129,15 @@ jobs:
if: needs.check-git-tag.outputs.exists == 'true'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo build
uses: Swatinem/rust-cache@v2

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
Expand All @@ -140,44 +147,10 @@ jobs:
- name: Display structure of downloaded files
run: ls -R artifacts

- name: Generate release notes
- name: Generate release notes body
run: |
echo "## Compiled guest programs" > release-body.md
echo "" >> release-body.md
echo "This release contains compiled guest programs for various zkVMs." >> release-body.md
echo "" >> release-body.md
echo "Built with Ere compiler version: \`${{ needs.compile.outputs.ere_tag }}\`." >> release-body.md
echo "" >> release-body.md

RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/${{ needs.check-git-tag.outputs.tag }}"

# Get unique guest names from .elf files
declare -A guests
for file in artifacts/*.elf; do
if [ -f "$file" ]; then
filename=$(basename "$file")
# Skip variant ELFs (e.g. -profiling.elf) — only canonical guest-zkvm.elf entries are listed.
[[ "$filename" == *-profiling.elf ]] && continue
# Strip the last -zkvm.elf segment to get the guest name
guest=$(echo "$filename" | sed 's/-[^-]*\.elf$//')
guests["$guest"]=1
fi
done

# Sort and group by guest
for guest in $(echo "${!guests[@]}" | tr ' ' '\n' | sort); do
echo "### $guest" >> release-body.md
echo "" >> release-body.md
for elf_file in artifacts/$guest-*.elf; do
if [ -f "$elf_file" ]; then
elf_name=$(basename "$elf_file")
[[ "$elf_name" == *-profiling.elf ]] && continue
program_vk_name="${elf_name%.elf}.vk"
echo "- [\`$elf_name\`]($RELEASE_URL/$elf_name) | [\`$program_vk_name\`]($RELEASE_URL/$program_vk_name)" >> release-body.md
fi
done
echo "" >> release-body.md
done
.github/scripts/release-body.py --tag "${{ needs.check-git-tag.outputs.tag }}" > release-body.md
cat release-body.md

- name: Install Minisign
run: sudo apt-get update && sudo apt-get install -y minisign
Expand Down Expand Up @@ -224,7 +197,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
EXISTING="$(gh release view --repo "${{ github.repository }}" "${{ needs.check-git-tag.outputs.tag }}" --json body -q .body)"
gh release edit --repo "${{ github.repository }}" "${{ needs.check-git-tag.outputs.tag }}" --notes "${EXISTING}

$(cat release-body.md)"
gh release view --repo "${{ github.repository }}" "${{ needs.check-git-tag.outputs.tag }}" --json body -q .body > combined-notes.md
printf '\n\n' >> combined-notes.md
cat release-body.md >> combined-notes.md
gh release edit --repo "${{ github.repository }}" "${{ needs.check-git-tag.outputs.tag }}" --notes-file combined-notes.md
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target
.vscode
.secrets
__pycache__
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ ere-codec = { git = "https://github.com/eth-act/ere", tag = "v0.11.0" }
ere-platform-core = { git = "https://github.com/eth-act/ere", tag = "v0.11.0" }
ere-prover-core = { git = "https://github.com/eth-act/ere", tag = "v0.11.0" }
ere-dockerized = { git = "https://github.com/eth-act/ere", tag = "v0.11.0" }
ere-util-build = { git = "https://github.com/eth-act/ere", tag = "v0.11.0" }

# local
guest = { path = "crates/guest", default-features = false }
Expand Down
Loading
Loading