How to set up a development environment, run the application, and work with the tooling. The contribution workflow and rules are in CONTRIBUTING.md; testing is in testing.md.
This section provides instructions for setting up your development environment to work on the project. We recommend
using a package manager like uv to manage dependencies and avoid conflicts with other
projects.
- Python 3.14 or higher
- Git
- Docker
Install every dependency group:
uv sync --all-groups-
Copy the example environment file to create your own:
cp .env.example .env
-
Configure your test credentials: Open the
.envfile and replace the example values with the details of a PESU Academy account you can test with. Each variable is documented in.env.example. There is aTEST_*variable for every profile field, and the live tests compare each one exactly with what the API returns for that account. Leave a variable empty when the account has no value for that field (for exampleTEST_MIDDLE_NAME, orTEST_SEMESTERandTEST_SECTIONif the account has graduated), since the API then returnsnull. CI reads the same variables from the repository's secrets, where such a field simply has no secret. The file is gitignored: never commit it or paste its values anywhere.
We use pre-commit hooks to ensure code quality and consistency. These will automatically run checks before you commit your code. Install the pre-commit hooks by running:
uv run pre-commit installYou can run the application using the same instructions as in the README.md file. To ensure parity with
production, we recommend testing the app both locally and inside Docker. See the README.md for Docker
instructions. The container runs as an unprivileged user (UID 10001:10001) with read-only application files.
The API Explorer at / is served directly from app/templates/playground.html, which holds its HTML, styles and
scripts. It needs no frontend build step or third-party UI dependencies.
| Setting | Default | What it does |
|---|---|---|
--host |
0.0.0.0 |
The address to listen on |
--port |
5000 |
The port to listen on. Also read from PORT; --port wins when both are set |
--debug |
off | Logs at DEBUG instead of INFO, and reloads the app when a file changes |
METRICS_TOKEN |
unset | Requires this bearer token on /metrics (see metrics.md) |
PESU_AUTH_ENVIRONMENT |
development |
The environment /health reports: development, staging or production. Any other value fails startup |
The application reads nothing else from the environment. .env is only read by the tests and the benchmarks.
This repository is set up for coding agents (Codex, Claude Code, Copilot, Cursor, Gemini and
others). Their instructions, skills and roles come from
pesu-dev/skills, mounted as a git submodule at
.agents/pesudev-skills. AGENTS.md and .agents/skills are links into it: the two provider-neutral
locations agents read.
Clone with the submodule, or the links point at nothing and agents see no instructions:
git clone --recurse-submodules https://github.com/pesu-dev/auth.git
# already cloned without it:
git submodule update --initA clone made from a fork, before pull requests moved to branches of this repository, has the fork as origin and this
repository as upstream. Point origin at this repository instead:
git remote rename origin fork && git remote rename upstream origin
git fetch origin && git branch -u origin/dev devIf the clone has no upstream remote, run git remote add origin https://github.com/pesu-dev/auth.git instead of the
second rename.
On Windows, turn on Developer Mode and run git config --global core.symlinks true before cloning,
so the links are checked out as links.
Do not edit AGENTS.md or .agents/ here: open a pull request against
pesu-dev/skills instead. Dependabot opens a pull request here to update the submodule when that
repository changes.
In GitHub Copilot, each role (planner, implementer, reviewer and so on) is also a custom agent you
can pick from the agent picker or assign an issue to. They live in .github/agents/, which is
generated from the roles in the submodule: do not edit it by hand. When a submodule update adds,
removes or changes a role's frontmatter, the sync-agents pre-commit hook fails until you
regenerate them and commit the result:
uv run python scripts/sync_agents.pyDependabot's submodule pull requests cannot do that themselves. If one fails sync-agents, a
maintainer can push the regenerated agents and a version bump to Dependabot's branch (the Version
check skips only the runs Dependabot itself triggers, so it checks that push), or anyone can open a
pull request from a new branch that bumps the submodule to the same commit and regenerates the
agents (with the usual version bump); Dependabot then closes its own.
To hand an issue to Copilot's cloud agent, assign it here: the agent opens its pull request from a
copilot/ branch of this repository, which is a pull request like any other. A maintainer may need
to approve its workflow runs before CI starts.
We enforce code quality and correctness using pre-commit, which runs formatters, linters, upgrade checks, and the test
suite automatically before every commit.
The following checks are enforced:
- ✅
rufffor linting and formatting (with auto-fix), running fromuv.lockso it is the same version as CI - ✅
uv-lockto check thatuv.lockmatchespyproject.toml - ✅
mdformatto format Markdown files (with GFM support) - ✅
end-of-file-fixer,trailing-whitespace,check-yaml,check-toml,check-added-large-filesfor formatting - ✅
check-symlinksto catch broken links such asAGENTS.md; it also fails until the submodule is checked out - ✅
name-tests-testto enforce test naming conventions - ✅
debug-statementsto prevent committed debugger calls, such aspdborbreakpoint() - ✅
sync-agentsto check that.github/agents/matches the roles in the agent submodule - ✅ A local
pytesthook that runs the full test suite
CI runs the same hooks in the Pre-commit hooks job, except pytest, because the tests run in the
Test suite & coverage job. A PR that passes uv run pre-commit run --all-files locally also passes that job.
Warning
You will not be able to commit code that fails these checks.
A few checks require the PR itself or Docker and cannot be run as pre-commit hooks. Here is how to run each one locally:
-
PR source checker: verifies the PR comes from a branch of this repository (not a fork) other than
devormain, and targetsdev. There is nothing to run; just open the PR that way. -
Version check: verifies the version in
pyproject.tomlwas bumped relative todev:git fetch origin git show origin/dev:pyproject.toml > /tmp/base_pyproject.toml python3 .github/scripts/check_version_bump.py --base-pyproject /tmp/base_pyproject.toml \ --head-pyproject pyproject.toml --head-lock uv.lock --base-ref dev -
Build image on PR, and Container security & smoke test: verifies the image builds, runs as UID 10001, and answers
/health:docker build . --tag pesu-auth-test docker run --rm pesu-auth-test id -u # prints 10001 docker run --rm -d --name pesu-auth-smoke -p 5000:5000 pesu-auth-test sleep 5 && curl --fail http://localhost:5000/health; docker stop pesu-auth-smoke
Every route in the OpenAPI schema needs a tag, a docstring (which becomes its description) and a module in app/docs/
holding an ApiDocs with an example for each response it can give, including a success and a 500. The route passes
them in with responses= and, for a request body, openapi_extra=. tests/unit/test_openapi_docs.py fails if any of
this is missing or an example does not match its model, and tests/unit/test_openapi_contract.py checks that the real
responses match what is documented.
The API Explorer and Swagger UI are built from /openapi.json, so a documented route appears in both with no other
changes. Add the route to the endpoint table and its own section in the README too.
Subclass PESUAcademyError in app/exceptions/, with the status code and a fixed message as defaults, and raise it
with the specifics in detail=. The existing handler renders it and counts it in errors_total under its class name.
Document the response in the route's app/docs/ module and in the README's response table, and add it to the error
table in architecture.md and the errors_total row in metrics.md.
Read it in the model for its block in app/models/upstream.py, return it from PESUAcademy._build_profile, and add it
to ProfileField and ProfileModel in app/models/profile.py, whose descriptions become the Swagger docs. Add it to
both full-profile examples in app/docs/authenticate.py and to the README's ProfileObject table, and to the fixture
payloads and FULL_PROFILE in tests/unit/conftest.py; tests check that each lists every field, in order.
For the live tests, give a new field a TEST_* variable named after it (TEST_BRANCH_SHORT_CODE for
branchShortCode) in .env.example and in PROFILE_VARIABLES in tests/conftest.py, so they compare it with the test
account's real value; a unit test fails until every field has one. If the field is an integer, also list it in
INTEGER_FIELDS there. Add the variable to the test job's env in .github/workflows/ci_checks.yml, and ask a
maintainer to add the matching repository secret.