Repository navigation
ci(deps): check dependency sources and pin workflow actions #1489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
68f4ffc
2c41caa
a35ed51
17ef3ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| #!/usr/bin/env python3 | ||
| """Reject Cargo source/path overrides in tracked repository configuration (Python 3.11+).""" | ||
|
|
||
| from pathlib import Path | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import tomllib | ||
|
|
||
|
|
||
| def check(root: Path) -> list[str]: | ||
| """Inspect tracked modern and legacy configs, including those in nested workspaces.""" | ||
| paths = os.fsdecode(subprocess.check_output(["git", "ls-files", "-z"], cwd=root)).split("\0") | ||
| errors = [] | ||
| for name in paths: | ||
| path = Path(name) | ||
| if path.name.lower() == ".cargo" and (root / path).is_symlink(): | ||
| errors.append(f"{name}: symlinked Cargo config directories are not allowed") | ||
| continue | ||
| if path.parent.name.lower() != ".cargo" or path.name.lower() not in {"config", "config.toml"}: | ||
| continue | ||
| try: | ||
| with (root / path).open("rb") as source: | ||
| config = tomllib.load(source) | ||
| except (OSError, ValueError): | ||
| errors.append(f"{name}: cannot read valid TOML") | ||
| continue | ||
| if any(config.get(key) for key in ("source", "paths", "patch", "include")): | ||
| errors.append(f"{name}: Cargo source/path/patch overrides and included configs are not allowed") | ||
| return errors | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| repo = Path(__file__).resolve().parents[2] | ||
| try: | ||
| problems = check(repo) | ||
| except subprocess.CalledProcessError: | ||
| sys.exit("cannot list tracked Cargo configuration") | ||
| for problem in problems: | ||
| print(problem, file=sys.stderr) | ||
| sys.exit(bool(problems)) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| """Regression checks for Cargo configuration that can bypass registry source checks.""" | ||
|
|
||
| import importlib.util | ||
| import os | ||
| from pathlib import Path | ||
| import subprocess | ||
| import tempfile | ||
| import unittest | ||
|
|
||
| SPEC = importlib.util.spec_from_file_location("cargo_sources", Path(__file__).with_name("check-cargo-sources.py")) | ||
| MODULE = importlib.util.module_from_spec(SPEC) | ||
| SPEC.loader.exec_module(MODULE) | ||
|
|
||
|
|
||
| class CargoSources(unittest.TestCase): | ||
| def check_config(self, name, text, directory_link=None): | ||
| """Track a config and optional directory link, with no dependency downloads or builds.""" | ||
| with tempfile.TemporaryDirectory() as folder: | ||
| root = Path(folder) | ||
| subprocess.run(["git", "init", "-q", folder], check=True) | ||
| path = root / name | ||
| path.parent.mkdir(parents=True) | ||
| path.write_text(text) | ||
| tracked = [name] | ||
| if directory_link is not None: | ||
| link = root / directory_link | ||
| link.parent.mkdir(parents=True, exist_ok=True) | ||
| link.symlink_to(path.parent, target_is_directory=True) | ||
| tracked.append(directory_link) | ||
| subprocess.run(["git", "add", "--", *tracked], cwd=root, check=True) | ||
| return MODULE.check(root) | ||
|
|
||
| def test_build_options_are_allowed(self): | ||
| for name in [".cargo/config.toml", ".Cargo/Config.toml"]: | ||
| with self.subTest(name=name): | ||
| self.assertEqual(self.check_config(name, '[alias]\nxtask = "run -p xtask --"\n'), []) | ||
|
|
||
| def test_overrides_and_includes_are_rejected_in_both_formats(self): | ||
| for name in [".cargo/config", "fuzz/.cargo/config.toml", ".Cargo/Config.toml", "fuzz/.CARGO/CONFIG"]: | ||
| for text in [ | ||
| '[source.crates-io]\nreplace-with = "mirror"\n', | ||
| 'paths = ["../dependency"]\n', | ||
| '[patch.crates-io]\nexample = { path = "../dependency" }\n', | ||
| 'include = ["other.toml"]\n', | ||
| 'include = [{ path = "other.toml", optional = true }]\n', | ||
| ]: | ||
| with self.subTest(name=name, text=text): | ||
| problems = self.check_config(name, text) | ||
| self.assertEqual(len(problems), 1) | ||
| self.assertTrue(problems[0].startswith(name + ":")) | ||
|
|
||
| def test_cargo_directory_symlinks_are_rejected(self): | ||
| for name in [".cargo", "fuzz/.Cargo"]: | ||
| with self.subTest(name=name): | ||
| problems = self.check_config("ci/cargo/config.toml", '[source.crates-io]\nreplace-with = "mirror"\n', name) | ||
| self.assertEqual(problems, [name + ": symlinked Cargo config directories are not allowed"]) | ||
|
|
||
| @unittest.skipUnless(os.name == "posix", "non-UTF-8 filename fixture requires POSIX") | ||
| def test_non_utf8_filenames_do_not_stop_config_checks(self): | ||
| self.assertEqual(self.check_config(os.fsdecode(b"unrelated/other-\xff"), "ordinary tracked file"), []) | ||
| name = os.fsdecode(b"other-\xff/.cargo/config.toml") | ||
| problems = self.check_config(name, '[source.crates-io]\nreplace-with = "mirror"\n') | ||
| self.assertEqual(problems, [name + ": Cargo source/path/patch overrides and included configs are not allowed"]) | ||
|
|
||
| def test_invalid_toml_is_rejected(self): | ||
| self.assertEqual(self.check_config(".cargo/config.toml", "[broken"), [".cargo/config.toml: cannot read valid TOML"]) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,28 +24,28 @@ jobs: | |||||||||
| name: Lint (pre-commit) | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@stable | ||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug (confidence: medium, about 70%; I could not run this offline):
Suggested change
The same applies to the 19
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pinned action definitions set the |
||||||||||
| with: | ||||||||||
| components: rustfmt, clippy | ||||||||||
| - uses: Swatinem/rust-cache@v2 | ||||||||||
| - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 | ||||||||||
| with: | ||||||||||
| # Only main saves the cache (see test-rust.yml). | ||||||||||
| save-if: ${{ github.ref == 'refs/heads/main' }} | ||||||||||
| - uses: actions/setup-python@v7 | ||||||||||
| - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 | ||||||||||
| with: | ||||||||||
| python-version: "3.12" | ||||||||||
| - uses: pre-commit/action@v3.0.1 | ||||||||||
| - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 | ||||||||||
|
|
||||||||||
| wasm: | ||||||||||
| name: Web app (wasm32) | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@stable | ||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable | ||||||||||
| with: | ||||||||||
| targets: wasm32-unknown-unknown | ||||||||||
| - uses: Swatinem/rust-cache@v2 | ||||||||||
| - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 | ||||||||||
| with: | ||||||||||
| # Only main saves the cache (see test-rust.yml). | ||||||||||
| save-if: ${{ github.ref == 'refs/heads/main' }} | ||||||||||
|
|
@@ -59,17 +59,21 @@ jobs: | |||||||||
| name: Licences | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@stable | ||||||||||
| - uses: Swatinem/rust-cache@v2 | ||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||||||||||
| - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable | ||||||||||
| - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 | ||||||||||
| with: | ||||||||||
| # Only main saves the cache (see test-rust.yml). | ||||||||||
| save-if: ${{ github.ref == 'refs/heads/main' }} | ||||||||||
| - uses: taiki-e/install-action@v2 | ||||||||||
| - uses: taiki-e/install-action@c509219cf42fba86913069efd0ade2458c59337b # v2 | ||||||||||
| with: | ||||||||||
| tool: cargo-deny,cargo-about@0.9.2 | ||||||||||
| - name: Dependency licences | ||||||||||
| run: cargo deny --locked check licenses | ||||||||||
| - name: Cargo source replacements | ||||||||||
| run: | | ||||||||||
| python3 -m unittest discover -s .github/scripts -p 'test_*.py' | ||||||||||
| python3 .github/scripts/check-cargo-sources.py | ||||||||||
| - name: Dependency licences and sources | ||||||||||
| run: cargo deny --locked check licenses sources | ||||||||||
| - name: Third-party notices | ||||||||||
| # Exit code 2 is stale notices (a warning); anything else (cargo-about failing, a licence | ||||||||||
| # it can't work out) fails the job. | ||||||||||
|
|
@@ -88,7 +92,7 @@ jobs: | |||||||||
| outputs: | ||||||||||
| os: ${{ steps.os.outputs.os }} | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v7 | ||||||||||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | ||||||||||
| with: | ||||||||||
| sparse-checkout: .github/scripts | ||||||||||
| - id: os | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Symlinked
.cargodirectory bypasses the check (confidence: medium-high, about 75%; I could not run this offline).git ls-fileslists a tracked symlink by its own path and does not list the files behind it. Take a tracked symlink.cargo -> ci/cargoand a trackedci/cargo/config.tomlcontaining[source.crates-io] replace-with = "mirror":.cargois listed, but it is a symlink, so it is not a config path and nothing is reported.ci/cargo/config.tomlis listed, but its parent iscargo, not.cargo, so the filter on line 22 skips it..cargo/config.toml, so the source replacement applies.cargo deny sourcesstill passes, becauseCargo.lockkeeps the crates.io source URL when a replacement is in use.So the docs' statement that these checks "cannot be bypassed" is too strong. A small fix is to reject any tracked symlink named
.cargo. With no such symlink, every.cargocomponent is a real directory, and the existing filter sees every config file Cargo can read:Please add a regression test next to
test_invalid_toml_is_rejected. It should track a.cargosymlink to a directory holding a[source]config and expect one error for the symlink path. Tests needgitto track symlinks, socheck_configmay need a variant that creates the link.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 17ef3ef: tracked symlinked
.cargodirectories are rejected, including nested and mixed-case names. The root and nested symlink regressions pass.