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
4 changes: 2 additions & 2 deletions .github/actions/validate-nvx/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ runs:
set -euo pipefail
python3 -m pip install --requirement scripts/setup/azure/requirements.txt
python3 -m compileall -q scripts
python3 -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_nightly_ci.py scripts/test_development_release.py scripts/test_crate_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py scripts/test_nvx_rs_mutants.py scripts/test_setup_azure.py -v
python3 -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_nightly_ci.py scripts/test_development_release.py scripts/test_crate_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py scripts/test_module_diagrams.py scripts/test_nvx_rs_mutants.py scripts/test_setup_azure.py -v
python3 scripts/test_adversarial.py -v
python3 .github/skills/nvx-host-connect/scripts/test_hosts.py -v
python3 scripts/nvx.py --help
Expand All @@ -31,7 +31,7 @@ runs:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python -m compileall -q scripts
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_nightly_ci.py scripts/test_development_release.py scripts/test_crate_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py scripts/test_nvx_rs_mutants.py scripts/test_setup_azure.py -v
python -m unittest scripts/test_performance.py scripts/test_nvx_tools.py scripts/test_guest_binaries.py scripts/test_microvm_tests.py scripts/test_nightly_ci.py scripts/test_development_release.py scripts/test_crate_release.py scripts/test_host_telemetry.py scripts/test_time_abi.py scripts/test_tool_versions.py scripts/test_markdown_links.py scripts/test_module_diagrams.py scripts/test_nvx_rs_mutants.py scripts/test_setup_azure.py -v
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
python scripts\test_adversarial.py -v
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Expand Down
252 changes: 252 additions & 0 deletions .github/workflows/module-diagrams.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
name: Module diagrams

# Draws the module diagrams of the nvx crate, which doc/modules publishes, with
# scripts/module_diagrams.py (doc/ci.md, "Module diagrams"). A push to dev that
# changes their inputs, such as a merge, commits them to dev when they change. A
# pull request that changes their inputs draws them without committing them, so
# that a change that breaks the drawing fails before it merges, and uploads them
# for review.
on:
push:
branches:
- dev
paths:
- nvx-rs/**
- rust-toolchain.toml
- scripts/module_diagrams.py
- scripts/nvx_tools/build_constants.py
- .github/workflows/module-diagrams.yml
pull_request:
branches:
- dev
types:
- opened
- synchronize
- reopened
- ready_for_review
paths:
- nvx-rs/**
- rust-toolchain.toml
- scripts/module_diagrams.py
- scripts/nvx_tools/build_constants.py
- .github/workflows/module-diagrams.yml
workflow_dispatch:

permissions:
contents: read

# Runs on dev wait for one another, and a newer push replaces the run that
# waits. Each run draws the newest dev, so the run that commits last commits
# the diagrams of the newest dev. A newer push to a pull request cancels its
# older run.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
CARGO_MODULES_VERSION: 0.27.0

jobs:
# The diagrams depend on the Graphviz release and the fonts that lay them
# out, so this job runs on a fixed Ubuntu release, and builds and runs the
# commit's code without credentials.
draw:
name: Draw module diagrams
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 30
outputs:
sha: ${{ steps.draw.outputs.sha }}
steps:
# A push or a dispatch draws the newest commit of its branch, rather than
# the commit that started it, so that whichever run commits last commits
# the diagrams of the newest dev.
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event_name != 'pull_request' && github.ref || '' }}
persist-credentials: false

# Graphviz measures the labels with Liberation Sans, which the diagrams
# name first, so that their layouts do not depend on the image's other
# fonts.
- name: Install Graphviz
shell: bash
run: |
set -euo pipefail
sudo apt-get update
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
graphviz fonts-liberation
dot -V

- name: Install the pinned Rust release
shell: bash
run: |
set -euo pipefail
toolchain=$(sed -n 's/^channel = "\([^"]*\)".*/\1/p' rust-toolchain.toml)
test -n "${toolchain}"
rustup toolchain install "${toolchain}" --profile minimal --no-self-update
echo "RUST_TOOLCHAIN=${toolchain}" >> "${GITHUB_ENV}"
echo "${RUNNER_TEMP}/cargo-modules/bin" >> "${GITHUB_PATH}"

- name: Restore cargo-modules
id: restore-cargo-modules
uses: actions/cache/restore@v5
with:
path: ${{ runner.temp }}/cargo-modules
key: cargo-modules-${{ env.CARGO_MODULES_VERSION }}-rust-${{ env.RUST_TOOLCHAIN }}-${{ runner.os }}-${{ runner.arch }}

- name: Install cargo-modules
if: steps.restore-cargo-modules.outputs.cache-hit != 'true'
shell: bash
run: >-
cargo "+${RUST_TOOLCHAIN}" install --locked cargo-modules
--version "${CARGO_MODULES_VERSION}" --root "${RUNNER_TEMP}/cargo-modules"

