Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4171bb3
docs: record M0 design decisions
hazeliscoding Sep 25, 2026
07d1927
build: scaffold .NET 10 solution with NativeAOT
hazeliscoding Sep 25, 2026
116a6a4
build: run tests on Microsoft.Testing.Platform
hazeliscoding Sep 25, 2026
3dff171
feat(checks): render finding templates with consistent units
hazeliscoding Sep 25, 2026
ee25934
feat(checks): compile checks into the binary at build time
hazeliscoding Sep 25, 2026
781196d
feat(engine): run every query in its own guarded read-only transaction
hazeliscoding Sep 25, 2026
4e4119f
feat(engine): turn check rows into findings
hazeliscoding Sep 25, 2026
5c1898e
feat(checks): add replication-slot-inactive
hazeliscoding Sep 25, 2026
38d21d7
test(checks): run every check's fixtures on Testcontainers
hazeliscoding Sep 25, 2026
46385bc
feat(cli): read connections from URLs, key-value strings and PG varia…
hazeliscoding Sep 25, 2026
51e196a
feat(cli): add scan and list with terminal output and exit codes
hazeliscoding Sep 25, 2026
33ffe4a
test(cli): run the published binary against an inactive slot
hazeliscoding Sep 25, 2026
029e331
ci: test on Postgres 14 to 18 and gate NativeAOT publish
hazeliscoding Sep 25, 2026
9bd7366
docs(agents): add build, test and publish commands
hazeliscoding Sep 25, 2026
e524581
docs(readme): add a recording of a scan
hazeliscoding Sep 25, 2026
6ba5e91
docs(roadmap): tick finished M0 items
hazeliscoding Sep 25, 2026
e422776
fix(engine): pin search_path so planted functions can't shadow built-ins
hazeliscoding Sep 25, 2026
c2b753c
fix(checks): deny functions that run SQL from a string or write WAL
hazeliscoding Sep 25, 2026
87f022b
fix(cli): never repeat connection input in an error
hazeliscoding Sep 25, 2026
4dd604a
fix(cli): exit 2 on any error a scan didn't plan for
hazeliscoding Sep 25, 2026
2c62a2d
fix(checks): skip slots synced from the primary on a standby
hazeliscoding Sep 25, 2026
dfe3a49
fix(checks): reject a threshold named twice
hazeliscoding Sep 25, 2026
c894e78
docs(roadmap): tick CI for M0
hazeliscoding Sep 25, 2026
8913956
docs(code): add XML docs to the public API
hazeliscoding Sep 25, 2026
f467ba9
build: fail the build on missing XML docs
hazeliscoding Sep 25, 2026
4a98cb1
docs(agents): require XML docs on public code
hazeliscoding Sep 25, 2026
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
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: CI

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Tests on Postgres ${{ matrix.postgres }}
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
postgres: ["14", "15", "16", "17", "18"]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Build and test
run: dotnet test --project tests/Pgcheckup.Tests
env:
PGCHECKUP_TEST_POSTGRES: ${{ matrix.postgres }}

native-aot:
name: NativeAOT (linux-x64)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Install the native toolchain
run: sudo apt-get update && sudo apt-get install -y clang zlib1g-dev
# Trim and AOT warnings are errors, so this fails on any IL2xxx or IL3xxx warning.
- name: Publish
run: dotnet publish src/Pgcheckup -c Release -r linux-x64 -o out
- name: Scan an inactive slot with the published binary
run: dotnet test --project tests/Pgcheckup.Tests -- --filter-class Pgcheckup.Tests.Cli.NativeBinaryTests
env:
PGCHECKUP_BINARY: ${{ github.workspace }}/out/pgcheckup
17 changes: 11 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ These are the working rules for agents in this repo. pgcheckup is a read-only CL

- `README.md`: the pitch and the "safe to run on production" promises.
- `ROADMAP.md`: decisions already made, the milestones, and what is out of scope. Check it before proposing features. Respect those decisions unless the owner reopens them. Record new or changed decisions there, with the date.
- The repo is still in planning. Don't build past the current milestone without asking.
- Work follows the milestones in `ROADMAP.md`. Don't build past the current milestone without asking.

