diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7ffd8420..b7e4f906 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,14 +1,26 @@ # Pull requests that touch these files need a maintainer's approval (branch # protection: "Require review from Code Owners"). They hold recorded results, # the tests and the settings that decide which tests run, and the CI that runs -# them; a pull request must not edit them to make itself pass. -/test/ @Hananel-Hazan -/.github/ @Hananel-Hazan -/pyproject.toml @Hananel-Hazan -/poetry.lock @Hananel-Hazan -/pytest.ini @Hananel-Hazan -/.pytest.ini @Hananel-Hazan -/tox.ini @Hananel-Hazan -/setup.cfg @Hananel-Hazan -/.coveragerc @Hananel-Hazan -/conftest.py @Hananel-Hazan +# them; a pull request must not edit them to make itself pass. The rest run +# automatically (editor, container and build configuration), decide what gets +# installed, or define the learning rules the tests pin to their papers. +/test/ @Hananel-Hazan +/.github/ @Hananel-Hazan +/pyproject.toml @Hananel-Hazan +/poetry.lock @Hananel-Hazan +/pytest.ini @Hananel-Hazan +/.pytest.ini @Hananel-Hazan +/tox.ini @Hananel-Hazan +/setup.cfg @Hananel-Hazan +/.coveragerc @Hananel-Hazan +conftest.py @Hananel-Hazan +/.vscode/ @Hananel-Hazan +/.devcontainer/ @Hananel-Hazan +/.pre-commit-config.yaml @Hananel-Hazan +/Dockerfile @Hananel-Hazan +/.dockerignore @Hananel-Hazan +/.readthedocs.yaml @Hananel-Hazan +/docs/source/conf.py @Hananel-Hazan +/bindsnet/learning/ @Hananel-Hazan +/SECURITY.md @Hananel-Hazan +/CONTRIBUTING.md @Hananel-Hazan diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 019c71d9..bd252bc5 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,7 +23,15 @@ - `test/api_surface.json` (public API) - `FIXED_BUGS` in `test/network/test_all_classes.py` - [ ] I added an entry to the `Unreleased` section of `CHANGELOG.md`. -- [ ] If I used an AI tool to write or find this change, I name it here: + +## AI assistance + + + +- [ ] No AI tools were used. +- [ ] AI tools were used, and I (a human) have read every changed line, run the tests myself, and can explain the change in my own words. + +Tools used and for which parts: ## Why the results or the API changed (if they did) diff --git a/.github/workflows/ai-statement.yml b/.github/workflows/ai-statement.yml new file mode 100644 index 00000000..94212c18 --- /dev/null +++ b/.github/workflows/ai-statement.yml @@ -0,0 +1,42 @@ +# Checks that a pull request fills in the "AI assistance" section of the template +# (CONTRIBUTING.md, "AI-assisted contributions"). It reads the description only and +# runs no code from the pull request. +name: AI statement + +on: + pull_request: + branches: [ master ] + types: [ opened, edited, synchronize, reopened ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + ai-statement: + if: github.event.pull_request.user.login != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + # The description is passed as an environment variable, never pasted into the + # script, so text in it cannot run as code. + - name: Check the AI assistance section + env: + PR_BODY: ${{ github.event.pull_request.body }} + shell: python + run: | + import os, re, sys + + body = os.environ.get("PR_BODY") or "" + none = re.search(r"^- \[[xX]\] No AI tools were used", body, re.M) + used = re.search(r"^- \[[xX]\] AI tools were used, and I \(a human\) have read every changed line", body, re.M) + tools = re.search(r"^Tools used and for which parts:[ \t]*(\S.*)?$\n?((?:(?!^#).*\n?)*)", body, re.M) + described = bool(tools and ((tools.group(1) or "") + tools.group(2)).strip()) + if bool(none) == bool(used): + sys.exit("Tick exactly one box in the 'AI assistance' section of the pull request description " + "(see CONTRIBUTING.md, 'AI-assisted contributions').") + if used and not described: + sys.exit("Say which AI tools were used and for which parts, under 'Tools used and for which parts:'.") + print("AI assistance section filled in.") diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 77f33fd4..f9ee51b4 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -26,12 +26,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # the base commit must be present for git worktree + persist-credentials: false - name: Install Poetry run: pipx install "poetry==${POETRY_VERSION}" - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" cache: poetry @@ -49,7 +50,7 @@ jobs: poetry run python .github/scripts/bench_ab.py --base "$(git rev-parse HEAD^1)" --head "$HEAD_SHA" --fail-above 2 --out bench-out --summary "$GITHUB_STEP_SUMMARY" - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 if: always() with: name: bench-ab diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index 8db2090e..3dae0d1b 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -18,8 +18,10 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" # Use the black and isort versions pinned in poetry.lock, so CI and local diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..7fb0155a --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,25 @@ +# Lists the packages a pull request adds or changes (pyproject.toml, poetry.lock, +# the Dockerfile's base image, GitHub Actions) and fails if any has a known +# vulnerability of moderate severity or worse. It reads GitHub's dependency graph +# only and runs no code from the pull request. +name: Dependency review + +on: + pull_request: + branches: [ master ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + comment-summary-in-pr: never diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b4657477..3c7810b9 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -22,7 +22,9 @@ jobs: docker: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # The image is about 7 GB (torch bundles its CUDA libraries); make room. - name: Free runner disk space run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2f1c5844..2deb2aaa 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,8 +12,10 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-python@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" - name: Build sdist and wheel @@ -30,7 +32,7 @@ jobs: echo "Release tag $GITHUB_REF_NAME does not match pyproject version $version" exit 1 fi - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: dist path: dist/ @@ -44,8 +46,8 @@ jobs: permissions: id-token: write steps: - - uses: actions/download-artifact@v8 + - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: dist path: dist/ - - uses: pypa/gh-action-pypi-publish@release/v1 + - uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a1132a37..ee427558 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -23,10 +23,12 @@ jobs: name: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install Poetry run: pipx install "poetry==${POETRY_VERSION}" - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" cache: poetry @@ -50,7 +52,7 @@ jobs: # below the value recorded in the script (see its docstring). - name: Check coverage floor run: python .github/scripts/coverage_floor.py coverage.xml - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: coverage-xml path: coverage.xml @@ -63,10 +65,12 @@ jobs: matrix: python-version: ["3.11", "3.12"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Install Poetry run: pipx install "poetry==${POETRY_VERSION}" - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: ${{ matrix.python-version }} cache: poetry diff --git a/.github/workflows/test-guard.yml b/.github/workflows/test-guard.yml index c37da6a9..3464146b 100644 --- a/.github/workflows/test-guard.yml +++ b/.github/workflows/test-guard.yml @@ -33,12 +33,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 # HEAD is the test merge; HEAD^1 is master it merges into + persist-credentials: false - name: Install Poetry run: pipx install "poetry==${POETRY_VERSION}" - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" cache: poetry @@ -57,12 +58,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 2 + persist-credentials: false - name: Install Poetry run: pipx install "poetry==${POETRY_VERSION}" - - uses: actions/setup-python@v7 + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 with: python-version: "3.13" cache: poetry diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 00000000..8cea8d56 --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,36 @@ +# Audits the workflow files themselves: zizmor looks for unsafe patterns (untrusted +# text reaching a shell, unpinned actions, leaked credentials), actionlint for mistakes. +# Both run from version-pinned packages, not third-party actions. +name: Workflow lint + +on: + push: + branches: [ master ] + paths: [ ".github/workflows/**" ] + pull_request: + branches: [ master ] + paths: [ ".github/workflows/**" ] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + workflow-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0 + with: + python-version: "3.13" + - name: zizmor + run: pipx run "zizmor==1.30.1" --offline .github/workflows + - name: actionlint + run: | + go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.12 + "$(go env GOPATH)/bin/actionlint" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb5b5eb..dd9d6185 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ see the [GitHub releases / tags](https://github.com/BindsNET/bindsnet/releases). ## [Unreleased] +### Added +- Contributor rules for AI-assisted work (`CONTRIBUTING.md`, "AI-assisted + contributions"): AI tools are welcome, a human must have read and run every change, + and autonomous agents may not submit. The pull request template has an "AI + assistance" section, and the `AI statement` workflow checks that it is filled in. +- `Workflow lint` workflow: zizmor 1.30.1 and actionlint 1.7.12 audit changes to + `.github/workflows/`. +- `Dependency review` workflow: fails a pull request that adds or updates a package + with a known vulnerability of moderate severity or worse. + +### Changed +- CI actions are pinned to exact commits instead of version tags, and checkouts no + longer keep the GitHub token on disk. +- `SECURITY.md` now describes the repository controls as they are. +- `CODEOWNERS` also covers files that run automatically or define tested behaviour: + `.vscode/`, `.devcontainer/`, `.pre-commit-config.yaml`, `Dockerfile`, + `.dockerignore`, `.readthedocs.yaml`, `docs/source/conf.py`, every `conftest.py`, + `bindsnet/learning/`, `SECURITY.md` and `CONTRIBUTING.md`. + +### Fixed +- The CUDA `test_transfer` tests in `test/network/test_nodes.py` and + `test/network/test_connections.py` are reported as skipped on machines without a + GPU; before, they returned early and were reported as passed. + ## [0.3.5] - 2026-10-29 ### Results changes (read this before upgrading) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b16e740..98ce2f9f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,31 @@ # Contributing Protocol +## AI-assisted contributions + +You are welcome to use AI tools to find bugs in BindsNET and to improve it. A +person must stay responsible for the work: + +- **You are the author.** The person who opens the pull request answers for every + line in it, whoever or whatever wrote it. +- **Read it and run it yourself.** Before you open the pull request, read every + changed line, run the tests on your own machine, and make sure you can explain + the change in your own words. +- **No autonomous agents.** An AI agent may not open pull requests, push commits or + post comments on its own. A pull request stating that no human has reviewed the + code is put on hold until its author has. +- **Say what you used.** Fill in the "AI assistance" section of the pull request + template: which tools, and for which parts. A CI check makes sure the section is + filled in. +- **Answer review questions yourself.** The maintainer may ask you about the design + of the change. Please answer in your own words, not with AI-generated text + (translation and grammar help are fine). + +This follows the policies of NumPy +(https://numpy.org/devdocs/dev/ai_policy.html) and LLVM +(https://llvm.org/docs/AIToolPolicy.html). + +## Getting started + To clone this project locally, issue ```shell diff --git a/SECURITY.md b/SECURITY.md index dad47ace..603f3f42 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -57,7 +57,14 @@ BindsNET is a research library, and its git history is part of what users rely on. The following controls are in place on this repository: - Force-pushes and branch deletions are blocked on every branch. -- `master` requires a pull request with an approving review. +- Changes reach `master` through pull requests with signed commits. Each needs + an approving review that covers its latest push, the code owners' review for + the paths in `.github/CODEOWNERS`, and passing test, formatting, test-guard + and speed checks. The maintainer reviews every pull request from an outside + contributor before it is merged. +- Workflows get a read-only token by default, every outside contributor's + pull request waits for the maintainer's approval before CI runs, and the + actions CI uses are pinned to exact commits. A supply-chain incident affecting this repository was reported and remediated in September 2026; see issue #781 for the full account. No PyPI release was diff --git a/test/network/test_connections.py b/test/network/test_connections.py index b5a5249d..3db015a1 100644 --- a/test/network/test_connections.py +++ b/test/network/test_connections.py @@ -87,10 +87,8 @@ def test_csrm_window_resizes_with_batch(self, previous_batch, new_batch): torch.testing.assert_close(target.v, reference.v) torch.testing.assert_close(target.s, reference.s) + @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a CUDA device") def test_transfer(self): - if not torch.cuda.is_available(): - return - connection_types = [ Connection, Conv2dConnection, diff --git a/test/network/test_nodes.py b/test/network/test_nodes.py index da5e319b..be01045c 100644 --- a/test/network/test_nodes.py +++ b/test/network/test_nodes.py @@ -1,3 +1,4 @@ +import pytest import torch from bindsnet.network import Network @@ -59,10 +60,8 @@ def test_init(self): assert (layer.s.float() == torch.zeros(n)).all() assert (layer.v == layer.rest * torch.ones(n)).all() + @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs a CUDA device") def test_transfer(self): - if not torch.cuda.is_available(): - return - for nodes in Nodes.__subclasses__(): layer = nodes(10)