# Only dev pushes save the build, as they save the other caches.
- name: Save cargo-modules
if: >-
github.event_name == 'push' &&
github.ref == 'refs/heads/dev' &&
steps.restore-cargo-modules.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ runner.temp }}/cargo-modules
key: cargo-modules-${{ env.CARGO_MODULES_VERSION }}-rust-${{ env.RUST_TOOLCHAIN }}-${{ runner.os }}-${{ runner.arch }}

- name: Draw the diagrams
id: draw
shell: bash
run: |
set -euo pipefail
version=$(cargo modules --version)
if [[ "${version}" != "cargo-modules ${CARGO_MODULES_VERSION}" ]]; then
echo "::error::${version} is not cargo-modules ${CARGO_MODULES_VERSION}"
exit 1
fi
python3 scripts/module_diagrams.py --output doc/modules
echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}"

- name: Summarize the changes
shell: bash
run: |
set -euo pipefail
sha=$(git rev-parse HEAD)
changes=$(git status --porcelain --untracked-files=all -- doc/modules)
{
echo "## Module diagrams"
echo
if [[ -z "${changes}" ]]; then
echo "The diagrams of \`${sha}\` match doc/modules."
else
echo "The diagrams of \`${sha}\` change doc/modules:"
echo
echo '```text'
echo "${changes}"
echo '```'
fi
} >> "${GITHUB_STEP_SUMMARY}"

- name: Upload the diagrams
uses: actions/upload-artifact@v7
with:
name: module-diagrams-${{ github.run_id }}
path: doc/modules
if-no-files-found: error
overwrite: true
retention-days: 7

# Commits the diagrams that the draw job drew to dev, with the deploy key that
# may push to dev past its rules, the one of the performance history. It
# builds and runs no code of the commit, and commits nothing but doc/modules.
publish:
name: Publish module diagrams
needs: draw
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/dev'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout dev
uses: actions/checkout@v5
with:
ref: dev
fetch-depth: 0
ssh-key: ${{ secrets.PERFORMANCE_PERSIST_DEPLOY_KEY }}

- name: Download the diagrams
uses: actions/download-artifact@v8
with:
name: module-diagrams-${{ github.run_id }}
path: ${{ runner.temp }}/module-diagrams