## Commands

There is no code yet. Add the build, test and publish commands here when M0 lands. Keep them cross-platform (`dotnet`, `docker`), because the owner develops on Windows. Avoid bash-only scripts.
Keep commands cross-platform (`dotnet`, `docker`), because the owner develops on Windows. Avoid bash-only scripts.

- Build: `dotnet build pgcheckup.slnx`. A broken check folder fails the build with its file and line.
- Test: `dotnet test --project tests/Pgcheckup.Tests`. It needs Docker, and uses Postgres 18 unless `PGCHECKUP_TEST_POSTGRES` names another major (14 to 17). CI runs all five.
- Publish: `dotnet publish src/Pgcheckup -c Release -r win-x64 -o out` (`linux-x64` on Linux). Trim and AOT warnings fail it.
- Test the published binary: set `PGCHECKUP_BINARY` to it, then run `dotnet test --project tests/Pgcheckup.Tests -- --filter-class Pgcheckup.Tests.Cli.NativeBinaryTests`.
- NativeAOT publish on this Windows machine fails with `'vswhere.exe' is not recognized` unless the VS Installer folder is on PATH. Run it as `$env:PATH = "C:\Program Files (x86)\Microsoft Visual Studio\Installer;$env:PATH"; dotnet publish …`. That is an environment problem, not an AOT warning.

## Safe to run on production (hard rules)

The product is only as good as these rules. Never break them, not even in debug modes or dev tooling.

- **Read-only, always.** A check is one `SELECT` against catalogs and statistics views. No DDL or DML, and no functions with side effects: `pg_terminate_backend`, `pg_cancel_backend`, `pg_reload_conf`, `pg_stat_reset*`, `pg_switch_wal`, `pg_create_*`, `pg_drop_*`, `nextval`, `setval`. Checks run under the session guards (`default_transaction_read_only`, `statement_timeout`, `lock_timeout`) inside a `READ ONLY` transaction. Never weaken or bypass them.
- **Read-only, always.** A check is one `SELECT` against catalogs and statistics views. No DDL or DML, and no functions with side effects: `pg_terminate_backend`, `pg_cancel_backend`, `pg_reload_conf`, `pg_stat_reset*`, `pg_switch_wal`, `pg_create_*`, `pg_drop_*`, `pg_advisory_*`, `nextval`, `setval`, `set_config`, `txid_current`. Every statement pgcheckup sends runs inside `BEGIN READ ONLY` with `SET LOCAL statement_timeout`, `lock_timeout` and `search_path = pg_catalog, pg_temp`, then rolls back. Never set anything for the whole session, because behind a transaction pooler it reaches the app's connections. Never weaken or bypass these guards.
- **Fixes are text.** pgcheckup prints fix SQL and never executes it.
- **Least privilege.** No check needs more than `pg_monitor`. Never require superuser or `rds_superuser`. If the role lacks a privilege, the check is skipped with the reason. It is never an error.
- **No network beyond the Postgres connection.** No telemetry, update checks, crash reporting or remote lookups. Data such as end-of-life dates ships inside the release.
Expand All @@ -29,10 +33,10 @@ The product is only as good as these rules. Never break them, not even in debug
## Checks

