Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/cursor-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ silently vanishing — the review tells you what didn't run.
| [`post-review.py`](post-review.py) | Reads the judge's consolidated findings and posts **one** PR review with line-anchored inline comments and severity badges. |
| [`gate-unresolved.py`](gate-unresolved.py) | The opt-in blocking gate (`blocking: true`). Queries the PR's review threads and exits non-zero while any cursor-review finding thread is unresolved. |
| [`slack-notify.sh`](slack-notify.sh) | Sends the start/complete Slack DMs to the triggerer (no-ops without a token). |
| [`install-cursor-cli.sh`](install-cursor-cli.sh) | Installs the Cursor agent CLI from the versioned, sha256-pinned release artifact — not `curl cursor.com/install \| bash`. Used by all three CLI-using jobs; the pin (`CURSOR_CLI_VERSION` / `CURSOR_CLI_SHA256`) lives in `cursor-review.yml`'s top-level `env:`. |

## Adopt it in your repo

Expand Down
91 changes: 91 additions & 0 deletions .github/cursor-review/install-cursor-cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Install the Cursor agent CLI from a versioned, sha256-pinned release artifact
# instead of the mutable `curl https://cursor.com/install | bash` bootstrap
# (BE-5869). Old versions stay downloadable, so a Cursor release never breaks a
# run; the pin moves only via a reviewed PR to cursor-review.yml.
#
# Lives here, not inline in cursor-review.yml, because three jobs (preflight,
# each review matrix cell, consolidate) install the CLI: a forked copy per job
# is exactly the drift mode AGENTS.md warns about, and every hardening below
# would otherwise have to be applied identically in three places. Loaded at run
# time from a pinned ref of THIS repo, like the rest of .github/cursor-review/.
#
# Inputs (env — set once in cursor-review.yml's top-level `env:`):
# CURSOR_CLI_VERSION version string, e.g. 2026.07.23-e383d2b
# CURSOR_CLI_SHA256 sha256 of that version's linux/x64 agent-cli-package.tar.gz
#
# Appends ~/.local/bin to $GITHUB_PATH so later steps can call `cursor-agent`.
set -euo pipefail

: "${CURSOR_CLI_VERSION:?CURSOR_CLI_VERSION must be set (see cursor-review.yml env)}"
: "${CURSOR_CLI_SHA256:?CURSOR_CLI_SHA256 must be set (see cursor-review.yml env)}"

url="https://downloads.cursor.com/lab/${CURSOR_CLI_VERSION}/linux/x64/agent-cli-package.tar.gz"

# mktemp so nothing can pre-place a symlink for curl to write through. The trap
# makes cleanup unconditional — under `set -e` a checksum or tar failure aborts
# before any explicit `rm` would run.
pkg="$(mktemp "${RUNNER_TEMP:-/tmp}/cursor-cli.XXXXXX")"
trap 'rm -f "$pkg"' EXIT

# Flag notes, since this is a supply-chain control and the payload is ~83 MB
# (the old bootstrap was ~6 KB, so none of this mattered before):
# --proto/--proto-redir -L would otherwise follow a redirect down to plaintext
# http; a checksum failure is a pipeline outage, so an
# on-path attacker shouldn't be able to force one.
# --speed-limit/--speed-time stall detection instead of a flat wall-clock cap,
# which would kill a slow-but-healthy transfer at 99%.
# --max-time is only a backstop: 300s is ~175x the
# observed download time and, with --retry-max-time 180,
# keeps the worst case (~8 min) inside the tightest job
# cap (preflight's timeout-minutes: 10).
# --max-filesize bounds what a hostile or malfunctioning CDN can write
# to the runner volume before the digest is ever checked.
# plain --retry NOT --retry-all-errors: a 404 from a typo'd or pruned
# CURSOR_CLI_VERSION is permanent, and retrying it turns
# a clear error into an opaque job timeout across all
# ~10 concurrent jobs.
curl -fsSL \
--proto '=https' --proto-redir '=https' \
--connect-timeout 10 \
--speed-limit 10240 --speed-time 30 --max-time 300 \
--max-filesize 268435456 \
--retry 3 --retry-delay 2 --retry-max-time 180 \
"$url" -o "$pkg"

echo "${CURSOR_CLI_SHA256} ${pkg}" | sha256sum -c -

# `tar --strip-components=1` silently DROPS any member that isn't nested at
# least one level deep, and still exits 0 — a layout change at pin-bump time
# would produce a partial install that the smoke test below can't see. The
# pinned archive is a single `dist-package/` tree, so any slash-free member
# means the layout moved: fail loudly instead of installing half of it.
if tar -tzf "$pkg" | grep -qv '/'; then
echo "::error::Unexpected ${CURSOR_CLI_VERSION} archive layout — top-level members would be dropped by --strip-components=1."
tar -tzf "$pkg" | grep -v '/'
exit 1
fi

# Mirror the vendor installer's layout: versions dir + ~/.local/bin symlink
# (~/.local/bin is already on the hosted runner's default PATH). Recreate $dest
# from scratch rather than `mkdir -p` onto whatever is there, so the tree that
# actually executes is exactly the archive we just verified.
dest="$HOME/.local/share/cursor-agent/versions/${CURSOR_CLI_VERSION}"
rm -rf "$dest"
mkdir -p "$dest" "$HOME/.local/bin"
tar --strip-components=1 -xzf "$pkg" -C "$dest"
ln -sf "$dest/cursor-agent" "$HOME/.local/bin/cursor-agent"

