Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
124 changes: 124 additions & 0 deletions .github/workflows/compliance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: Compliance (shared)

on:
workflow_call:
inputs:
tooling_repo:
description: Repository holding the shared compliance scripts and AI-POLICY.md
type: string
default: hyperspy/.github
tooling_ref:
description: Git ref of tooling_repo to use (tag, branch or SHA)
type: string
default: v1
pyproject:
description: Path to pyproject.toml used to derive towncrier settings
type: string
default: pyproject.toml
source_dirs:
description: Space-separated source directories whose .py changes require a changelog fragment (empty = derive from [tool.towncrier])
type: string
default: ''
changelog_dir:
description: Changelog fragment directory (empty = derive from [tool.towncrier].directory)
type: string
default: ''
check_changelog:
type: boolean
default: true
check_agents_policy:
type: boolean
default: true
agents_file:
description: Root AGENTS.md that must carry the pinned policy block
type: string
default: AGENTS.md

permissions:
contents: read

jobs:
ai-trailers:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: ${{ inputs.tooling_repo }}
ref: ${{ inputs.tooling_ref }}
path: .hyperspy-github
- name: Resolve commit range
id: range
shell: bash
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "mode=pr" >> "$GITHUB_OUTPUT"
echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
echo "head=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ] || [ -z "${{ github.event.before }}" ]; then
echo "mode=skip" >> "$GITHUB_OUTPUT"
else
echo "mode=push" >> "$GITHUB_OUTPUT"
echo "base=${{ github.event.before }}" >> "$GITHUB_OUTPUT"
echo "head=${{ github.event.after }}" >> "$GITHUB_OUTPUT"
fi
- name: Check for prohibited Co-authored-by AI trailers
if: steps.range.outputs.mode == 'pr'
shell: bash
run: python3 .hyperspy-github/scripts/ci-check-ai-trailers.py "${{ steps.range.outputs.base }}" "${{ steps.range.outputs.head }}"
- name: Check pushed commits (warning only)
if: steps.range.outputs.mode == 'push'
shell: bash
run: python3 .hyperspy-github/scripts/ci-check-ai-trailers.py "${{ steps.range.outputs.base }}" "${{ steps.range.outputs.head }}" || echo "::warning::Pushed commits contain prohibited AI Co-authored-by trailers (or the range was not fully reachable)."
- name: Nothing to check
if: steps.range.outputs.mode == 'skip'
run: echo "::notice::New branch push - no commit range to check."