- One folder per check: `checks/<id>/check.sql`, `check.md`, `fixtures/fires.sql` and `fixtures/healthy.sql`. The shape is in the `ROADMAP.md` decisions.
- `check.sql` is one read-only query that returns the fixed shape. Thresholds come in as parameters and are never hard-coded.
- `check.sql` is one read-only query that returns values, never prose. The wording lives in the `message` and `fix` templates in `check.md`. Thresholds come in as `@name` parameters and are never hard-coded. The search path is `pg_catalog` only, so qualify anything in another schema.
- Compute ages and durations in SQL from the server's `now()`, not the client's clock.
- `check.md` has **What breaks**, **Fix** and **Seen in** sections. Every check has at least one **Seen in** link to a public incident or the Postgres docs. Never cite anything a reader can't open.
- Both fixtures are required. `fires.sql` is the positive control, so a check without one isn't done. A fixture may lower a threshold when the real condition can't be reproduced at scale.
- Both fixtures are required. `fires.sql` is the positive control, so a check without one isn't done. A fixture may lower a threshold (`-- threshold name = value`) when the real condition can't be reproduced at scale.
- Check ids are kebab-case and stable, because baselines and ignore lists depend on them. Renaming one is a breaking change that needs a decision in `ROADMAP.md`.
- Severity: `critical` can take the database down or lose data soon. `warning` is heading there, or removes a safety net. `info` is housekeeping. Don't inflate severity.
- A check declares its minimum Postgres version and the providers where it is skipped. Every check is tested on every supported version.
Expand Down Expand Up @@ -69,4 +73,5 @@ The product is only as good as these rules. Never break them, not even in debug
- **Checks:** automate acceptance checks instead of handing manual steps to the owner. Give every check that tests for an absence a positive control, meaning a case that proves the check can fail.
- **Validation:** evidence comes from dogfooding (the log) and public async signals (issues, PRs, downloads, image pulls). Don't plan interviews, recruiting or outreach.
- **Docs:** short and concise. Prefer editing `ROADMAP.md` over creating new planning documents. Repo files never reference the owner's private notes.
- **Code comments:** explain why, not what. Only comment on what the code can't say for itself: a non-obvious constraint, a workaround and its cause, or a line that keeps a hard rule. Don't restate names or types, don't add boilerplate XML docs, and don't leave commented-out code. A check's `check.md` is its documentation.
- **XML docs:** every public type and member in `src/` has an XML doc comment (`///`), including new ones. The build enforces it: `src/Directory.Build.props` turns on the doc file, so a missing comment is error CS1591. Say what the member does and its contract: parameters, what it returns, what it throws and edge cases, with `<see cref>` to related types. Don't just restate the name. Document internal and private members too when their purpose isn't obvious from the name. Tests don't need XML docs, because their names say what they check.
- **Code comments:** inside code, explain why, not what. Only comment on what the code can't say for itself: a non-obvious constraint, a workaround and its cause, or a line that keeps a hard rule. Don't leave commented-out code. A check's `check.md` is its documentation.
17 changes: 17 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<InvariantGlobalization>true</InvariantGlobalization>
<Version>0.0.0</Version>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
</PropertyGroup>
<ItemGroup>
<!-- Without rewriting, Postgres itself rejects a check that sneaks in a second statement.
Set for every project, so tests run Npgsql the way pgcheckup does. -->
<RuntimeHostConfigurationOption Include="Npgsql.EnableSqlRewriting" Value="false" Trim="true" />
</ItemGroup>
</Project>
14 changes: 14 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.11.0" />
<!-- The oldest compiler a .NET 10 SDK ships with must be able to load the generator. -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.14.0" />
<PackageVersion Include="Npgsql" Version="10.0.3" />
<PackageVersion Include="System.CommandLine" Version="2.0.12" />
<PackageVersion Include="Testcontainers.PostgreSql" Version="4.15.0" />
<PackageVersion Include="xunit.v3" Version="4.0.1" />
</ItemGroup>
</Project>
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ In February 2019, one of the Postgres shards behind Mailchimp's Mandrill [ran ou

Most of these failures show up in the system catalogs weeks ahead: a table's transaction ID age, a replication slot nobody reads, WAL archiving that failed last night. Teams without a DBA rarely look. pgcheckup looks for them and tells you what to do.

> **Status:** planning. There is nothing to install yet. See [ROADMAP.md](ROADMAP.md).
> **Status:** early development. The first check, `replication-slot-inactive`, runs end to end. There is no release to install yet. See [ROADMAP.md](ROADMAP.md).

![pgcheckup scanning a database whose inactive replication slot is holding 1.07 GB of WAL](docs/scan.gif)

## How it works

Expand Down
Loading
Loading