|
| 1 | +<!-- Generated from AI.md by 'just ai'. Edit AI.md, not this file. --> |
| 2 | + |
| 3 | +# Instructions for AI coding assistants |
| 4 | + |
| 5 | +This file is for Claude Code, GitHub Copilot, OpenAI Codex and any other assistant working |
| 6 | +in this repository. It is the one to edit: `AGENTS.md` is generated from it (`just ai`), |
| 7 | +`CLAUDE.md` imports it, and `.github/copilot-instructions.md` points here. |
| 8 | + |
| 9 | +## What this is |
| 10 | + |
| 11 | +`ldo` (distribution `libre-devops-helpers`, import `libre_devops_helpers`): a fast, read-only |
| 12 | +CLI and library for Microsoft (Entra ID, Defender XDR, Intune, Azure, Graph, PIM, Logic Apps, |
| 13 | +Automation) and ServiceNow. Its users are people signing in as themselves, usually through |
| 14 | +the Azure CLI; automation is second. Everything reads, apart from `ldo az use`, which switches |
| 15 | +the Azure CLI's account. Never add a command that changes a tenant or an instance without |
| 16 | +being asked. |
| 17 | + |
| 18 | +## Working here |
| 19 | + |
| 20 | +- Run everything through `just` (or `uv run`): `just check` before calling anything done, |
| 21 | + `just ci` for all of CI's checks, `just fmt` to format, `just run <args>` to try the CLI. |
| 22 | +- Python 3.11 or later: nothing newer than 3.11 in the code (`just test-311` proves it). |
| 23 | +- ruff, line length 100. Match the surrounding code's naming, comment density and idiom. |
| 24 | +- Runtime dependencies are `requests` and `typer` only. Everything else is the standard |
| 25 | + library. Do not add a runtime dependency without asking; test-only ones go in the `dev` |
| 26 | + group. |
| 27 | + |
| 28 | +## Layout and layering |
| 29 | + |
| 30 | +```text |
| 31 | +src/libre_devops_helpers/ |
| 32 | + core/ vendor-neutral: errors, config, brand, HTTP client, token store, |
| 33 | + polling, inputs (CSV and Excel), logging, YAML writer |
| 34 | + microsoft/ the shared Microsoft layer: clouds, credentials, tokens, profiles |
| 35 | + microsoft/<feature>/ one package per API: entra, xdr, graph, azure, pim, automation ... |
| 36 | + microsoft/devices/ the one composite, using entra, xdr and intune |
| 37 | + servicenow/ the ServiceNow vendor, same shape |
| 38 | + cli/ the ldo command: parses, calls a client, renders. No logic here. |
| 39 | +``` |
| 40 | + |
| 41 | +`tests/project/test_package_surface.py` enforces the layering: `core` imports nothing of |
| 42 | +ours, a feature imports only `core` and its vendor layer, `cli` sits on top. Cross-cutting |
| 43 | +code (auth, HTTP, input parsing) goes in `core` or the vendor layer, never in a feature. A new |
| 44 | +feature package needs its entry in that test, a `REQUIREMENTS` tuple, and a test folder. |
| 45 | + |
| 46 | +## Tests |
| 47 | + |
| 48 | +- Tests mirror `src` (`tests/core`, `tests/microsoft/<feature>`, `tests/cli/commands`), one |
| 49 | + test module per module, and `tests/project/test_layout.py` checks it. |
| 50 | +- Nothing touches the network, a real `az` or a real clock. HTTP goes through |
| 51 | + `tests/fakes/http.py` (`routes`, `fake_session`), `az` through `fakes/azcli.py`, time |
| 52 | + through `fakes/clock.py`. Shared fakes live in `tests/fakes`, one module per concern. |
| 53 | +- Coverage is gated at 93% (`fail_under` in `pyproject.toml`). Keep new code covered. |
| 54 | +- CI runs with `GITHUB_ACTIONS` set, which makes Typer colour and wrap errors: compare error |
| 55 | + text with `usage_error(result)`, never a raw substring of `result.output`. |
| 56 | +- Windows caps an environment variable at 32,767 characters and pytest puts each test id in |
| 57 | + one: give big parametrised values short `ids`. |
| 58 | +- `tests/project/test_rebrand.py` renames a copy of the repository and runs its whole suite. |
| 59 | + |
| 60 | +## Names and branding |
| 61 | + |
| 62 | +The project can be renamed (`just rebrand`). Never hard-code `ldo`, `LDO_` or the package |
| 63 | +name in code: use `core/brand.py` (`brand.COMMAND`, `brand.env_var("X")`, |
| 64 | +`brand.command("config init")`, `brand.docs("page")` for a link to a docs page). |
| 65 | + |
| 66 | +## Writing code |
| 67 | + |
| 68 | +- Library code raises `LdoError` subclasses (`InputError`, `NotFoundError`, |
| 69 | + `AmbiguousError`, `ApiError`, ...) with a `hint` saying what to do, and never exits. Only |
| 70 | + the CLI turns errors into messages and exit codes (0 fine, 1 error, 2 usage, 3 needs |
| 71 | + attention, 130 interrupted). |
| 72 | +- Data goes to stdout; notes, warnings and progress to stderr, through `cli/render.py` |
| 73 | + (`note`, `warn`, `error`, `emit`). In the `json` and `otlp` log formats those become log |
| 74 | + records, so never write to stderr directly. |
| 75 | +- Every data command takes `-o table|json|csv` and `-p PROFILE`; lists of names take |
| 76 | + arguments, `-` for stdin and `-f FILE` with `--column` and `--sheet`. |
| 77 | +- All HTTP goes through `core.http.ApiClient`, which applies `core/network.py` (the proxy |
| 78 | + and `no_proxy`) and `core/trust.py` (the public roots, the OS store and `ca_bundle`), and |
| 79 | + `az` runs with the same through `network.subprocess_env()`. Never call `requests` directly. |
| 80 | +- Anything a person types that goes into a URL path or a query is validated first (names, |
| 81 | + ids, KQL values). A token or secret is never printed (only with an explicit `--raw`), never |
| 82 | + logged, and never accepted on the command line. |
| 83 | +- Help text is rendered as markdown: no `<`, no `*`, and paragraphs reflow. A test checks. |
| 84 | +- Do not hide findings from a scanner or linter by renaming things; fix the code, or say it |
| 85 | + is a false positive and why. |
| 86 | + |
| 87 | +## Docs |
| 88 | + |
| 89 | +- `README.md` is a short front page: the command table, install, quickstart and links. |
| 90 | + Detail goes in `docs/`, one page per area. `CHANGELOG.md` gets an `Unreleased` entry for |
| 91 | + every change a user would notice. |
| 92 | +- `tests/project/test_docs.py` runs every `ldo` example in the README, `docs/` and this file |
| 93 | + with `--help`, checks every `just` example is a recipe, and follows every link. Keep |
| 94 | + examples real. |
| 95 | +- UK English (colour, organisation, licence as a noun). Never use em or en dashes, in any |
| 96 | + file: use commas, colons, brackets or a plain hyphen. |
| 97 | + |
| 98 | +## Commits and releases |
| 99 | + |
| 100 | +- Do not commit, push, tag or release until the person asks. Use their own git identity. |
| 101 | +- No AI attribution in commits or pull requests: no `Co-Authored-By` or "Generated with" |
| 102 | + lines. |
| 103 | +- A release is `just release` after the version is set in `pyproject.toml` and |
| 104 | + `src/libre_devops_helpers/__init__.py` and `CHANGELOG.md` has its section; see |
| 105 | + `docs/development.md`. Never move or reuse a released tag, and remember a PyPI version can |
| 106 | + never be replaced. |
| 107 | + |
| 108 | +## Pitfalls already met |
| 109 | + |
| 110 | +- `.gitignore` rules match anywhere unless anchored with `/`; ruff and hatchling honour |
| 111 | + `.gitignore`, so an ignored folder is silently left out of linting and the wheel. |
| 112 | +- The arm64 image is built under emulation: keep heavy work (such as compiling bytecode) in |
| 113 | + a `FROM --platform=$BUILDPLATFORM` stage. |
| 114 | +- The Azure CLI pins some dependencies exactly. A fix it holds back is forced in |
| 115 | + `container/azure-cli/pyproject.toml` (see the comment there), then signed in with. |
| 116 | +- Microsoft APIs return stale duplicates (devices share names), paginate with `nextLink` |
| 117 | + or `@odata.nextLink`, and answer 403 for a missing scope and a suspended tenant alike: say |
| 118 | + which in the hint. |
0 commit comments