# Prove the pinned binary runs AND is the build we verified. Printing the
# version without comparing it would not catch the bits being swapped (a CLI
# self-update, a stale symlink) — which is the whole point of the pin.
installed="$("$HOME/.local/bin/cursor-agent" --version)"
if [ "$installed" != "$CURSOR_CLI_VERSION" ]; then
echo "::error::cursor-agent reports '${installed}', expected the pinned '${CURSOR_CLI_VERSION}'."
exit 1
fi
echo "cursor-agent ${installed} installed from a sha256-verified artifact."

if [ -n "${GITHUB_PATH:-}" ]; then
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
fi
50 changes: 35 additions & 15 deletions .github/workflows/cursor-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,15 @@ env:
DIFF_EXCLUDES: ${{ inputs.diff_excludes }}
# Where the assets checkout lands (this repo, .github/cursor-review/*).
CURSOR_REVIEW_ASSETS: ${{ github.workspace }}/_cursor_review_assets/.github/cursor-review
# Cursor CLI pin — a versioned, checksummed artifact instead of the mutable
# cursor.com/install script (BE-5869). Old versions stay downloadable, so a
# Cursor release never breaks this; the pin moves only via a reviewed PR.
# Consumed by .github/cursor-review/install-cursor-cli.sh, the ONE installer
# all three CLI-using jobs run (preflight, review matrix, consolidate).
CURSOR_CLI_VERSION: 2026.07.23-e383d2b
# sha256 of the linux/x64 agent-cli-package.tar.gz (all jobs run ubuntu-latest).
# Cross-checked against nixpkgs pkgs/by-name/cu/cursor-cli at pin time.
CURSOR_CLI_SHA256: 702ad595213bee5df0268be9f80a19f29fcceaa2a42fc55e39f2b5199051f0c4

jobs:
gate:
Expand Down Expand Up @@ -339,6 +348,16 @@ jobs:
JSON
echo "models=$(jq -c . /tmp/models.json)" >> "$GITHUB_OUTPUT"

- name: Load cursor-review assets
# Same trusted-assets checkout the review/consolidate jobs do — this
# job needs it for install-cursor-cli.sh.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: Comfy-Org/github-workflows
ref: ${{ inputs.workflows_ref }}
path: _cursor_review_assets
persist-credentials: false

- name: Install Cursor agent CLI
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
Expand All @@ -350,8 +369,7 @@ jobs:
echo "No CURSOR_API_KEY (fork PR or unset secret) — skipping catalog preflight."
exit 0
fi
curl https://cursor.com/install -fsSL | bash
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"
bash "$CURSOR_REVIEW_ASSETS/install-cursor-cli.sh"

- name: Validate model pins against live catalog
env:
Expand Down Expand Up @@ -485,17 +503,21 @@ jobs:
echo "Diff size: $(wc -l < /tmp/pr-diff.patch) lines"

- name: Install Cursor agent CLI
run: bash "$CURSOR_REVIEW_ASSETS/install-cursor-cli.sh"

- name: Verify Cursor agent version
# The installer already asserts this once. Re-asserted here, on the
# PATH-resolved binary immediately before the review runs, so a swap
# between install and use (a CLI self-update, a stale symlink) can't
# quietly hand the review to unpinned bits — and the version still
# lands in the log for forensics.
run: |
curl https://cursor.com/install -fsSL | bash
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"

- name: Log Cursor agent version
# The install script is unpinned (curl | bash from cursor.com), so we
# log the installed version into the run for forensics rather than
# gating on a hard-coded baseline. A version equality check produced
# daily toil without adding integrity protection — a CDN compromise
# would just ship malicious bits as a "new version" we'd then bump to.
run: cursor-agent --version
installed="$(cursor-agent --version)"
if [ "$installed" != "$CURSOR_CLI_VERSION" ]; then
echo "::error::cursor-agent reports '${installed}', expected the pinned '${CURSOR_CLI_VERSION}'."
exit 1
fi
echo "cursor-agent ${installed} (matches CURSOR_CLI_VERSION)."

- name: Build prompt
env:
Expand Down Expand Up @@ -610,9 +632,7 @@ jobs:
git diff "$BASE_SHA...$HEAD_SHA" -- . $DIFF_EXCLUDES > /tmp/pr-diff.patch

- name: Install Cursor agent CLI
run: |
curl https://cursor.com/install -fsSL | bash
echo "$HOME/.cursor/bin" >> "$GITHUB_PATH"
run: bash "$CURSOR_REVIEW_ASSETS/install-cursor-cli.sh"

- name: Download panel findings
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/test-cursor-review-scripts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,20 @@ permissions:

jobs:
test:
name: unittest
name: unittest + shellcheck
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: ShellCheck the cursor-review shell scripts
# These run inside the review jobs with no unit tests behind them, so
# shellcheck is the only automated guard on install-cursor-cli.sh (the
# sha256-pinned CLI installer) and slack-notify.sh.
run: shellcheck -x .github/cursor-review/install-cursor-cli.sh .github/cursor-review/slack-notify.sh

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
Expand Down