From 66648bcbe26f02d06503b01900bf3ec133a78ecf Mon Sep 17 00:00:00 2001 From: Siddhartha Srinivasa Date: Sun, 5 Apr 2026 13:43:13 -0700 Subject: [PATCH 1/2] Phase 0: canonical templates, integration workflow, and scrub guards Groundwork for making the 10 sibling repos public (see #3): - .github-templates/: canonical LICENSE, CI, CODEOWNERS, CONTRIBUTING, SECURITY, CODE_OF_CONDUCT, issue/PR templates, branch-protection.json. - .github/workflows/integration.yml: meta workflow triggered on PR, nightly cron, workflow_dispatch, and repository_dispatch (sibling-updated) that runs all per-sibling test suites + cross-repo integration tests. - tests/integration/: workspace-level tests. test_no_stale_attribution.py guards against UW/PRL strings returning to any sibling. - scripts/sync_templates.sh: copies .github-templates/ into each sibling. - scripts/add_headers.py: prepends SPDX+copyright to .py files idempotently. - .gitignore: un-ignore scripts/*.py and tests/**/*.py. --- .github-templates/CODEOWNERS | 1 + .github-templates/CODE_OF_CONDUCT.md | 13 +++ .github-templates/CONTRIBUTING.md | 50 ++++++++ .../ISSUE_TEMPLATE/bug_report.md | 22 ++++ .github-templates/ISSUE_TEMPLATE/config.yml | 5 + .../ISSUE_TEMPLATE/improvement.md | 13 +++ .github-templates/ISSUE_TEMPLATE/task.md | 12 ++ .github-templates/LICENSE | 21 ++++ .github-templates/SECURITY.md | 7 ++ .github-templates/branch-protection.json | 20 ++++ .github-templates/pull_request_template.md | 24 ++++ .github-templates/workflows/ci.yml | 53 +++++++++ .github/workflows/integration.yml | 107 +++++++++++++++++ .gitignore | 2 + scripts/add_headers.py | 101 ++++++++++++++++ scripts/sync_templates.sh | 108 ++++++++++++++++++ tests/integration/README.md | 18 +++ tests/integration/__init__.py | 0 tests/integration/test_imports.py | 25 ++++ .../integration/test_no_stale_attribution.py | 55 +++++++++ 20 files changed, 657 insertions(+) create mode 100644 .github-templates/CODEOWNERS create mode 100644 .github-templates/CODE_OF_CONDUCT.md create mode 100644 .github-templates/CONTRIBUTING.md create mode 100644 .github-templates/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github-templates/ISSUE_TEMPLATE/config.yml create mode 100644 .github-templates/ISSUE_TEMPLATE/improvement.md create mode 100644 .github-templates/ISSUE_TEMPLATE/task.md create mode 100644 .github-templates/LICENSE create mode 100644 .github-templates/SECURITY.md create mode 100644 .github-templates/branch-protection.json create mode 100644 .github-templates/pull_request_template.md create mode 100644 .github-templates/workflows/ci.yml create mode 100644 .github/workflows/integration.yml create mode 100755 scripts/add_headers.py create mode 100755 scripts/sync_templates.sh create mode 100644 tests/integration/README.md create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/test_imports.py create mode 100644 tests/integration/test_no_stale_attribution.py diff --git a/.github-templates/CODEOWNERS b/.github-templates/CODEOWNERS new file mode 100644 index 0000000..46cae53 --- /dev/null +++ b/.github-templates/CODEOWNERS @@ -0,0 +1 @@ +* @siddhss5 diff --git a/.github-templates/CODE_OF_CONDUCT.md b/.github-templates/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..00ac68a --- /dev/null +++ b/.github-templates/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Code of Conduct + +This project adopts the [Contributor Covenant, version 2.1][cc]. + +The full text is available at . + +## Reporting + +Instances of unacceptable behavior may be reported privately to the project +maintainer at **siddhartha.srinivasa@gmail.com**. All reports will be +reviewed and investigated promptly and fairly. + +[cc]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/ diff --git a/.github-templates/CONTRIBUTING.md b/.github-templates/CONTRIBUTING.md new file mode 100644 index 0000000..a93a66d --- /dev/null +++ b/.github-templates/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# Contributing + +This repository is part of the [robot-code workspace](https://github.com/personalrobotics/robot-code). +All projects in the workspace share a common layout (`src/` + `tests/`), a single +package manager (`uv`), and a common CI setup. + +## Development setup + +```bash +git clone https://github.com/personalrobotics/robot-code +cd robot-code +./setup.sh +``` + +This clones every sibling repo (including this one) into a single uv workspace +and runs `uv sync`. You can then work in any sibling's directory. + +## Running tests and linters + +From inside this repo: + +```bash +uv run pytest tests/ -v +uv run ruff check . +uv run ruff format --check . +``` + +From the workspace root, you can also run the cross-repo integration suite: + +```bash +uv run pytest tests/integration -v +``` + +## Pull requests + +- Branch from `main`. Open a PR with a clear summary and test plan. +- Per-repo CI (ruff + pytest) must pass. +- Cross-repo integration CI in robot-code runs automatically when this repo's + `main` advances, and on scheduled nightly runs. +- Review is by [@siddhss5](https://github.com/siddhss5) (enforced via CODEOWNERS). + +## Package manager: uv only + +We use [uv](https://docs.astral.sh/uv/) exclusively. **Do not use pip.** +The workspace layout in robot-code relies on uv's workspace resolution. + +## License + +By contributing, you agree that your contributions will be licensed under the +MIT License (see [LICENSE](LICENSE)). diff --git a/.github-templates/ISSUE_TEMPLATE/bug_report.md b/.github-templates/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..182f8e2 --- /dev/null +++ b/.github-templates/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Report a reproducible bug +labels: bug +--- + +## Environment +- OS: +- Python version: +- uv version: +- Package version or commit: + +## Reproduction + + +## Expected behavior + +## Actual behavior + +## Logs / traceback +``` +``` diff --git a/.github-templates/ISSUE_TEMPLATE/config.yml b/.github-templates/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cefaf35 --- /dev/null +++ b/.github-templates/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and discussion + url: https://github.com/personalrobotics/robot-code/discussions + about: Ask questions or discuss design in the umbrella repo. diff --git a/.github-templates/ISSUE_TEMPLATE/improvement.md b/.github-templates/ISSUE_TEMPLATE/improvement.md new file mode 100644 index 0000000..bdc37c9 --- /dev/null +++ b/.github-templates/ISSUE_TEMPLATE/improvement.md @@ -0,0 +1,13 @@ +--- +name: Improvement +about: Propose an enhancement to existing functionality +labels: enhancement +--- + +## Motivation + + +## Proposal + + +## Alternatives considered diff --git a/.github-templates/ISSUE_TEMPLATE/task.md b/.github-templates/ISSUE_TEMPLATE/task.md new file mode 100644 index 0000000..7f1e3fd --- /dev/null +++ b/.github-templates/ISSUE_TEMPLATE/task.md @@ -0,0 +1,12 @@ +--- +name: Task +about: A planned unit of work +labels: task +--- + +## Goal + +## Acceptance criteria +- [ ] + +## Notes diff --git a/.github-templates/LICENSE b/.github-templates/LICENSE new file mode 100644 index 0000000..5834249 --- /dev/null +++ b/.github-templates/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Siddhartha Srinivasa + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/.github-templates/SECURITY.md b/.github-templates/SECURITY.md new file mode 100644 index 0000000..931e848 --- /dev/null +++ b/.github-templates/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +If you discover a security vulnerability in this project, please report it +privately to **siddhartha.srinivasa@gmail.com**. + +Please do not open a public issue for security reports. You can expect an +initial response within a few business days. diff --git a/.github-templates/branch-protection.json b/.github-templates/branch-protection.json new file mode 100644 index 0000000..e758d84 --- /dev/null +++ b/.github-templates/branch-protection.json @@ -0,0 +1,20 @@ +{ + "required_status_checks": { + "strict": true, + "contexts": ["test (3.10)", "test (3.11)", "test (3.12)"] + }, + "enforce_admins": false, + "required_pull_request_reviews": { + "dismiss_stale_reviews": true, + "require_code_owner_reviews": true, + "required_approving_review_count": 1, + "require_last_push_approval": false + }, + "restrictions": null, + "required_linear_history": true, + "allow_force_pushes": false, + "allow_deletions": false, + "required_conversation_resolution": true, + "lock_branch": false, + "allow_fork_syncing": true +} diff --git a/.github-templates/pull_request_template.md b/.github-templates/pull_request_template.md new file mode 100644 index 0000000..7b85935 --- /dev/null +++ b/.github-templates/pull_request_template.md @@ -0,0 +1,24 @@ +## Summary + + + +## Changes + + +- + +## Testing + +- [ ] `uv run pytest tests/ -v` passes +- [ ] `uv run ruff check .` passes +- [ ] `uv run ruff format --check .` passes +- [ ] Integration tested locally against the robot-code workspace (if cross-repo) + +## Breaking changes + +- [ ] None +- [ ] Yes (describe migration below): + +## Related issues + + diff --git a/.github-templates/workflows/ci.yml b/.github-templates/workflows/ci.yml new file mode 100644 index 0000000..32f3279 --- /dev/null +++ b/.github-templates/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + steps: + - uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Ruff lint + run: uv run ruff check . + + - name: Ruff format check + run: uv run ruff format --check . + + - name: Pytest + run: uv run pytest tests/ -v + + notify-robot-code: + needs: test + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - name: Dispatch integration run + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.ROBOT_CODE_DISPATCH_TOKEN }} + repository: personalrobotics/robot-code + event-type: sibling-updated + client-payload: '{"repo":"${{ github.event.repository.name }}","sha":"${{ github.sha }}"}' diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..8a3c610 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,107 @@ +name: Workspace Integration + +on: + pull_request: + branches: [main] + schedule: + # 08:00 UTC nightly + - cron: "0 8 * * *" + workflow_dispatch: + inputs: + override_repo: + description: "Sibling repo name to pin to a specific sha (optional)" + required: false + override_sha: + description: "Commit sha for the override_repo (optional)" + required: false + repository_dispatch: + types: [sibling-updated] + +concurrency: + group: integration-${{ github.ref }} + cancel-in-progress: false + +jobs: + integration: + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.12"] + steps: + - name: Checkout robot-code + uses: actions/checkout@v4 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Bootstrap siblings + env: + # While any sibling repo is still private, setup.sh needs auth to clone. + # Once all repos are public, GH_TOKEN can be removed. + GH_TOKEN: ${{ secrets.SIBLING_CHECKOUT_TOKEN || secrets.GITHUB_TOKEN }} + run: | + # Configure git to use the token for personalrobotics clones. + git config --global url."https://x-access-token:${GH_TOKEN}@github.com/personalrobotics/".insteadOf "https://github.com/personalrobotics/" + ./setup.sh + + - name: Override sibling at specific sha (dispatch) + if: github.event_name == 'repository_dispatch' + working-directory: ${{ github.event.client_payload.repo }} + run: | + git fetch origin "${{ github.event.client_payload.sha }}" + git checkout "${{ github.event.client_payload.sha }}" + + - name: Override sibling at specific sha (manual) + if: github.event_name == 'workflow_dispatch' && inputs.override_repo != '' + working-directory: ${{ inputs.override_repo }} + run: | + git fetch origin "${{ inputs.override_sha }}" + git checkout "${{ inputs.override_sha }}" + + - name: Re-sync after override + if: github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_dispatch' && inputs.override_repo != '') + run: uv sync + + - name: Per-sibling unit tests + run: | + set +e + fail=0 + for d in asset_manager geodude geodude_assets mj_environment mj_manipulator mj_viser prl_assets pycbirrt tsr; do + if [ -d "$d/tests" ]; then + echo "::group::$d tests" + (cd "$d" && uv run pytest tests/ -q) || fail=1 + echo "::endgroup::" + fi + done + exit $fail + + - name: Cross-repo integration tests + run: uv run pytest tests/integration -v + + - name: Import smoke check + run: | + uv run python -c "import asset_manager, geodude, geodude_assets, mj_environment, mj_manipulator, mj_viser, prl_assets, pycbirrt, tsr; print('all imports OK')" + + notify-on-nightly-failure: + needs: integration + if: failure() && github.event_name == 'schedule' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - uses: actions/github-script@v7 + with: + script: | + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Nightly integration failed (run ${context.runId})`, + body: `Nightly workspace-integration run failed.\n\nSee: ${runUrl}`, + labels: ["ci", "nightly-fail"], + }); diff --git a/.gitignore b/.gitignore index a302cf3..240321c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ pr_assets/ *.txt *.md.bak # Explicitly tracked: CLAUDE.md, README.md, GEODUDE_DESIGN.md +!scripts/*.py +!tests/**/*.py *.zip *.jpg *.png diff --git a/scripts/add_headers.py b/scripts/add_headers.py new file mode 100755 index 0000000..fbf38f1 --- /dev/null +++ b/scripts/add_headers.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Prepend SPDX + copyright header to every .py file in one or more directories. + +Usage: + scripts/add_headers.py --dry-run [ ...] + scripts/add_headers.py --apply [ ...] + +The header is two lines: + # SPDX-License-Identifier: MIT + # Copyright (c) 2025 Siddhartha Srinivasa + +Files already containing "SPDX-License-Identifier" are skipped. Shebangs and +coding declarations on lines 1-2 are preserved. +""" + +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 Siddhartha Srinivasa + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +HEADER_LINES = [ + "# SPDX-License-Identifier: MIT", + "# Copyright (c) 2025 Siddhartha Srinivasa", +] +SKIP_DIR_NAMES = {".git", ".venv", "__pycache__", "node_modules", "build", "dist", ".eggs"} + + +def should_skip(path: Path) -> bool: + return any(part in SKIP_DIR_NAMES for part in path.parts) + + +def add_header(path: Path, *, apply: bool) -> str: + """Return one of: 'added', 'present', 'empty'.""" + text = path.read_text(encoding="utf-8") + if "SPDX-License-Identifier" in text: + return "present" + lines = text.splitlines(keepends=True) + if not lines: + return "empty" + + # Preserve shebang and optional coding declaration. + prefix: list[str] = [] + i = 0 + if lines and lines[0].startswith("#!"): + prefix.append(lines[0]) + i = 1 + if i < len(lines) and ("coding:" in lines[i] or "coding=" in lines[i]) and lines[i].lstrip().startswith("#"): + prefix.append(lines[i]) + i += 1 + + new_header = "\n".join(HEADER_LINES) + "\n" + rest = "".join(lines[i:]) + # Ensure a blank line between header and rest if rest is non-empty and does not already start with one. + if rest and not rest.startswith("\n"): + new_header += "\n" + new_text = "".join(prefix) + new_header + rest + + if apply: + path.write_text(new_text, encoding="utf-8") + return "added" + + +def main() -> int: + parser = argparse.ArgumentParser() + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--dry-run", action="store_true") + mode.add_argument("--apply", action="store_true") + parser.add_argument("roots", nargs="+", type=Path) + args = parser.parse_args() + + added = 0 + present = 0 + empty = 0 + for root in args.roots: + if not root.exists(): + print(f"[warn] missing: {root}", file=sys.stderr) + continue + for py in root.rglob("*.py"): + if should_skip(py): + continue + result = add_header(py, apply=args.apply) + if result == "added": + added += 1 + prefix = "[add] " if args.apply else "[would-add] " + print(f"{prefix}{py}") + elif result == "present": + present += 1 + else: + empty += 1 + + verb = "added" if args.apply else "would add" + print(f"\nSummary: {verb}={added} already-present={present} empty={empty}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/sync_templates.sh b/scripts/sync_templates.sh new file mode 100755 index 0000000..b3e1598 --- /dev/null +++ b/scripts/sync_templates.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# Sync canonical templates from .github-templates/ into each sibling repo. +# +# Usage: +# scripts/sync_templates.sh --dry-run # show what would change (all repos) +# scripts/sync_templates.sh --apply # apply to all repos +# scripts/sync_templates.sh --apply --repo=tsr # apply to one repo +# +# The script copies files from .github-templates/ to each sibling. It does NOT +# touch each sibling's pyproject.toml, README, LICENSE text, or source headers +# — those are per-repo and handled separately. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +TEMPLATES="$ROOT/.github-templates" + +SIBLINGS=( + asset_manager + geodude + geodude_assets + mj_environment + mj_manipulator + mj_manipulator_ros + mj_viser + prl_assets + pycbirrt + tsr +) + +# Files to copy from .github-templates/ into each sibling (src -> dst). +# Left column is the path under .github-templates/; right column is the +# destination path under the sibling repo. +FILES=( + "CODEOWNERS:.github/CODEOWNERS" + "pull_request_template.md:.github/pull_request_template.md" + "ISSUE_TEMPLATE/bug_report.md:.github/ISSUE_TEMPLATE/bug_report.md" + "ISSUE_TEMPLATE/improvement.md:.github/ISSUE_TEMPLATE/improvement.md" + "ISSUE_TEMPLATE/task.md:.github/ISSUE_TEMPLATE/task.md" + "ISSUE_TEMPLATE/config.yml:.github/ISSUE_TEMPLATE/config.yml" + "workflows/ci.yml:.github/workflows/ci.yml" + "CONTRIBUTING.md:CONTRIBUTING.md" + "SECURITY.md:SECURITY.md" + "CODE_OF_CONDUCT.md:CODE_OF_CONDUCT.md" + "LICENSE:LICENSE" +) + +MODE="dry-run" +ONLY_REPO="" +for arg in "$@"; do + case "$arg" in + --dry-run) MODE="dry-run" ;; + --apply) MODE="apply" ;; + --repo=*) ONLY_REPO="${arg#--repo=}" ;; + -h|--help) + sed -n '2,10p' "$0" | sed 's/^# \{0,1\}//' + exit 0 + ;; + *) echo "unknown arg: $arg" >&2; exit 2 ;; + esac +done + +sync_one() { + local sibling="$1" + local sib_dir="$ROOT/$sibling" + if [ ! -d "$sib_dir" ]; then + echo " [skip] $sibling (not cloned)" + return + fi + echo "== $sibling ==" + for pair in "${FILES[@]}"; do + local src_rel="${pair%%:*}" + local dst_rel="${pair##*:}" + local src="$TEMPLATES/$src_rel" + local dst="$sib_dir/$dst_rel" + if [ ! -f "$src" ]; then + echo " [warn] missing template: $src_rel" + continue + fi + if [ -f "$dst" ] && cmp -s "$src" "$dst"; then + # Already in sync. + continue + fi + if [ "$MODE" = "apply" ]; then + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + echo " [sync] $dst_rel" + else + if [ -f "$dst" ]; then + echo " [diff] $dst_rel" + else + echo " [new] $dst_rel" + fi + fi + done +} + +if [ -n "$ONLY_REPO" ]; then + sync_one "$ONLY_REPO" +else + for s in "${SIBLINGS[@]}"; do + sync_one "$s" + done +fi + +if [ "$MODE" = "dry-run" ]; then + echo "" + echo "Dry run only. Re-run with --apply to make changes." +fi diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..35eb9a9 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,18 @@ +# Workspace integration tests + +Cross-repo tests that exercise multiple sibling packages together. Unlike each +sibling's own `tests/`, these tests assume the full uv workspace is checked out +(i.e. `./setup.sh` has been run) and verify that the packages compose correctly. + +Run them from the workspace root: + +```bash +uv run pytest tests/integration -v +``` + +## Guidelines + +- Keep tests **headless** (no display, no interactive viewer). +- Keep runtime under ~10s per test when possible. +- Prefer smoke checks (construct, step once, destroy) over full demos. +- If a test needs a real robot or GPU, skip it via `pytest.mark.skipif` so CI stays green. diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_imports.py b/tests/integration/test_imports.py new file mode 100644 index 0000000..83077f5 --- /dev/null +++ b/tests/integration/test_imports.py @@ -0,0 +1,25 @@ +"""Verify every workspace sibling package is importable together.""" + +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 Siddhartha Srinivasa + +import importlib + +import pytest + +SIBLINGS = [ + "asset_manager", + "geodude", + "geodude_assets", + "mj_environment", + "mj_manipulator", + "mj_viser", + "prl_assets", + "pycbirrt", + "tsr", +] + + +@pytest.mark.parametrize("name", SIBLINGS) +def test_sibling_importable(name: str) -> None: + importlib.import_module(name) diff --git a/tests/integration/test_no_stale_attribution.py b/tests/integration/test_no_stale_attribution.py new file mode 100644 index 0000000..34580e7 --- /dev/null +++ b/tests/integration/test_no_stale_attribution.py @@ -0,0 +1,55 @@ +"""Guard against regressions: no UW/PRL attribution strings in any sibling repo. + +This test fails if any sibling repo reintroduces 'University of Washington', +'Personal Robotics Lab(oratory)', or the old UW email address in text files. +""" + +# SPDX-License-Identifier: MIT +# Copyright (c) 2025 Siddhartha Srinivasa + +from pathlib import Path + +BANNED_PATTERNS = [ + "University of Washington", + "Personal Robotics Laboratory", + "Personal Robotics Lab,", + "siddh@cs.washington.edu", +] + +SIBLING_DIRS = [ + "asset_manager", + "geodude", + "geodude_assets", + "mj_environment", + "mj_manipulator", + "mj_manipulator_ros", + "mj_viser", + "prl_assets", + "pycbirrt", + "tsr", +] + +TEXT_SUFFIXES = {".py", ".md", ".toml", ".yaml", ".yml", ".txt", ".cfg", ".ini", ".xml"} +SKIP_DIR_NAMES = {".git", ".venv", "__pycache__", "node_modules", "mujoco_menagerie", "build", "dist"} + + +def test_no_banned_attribution() -> None: + root = Path(__file__).resolve().parents[2] + hits: list[str] = [] + for sibling in SIBLING_DIRS: + sib_dir = root / sibling + if not sib_dir.is_dir(): + continue + for path in sib_dir.rglob("*"): + if not path.is_file() or path.suffix not in TEXT_SUFFIXES: + continue + if any(part in SKIP_DIR_NAMES for part in path.parts): + continue + try: + text = path.read_text(encoding="utf-8", errors="ignore") + except OSError: + continue + for pat in BANNED_PATTERNS: + if pat in text: + hits.append(f"{path.relative_to(root)}: {pat!r}") + assert not hits, "Banned attribution strings found:\n " + "\n ".join(hits) From 24ac23ae9619021975eb60e78fabe5ec2641a470 Mon Sep 17 00:00:00 2001 From: Siddhartha Srinivasa Date: Sun, 5 Apr 2026 16:15:27 -0700 Subject: [PATCH 2/2] CI template: skip pytest when tests/ directory is absent --- .github-templates/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github-templates/workflows/ci.yml b/.github-templates/workflows/ci.yml index 32f3279..073ecc1 100644 --- a/.github-templates/workflows/ci.yml +++ b/.github-templates/workflows/ci.yml @@ -37,7 +37,12 @@ jobs: run: uv run ruff format --check . - name: Pytest - run: uv run pytest tests/ -v + run: | + if [ -d tests ]; then + uv run pytest tests/ -v + else + echo "No tests/ directory; skipping pytest." + fi notify-robot-code: needs: test