diff --git a/.github/configs/docs-branches.yaml b/.github/configs/docs-branches.yaml new file mode 100644 index 00000000000..bcc44b6b1dd --- /dev/null +++ b/.github/configs/docs-branches.yaml @@ -0,0 +1,18 @@ +# Branches whose documentation is built and published to +# steel.ethereum.foundation/docs/execution-specs/ +# +# The `branches[]` list must stay in sync with steel-website's +# `docs-config.yml`; a branch that publishes here but isn't listed there +# is dispatched but dropped by the aggregator. +# +# Each entry defines: +# - path: The branch name (must match the Git branch exactly) +# - label: Human-readable label for the version switcher UI + +branches: + - path: "mainnet" + label: "Mainnet (BPO2)" + - path: "forks/amsterdam" + label: "Amsterdam" + - path: "devnets/bal/4" + label: "bal-devnet-4" diff --git a/.github/workflows/docs-build.yaml b/.github/workflows/docs-build.yaml new file mode 100644 index 00000000000..a9617549142 --- /dev/null +++ b/.github/workflows/docs-build.yaml @@ -0,0 +1,356 @@ +# Build Docs +# +# Dual-purpose workflow: +# - On PRs: Tests that MkDocs and spec docs build successfully +# - On push to allowlisted branches: Builds, uploads artifacts, and triggers +# the aggregator workflow in steel-website for deployment +# +# Site structure at steel.ethereum.foundation/docs/execution-specs/: +# /docs/execution-specs/ - Default branch docs (mirrored) +# /docs/execution-specs/specs/reference/ - Default branch spec reference (mirrored) +# /docs/execution-specs// - Branch-specific docs +# /docs/execution-specs//specs/reference - Branch-specific spec reference (docc output) + +name: Build Docs + +on: + push: + # Branches that auto-publish on push. Other allowlisted branches in + # .github/configs/docs-branches.yaml publish only via workflow_dispatch + # (use the `ref:` input to publish a tag/SHA). + branches: + - mainnet + - forks/amsterdam + paths: &docs_paths + - "src/ethereum/**" + - "src/ethereum_spec_tools/docc.py" + - "src/ethereum_spec_tools/forks.py" + - "static/**" + - "docs/**" + - "packages/testing/**" + - "*.md" + - "mkdocs.yml" + - "uv.lock" + - "pyproject.toml" + - ".github/workflows/docs-build.yaml" + - ".github/configs/docs-branches.yaml" + + pull_request: + paths: *docs_paths + + workflow_dispatch: + inputs: + branch: + description: "Branch to publish under. Must be in the allowlist (see .github/configs/docs-branches.yaml)." + required: true + type: string + default: forks/amsterdam + ref: + description: "Ref to build: branch name, tag (e.g. v1.2.3), or commit SHA. Empty = tip of the branch above." + required: false + type: string + default: "" + +concurrency: + group: docs-build-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint-md: + name: "Lint Markdown" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # TODO: Still on node20 — update when upstream releases a node22+ version + - uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0 + with: + globs: | + docs/**/*.md + *.md + + changelog: + name: "Validate Changelog Entries" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./.github/actions/setup-uv + - name: Run changelog validation + run: just changelog + + check-should-publish: + name: "Check Should Publish" + runs-on: ubuntu-latest + needs: [lint-md, changelog] + outputs: + should_publish: ${{ steps.check.outputs.should_publish }} + branch: ${{ steps.check.outputs.branch }} + branch_artifact_name: ${{ steps.check.outputs.branch_artifact_name }} + commit_sha: ${{ steps.check.outputs.commit_sha }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + sparse-checkout: .github/configs/docs-branches.yaml + sparse-checkout-cone-mode: false + + - name: Resolve publish configuration + id: check + env: + BRANCH_INPUT: ${{ github.event.inputs.branch || github.ref_name }} + REF_INPUT: ${{ github.event.inputs.ref }} + EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} + run: | + echo "branch=$BRANCH_INPUT" >> "$GITHUB_OUTPUT" + + # Create artifact-safe name (replace / with -) + BRANCH_ARTIFACT_NAME="${BRANCH_INPUT//\//-}" + echo "branch_artifact_name=$BRANCH_ARTIFACT_NAME" >> "$GITHUB_OUTPUT" + + # Extract branch paths from allowlist config + ALLOWLISTED_BRANCHES=$(yq '.branches[].path' .github/configs/docs-branches.yaml 2>/dev/null || echo "") + + if [ -z "$ALLOWLISTED_BRANCHES" ]; then + echo "ERROR: Could not read allowlist from .github/configs/docs-branches.yaml" + exit 1 + fi + + # Check if branch is in allowlist + if echo "$ALLOWLISTED_BRANCHES" | grep -qxF "$BRANCH_INPUT"; then + SHOULD_PUBLISH="true" + else + SHOULD_PUBLISH="false" + fi + echo "should_publish=$SHOULD_PUBLISH" >> "$GITHUB_OUTPUT" + + # Resolve the concrete SHA we will build, so metadata.json and the + # aggregator dispatch payload match what the build jobs check out. + # + # On push, GITHUB_SHA is the pushed commit; using it avoids a race + # where another push lands between the event and a `gh api` lookup. + # On workflow_dispatch, the user may specify any branch/tag/SHA via + # the `ref` input (empty falls back to the branch tip), and + # github.sha points at whatever ref the dispatch was launched from + # (often the default branch), so we resolve via the API. + if [ "$EVENT_NAME" != "pull_request" ] && [ "$SHOULD_PUBLISH" = "true" ]; then + if [ "$EVENT_NAME" = "push" ]; then + COMMIT_SHA="$GITHUB_SHA" + else + RESOLVE_REF="${REF_INPUT:-$BRANCH_INPUT}" + COMMIT_SHA=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${RESOLVE_REF}" --jq .sha 2>/dev/null || true) + if [ -z "$COMMIT_SHA" ]; then + echo "ERROR: could not resolve commit SHA for ref '${RESOLVE_REF}'" + exit 1 + fi + fi + echo "commit_sha=$COMMIT_SHA" >> "$GITHUB_OUTPUT" + fi + + # Write job summary + { + echo "## Check Should Publish" + echo "" + echo "| Setting | Value |" + echo "|---------|-------|" + echo "| **Branch** | \`$BRANCH_INPUT\` |" + if [ -n "$REF_INPUT" ]; then + echo "| **Ref** | \`$REF_INPUT\` |" + fi + if [ -n "${COMMIT_SHA:-}" ]; then + echo "| **Resolved SHA** | \`${COMMIT_SHA:0:7}\` |" + fi + echo "| **Event** | \`$EVENT_NAME\` |" + echo "| **Allowlisted** | $SHOULD_PUBLISH |" + echo "" + + if [ "$EVENT_NAME" = "pull_request" ]; then + echo "-> **Build only** -- pull request; artifacts are not published." + elif [ "$SHOULD_PUBLISH" = "true" ]; then + echo "-> **Will publish** at " + echo "" + echo "_Note: the URL only resolves if \`${BRANCH_INPUT}\` is also configured in steel-website's \`BRANCH_CONFIG\` (\`deploy.yml\`). If not, the aggregator will receive the dispatch but drop the artifact._" + else + echo "-> **Skipping publish** -- branch is not in the allowlist (\`.github/configs/docs-branches.yaml\`)." + echo "" + echo "Allowlisted branches:" + echo "$ALLOWLISTED_BRANCHES" | while read -r b; do + echo "- \`$b\`" + done + fi + } >> "$GITHUB_STEP_SUMMARY" + + html-docs: + name: "Build HTML Docs" + runs-on: ubuntu-latest + needs: check-should-publish + # Always build for PRs (testing); only build for push if allowlisted + if: github.event_name == 'pull_request' || needs.check-should-publish.outputs.should_publish == 'true' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && needs.check-should-publish.outputs.commit_sha || '' }} + fetch-depth: 0 + submodules: recursive + + - uses: ./.github/actions/setup-uv + + - name: Build MkDocs documentation + env: + BRANCH: ${{ needs.check-should-publish.outputs.branch }} + SHOULD_PUBLISH: ${{ needs.check-should-publish.outputs.should_publish }} + run: | + if [ "$SHOULD_PUBLISH" = "true" ]; then + export SITE_URL="https://steel.ethereum.foundation/docs/execution-specs/${BRANCH}/" + else + export SITE_URL="https://example.com/docs/${BRANCH}/" + fi + + echo "Building MkDocs with SITE_URL=$SITE_URL" + just docs + + - name: Upload HTML docs artifact + if: needs.check-should-publish.outputs.should_publish == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: docs-html-${{ needs.check-should-publish.outputs.branch_artifact_name }} + path: .just/docs/site/ + retention-days: 1 + if-no-files-found: error + + spec-docs: + name: "Build Spec Docs" + runs-on: ubuntu-latest + needs: check-should-publish + if: github.event_name == 'pull_request' || needs.check-should-publish.outputs.should_publish == 'true' + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && needs.check-should-publish.outputs.commit_sha || '' }} + fetch-depth: 0 + submodules: recursive + + - uses: ./.github/actions/setup-uv + + - name: Build spec documentation + run: just docs-spec + env: + DOCC_SKIP_DIFFS: ${{ case(github.event_name == 'push' && github.ref_name == github.event.repository.default_branch, '', '1') }} + + - name: Upload spec docs artifact + if: needs.check-should-publish.outputs.should_publish == 'true' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: docs-spec-${{ needs.check-should-publish.outputs.branch_artifact_name }} + path: .just/docs-spec/ + retention-days: 1 + if-no-files-found: error + + combine: + name: "Combine and Upload" + runs-on: ubuntu-latest + needs: [check-should-publish, html-docs, spec-docs] + if: needs.check-should-publish.outputs.should_publish == 'true' + + steps: + - name: Download HTML docs artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: docs-html-${{ needs.check-should-publish.outputs.branch_artifact_name }} + path: html-docs/ + + - name: Download spec docs artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: docs-spec-${{ needs.check-should-publish.outputs.branch_artifact_name }} + path: spec-docs/ + + - name: Stage combined artifact + env: + BRANCH: ${{ needs.check-should-publish.outputs.branch }} + COMMIT_SHA: ${{ needs.check-should-publish.outputs.commit_sha }} + run: | + mkdir -p "stage/${BRANCH}/specs/reference" + + # HTML docs at root of branch directory + rsync -a html-docs/ "stage/${BRANCH}/" + + # Spec reference (docc output) nested under specs/ to mirror nav hierarchy + rsync -a spec-docs/ "stage/${BRANCH}/specs/reference/" + + # Build metadata + cat > "stage/${BRANCH}/metadata.json" < +- **Repo documentation (default branch/fork)**: - **Protocol history**: [docs/specs/protocol_history.md](docs/specs/protocol_history.md) - **Versioning scheme**: [docs/specs/spec_releases.md](docs/specs/spec_releases.md) (PEP 440 compatible; hardfork encoded in the minor version, `rcN` marks devnets). diff --git a/docs/dev/docs.md b/docs/dev/docs.md index a60dc352985..96fe14f6044 100644 --- a/docs/dev/docs.md +++ b/docs/dev/docs.md @@ -1,6 +1,6 @@ # Documentation -The `execution-specs` documentation is generated via [`mkdocs`](https://www.mkdocs.org/) and (soon) hosted remotely on Github Pages at [steel.ethereum.foundation/docs/](https://steel.ethereum.foundation/docs/). +The `execution-specs` documentation is generated via [`mkdocs`](https://www.mkdocs.org/) and hosted at [steel.ethereum.foundation/docs/execution-specs/](https://steel.ethereum.foundation/docs/execution-specs/). ## Prerequisites diff --git a/docs/hooks/reference_new_tab.py b/docs/hooks/reference_new_tab.py new file mode 100644 index 00000000000..97937b0be4b --- /dev/null +++ b/docs/hooks/reference_new_tab.py @@ -0,0 +1,34 @@ +""" +Add `target="_blank"` to links pointing at the `docc`-rendered reference. + +Without this, Material's `navigation.instant` intercepts clicks and tries to +XHR-swap the target page into the current DOM, which fails silently because +`docc`'s HTML does not match Material's template markers: the URL and title +update but the body stays on the original page. `target="_blank"` makes +instant-nav opt out and forces a full-page load, matching the home page card +that already sets the attribute via `attr_list`. + +Matches every relative href form mkdocs emits for the reference page across +page depths (e.g., `reference/`, `../reference/`, `specs/reference/`, +`../../specs/reference/`). The trailing slash after `reference` prevents +matching unrelated paths like `reference_specification/`. +""" + +import re + +_A_TAG_REF = re.compile( + r']*?href="(?:\.\./)*(?:specs/)?reference/(?:index\.html)?"[^>]*?)>' +) + + +def _rewrite(match: "re.Match[str]") -> str: + attrs = match.group(1) + if "target=" in attrs: + return match.group(0) + return f'' + + +def on_post_page(output: str, page, config) -> str: + """Rewrite anchor tags pointing at the reference to open in a new tab.""" + del page, config + return _A_TAG_REF.sub(_rewrite, output) diff --git a/docs/index.md b/docs/index.md index 277f0f58f94..c16b008cb7f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -56,7 +56,7 @@ EELS is a collaborative effort between Ethereum Improvement Proposals (EIP) auth Browse the rendered Python specifications for the current fork and EIP. - [:octicons-arrow-right-24: Specifications](spec/) + [:octicons-arrow-right-24: Reference ↗](specs/reference/index.md){target=_blank rel=noopener} - :material-format-list-checks: **Test Case Reference** @@ -64,7 +64,7 @@ EELS is a collaborative effort between Ethereum Improvement Proposals (EIP) auth Browse all test cases organized by fork and EIP. - [:octicons-arrow-right-24: Browse tests](tests/) + [:octicons-arrow-right-24: Browse tests](tests/index.md) diff --git a/docs/navigation.md b/docs/navigation.md index 3415721cc1a..e5344212474 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -11,6 +11,7 @@ * [Installation Troubleshooting](getting_started/installation_troubleshooting.md) * [Getting Help](getting_started/getting_help.md) * [Specifications](specs/index.md) + * [Reference ↗](specs/reference/index.md) * [Writing Specs](specs/writing_specs.md) * [Adding a New EIP](specs/adding_a_new_eip.md) * [Spec Releases](specs/spec_releases.md) diff --git a/docs/specs/index.md b/docs/specs/index.md index bbe3555553c..a7cdb96f34e 100644 --- a/docs/specs/index.md +++ b/docs/specs/index.md @@ -51,7 +51,7 @@ The trade-off is that a bug fix that applies to every fork must be applied to ev - [Adding a New EIP](adding_a_new_eip.md): the EIP lifecycle from pre-draft to final, and how to land a new EIP in EELS. - [Spec Releases](spec_releases.md): how EELS versions relate to Ethereum hardforks and devnets. - [Protocol History](protocol_history.md): the full table of mainnet hardforks, their included EIPs, and their fork manifests. -- [Rendered specification](https://ethereum.github.io/execution-specs/): the `docc`-rendered narrative view of the Python spec, with side-by-side diffs between forks. +- [Rendered specification](https://steel.ethereum.foundation/docs/execution-specs/specs/reference/): the `docc`-rendered narrative view of the Python spec, with side-by-side diffs between forks. !!! bug "Reporting a vulnerability" Care is required when filing issues or PRs for functionality that is live on Ethereum mainnet. Please report vulnerabilities and verify bounty eligibility via the [bug bounty program](https://bounty.ethereum.org). diff --git a/docs/specs/reference/index.md b/docs/specs/reference/index.md new file mode 100644 index 00000000000..73f068664f2 --- /dev/null +++ b/docs/specs/reference/index.md @@ -0,0 +1,11 @@ +# Reference + +The rendered Ethereum execution specification is generated by `docc` +from the Python source under `src/ethereum/`. + +At deploy time, this placeholder is replaced by the `docc` output. To +build the reference locally, run: + +```bash +just docs-spec +``` diff --git a/docs/specs/writing_specs.md b/docs/specs/writing_specs.md index 462dbcd5189..03f862251c6 100644 --- a/docs/specs/writing_specs.md +++ b/docs/specs/writing_specs.md @@ -119,7 +119,7 @@ The marked lines (`<-`) are now incorrectly attributed to EIP-4567 in Fork+1. In ## Changes across multiple forks -Many contributions require changes across multiple forks, organized under `src/ethereum/forks/`. When making such changes, ensure that differences between the forks are minimal and consist only of necessary differences. This produces cleaner [diff outputs](https://ethereum.github.io/execution-specs/diffs/index.html). +Many contributions require changes across multiple forks, organized under `src/ethereum/forks/`. When making such changes, ensure that differences between the forks are minimal and consist only of necessary differences. This produces cleaner [diff outputs](https://steel.ethereum.foundation/docs/execution-specs/specs/reference/diffs/index.html). When creating pull requests affecting multiple forks, we recommend submitting your PR in two steps: diff --git a/docs/writing_tests/post_mortems.md b/docs/writing_tests/post_mortems.md index b9f5dd0ec30..5c18ef41fd4 100644 --- a/docs/writing_tests/post_mortems.md +++ b/docs/writing_tests/post_mortems.md @@ -78,7 +78,7 @@ IDs of the tests added that now cover the missed scenario and link to the docume *Example:* -- [`tests/prague/eip2537_bls_12_381_precompiles/test_bls12_g1msm.py::test_invalid\[fork_Prague-state_test---bls_g1_truncated_input-\]`](../tests/prague/eip2537_bls_12_381_precompiles/test_bls12_g1msm/test_invalid/) +- [`tests/prague/eip2537_bls_12_381_precompiles/test_bls12_g1msm.py::test_invalid\[fork_Prague-state_test---bls_g1_truncated_input-\]`](../tests/prague/eip2537_bls_12_381_precompiles/test_bls12_g1msm/test_invalid.md) ### Framework/Documentation Changes diff --git a/mkdocs.yml b/mkdocs.yml index 9f5018c01b7..f5f9fa55257 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,8 @@ -site_name: Ethereum Execution Layer Specifications +site_name: Ethereum Execution Specs site_description: The Ethereum Execution Layer Specifications and Tests -site_url: https://steel.ethereum.foundation/docs/ +site_url: !ENV [SITE_URL, "https://steel.ethereum.foundation/docs/execution-specs/"] repo_url: https://github.com/ethereum/execution-specs repo_name: execution-specs -edit_uri: edit/forks/amsterdam/docs/ copyright: "Copyright: 2026, Ethereum Community" plugins: @@ -32,6 +31,9 @@ plugins: - literate-nav: nav_file: navigation.md +hooks: + - docs/hooks/reference_new_tab.py + watch: - CONTRIBUTING.md - SECURITY.md diff --git a/packages/testing/src/execution_testing/config/docs.py b/packages/testing/src/execution_testing/config/docs.py index 291f6552311..21f21a1b5a8 100644 --- a/packages/testing/src/execution_testing/config/docs.py +++ b/packages/testing/src/execution_testing/config/docs.py @@ -17,8 +17,10 @@ class DocsConfig(BaseModel): GENERATE_UNTIL_FORK: str = "Amsterdam" """The fork until which documentation should be generated.""" - DOCS_BASE_URL: str = "https://steel.ethereum.foundation/docs" + DOCS_BASE_URL: str = ( + "https://steel.ethereum.foundation/docs/execution-specs" + ) - # Documentation URLs prefixed with `DOCS_URL__` to avoid conflicts - # with other URLs + # Documentation URLs prefixed with `DOCS_URL__` to avoid conflicts with + # other URLs DOCS_URL__WRITING_TESTS: str = f"{DOCS_BASE_URL}/writing_tests/"