refactor(ci): read the CI environment through the funnel #4913
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Continuous Integration | |
| permissions: read-all | |
| on: | |
| pull_request: | |
| branches: | |
| - main | |
| - devs/** | |
| # Cancel superseded runs on the same ref — a force-push or a new | |
| # commit makes the in-flight matrix obsolete, so don't keep runners | |
| # busy finishing it. | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| rust: | |
| timeout-minutes: 10 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Install Rust toolchain | |
| # No version argument: installs the channel + components | |
| # pinned in rust-toolchain.toml. | |
| run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| - name: cargo fmt | |
| run: cargo fmt --all --check | |
| - name: cargo clippy | |
| run: cargo clippy --all-targets --all-features --workspace --locked -- -D warnings | |
| - name: cargo test | |
| run: cargo test --workspace --all-features --locked | |
| - name: cargo build --release | |
| run: cargo build --release --locked | |
| # Verify the workspace compiles on the declared MSRV. The version | |
| # is read from Cargo.toml so the floor stays a single source of | |
| # truth — bump rust-version there and this job follows it. `--locked` | |
| # makes the committed lockfile part of the contract. | |
| msrv: | |
| name: cargo check (MSRV) | |
| timeout-minutes: 15 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Resolve MSRV from Cargo.toml | |
| id: msrv | |
| run: | | |
| set -euo pipefail | |
| msrv=$(grep -m1 '^rust-version' Cargo.toml | sed -E 's/.*"([0-9.]+)".*/\1/') | |
| echo "Declared MSRV: ${msrv}" | |
| echo "version=${msrv}" >> "${GITHUB_OUTPUT}" | |
| - name: Install MSRV toolchain | |
| run: rustup toolchain install "${{ steps.msrv.outputs.version }}" --profile minimal | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| key: msrv | |
| - name: cargo check --locked on MSRV | |
| # RUSTUP_TOOLCHAIN outranks rust-toolchain.toml (which | |
| # outranks `rustup default` — so without this env the job | |
| # silently checks the pinned toolchain, not the MSRV). | |
| run: cargo check --workspace --all-targets --locked | |
| env: | |
| RUSTUP_TOOLCHAIN: ${{ steps.msrv.outputs.version }} | |
| # Supply-chain gate: RUSTSEC advisories, license policy, banned / | |
| # duplicate crates, and source provenance. Policy lives in | |
| # deny.toml; reproduce locally with `cargo deny check`. | |
| cargo-deny: | |
| name: cargo-deny | |
| timeout-minutes: 10 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2 | |
| with: | |
| command: check advisories bans licenses sources | |
| # Spellcheck source, comments, and docs. Allow-list of project | |
| # nouns lives in _typos.toml; reproduce locally with `typos`. | |
| typos: | |
| name: typos | |
| timeout-minutes: 5 | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: crate-ci/typos@d43b6c087ac471e2ea7b8af622ff15f05c0c365b # v1.50.1 | |
| # Smoke build the full release wheel matrix on every PR so a | |
| # cross-compile or platform-specific maturin failure is caught | |
| # here, not on the next `release: published` event. Calls the | |
| # same reusable workflow the release uses, with version stamping | |
| # disabled (the placeholder "0.0.0" stays in pyproject.toml so | |
| # the wheel name doesn't pretend to be a release version). | |
| wheels: | |
| uses: ./.github/workflows/build-wheels.yml | |
| with: | |
| stamp-version: false | |
| # Confirm the binary actually runs out of the freshly-built wheel | |
| # on each supported runtime. Lightweight smoke — `mergify --help` | |
| # exits 0 — covers the most common breakage class (a maturin | |
| # config change that ships a wheel with a broken or missing | |
| # binary entry, or a libc / runtime-link issue that only shows | |
| # up post-install). Builds the wheel locally with maturin | |
| # rather than downloading the `wheels`-job artifacts, since | |
| # `wheels` only uploads in release mode (PR smoke runs are | |
| # `stamp-version: false`). | |
| smoke-test-binary: | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-24.04, windows-2025, macos-15] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: 3.14 | |
| - name: Install Rust toolchain | |
| # No version argument: installs the channel pinned in | |
| # rust-toolchain.toml. | |
| run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| key: smoke-${{ matrix.os }} | |
| - name: Build wheel | |
| shell: bash | |
| run: | | |
| pip install 'maturin~=1.7' | |
| maturin build --release --out dist | |
| - name: Install wheel | |
| shell: bash | |
| run: pip install dist/*.whl | |
| - name: mergify --help | |
| shell: bash | |
| run: mergify --help | |
| # Lock the dev-build version fallback in. Release builds get | |
| # `MERGIFY_RELEASE_VERSION` from the stamp step in | |
| # `build-wheels.yml`; PR smoke builds don't, so the binary | |
| # must report `Cargo.toml`'s `0.0.0` placeholder. If this | |
| # ever changes silently, every release-version assumption | |
| # downstream (PyPI install reporting, support diagnostics) | |
| # rests on the same code path — catch the drift here. | |
| - name: mergify --version reports placeholder on unstamped build | |
| shell: bash | |
| run: | | |
| out=$(mergify --version) | |
| echo "$out" | |
| [[ "$out" == "mergify 0.0.0" ]] || { | |
| echo "expected 'mergify 0.0.0', got '$out'" | |
| exit 1 | |
| } | |
| # End-to-end exercise of the `install.sh` one-liner: build the | |
| # release binary, pretend to be a GitHub Release by serving the | |
| # repackaged `mergify-<target>.tar.gz` + `SHA256SUMS` through a | |
| # local HTTP fixture, then run `install.sh` against it and assert | |
| # the installed binary works. This catches the install path that | |
| # the unit-style shellcheck pass can't — detect_target mapping, | |
| # sha256 verification, the `install -m 0755` step, and the final | |
| # `--version` exec. | |
| # | |
| # Windows runs the same POSIX `install.sh` under git-bash | |
| # (`shell: bash`), pulling the `.zip` asset and `mergify.exe` | |
| # instead of the `.tar.gz` + `mergify` the Unix legs use. | |
| install-script-smoke: | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-24.04, macos-15, windows-2025] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Install Rust toolchain | |
| # No version argument: installs the channel pinned in | |
| # rust-toolchain.toml. | |
| run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| key: install-${{ matrix.os }} | |
| - name: Install shellcheck (Linux only) | |
| if: matrix.os == 'ubuntu-24.04' | |
| run: sudo apt-get install -y --no-install-recommends shellcheck | |
| - name: Lint install.sh | |
| if: matrix.os == 'ubuntu-24.04' | |
| run: shellcheck install.sh | |
| # Build the binary the same way the release path does, then | |
| # repackage it under the exact name/structure `install.sh` | |
| # expects so the fixture is bit-identical to a real release | |
| # asset. | |
| - name: Build release binary | |
| run: cargo build --release -p mergify-cli | |
| - name: Pack release fixture | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| # `sha256sum` (Linux, git-bash) or `shasum -a 256` (macOS) — | |
| # same fallback install.sh uses; git-bash ships no `shasum`. | |
| sha256sums() { | |
| if command -v sha256sum > /dev/null 2>&1; then sha256sum "$@" | |
| else shasum -a 256 "$@"; fi | |
| } | |
| target=$(rustc -vV | awk '/^host:/ {print $2}') | |
| echo "Building fixture for target: ${target}" | |
| # Mirror what install.sh derives from the triple: Windows | |
| # ships a `.zip` holding `mergify.exe`, everything else a | |
| # `.tar.gz` holding `mergify`. | |
| case "${target}" in | |
| *windows*) ext="zip"; bin="mergify.exe" ;; | |
| *) ext="tar.gz"; bin="mergify" ;; | |
| esac | |
| asset="mergify-2099.1.1.1-${target}.${ext}" | |
| mkdir -p fixture/release | |
| # install.sh resolves the version from latest-release.json (fixture | |
| # mode) before building the per-version asset name. | |
| echo '{"tag_name":"2099.1.1.1"}' > fixture/release/latest-release.json | |
| if [ "${ext}" = "zip" ]; then | |
| # Pack with python's zipfile rather than betting on a | |
| # `zip` binary in git-bash; python is already required | |
| # below for the fixture server. Resolve the interpreter | |
| # name the same way the serve step does (`python3` on | |
| # Unix, `python` on the Windows runner). | |
| py=python3; command -v "$py" > /dev/null 2>&1 || py=python | |
| ( cd target/release \ | |
| && "$py" -m zipfile -c "${OLDPWD}/fixture/release/${asset}" "${bin}" ) | |
| else | |
| tar -C target/release -czf "fixture/release/${asset}" "${bin}" | |
| fi | |
| # Normalise to the canonical "HASH␣␣filename" form the | |
| # release pipeline emits on Linux. git-bash's `sha256sum` | |
| # tags binary files with a `*` marker (`HASH *file`), which | |
| # would break install.sh's `$2 == asset` lookup and its | |
| # two-space line-shape check. | |
| ( cd fixture/release \ | |
| && hash=$(sha256sums "${asset}" | awk '{print $1}') \ | |
| && printf '%s %s\n' "${hash}" "${asset}" > SHA256SUMS ) | |
| ls -la fixture/release | |
| - name: Serve fixture | |
| shell: bash | |
| run: | | |
| cd fixture/release | |
| # `python3` on the Unix runners, `python` on the Windows | |
| # runner (it ships no `python3` shim). The http.server | |
| # module is in the stdlib on all of them. Bind to localhost | |
| # only; the runner is ephemeral but there's no point | |
| # exposing the fixture. | |
| py=python3; command -v "$py" > /dev/null 2>&1 || py=python | |
| "$py" -m http.server 8765 --bind 127.0.0.1 > /tmp/server.log 2>&1 & | |
| echo $! > /tmp/server.pid | |
| # Wait for the server to actually accept connections. | |
| for _ in $(seq 1 50); do | |
| curl -fsS http://127.0.0.1:8765/SHA256SUMS > /dev/null && break | |
| sleep 0.1 | |
| done | |
| - name: Run install.sh against the fixture | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| export MERGIFY_BASE_URL=http://127.0.0.1:8765 | |
| export MERGIFY_INSTALL_DIR=/tmp/mergify-install | |
| ./install.sh | |
| # `install.sh` runs `--version` itself, but pin it here | |
| # too so a future refactor that drops the post-install | |
| # smoke from the script still trips this job. The binary | |
| # is `mergify.exe` on Windows. | |
| case "$(rustc -vV | awk '/^host:/ {print $2}')" in | |
| *windows*) bin=mergify.exe ;; | |
| *) bin=mergify ;; | |
| esac | |
| "/tmp/mergify-install/${bin}" --version | |
| # Negative paths: corrupted SHA256SUMS must fail closed, not | |
| # silently install a tampered binary. Tests both the | |
| # `sha256sum -c` mismatch path (right shape, wrong hash) and | |
| # the malformed-line path (`sha256sum -c` only warns on | |
| # malformed lines, so install.sh validates the line shape | |
| # separately). | |
| - name: install.sh rejects a tampered SHA256SUMS | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target=$(rustc -vV | awk '/^host:/ {print $2}') | |
| case "${target}" in *windows*) ext=zip ;; *) ext=tar.gz ;; esac | |
| asset="mergify-2099.1.1.1-${target}.${ext}" | |
| # Keep a clean copy so the malformed test below can write | |
| # its own bad version without losing the original. | |
| cp fixture/release/SHA256SUMS fixture/release/SHA256SUMS.orig | |
| echo "Case 1: wrong-but-well-formed hash" | |
| printf '%064d %s\n' 0 "${asset}" > fixture/release/SHA256SUMS | |
| ! MERGIFY_BASE_URL=http://127.0.0.1:8765 \ | |
| MERGIFY_INSTALL_DIR=/tmp/mergify-bad1 \ | |
| ./install.sh > /tmp/case1.log 2>&1 | |
| grep -q 'checksum verification failed' /tmp/case1.log | |
| echo "Case 2: malformed line" | |
| echo "bogus ${asset}" > fixture/release/SHA256SUMS | |
| ! MERGIFY_BASE_URL=http://127.0.0.1:8765 \ | |
| MERGIFY_INSTALL_DIR=/tmp/mergify-bad2 \ | |
| ./install.sh > /tmp/case2.log 2>&1 | |
| grep -q 'malformed checksum entry' /tmp/case2.log | |
| # Restore — the next steps may depend on a working fixture. | |
| mv fixture/release/SHA256SUMS.orig fixture/release/SHA256SUMS | |
| - name: Stop fixture server | |
| if: always() | |
| shell: bash | |
| run: | | |
| if [ -f /tmp/server.pid ]; then | |
| kill "$(cat /tmp/server.pid)" || true | |
| fi | |
| # Same fixture pattern as `install-script-smoke`, but exercises | |
| # `mergify self-update`. Builds the release binary, packages it | |
| # like a real release would, points the binary at a localhost | |
| # fixture via `MERGIFY_BASE_URL`, and runs the full download + | |
| # verify + atomic-swap path. Plus a negative case proving a | |
| # tampered `SHA256SUMS` reject (binary must stay untouched). | |
| self-update-smoke: | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-24.04, macos-15] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Install Rust toolchain | |
| # No version argument: installs the channel pinned in | |
| # rust-toolchain.toml. | |
| run: rustup toolchain install | |
| - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 | |
| with: | |
| key: self-update-${{ matrix.os }} | |
| - name: Build release binary | |
| run: cargo build --release -p mergify-cli | |
| # Fixture shape `self_update` expects in `MERGIFY_BASE_URL` | |
| # mode: a flat dir holding `latest-release.json` (the API | |
| # stub), the asset tarball, and `SHA256SUMS`. The stub mirrors | |
| # GitHub's `releases/latest` payload — `assets[]` carries the | |
| # `browser_download_url` the binary downloads from, so the | |
| # smoke test exercises the same discovery path as production | |
| # (no filename reconstruction). | |
| - name: Pack self-update fixture | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target=$(rustc -vV | awk '/^host:/ {print $2}') | |
| echo "Building fixture for target: ${target}" | |
| mkdir -p fixture/su | |
| asset="mergify-2099.1.1.1-${target}.tar.gz" | |
| tar -C target/release -czf "fixture/su/${asset}" mergify | |
| (cd fixture/su && shasum -a 256 "${asset}" > SHA256SUMS) | |
| # Pretend a far-future release is available so the | |
| # `current == latest` short-circuit doesn't fire. | |
| base="http://127.0.0.1:8766" | |
| cat > fixture/su/latest-release.json <<JSON | |
| { | |
| "tag_name": "2099.1.1.1", | |
| "assets": [ | |
| {"name": "${asset}", "browser_download_url": "${base}/${asset}"}, | |
| {"name": "SHA256SUMS", "browser_download_url": "${base}/SHA256SUMS"} | |
| ] | |
| } | |
| JSON | |
| ls -la fixture/su | |
| - name: Serve fixture | |
| shell: bash | |
| run: | | |
| cd fixture/su | |
| python3 -m http.server 8766 --bind 127.0.0.1 > /tmp/su-server.log 2>&1 & | |
| echo $! > /tmp/su-server.pid | |
| for _ in $(seq 1 50); do | |
| curl -fsS http://127.0.0.1:8766/SHA256SUMS > /dev/null && break | |
| sleep 0.1 | |
| done | |
| - name: self-update --check reports both versions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| out=$(MERGIFY_BASE_URL=http://127.0.0.1:8766 target/release/mergify self-update --check) | |
| echo "$out" | |
| echo "$out" | grep -q 'Current: 0.0.0' | |
| echo "$out" | grep -q 'Latest: 2099.1.1.1' | |
| - name: self-update happy path swaps the binary | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| cp target/release/mergify /tmp/mergify-su | |
| MERGIFY_BASE_URL=http://127.0.0.1:8766 /tmp/mergify-su self-update | |
| # Sanity: the swapped binary still runs. | |
| /tmp/mergify-su --version | |
| - name: self-update rejects tampered SHA256SUMS | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target=$(rustc -vV | awk '/^host:/ {print $2}') | |
| cp target/release/mergify /tmp/mergify-su-tamper | |
| before=$(shasum -a 256 /tmp/mergify-su-tamper | awk '{print $1}') | |
| # Replace SHA256SUMS with a wrong-but-well-formed hash. | |
| printf '%064d mergify-2099.1.1.1-%s.tar.gz\n' 0 "${target}" > fixture/su/SHA256SUMS | |
| ! MERGIFY_BASE_URL=http://127.0.0.1:8766 /tmp/mergify-su-tamper self-update \ | |
| > /tmp/su-tamper.log 2>&1 | |
| grep -q 'checksum mismatch' /tmp/su-tamper.log | |
| # The binary must NOT have been swapped. | |
| after=$(shasum -a 256 /tmp/mergify-su-tamper | awk '{print $1}') | |
| [ "$before" = "$after" ] || { echo "binary was tampered!"; exit 1; } | |
| - name: Stop fixture server | |
| if: always() | |
| shell: bash | |
| run: | | |
| if [ -f /tmp/su-server.pid ]; then | |
| kill "$(cat /tmp/su-server.pid)" || true | |
| fi | |
| ci-gate: | |
| if: ${{ !cancelled() }} | |
| needs: | |
| - rust | |
| - msrv | |
| - cargo-deny | |
| - typos | |
| - wheels | |
| - smoke-test-binary | |
| - install-script-smoke | |
| - self-update-smoke | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verify all jobs succeeded | |
| uses: Mergifyio/gha-mergify-ci@5e8176734bc525c7fd8b2e812f0fa47246f601b7 # v25 | |
| with: | |
| action: wait-jobs | |
| jobs: ${{ toJSON(needs) }} |