changelog:
if: ${{ inputs.check_changelog }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/checkout@v4
with:
repository: ${{ inputs.tooling_repo }}
ref: ${{ inputs.tooling_ref }}
path: .hyperspy-github
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Check for changelog entry on source changes
shell: bash
env:
SOURCE_DIRS: ${{ inputs.source_dirs }}
CHANGELOG_DIR: ${{ inputs.changelog_dir }}
PYPROJECT: ${{ inputs.pyproject }}
run: |
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "::notice::Changelog check runs on pull requests only."; exit 0
fi
args=(--pyproject "$PYPROJECT")
for d in $SOURCE_DIRS; do args+=(--source-dir "$d"); done
[ -n "$CHANGELOG_DIR" ] && args+=(--changelog-dir "$CHANGELOG_DIR")
python3 .hyperspy-github/scripts/ci-check-changelog.py "${args[@]}" "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}"

agents-policy:
if: ${{ inputs.check_agents_policy }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: ${{ inputs.tooling_repo }}
ref: ${{ inputs.tooling_ref }}
path: .hyperspy-github
- name: Verify the pinned HyperSpy AI policy block
shell: bash
run: python3 .hyperspy-github/scripts/check-agents-policy.py --policy .hyperspy-github/AI-POLICY.md "${{ inputs.agents_file }}"
54 changes: 54 additions & 0 deletions .github/workflows/self-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Self-test
on: [push, pull_request]
permissions:
contents: read

jobs:
pytest:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.9', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install test dependencies
run: |
python3 -m pip install --upgrade pip
pip install -r tests/requirements.txt
pip install ruff
- name: ruff (3.12 only)
if: matrix.python-version == '3.12'
run: ruff check scripts tests
- name: pytest
run: python3 -m pytest -q tests

smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install pre-commit
run: |
python3 -m pip install --upgrade pip
pip install pre-commit
- name: check-agents-policy against AI-POLICY.md and AGENTS.md
run: python3 scripts/check-agents-policy.py --policy AI-POLICY.md AGENTS.md
- name: synthetic negative trailer proof
run: |
tmp=$(mktemp -d) && git -C "$tmp" init -q && git -C "$tmp" -c user.email=t@t -c user.name=t commit -q --allow-empty -m base && git -C "$tmp" -c user.email=t@t -c user.name=t commit -q --allow-empty -m bad -m 'Co-authored-by: Claude <noreply@anthropic.com>' && if (cd "$tmp" && python3 "$GITHUB_WORKSPACE/scripts/ci-check-ai-trailers.py" HEAD~1 HEAD); then echo 'CI scan did not reject AI co-author' >&2; exit 1; fi
- name: PR trailer scan
if: github.event_name == 'pull_request'
run: python3 scripts/ci-check-ai-trailers.py "${{ github.event.pull_request.base.sha }}" "${{ github.event.pull_request.head.sha }}"
- name: hook proof (bad - must reject)
run: |
printf 'x\n\nCo-authored-by: Claude <noreply@anthropic.com>\n' > /tmp/bad && if pre-commit try-repo . check-ai-co-author --hook-stage commit-msg --commit-msg-filename /tmp/bad; then echo 'hook did not reject AI co-author' >&2; exit 1; fi
- name: hook proof (ok - must accept)
run: |
printf 'x\n\nAssisted-by: Claude:claude-opus\n' > /tmp/ok && pre-commit try-repo . check-ai-co-author --hook-stage commit-msg --commit-msg-filename /tmp/ok
41 changes: 41 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
default_install_hook_types: [pre-commit, commit-msg]

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.10
hooks:
- id: ruff-check
args: [--fix]
files: ^(scripts|tests)/.*\.py$
- id: ruff-format
files: ^(scripts|tests)/.*\.py$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
exclude: ^README\.md$
- repo: local
hooks:
- id: interim-ai-co-author-guard
name: Block AI Co-authored-by (interim)
language: pygrep
entry: '^Co-authored-by:.*(claude|copilot|cursor|codex|gemini|aider|cline|windsurf|opencode|anthropic|openai|gpt|qwen|deepseek|mistral)'
args: [-i]
stages: [commit-msg]
- repo: local
hooks:
- id: check-ai-co-author
name: Block Co-authored-by AI trailers
language: script
entry: scripts/check-ai-co-author.py
stages: [commit-msg]
always_run: true
pass_filenames: true
- id: check-agents-policy
name: Pin the HyperSpy AI policy block in AGENTS.md
language: script
entry: scripts/check-agents-policy.py
files: ^AGENTS\.md$
pass_filenames: true
args: [--fix, --policy, AI-POLICY.md]
16 changes: 16 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- id: check-ai-co-author
name: Block Co-authored-by AI trailers
description: "Rejects Co-authored-by trailers naming AI tools; use Assisted-by: <tool>:<model>."
entry: scripts/check-ai-co-author.py
language: script
stages: [commit-msg]
always_run: true
pass_filenames: true
- id: check-agents-policy
name: Pin the HyperSpy AI policy block in AGENTS.md
description: Verifies (and with --fix inserts/refreshes) the pinned policy block from AI-POLICY.md in the root AGENTS.md.
entry: scripts/check-agents-policy.py
language: script
files: ^AGENTS\.md$
pass_filenames: true
args: [--fix]
56 changes: 56 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# hyperspy/.github

## Purpose

The organisation-level shared infrastructure for the HyperSpy GitHub
organisation: reusable workflows, composite actions, default community files
(`PULL_REQUEST_TEMPLATE.md`, `CONTRIBUTING.md`) and the shared AI policy and
compliance tooling (`AI-POLICY.md`, `scripts/`, the `compliance.yml` and
`self-test.yml` workflows). Adopting repositories point at this repository
to receive the policy, the commit-msg hook and the CI compliance check
without copying any scripts.

## Key Files

| File | What it is |
| --- | --- |
| `AI-POLICY.md` | Normative HyperSpy AI policy. Contains the pinned block embedded verbatim in every consumer's `AGENTS.md`. |
| `.pre-commit-hooks.yaml` | Public hook manifest for `check-ai-co-author` (commit-msg stage) and `check-agents-policy` (pins the AI policy block in `AGENTS.md`). |
| `.github/workflows/compliance.yml` | Reusable workflow. Three jobs: `ai-trailers`, `changelog`, `agents-policy`. |
| `.github/workflows/self-test.yml` | Internal CI: pytest + ruff, plus synthetic hook and script smoke tests against a scratch repo. |
| `scripts/*.py` | Stdlib-only enforcement scripts (commit-msg hook, CI trailer scan, changelog-fragment check, agents-policy pin checker, shared AI-pattern module). |
| `tests/` | pytest coverage for every script. |

## Working in this repository

- Run `python3 -m pytest -q tests` to execute the test suite.
- Run `pre-commit run --all-files` before opening a pull request.
- The scripts are stdlib-only and must stay so; never add third-party
dependencies.
- Change the pinned block ONLY in `AI-POLICY.md`; every consumer's
`AGENTS.md` is updated from there by `check-agents-policy --fix`.

<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
<!-- hyperspy-ai-policy:begin -->
## HyperSpy AI policy (ecosystem contract)

Canonical source: https://github.com/hyperspy/.github/blob/main/AI-POLICY.md - do not reword
this block; the `check-agents-policy` pre-commit hook keeps it synchronized.

- Non-trivial AI-assisted changes need an accepted proposal in
https://github.com/hyperspy/hyperspy-proposals BEFORE the implementation PR is reviewed.
Trivial changes: PR review suffices.
- Every AI-assisted commit carries the trailer `Assisted-by: <tool>:<model>`
(example: `Assisted-by: OpenCode:deepseek-4.0-pro`). Attribute the tool you interact with,
not a wrapper.
- NEVER add `Co-authored-by:` trailers for AI tools; they are reserved for human co-authors.
The `check-ai-co-author` commit-msg hook and the `compliance / ai-trailers` CI check reject
them.
- Disable your tool's automatic co-author injection before committing: Claude Code
`"includeCoAuthoredBy": false`; GitHub Copilot `github.copilot.chat.commitMessageGeneration`
off; Cursor commit attribution off; OpenCode / oh-my-openagent: check skill and plugin configs.
- Run `pre-commit install` (installs the commit-msg stage) before your first commit; CI
re-checks every PR commit regardless.
- Declare AI assistance in the pull-request template and link the accepted proposal when one
is required.
<!-- hyperspy-ai-policy:end -->
65 changes: 65 additions & 0 deletions AI-POLICY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# HyperSpy AI Policy

This file is the normative contract for AI-assisted contributions to repositories of the
[HyperSpy GitHub organization](https://github.com/hyperspy). The explanatory guide is the
HyperSpy developer guide section *Coding with AI assistants*; the proposal process lives in
[hyperspy/hyperspy-proposals](https://github.com/hyperspy/hyperspy-proposals). When texts
disagree, this file wins.

## Rules

- **R1 Proposal gate.** Non-trivial AI-assisted changes require an accepted proposal in
hyperspy/hyperspy-proposals before the implementation pull request is reviewed. Trivial
AI-assisted changes and human-only changes need no proposal (recommended for large
human-only changes).
- **R2 Attribution.** Every commit produced with AI assistance carries the git trailer
`Assisted-by: <tool>:<model>` (the Linux kernel convention), naming the tool you interact
with directly, not a wrapper around it.
- **R3 `Co-authored-by:` is for humans.** Commits must not carry `Co-authored-by:` trailers
naming AI tools. The shared commit-msg hook and CI check reject them.
- **R4 Disable auto-injection.** Configure your tool so it does not add AI co-author trailers
(Claude Code: `"includeCoAuthoredBy": false`; GitHub Copilot: disable
`github.copilot.chat.commitMessageGeneration`; Cursor: disable commit attribution;
OpenCode / oh-my-openagent: check skill and plugin configurations).
- **R5 Disclosure.** Declare AI assistance in the pull-request template and link the accepted
proposal when R1 applies.
- **R6 Responsibility.** The contributor reviews, understands and takes full responsibility for
every submitted line; AI-assisted contributions meet the same quality bar as any other.

## Enforcement (shared tooling in this repository)

- pre-commit hooks (`.pre-commit-hooks.yaml`): `check-ai-co-author` (commit-msg stage) and
`check-agents-policy` (keeps the pinned block below in sync).
- Reusable workflow `.github/workflows/compliance.yml` with jobs `ai-trailers`, `changelog`
and `agents-policy`.
- Adoption instructions: see [README.md](README.md).

## Pinned block for AGENTS.md

Repositories embed the block below verbatim in the MANUAL section of their root `AGENTS.md`;
the `check-agents-policy` hook inserts and updates it. Never edit it in a repository - change
it here.

<!-- hyperspy-ai-policy:begin -->
## HyperSpy AI policy (ecosystem contract)

Canonical source: https://github.com/hyperspy/.github/blob/main/AI-POLICY.md - do not reword
this block; the `check-agents-policy` pre-commit hook keeps it synchronized.

- Non-trivial AI-assisted changes need an accepted proposal in
https://github.com/hyperspy/hyperspy-proposals BEFORE the implementation PR is reviewed.
Trivial changes: PR review suffices.
- Every AI-assisted commit carries the trailer `Assisted-by: <tool>:<model>`
(example: `Assisted-by: OpenCode:deepseek-4.0-pro`). Attribute the tool you interact with,
not a wrapper.
- NEVER add `Co-authored-by:` trailers for AI tools; they are reserved for human co-authors.
The `check-ai-co-author` commit-msg hook and the `compliance / ai-trailers` CI check reject
them.
- Disable your tool's automatic co-author injection before committing: Claude Code
`"includeCoAuthoredBy": false`; GitHub Copilot `github.copilot.chat.commitMessageGeneration`
off; Cursor commit attribution off; OpenCode / oh-my-openagent: check skill and plugin configs.
- Run `pre-commit install` (installs the commit-msg stage) before your first commit; CI
re-checks every PR commit regardless.
- Declare AI assistance in the pull-request template and link the accepted proposal when one
is required.
<!-- hyperspy-ai-policy:end -->
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Contributing to HyperSpy projects

If you want to contribute to a repository of the HyperSpy organisation, please read the [HyperSpy developer guide](https://hyperspy.readthedocs.io/en/latest/dev_guide/intro.html), and the repository's own CONTRIBUTING file when it has one.

## AI-Assisted Contributions

Non-trivial AI-assisted changes require an accepted proposal in [hyperspy/hyperspy-proposals](https://github.com/hyperspy/hyperspy-proposals) before the implementation PR will be reviewed. Trivial AI-assisted changes do not require a proposal.

For more details, see the [proposals section](https://hyperspy.readthedocs.io/en/latest/dev_guide/proposals.html) of the developer guide.

All AI-assisted commits must include the `Assisted-by: <tool>:<model>` trailer.

The normative rules and the shared enforcement tooling (pre-commit hooks and the reusable compliance workflow) live in [AI-POLICY.md](AI-POLICY.md).
14 changes: 14 additions & 0 deletions PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
### Description of the change
<!-- A few sentences and/or a bulleted list describing and motivating the change. -->

### AI-assisted contribution?
- [ ] Human-only contribution or trivial change (no proposal needed)
- [ ] AI-assisted, non-trivial — accepted proposal: <link to the PR in hyperspy/hyperspy-proposals>
- [ ] If AI-assisted, `Assisted-by: <tool>:<model>` in every commit and no AI `Co-authored-by:` trailer

### Progress of the PR
- [ ] Change implemented (can be split into several points),
- [ ] tests added or updated (if appropriate),
- [ ] documentation updated (if appropriate),
- [ ] changelog entry added (if the repository uses `upcoming_changes/`),
- [ ] ready for review.
Loading
Loading