- name: Commit the diagrams to dev
shell: bash
env:
DIAGRAMS: ${{ runner.temp }}/module-diagrams
DRAWN: ${{ needs.draw.outputs.sha }}
# The inputs of the diagrams: the paths that start this workflow.
INPUTS: >-
nvx-rs rust-toolchain.toml scripts/module_diagrams.py
scripts/nvx_tools/build_constants.py
.github/workflows/module-diagrams.yml
run: |
set -euo pipefail
if [[ ! "${DRAWN}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::The draw job named no commit: '${DRAWN}'"
exit 1
fi
unexpected=$(find "${DIAGRAMS}" -mindepth 1 \
! \( -type f \( -name README.md -o -name '*.svg' \) \) -print)
if [[ -n "${unexpected}" || ! -f "${DIAGRAMS}/README.md" ]]; then
echo "::error::The diagrams hold more than a page and SVG files, or no page:"
echo "${unexpected}"
exit 1
fi
read -r -a inputs <<< "${INPUTS}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

for attempt in 1 2 3; do
git fetch origin dev
git checkout -B module-diagrams origin/dev
# A rerun of this job alone reuses the drawing of its run, which a
# newer run may have replaced. Publish it only while dev holds the
# inputs that it was drawn from: the push that changed them started
# a run that publishes their diagrams.
status=0
git diff --quiet "${DRAWN}" HEAD -- "${inputs[@]}" || status=$?
if [[ "${status}" -eq 1 ]]; then
echo "::notice::dev changed the inputs of the diagrams since ${DRAWN}; the run of the newer commit publishes their diagrams"
exit 0
elif [[ "${status}" -ne 0 ]]; then
echo "::error::Cannot compare the inputs of the diagrams at ${DRAWN} with dev"
exit 1
fi
rm -rf doc/modules
mkdir -p doc/modules
cp "${DIAGRAMS}"/* doc/modules/
Comment thread
ppenna marked this conversation as resolved.
git add --all -- doc/modules

unexpected=$(git diff --cached --no-renames --name-only |
grep -v '^doc/modules/' || true)
if [[ -n "${unexpected}" ]]; then
echo "::error::Refusing to commit changes outside doc/modules:"
echo "${unexpected}"
exit 1
fi

if git diff --cached --quiet; then
echo "The module diagrams on dev are up to date"
exit 0
fi

git commit --no-verify \
-m "doc: update module diagrams (${DRAWN}) [skip ci]"
if git push origin HEAD:dev; then
exit 0
fi
if [ "${attempt}" -eq 3 ]; then
echo "::error::Failed to publish the module diagrams after ${attempt} attempts"
exit 1
fi
done
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ nvx-rs\target\release\nvx-rs.exe run
- [Azure Linux guest support](doc/guests/azurelinux-guest.md) - How NVX runs Azure Linux userland
on its own kernel.
- [Project structure](doc/project-structure.md) - Overview of the NVX repository layout.
- [Module diagrams](doc/modules/README.md) - Diagrams of the modules of the `nvx` crate and the
dependencies between them.
- [Continuous integration](doc/ci.md) - Instructions for running and maintaining NVX CI.
- [Copilot-driven adversarial testing](doc/adversarial-testing.md) - Bounded adaptive stress and
containment campaigns.
Expand Down
64 changes: 64 additions & 0 deletions doc/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -1804,6 +1804,70 @@ comm -23 replaced.txt replacement.txt # The mutants that only the replaced test
A push that changes the workflow checks it on `src/gpt/crc.rs` with the
library's `gpt::crc::` tests, in two jobs.

## Module diagrams

[`doc/modules`](modules/README.md) publishes diagrams of the modules of the
[`nvx` crate](../nvx-rs/README.md): its top-level modules, the dependencies
between them, and the modules that each contains, on Linux and Windows.
[`module_diagrams.py`](../scripts/module_diagrams.py) draws them. It exports
the modules of the crate's library, with its `cli`, `testing`, and `guest`
features, for each platform's target with
[cargo-modules](https://github.com/regexident/cargo-modules), merges the
platforms' modules, lays out the diagrams with Graphviz, and writes them with
the page that shows them. Unless it analyzes with `cfg(test)`, cargo-modules
drops as a test module every module whose cfg holds when `test` holds and every
other condition fails, such as one under `cfg(not(windows))`, so the script
also reads its analysis with `cfg(test)` and restores those modules, with their
own uses in place of those that the plain analysis moves to the modules that
contain them, and stops at a cfg that depends on `test` in another way. Nobody
edits the diagrams by hand:
[`module-diagrams.yml`](../.github/workflows/module-diagrams.yml) draws them
again after every push to `dev` that changes their inputs, `nvx-rs/`, the Rust
pin, the script, the build constants that name the crate's targets, or the
workflow, as the merge of such a pull request does, and commits them to `dev`
when they change.

Its `draw` job builds cargo-modules, at the release that the workflow pins,
with the Rust release that `rust-toolchain.toml` pins, and restores the build
from a cache that only `dev` pushes save. It runs on a fixed Ubuntu release,
whose Graphviz release lays out the diagrams, and installs Liberation Sans, the
font that they name first, with which Graphviz measures their labels, so that
their layouts depend neither on the runner image's other fonts nor on its
updates. It checks out, without credentials, the newest commit of the branch
that a push or a dispatch names, rather than the commit that started the run,
or a pull request's merge commit, draws the diagrams into `doc/modules`,
summarizes the files that they change, and uploads them as
`module-diagrams-<run-id>`. For a push to `dev`, or a manual dispatch on it,
the `publish` job then checks out the newest `dev` with
`PERFORMANCE_PERSIST_DEPLOY_KEY`, the deploy key with which the
[performance history](#development-releases-and-performance-history) is
persisted, since deploy keys may push to `dev` past its rules. It replaces
`doc/modules` with the uploaded diagrams and, if that changes anything,
commits it as `doc: update module diagrams (<sha>) [skip ci]`, where `<sha>`
is the commit that the `draw` job drew, and pushes it, retrying up to three
attempts in all on the newest `dev` when a push fails because `dev` moved. The
job builds and runs nothing of the commit, refuses an upload that holds
anything but the page and SVG files, and commits nothing outside
`doc/modules`. `[skip ci]` keeps its commit, which changes only documentation,
from starting the workflows of a push.

Runs on `dev` wait for one another, and a newer push replaces the run that
waits. Each run draws the newest `dev`, so whichever run commits last commits
the diagrams of the newest `dev`, in whatever order the runs start. A rerun of
the `publish` job alone, as of a run's failed jobs, reuses the drawing of its
run, which a newer run may have replaced. So, on each attempt, the job compares
the inputs, the paths that start the workflow, at the drawn commit and on the
newest `dev`, and publishes nothing once `dev` has changed them: the push that
changed them started a run that publishes their diagrams.
A ready pull request that changes the inputs runs the `draw` job alone, so that
a change that breaks the drawing fails before it merges, and its summary and
artifact show the diagrams that its merge will commit. The workflow is not
part of `Required status check`. To draw the diagrams locally, install
cargo-modules and Graphviz, and run `python3 scripts/module_diagrams.py`, which
writes them to `build/modules`, or with `--output` to another directory that
holds nothing but an earlier run's diagrams. Leave `doc/modules` to CI: other
Graphviz releases and fonts lay the diagrams out differently.

## Adversarial campaigns

The separate
Expand Down
Loading
Loading