From a880b51ff3dbfbee812a857b456b8957b61a1685 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:06:14 -0400 Subject: [PATCH 1/4] rust-version: update flake.nix and open the PR; fix version checks rust-version now detects the release channel from the pinned toolchain instead of taking it as an input, so a nightly pin can never be rewritten to a stable release or the other way round; beta pins are rejected. With `update-flake` it also rewrites the `sha256` of the fenix `fromToolchainFile` call in flake.nix. fenix fetches the release's channel manifest with `pkgs.fetchurl` at evaluation time, so the hash is that manifest's, computed with curl and openssl rather than Nix. Flake inputs are deliberately left alone. With `pr` it opens the pull request itself through create-pull-request, adding only the files it edited, so callers no longer wire that up. The PR uses GITHUB_TOKEN by default, so a maintainer approves its CI run. Also installs the channel before `rustup check` and anchors the channel line it parses, so a repository that pins a version or date no longer reads as up to date, and drops the stale cleanup step from the pre-`$/` checkout convention. rust-version-check compared versions as strings, which orders 1.100 before 1.99 and would have stopped reporting once the minor version reached three digits; it now sorts them as versions and anchors the same parse. The lint-workflows shellcheck helper also covers a repository that is itself an action, i.e. has action.yml at its root, and skips manifests without `runs.steps`. The test workflow exercises rust-version on an up-to-date pin, an outdated stable pin with a flake, an outdated nightly pin, and a beta pin. --- .../lint-workflows/shellcheck_actions.py | 15 +- .github/actions/rust-version-check/action.yml | 9 +- .github/actions/rust-version/action.yml | 198 +++++++++++++++--- .github/workflows/test.yml | 94 +++++++++ 4 files changed, 284 insertions(+), 32 deletions(-) diff --git a/.github/actions/lint-workflows/shellcheck_actions.py b/.github/actions/lint-workflows/shellcheck_actions.py index bef8c55..0726bdc 100644 --- a/.github/actions/lint-workflows/shellcheck_actions.py +++ b/.github/actions/lint-workflows/shellcheck_actions.py @@ -3,7 +3,10 @@ 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. +masks them in workflow scripts. Covers the actions under `.github/actions` +and a repository that is itself an action, i.e. has `action.yml` at its +root; a manifest without `runs.steps` (a JavaScript or Docker action) has +no scripts and is skipped. """ import json @@ -19,13 +22,17 @@ def main(): severity = os.environ.get("SEVERITY", "warning") out = Path(tempfile.mkdtemp()) scripts = [] - for manifest in sorted(Path(".github/actions").glob("*/action.y*ml")): + manifests = sorted(Path(".").glob("action.y*ml")) + sorted( + Path(".github/actions").glob("*/action.y*ml") + ) + for manifest in manifests: 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"]): + steps = json.loads(parsed.stdout).get("runs", {}).get("steps") or [] + for i, step in enumerate(steps): if step.get("shell") == "bash": - script = out / f"{manifest.parent.name}-{i}.sh" + script = out / f"{manifest.parent.resolve().name}-{i}.sh" script.write_text(re.sub(r"\$\{\{.*?\}\}", "EXPR", step["run"])) scripts.append(script) if scripts: diff --git a/.github/actions/rust-version-check/action.yml b/.github/actions/rust-version-check/action.yml index 1de4859..4563141 100644 --- a/.github/actions/rust-version-check/action.yml +++ b/.github/actions/rust-version-check/action.yml @@ -28,12 +28,17 @@ runs: - name: Get latest stable Rust version shell: bash - run: echo "RUST_VERSION=$(rustup check | grep stable | awk '{print $(NF-2)}')" | tee -a $GITHUB_ENV # zizmor: ignore[github-env] + # `^stable-` selects the channel's own line; a toolchain pinned to a + # version number is listed as `-` and must not match. + 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: | # zizmor: ignore[github-env] - if [[ $TOOLCHAIN_VERSION < $RUST_VERSION ]]; then + # Numeric comparison: a string compare orders 1.100 before 1.99 and + # would stop reporting once the minor version reaches three digits. + oldest=$(printf '%s\n' "$TOOLCHAIN_VERSION" "$RUST_VERSION" | sort -V | head -n 1) + if [[ "$TOOLCHAIN_VERSION" != "$RUST_VERSION" && "$oldest" == "$TOOLCHAIN_VERSION" ]]; then echo "VERSION_MISMATCH=true" | tee -a $GITHUB_ENV else echo "VERSION_MISMATCH=false" | tee -a $GITHUB_ENV diff --git a/.github/actions/rust-version/action.yml b/.github/actions/rust-version/action.yml index 3d4e053..8f80820 100644 --- a/.github/actions/rust-version/action.yml +++ b/.github/actions/rust-version/action.yml @@ -1,22 +1,56 @@ -# Checks if `rust-toolchain.toml` has an outdated Rust version. If so, updates to the latest Rust release for either stable or nightly -# Assumes `rust-toolchain.toml` exists at the base of the repo or an optional subdir -# Returns the outdated test result, and old and new Rust versions as output -# -# The workflow caller is then responsible for making a commit or opening a PR with `peter-evans/create-pull-request` -# Assumes the caller checks this action out in `${{ github.workspace }}/ci-workflows`, as this path is removed at the end of the action -name: Rust version check +name: Rust version update -description: Check if crate has outdated Rust version, updating `rust-toolchain.toml` to latest release if so +description: >- + Check whether `rust-toolchain.toml` pins an outdated Rust version and, if + so, rewrite its `channel` to the latest release on the same release + channel: a `nightly` pin moves to the latest nightly, `stable` or a + version number to the latest stable, and `beta` is rejected. Reports the + result and the old and new versions as outputs and, + with `pr`, opens a pull request with the change; otherwise the caller + commits it. A PR opened with the default `GITHUB_TOKEN` does not start CI + on its own; a maintainer approves the run, which is the intended review + step. The calling job needs `contents` and `pull-requests` write + permissions for `pr`. Assumes the repo is already checked out with + `rust-toolchain.toml` at its root or in `workdir`, and that `rustup` is + installed. With `update-flake`, also rewrites the `sha256` of the fenix + `fromToolchainFile` call in the `flake.nix` next to `rust-toolchain.toml`, + which hashes the release's channel manifest; this needs only `curl` and + `openssl`, not Nix. Flake inputs are left alone, so `flake.lock` does not + change. inputs: - toolchain: - description: "Stable or nightly. Defaults to stable" - required: false workdir: description: "Optional subdirectory" required: false + update-flake: + description: + Also update the `sha256` of fenix's `fromToolchainFile { file; sha256; }` + in `flake.nix` to the new release's + required: false + default: 'false' + pr: + description: + Open a pull request with the update on an `update/rust-` + branch, against the checked-out branch + required: false + default: 'false' + token: + description: GitHub token with `contents` and `pull-requests` write access + required: false + default: ${{ github.token }} + labels: + description: Comma-separated labels for the pull request + required: false + default: '' + reviewers: + description: Comma-separated reviewers for the pull request + required: false + default: '' outputs: + channel: + description: "`stable` or `nightly`, the release channel of the pinned toolchain" + value: ${{ steps.channel.outputs.channel }} outdated: description: "Boolean denoting whether `rust-toolchain.toml` is outdated" value: ${{ steps.compare-versions.outputs.outdated }} @@ -26,33 +60,71 @@ outputs: new-version: description: "Latest Rust version" value: ${{ steps.latest-rust.outputs.version }} + nix-hash: + description: "New fenix `sha256`, when `flake.nix` was updated" + value: ${{ steps.nix-hash.outputs.hash }} + pr-number: + description: "Number of the pull request, when `pr` opened or updated one" + value: ${{ steps.pr.outputs.pull-request-number }} + pr-url: + description: "URL of the pull request, when `pr` opened or updated one" + value: ${{ steps.pr.outputs.pull-request-url }} runs: using: "composite" steps: + # The release channel comes from the pinned toolchain itself, so a nightly + # pin can never be rewritten to a stable version or the other way round. + - name: Detect the release channel + id: channel + shell: bash + run: | + pin=$(sed -n 's/^channel *= *"\(.*\)".*/\1/p' rust-toolchain.toml) + case "$pin" in + nightly*) channel=nightly ;; + stable | [0-9]*) channel=stable ;; + *) + echo "::error::rust-toolchain.toml pins '$pin'; only stable and nightly toolchains are updated" + exit 1 + ;; + esac + echo "channel=$channel" | tee -a "$GITHUB_OUTPUT" + working-directory: ${{ github.workspace }}/${{ inputs.workdir }} + # `rustup show` below must report the `rust-toolchain.toml` version while + # `rustup check` reports the channel's latest release, so both must be + # installed; without the channel, `rustup check` has no line to parse and + # the comparison silently reports up to date. + - name: Install latest ${{ steps.channel.outputs.channel }} + shell: bash + env: + CHANNEL: ${{ steps.channel.outputs.channel }} + run: rustup toolchain install "$CHANNEL" --profile minimal - name: Parse `rust-toolchain.toml` shell: bash id: current-rust env: - TOOLCHAIN: ${{ inputs.toolchain }} + CHANNEL: ${{ steps.channel.outputs.channel }} run: | - if [[ "$TOOLCHAIN" == "nightly" ]]; then + if [[ "$CHANNEL" == "nightly" ]]; then version=$(rustup show | grep rustc | awk -F'[()]| ' '{ print $(NF-1) }') else version=$(rustup show | grep rustc | awk '{ printf $2 }') fi echo "version=$version" | tee -a $GITHUB_OUTPUT working-directory: ${{ github.workspace }}/${{ inputs.workdir }} - - name: Get latest `${{ inputs.toolchain }}` Rust release + - name: Get latest ${{ steps.channel.outputs.channel }} Rust release id: latest-rust shell: bash env: - TOOLCHAIN: ${{ inputs.toolchain }} + CHANNEL: ${{ steps.channel.outputs.channel }} + # The anchored patterns select the channel's own line: a toolchain + # pinned to a date or version is listed as `nightly--` or + # `-` and must not match. run: | - if [[ "$TOOLCHAIN" == "nightly" ]]; then - version=$(rustup check | grep "$TOOLCHAIN" | awk -F'[()]| ' '{print $(NF-1)}') + if [[ "$CHANNEL" == "nightly" ]]; then + version=$(rustup check | grep -E '^nightly-[^0-9]' | awk -F'[()]| ' '{print $(NF-1)}') else - version=$(rustup check | grep stable | awk '{print $(NF-2)}') + version=$(rustup check | grep '^stable-' | awk '{print $(NF-2)}') fi echo "version=$version" | tee -a $GITHUB_OUTPUT - name: Compare Rust versions @@ -66,24 +138,98 @@ runs: echo "outdated=true" | tee -a $GITHUB_OUTPUT fi working-directory: ${{ github.workspace }}/${{ inputs.workdir }} - - name: Update `Cargo.toml` + - name: Update `rust-toolchain.toml` if: steps.compare-versions.outputs.outdated == 'true' + id: update-toolchain shell: bash env: - TOOLCHAIN: ${{ inputs.toolchain }} + CHANNEL: ${{ steps.channel.outputs.channel }} LATEST_VERSION: ${{ steps.latest-rust.outputs.version }} run: | - if [[ "$TOOLCHAIN" == "nightly" ]]; then - sed -i "s/channel = .*/channel = \"nightly-$LATEST_VERSION\"/" rust-toolchain.toml + if [[ "$CHANNEL" == "nightly" ]]; then + toolchain="nightly-$LATEST_VERSION" else - sed -i "s/channel = .*/channel = \"$LATEST_VERSION\"/" rust-toolchain.toml + toolchain="$LATEST_VERSION" fi + sed -i "s/channel = .*/channel = \"$toolchain\"/" rust-toolchain.toml echo "Outdated Rust, updating" cat rust-toolchain.toml + echo "toolchain=$toolchain" | tee -a "$GITHUB_OUTPUT" working-directory: ${{ github.workspace }}/${{ inputs.workdir }} - - name: Clean up + # fenix's `fromToolchainFile { file; sha256; }` fetches the release's + # channel manifest with `pkgs.fetchurl` and reads it at evaluation time, + # so `sha256` is that manifest's hash, in the SRI form `nix hash` prints. + - name: Update the fenix `sha256` in `flake.nix` + if: steps.compare-versions.outputs.outdated == 'true' && inputs.update-flake == 'true' + id: nix-hash shell: bash env: - WORKSPACE: ${{ github.workspace }} + CHANNEL: ${{ steps.channel.outputs.channel }} + VERSION: ${{ steps.latest-rust.outputs.version }} run: | - rm -rf "$WORKSPACE/ci-workflows" + if [[ "$CHANNEL" == "nightly" ]]; then + url="https://static.rust-lang.org/dist/$VERSION/channel-rust-nightly.toml" + else + url="https://static.rust-lang.org/dist/channel-rust-$VERSION.toml" + fi + hash="sha256-$(curl -fsSL --retry 3 "$url" | openssl dgst -sha256 -binary | base64 -w0)" + sed -i "/fromToolchainFile/,/}/ s|sha256 = \"sha256-[^\"]*\"|sha256 = \"$hash\"|" flake.nix + if ! grep -qF "$hash" flake.nix; then + echo "::error::flake.nix has no fromToolchainFile call with a sha256 to update" + exit 1 + fi + echo "hash=$hash" | tee -a "$GITHUB_OUTPUT" + working-directory: ${{ github.workspace }}/${{ inputs.workdir }} + # Only the files this action edits go into the PR, so unrelated changes in + # the caller's working tree never ride along. + - name: Describe the pull request + if: steps.compare-versions.outputs.outdated == 'true' && inputs.pr == 'true' + id: describe + shell: bash + env: + WORKDIR: ${{ inputs.workdir }} + UPDATE_FLAKE: ${{ inputs.update-flake }} + CHANNEL: ${{ steps.channel.outputs.channel }} + OLD_VERSION: ${{ steps.current-rust.outputs.version }} + TOOLCHAIN: ${{ steps.update-toolchain.outputs.toolchain }} + NIX_HASH: ${{ steps.nix-hash.outputs.hash }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + prefix="${WORKDIR:+${WORKDIR%/}/}" + { + echo "paths<> "$GITHUB_OUTPUT" + + if [[ "$CHANNEL" == "nightly" ]]; then + release="\`$TOOLCHAIN\`" + else + release="[$TOOLCHAIN](https://github.com/rust-lang/rust/releases/tag/$TOOLCHAIN)" + fi + { + echo "body<> "$GITHUB_OUTPUT" + - name: Open a pull request + if: steps.compare-versions.outputs.outdated == 'true' && inputs.pr == 'true' + id: pr + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ inputs.token }} + add-paths: ${{ steps.describe.outputs.paths }} + branch: update/rust-${{ steps.update-toolchain.outputs.toolchain }} + delete-branch: true + commit-message: "chore: Update Rust to ${{ steps.update-toolchain.outputs.toolchain }}" + title: "chore: Update Rust to ${{ steps.update-toolchain.outputs.toolchain }}" + body: ${{ steps.describe.outputs.body }} + labels: ${{ inputs.labels }} + reviewers: ${{ inputs.reviewers }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9da006..82ae62a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -136,3 +136,97 @@ jobs: 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" + + # An up-to-date pin leaves both files alone; an outdated pin moves the + # channel and the fenix hash to the latest stable. The expected hash is + # recomputed here independently of the action's own code. + rust-version: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Pin an up-to-date toolchain with a placeholder fenix hash + run: | + printf '[toolchain]\nchannel = "stable"\n' > rust-toolchain.toml + cat > flake.nix <<'EOF' + { + rustToolchain = fenix.packages.x86_64-linux.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; + } + EOF + - uses: $/.github/actions/rust-version + id: current + with: + update-flake: 'true' + - name: Assert an up-to-date pin is left alone + env: + CHANNEL: ${{ steps.current.outputs.channel }} + OUTDATED: ${{ steps.current.outputs.outdated }} + run: | + set -x + [[ "$CHANNEL" == stable ]] + [[ "$OUTDATED" != true ]] + grep -qF 'channel = "stable"' rust-toolchain.toml + grep -qF 'sha256-AAAA' flake.nix + - 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 + id: outdated + with: + update-flake: 'true' + - name: Assert the channel and fenix hash moved to the latest stable + env: + OUTDATED: ${{ steps.outdated.outputs.outdated }} + OLD_VERSION: ${{ steps.outdated.outputs.old-version }} + NEW_VERSION: ${{ steps.outdated.outputs.new-version }} + NIX_HASH: ${{ steps.outdated.outputs.nix-hash }} + run: | + set -x + [[ "$OUTDATED" == true ]] + [[ "$OLD_VERSION" == 1.70.0 ]] + [[ "$NEW_VERSION" == "$(rustc +stable --version | awk '{ print $2 }')" ]] + grep -qF "channel = \"$NEW_VERSION\"" rust-toolchain.toml + expected="sha256-$(curl -fsSL "https://static.rust-lang.org/dist/channel-rust-$NEW_VERSION.toml" | openssl dgst -sha256 -binary | base64 -w0)" + [[ "$NIX_HASH" == "$expected" ]] + grep -qF "sha256 = \"$expected\"" flake.nix + # A nightly pin stays on the nightly channel, moving to the latest date + - name: Pin an outdated nightly + run: | + printf '[toolchain]\nchannel = "nightly-2025-01-01"\n' > rust-toolchain.toml + rustup toolchain install nightly-2025-01-01 --profile minimal + - uses: $/.github/actions/rust-version + id: nightly + - name: Assert the channel moved to the latest nightly + env: + CHANNEL: ${{ steps.nightly.outputs.channel }} + OUTDATED: ${{ steps.nightly.outputs.outdated }} + OLD_VERSION: ${{ steps.nightly.outputs.old-version }} + NEW_VERSION: ${{ steps.nightly.outputs.new-version }} + run: | + set -x + [[ "$CHANNEL" == nightly ]] + [[ "$OUTDATED" == true ]] + [[ "$OLD_VERSION" == 2025-01-01 ]] + [[ "$NEW_VERSION" == "$(rustc +nightly --version | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')" ]] + grep -qF "channel = \"nightly-$NEW_VERSION\"" rust-toolchain.toml + # A beta pin is on neither supported channel and must fail before any edit + - name: Pin beta + run: printf '[toolchain]\nchannel = "beta"\n' > rust-toolchain.toml + - uses: $/.github/actions/rust-version + id: beta + continue-on-error: true + - name: Assert the beta pin was rejected untouched + env: + BETA_OUTCOME: ${{ steps.beta.outcome }} + run: | + set -x + [[ "$BETA_OUTCOME" == failure ]] + grep -qF 'channel = "beta"' rust-toolchain.toml From 10a06419eac5f83476602c961b69f4a0178a8592 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:00:56 -0400 Subject: [PATCH 2/4] Update Rust flake inputs with the toolchain --- .github/actions/rust-version/action.yml | 39 +++++++++++++++++-------- .github/workflows/test.yml | 19 ++++++++++-- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/actions/rust-version/action.yml b/.github/actions/rust-version/action.yml index 8f80820..0e150ea 100644 --- a/.github/actions/rust-version/action.yml +++ b/.github/actions/rust-version/action.yml @@ -12,11 +12,11 @@ description: >- step. The calling job needs `contents` and `pull-requests` write permissions for `pr`. Assumes the repo is already checked out with `rust-toolchain.toml` at its root or in `workdir`, and that `rustup` is - installed. With `update-flake`, also rewrites the `sha256` of the fenix - `fromToolchainFile` call in the `flake.nix` next to `rust-toolchain.toml`, - which hashes the release's channel manifest; this needs only `curl` and - `openssl`, not Nix. Flake inputs are left alone, so `flake.lock` does not - change. + installed. With `update-flake`, also moves the `flake.nix` next to + `rust-toolchain.toml` to the release: the `sha256` of its fenix + `fromToolchainFile` call, which hashes the release's channel manifest, and + the `flake-inputs` in `flake.lock` via `nix flake update`. Requires Nix + 2.19 or newer installed by the caller. inputs: workdir: @@ -24,10 +24,17 @@ inputs: required: false update-flake: description: - Also update the `sha256` of fenix's `fromToolchainFile { file; sha256; }` - in `flake.nix` to the new release's + Also update `flake.nix` for the new release, i.e. the `sha256` of + fenix's `fromToolchainFile { file; sha256; }` and the `flake-inputs` in + `flake.lock` required: false default: 'false' + flake-inputs: + description: + Space-separated flake inputs to update alongside the toolchain when + `update-flake` is set; each must exist in the flake + required: false + default: fenix crane pr: description: Open a pull request with the update on an `update/rust-` @@ -158,26 +165,30 @@ runs: working-directory: ${{ github.workspace }}/${{ inputs.workdir }} # fenix's `fromToolchainFile { file; sha256; }` fetches the release's # channel manifest with `pkgs.fetchurl` and reads it at evaluation time, - # so `sha256` is that manifest's hash, in the SRI form `nix hash` prints. - - name: Update the fenix `sha256` in `flake.nix` + # so `sha256` is that manifest's hash. The Rust build inputs move in the + # same PR so their updates ride on a change that CI has to validate anyway. + - name: Update `flake.nix` for the new release if: steps.compare-versions.outputs.outdated == 'true' && inputs.update-flake == 'true' id: nix-hash shell: bash env: CHANNEL: ${{ steps.channel.outputs.channel }} VERSION: ${{ steps.latest-rust.outputs.version }} + FLAKE_INPUTS: ${{ inputs.flake-inputs }} run: | if [[ "$CHANNEL" == "nightly" ]]; then url="https://static.rust-lang.org/dist/$VERSION/channel-rust-nightly.toml" else url="https://static.rust-lang.org/dist/channel-rust-$VERSION.toml" fi - hash="sha256-$(curl -fsSL --retry 3 "$url" | openssl dgst -sha256 -binary | base64 -w0)" + hash=$(nix store prefetch-file --json "$url" | jq -r .hash) sed -i "/fromToolchainFile/,/}/ s|sha256 = \"sha256-[^\"]*\"|sha256 = \"$hash\"|" flake.nix if ! grep -qF "$hash" flake.nix; then echo "::error::flake.nix has no fromToolchainFile call with a sha256 to update" exit 1 fi + read -ra flake_inputs <<< "$FLAKE_INPUTS" + nix flake update "${flake_inputs[@]}" --refresh echo "hash=$hash" | tee -a "$GITHUB_OUTPUT" working-directory: ${{ github.workspace }}/${{ inputs.workdir }} # Only the files this action edits go into the PR, so unrelated changes in @@ -189,6 +200,7 @@ runs: env: WORKDIR: ${{ inputs.workdir }} UPDATE_FLAKE: ${{ inputs.update-flake }} + FLAKE_INPUTS: ${{ inputs.flake-inputs }} CHANNEL: ${{ steps.channel.outputs.channel }} OLD_VERSION: ${{ steps.current-rust.outputs.version }} TOOLCHAIN: ${{ steps.update-toolchain.outputs.toolchain }} @@ -199,7 +211,10 @@ runs: { echo "paths<> "$GITHUB_OUTPUT" @@ -213,7 +228,7 @@ runs: echo "Updates \`rust-toolchain.toml\` from \`$OLD_VERSION\` to $release." if [[ -n "$NIX_HASH" ]]; then echo - echo "The fenix \`sha256\` in \`flake.nix\` moves with it, to \`$NIX_HASH\`." + echo "The fenix \`sha256\` in \`flake.nix\` moves with it, to \`$NIX_HASH\`, and the \`$FLAKE_INPUTS\` inputs are updated in \`flake.lock\`." fi echo echo "Opened by the [rust-version](https://github.com/argumentcomputer/ci-workflows/tree/main/.github/actions/rust-version) action from [this run]($RUN_URL). CI on this PR starts once a maintainer approves it." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 82ae62a..150a81e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -148,17 +148,27 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31.11.1 + with: + github_access_token: ${{ github.token }} + # A flake in a git checkout only sees tracked files, so the fixture is + # staged for `nix flake update` to find it. - name: Pin an up-to-date toolchain with a placeholder fenix hash run: | printf '[toolchain]\nchannel = "stable"\n' > rust-toolchain.toml cat > flake.nix <<'EOF' { - rustToolchain = fenix.packages.x86_64-linux.fromToolchainFile { - file = ./rust-toolchain.toml; - sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + inputs.fenix.url = "github:nix-community/fenix"; + inputs.crane.url = "github:ipetkov/crane"; + outputs = { fenix, ... }: { + rustToolchain = fenix.packages.x86_64-linux.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + }; }; } EOF + git add flake.nix rust-toolchain.toml - uses: $/.github/actions/rust-version id: current with: @@ -173,6 +183,7 @@ jobs: [[ "$OUTDATED" != true ]] grep -qF 'channel = "stable"' rust-toolchain.toml grep -qF 'sha256-AAAA' flake.nix + [[ ! -e flake.lock ]] - name: Pin an outdated toolchain run: | printf '[toolchain]\nchannel = "1.70.0"\n' > rust-toolchain.toml @@ -197,6 +208,8 @@ jobs: expected="sha256-$(curl -fsSL "https://static.rust-lang.org/dist/channel-rust-$NEW_VERSION.toml" | openssl dgst -sha256 -binary | base64 -w0)" [[ "$NIX_HASH" == "$expected" ]] grep -qF "sha256 = \"$expected\"" flake.nix + # `nix flake update fenix crane` locked both inputs + jq -e '.nodes.fenix.locked.rev and .nodes.crane.locked.rev' flake.lock # A nightly pin stays on the nightly channel, moving to the latest date - name: Pin an outdated nightly run: | From 87bf3a2914a9b27d478cdbf5ee7b535ac9fee7c3 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:06:14 -0400 Subject: [PATCH 3/4] rust-version: read versions from rustc and the nightly manifest; drop rust-version-check The toolchain parse piped `rustup show` through `grep rustc`, but rustup 1.28 reworked that output and no longer prints the rustc line, so the grep found nothing and pipefail failed the step. `rustc --version` under the `rust-toolchain.toml` override reports the pinned version directly. Nightlies were dated by the commit date rustc and `rustup check` report, which is the day before the nightly cut from it, so a dated pin read as one day older than it was and the "latest" date was one day behind the real latest. A dated pin now supplies its own date, and the latest date comes from the nightly channel manifest, the file the fenix hash is taken from. Only stable needs the channel installed for `rustup check`. rust-version-check is removed: rust-version covers the same check and can open the pull request itself. Its only remaining callers use the reusable workflow form that #89 already removed. --- .github/actions/rust-version-check/action.yml | 58 ------------------- .github/actions/rust-version/action.yml | 37 +++++++----- .github/workflows/test.yml | 51 ++-------------- 3 files changed, 26 insertions(+), 120 deletions(-) delete mode 100644 .github/actions/rust-version-check/action.yml diff --git a/.github/actions/rust-version-check/action.yml b/.github/actions/rust-version-check/action.yml deleted file mode 100644 index 4563141..0000000 --- a/.github/actions/rust-version-check/action.yml +++ /dev/null @@ -1,58 +0,0 @@ -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 `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: - description: GitHub token with `issues` write access - required: false - default: ${{ github.token }} - -runs: - using: composite - steps: - # `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=$(rustc --version | awk '{ print $2 }')" | tee -a $GITHUB_ENV # zizmor: ignore[github-env] - - - name: Get latest stable Rust version - shell: bash - # `^stable-` selects the channel's own line; a toolchain pinned to a - # version number is listed as `-` and must not match. - 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: | # zizmor: ignore[github-env] - # Numeric comparison: a string compare orders 1.100 before 1.99 and - # would stop reporting once the minor version reaches three digits. - oldest=$(printf '%s\n' "$TOOLCHAIN_VERSION" "$RUST_VERSION" | sort -V | head -n 1) - if [[ "$TOOLCHAIN_VERSION" != "$RUST_VERSION" && "$oldest" == "$TOOLCHAIN_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/rust-version/action.yml b/.github/actions/rust-version/action.yml index 0e150ea..278729c 100644 --- a/.github/actions/rust-version/action.yml +++ b/.github/actions/rust-version/action.yml @@ -97,43 +97,50 @@ runs: esac echo "channel=$channel" | tee -a "$GITHUB_OUTPUT" working-directory: ${{ github.workspace }}/${{ inputs.workdir }} - # `rustup show` below must report the `rust-toolchain.toml` version while - # `rustup check` reports the channel's latest release, so both must be - # installed; without the channel, `rustup check` has no line to parse and - # the comparison silently reports up to date. - - name: Install latest ${{ steps.channel.outputs.channel }} + # `rustc --version` below must report the `rust-toolchain.toml` version + # while `rustup check` reports stable's latest release, so both must be + # installed; without stable, `rustup check` has no line to parse and the + # comparison silently reports up to date. + - name: Install latest stable + if: steps.channel.outputs.channel == 'stable' shell: bash - env: - CHANNEL: ${{ steps.channel.outputs.channel }} - run: rustup toolchain install "$CHANNEL" --profile minimal + run: rustup toolchain install stable --profile minimal - name: Parse `rust-toolchain.toml` shell: bash id: current-rust env: CHANNEL: ${{ steps.channel.outputs.channel }} + # A nightly is identified by its date, which a dated pin carries itself. + # A bare `nightly` pin only offers the installed build's commit date, + # the day before the nightly cut from it, so it reads as outdated and + # gets pinned. For stable, `rustc --version` honours the + # `rust-toolchain.toml` override and prints `rustc (...)`. run: | if [[ "$CHANNEL" == "nightly" ]]; then - version=$(rustup show | grep rustc | awk -F'[()]| ' '{ print $(NF-1) }') + version=$(sed -n 's/^channel *= *"nightly-\([0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\).*/\1/p' rust-toolchain.toml) + [[ -n "$version" ]] || version=$(rustc --version | awk -F'[()]| ' '{ print $(NF-1) }') else - version=$(rustup show | grep rustc | awk '{ printf $2 }') + version=$(rustc --version | awk '{ print $2 }') fi - echo "version=$version" | tee -a $GITHUB_OUTPUT + echo "version=$version" | tee -a "$GITHUB_OUTPUT" working-directory: ${{ github.workspace }}/${{ inputs.workdir }} - name: Get latest ${{ steps.channel.outputs.channel }} Rust release id: latest-rust shell: bash env: CHANNEL: ${{ steps.channel.outputs.channel }} - # The anchored patterns select the channel's own line: a toolchain - # pinned to a date or version is listed as `nightly--` or + # The latest nightly's date comes from its channel manifest, the same + # file the fenix hash is taken from; rustup only reports commit dates. + # For stable, the anchored pattern selects the channel's own `rustup + # check` line: a toolchain pinned to a version is listed as # `-` and must not match. run: | if [[ "$CHANNEL" == "nightly" ]]; then - version=$(rustup check | grep -E '^nightly-[^0-9]' | awk -F'[()]| ' '{print $(NF-1)}') + version=$(curl -fsSL --retry 3 https://static.rust-lang.org/dist/channel-rust-nightly.toml | sed -n 's/^date = "\(.*\)"/\1/p') else version=$(rustup check | grep '^stable-' | awk '{print $(NF-2)}') fi - echo "version=$version" | tee -a $GITHUB_OUTPUT + echo "version=$version" | tee -a "$GITHUB_OUTPUT" - name: Compare Rust versions id: compare-versions shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 150a81e..12ef75a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,49 +94,6 @@ jobs: - 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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - 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" - # An up-to-date pin leaves both files alone; an outdated pin moves the # channel and the fenix hash to the latest stable. The expected hash is # recomputed here independently of the action's own code. @@ -212,9 +169,7 @@ jobs: jq -e '.nodes.fenix.locked.rev and .nodes.crane.locked.rev' flake.lock # A nightly pin stays on the nightly channel, moving to the latest date - name: Pin an outdated nightly - run: | - printf '[toolchain]\nchannel = "nightly-2025-01-01"\n' > rust-toolchain.toml - rustup toolchain install nightly-2025-01-01 --profile minimal + run: printf '[toolchain]\nchannel = "nightly-2025-01-01"\n' > rust-toolchain.toml - uses: $/.github/actions/rust-version id: nightly - name: Assert the channel moved to the latest nightly @@ -228,7 +183,9 @@ jobs: [[ "$CHANNEL" == nightly ]] [[ "$OUTDATED" == true ]] [[ "$OLD_VERSION" == 2025-01-01 ]] - [[ "$NEW_VERSION" == "$(rustc +nightly --version | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}')" ]] + # rustc reports the commit date, the day before the nightly's own + # date, so the manifest is the independent source for the latter + [[ "$NEW_VERSION" == "$(curl -fsSL https://static.rust-lang.org/dist/channel-rust-nightly.toml | sed -n 's/^date = "\(.*\)"/\1/p')" ]] grep -qF "channel = \"nightly-$NEW_VERSION\"" rust-toolchain.toml # A beta pin is on neither supported channel and must fail before any edit - name: Pin beta From c4516f7a558d0085c898ac20ae8da69b119bd545 Mon Sep 17 00:00:00 2001 From: samuelburnham <45365069+samuelburnham@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:12:59 -0400 Subject: [PATCH 4/4] rust-version: open the flake.nix sed range at the call's brace The range opened at any `fromToolchainFile` mention and closed at the next `}`. In a flake that also calls lean4-nix's `fromToolchainFile` on a `${system}` line, that brace closed the range before the fenix block, so its `sha256` was never rewritten and the action failed on ix, lean-ffi and Blake3.lean. The range now opens at `fromToolchainFile {` and closes at the `sha256` line itself. --- .github/actions/rust-version/action.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/actions/rust-version/action.yml b/.github/actions/rust-version/action.yml index 278729c..e9458aa 100644 --- a/.github/actions/rust-version/action.yml +++ b/.github/actions/rust-version/action.yml @@ -189,7 +189,10 @@ runs: url="https://static.rust-lang.org/dist/channel-rust-$VERSION.toml" fi hash=$(nix store prefetch-file --json "$url" | jq -r .hash) - sed -i "/fromToolchainFile/,/}/ s|sha256 = \"sha256-[^\"]*\"|sha256 = \"$hash\"|" flake.nix + # The range opens at the call's brace, not at any `fromToolchainFile` + # mention: lean4-nix's call on a `${system}` line would otherwise + # open and close a range before the fenix block's `sha256`. + sed -i "/fromToolchainFile *{/,/sha256 *= *\"/ s|sha256 *= *\"sha256-[^\"]*\"|sha256 = \"$hash\"|" flake.nix if ! grep -qF "$hash" flake.nix; then echo "::error::flake.nix has no fromToolchainFile call with a sha256 to update" exit 1