From cfab34b39c76a73154e1ab5aa7274fc82fa3f501 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 12 Nov 2026 19:51:18 +0100 Subject: [PATCH 1/4] DOC: update the README --- examples/pyperf-suite/README.md | 69 +++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/examples/pyperf-suite/README.md b/examples/pyperf-suite/README.md index 5181911..50f068c 100644 --- a/examples/pyperf-suite/README.md +++ b/examples/pyperf-suite/README.md @@ -4,24 +4,69 @@ A runnable example of a benchmark suite. `bench_suite.py` holds five small functions; `bx bench` runs each one under [pyperf](https://pyperf.readthedocs.io), writes benchx result files, delivers them to a local store, and prints a table. -## Run it +## First, install benchx + +We install `benchx` with `pyperf` + +``` +$ python3 -m venv .venv && . .venv/bin/activate # Python >= 3.11, or any environment you like +$ pip install pyperf benchx +``` + +## Benchmarks + +In the simplest case, benchmarks are plain python functions. Here is an example suite +of several small microbenchmarks: + +```sh +head -20 bench_suite.py +``` + +(here we only show one benchmark function out of five in the suite). + +```text +"""Example benchmark suite: plain Python functions, one case each. + +`bx bench bench_suite.py` runs every function named bench_* or benchmark_*. +Name functions on the command line to run only those: + bx bench bench_suite.py bench_sorted bench_join + +Each function is the timed body; pyperf calls it many times. Do your setup +at module level (it runs once per worker process, outside the timing). +""" + +import os + +DATA = list(range(2000, 0, -1)) +WORDS = [f"word{i}" for i in range(500)] + + +def bench_sorted(): + sorted(DATA) +``` + +Additionally, this directory contains two additional optional files: `benchx.conf.json` +is an optional configuration file, and `run.sh` is a helper script which demonstrates. + +## Run the benchmarks + +The main CLI entry point for the `benchx` package is the `bx` executable, which +supports several subcommands. The first subcommand is `bx bench` which accepts the +name of the suite or a selection of benchmarks, and runs it: to run the whole suite, +use `$ bx bench bench_suite.py`. Here we demonstrate several `bx` subcommands, in a +shell script (see below for specific steps the script): -Install benchx in editable mode from a checkout (it reads its schemas from there), with -pyperf, then run the script from the root of this repository: ```sh -git clone https://github.com/benchx-dev/benchx.git -python3 -m venv .venv && . .venv/bin/activate # Python >= 3.11, or any environment you like -pip install -e benchx/prototype pyperf -bash examples/pyperf-suite/run.sh +bash ./run.sh ``` -The script runs `bx` and `python` from the active environment, so benchx and pyperf must -be installed there; the workers use that same `python`. Extra arguments go to +The script runs `bx` and `python` from the active environment, so `benchx` and `pyperf` +must be installed there; the workers use that same `python`. Extra arguments go to `bx bench` in step 1 and win over the config file, e.g. `./run.sh --processes 20`. -It takes about 20 seconds. Nothing outside this directory changes: the local store is -`out/store.parquet` (`paths.store` in `benchx.conf.json`), and `out/` is deleted at the start -of each run. +On a particular machine, the run takes about 20 seconds. +Nothing outside the root directory changes: the local store is `out/store.parquet` +(`paths.store` in `benchx.conf.json`). ## What goes in From 24a56bbe5f7998e808ce5e0371fa03572ff2e806 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Thu, 12 Nov 2026 20:08:00 +0100 Subject: [PATCH 2/4] Make pyperf-suite README executable --- .github/workflows/e2e.yml | 56 +++++- .gitignore | 2 + requirements-e2e.txt | 3 + scripts/execute_markdown.py | 340 ++++++++++++++++++++++++++++++++++++ 4 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 requirements-e2e.txt create mode 100644 scripts/execute_markdown.py diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index bc9a1e7..18234b4 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -2,6 +2,13 @@ name: E2E pyperf-suite on: push: + branches: [main] + paths: + - "examples/pyperf-suite/**" + - "scripts/execute_markdown.py" + - "requirements-e2e.txt" + - ".github/workflows/e2e-pyperf.yml" + pull_request: workflow_dispatch: @@ -10,8 +17,53 @@ permissions: jobs: pyperf-suite: + name: pyperf-suite (PyPI BenchX) runs-on: ubuntu-latest + timeout-minutes: 20 + steps: - - name: Bootstrap - run: echo "Workflow registered successfully" + - name: Check out benchx_examples + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: pip + cache-dependency-path: requirements-e2e.txt + + - name: Install dependencies + run: | + python -m pip install -r requirements-e2e.txt + + - name: Show environment + run: | + python --version + python -m pip show benchx pyperf + git rev-parse HEAD + + - name: Execute pyperf-suite README + run: | + python scripts/execute_markdown.py \ + --source examples/pyperf-suite/README.md \ + --output generated/pyperf-suite/README.md \ + --logs logs/pyperf-suite + + - name: Upload diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-pyperf-suite + path: logs/pyperf-suite/ + if-no-files-found: warn + retention-days: 14 + + - name: Upload generated documentation + if: success() + uses: actions/upload-artifact@v4 + with: + name: docs-pyperf-suite + path: generated/pyperf-suite/ + if-no-files-found: error + retention-days: 14 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..618b2a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +generated/ +logs/ diff --git a/requirements-e2e.txt b/requirements-e2e.txt new file mode 100644 index 0000000..8236165 --- /dev/null +++ b/requirements-e2e.txt @@ -0,0 +1,3 @@ +benchx +pyperf +markdown-it-py diff --git a/scripts/execute_markdown.py b/scripts/execute_markdown.py new file mode 100644 index 0000000..38caafc --- /dev/null +++ b/scripts/execute_markdown.py @@ -0,0 +1,340 @@ + +#!/usr/bin/env python3 +"""Execute Markdown examples and generate documentation. + +Conventions: +- sh/bash fences: execute with Bash, including multiline scripts. +- python fences: execute in a shared Python namespace. +- First text fence after an executable block: committed output + snapshot, removed from generated documentation. +- Ordinary prose may separate code and its output snapshot. +- A heading or another fence ends the output association. +- Unlabelled and other language fences: preserved unchanged. +- No output comparisons or doctesting. + +The source Markdown is never modified. Generated Markdown and +diagnostic logs are written to separate directories. +""" + +import argparse +import contextlib +import io +import os +from pathlib import Path +import re +import subprocess +import sys +import traceback + +from markdown_it import MarkdownIt + + +EXECUTABLE_LANGUAGES = {"sh", "bash", "python"} + + +def text_fence(text: str) -> str: + """Format captured output as a Markdown text fence.""" + if not text.strip(): + return "" + + # Use a fence longer than any backtick sequence in output. + runs = re.findall(r"`+", text) + marker = "`" * max(3, max(map(len, runs), default=0) + 1) + + return f"\n{marker}text\n{text.rstrip()}\n{marker}\n" + + +def execute_bash( + code: str, + workdir: Path, + logfile: Path, +) -> str: + """Execute a complete Bash block and stream output to logs.""" + chunks = [] + + with logfile.open("w", encoding="utf-8") as log: + with subprocess.Popen( + ["bash", "-e", "-o", "pipefail", "-c", code], + cwd=workdir, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, + ) as process: + assert process.stdout is not None + + for line in process.stdout: + print(line, end="", flush=True) + log.write(line) + log.flush() + chunks.append(line) + + status = process.wait() + + if status != 0: + raise RuntimeError( + f"Bash exited with status {status}; see {logfile}" + ) + + return "".join(chunks) + + +def execute_python( + code: str, + namespace: dict, + logfile: Path, + filename: str, +) -> str: + """Execute Python with shared state and capture diagnostics.""" + captured = io.StringIO() + + try: + with contextlib.redirect_stdout(captured): + with contextlib.redirect_stderr(captured): + exec(compile(code, filename, "exec"), namespace) + + except BaseException: + captured.write(traceback.format_exc()) + output = captured.getvalue() + + logfile.write_text(output, encoding="utf-8") + print(output, file=sys.stderr, flush=True) + raise + + output = captured.getvalue() + logfile.write_text(output, encoding="utf-8") + + if output: + print(output, end="", flush=True) + + return output + + +def github_annotation( + source: Path, + line: int, + message: str, +) -> None: + """Emit a GitHub Actions error annotation for a source line.""" + def escape(value: str, property: bool = False) -> str: + value = ( + str(value) + .replace("%", "%25") + .replace("\r", "%0D") + .replace("\n", "%0A") + ) + + if property: + value = ( + value.replace(":", "%3A") + .replace(",", "%2C") + ) + + return value + + print( + f"::error file={escape(source, True)}," + f"line={line}::{escape(message)}", + flush=True, + ) + + +def run( + source: Path, + output: Path, + logs: Path, +) -> None: + """Execute a Markdown file and render fresh output.""" + repository_root = Path.cwd().resolve() + + source = source.resolve() + output = output.resolve() + logs = logs.resolve() + + if source == output: + raise ValueError("Output must differ from source") + + output.parent.mkdir(parents=True, exist_ok=True) + logs.mkdir(parents=True, exist_ok=True) + + # Execute in the original example directory. + # BenchX needs the real Git checkout to record revision and + # dirty-tree information correctly. + workdir = source.parent + + original = source.read_text(encoding="utf-8") + lines = original.splitlines(keepends=True) + + tokens = MarkdownIt("commonmark").parse(original) + + # markdown-it-py identifies fences and headings. + # Do not filter on token.level: valid fenced blocks may + # have a nonzero nesting level. + events = [ + token + for token in tokens + if token.map is not None + and token.type in {"fence", "heading_open"} + ] + + rendered = [] + cursor = 0 + awaiting_output = False + block_number = 0 + + # Python blocks share variables across the document. + namespace = {"__name__": "__main__"} + + previous_cwd = Path.cwd() + + try: + os.chdir(workdir) + + for token in events: + start, end = token.map + + # Preserve all ordinary Markdown between events. + rendered.extend(lines[cursor:start]) + + if token.type == "heading_open": + awaiting_output = False + rendered.extend(lines[start:end]) + + elif token.type == "fence": + parts = token.info.strip().split() + language = parts[0] if parts else "" + + executable = ( + language in EXECUTABLE_LANGUAGES + and "noexec" not in parts[1:] + ) + + if language == "text" and awaiting_output: + # Remove the first committed text-output + # snapshot after an executable block. + # + # Ordinary prose may intervene, but a heading + # or another fence ends the association. + # + # Fresh output has already been inserted + # immediately after the executable block. + # + # We never compare committed and fresh output. + awaiting_output = False + + elif executable: + block_number += 1 + line = start + 1 + logfile = logs / f"block-{block_number:03d}.log" + + # Preserve original code fence in rendered docs. + rendered.extend(lines[start:end]) + + print( + f"\n[{block_number}] " + f"{source}:{line} ({language})", + flush=True, + ) + + try: + if language in {"sh", "bash"}: + result = execute_bash( + token.content, + workdir, + logfile, + ) + else: + result = execute_python( + token.content, + namespace, + logfile, + f"{source}:{line}", + ) + + except BaseException as exc: + try: + relative_source = ( + source.relative_to(repository_root) + ) + except ValueError: + relative_source = source + + github_annotation( + relative_source, + line, + f"{language} block failed: {exc}", + ) + + print( + f"Failed block {block_number}. " + f"Diagnostics: {logfile}", + file=sys.stderr, + ) + raise + + # Insert fresh text output after executable code. + # Empty output does not produce a text fence. + rendered.append(text_fence(result)) + + awaiting_output = True + + else: + # Preserve unlabelled, json, yaml, noexec, + # and other non-executable fences unchanged. + rendered.extend(lines[start:end]) + awaiting_output = False + + cursor = end + + rendered.extend(lines[cursor:]) + + # CI must not pass if no examples were actually executed. + if block_number == 0: + raise RuntimeError( + f"No executable blocks found in {source}. " + "Expected sh, bash, or python fences." + ) + + # Write generated Markdown only after successful execution. + output.write_text( + "".join(rendered), + encoding="utf-8", + ) + + finally: + os.chdir(previous_cwd) + + print(f"\nGenerated documentation: {output}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Execute Markdown examples and capture outputs" + ) + + parser.add_argument( + "--source", + type=Path, + required=True, + help="Source Markdown file", + ) + parser.add_argument( + "--output", + type=Path, + required=True, + help="Generated Markdown file", + ) + parser.add_argument( + "--logs", + type=Path, + required=True, + help="Directory for per-block diagnostic logs", + ) + + args = parser.parse_args() + run(args.source, args.output, args.logs) + + +if __name__ == "__main__": + main() + From 302887af59eaea875b3aa25b52c1a5445d8d30cc Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 14 Nov 2026 16:37:09 +0000 Subject: [PATCH 3/4] ENH: make the workflow reusable --- .github/workflows/e2e.yml | 64 ++--------- .github/workflows/reusable-example.yml | 153 +++++++++++++++++++++++++ examples/pyperf-suite/README.md | 17 +-- requirements-e2e.txt | 3 - scripts/execute_markdown.py | 2 - 5 files changed, 173 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/reusable-example.yml delete mode 100644 requirements-e2e.txt diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 18234b4..0f000e7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -1,13 +1,15 @@ -name: E2E pyperf-suite +name: E2E +# One job per example, each calling the reusable workflow. The `paths` filter below applies +# to the whole workflow, so extend it when an example is added. on: push: branches: [main] paths: - "examples/pyperf-suite/**" - "scripts/execute_markdown.py" - - "requirements-e2e.txt" - - ".github/workflows/e2e-pyperf.yml" + - ".github/workflows/e2e.yml" + - ".github/workflows/reusable-example.yml" pull_request: workflow_dispatch: @@ -17,53 +19,9 @@ permissions: jobs: pyperf-suite: - name: pyperf-suite (PyPI BenchX) - runs-on: ubuntu-latest - timeout-minutes: 20 - - steps: - - name: Check out benchx_examples - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.13" - cache: pip - cache-dependency-path: requirements-e2e.txt - - - name: Install dependencies - run: | - python -m pip install -r requirements-e2e.txt - - - name: Show environment - run: | - python --version - python -m pip show benchx pyperf - git rev-parse HEAD - - - name: Execute pyperf-suite README - run: | - python scripts/execute_markdown.py \ - --source examples/pyperf-suite/README.md \ - --output generated/pyperf-suite/README.md \ - --logs logs/pyperf-suite - - - name: Upload diagnostics - if: always() - uses: actions/upload-artifact@v4 - with: - name: logs-pyperf-suite - path: logs/pyperf-suite/ - if-no-files-found: warn - retention-days: 14 - - - name: Upload generated documentation - if: success() - uses: actions/upload-artifact@v4 - with: - name: docs-pyperf-suite - path: generated/pyperf-suite/ - if-no-files-found: error - retention-days: 14 - + uses: ./.github/workflows/reusable-example.yml + with: + example: pyperf-suite + benchx-source: pypi + examples-repository: ${{ github.repository }} + examples-ref: ${{ github.sha }} diff --git a/.github/workflows/reusable-example.yml b/.github/workflows/reusable-example.yml new file mode 100644 index 0000000..010902e --- /dev/null +++ b/.github/workflows/reusable-example.yml @@ -0,0 +1,153 @@ +name: Reusable example E2E + +# Executes examples//README.md with scripts/execute_markdown.py and uploads +# three artifacts: logs-, docs- (the generated page) and results- +# (the example's out/ directory). +# +# benchx-source: pypi the released BenchX; used by the benchx_examples own CI +# benchx-source: checkout the calling repository's checkout, which must be benchx; +# used by benchx's CI +# +# The `example` input generalizes paths and artifact names, not the environment: the +# install steps below fit examples that need only benchx, pyperf and the runner. Add +# example-specific setup here, or a specialized workflow, as more examples are migrated. + +on: + workflow_call: + inputs: + example: + description: Directory name under examples/ + required: true + type: string + benchx-source: + description: "Where BenchX comes from: pypi or checkout" + required: true + type: string + examples-ref: + description: Branch, tag or commit of benchx_examples to run + required: true + type: string + examples-repository: + description: >- + Repository to take the examples from. The benchx_examples own workflow passes + github.repository, so that runs in a fork use the fork's commits. + required: false + type: string + default: benchx-dev/benchx_examples + +permissions: + contents: read + +jobs: + example: + name: BenchX from ${{ inputs.benchx-source }} + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + EXAMPLE: ${{ inputs.example }} + BENCHX_SOURCE: ${{ inputs.benchx-source }} + + steps: + - name: Validate inputs + run: | + case "$BENCHX_SOURCE" in + pypi|checkout) ;; + *) echo "Invalid benchx-source" >&2; exit 1 ;; + esac + case "$EXAMPLE" in + ""|*[!a-zA-Z0-9_-]*) echo "Invalid example name" >&2; exit 1 ;; + esac + + - name: Check out benchx_examples + uses: actions/checkout@v4 + with: + repository: ${{ inputs.examples-repository }} + ref: ${{ inputs.examples-ref }} + path: benchx_examples + persist-credentials: false + + # No `repository`: this is the calling repository, which is benchx in this mode. + - name: Check out benchx + if: inputs.benchx-source == 'checkout' + uses: actions/checkout@v4 + with: + path: benchx + persist-credentials: false + + # Only the checkout mode has a lock file to key the cache on. + - uses: astral-sh/setup-uv@v5 + with: + enable-cache: ${{ inputs.benchx-source == 'checkout' }} + cache-dependency-glob: benchx/uv.lock + + # The example scripts run plain `python` and `bx`, and the benchmark workers use the + # same `python`, so the environment goes on the PATH. + - name: Install BenchX from PyPI + if: inputs.benchx-source == 'pypi' + run: | + uv python install 3.13 + uv venv --python 3.13 "$RUNNER_TEMP/venv" + uv pip install --python "$RUNNER_TEMP/venv/bin/python" benchx pyperf markdown-it-py + echo "$RUNNER_TEMP/venv/bin" >> "$GITHUB_PATH" + + # An editable install, which is where BenchX reads its schemas from. The `test` + # extra brings pyperf. + - name: Install BenchX from the checkout + if: inputs.benchx-source == 'checkout' + working-directory: benchx + run: | + uv python install 3.13 + uv sync --extra test --python 3.13 + uv pip install --python .venv/bin/python markdown-it-py + echo "$PWD/.venv/bin" >> "$GITHUB_PATH" + + # `benchx` should be under site-packages for pypi and under benchx/src for checkout. + - name: Show what is under test + run: | + echo "python: $(python --version) ($(command -v python))" + echo "bx: $(command -v bx)" + echo "benchx: $(python -c 'import benchx; print(benchx.__file__)')" + echo "pyperf: $(python -c 'import pyperf; print(pyperf.__version__)')" + if [ -d benchx ]; then + echo "benchx checkout: $(git -C benchx rev-parse --short HEAD)" + fi + echo "benchx_examples: $(git -C benchx_examples rev-parse --short HEAD)" + + # Run from the root of the examples checkout, so that annotations carry a path + # relative to the repository. The generated page and the logs go outside it: BenchX + # records the state of the whole examples tree, and new files in it could make + # later results dirty. + - name: Execute example + working-directory: benchx_examples + run: | + python scripts/execute_markdown.py \ + --source "examples/$EXAMPLE/README.md" \ + --output "../generated/$EXAMPLE/$EXAMPLE.md" \ + --logs "../logs/$EXAMPLE" + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-${{ inputs.example }} + path: logs/${{ inputs.example }}/ + if-no-files-found: warn + retention-days: 14 + + - name: Upload generated documentation + if: success() + uses: actions/upload-artifact@v4 + with: + name: docs-${{ inputs.example }} + path: generated/${{ inputs.example }}/ + if-no-files-found: error + retention-days: 14 + + - name: Upload benchmark results + if: always() + uses: actions/upload-artifact@v4 + with: + name: results-${{ inputs.example }} + path: benchx_examples/examples/${{ inputs.example }}/out/ + if-no-files-found: ignore + retention-days: 14 diff --git a/examples/pyperf-suite/README.md b/examples/pyperf-suite/README.md index 50f068c..9fd6814 100644 --- a/examples/pyperf-suite/README.md +++ b/examples/pyperf-suite/README.md @@ -16,14 +16,12 @@ $ pip install pyperf benchx ## Benchmarks In the simplest case, benchmarks are plain python functions. Here is an example suite -of several small microbenchmarks: +of several small microbenchmarks (here we only show one function out of five in the suite): ```sh head -20 bench_suite.py ``` -(here we only show one benchmark function out of five in the suite). - ```text """Example benchmark suite: plain Python functions, one case each. @@ -45,8 +43,9 @@ def bench_sorted(): sorted(DATA) ``` -Additionally, this directory contains two additional optional files: `benchx.conf.json` -is an optional configuration file, and `run.sh` is a helper script which demonstrates. +Additionally, this directory contains two additional files: `benchx.conf.json` +is an optional configuration file, and `run.sh` is a helper script which demonstrates +basic CLI commands. ## Run the benchmarks @@ -148,7 +147,8 @@ bench_join 3.585 us 3.543 us ± 272.4 ns 3.236 us .. 3.997 us 15 suc underneath. A `[warning]` after the status means the result carries a quality warning, such as `env-mismatch`. -Files, under `out/all/` and `out/env/`: +Files, under `out/all/`, `out/env/` and `out/again/` (one directory for each of the first +three `bx bench` calls in `run.sh`): - `__wall-time.json`: one benchx result per case (schema `measurement-result`; with `-e` the name also carries the percent-encoded parameters, as in @@ -165,8 +165,9 @@ Files, under `out/all/` and `out/env/`: script that ran the function; `.worker-env.json`: the environment a worker really had; `.stdout` and `.stderr`. -The local store, `out/home/store.parquet`, holds the results. `run.sh` reads it back -(your numbers and fingerprints will differ): +The local store, `out/store.parquet`, holds the results. `run.sh` reads it back, and +keeps what `bx series` and `bx history` print in `out/series.txt`, `out/join-series.txt` +and `out/history.txt` (your numbers and fingerprints will differ): ``` $ bx series --quantity wall-time diff --git a/requirements-e2e.txt b/requirements-e2e.txt deleted file mode 100644 index 8236165..0000000 --- a/requirements-e2e.txt +++ /dev/null @@ -1,3 +0,0 @@ -benchx -pyperf -markdown-it-py diff --git a/scripts/execute_markdown.py b/scripts/execute_markdown.py index 38caafc..97de13a 100644 --- a/scripts/execute_markdown.py +++ b/scripts/execute_markdown.py @@ -1,5 +1,3 @@ - -#!/usr/bin/env python3 """Execute Markdown examples and generate documentation. Conventions: From 0d1c47b99c1a2b6b9c7b8a5cd847e031ec167f67 Mon Sep 17 00:00:00 2001 From: Evgeni Burovski Date: Sat, 14 Nov 2026 16:49:21 +0000 Subject: [PATCH 4/4] CI: use hashes not tags for third-party GH actions --- .github/workflows/reusable-example.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reusable-example.yml b/.github/workflows/reusable-example.yml index 010902e..02600d8 100644 --- a/.github/workflows/reusable-example.yml +++ b/.github/workflows/reusable-example.yml @@ -59,7 +59,7 @@ jobs: esac - name: Check out benchx_examples - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: ${{ inputs.examples-repository }} ref: ${{ inputs.examples-ref }} @@ -69,13 +69,13 @@ jobs: # No `repository`: this is the calling repository, which is benchx in this mode. - name: Check out benchx if: inputs.benchx-source == 'checkout' - uses: actions/checkout@v4 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: path: benchx persist-credentials: false # Only the checkout mode has a lock file to key the cache on. - - uses: astral-sh/setup-uv@v5 + - uses: astral-sh/setup-uv@68559e99cc807c1eb7e9238fd43a15bba460eec2 # v10.3.3 with: enable-cache: ${{ inputs.benchx-source == 'checkout' }} cache-dependency-glob: benchx/uv.lock @@ -127,7 +127,7 @@ jobs: - name: Upload logs if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: logs-${{ inputs.example }} path: logs/${{ inputs.example }}/ @@ -136,7 +136,7 @@ jobs: - name: Upload generated documentation if: success() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: docs-${{ inputs.example }} path: generated/${{ inputs.example }}/ @@ -145,7 +145,7 @@ jobs: - name: Upload benchmark results if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: results-${{ inputs.example }} path: benchx_examples/examples/${{ inputs.example }}/out/