Skip to content

Commit 5f94b82

Browse files
authored
Merge pull request #22590 from github/paldepind/rust-analyzer-update-automation
Rust: rust-analyzer update automation
2 parents dae2a8d + 516986e commit 5f94b82

6 files changed

Lines changed: 2255 additions & 55 deletions

File tree

‎.gitattributes‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
/ruby/extractor/cargo-bazel-lock.json linguist-generated=true
7676
/ruby/extractor/cargo-bazel-lock.json -merge
7777

78+
# GitHub Agentic Workflows compiled output
79+
.github/workflows/*.lock.yml linguist-generated=true
80+
7881
# auto-generated files for the C# build
7982
/csharp/paket.lock linguist-generated=true
8083
# needs eol=crlf, as `paket` touches this file and saves it as crlf

‎.github/workflows/update-rust-analyzer.lock.yml‎

Lines changed: 1822 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
---
2+
name: Update rust-analyzer
3+
emoji: 🦀
4+
description: Update the rust-analyzer version used by the Rust extractor and prepare a pull request.
5+
intent: Keep the Rust extractor on the latest compatible rust-analyzer version with a reviewable, validated pull request.
6+
on:
7+
workflow_dispatch:
8+
roles: [admin, maintainer, write]
9+
permissions:
10+
contents: read
11+
actions: read
12+
pull-requests: read
13+
copilot-requests: write
14+
strict: true
15+
checkout:
16+
fetch-depth: 0
17+
concurrency:
18+
group: update-rust-analyzer
19+
cancel-in-progress: false
20+
timeout-minutes: 180
21+
tools:
22+
github:
23+
mode: gh-proxy
24+
toolsets: [repos, pull_requests, actions]
25+
bash: ["*"]
26+
edit: true
27+
network:
28+
allowed:
29+
- defaults
30+
- github
31+
- github-actions
32+
- rust
33+
- bazel
34+
- python
35+
steps:
36+
- name: Configure Git
37+
run: |
38+
git config user.name "github-actions[bot]"
39+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
40+
41+
- name: Install cargo-edit
42+
continue-on-error: true
43+
run: cargo install cargo-edit@0.13.0 --locked
44+
45+
- name: Run rust-analyzer updater script
46+
run: |
47+
mkdir -p /tmp/gh-aw/agent
48+
set +e
49+
python3 rust/scripts/update_rust_analyzer.py \
50+
2>&1 | tee /tmp/gh-aw/agent/rust-analyzer-update.log
51+
status=${PIPESTATUS[0]}
52+
set -e
53+
54+
printf '%s\n' "$status" > /tmp/gh-aw/agent/rust-analyzer-update-status.txt
55+
if grep -Fxq "No new rust-analyzer version available." \
56+
/tmp/gh-aw/agent/rust-analyzer-update.log; then
57+
printf '%s\n' "no-update" > /tmp/gh-aw/agent/rust-analyzer-update-result.txt
58+
else
59+
printf '%s\n' "update" > /tmp/gh-aw/agent/rust-analyzer-update-result.txt
60+
fi
61+
safe-outputs:
62+
report-incomplete: {}
63+
create-pull-request:
64+
title-prefix: "Rust: "
65+
branch-prefix: "automation/update-rust-analyzer/"
66+
draft: true
67+
max-patch-size: 10240
68+
max-patch-files: 1000
69+
allowed-files:
70+
- "Cargo.lock"
71+
- "shared/tree-sitter-extractor/Cargo.toml"
72+
- "shared/yeast/Cargo.toml"
73+
- "shared/yeast-macros/Cargo.toml"
74+
- "shared/yeast-schema/Cargo.toml"
75+
- "ruby/extractor/Cargo.toml"
76+
- "unified/extractor/Cargo.toml"
77+
- "unified/swift-syntax-rs/Cargo.toml"
78+
- "MODULE.bazel"
79+
- "MODULE.bazel.lock"
80+
- "rust-toolchain.toml"
81+
- "rust/**"
82+
- "misc/bazel/3rdparty/**"
83+
---
84+
85+
# Update rust-analyzer
86+
87+
## Task
88+
89+
The workflow has already run `rust/scripts/update_rust_analyzer.py`. Read:
90+
91+
- `/tmp/gh-aw/agent/rust-analyzer-update.log` for its complete output.
92+
- `/tmp/gh-aw/agent/rust-analyzer-update-status.txt` for its exit status.
93+
- `/tmp/gh-aw/agent/rust-analyzer-update-result.txt` for the deterministic result classification.
94+
95+
If the result is `no-update`, call `noop` with the reason
96+
`No new rust-analyzer version available.` and stop immediately. Do not inspect
97+
CI, modify files, or create a pull request.
98+
99+
Otherwise, continue the update from the existing working tree and commits:
100+
101+
1. Read `rust/updating-rust-analyzer.md` and all applicable repository
102+
instructions.
103+
2. Review the updater log, exit status, commits, and working tree. Do not rerun
104+
the updater script.
105+
3. Complete as much of the documented update as possible. Fix extractor or
106+
code-generation breakage, keep all `ra_ap_` dependency versions aligned,
107+
regenerate required files, and add schema upgrade/downgrade scripts, tests,
108+
and a change note when the schema changed.
109+
4. Run the relevant formatting, linting, code generation, build, and tests,
110+
including `bazel run //rust:install`. Use `gh` to inspect relevant existing
111+
CI configuration and prior failures while diagnosing problems.
112+
5. Review the complete diff and commits. Do not include secrets. Do not refer
113+
to private repositories, internal issues, or internal pull requests in the
114+
public pull request.
115+
6. Use `create_pull_request` exactly once to create a focused draft pull
116+
request. Summarize the updater output, changes, and validation. If the
117+
update cannot be finished, still create a partial draft pull request when
118+
there are useful changes, and clearly list failures, missing work, and the
119+
next commands for a maintainer.
120+
121+
The pull request is created after this agent execution, so its newly triggered
122+
CI cannot be awaited in this run. Compensate with the strongest practical
123+
local validation and state this limitation accurately in the pull request.
124+
If the updater did not report `no-update` but no useful patch can be produced,
125+
call `report_incomplete` with the updater failure and exact blocker.
126+
127+
## Safe Outputs
128+
129+
- Use `create_pull_request` for the update or partial update.
130+
- Use `noop` only for the exact no-update result.
131+
- Use `report_incomplete` only when no useful pull request can be created.

‎rust/README.md‎

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -62,58 +62,4 @@ for code generation to succeed.
6262

6363
### Updating `rust-analyzer`
6464

65-
Here's a rundown of the typical actions to perform to do a rust-analyzer (and other dependencies) update. A one-time setup consists in
66-
installing [`cargo-edit`](https://crates.io/crates/cargo-edit) with `cargo install cargo-edit`. On Ubuntu that also requires
67-
`sudo apt install libssl-dev pkg-config`.
68-
69-
1. From the root of the `codeql` repo checkout, run a Cargo upgrade:
70-
```
71-
cargo upgrade --incompatible --pinned
72-
```
73-
2. Look at a diff of the `Cargo.toml` files: if all `ra_ap_` prefixed dependencies have been updated to the same number, go on to the next step.
74-
Otherwise, it means the latest `rust-analyzer` update has not been fully rolled out to all its crates in `crates.io`.
75-
_All `ra_ap_` versions must agree!_
76-
Downgrade by hand to the minimum one you see, and run a `cargo update` after that to fix the `Cargo.lock` file.
77-
3. Commit the changes, skipping `pre-commit` hooks if you have them enabled:
78-
```
79-
git commit -am 'Cargo: upgrade dependencies' --no-verify
80-
```
81-
4. Regenerate vendored bazel files (these allow faster builds, particularly on CI where it has to start from scratch each time), commit the changes:
82-
```
83-
misc/bazel/3rdparty/update_tree_sitter_extractors_deps.sh
84-
git add .
85-
git commit -am 'Bazel: regenerate vendored cargo dependencies' --no-verify
86-
```
87-
> [!NOTE]
88-
> If in step 6 you also bump `rules_rust` or the rust toolchain, those changes invalidate _all_ vendored files (including the
89-
> Python ones under `misc/bazel/3rdparty/py_deps`), not just the tree-sitter ones. In that case run the umbrella script
90-
> `misc/bazel/3rdparty/update_cargo_deps.sh` instead (it regenerates both `py_deps` and `tree_sitter_extractors_deps`, and runs
91-
> `bazel mod tidy`), then commit all the regenerated files.
92-
5. Run codegen
93-
```
94-
bazel run //rust/codegen
95-
```
96-
Take note whether `rust/schema/ast.py` was changed. That might need tweaks, new tests and/or downgrade/upgrade scripts down the line.
97-
6. Try compiling
98-
```
99-
bazel run //rust:install
100-
```
101-
* if it succeeds: good! You can move on to the next step.
102-
* if it fails while compiling rust-analyzer dependencies, you need to update the rust toolchain. Sometimes the error will tell you
103-
so explicitly, but it may happen that the error is more obscure. To update the rust toolchain:
104-
* you will need to open a PR on the internal repo updating `RUST_VERSION` in `MODULE.bazel`. In general you can have this merged
105-
independently of the changes in `codeql`.
106-
* in `codeql`, update both `RUST_VERSION` in `MODULE.bazel` _and_ `rust-toolchain.toml` files. You may want to also update the
107-
nightly toolchain in `rust/extractor/src/nightly-toolchain/rust-toolchain.toml` to a more recent date while you're at it.
108-
* a toolchain and/or `rules_rust` bump invalidates the vendored files, so re-run `misc/bazel/3rdparty/update_cargo_deps.sh`
109-
(see the note in step 4) and commit the regenerated files.
110-
* if it fails while compiling rust extractor code, you will need to adapt it to the new library version.
111-
* for example updating annotations in `annotations.py`, adding / removing generated tests.
112-
113-
If you had to do any changes, commit them. If you updated the rust toolchain, running `rust/lint.py` might reformat or apply new
114-
lints to the code.
115-
7. Check with CI if everything is in order.
116-
8. Run DCA with database caching disabled. Iterate on the code if needed.
117-
9. If in step 5 the schema was updated, add upgrade/downgrade scripts and a change note. This is best done last to reduce the chance of
118-
merge conflicts (none of the other testing depends on having upgrade and downgrade scripts in place). See
119-
[Upgrading a language database schema](docs/prepare-db-upgrade.md).
65+
See [Updating rust-analyzer](updating-rust-analyzer.md).
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import shlex
5+
import subprocess
6+
import sys
7+
import tomllib
8+
import urllib.request
9+
from pathlib import Path
10+
11+
12+
REPO_ROOT = Path(__file__).resolve().parents[2]
13+
RUST_EXTRACTOR_MANIFEST = REPO_ROOT / "rust/extractor/Cargo.toml"
14+
15+
# The files that mention the fixed Rust toolchain version
16+
TOOLCHAIN_RS = REPO_ROOT / "rust/extractor/src/toolchain.rs"
17+
INTEGRATION_TEST_CONFTEST = REPO_ROOT / "rust/ql/integration-tests/conftest.py"
18+
QL_TEST_SETUP = REPO_ROOT / "rust/ql/test/setup.sh"
19+
20+
21+
def print_step(number: int, description: str) -> None:
22+
print(f"\nStep {number}: {description}", flush=True)
23+
24+
25+
def run(*command: str) -> None:
26+
print(f"+ {shlex.join(command)}", flush=True)
27+
subprocess.run(command, cwd=REPO_ROOT, check=True)
28+
29+
30+
def run_codegen() -> None:
31+
try:
32+
run("bazel", "run", "//rust/codegen")
33+
except subprocess.CalledProcessError as error:
34+
print(
35+
"\nCodegen failed. Carry out the instructions from step 4 in rust/updating-rust-analyzer.md manually.",
36+
file=sys.stderr,
37+
flush=True,
38+
)
39+
raise SystemExit(error.returncode) from None
40+
41+
42+
def get_min_rust_analyzer_version(manifest: str) -> str:
43+
"""Get the minimum version of all ra_ap dependencies from `Cargo.toml`."""
44+
dependencies = tomllib.loads(manifest)["dependencies"]
45+
return min(
46+
(
47+
version
48+
for name, version in dependencies.items()
49+
if name.startswith("ra_ap_")
50+
),
51+
key=lambda value: tuple(map(int, value.split("."))),
52+
)
53+
54+
55+
def fetch(url: str) -> bytes:
56+
request = urllib.request.Request(
57+
url,
58+
headers={"User-Agent": "github/codeql rust-analyzer updater"},
59+
)
60+
with urllib.request.urlopen(request) as response:
61+
return response.read()
62+
63+
64+
def get_compatible_rust_toolchain(rust_analyzer_version: str) -> str:
65+
"""Get the latest Rust toolchain released no later than rust-analyzer."""
66+
# Get the release date of the rust-analyzer version
67+
crate_url = f"https://crates.io/api/v1/crates/ra_ap_syntax/{rust_analyzer_version}"
68+
crate = json.loads(fetch(crate_url))
69+
rust_analyzer_release = crate["version"]["created_at"].split("T")[0]
70+
71+
# `manifests.txt` is a list of all toolchains. The one we're interested in looks like
72+
# ```
73+
# static.rust-lang.org/dist/YYYY-MM-DD/channel-rust-stable.toml
74+
# ```
75+
# where `YYYY-MM-DD` is no later than the rust-analyzer release date.
76+
manifests = (
77+
fetch("https://static.rust-lang.org/manifests.txt").decode().splitlines()
78+
)
79+
rust_manifest = next(
80+
manifest
81+
for manifest in reversed(manifests)
82+
if manifest.endswith("/channel-rust-stable.toml")
83+
and manifest.split("/")[2] <= rust_analyzer_release
84+
)
85+
manifest = tomllib.loads(fetch(f"https://{rust_manifest}").decode())
86+
# The version looks like `version = "0.99.0 (797e8a9bc 2026-08-05)"` - we only want the first part.
87+
return manifest["pkg"]["rust"]["version"].split()[0]
88+
89+
90+
def update_fixed_rust_toolchain_versions(version: str) -> None:
91+
"""Change the fixed toolchain in the places where it's hardcoded"""
92+
toolchain_rs = TOOLCHAIN_RS.read_text()
93+
prefix = 'const FIXED_RUST_TOOLCHAIN: &str = "'
94+
old_version = toolchain_rs.split(prefix, 1)[1].split('"', 1)[0]
95+
TOOLCHAIN_RS.write_text(
96+
toolchain_rs.replace(
97+
f'{prefix}{old_version}"',
98+
f'{prefix}{version}"',
99+
)
100+
)
101+
102+
for test_setup in (INTEGRATION_TEST_CONFTEST, QL_TEST_SETUP):
103+
contents = test_setup.read_text()
104+
test_setup.write_text(
105+
contents.replace(
106+
f"rustup toolchain install {old_version} ",
107+
f"rustup toolchain install {version} ",
108+
)
109+
)
110+
111+
112+
def align_rust_analyzer_versions(manifest: str) -> tuple[str, bool]:
113+
"""Align ra_ap dependency versions in a Cargo manifest.
114+
115+
rust-analyzer crates may be published gradually, so this selects the newest
116+
version that is available for every ra_ap dependency.
117+
"""
118+
dependencies = {
119+
name: version
120+
for name, version in tomllib.loads(manifest)["dependencies"].items()
121+
if name.startswith("ra_ap_")
122+
}
123+
if len(set(dependencies.values())) == 1:
124+
# All the `ra_ap_` dependencies agree
125+
return manifest, False
126+
127+
version = get_min_rust_analyzer_version(manifest)
128+
129+
for name, old_version in dependencies.items():
130+
manifest = manifest.replace(
131+
f'{name} = "{old_version}"',
132+
f'{name} = "{version}"',
133+
)
134+
return manifest, True
135+
136+
137+
def commit_all(title: str) -> None:
138+
run("git", "add", "--all")
139+
run("git", "commit", "--no-verify", "--allow-empty", "-m", title)
140+
141+
142+
def main() -> None:
143+
status = subprocess.run(
144+
["git", "status", "--porcelain"],
145+
cwd=REPO_ROOT,
146+
check=True,
147+
capture_output=True,
148+
text=True,
149+
).stdout
150+
if status:
151+
raise RuntimeError("the working tree must be clean")
152+
153+
print_step(1, "Update dependencies")
154+
manifest = RUST_EXTRACTOR_MANIFEST.read_text()
155+
old_rust_analyzer_version = get_min_rust_analyzer_version(manifest)
156+
run("cargo", "upgrade", "--incompatible", "--pinned")
157+
# Re-read the (potentially) changed manifest
158+
manifest = RUST_EXTRACTOR_MANIFEST.read_text()
159+
new_rust_analyzer_version = get_min_rust_analyzer_version(manifest)
160+
if new_rust_analyzer_version == old_rust_analyzer_version:
161+
run("git", "restore", ".")
162+
print("No new rust-analyzer version available.")
163+
return
164+
165+
aligned_manifest, manifest_changed = align_rust_analyzer_versions(manifest)
166+
if manifest_changed:
167+
RUST_EXTRACTOR_MANIFEST.write_text(aligned_manifest)
168+
run("cargo", "update")
169+
commit_all("Cargo: Upgrade dependencies")
170+
171+
print_step(2, "Update the fixed Rust toolchain used by the extractor")
172+
rust_toolchain = get_compatible_rust_toolchain(new_rust_analyzer_version)
173+
update_fixed_rust_toolchain_versions(rust_toolchain)
174+
commit_all("Rust: Update fixed toolchain")
175+
176+
print_step(3, "Regenerate vendored bazel files")
177+
run("misc/bazel/3rdparty/update_tree_sitter_extractors_deps.sh")
178+
commit_all("Bazel: Regenerate vendored cargo dependencies")
179+
180+
print_step(4, "Run codegen")
181+
run_codegen()
182+
commit_all("Rust: Run codegen")
183+
184+
print_step(5, "Try compiling")
185+
run("bazel", "run", "//rust:install")
186+
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)