From 0d8aaad1c3aa4f7d4416db79a395a6608976fddb Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:55:03 -0400 Subject: [PATCH 1/9] Replace org app token and reusable workflows with composite actions - Drop the GitHub App token everywhere: automation runs on the scoped `GITHUB_TOKEN` with job-level `permissions` blocks and fail-closed workflow defaults. repo-sync falls back to opening an issue asking for a manual sync when a push touches workflow files, which the token cannot write. - Replace JasonEtco/create-an-issue and peter-evans/create-issue-from-file with a first-party `create-issue` action: stdlib Python around the gh CLI that dedupes by title and updates the existing open issue. - Convert reusable workflows to composite actions: repo-sync, rust-version-check, unused-deps, links-check, typos, lints, msrv, wasm, codecov, gpu-ci (cuda and opencl merged behind a `gpu-framework` input), and gpu-bench. Keep docs and licenses-audits as reusable workflows since they take no configuration. Remove check-lurk-compiles and bench-pr-comment, superseded in ix. - Reference sibling actions and same-repo actions with `$/` self-repository refs, deleting all ci-workflows self-checkouts along with their default-branch version skew; actionlint gets an ignore for its `$/` false positive (rhysd/actionlint#711), and zizmor's self-repository audit endorses the syntax. - Standardize on actions-rust-lang/setup-rust-toolchain: Rust CI actions assume the caller provisions the toolchain and cache (or a wrapper like ix's), while release-pr and typos provision their own since Rust is incidental there. ci-env is gone; the toolchain action covers its env vars. - gpu-bench reports regressions via a step output because composite steps silently ignore `continue-on-error`; also fix its NUM_VCPUS typo and release-pr's unguarded `cd`s. Callers migrate from `uses: .../.github/workflows/.yml@main` to job steps calling `.../.github/actions/@`; each action description documents its required permissions and setup assumptions. --- .github/actions/ci-env/action.yml | 38 ---- .github/actions/codecov/action.yml | 37 ++++ .github/actions/create-issue/action.yml | 49 +++++ .github/actions/create-issue/create_issue.py | 56 +++++ .github/actions/gpu-bench/action.yml | 165 ++++++++++++++ .github/actions/gpu-ci/action.yml | 50 +++++ .github/actions/links-check/action.yml | 37 ++++ .github/actions/lint-workflows/action.yml | 4 + .github/actions/lints/action.yml | 50 +++++ .github/actions/msrv/action.yml | 26 +++ .github/actions/release-pr/action.yml | 13 +- .github/actions/repo-sync/action.yml | 49 +++++ .github/actions/rust-version-check/action.yml | 52 +++++ .github/actions/typos/action.yml | 73 +++++++ .github/actions/unused-deps/action.yml | 53 +++++ .github/actions/wasm/action.yml | 23 ++ .github/templates/UNUSED_DEPS_ISSUE.md | 15 -- .github/templates/VERSION_CHECK.md | 10 - .github/workflows/actions-lint.yml | 2 +- .github/workflows/bench-pr-comment.yml | 206 ------------------ .github/workflows/check-lurk-compiles.yml | 57 ----- .github/workflows/codecov.yml | 51 ----- .github/workflows/docs.yml | 12 +- .github/workflows/gpu-bench.yml | 169 -------------- .github/workflows/gpu-ci-cuda.yml | 51 ----- .github/workflows/gpu-ci-opencl.yml | 51 ----- .github/workflows/licenses-audits.yml | 4 + .github/workflows/links-check.yml | 38 ---- .github/workflows/lints.yml | 57 ----- .github/workflows/msrv.yml | 35 --- .github/workflows/repo-sync.yml | 46 ---- .github/workflows/rust-version-check.yml | 53 ----- .github/workflows/typos.yml | 73 ------- .github/workflows/unused-deps.yml | 57 ----- .github/workflows/wasm.yml | 30 --- 35 files changed, 745 insertions(+), 1047 deletions(-) delete mode 100644 .github/actions/ci-env/action.yml create mode 100644 .github/actions/codecov/action.yml create mode 100644 .github/actions/create-issue/action.yml create mode 100644 .github/actions/create-issue/create_issue.py create mode 100644 .github/actions/gpu-bench/action.yml create mode 100644 .github/actions/gpu-ci/action.yml create mode 100644 .github/actions/links-check/action.yml create mode 100644 .github/actions/lints/action.yml create mode 100644 .github/actions/msrv/action.yml create mode 100644 .github/actions/repo-sync/action.yml create mode 100644 .github/actions/rust-version-check/action.yml create mode 100644 .github/actions/typos/action.yml create mode 100644 .github/actions/unused-deps/action.yml create mode 100644 .github/actions/wasm/action.yml delete mode 100644 .github/templates/UNUSED_DEPS_ISSUE.md delete mode 100644 .github/templates/VERSION_CHECK.md delete mode 100644 .github/workflows/bench-pr-comment.yml delete mode 100644 .github/workflows/check-lurk-compiles.yml delete mode 100644 .github/workflows/codecov.yml delete mode 100644 .github/workflows/gpu-bench.yml delete mode 100644 .github/workflows/gpu-ci-cuda.yml delete mode 100644 .github/workflows/gpu-ci-opencl.yml delete mode 100644 .github/workflows/links-check.yml delete mode 100644 .github/workflows/lints.yml delete mode 100644 .github/workflows/msrv.yml delete mode 100644 .github/workflows/repo-sync.yml delete mode 100644 .github/workflows/rust-version-check.yml delete mode 100644 .github/workflows/typos.yml delete mode 100644 .github/workflows/unused-deps.yml delete mode 100644 .github/workflows/wasm.yml diff --git a/.github/actions/ci-env/action.yml b/.github/actions/ci-env/action.yml deleted file mode 100644 index a577a08..0000000 --- a/.github/actions/ci-env/action.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: CI env setup - -description: Set Rust env vars - -runs: - using: "composite" - steps: - # `cargo` and `rustc` read these from the environment, so they must be - # exported for the caller's later steps rather than passed as step outputs. - # Every value written here is a literal. - - run: | # zizmor: ignore[github-env] - echo "CARGO_TERM_COLOR=always" | tee -a $GITHUB_ENV - - # Disable incremental compilation. - # - # Incremental compilation is useful as part of an edit-build-test-edit cycle, - # as it lets the compiler avoid recompiling code that hasn't changed. However, - # on CI, we're not making small edits; we're almost always building the entire - # project from scratch. Thus, incremental compilation on CI actually - # introduces *additional* overhead to support making future builds - # faster...but no future builds will ever occur in any given CI environment. - # - # See https://matklad.github.io/2021/09/04/fast-rust-builds.html#ci-workflow - # for details. - echo "CARGO_INCREMENTAL=0" | tee -a $GITHUB_ENV - - # Allow more retries for network requests in cargo (downloading crates) and - # rustup (installing toolchains). This should help to reduce flaky CI failures - # from transient network timeouts or other issues. - echo "CARGO_NET_RETRY=10" | tee -a $GITHUB_ENV - - echo "CARGO_MAX_RETRIES=10" | tee -a $GITHUB_ENV - - # Don't emit giant backtraces in the CI logs. - echo "RUST_BACKTRACE=short" | tee -a $GITHUB_ENV - - echo "RUSTFLAGS=-D warnings" | tee -a $GITHUB_ENV - shell: bash diff --git a/.github/actions/codecov/action.yml b/.github/actions/codecov/action.yml new file mode 100644 index 0000000..1de3e65 --- /dev/null +++ b/.github/actions/codecov/action.yml @@ -0,0 +1,37 @@ +name: Generate and deploy Codecov results +description: >- + Collect llvm-cov coverage data and upload it to Codecov. Assumes the repo + is already checked out with `submodules: recursive` and a Rust toolchain + with caching is already set up, e.g. via + `actions-rust-lang/setup-rust-toolchain`. + +inputs: + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' +runs: + using: composite + steps: + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - shell: bash + run: rustup component add llvm-tools-preview + - uses: taiki-e/install-action@nextest + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Clean the workspace + shell: bash + run: cargo llvm-cov clean --workspace + - name: Build + shell: bash + run: cargo build --workspace --release + - name: Collect coverage data + shell: bash + run: cargo llvm-cov nextest --lcov --output-path lcov.info --profile ci --release --workspace + - name: Upload coverage data to codecov + uses: codecov/codecov-action@v7 + with: + files: lcov.info diff --git a/.github/actions/create-issue/action.yml b/.github/actions/create-issue/action.yml new file mode 100644 index 0000000..fd266cf --- /dev/null +++ b/.github/actions/create-issue/action.yml @@ -0,0 +1,49 @@ +name: Create or update issue +description: + Create an issue in the current repository, updating the body of an existing + open issue with the same title instead of filing a duplicate. Requires + `issues` write permission and the gh CLI on the runner. + +inputs: + title: + description: Issue title, also used to match an existing open issue + required: true + body: + description: + Issue body text. Exactly one of `body` and `body-file` must be set. + required: false + default: '' + body-file: + description: + Path to a file containing the issue body, e.g. a generated report + required: false + default: '' + labels: + description: + Comma-separated labels to apply on creation. Best-effort — a label that + cannot be applied logs a warning instead of failing. + required: false + default: '' + update-existing: + description: Update the existing open issue's body instead of skipping it + required: false + default: 'true' + token: + description: GitHub token with `issues` write access + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + - name: Create or update issue + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + INPUT_TITLE: ${{ inputs.title }} + INPUT_BODY: ${{ inputs.body }} + INPUT_BODY_FILE: ${{ inputs.body-file }} + INPUT_LABELS: ${{ inputs.labels }} + INPUT_UPDATE_EXISTING: ${{ inputs.update-existing }} + ACTION_PATH: ${{ github.action_path }} + run: python3 "$ACTION_PATH/create_issue.py" diff --git a/.github/actions/create-issue/create_issue.py b/.github/actions/create-issue/create_issue.py new file mode 100644 index 0000000..9499c56 --- /dev/null +++ b/.github/actions/create-issue/create_issue.py @@ -0,0 +1,56 @@ +"""Create or update a GitHub issue via the gh CLI. + +Reads its configuration from the INPUT_* environment variables set in +action.yml. +""" + +import json +import os +import subprocess +import sys + + +def gh(*args): + return subprocess.run( + ["gh", *args], check=True, text=True, stdout=subprocess.PIPE + ).stdout + + +def main(): + repo = os.environ["GITHUB_REPOSITORY"] + title = os.environ["INPUT_TITLE"] + labels = os.environ.get("INPUT_LABELS", "") + update_existing = os.environ.get("INPUT_UPDATE_EXISTING", "true") == "true" + + body = os.environ.get("INPUT_BODY", "") + body_file = os.environ.get("INPUT_BODY_FILE", "") + if bool(body) == bool(body_file): + sys.exit("::error::Set exactly one of `body` and `body-file`") + if body_file: + with open(body_file) as f: + body = f.read() + + issues = json.loads( + gh("issue", "list", "--repo", repo, "--state", "open", + "--limit", "100", "--json", "number,title") + ) + existing = [issue["number"] for issue in issues if issue["title"] == title] + if existing: + if update_existing: + gh("issue", "edit", str(existing[0]), "--repo", repo, "--body", body) + print(f"Updated existing issue #{existing[0]}") + else: + print(f"Open issue #{existing[0]} already exists, skipping") + return + + url = gh("issue", "create", "--repo", repo, "--title", title, "--body", body).strip() + print(f"Created issue {url}") + if labels: + try: + gh("issue", "edit", url, "--repo", repo, "--add-label", labels) + except subprocess.CalledProcessError: + print(f"::warning::Could not apply labels {labels!r}") + + +if __name__ == "__main__": + main() diff --git a/.github/actions/gpu-bench/action.yml b/.github/actions/gpu-bench/action.yml new file mode 100644 index 0000000..19c669f --- /dev/null +++ b/.github/actions/gpu-bench/action.yml @@ -0,0 +1,165 @@ +name: Comparative benchmarks on GPU +description: >- + Run comparative criterion benchmarks against the base branch on a CUDA GPU, + posting the results as a commit comment. On a regression >= 10%, open an + issue instead of committing the bench result to `gh-pages`; the merge is + not blocked either way. Performs its own checkouts, so no prior checkout is + needed, but assumes a Rust toolchain with caching is already set up, e.g. + via `actions-rust-lang/setup-rust-toolchain`. Run on the `merge_group` trigger only, from a job with a + self-hosted Nvidia GPU runner (typically labeled `gpu-bench`) and + `contents` and `issues` write permissions — `contents: write` covers the + `gh-pages` push and the commit comment. Prerequisites — `cuda` Cargo + features, benchmarks formatted for `criterion-table` via + `${REPOSITORY_NAME}_BENCH_OUTPUT=commit-comment` (e.g. + `$LURK_BENCH_OUTPUT=commit-comment`), and a pre-existing `gh-pages` branch. + +inputs: + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' +runs: + using: composite + steps: + - uses: $/.github/actions/gpu-setup + with: + gpu-framework: 'cuda' + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + # `git-auto-commit-action` below pushes to `gh-pages` with the credentials + # this checkout persists, so they cannot be disabled here. + - uses: actions/checkout@v7 # zizmor: ignore[artipacked] + - name: Install criterion + shell: bash + run: | + cargo install cargo-criterion + cargo install criterion-table + - name: Set env vars + shell: bash + run: | + REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F'/' '{ print toupper($2) }') + echo "${REPOSITORY_NAME}_BENCH_OUTPUT=commit-comment" | tee -a $GITHUB_ENV + echo "BASE_COMMIT=${{ github.event.merge_group.base_sha }}" | tee -a $GITHUB_ENV + echo "GPU_ID=$(echo $GPU_NAME | awk '{ print $NF }')" | tee -a $GITHUB_ENV + # Checkout gh-pages to check for cached bench result + - name: Checkout gh-pages + uses: actions/checkout@v7 + with: + ref: gh-pages + path: gh-pages + persist-credentials: false + - name: Check for cached bench result + id: cached-bench + shell: bash + run: | + if [ -f "$BASE_COMMIT-$GPU_ID.json" ] + then + echo "cached=true" | tee -a $GITHUB_OUTPUT + cp "$BASE_COMMIT-$GPU_ID.json" "../$BASE_COMMIT.json" + else + echo "cached=false" | tee -a $GITHUB_OUTPUT + fi + working-directory: ${{ github.workspace }}/gh-pages + # Checkout base branch for comparative bench + - uses: actions/checkout@v7 + if: steps.cached-bench.outputs.cached == 'false' + with: + ref: ${{ github.base_ref }} + path: ${{ github.base_ref }} + persist-credentials: false + - name: Run GPU bench on base branch + if: steps.cached-bench.outputs.cached == 'false' + shell: bash + run: | + # Run benchmark + cargo criterion --features "cuda" --message-format=json > "$BASE_COMMIT.json" + # Copy bench output to PR branch + cp "$BASE_COMMIT.json" .. + working-directory: ${{ github.workspace }}/${{ github.base_ref }} + - name: Run GPU bench on PR branch + shell: bash + run: | + cargo criterion --features "cuda" --message-format=json > ${{ github.sha }}.json + cp ${{ github.sha }}.json .. + working-directory: ${{ github.workspace }}/benches + - name: copy the benchmark template and prepare it with data + shell: bash + run: | + cp .github/tables.toml . + # Get CPU model + CPU_MODEL=$(grep '^model name' /proc/cpuinfo | head -1 | awk -F ': ' '{ print $2 }') + # Get num vCPUS + NUM_VCPUS="$(nproc --all) vCPUs" + # Get total RAM in GB + TOTAL_RAM=$(grep MemTotal /proc/meminfo | awk '{$2=$2/(1024^2); print int($2), "GB RAM";}') + + # Use conditionals to ensure that only non-empty variables are inserted + [[ ! -z "$GPU_NAME" ]] && sed -i "/^\"\"\"$/i $GPU_NAME" tables.toml + [[ ! -z "$CPU_MODEL" ]] && sed -i "/^\"\"\"$/i $CPU_MODEL" tables.toml + [[ ! -z "$NUM_VCPUS" ]] && sed -i "/^\"\"\"$/i $NUM_VCPUS" tables.toml + [[ ! -z "$TOTAL_RAM" ]] && sed -i "/^\"\"\"$/i $TOTAL_RAM" tables.toml + sed -i "/^\"\"\"$/i Workflow run: $GITHUB_SERVER_URL/$REPO/actions/runs/$RUN_ID" tables.toml + working-directory: ${{ github.workspace }} + env: + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + # Create a `criterion-table` and write in commit comment + - name: Run `criterion-table` + shell: bash + run: cat "$BASE_COMMIT.json" "$GITHUB_SHA.json" | criterion-table > BENCHMARKS.md + - name: Write bench on commit comment + uses: peter-evans/commit-comment@v4 + with: + body-path: BENCHMARKS.md + # Check for a slowdown >= 10%. If so, open an issue but don't block merge. + # Reported as a step output because `continue-on-error` is silently + # ignored on composite action steps, so a failing step would abort here. + - name: Check for perf regression + id: regression-check + shell: bash + run: | + regressions=$(awk -F'[*x]' '/slower/{print $12}' BENCHMARKS.md) + + echo $regressions + + regression=false + for r in $regressions + do + if (( $(echo "$r >= 1.10" | bc -l) )) + then + regression=true + fi + done + echo "regression=$regression" | tee -a $GITHUB_OUTPUT + # Not possible to use ${{ github.event.number }} with the `merge_group` trigger + - name: Get PR number from merge branch + shell: bash + env: + HEAD_REF: ${{ github.event.merge_group.head_ref }} + run: | + echo "PR_NUMBER=$(echo "$HEAD_REF" | sed -e 's/.*pr-\(.*\)-.*/\1/')" | tee -a $GITHUB_ENV + - name: Open issue on regression + if: steps.regression-check.outputs.regression == 'true' + uses: $/.github/actions/create-issue + with: + title: ':rotating_light: Performance regression detected for PR #${{ env.PR_NUMBER }}' + labels: P-Performance,automated issue + body: | + Regression >= 10% found during merge for PR #${{ env.PR_NUMBER }} + Commit: ${{ github.sha }} + Workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + - name: Remove old base bench + shell: bash + run: | + rm "$BASE_COMMIT.json" + mv "$GITHUB_SHA.json" "$GITHUB_SHA-$GPU_ID.json" + working-directory: ${{ github.workspace }} + - name: Commit bench result to `gh-pages` branch if no regression + if: steps.regression-check.outputs.regression != 'true' + uses: stefanzweifel/git-auto-commit-action@v7 + with: + branch: gh-pages + commit_message: '[automated] GPU Benchmark from PR #${{ env.PR_NUMBER }}' + file_pattern: '${{ github.sha }}-${{ env.GPU_ID }}.json' diff --git a/.github/actions/gpu-ci/action.yml b/.github/actions/gpu-ci/action.yml new file mode 100644 index 0000000..26499de --- /dev/null +++ b/.github/actions/gpu-ci/action.yml @@ -0,0 +1,50 @@ +name: GPU CI tests +description: >- + Rust tests on a CUDA or OpenCL GPU. Assumes the repo is already checked out + with `submodules: recursive` and a Rust toolchain with caching is already + set up, e.g. via `actions-rust-lang/setup-rust-toolchain`. Prerequisites — a self-hosted Nvidia GPU + runner with CUDA enabled attached to the caller repo (typically labeled + `gpu-ci`), and the `cuda` (plus `opencl` if selected) Cargo features. We + expect dependents to run this on the `pull_request` and `merge_group` + triggers, gating the job with `if: github.event_name == 'merge_group'` so + it shows as a skipped status check on the PR, then runs once on the merge + queue's merge commit when attempting to merge. + +inputs: + gpu-framework: + description: GPU framework to test, either 'cuda' or 'opencl' + required: false + default: 'cuda' + features: + description: + Comma-separated list of features to run in addition to the GPU framework + features + required: false + default: '' + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' +runs: + using: composite + steps: + - uses: $/.github/actions/gpu-setup + with: + gpu-framework: ${{ inputs.gpu-framework }} + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - uses: taiki-e/install-action@nextest + - name: GPU tests + shell: bash + env: + FEATURES: ${{ inputs.features }} + GPU_FRAMEWORK: ${{ inputs.gpu-framework }} + run: | + if [[ "$GPU_FRAMEWORK" == "opencl" ]]; then + gpu_features="cuda,opencl" + else + gpu_features="cuda" + fi + cargo nextest run --profile ci --cargo-profile dev-ci --features "$gpu_features,$FEATURES" diff --git a/.github/actions/links-check/action.yml b/.github/actions/links-check/action.yml new file mode 100644 index 0000000..111f50e --- /dev/null +++ b/.github/actions/links-check/action.yml @@ -0,0 +1,37 @@ +name: Check documentation links +description: >- + Run lychee over the repo's documentation links. If `fail-fast` is 'false', + opens an issue with the report instead of failing the job. Assumes the repo + is already checked out. The calling job needs `issues: write` when + `fail-fast` is 'false'. + +inputs: + fail-fast: + description: Whether to error on failure instead of opening an issue + required: false + default: 'true' + token: + description: GitHub token, needs `issues` write access when `fail-fast` is 'false' + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2.9.0 + with: + fail: ${{ inputs.fail-fast }} + env: + GITHUB_TOKEN: ${{ inputs.token }} + # lychee stopped exporting `lychee_exit_code` to the environment in v2; + # the exit code is only available as a step output. + - name: Open issue on failure if `fail-fast` input is false + if: steps.lychee.outputs.exit_code != 0 && inputs.fail-fast != 'true' + uses: $/.github/actions/create-issue + with: + token: ${{ inputs.token }} + title: Link Checker Report + body-file: ./lychee/out.md + labels: report,automated issue diff --git a/.github/actions/lint-workflows/action.yml b/.github/actions/lint-workflows/action.yml index 9f9c8c5..4bf34c5 100644 --- a/.github/actions/lint-workflows/action.yml +++ b/.github/actions/lint-workflows/action.yml @@ -21,6 +21,10 @@ runs: - uses: raven-actions/actionlint@v2 env: SHELLCHECK_OPTS: -S ${{ inputs.shellcheck-severity }} + with: + # actionlint doesn't know the `$/` self-repository syntax yet and + # misparses it as {owner}/{repo}@{ref} + flags: -ignore 'specifying action "\$/.+" in invalid format' # Note that `continue-on-error` is silently ignored on composite action # steps, so any zizmor finding fails the calling job. Suppress findings # that don't apply via `.github/zizmor.yml` or `# zizmor: ignore` comments. diff --git a/.github/actions/lints/action.yml b/.github/actions/lints/action.yml new file mode 100644 index 0000000..64bdedf --- /dev/null +++ b/.github/actions/lints/action.yml @@ -0,0 +1,50 @@ +name: Check lints and code quality +description: + Rustfmt, clippy, and doctests. Assumes the repo is already checked out and + a Rust toolchain with caching is already set up, e.g. via + `actions-rust-lang/setup-rust-toolchain`. + +inputs: + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' + nightly-fmt: + description: + Runs `cargo fmt +nightly`, for use with nightly config options in + `rustfmt.toml` + required: false + default: 'false' +runs: + using: composite + steps: + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - shell: bash + run: rustup component add rustfmt clippy + - if: inputs.nightly-fmt == 'true' + shell: bash + run: rustup toolchain install nightly --component rustfmt + - name: Check Rustfmt Code Style + shell: bash + env: + NIGHTLY_FMT: ${{ inputs.nightly-fmt }} + run: | + if [[ "$NIGHTLY_FMT" == "true" ]]; then + cargo +nightly fmt --all -- --check + else + cargo fmt --all -- --check + fi + - name: Check clippy warnings + shell: bash + run: | + if cargo --list | grep -q xclippy; then + cargo xclippy -Dwarnings + else + cargo clippy -Dwarnings + fi + - name: Doctests + shell: bash + run: cargo test --doc --workspace diff --git a/.github/actions/msrv/action.yml b/.github/actions/msrv/action.yml new file mode 100644 index 0000000..00504ab --- /dev/null +++ b/.github/actions/msrv/action.yml @@ -0,0 +1,26 @@ +name: Check MSRV +description: + Check the MSRV (aka `rust-version`) in `Cargo.toml` is valid. Assumes the + repo is already checked out and a Rust toolchain with caching is already + set up, e.g. via `actions-rust-lang/setup-rust-toolchain`. Does not + currently work with Cargo workspaces, see + https://github.com/argumentcomputer/ci-workflows/issues/8 + +inputs: + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' +runs: + using: composite + steps: + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - name: Install cargo-msrv + shell: bash + run: cargo install cargo-msrv + - name: Check Rust MSRV + shell: bash + run: cargo msrv verify diff --git a/.github/actions/release-pr/action.yml b/.github/actions/release-pr/action.yml index 9e5b5a8..777abae 100644 --- a/.github/actions/release-pr/action.yml +++ b/.github/actions/release-pr/action.yml @@ -52,7 +52,14 @@ inputs: runs: using: "composite" steps: - - uses: dtolnay/rust-toolchain@stable + # Rust is incidental here, so provision a toolchain rather than requiring + # one from the caller. Empty `rustflags` so the install doesn't fail on + # `-D warnings`. + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + cache: false + matcher: false + rustflags: '' - run: cargo install tq-rs shell: bash @@ -198,7 +205,7 @@ runs: fi bump_version() { - cd "$1" + cd "$1" || exit 1 OLD_VERSION=$(grep -oP 'version = "\K[^"]+' Cargo.toml | head -n1) if [[ "${CRATE_VERSION}" > "$OLD_VERSION" ]]; then sed -i "s/version = \"$OLD_VERSION\"/version = \"${CRATE_VERSION}\"/" Cargo.toml @@ -206,7 +213,7 @@ runs: echo "New version is not greater than the current version for $1. Aborting..." exit 1 fi - cd ${{ github.workspace }}/${INPUTS_PATH} + cd "${{ github.workspace }}/${INPUTS_PATH}" || exit 1 } while IFS= read -r path; do diff --git a/.github/actions/repo-sync/action.yml b/.github/actions/repo-sync/action.yml new file mode 100644 index 0000000..4ada993 --- /dev/null +++ b/.github/actions/repo-sync/action.yml @@ -0,0 +1,49 @@ +name: Sync from upstream repo +description: + Force-sync a branch from its upstream repository, opening an issue asking + for a manual sync when the push is rejected — e.g. when upstream changed + files under `.github/workflows/`, which `GITHUB_TOKEN` cannot push. The + calling job needs `contents` and `issues` write permissions. No checkout is + required. + +inputs: + repository: + description: Upstream repository formatted as "owner/repo", e.g. "argumentcomputer/ix" + required: true + branch: + description: + Branch to sync. `gh repo sync` takes a single branch name, so the + upstream and the mirror must share it. + required: false + default: 'main' + token: + description: GitHub token with `contents` and `issues` write access + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + # Pushes made with `GITHUB_TOKEN` don't trigger `on: push` workflows in + # the fork, so a sync can't start a recursive run + - name: repo-sync + shell: bash + run: gh repo sync "$GITHUB_REPOSITORY" --source "$INPUTS_REPOSITORY" --branch "$INPUTS_BRANCH" --force + env: + GH_TOKEN: ${{ inputs.token }} + INPUTS_REPOSITORY: ${{ inputs.repository }} + INPUTS_BRANCH: ${{ inputs.branch }} + - uses: $/.github/actions/create-issue + if: failure() + with: + token: ${{ inputs.token }} + title: "chore: manual sync required from ${{ inputs.repository }}" + labels: automated-issue + body: | + The scheduled sync of `${{ inputs.branch }}` from [`${{ inputs.repository }}`](https://github.com/${{ inputs.repository }}) failed. + + The usual cause is an upstream change under `.github/workflows/`, which `GITHUB_TOKEN` is not allowed to push. Review the incoming changes, then sync manually with the "Sync fork" button or a token carrying the `workflow` scope. Close this issue once the branch is synced. + + Check the [failed sync run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for error details. + + This issue was raised by `https://github.com/argumentcomputer/ci-workflows/tree/main/.github/actions/repo-sync`. diff --git a/.github/actions/rust-version-check/action.yml b/.github/actions/rust-version-check/action.yml new file mode 100644 index 0000000..119723f --- /dev/null +++ b/.github/actions/rust-version-check/action.yml @@ -0,0 +1,52 @@ +name: Rust version check +description: >- + Check whether the Rust version specified in `rust-toolchain.toml` is out of + date with the latest stable, opening an issue if so. Compares the full + `..` of `rustup show` with `rustup check`, because the + patch version auto-updates if unspecified in `rust-toolchain.toml`. Assumes + the repo is already checked out. The calling job needs `issues: write`. + +inputs: + token: + description: GitHub token with `issues` write access + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + # `rustup show` below must report the `rust-toolchain.toml` version while + # `rustup check` reports the latest stable, so both must be installed + - name: Install latest stable + shell: bash + run: rustup toolchain install stable + + - name: Parse rust-toolchain.toml + shell: bash + run: echo "TOOLCHAIN_VERSION=$(rustup show | grep rustc | awk '{ print $2 }')" | tee -a $GITHUB_ENV + + - name: Get latest stable Rust version + shell: bash + run: echo "RUST_VERSION=$(rustup check | grep stable | awk '{print $(NF-2)}')" | tee -a $GITHUB_ENV + + - name: Compare Rust versions + shell: bash + run: | + if [[ $TOOLCHAIN_VERSION < $RUST_VERSION ]]; then + echo "VERSION_MISMATCH=true" | tee -a $GITHUB_ENV + else + echo "VERSION_MISMATCH=false" | tee -a $GITHUB_ENV + fi + + - uses: $/.github/actions/create-issue + if: env.VERSION_MISMATCH == 'true' + with: + token: ${{ inputs.token }} + title: "chore: rust toolchain needs an upgrade" + labels: debt,automated-issues + body: | + The rust version specified in `rust-toolchain.toml` (${{ env.TOOLCHAIN_VERSION }}) is out of date with the latest stable (${{ env.RUST_VERSION }}). + + Check the [rust version check](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow for details. + + This issue was raised by the workflow at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/workflow. diff --git a/.github/actions/typos/action.yml b/.github/actions/typos/action.yml new file mode 100644 index 0000000..2ec1e07 --- /dev/null +++ b/.github/actions/typos/action.yml @@ -0,0 +1,73 @@ +name: Check for typos +description: + Run `typos --write-changes` and open a pull request with the fixes, + listing any unfixable typos in the PR body. Assumes the repo is already + checked out. The calling job needs `contents` and `pull-requests` write + permissions. The PR is created with `GITHUB_TOKEN`, so CI does not run on + it automatically — a maintainer reviews and clicks "Approve workflow". + +inputs: + token: + description: GitHub token with `contents` and `pull-requests` write access + required: false + default: ${{ github.token }} +runs: + using: composite + steps: + # Rust is incidental here, so provision a toolchain rather than requiring + # one from the caller. Empty `rustflags` so the install doesn't fail on + # `-D warnings`. + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: '' + - name: Install typos binary + shell: bash + run: cargo +stable install typos-cli + - name: Check typos and write suggestions + id: typo-check + shell: bash + run: | + typos --write-changes > _typos.txt || true + if [[ `git status --porcelain --untracked-files=no` ]]; then + echo "typos=true" | tee -a $GITHUB_OUTPUT + else + echo "typos=false" | tee -a $GITHUB_OUTPUT + fi + - name: Create file for PR + if: steps.typo-check.outputs.typos == 'true' + shell: bash + run: | + printf '%s\n' "Fixes typos found by running \`typos --write-changes\` + Commit: ${{ github.sha }} + Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" > _body.md + if [[ -s _typos.txt ]]; then + printf "## Unfixed typos\n" >> _body.md + printf "Reviewers: Please manually fix & commit the following typos:\n\`\`\`\n" >> _body.md + cat _typos.txt >> _body.md + printf "\`\`\`\n" >> _body.md + rm _typos.txt + fi + printf '%s\n' "> [!NOTE] + > If a false positive is found, please add it to \`_typos.toml\` as per the [documentation](https://github.com/crate-ci/typos/tree/master?tab=readme-ov-file#false-positives)" >> _body.md + # Checks which file types should be committed with typo corrections + # Git pathspecs cause errors if the given pattern doesn't exist, e.g. `git add -- **/*.txt` without any `.txt` files + - name: Check for common file types + if: steps.typo-check.outputs.typos == 'true' + id: file-types + shell: bash + run: | + FILE_PATHS=":!*\_body.md,$(git status --porcelain | awk -F. '{OFS=""; print "**/*."$NF}' | sort -u | paste -sd,)" + echo "paths=$FILE_PATHS" | tee -a $GITHUB_OUTPUT + - name: Create pull request + uses: peter-evans/create-pull-request@v8 + if: steps.typo-check.outputs.typos == 'true' + with: + token: ${{ inputs.token }} + commit-message: '[automated] Fix typos' + title: '[automated] Fix typos' + branch: 'patch/fix-typos' + delete-branch: true + body-path: ./_body.md + labels: automated issue, documentation + # Required in order to exclude the `_body.md` file from the PR + add-paths: ${{ steps.file-types.outputs.paths }} diff --git a/.github/actions/unused-deps/action.yml b/.github/actions/unused-deps/action.yml new file mode 100644 index 0000000..f9858df --- /dev/null +++ b/.github/actions/unused-deps/action.yml @@ -0,0 +1,53 @@ +name: Unused dependency check +description: >- + Run cargo-udeps and open an issue if unused dependencies are found. Assumes + the repo is already checked out and a Rust toolchain with caching is + already set up, e.g. via `actions-rust-lang/setup-rust-toolchain`. The calling job needs `issues: write`. + +inputs: + features: + description: Comma-separated list of features to check + required: false + default: '' + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' + token: + description: GitHub token with `issues` write access + required: false + default: ${{ github.token }} +runs: + using: composite + steps: + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - uses: taiki-e/install-action@cargo-udeps + # Normally running cargo-udeps requires use of a nightly compiler + # In order to have a more stable and less noisy experience, lets instead + # opt to use the stable toolchain specified via the 'rust-toolchain' file + # and instead enable nightly features via 'RUSTC_BOOTSTRAP' + - name: run cargo-udeps + shell: bash + run: RUSTC_BOOTSTRAP=1 cargo udeps --workspace --all-targets --features "${INPUTS_FEATURES}" + env: + INPUTS_FEATURES: ${{ inputs.features }} + - uses: $/.github/actions/create-issue + if: failure() + with: + token: ${{ inputs.token }} + title: "chore: some installed deps are not needed" + labels: automated-issue + body: | + Some dependencies specified in `Cargo.toml` are not needed. + + Check the [unused dependencies sanity check](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) workflow for details. + + This issue was raised by `https://github.com/argumentcomputer/ci-workflows/tree/main/.github/actions/unused-deps`. + + > **Note** + > If this is a false positive, please refer to the [`cargo-udeps` docs][cargo-udeps-docs] on how to ignore the dependencies. + + [cargo-udeps-docs]: https://github.com/est31/cargo-udeps#ignoring-some-of-the-dependencies diff --git a/.github/actions/wasm/action.yml b/.github/actions/wasm/action.yml new file mode 100644 index 0000000..d718f70 --- /dev/null +++ b/.github/actions/wasm/action.yml @@ -0,0 +1,23 @@ +name: Wasm build +description: + Build the workspace for the `wasm32-unknown-unknown` target. Assumes the + repo is already checked out and a Rust toolchain with caching is already + set up, e.g. via `actions-rust-lang/setup-rust-toolchain`. + +inputs: + packages: + description: List of prerequisite Ubuntu packages, separated by whitespace + required: false + default: '' +runs: + using: composite + steps: + - uses: $/.github/actions/install-deps + if: inputs.packages != '' + with: + packages: "${{ inputs.packages }}" + - shell: bash + run: rustup target add wasm32-unknown-unknown + - name: Wasm build + shell: bash + run: cargo build --target wasm32-unknown-unknown diff --git a/.github/templates/UNUSED_DEPS_ISSUE.md b/.github/templates/UNUSED_DEPS_ISSUE.md deleted file mode 100644 index a153047..0000000 --- a/.github/templates/UNUSED_DEPS_ISSUE.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -title: "chore: some installed deps are not needed" -labels: automated-issue ---- - -Some dependencies specified in `Cargo.toml` are not needed. - -Check the [unused dependencies sanity check]({{env.WORKFLOW_URL}}) workflow for details. - -This issue was raised by the workflow at `https://github.com/argumentcomputer/ci-workflows/tree/main/.github/workflows/unused-deps.yml`. - -> **Note** -> If this is a false positive, please refer to the [`cargo-udeps` docs][cargo-udeps-docs] on how to ignore the dependencies. - -[cargo-udeps-docs]: https://github.com/est31/cargo-udeps#ignoring-some-of-the-dependencies diff --git a/.github/templates/VERSION_CHECK.md b/.github/templates/VERSION_CHECK.md deleted file mode 100644 index cfa2c38..0000000 --- a/.github/templates/VERSION_CHECK.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: "chore: rust toolchain needs an upgrade" -labels: debt, automated-issues ---- - -The rust version specified in `rust-toolchain.toml` ({{env.TOOLCHAIN_VERSION}}) is out of date with the latest stable ({{env.RUST_VERSION}}). - -Check the [rust version check]({{env.WORKFLOW_URL}}) workflow for details. - -This issue was raised by the workflow at {{env.WORKFLOW_FILE}}. diff --git a/.github/workflows/actions-lint.yml b/.github/workflows/actions-lint.yml index f5b2513..8ce5fde 100644 --- a/.github/workflows/actions-lint.yml +++ b/.github/workflows/actions-lint.yml @@ -21,4 +21,4 @@ jobs: - uses: actions/checkout@v7 with: persist-credentials: false - - uses: ./.github/actions/lint-workflows + - uses: $/.github/actions/lint-workflows diff --git a/.github/workflows/bench-pr-comment.yml b/.github/workflows/bench-pr-comment.yml deleted file mode 100644 index 590187d..0000000 --- a/.github/workflows/bench-pr-comment.yml +++ /dev/null @@ -1,206 +0,0 @@ -# Creates a PR benchmark comment with a comparison to the base branch -# -# USER NOTE: If you want to use a GPU runner with CUDA acceleration, you must specify `!gpu-benchmark` or `--features cuda` (see below syntax) -# -# Usage: -# ``` -# --bench --features -# ENV_A=a -# ENV_B=b -# ``` -# -# Notes -# - There can be multiple instances of `--bench `, each will spawn a new matrix job and associated PR comment -# - If only `` is passed as input, then the workflow will run with the caller's `default-benches` and `default-env` inputs -# -# Restrictions -# - Only for use with `issue_comment` trigger on a PR -# - If the `cuda` feature is specified, there must be a self-hosted runner attached to the repo with the `gpu-bench` label -name: Benchmark pull requests - -on: - workflow_call: - inputs: - # Comma-separated list of runner labels used for benchmarks when `cuda` feature is not activated - # E.g. "ubuntu-latest", "self-hosted,gpu-bench" The latter will run on a GPU machine but not actually use the GPU - # To use the GPU you must set `--features cuda`, which will always run on a `["self-hosted", "gpu-bench"]` runner - default-runner: - type: string - required: false - default: 'ubuntu-latest' - # Comma-separated list of default benchmarks when they are unspecified in the comment body - default-benches: - type: string - required: true - # Whitespace-separated list of default env vars, set regardless of comment body - default-env: - type: string - required: false - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - setup: - name: Set up benchmark parameters - runs-on: ubuntu-latest - env: - GPU_BENCHMARK: ${{ contains(github.event.comment.body, '!gpu-benchmark') }} - outputs: - # Default runner formatted for JSON parsing - runner: ${{ steps.format-runner.outputs.runner }} - # Benches specified by `--bench ` repeated for each bench - benches: ${{ steps.bench-params.outputs.benches }} - # Features specified by `--features ` - features: ${{ steps.bench-params.outputs.features }} - # Env vars specified by `ENV_VAR=`, starting on the second line of the `issue_comment` input - # Separated by whitespace but ideally newlines for readability - env-vars: ${{ steps.bench-params.outputs.env-vars }} - # Flag to denote the `cuda` feature is active, which means we need a self-hosted GPU runner - cuda: ${{ steps.bench-params.outputs.cuda }} - # `benchmark` or `gpu-benchmark`, used for debugging and comment output - command: ${{ steps.bench-params.outputs.command }} - - steps: - - name: Format default runner string - id: format-runner - run: | - # Parse `default-runner` if it's a list of strings (e.g. `"self-hosted,gpu-bench") - RUNNER=$(echo ${INPUTS_DEFAULT_RUNNER} | awk -F"," -v q=\" '{for (i=0; i> $GITHUB_OUTPUT - echo "$RUNNER" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - env: - INPUTS_DEFAULT_RUNNER: ${{ inputs.default-runner }} - - name: Parse PR comment body - id: bench-params - env: - COMMENT_BODY: ${{ github.event.comment.body }} - INPUTS_DEFAULT_BENCHES: ${{ inputs.default-benches }} - run: | - # Parse `issue_comment` body - printf '%s' "$COMMENT_BODY" > comment.txt - BENCH_COMMAND=$(head -n 1 comment.txt) - echo "$BENCH_COMMAND" - - # Get each input bench name and format as quoted list - BENCHES=$(echo $BENCH_COMMAND | awk -v q=\" '{for (i=1; i<=NF; i++) {if ($i ~ /^--bench/) {print q$(i+1)q","}}}') - if [[ -z $BENCHES ]]; then - # Add quotes to each default bench name in comma-separated list for `fromJSON` parsing - BENCHES=$(echo ${INPUTS_DEFAULT_BENCHES} | awk -F"," -v q=\" '{for (i=0; i> $GITHUB_OUTPUT - echo "$BENCHES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - # Get the list of features to run on each benchmark - FEATURES=$(echo $BENCH_COMMAND | awk '{for (i=1; i<=NF; i++) {if ($i ~ /^--features/) {print $(i+1) }}}') - if [[ ${GPU_BENCHMARK} = 'true' || $(echo $FEATURES | grep -s cuda) ]]; then - echo "cuda=true" | tee -a $GITHUB_OUTPUT - COMMAND="gpu-benchmark" - # Add the "cuda" feature if not already specified - if echo "$FEATURES" | grep -vq "cuda" 2>/dev/null; then - FEATURES="${FEATURES},cuda" - fi - else - COMMAND="benchmark" - fi - echo "command=$COMMAND" | tee -a $GITHUB_OUTPUT - echo "features=$FEATURES" | tee -a $GITHUB_OUTPUT - # Can't persist env vars between jobs, so we pass them as an output and set them in the next job - echo "env-vars=$(tail -n +2 comment.txt)" | tee -a $GITHUB_OUTPUT - - benchmark: - needs: [ setup ] - # Uses a self-hosted GPU runner if the `cuda` feature is specified, otherwise uses the default runner - runs-on: ${{ (needs.setup.outputs.cuda) && fromJSON('[ "self-hosted", "gpu-bench" ]') || fromJSON(needs.setup.outputs.runner) }} - strategy: - matrix: - # Runs a job for each benchmark specified in the `issue_comment` input - bench: ${{ fromJSON(needs.setup.outputs.benches) }} - steps: - # When using the `cuda` feature, several GPU-related env vars are set by the `gpu-setup` action below. - # Thus there is no need to set them here. These inputs are mainly for benchmark parameters such as `LURK_RC` - - name: Set env vars - env: - DEFAULT_ENV: ${{ inputs.default-env }} - COMMENT_ENV: ${{ needs.setup.outputs.env-vars }} - run: | - # Trims newlines that may arise from `$GITHUB_OUTPUT` - # Both lists are deliberately unquoted so they word-split into - # individual `NAME=VALUE` pairs. - # shellcheck disable=SC2086 - for var in $DEFAULT_ENV - do - echo "$(echo $var | tr -d '\n')" | tee -a $GITHUB_ENV - done - # Overrides default env vars with those specified in the `issue_comment` input if identically named - # shellcheck disable=SC2086 - for var in $COMMENT_ENV - do - echo "$(echo $var | tr -d '\n')" | tee -a $GITHUB_ENV - done - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/gpu-setup - if: ${{ needs.setup.outputs.cuda }} - with: - gpu-framework: 'cuda' - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - # Get base branch of the PR - - uses: xt0rted/pull-request-comment-branch@v3 - id: comment-branch - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Checkout PR branch - run: gh pr checkout $PR_NUMBER - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.issue.number }} - # Install dependencies - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - # Run the comparative benchmark and comment output on the PR - - uses: boa-dev/criterion-compare-action@v3 - with: - # Note: Removing `benchName` causes `criterion` `save-baseline` errors: - # https://github.com/boa-dev/criterion-compare-action#troubleshooting - # Optional. Compare only this benchmark target - benchName: ${{ matrix.bench }} - # Optional. Features activated in the benchmark - features: "${{ needs.setup.outputs.features }}" - # Needed. The name of the branch to compare with - branchName: ${{ steps.comment-branch.outputs.base_ref }} - - name: Comment on successful run - if: success() - uses: peter-evans/create-or-update-comment@v5 - with: - issue-number: ${{ github.event.issue.number }} - body: | - `!${{ needs.setup.outputs.command }}` action succeeded! :rocket: - - https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} - - - name: Comment on failing run - if: failure() - uses: peter-evans/create-or-update-comment@v5 - with: - issue-number: ${{ github.event.issue.number }} - body: | - `!${{ needs.setup.outputs.command }}` action failed :x: - - https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} diff --git a/.github/workflows/check-lurk-compiles.yml b/.github/workflows/check-lurk-compiles.yml deleted file mode 100644 index d50794f..0000000 --- a/.github/workflows/check-lurk-compiles.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Check upstream `lurk-rs` compiles - -on: - workflow_call: - inputs: - runner: - required: false - default: 'ubuntu-latest' - type: string - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - check-lurk-compiles: - if: github.event_name == 'pull_request' - runs-on: ${{ inputs.runner }} - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/lurk-rs - path: ./lurk-rs - submodules: recursive - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Patch Cargo.toml - working-directory: ${{ github.workspace }}/lurk-rs - run: | - URL=https://github.com/${{ github.repository }} - # the dependency we want to patch is usually the same as the package, but - # we e.g. want to override dependency 'nova' with an 'arecibo' package - DEPENDENCY=$(grep "git = \"$URL\"" Cargo.toml | awk '{ print $1 }') - PACKAGE=$(grep "git = \"$URL\"" Cargo.toml | grep -oP 'package = "\K[^"]*'| cat) - echo "[patch.'$URL']" >> Cargo.toml - if [ ! -z "$PACKAGE" ]; - then - echo "$DEPENDENCY = { path='../', package='$PACKAGE' }" >> Cargo.toml - else - echo "$DEPENDENCY = { path='../' }" >> Cargo.toml - fi - - name: Check Lurk-rs types don't break spectacularly - working-directory: ${{ github.workspace }}/lurk-rs - run: cargo check --workspace --tests --benches --examples diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml deleted file mode 100644 index 89e315d..0000000 --- a/.github/workflows/codecov.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: Generate and deploy Codecov results - -on: - workflow_call: - inputs: - runner: - required: false - default: 'ubuntu-latest' - type: string - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - codecov-grcov: - name: Generate code coverage - runs-on: ${{ inputs.runner }} - strategy: - fail-fast: true - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - submodules: recursive - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - components: llvm-tools-preview - - uses: Swatinem/rust-cache@v2 - - uses: taiki-e/install-action@nextest - - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov - - name: Clean the workspace - run: cargo llvm-cov clean --workspace - - name: Build - run: cargo build --workspace --release - - name: Collect coverage data - run: cargo llvm-cov nextest --lcov --output-path lcov.info --profile ci --release --workspace - - name: Upload coverage data to codecov - uses: codecov/codecov-action@v7 - with: - files: lcov.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9e826ad..2add0e8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,20 +3,20 @@ name: Generate and deploy crate docs on: workflow_call: +permissions: {} + jobs: docs: name: Generate crate documentation runs-on: ubuntu-latest + # `contents: write` for the `gh-pages` deploy + permissions: + contents: write steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - uses: actions/checkout@v7 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable + - uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Generate documentation env: RUSTDOCFLAGS: "--enable-index-page -Zunstable-options" diff --git a/.github/workflows/gpu-bench.yml b/.github/workflows/gpu-bench.yml deleted file mode 100644 index 539bce6..0000000 --- a/.github/workflows/gpu-bench.yml +++ /dev/null @@ -1,169 +0,0 @@ -# Run final tests only when attempting to merge, shown as skipped status checks beforehand -# Prerequisites -# - Self-hosted Nvidia GPU runner with `gpu-bench` tag in caller repo -# - `cuda` Cargo features -# - Pre-existing `gh-pages` branch -# - Run on `merge_group` trigger only -name: Comparative benchmarks on GPU - -on: - workflow_call: - inputs: - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - # Run comparative benchmark against base branch, open issue on regression - gpu-benchmark: - runs-on: [self-hosted, gpu-bench] - steps: - # Set up GPU - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/gpu-setup - with: - gpu-framework: 'cuda' - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - # `git-auto-commit-action` below pushes to `gh-pages` with the credentials - # this checkout persists, so they cannot be disabled here. - - uses: actions/checkout@v7 # zizmor: ignore[artipacked] - # Install dependencies - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Install criterion - run: | - cargo install cargo-criterion - cargo install criterion-table - # Requires benchmarks to be formatted for `criterion-table` using `${REPOSITORY_NAME}_BENCH_OUTPUT=commit-comment` - # e.g. `$LURK_BENCH_OUTPUT=commit-comment` - - name: Set env vars - run: | - REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F'/' '{ print toupper($2) }') - echo "${REPOSITORY_NAME}_BENCH_OUTPUT=commit-comment" | tee -a $GITHUB_ENV - echo "BASE_COMMIT=${{ github.event.merge_group.base_sha }}" | tee -a $GITHUB_ENV - echo "GPU_ID=$(echo $GPU_NAME | awk '{ print $NF }')" | tee -a $GITHUB_ENV - # Checkout gh-pages to check for cached bench result - - name: Checkout gh-pages - uses: actions/checkout@v7 - with: - ref: gh-pages - path: gh-pages - persist-credentials: false - - name: Check for cached bench result - id: cached-bench - run: | - if [ -f "$BASE_COMMIT-$GPU_ID.json" ] - then - echo "cached=true" | tee -a $GITHUB_OUTPUT - cp "$BASE_COMMIT-$GPU_ID.json" "../$BASE_COMMIT.json" - else - echo "cached=false" | tee -a $GITHUB_OUTPUT - fi - working-directory: ${{ github.workspace }}/gh-pages - # Checkout base branch for comparative bench - - uses: actions/checkout@v7 - if: steps.cached-bench.outputs.cached == 'false' - with: - ref: ${{ github.base_ref }} - path: ${{ github.base_ref }} - persist-credentials: false - - name: Run GPU bench on base branch - if: steps.cached-bench.outputs.cached == 'false' - run: | - # Run benchmark - cargo criterion --features "cuda" --message-format=json > "$BASE_COMMIT.json" - # Copy bench output to PR branch - cp "$BASE_COMMIT.json" .. - working-directory: ${{ github.workspace }}/${{ github.base_ref }} - - name: Run GPU bench on PR branch - run: | - cargo criterion --features "cuda" --message-format=json > ${{ github.sha }}.json - cp ${{ github.sha }}.json .. - working-directory: ${{ github.workspace }}/benches - - name: copy the benchmark template and prepare it with data - run: | - cp .github/tables.toml . - # Get CPU model - CPU_MODEL=$(grep '^model name' /proc/cpuinfo | head -1 | awk -F ': ' '{ print $2 }') - # Get num vCPUS - NUM_VCPUS="$(nproc --all) vCPUs" - # Get total RAM in GB - TOTAL_RAM=$(grep MemTotal /proc/meminfo | awk '{$2=$2/(1024^2); print int($2), "GB RAM";}') - - # Use conditionals to ensure that only non-empty variables are inserted - [[ ! -z "$GPU_NAME" ]] && sed -i "/^\"\"\"$/i $GPU_NAME" tables.toml - [[ ! -z "$CPU_MODEL" ]] && sed -i "/^\"\"\"$/i $CPU_MODEL" tables.toml - [[ ! -z "$NUM_VCPUS" ]] && sed -i "/^\"\"\"$/i $NUM_VCPUs" tables.toml - [[ ! -z "$TOTAL_RAM" ]] && sed -i "/^\"\"\"$/i $TOTAL_RAM" tables.toml - sed -i "/^\"\"\"$/i Workflow run: $GITHUB_SERVER_URL/$REPO/actions/runs/$RUN_ID" tables.toml - working-directory: ${{ github.workspace }} - env: - REPO: ${{ github.repository }} - RUN_ID: ${{ github.run_id }} - # Create a `criterion-table` and write in commit comment - - name: Run `criterion-table` - run: cat "$BASE_COMMIT.json" "$GITHUB_SHA.json" | criterion-table > BENCHMARKS.md - - name: Write bench on commit comment - uses: peter-evans/commit-comment@v4 - with: - body-path: BENCHMARKS.md - # Check for a slowdown >= 10%. If so, open an issue but don't block merge - - name: Check for perf regression - id: regression-check - run: | - regressions=$(awk -F'[*x]' '/slower/{print $12}' BENCHMARKS.md) - - echo $regressions - - for r in $regressions - do - if (( $(echo "$r >= 1.10" | bc -l) )) - then - exit 1 - fi - done - continue-on-error: true - # Not possible to use ${{ github.event.number }} with the `merge_group` trigger - - name: Get PR number from merge branch - env: - HEAD_REF: ${{ github.event.merge_group.head_ref }} - run: | - echo "PR_NUMBER=$(echo "$HEAD_REF" | sed -e 's/.*pr-\(.*\)-.*/\1/')" | tee -a $GITHUB_ENV - - name: Create file for issue - if: steps.regression-check.outcome == 'failure' - run: | - printf '%s\n' "Regression >= 10% found during merge for PR #$PR_NUMBER - Commit: $GITHUB_SHA - Workflow run: $GITHUB_SERVER_URL/$REPO/actions/runs/$RUN_ID" > ./_body.md - env: - REPO: ${{ github.repository }} - RUN_ID: ${{ github.run_id }} - - name: Open issue on regression - if: steps.regression-check.outcome == 'failure' - uses: peter-evans/create-issue-from-file@v6 - with: - title: ':rotating_light: Performance regression detected for PR #${{ env.PR_NUMBER }}' - content-filepath: ./_body.md - labels: | - P-Performance - automated issue - - name: Remove old base bench - run: | - rm "$BASE_COMMIT.json" - mv "$GITHUB_SHA.json" "$GITHUB_SHA-$GPU_ID.json" - working-directory: ${{ github.workspace }} - - name: Commit bench result to `gh-pages` branch if no regression - if: steps.regression-check.outcome != 'failure' - uses: stefanzweifel/git-auto-commit-action@v7 - with: - branch: gh-pages - commit_message: '[automated] GPU Benchmark from PR #${{ env.PR_NUMBER }}' - file_pattern: '${{ github.sha }}-${{ env.GPU_ID }}.json' diff --git a/.github/workflows/gpu-ci-cuda.yml b/.github/workflows/gpu-ci-cuda.yml deleted file mode 100644 index 8deae4e..0000000 --- a/.github/workflows/gpu-ci-cuda.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Prerequisites -# - Self-hosted Nvidia GPU runner with CUDA enabled -# - Runner attached in caller repo with `gpu-ci` label -# - `cuda` Cargo feature -name: GPU CI Tests with CUDA - -on: - # We expect dependents to call this with the `pull_request` and `merge_group` trigger - # This will show as a skipped status check on the PR, and then run once when attempting to merge - workflow_call: - inputs: - # comma-separated list of features to run in addition to `cuda` - features: - required: false - default: "" - type: string - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - cuda: - name: Rust tests on CUDA - if: github.event_name != 'pull_request' || github.event.action == 'enqueued' - runs-on: [self-hosted, gpu-ci] - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/gpu-setup - with: - gpu-framework: 'cuda' - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - submodules: recursive - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@nextest - - uses: Swatinem/rust-cache@v2 - - name: CUDA tests - env: - FEATURES: ${{ inputs.features }} - run: | - cargo nextest run --profile ci --cargo-profile dev-ci --features "cuda,$FEATURES" diff --git a/.github/workflows/gpu-ci-opencl.yml b/.github/workflows/gpu-ci-opencl.yml deleted file mode 100644 index 2c3e69c..0000000 --- a/.github/workflows/gpu-ci-opencl.yml +++ /dev/null @@ -1,51 +0,0 @@ -# Prerequisites -# - Self-hosted Nvidia GPU runner with CUDA enabled -# - Runner attached in caller repo with `gpu-ci` label -# - `cuda` and `opencl` Cargo features -name: GPU CI Tests with OpenCL - -on: - # We expect dependents to call this with the `pull_request` and `merge_group` trigger - # This will show as a skipped status check on the PR, and then run once when attempting to merge - workflow_call: - inputs: - # comma-separated list of features to run in addition to `cuda`/`opencl` - features: - required: false - default: "" - type: string - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - opencl: - name: Rust tests on OpenCL - if: github.event_name != 'pull_request' || github.event.action == 'enqueued' - runs-on: [self-hosted, gpu-ci] - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/gpu-setup - with: - gpu-framework: 'opencl' - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - submodules: recursive - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@nextest - - uses: Swatinem/rust-cache@v2 - - name: OpenCL tests - env: - FEATURES: ${{ inputs.features }} - run: | - cargo nextest run --profile ci --cargo-profile dev-ci --features "cuda,opencl,$FEATURES" diff --git a/.github/workflows/licenses-audits.yml b/.github/workflows/licenses-audits.yml index 54d13bd..d4547f4 100644 --- a/.github/workflows/licenses-audits.yml +++ b/.github/workflows/licenses-audits.yml @@ -4,8 +4,12 @@ name: cargo-deny on: workflow_call: +permissions: {} + jobs: cargo-deny: + permissions: + contents: read name: cargo-deny (advisories, licenses, bans, ...) runs-on: ubuntu-latest steps: diff --git a/.github/workflows/links-check.yml b/.github/workflows/links-check.yml deleted file mode 100644 index 0572139..0000000 --- a/.github/workflows/links-check.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Check documentation links - -on: - workflow_call: - inputs: - # Whether or not to error on failure - # If false, opens an issue instead - fail-fast: - required: false - default: true - type: boolean - -permissions: - contents: read - -jobs: - linkChecker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - name: Link Checker - id: lychee - uses: lycheeverse/lychee-action@v2.9.0 - with: - fail: ${{ inputs.fail-fast }} - env: - GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} - # lychee stopped exporting `lychee_exit_code` to the environment in v2; - # the exit code is only available as a step output. - - name: Open issue on failure if `fail-fast` input is false - if: steps.lychee.outputs.exit_code != 0 && inputs.fail-fast != true - uses: peter-evans/create-issue-from-file@v6 - with: - title: Link Checker Report - content-filepath: ./lychee/out.md - labels: report, automated issue diff --git a/.github/workflows/lints.yml b/.github/workflows/lints.yml deleted file mode 100644 index c382359..0000000 --- a/.github/workflows/lints.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Check lints and code quality - -on: - workflow_call: - inputs: - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - # Runs `cargo fmt +nightly`, for use with nightly config options in `rustfmt.toml` - nightly-fmt: - required: false - type: boolean - -jobs: - # Rustfmt, clippy, and doctests - lints: - runs-on: ubuntu-latest - strategy: - fail-fast: false - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - uses: dtolnay/rust-toolchain@nightly - if: inputs.nightly-fmt - with: - components: rustfmt - - uses: Swatinem/rust-cache@v2 - - name: Check Rustfmt Code Style - run: | - if [[ "${{ inputs.nightly-fmt }}" == "true" ]]; then - cargo +nightly fmt --all -- --check - else - cargo fmt --all -- --check - fi - - name: Check clippy warnings - run: | - if cargo --list | grep -q xclippy; then - cargo xclippy -Dwarnings - else - cargo clippy -Dwarnings - fi - - name: Doctests - run: cargo test --doc --workspace diff --git a/.github/workflows/msrv.yml b/.github/workflows/msrv.yml deleted file mode 100644 index b8a0391..0000000 --- a/.github/workflows/msrv.yml +++ /dev/null @@ -1,35 +0,0 @@ -# NOTE: Does not currently work with Cargo workspaces -# See https://github.com/argumentcomputer/ci-workflows/issues/8 -name: Check MSRV - -on: - workflow_call: - inputs: - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string - -jobs: - # Check MSRV (aka `rust-version`) in `Cargo.toml` is valid - msrv: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Install cargo-msrv - run: cargo install cargo-msrv - - name: Check Rust MSRV - run: cargo msrv verify diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml deleted file mode 100644 index 4eca8e0..0000000 --- a/.github/workflows/repo-sync.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Sync changes from upstream repo - -on: - workflow_call: - inputs: - # Input must be formatted as "owner/repo", e.g. "argumentcomputer/lurk-rs" - repository: - required: true - type: string - # `gh repo sync` takes a single branch name, so the upstream and the - # mirror must share it. - branch: - required: false - default: 'main' - type: string - secrets: - TOKEN_APP_ID: - required: true - TOKEN_APP_PRIVATE_KEY: - required: true - -# `secrets.GITHUB_TOKEN` cannot carry the `workflow` scope, so it is rejected -# whenever an upstream commit touches `.github/workflows/**`. A GitHub App -# installation token with Workflows: write can push those commits. -permissions: {} - -jobs: - repo-sync: - name: Sync changes from upstream - runs-on: ubuntu-latest - steps: - - uses: actions/create-github-app-token@v3 - id: generate-token - with: - client-id: ${{ secrets.TOKEN_APP_ID }} - private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - permission-contents: write - permission-workflows: write - # `github.repository` is the caller's repo, not this one, and the token is - # scoped to it by default. - - name: repo-sync - run: gh repo sync ${{ github.repository }} --source ${INPUTS_REPOSITORY} --branch ${INPUTS_BRANCH} --force - env: - GH_TOKEN: ${{ steps.generate-token.outputs.token }} - INPUTS_REPOSITORY: ${{ inputs.repository }} - INPUTS_BRANCH: ${{ inputs.branch }} diff --git a/.github/workflows/rust-version-check.yml b/.github/workflows/rust-version-check.yml deleted file mode 100644 index 039b17b..0000000 --- a/.github/workflows/rust-version-check.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Checks whether Rust version specified in `rust-toolchain.toml` is out of date with latest stable -# Compares the full `..` of `rustup show` with `rustup check` -# This is because the patch version will auto-update if unspecified in `rust-toolchain.toml` -name: Rust Version Check - -on: - workflow_call: - -jobs: - rust-version-check: - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v7 - with: - persist-credentials: false - - - name: Check out `ci-workflows` - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - path: ci-workflows - persist-credentials: false - - - name: Set up Rust - uses: dtolnay/rust-toolchain@stable - - - name: Parse rust-toolchain.toml - run: echo "TOOLCHAIN_VERSION=$(rustup show | grep rustc | awk '{ print $2 }')" | tee -a $GITHUB_ENV - - - name: Get latest stable Rust version - run: echo "RUST_VERSION=$(rustup check | grep stable | awk '{print $(NF-2)}')" | tee -a $GITHUB_ENV - - - name: Compare Rust versions - run: | - if [[ $TOOLCHAIN_VERSION < $RUST_VERSION ]]; then - echo "VERSION_MISMATCH=true" | tee -a $GITHUB_ENV - else - echo "VERSION_MISMATCH=false" | tee -a $GITHUB_ENV - fi - - # Open issue if crate Rust version is out of date with latest stable - - uses: JasonEtco/create-an-issue@v2 - if: env.VERSION_MISMATCH == 'true' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TOOLCHAIN_VERSION: ${{ env.TOOLCHAIN_VERSION }} - RUST_VERSION: ${{ env.RUST_VERSION }} - WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - WORKFLOW_FILE: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}/workflow - with: - update_existing: true - filename: ci-workflows/.github/templates/VERSION_CHECK.md diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml deleted file mode 100644 index 209ec20..0000000 --- a/.github/workflows/typos.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Check for typos - -on: - # Supported triggers: - # `workflow_dispatch` and nightly, e.g.: - # schedule: - # - cron: "0 0 * * *" - workflow_call: - -jobs: - typo-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - name: Install typos binary - run: cargo +stable install typos-cli - - name: Check typos and write suggestions - id: typo-check - run: | - typos --write-changes > _typos.txt || true - if [[ `git status --porcelain --untracked-files=no` ]]; then - echo "typos=true" | tee -a $GITHUB_OUTPUT - else - echo "typos=false" | tee -a $GITHUB_OUTPUT - fi - - name: Create file for PR - if: steps.typo-check.outputs.typos == 'true' - run: | - printf '%s\n' "Fixes typos found by running \`typos --write-changes\` - Commit: ${{ github.sha }} - Workflow run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" > _body.md - if [[ -s _typos.txt ]]; then - printf "## Unfixed typos\n" >> _body.md - printf "Reviewers: Please manually fix & commit the following typos:\n\`\`\`\n" >> _body.md - cat _typos.txt >> _body.md - printf "\`\`\`\n" >> _body.md - rm _typos.txt - fi - printf '%s\n' "> [!NOTE] - > If a false positive is found, please add it to \`_typos.toml\` as per the [documentation](https://github.com/crate-ci/typos/tree/master?tab=readme-ov-file#false-positives)" >> _body.md - # Checks which file types should be committed with typo corrections - # Git pathspecs cause errors if the given pattern doesn't exist, e.g. `git add -- **/*.txt` without any `.txt` files - - name: Check for common file types - if: steps.typo-check.outputs.typos == 'true' - id: file-types - run: | - FILE_PATHS=":!*\_body.md,$(git status --porcelain | awk -F. '{OFS=""; print "**/*."$NF}' | sort -u | paste -sd,)" - echo "paths=$FILE_PATHS" | tee -a $GITHUB_OUTPUT - - uses: actions/create-github-app-token@v3 - if: steps.typo-check.outputs.typos == 'true' - id: generate-token - with: - client-id: ${{ secrets.TOKEN_APP_ID }} - private-key: ${{ secrets.TOKEN_APP_PRIVATE_KEY }} - permission-contents: write - permission-pull-requests: write - - name: Create pull request - uses: peter-evans/create-pull-request@v8 - if: steps.typo-check.outputs.typos == 'true' - with: - token: ${{ steps.generate-token.outputs.token }} - commit-message: '[automated] Fix typos' - title: '[automated] Fix typos' - branch: 'patch/fix-typos' - delete-branch: true - body-path: ./_body.md - labels: automated issue, documentation - # Required in order to exclude the `_body.md` file from the PR - add-paths: ${{ steps.file-types.outputs.paths }} diff --git a/.github/workflows/unused-deps.yml b/.github/workflows/unused-deps.yml deleted file mode 100644 index 9518f1e..0000000 --- a/.github/workflows/unused-deps.yml +++ /dev/null @@ -1,57 +0,0 @@ -# Runs unused dependency check for crate consumers. - -name: Unused dependency check - -on: - # we expect dependents to call this on a nightly basis - # schedule: - # - cron: "0 0 * * *" - workflow_call: - inputs: - # comma-separated list of features to check - features: - required: false - default: "" - type: string - packages: - required: false - type: string - -env: - CARGO_TERM_COLOR: always - -jobs: - unused-dependencies: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - path: ci-workflows - persist-credentials: false - - uses: ./ci-workflows/.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: dtolnay/rust-toolchain@stable - - uses: taiki-e/install-action@cargo-udeps - # Normally running cargo-udeps requires use of a nightly compiler - # In order to have a more stable and less noisy experience, lets instead - # opt to use the stable toolchain specified via the 'rust-toolchain' file - # and instead enable nightly features via 'RUSTC_BOOTSTRAP' - - name: run cargo-udeps - run: RUSTC_BOOTSTRAP=1 cargo udeps --workspace --all-targets --features "${INPUTS_FEATURES}" - env: - INPUTS_FEATURES: ${{ inputs.features }} - - uses: JasonEtco/create-an-issue@v2 - if: ${{ failure() }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - WORKFLOW_URL: - ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - with: - update_existing: true - filename: ci-workflows/.github/templates/UNUSED_DEPS_ISSUE.md diff --git a/.github/workflows/wasm.yml b/.github/workflows/wasm.yml deleted file mode 100644 index cfedb25..0000000 --- a/.github/workflows/wasm.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Wasm build - -on: - workflow_call: - inputs: - # List of prerequisite Ubuntu packages, separated by whitespace - packages: - required: false - type: string -jobs: - wasm-build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - with: - repository: argumentcomputer/ci-workflows - persist-credentials: false - - uses: ./.github/actions/ci-env - - uses: ./.github/actions/install-deps - if: inputs.packages != '' - with: - packages: "${{ inputs.packages }}" - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: rustup target add wasm32-unknown-unknown - - name: Wasm build - run: cargo build --target wasm32-unknown-unknown From 17b5458d67d2732c348a4779035f82ee6914b9f7 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:55:03 -0400 Subject: [PATCH 2/9] Extend dependabot to composite action directories The github-actions ecosystem's "/" directory only covers `.github/workflows/`, so the third-party actions that moved into composite actions would no longer receive update PRs without listing their directories explicitly. --- .github/dependabot.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a1203f0..43b33f0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,11 @@ version: 2 updates: - package-ecosystem: "github-actions" - directory: "/" + # "/" is a special case meaning `.github/workflows/`; composite action + # directories must be listed separately for their dependencies to be seen + directories: + - "/" + - "/.github/actions/**" schedule: interval: "weekly" cooldown: From 819ff7f1b34c703f7c6a6abd012c34a9a067d1f5 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:55:03 -0400 Subject: [PATCH 3/9] Add smoke tests for the composite actions - create-issue runs end-to-end against a stub `gh` on PATH that records its calls: the create + label path, the update-existing-by-title path, and rejection when both `body` and `body-file` are set, asserted against the recorded call log - lints, wasm, and unused-deps run on a generated fixture crate through the documented caller pattern (checkout, then setup-rust-toolchain, then the action) - rust-version-check runs twice against the stub `gh`: an up-to-date pin must not open an issue, and an outdated pin (installed so `rustup show` reports it as active) must open exactly one with the parsed version in the body --- .github/workflows/test.yml | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a39d984 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,138 @@ +# Smoke tests for this repo's composite actions +name: Test actions + +on: + push: + branches: main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: {} + +jobs: + # Exercises the create/update/invalid-input paths against a stub `gh` that + # records its calls, so no real issues are touched + create-issue: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Install stub gh + run: | + mkdir -p "$RUNNER_TEMP/stub-bin" + cat > "$RUNNER_TEMP/stub-bin/gh" <<'EOF' + #!/usr/bin/env bash + echo "gh $*" >> "$GH_CALL_LOG" + case "$1 $2" in + "issue list") cat "$GH_ISSUES" ;; + "issue create") echo "https://github.com/example/repo/issues/1" ;; + esac + EOF + chmod +x "$RUNNER_TEMP/stub-bin/gh" + echo "$RUNNER_TEMP/stub-bin" >> "$GITHUB_PATH" + echo "GH_CALL_LOG=$RUNNER_TEMP/gh-calls.log" | tee -a "$GITHUB_ENV" + echo "GH_ISSUES=$RUNNER_TEMP/issues.json" | tee -a "$GITHUB_ENV" + echo "[]" > "$RUNNER_TEMP/issues.json" + - name: Create a new issue + uses: $/.github/actions/create-issue + with: + title: Test issue + labels: test-label + body: Test body + - name: Update an existing issue with the same title + run: | + echo '[{"number": 7, "title": "Test issue"}, {"number": 9, "title": "other"}]' > "$GH_ISSUES" + - uses: $/.github/actions/create-issue + with: + title: Test issue + body: Updated body + - name: Reject setting both body and body-file + id: invalid + continue-on-error: true + uses: $/.github/actions/create-issue + with: + title: Test issue + body: Test body + body-file: README.md + - name: Assert recorded gh calls + env: + INVALID_OUTCOME: ${{ steps.invalid.outcome }} + run: | + cat "$GH_CALL_LOG" + set -x + [[ "$INVALID_OUTCOME" == failure ]] + grep -qF -- "issue create --repo $GITHUB_REPOSITORY --title Test issue --body Test body" "$GH_CALL_LOG" + grep -qF -- "issue edit https://github.com/example/repo/issues/1 --repo $GITHUB_REPOSITORY --add-label test-label" "$GH_CALL_LOG" + grep -qF -- "issue edit 7 --repo $GITHUB_REPOSITORY --body Updated body" "$GH_CALL_LOG" + # The invalid input errors out before any gh call, so only the two + # earlier action runs listed issues + [[ "$(grep -cF -- "issue list" "$GH_CALL_LOG")" == 2 ]] + + # Runs the Rust CI actions on a generated fixture crate via the documented + # caller pattern: checkout, then toolchain setup, then the action + rust-actions: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Generate fixture crate + run: cargo init --lib --name fixture . + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + # Don't save caches from test fixtures + cache: false + - uses: $/.github/actions/lints + - uses: $/.github/actions/wasm + - uses: $/.github/actions/unused-deps + + # An up-to-date pin must not open an issue; an outdated pin must open one + # with the parsed versions. Issue calls go to the same stub `gh` as above. + rust-version-check: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - name: Install stub gh + run: | + mkdir -p "$RUNNER_TEMP/stub-bin" + cat > "$RUNNER_TEMP/stub-bin/gh" <<'EOF' + #!/usr/bin/env bash + echo "gh $*" >> "$GH_CALL_LOG" + case "$1 $2" in + "issue list") echo "[]" ;; + "issue create") echo "https://github.com/example/repo/issues/1" ;; + esac + EOF + chmod +x "$RUNNER_TEMP/stub-bin/gh" + echo "$RUNNER_TEMP/stub-bin" >> "$GITHUB_PATH" + echo "GH_CALL_LOG=$RUNNER_TEMP/gh-calls.log" | tee -a "$GITHUB_ENV" + - name: Pin an up-to-date toolchain + run: printf '[toolchain]\nchannel = "stable"\n' > rust-toolchain.toml + - uses: $/.github/actions/rust-version-check + - name: Pin an outdated toolchain + run: | + printf '[toolchain]\nchannel = "1.70.0"\n' > rust-toolchain.toml + # Install it so `rustup show` reports it as the active version + rustup toolchain install 1.70.0 --profile minimal + - uses: $/.github/actions/rust-version-check + - name: Assert only the outdated pin opened an issue + run: | + touch "$GH_CALL_LOG" + cat "$GH_CALL_LOG" + set -x + [[ "$(grep -cF -- "issue create" "$GH_CALL_LOG")" == 1 ]] + grep -qF -- "issue create --repo $GITHUB_REPOSITORY --title chore: rust toolchain needs an upgrade" "$GH_CALL_LOG" + # The parsed toolchain version made it into the issue body + grep -qF -- "(1.70.0)" "$GH_CALL_LOG" From 44bb0db1f46b1d2592c1ad9133635350492b2263 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 27 Aug 2026 19:55:03 -0400 Subject: [PATCH 4/9] Shellcheck composite action scripts in lint-workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actionlint can't parse composite actions (rhysd/actionlint#46), so their bash `run:` blocks otherwise go unchecked — the class of bug that produced the NUM_VCPUS typo and release-pr's unguarded `cd`s. A Python script beside the action extracts each block via `yq -o=json`, masks `${{ }}` expressions the way actionlint does, and batches everything through one shellcheck invocation. Both tools are preinstalled on GitHub-hosted runners. --- .github/actions/lint-workflows/action.yml | 6 +++ .../lint-workflows/shellcheck_actions.py | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 .github/actions/lint-workflows/shellcheck_actions.py diff --git a/.github/actions/lint-workflows/action.yml b/.github/actions/lint-workflows/action.yml index 4bf34c5..aea3a89 100644 --- a/.github/actions/lint-workflows/action.yml +++ b/.github/actions/lint-workflows/action.yml @@ -25,6 +25,12 @@ runs: # actionlint doesn't know the `$/` self-repository syntax yet and # misparses it as {owner}/{repo}@{ref} flags: -ignore 'specifying action "\$/.+" in invalid format' + - name: Shellcheck composite action scripts + shell: bash + env: + SEVERITY: ${{ inputs.shellcheck-severity }} + ACTION_PATH: ${{ github.action_path }} + run: python3 "$ACTION_PATH/shellcheck_actions.py" # Note that `continue-on-error` is silently ignored on composite action # steps, so any zizmor finding fails the calling job. Suppress findings # that don't apply via `.github/zizmor.yml` or `# zizmor: ignore` comments. diff --git a/.github/actions/lint-workflows/shellcheck_actions.py b/.github/actions/lint-workflows/shellcheck_actions.py new file mode 100644 index 0000000..bef8c55 --- /dev/null +++ b/.github/actions/lint-workflows/shellcheck_actions.py @@ -0,0 +1,40 @@ +"""Shellcheck the bash `run:` blocks of composite actions. + +actionlint can't parse composite action manifests (rhysd/actionlint#46), +so their scripts are extracted and batched through one shellcheck +invocation here, with `${{ }}` expressions masked the way actionlint +masks them in workflow scripts. +""" + +import json +import os +import re +import subprocess +import sys +import tempfile +from pathlib import Path + + +def main(): + severity = os.environ.get("SEVERITY", "warning") + out = Path(tempfile.mkdtemp()) + scripts = [] + for manifest in sorted(Path(".github/actions").glob("*/action.y*ml")): + parsed = subprocess.run( + ["yq", "-o=json", ".", manifest], check=True, text=True, stdout=subprocess.PIPE + ) + for i, step in enumerate(json.loads(parsed.stdout)["runs"]["steps"]): + if step.get("shell") == "bash": + script = out / f"{manifest.parent.name}-{i}.sh" + script.write_text(re.sub(r"\$\{\{.*?\}\}", "EXPR", step["run"])) + scripts.append(script) + if scripts: + sys.exit( + subprocess.run( + ["shellcheck", f"--severity={severity}", "--shell=bash", *scripts] + ).returncode + ) + + +if __name__ == "__main__": + main() From fa3024260a8e3dbb1421379bcc9b9ac7e3c6d634 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:55:37 -0400 Subject: [PATCH 5/9] Drop actionlint ignores for the GitHub App token action The workarounds covered `actions/create-github-app-token`'s `app-id` to `client-id` rename, and no workflow calls that action any more. --- .github/actionlint.yaml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 19b051f..7a9720b 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -3,13 +3,3 @@ self-hosted-runner: labels: - gpu-bench - gpu-ci - -paths: - .github/workflows/**/*.{yml,yaml}: - ignore: - # `actions/create-github-app-token@v3` renamed `app-id` to `client-id` - # and deprecated the former. actionlint's bundled metadata for the action - # predates the rename, so it reports the new input as unknown and the - # deprecated one as missing. Drop these once actionlint refreshes it. - - 'input "client-id" is not defined in action "actions/create-github-app-token' - - 'missing input "app-id" which is required by action "actions/create-github-app-token' From ae5b0367c3cf3a5cf64501adb803cd8f09816de1 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:05:59 -0400 Subject: [PATCH 6/9] Pin third-party actions to commit SHAs Every `uses:` now carries a full commit SHA with a version comment, which is the form Dependabot reads to offer updates. `taiki-e/install-action`'s per-tool tags (`@nextest`, `@cargo-udeps`, `@cargo-llvm-cov`) move and cannot be pinned, so those call sites switch to the action's `tool:` input against a pinnable release. --- .github/actions/codecov/action.yml | 10 +++++++--- .github/actions/gpu-bench/action.yml | 10 +++++----- .github/actions/gpu-ci/action.yml | 4 +++- .github/actions/links-check/action.yml | 2 +- .github/actions/release-pr/action.yml | 4 ++-- .github/actions/tag-release/action.yml | 4 ++-- .github/actions/typos/action.yml | 4 ++-- .github/actions/unused-deps/action.yml | 4 +++- .github/workflows/actions-lint.yml | 2 +- .github/workflows/docs.yml | 6 +++--- .github/workflows/licenses-audits.yml | 4 ++-- .github/workflows/test.yml | 8 ++++---- .gitignore | 1 + 13 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/actions/codecov/action.yml b/.github/actions/codecov/action.yml index 1de3e65..3b59d4d 100644 --- a/.github/actions/codecov/action.yml +++ b/.github/actions/codecov/action.yml @@ -19,9 +19,13 @@ runs: packages: "${{ inputs.packages }}" - shell: bash run: rustup component add llvm-tools-preview - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@c3ec0de9ae7f1019cea21aa96aa0a895b9552063 # v2.87.9 + with: + tool: nextest - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov + uses: taiki-e/install-action@c3ec0de9ae7f1019cea21aa96aa0a895b9552063 # v2.87.9 + with: + tool: cargo-llvm-cov - name: Clean the workspace shell: bash run: cargo llvm-cov clean --workspace @@ -32,6 +36,6 @@ runs: shell: bash run: cargo llvm-cov nextest --lcov --output-path lcov.info --profile ci --release --workspace - name: Upload coverage data to codecov - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: lcov.info diff --git a/.github/actions/gpu-bench/action.yml b/.github/actions/gpu-bench/action.yml index 19c669f..2251b1a 100644 --- a/.github/actions/gpu-bench/action.yml +++ b/.github/actions/gpu-bench/action.yml @@ -30,7 +30,7 @@ runs: packages: "${{ inputs.packages }}" # `git-auto-commit-action` below pushes to `gh-pages` with the credentials # this checkout persists, so they cannot be disabled here. - - uses: actions/checkout@v7 # zizmor: ignore[artipacked] + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install criterion shell: bash run: | @@ -45,7 +45,7 @@ runs: echo "GPU_ID=$(echo $GPU_NAME | awk '{ print $NF }')" | tee -a $GITHUB_ENV # Checkout gh-pages to check for cached bench result - name: Checkout gh-pages - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: gh-pages path: gh-pages @@ -63,7 +63,7 @@ runs: fi working-directory: ${{ github.workspace }}/gh-pages # Checkout base branch for comparative bench - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: steps.cached-bench.outputs.cached == 'false' with: ref: ${{ github.base_ref }} @@ -110,7 +110,7 @@ runs: shell: bash run: cat "$BASE_COMMIT.json" "$GITHUB_SHA.json" | criterion-table > BENCHMARKS.md - name: Write bench on commit comment - uses: peter-evans/commit-comment@v4 + uses: peter-evans/commit-comment@f6d60c65d05bb59f750fa51ad3de1d443ba0eb52 # v4.0.0 with: body-path: BENCHMARKS.md # Check for a slowdown >= 10%. If so, open an issue but don't block merge. @@ -158,7 +158,7 @@ runs: working-directory: ${{ github.workspace }} - name: Commit bench result to `gh-pages` branch if no regression if: steps.regression-check.outputs.regression != 'true' - uses: stefanzweifel/git-auto-commit-action@v7 + uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 with: branch: gh-pages commit_message: '[automated] GPU Benchmark from PR #${{ env.PR_NUMBER }}' diff --git a/.github/actions/gpu-ci/action.yml b/.github/actions/gpu-ci/action.yml index 26499de..2728f3d 100644 --- a/.github/actions/gpu-ci/action.yml +++ b/.github/actions/gpu-ci/action.yml @@ -35,7 +35,9 @@ runs: if: inputs.packages != '' with: packages: "${{ inputs.packages }}" - - uses: taiki-e/install-action@nextest + - uses: taiki-e/install-action@c3ec0de9ae7f1019cea21aa96aa0a895b9552063 # v2.87.9 + with: + tool: nextest - name: GPU tests shell: bash env: diff --git a/.github/actions/links-check/action.yml b/.github/actions/links-check/action.yml index 111f50e..d1bcc3c 100644 --- a/.github/actions/links-check/action.yml +++ b/.github/actions/links-check/action.yml @@ -20,7 +20,7 @@ runs: steps: - name: Link Checker id: lychee - uses: lycheeverse/lychee-action@v2.9.0 + uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0 with: fail: ${{ inputs.fail-fast }} env: diff --git a/.github/actions/release-pr/action.yml b/.github/actions/release-pr/action.yml index 777abae..8e3133b 100644 --- a/.github/actions/release-pr/action.yml +++ b/.github/actions/release-pr/action.yml @@ -55,7 +55,7 @@ runs: # Rust is incidental here, so provision a toolchain rather than requiring # one from the caller. Empty `rustflags` so the install doesn't fail on # `-D warnings`. - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: cache: false matcher: false @@ -262,7 +262,7 @@ runs: # TODO: Also open PR to `dev` to bump version if this is the latest release - name: Create release PR - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: token: ${{ inputs.token }} add-paths: | diff --git a/.github/actions/tag-release/action.yml b/.github/actions/tag-release/action.yml index de629bf..2155fa0 100644 --- a/.github/actions/tag-release/action.yml +++ b/.github/actions/tag-release/action.yml @@ -97,7 +97,7 @@ runs: # TODO: Add an automatic labeler for PRs based on title/commit prefix - name: Build Changelog id: github-release - uses: mikepenz/release-changelog-builder-action@v6 + uses: mikepenz/release-changelog-builder-action@cb021f9b36a51a7c6f18e4679b6fa2cb77a1260c # v6.3.0 with: mode: "COMMIT" configuration: ${{ inputs.changelog-config-file }} @@ -111,7 +111,7 @@ runs: # `gh release` has no equivalent of `allowUpdates`, which re-releases an # existing tag rather than failing. Replacing this would mean # hand-rolling create-or-update logic in a release-critical path. - uses: ncipollo/release-action@v1 # zizmor: ignore[superfluous-actions] + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 with: body: ${{ steps.github-release.outputs.changelog }} tag: ${{ steps.get-tag.outputs.release-tag }} diff --git a/.github/actions/typos/action.yml b/.github/actions/typos/action.yml index 2ec1e07..3aee514 100644 --- a/.github/actions/typos/action.yml +++ b/.github/actions/typos/action.yml @@ -17,7 +17,7 @@ runs: # Rust is incidental here, so provision a toolchain rather than requiring # one from the caller. Empty `rustflags` so the install doesn't fail on # `-D warnings`. - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: rustflags: '' - name: Install typos binary @@ -59,7 +59,7 @@ runs: FILE_PATHS=":!*\_body.md,$(git status --porcelain | awk -F. '{OFS=""; print "**/*."$NF}' | sort -u | paste -sd,)" echo "paths=$FILE_PATHS" | tee -a $GITHUB_OUTPUT - name: Create pull request - uses: peter-evans/create-pull-request@v8 + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 if: steps.typo-check.outputs.typos == 'true' with: token: ${{ inputs.token }} diff --git a/.github/actions/unused-deps/action.yml b/.github/actions/unused-deps/action.yml index f9858df..44cbb6f 100644 --- a/.github/actions/unused-deps/action.yml +++ b/.github/actions/unused-deps/action.yml @@ -24,7 +24,9 @@ runs: if: inputs.packages != '' with: packages: "${{ inputs.packages }}" - - uses: taiki-e/install-action@cargo-udeps + - uses: taiki-e/install-action@c3ec0de9ae7f1019cea21aa96aa0a895b9552063 # v2.87.9 + with: + tool: cargo-udeps # Normally running cargo-udeps requires use of a nightly compiler # In order to have a more stable and less noisy experience, lets instead # opt to use the stable toolchain specified via the 'rust-toolchain' file diff --git a/.github/workflows/actions-lint.yml b/.github/workflows/actions-lint.yml index 8ce5fde..317b90c 100644 --- a/.github/workflows/actions-lint.yml +++ b/.github/workflows/actions-lint.yml @@ -18,7 +18,7 @@ jobs: name: Lint workflows runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: $/.github/actions/lint-workflows diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2add0e8..5b08e50 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -13,10 +13,10 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 - name: Generate documentation env: RUSTDOCFLAGS: "--enable-index-page -Zunstable-options" @@ -24,7 +24,7 @@ jobs: run: | cargo doc --workspace --no-deps - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v4 + uses: peaceiris/actions-gh-pages@84c30a85c19949d7eee79c4ff27748b70285e453 # v4.1.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc diff --git a/.github/workflows/licenses-audits.yml b/.github/workflows/licenses-audits.yml index d4547f4..fc25aa7 100644 --- a/.github/workflows/licenses-audits.yml +++ b/.github/workflows/licenses-audits.yml @@ -13,7 +13,7 @@ jobs: name: cargo-deny (advisories, licenses, bans, ...) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: EmbarkStudios/cargo-deny-action@v2 + - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a39d984..b9da006 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install stub gh @@ -81,12 +81,12 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Generate fixture crate run: cargo init --lib --name fixture . - - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0 with: # Don't save caches from test fixtures cache: false @@ -101,7 +101,7 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install stub gh diff --git a/.gitignore b/.gitignore index 764515c..382d616 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/target/ **/Cargo.lock +__pycache__/ From f36b61549cdf2c9b99d645c909572372b060f197 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:06:05 -0400 Subject: [PATCH 7/9] Enforce SHA-pinned actions with pinact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pinact fails the lint job when an action is not pinned to a full commit SHA. `verify` additionally rejects a pinned SHA whose version comment names a different release — the form an attacker would use to make a malicious commit look like an innocuous tag. The comments pinact maintains are also what Dependabot reads, so pinned actions still receive update PRs. `fix: "false"` implies `skip_push`, so the check runs no git commands and needs no write permissions. Wired into lint-workflows, so callers pick it up when they bump their pin. --- .github/actions/lint-workflows/action.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/actions/lint-workflows/action.yml b/.github/actions/lint-workflows/action.yml index aea3a89..2a87f6f 100644 --- a/.github/actions/lint-workflows/action.yml +++ b/.github/actions/lint-workflows/action.yml @@ -1,7 +1,9 @@ # Assumes the repo under test is already checked out. name: Lint workflows -description: Run actionlint and zizmor over a repo's workflows and composite actions +description: >- + Run actionlint, shellcheck, zizmor and pinact over a repo's workflows and + composite actions inputs: # actionlint pipes every `run:` block through shellcheck, whose info- and @@ -18,7 +20,7 @@ inputs: runs: using: composite steps: - - uses: raven-actions/actionlint@v2 + - uses: raven-actions/actionlint@3d39aea434753780c3b3d4a1a31c854b4dbf49d7 # v2.2.0 env: SHELLCHECK_OPTS: -S ${{ inputs.shellcheck-severity }} with: @@ -31,10 +33,17 @@ runs: SEVERITY: ${{ inputs.shellcheck-severity }} ACTION_PATH: ${{ github.action_path }} run: python3 "$ACTION_PATH/shellcheck_actions.py" + # `fix: false` implies `skip_push`, so this only reports: no git + # commands, no write permissions. `verify` additionally rejects a + # pinned SHA whose version comment names a different release. + - uses: suzuki-shunsuke/pinact-action@896d595f299e71d65b9d28349d6956abe144390a # v3.0.0 + with: + fix: "false" + verify: "true" # Note that `continue-on-error` is silently ignored on composite action # steps, so any zizmor finding fails the calling job. Suppress findings # that don't apply via `.github/zizmor.yml` or `# zizmor: ignore` comments. - - uses: zizmorcore/zizmor-action@v0.6.2 + - uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 with: config: ${{ inputs.zizmor-config }} # Mutually exclusive with `annotations`; the Security tab needs From 73d4404c2f6b89e049df7bf864087754afddf3cb Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:12:33 -0400 Subject: [PATCH 8/9] Use a documented glob for the composite action directories Dependabot documents `directories` globbing with `*`, not `**`, and every composite action is one level under `.github/actions/`. An unsupported pattern would match nothing and silently stop update PRs. --- .github/dependabot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 43b33f0..662662d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ updates: # directories must be listed separately for their dependencies to be seen directories: - "/" - - "/.github/actions/**" + - "/.github/actions/*" schedule: interval: "weekly" cooldown: From a024ed0c9985175324f5fa7a707a71848289643c Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Wed, 23 Sep 2026 22:33:20 -0400 Subject: [PATCH 9/9] Fixes --- .github/actions/gpu-bench/action.yml | 6 +++--- .github/actions/lint-workflows/action.yml | 6 ++++-- .github/actions/lints/action.yml | 2 +- .github/actions/rust-version-check/action.yml | 17 +++++++++-------- .github/actions/tag-release/action.yml | 2 +- .github/zizmor.yml | 5 ----- 6 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 .github/zizmor.yml diff --git a/.github/actions/gpu-bench/action.yml b/.github/actions/gpu-bench/action.yml index 2251b1a..a7ca5ae 100644 --- a/.github/actions/gpu-bench/action.yml +++ b/.github/actions/gpu-bench/action.yml @@ -30,7 +30,7 @@ runs: packages: "${{ inputs.packages }}" # `git-auto-commit-action` below pushes to `gh-pages` with the credentials # this checkout persists, so they cannot be disabled here. - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # zizmor: ignore[artipacked] - name: Install criterion shell: bash run: | @@ -38,7 +38,7 @@ runs: cargo install criterion-table - name: Set env vars shell: bash - run: | + run: | # zizmor: ignore[github-env] REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F'/' '{ print toupper($2) }') echo "${REPOSITORY_NAME}_BENCH_OUTPUT=commit-comment" | tee -a $GITHUB_ENV echo "BASE_COMMIT=${{ github.event.merge_group.base_sha }}" | tee -a $GITHUB_ENV @@ -138,7 +138,7 @@ runs: shell: bash env: HEAD_REF: ${{ github.event.merge_group.head_ref }} - run: | + run: | # zizmor: ignore[github-env] echo "PR_NUMBER=$(echo "$HEAD_REF" | sed -e 's/.*pr-\(.*\)-.*/\1/')" | tee -a $GITHUB_ENV - name: Open issue on regression if: steps.regression-check.outputs.regression == 'true' diff --git a/.github/actions/lint-workflows/action.yml b/.github/actions/lint-workflows/action.yml index 2a87f6f..48a478a 100644 --- a/.github/actions/lint-workflows/action.yml +++ b/.github/actions/lint-workflows/action.yml @@ -25,8 +25,10 @@ runs: SHELLCHECK_OPTS: -S ${{ inputs.shellcheck-severity }} with: # actionlint doesn't know the `$/` self-repository syntax yet and - # misparses it as {owner}/{repo}@{ref} - flags: -ignore 'specifying action "\$/.+" in invalid format' + # misparses it as {owner}/{repo}@{ref}. The wrapper splits `flags` on + # whitespace and only honours double quotes, so the pattern must not + # contain literal spaces. + flags: -ignore specifying\saction\s"\$/.+"\sin\sinvalid\sformat - name: Shellcheck composite action scripts shell: bash env: diff --git a/.github/actions/lints/action.yml b/.github/actions/lints/action.yml index 64bdedf..606cb66 100644 --- a/.github/actions/lints/action.yml +++ b/.github/actions/lints/action.yml @@ -43,7 +43,7 @@ runs: if cargo --list | grep -q xclippy; then cargo xclippy -Dwarnings else - cargo clippy -Dwarnings + cargo clippy -- -Dwarnings fi - name: Doctests shell: bash diff --git a/.github/actions/rust-version-check/action.yml b/.github/actions/rust-version-check/action.yml index 119723f..1de4859 100644 --- a/.github/actions/rust-version-check/action.yml +++ b/.github/actions/rust-version-check/action.yml @@ -2,9 +2,10 @@ name: Rust version check description: >- Check whether the Rust version specified in `rust-toolchain.toml` is out of date with the latest stable, opening an issue if so. Compares the full - `..` of `rustup show` with `rustup check`, because the - patch version auto-updates if unspecified in `rust-toolchain.toml`. Assumes - the repo is already checked out. The calling job needs `issues: write`. + `..` of `rustc --version` with `rustup check`, because + the patch version auto-updates if unspecified in `rust-toolchain.toml`. + Assumes the repo is already checked out. The calling job needs + `issues: write`. inputs: token: @@ -15,23 +16,23 @@ inputs: runs: using: composite steps: - # `rustup show` below must report the `rust-toolchain.toml` version while - # `rustup check` reports the latest stable, so both must be installed + # `rustc --version` below must report the `rust-toolchain.toml` version + # while `rustup check` reports the latest stable, so both must be installed - name: Install latest stable shell: bash run: rustup toolchain install stable - name: Parse rust-toolchain.toml shell: bash - run: echo "TOOLCHAIN_VERSION=$(rustup show | grep rustc | awk '{ print $2 }')" | tee -a $GITHUB_ENV + run: echo "TOOLCHAIN_VERSION=$(rustc --version | awk '{ print $2 }')" | tee -a $GITHUB_ENV # zizmor: ignore[github-env] - name: Get latest stable Rust version shell: bash - run: echo "RUST_VERSION=$(rustup check | grep stable | awk '{print $(NF-2)}')" | tee -a $GITHUB_ENV + run: echo "RUST_VERSION=$(rustup check | grep stable | awk '{print $(NF-2)}')" | tee -a $GITHUB_ENV # zizmor: ignore[github-env] - name: Compare Rust versions shell: bash - run: | + run: | # zizmor: ignore[github-env] if [[ $TOOLCHAIN_VERSION < $RUST_VERSION ]]; then echo "VERSION_MISMATCH=true" | tee -a $GITHUB_ENV else diff --git a/.github/actions/tag-release/action.yml b/.github/actions/tag-release/action.yml index 2155fa0..ed2ab0b 100644 --- a/.github/actions/tag-release/action.yml +++ b/.github/actions/tag-release/action.yml @@ -111,7 +111,7 @@ runs: # `gh release` has no equivalent of `allowUpdates`, which re-releases an # existing tag rather than failing. Replacing this would mean # hand-rolling create-or-update logic in a release-critical path. - uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 + uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0 # zizmor: ignore[superfluous-actions] with: body: ${{ steps.github-release.outputs.changelog }} tag: ${{ steps.get-tag.outputs.release-tag }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml deleted file mode 100644 index 41a93d1..0000000 --- a/.github/zizmor.yml +++ /dev/null @@ -1,5 +0,0 @@ -rules: - # Every action here is version-pinned and tracked by Dependabot; hash-pinning - # is a posture this org has not adopted. - unpinned-uses: - disable: true