diff --git a/.github/scripts/check-cargo-sources.py b/.github/scripts/check-cargo-sources.py new file mode 100644 index 000000000..89d414712 --- /dev/null +++ b/.github/scripts/check-cargo-sources.py @@ -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)) diff --git a/.github/scripts/test_cargo_sources.py b/.github/scripts/test_cargo_sources.py new file mode 100644 index 000000000..7444d7782 --- /dev/null +++ b/.github/scripts/test_cargo_sources.py @@ -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() diff --git a/.github/workflows/ci-label.yml b/.github/workflows/ci-label.yml index e3ada4342..61b47b7f5 100644 --- a/.github/workflows/ci-label.yml +++ b/.github/workflows/ci-label.yml @@ -21,7 +21,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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 632dd21e6..297b0cc75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 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 diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 0ab862566..0dc388d38 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -185,7 +185,7 @@ jobs: echo "sha=$sha" >> "$GITHUB_OUTPUT" - name: Checkout PR head for review context (read-only; never executed) - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Check out the PR head so Read/Grep/Glob show the *proposed* file # contents (and newly added files) that Claude is reviewing — the base diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 34c3cc20c..319011ba1 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -75,15 +75,15 @@ jobs: packages: write steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: docker/setup-buildx-action@v4 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - name: Image tags and labels id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 env: # The registry shows the index's annotations for a multi-platform image. DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index @@ -103,7 +103,7 @@ jobs: # The image for the runner's platform, loaded into Docker to check that it serves the app. - name: Build for the smoke test - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . platforms: linux/amd64 @@ -132,7 +132,7 @@ jobs: - name: Sign in to the GitHub Container Registry if: env.PUBLISH == 'true' - uses: docker/login-action@v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -141,7 +141,7 @@ jobs: # Both platforms; the build stage is the smoke test's, from the builder's cache. - name: Publish if: env.PUBLISH == 'true' - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . platforms: linux/amd64,linux/arm64 @@ -169,15 +169,15 @@ jobs: runner: ubuntu-24.04-arm steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: docker/setup-buildx-action@v4 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - name: Image labels id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.CLI_IMAGE }} flavor: latest=false @@ -189,7 +189,7 @@ jobs: org.opencontainers.image.url=https://rustgis.app - name: Build for the smoke test - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . file: packaging/docker/cli.Dockerfile @@ -214,7 +214,7 @@ jobs: - name: Sign in to the GitHub Container Registry if: env.PUBLISH == 'true' - uses: docker/login-action@v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -224,7 +224,7 @@ jobs: - name: Publish by digest id: push if: env.PUBLISH == 'true' - uses: docker/build-push-action@v7 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . file: packaging/docker/cli.Dockerfile @@ -244,7 +244,7 @@ jobs: - name: Upload the digest if: env.PUBLISH == 'true' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: cli-digest-${{ matrix.arch }} path: ${{ runner.temp }}/digests/* @@ -262,24 +262,24 @@ jobs: packages: write steps: - name: Download the digests - uses: actions/download-artifact@v8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: path: ${{ runner.temp }}/digests pattern: cli-digest-* merge-multiple: true - - uses: docker/setup-buildx-action@v4 + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - name: Image tags id: meta - uses: docker/metadata-action@v6 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 with: images: ${{ env.CLI_IMAGE }} flavor: latest=false tags: ${{ env.TAGS }} - name: Sign in to the GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/docs-preview-publish.yml b/.github/workflows/docs-preview-publish.yml index 60d2d7fad..963c5dfe0 100644 --- a/.github/workflows/docs-preview-publish.yml +++ b/.github/workflows/docs-preview-publish.yml @@ -63,7 +63,7 @@ jobs: echo "has_pr=$(jq 'index("preview-pr") != null' <<< "$names")" >> "$GITHUB_OUTPUT" echo "has_site=$(jq 'index("preview-site") != null' <<< "$names")" >> "$GITHUB_OUTPUT" - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 if: steps.artifacts.outputs.has_pr == 'true' with: name: preview-pr @@ -144,7 +144,7 @@ jobs: echo "fork=${fork}" } >> "$GITHUB_OUTPUT" - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 if: steps.pr.outputs.mode == 'deploy' with: name: preview-site @@ -240,7 +240,7 @@ jobs: if: >- (steps.deploy.outcome == 'success' && steps.pr.outputs.mode == 'deploy') || steps.pr.outputs.mode == 'awaiting' - uses: actions/github-script@v9 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 env: PR: ${{ steps.pr.outputs.number }} MODE: ${{ steps.pr.outputs.mode }} diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml index 9baca9c46..e00539a32 100644 --- a/.github/workflows/docs-preview.yml +++ b/.github/workflows/docs-preview.yml @@ -34,10 +34,10 @@ jobs: permissions: contents: read steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: astral-sh/setup-uv@v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: enable-cache: false - name: Build docs with Zensical @@ -49,10 +49,10 @@ jobs: rm -f site/CNAME # Production serves the app from web.rustgis.app (web-deploy.yml); previews include it # at /app/ so a pull request's web build can be tried before merging. - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 - name: Build the web app run: | # Pinned Trunk release, checksum-verified, installed outside root-owned directories. @@ -67,7 +67,7 @@ jobs: mkdir -p ../../site/app cp -r ../../dist/web/. ../../site/app/ - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: preview-site path: site @@ -85,7 +85,7 @@ jobs: run: | mkdir -p preview-pr printf '%s\n' "$PR" > preview-pr/pr.txt - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: preview-pr path: preview-pr diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 78654ac6e..ffe671d8f 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -18,8 +18,8 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: astral-sh/setup-uv@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 with: enable-cache: false - name: Configure Google Analytics @@ -46,7 +46,7 @@ jobs: uv venv --python 3.12 uv pip install -r requirements_docs.txt .venv/bin/zensical build - - uses: actions/upload-pages-artifact@v5 + - uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5 with: path: ./site @@ -58,4 +58,4 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} steps: - id: deployment - uses: actions/deploy-pages@v5 + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5 diff --git a/.github/workflows/downloads-mirror.yml b/.github/workflows/downloads-mirror.yml index 90bc4f91f..55480f705 100644 --- a/.github/workflows/downloads-mirror.yml +++ b/.github/workflows/downloads-mirror.yml @@ -48,7 +48,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DESTINATION_REPOSITORY: opengeos/rustgis-downloads steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 2fba0efcb..9b1c62cc0 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -37,15 +37,15 @@ jobs: if: (github.event_name == 'pull_request' && github.event.label.name == 'ci:fuzz') || (github.event_name != 'pull_request' && (github.repository == 'opengeos/RustGIS' || github.event_name == 'workflow_dispatch')) 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: components: rustfmt - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@c509219cf42fba86913069efd0ade2458c59337b # v2 with: tool: cargo-deny - # fuzz/deny.toml: the workspace's licences plus NCSA, for libFuzzer. - - run: cargo deny --manifest-path fuzz/Cargo.toml --locked check licenses + # fuzz/deny.toml: the workspace's source policy and licences plus NCSA, for libFuzzer. + - run: cargo deny --manifest-path fuzz/Cargo.toml --locked check licenses sources # The workspace's `cargo fmt --all` doesn't reach this crate. - run: cargo fmt --manifest-path fuzz/Cargo.toml --check @@ -63,19 +63,19 @@ jobs: SECONDS_PER_TARGET: ${{ github.event_name == 'pull_request' && '60' || inputs.seconds || '600' }} TARGET: ${{ matrix.target }} steps: - - uses: actions/checkout@v7 - - uses: dtolnay/rust-toolchain@nightly - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: dtolnay/rust-toolchain@2c76c034f9e32a5d6e2c104dc4df23a22476db04 # nightly + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 with: workspaces: fuzz shared-key: fuzz save-if: ${{ github.ref == 'refs/heads/main' && matrix.target == 'read_layers' }} - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@c509219cf42fba86913069efd0ade2458c59337b # v2 with: tool: cargo-fuzz # The corpus each target has grown, kept between nights. Restored and saved as separate steps: # the combined action saves only when the job passes, and a target with a known finding fails. - - uses: actions/cache/restore@v6 + - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: fuzz/corpus/${{ matrix.target }} key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} @@ -119,13 +119,13 @@ jobs: fi - name: Keep the corpus if: ${{ !cancelled() && github.event_name != 'pull_request' }} - uses: actions/cache/save@v6 + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: fuzz/corpus/${{ matrix.target }} key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }} - name: Keep the failing inputs if: failure() && steps.findings.outcome == 'failure' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: fuzz-${{ matrix.target }} path: fuzz/artifacts/${{ matrix.target }} @@ -140,7 +140,7 @@ jobs: permissions: issues: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: pattern: fuzz-* path: findings diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 8348a8f45..e5da47888 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -68,12 +68,12 @@ jobs: outputs: tested: ${{ steps.data.outputs.ok }} 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: save-if: false - - uses: actions/cache@v6 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: workflow-data key: workflow-data-census-2024-1 diff --git a/.github/workflows/package-managers.yml b/.github/workflows/package-managers.yml index ef6f6cbf1..357aecbdf 100644 --- a/.github/workflows/package-managers.yml +++ b/.github/workflows/package-managers.yml @@ -83,7 +83,7 @@ jobs: env: TAP_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 if: env.TAP_KEY != '' with: persist-credentials: false @@ -122,7 +122,7 @@ jobs: env: AUR_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 if: env.AUR_KEY != '' with: persist-credentials: false @@ -165,7 +165,7 @@ jobs: env: COPR_CONFIG: ${{ secrets.COPR_CONFIG }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 if: env.COPR_CONFIG != '' with: persist-credentials: false diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index f7da97686..a9f8ce846 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -41,7 +41,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 @@ -72,17 +72,17 @@ jobs: - { name: windows-x86_64, runner: windows-latest, target: x64 } - { name: windows-aarch64, runner: windows-11-arm, target: aarch64 } steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # The Windows ARM64 image ships no Rust toolchain; maturin-action needs rustup. - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable if: runner.os == 'Windows' && runner.arch == 'ARM64' - - uses: PyO3/maturin-action@v1 + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1 with: working-directory: apps/rustgis-python target: ${{ matrix.platform.target }} manylinux: ${{ matrix.platform.manylinux || 'auto' }} args: --release --locked --out dist - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 with: python-version: "3.14" # Each runner builds for its own architecture, so the wheel installs and runs here. @@ -91,7 +91,7 @@ jobs: run: | python -m pip install --no-index --find-links apps/rustgis-python/dist rustgis python -c "import rustgis; s = rustgis.Session(sample=True); assert s.layers() and s.to_png(width=64, height=32)" - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: wheels-${{ matrix.platform.name }} path: apps/rustgis-python/dist @@ -102,9 +102,9 @@ jobs: name: Source distribution 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' }} @@ -112,12 +112,12 @@ jobs: # reach the repository root). - name: The license copy matches LICENSE run: cmp LICENSE apps/rustgis-python/LICENSE || { echo "::error::apps/rustgis-python/LICENSE differs from LICENSE."; exit 1; } - - uses: PyO3/maturin-action@v1 + - uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1 with: working-directory: apps/rustgis-python command: sdist args: --out dist - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 with: python-version: "3.14" - name: Build and test from the sdist alone @@ -132,7 +132,7 @@ jobs: tar tzf ./*.tar.gz | grep -E '^[^/]+/THIRD-PARTY-LICENSES.txt$' || { echo "::error::The sdist has no THIRD-PARTY-LICENSES.txt at its root."; exit 1; } python -m pip install ./*.tar.gz pytest python -m pytest -q test_session.py - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: wheels-sdist path: apps/rustgis-python/dist @@ -146,7 +146,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: pattern: wheels-* merge-multiple: true @@ -166,11 +166,11 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: pattern: wheels-* merge-multiple: true path: dist - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1 with: packages-dir: dist diff --git a/.github/workflows/reference.yml b/.github/workflows/reference.yml index 871b4f0f3..ecd4da541 100644 --- a/.github/workflows/reference.yml +++ b/.github/workflows/reference.yml @@ -25,8 +25,8 @@ jobs: name: Reference answers match generate.py runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: astral-sh/setup-uv@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7 - name: Regenerate and compare working-directory: crates/analysis/tests/reference run: uv run generate.py --check diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml index 5c734b895..6ba5932aa 100644 --- a/.github/workflows/release-dry-run.yml +++ b/.github/workflows/release-dry-run.yml @@ -53,7 +53,7 @@ jobs: # Dispatch the release workflows and read back their conclusions. actions: write steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ inputs.ref || github.ref }} persist-credentials: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86dfb95d8..4f487a379 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,13 +45,13 @@ jobs: - name: macOS Intel os: macos-15-intel steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: targets: ${{ matrix.target }} - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 - name: Version id: version @@ -267,7 +267,7 @@ jobs: rm -rf squashfs-root - name: Upload workflow artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: rustgis-${{ matrix.os }} path: dist/* @@ -277,11 +277,11 @@ jobs: name: Web app 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 - name: Install Trunk run: | set -euo pipefail @@ -308,7 +308,7 @@ jobs: exit 1 fi (cd dist/web && zip -r "../RustGIS-$version-web.zip" .) - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: rustgis-web path: dist/RustGIS-*-web.zip @@ -320,12 +320,12 @@ jobs: name: Third-party notices 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: save-if: false - - uses: taiki-e/install-action@v2 + - uses: taiki-e/install-action@c509219cf42fba86913069efd0ade2458c59337b # v2 with: tool: cargo-about@0.9.2 - name: The notices are up to date @@ -336,7 +336,7 @@ jobs: needs: [build, web, notices] runs-on: ubuntu-latest steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: path: dist merge-multiple: true @@ -345,7 +345,7 @@ jobs: working-directory: dist run: sha256sum -- * | tee SHA256SUMS - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7 with: name: rustgis-sha256sums path: dist/SHA256SUMS @@ -358,7 +358,7 @@ jobs: permissions: contents: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8 with: path: dist merge-multiple: true diff --git a/.github/workflows/test-python.yml b/.github/workflows/test-python.yml index bfaa622c9..bd96cf84d 100644 --- a/.github/workflows/test-python.yml +++ b/.github/workflows/test-python.yml @@ -40,15 +40,15 @@ jobs: matrix: include: ${{ fromJSON(needs.plan.outputs.include) }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - 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: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7 with: python-version: ${{ matrix.python }} # rustgis.qt's tests need Qt WebEngine: one Linux job installs it (a large download) and diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 9f7451e8f..718d86ca6 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -42,11 +42,11 @@ jobs: matrix: os: ${{ fromJSON(inputs.os) }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 with: # Pull requests restore main's cache but don't save their own: saving takes minutes on # Windows, and pull request caches crowd main's out of the repository's cache space. diff --git a/.github/workflows/web-deploy.yml b/.github/workflows/web-deploy.yml index 6c0de301c..1fe191ff9 100644 --- a/.github/workflows/web-deploy.yml +++ b/.github/workflows/web-deploy.yml @@ -30,7 +30,7 @@ jobs: timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ inputs.ref || github.sha }} persist-credentials: false @@ -39,10 +39,10 @@ jobs: id: built run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@b725eda525362e7cf34af9a7243cac1e3f01a7ec # stable with: targets: wasm32-unknown-unknown - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2 - name: Build the web app run: | diff --git a/deny.toml b/deny.toml index 3c12fd75c..78e882a36 100644 --- a/deny.toml +++ b/deny.toml @@ -3,7 +3,7 @@ [graph] all-features = false -exclude-dev = true +exclude-dev = false [licenses] confidence-threshold = 0.8 @@ -29,3 +29,12 @@ allow = [ [licenses.private] # RustGIS's own apps and xtask (`publish = false`). ignore = true + +# Dependencies currently come only from crates.io. An intentional git or registry source must +# be reviewed here too; git sources must name a revision rather than follow a branch or tag. +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] +required-git-spec = "rev" diff --git a/docs/contributing.md b/docs/contributing.md index 1c3dfdecd..d8fcc18af 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -39,7 +39,20 @@ breaks of the rules in AGENTS.md are fixed in the pull request. `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace -q` and `cargo xtask layers`. The pre-commit hooks also check TOML, YAML, JSON, whitespace and large files. CI separately builds `rustgis-web` for `wasm32-unknown-unknown` and runs -`cargo deny --locked check licenses` against `deny.toml`. +`cargo deny --locked check licenses sources` against `deny.toml`, including development +dependencies. The fuzz workflow applies the same source policy using `fuzz/deny.toml`, which +also permits NCSA for libFuzzer. Sources are limited to crates.io; an intentional git source needs an explicit +allowlist entry and a pinned revision. CI also runs `python3 .github/scripts/check-cargo-sources.py` +to reject repository Cargo configuration with `source`, `paths` or `patch` overrides, including +case variants of `.cargo/config` and `.cargo/config.toml` used on case-insensitive filesystems. +Tracked `.cargo` directories must be real directories. +Config `include` directives are also rejected so those checks cannot be bypassed by loading +another file; put intentional dependency patches in `Cargo.toml` for review. +User-level Cargo mirrors are outside this repository check. + +Workflow actions are pinned to full commit SHAs with the previous version or branch in a comment. +Dependabot's existing GitHub Actions updates keep these pins current. When changing an action, +resolve the commit in that action's own repository and keep the version comment in step. Other workspace tasks: diff --git a/fuzz/deny.toml b/fuzz/deny.toml index 9947a6c7a..788cb9b37 100644 --- a/fuzz/deny.toml +++ b/fuzz/deny.toml @@ -1,10 +1,10 @@ -# cargo-deny for the fuzz crate (`cargo deny --manifest-path fuzz/Cargo.toml check licenses`, run by +# cargo-deny for the fuzz crate (`cargo deny --manifest-path fuzz/Cargo.toml check licenses sources`, run by # the fuzz workflow): the workspace's deny.toml, plus NCSA for libFuzzer, which `libfuzzer-sys` # compiles. Nothing here is shipped; keep the rest in step with ../deny.toml. [graph] all-features = false -exclude-dev = true +exclude-dev = false [licenses] confidence-threshold = 0.8 @@ -31,3 +31,11 @@ allow = [ [licenses.private] # RustGIS's own crates and this one (`publish = false`). ignore = true + +# Apply the workspace source policy here too; fuzz dependencies are built by CI. +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] +required-git-spec = "rev"