pgcommon 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pgcommon-0.1.0/.env-example +28 -0
- pgcommon-0.1.0/.github/CODEOWNERS +10 -0
- pgcommon-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
- pgcommon-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +28 -0
- pgcommon-0.1.0/.github/SECURITY.md +34 -0
- pgcommon-0.1.0/.github/dependabot.yml +22 -0
- pgcommon-0.1.0/.github/pull_request_template.md +62 -0
- pgcommon-0.1.0/.github/workflows/ci.yml +319 -0
- pgcommon-0.1.0/.github/workflows/quality.yml +49 -0
- pgcommon-0.1.0/.github/workflows/release.yml +211 -0
- pgcommon-0.1.0/.github/workflows/test.yml +43 -0
- pgcommon-0.1.0/.github/workflows/validate.yml +33 -0
- pgcommon-0.1.0/.gitignore +221 -0
- pgcommon-0.1.0/ARCHITECTURE.md +604 -0
- pgcommon-0.1.0/CHANGELOG.md +29 -0
- pgcommon-0.1.0/CONTRIBUTING.md +199 -0
- pgcommon-0.1.0/Dockerfile +21 -0
- pgcommon-0.1.0/LICENSE +201 -0
- pgcommon-0.1.0/Makefile +174 -0
- pgcommon-0.1.0/PKG-INFO +1369 -0
- pgcommon-0.1.0/README.md +1343 -0
- pgcommon-0.1.0/SECURITY.md +74 -0
- pgcommon-0.1.0/VERSIONING.md +215 -0
- pgcommon-0.1.0/docker-compose.yml +29 -0
- pgcommon-0.1.0/docs/Architecture.md +9 -0
- pgcommon-0.1.0/docs/MigrationGuide.md +61 -0
- pgcommon-0.1.0/docs/adr/ADR-001-contextvars-guc-propagation.md +58 -0
- pgcommon-0.1.0/docs/adr/ADR-002-cancellation-and-rollback-safety.md +59 -0
- pgcommon-0.1.0/docs/adr/ADR-003-guc-injection-timing.md +61 -0
- pgcommon-0.1.0/docs/adr/ADR-004-migration-runner-design.md +60 -0
- pgcommon-0.1.0/docs/architecture/README.md +20 -0
- pgcommon-0.1.0/docs/architecture/mermaid/consuming-service-wiring.mmd +27 -0
- pgcommon-0.1.0/docs/architecture/mermaid/documentation-assets.mmd +38 -0
- pgcommon-0.1.0/docs/architecture/mermaid/layer-model.mmd +56 -0
- pgcommon-0.1.0/docs/architecture/mermaid/migration-runner.mmd +23 -0
- pgcommon-0.1.0/docs/architecture/mermaid/observability-stack.mmd +15 -0
- pgcommon-0.1.0/docs/architecture/mermaid/pool-create-hook-composition.mmd +16 -0
- pgcommon-0.1.0/docs/architecture/mermaid/runinsavepoint-lifecycle.mmd +23 -0
- pgcommon-0.1.0/docs/architecture/mermaid/runintx-lifecycle.mmd +30 -0
- pgcommon-0.1.0/docs/architecture/mermaid/withconn-pgbouncer-mode.mmd +31 -0
- pgcommon-0.1.0/docs/architecture/mermaid/withconn-standard-mode.mmd +23 -0
- pgcommon-0.1.0/migrations/0001_create_example_table.down.sql +1 -0
- pgcommon-0.1.0/migrations/0001_create_example_table.up.sql +7 -0
- pgcommon-0.1.0/pyproject.toml +110 -0
- pgcommon-0.1.0/src/pgcommon/__init__.py +91 -0
- pgcommon-0.1.0/src/pgcommon/_internal/__init__.py +1 -0
- pgcommon-0.1.0/src/pgcommon/_internal/dsn.py +15 -0
- pgcommon-0.1.0/src/pgcommon/_internal/duration.py +38 -0
- pgcommon-0.1.0/src/pgcommon/_internal/query_event.py +22 -0
- pgcommon-0.1.0/src/pgcommon/_internal/sql_verb.py +36 -0
- pgcommon-0.1.0/src/pgcommon/_internal/sqlstate.py +20 -0
- pgcommon-0.1.0/src/pgcommon/config.py +222 -0
- pgcommon-0.1.0/src/pgcommon/errors.py +123 -0
- pgcommon-0.1.0/src/pgcommon/guc.py +142 -0
- pgcommon-0.1.0/src/pgcommon/integrations/__init__.py +5 -0
- pgcommon-0.1.0/src/pgcommon/integrations/fastapi.py +151 -0
- pgcommon-0.1.0/src/pgcommon/logger.py +71 -0
- pgcommon-0.1.0/src/pgcommon/metrics.py +179 -0
- pgcommon-0.1.0/src/pgcommon/migrate/__init__.py +8 -0
- pgcommon-0.1.0/src/pgcommon/migrate/__main__.py +6 -0
- pgcommon-0.1.0/src/pgcommon/migrate/cli.py +78 -0
- pgcommon-0.1.0/src/pgcommon/migrate/errors.py +12 -0
- pgcommon-0.1.0/src/pgcommon/migrate/runner.py +261 -0
- pgcommon-0.1.0/src/pgcommon/pool.py +346 -0
- pgcommon-0.1.0/src/pgcommon/py.typed +0 -0
- pgcommon-0.1.0/src/pgcommon/retry.py +121 -0
- pgcommon-0.1.0/src/pgcommon/tracer.py +85 -0
- pgcommon-0.1.0/src/pgcommon/tx.py +98 -0
- pgcommon-0.1.0/tests/__init__.py +0 -0
- pgcommon-0.1.0/tests/e2e/__init__.py +0 -0
- pgcommon-0.1.0/tests/e2e/conftest.py +100 -0
- pgcommon-0.1.0/tests/e2e/test_rls_pgbouncer_e2e.py +145 -0
- pgcommon-0.1.0/tests/fixtures/__init__.py +0 -0
- pgcommon-0.1.0/tests/fixtures/fake_asyncpg.py +146 -0
- pgcommon-0.1.0/tests/fixtures/fake_migrate_conn.py +66 -0
- pgcommon-0.1.0/tests/fixtures/fakes.py +49 -0
- pgcommon-0.1.0/tests/integration/__init__.py +0 -0
- pgcommon-0.1.0/tests/integration/conftest.py +41 -0
- pgcommon-0.1.0/tests/integration/test_concurrency_stress.py +98 -0
- pgcommon-0.1.0/tests/integration/test_error_helpers_real_violations.py +89 -0
- pgcommon-0.1.0/tests/integration/test_migrate_runner_integration.py +108 -0
- pgcommon-0.1.0/tests/integration/test_pool_lifecycle.py +89 -0
- pgcommon-0.1.0/tests/integration/test_retry_real_serialization_failure.py +93 -0
- pgcommon-0.1.0/tests/integration/test_rls_guc_injection.py +124 -0
- pgcommon-0.1.0/tests/integration/test_tx_and_savepoint.py +119 -0
- pgcommon-0.1.0/tests/unit/__init__.py +0 -0
- pgcommon-0.1.0/tests/unit/test_config.py +197 -0
- pgcommon-0.1.0/tests/unit/test_errors.py +71 -0
- pgcommon-0.1.0/tests/unit/test_guc.py +158 -0
- pgcommon-0.1.0/tests/unit/test_integrations_fastapi.py +186 -0
- pgcommon-0.1.0/tests/unit/test_internal.py +76 -0
- pgcommon-0.1.0/tests/unit/test_logger.py +54 -0
- pgcommon-0.1.0/tests/unit/test_metrics.py +121 -0
- pgcommon-0.1.0/tests/unit/test_migrate_cli.py +136 -0
- pgcommon-0.1.0/tests/unit/test_migrate_runner.py +365 -0
- pgcommon-0.1.0/tests/unit/test_pool.py +439 -0
- pgcommon-0.1.0/tests/unit/test_retry.py +206 -0
- pgcommon-0.1.0/tests/unit/test_tracer.py +107 -0
- pgcommon-0.1.0/tests/unit/test_tx.py +236 -0
- pgcommon-0.1.0/uv.lock +1259 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copy to .env for local development: `make setup` does this for you.
|
|
2
|
+
|
|
3
|
+
# Either set DATABASE_URL directly...
|
|
4
|
+
DATABASE_URL=postgres://test:test@localhost:5432/test
|
|
5
|
+
|
|
6
|
+
# ...or the individual PG_* variables (used only when DATABASE_URL is unset):
|
|
7
|
+
PG_HOST=localhost
|
|
8
|
+
PG_PORT=5432
|
|
9
|
+
PG_USER=test
|
|
10
|
+
PG_PASSWORD=test
|
|
11
|
+
PG_DBNAME=test
|
|
12
|
+
PG_SSLMODE=disable
|
|
13
|
+
PG_BOUNCER_MODE=false
|
|
14
|
+
|
|
15
|
+
# Pool tuning (all optional — see README.md's Configuration reference for defaults).
|
|
16
|
+
PG_MAX_CONNS=10
|
|
17
|
+
PG_MIN_CONNS=2
|
|
18
|
+
PG_MAX_CONN_LIFETIME=1h
|
|
19
|
+
PG_MAX_CONN_LIFETIME_JITTER=0
|
|
20
|
+
PG_MAX_CONN_IDLE_TIME=30m
|
|
21
|
+
PG_HEALTH_CHECK_PERIOD=1m
|
|
22
|
+
PG_SLOW_QUERY_THRESHOLD=200ms
|
|
23
|
+
|
|
24
|
+
# PgBouncer, if using `make docker-up-all`.
|
|
25
|
+
PGBOUNCER_DATABASE_URL=postgres://test:test@localhost:6432/test
|
|
26
|
+
|
|
27
|
+
APP_NAME=pgcommon-dev
|
|
28
|
+
BUILD_VERSION=0.0.0-dev
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Default: every file requires review from the platform team.
|
|
2
|
+
# Replace @BCBP-SOLUTIONS-FZC-LLC/platform-team with your actual team slug.
|
|
3
|
+
* @BCBP-SOLUTIONS-FZC-LLC/platform-team
|
|
4
|
+
|
|
5
|
+
# Workflow and release automation changes require explicit sign-off.
|
|
6
|
+
.github/workflows/ @BCBP-SOLUTIONS-FZC-LLC/platform-team
|
|
7
|
+
.github/dependabot.yml @BCBP-SOLUTIONS-FZC-LLC/platform-team
|
|
8
|
+
|
|
9
|
+
# Public API surface — extra scrutiny.
|
|
10
|
+
src/pgcommon/ @BCBP-SOLUTIONS-FZC-LLC/platform-team
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Report a defect in pgcommon
|
|
4
|
+
title: '[BUG] '
|
|
5
|
+
labels: bug
|
|
6
|
+
assignees: ''
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Description
|
|
10
|
+
A clear description of the bug.
|
|
11
|
+
|
|
12
|
+
## Library version
|
|
13
|
+
`pgcommon==X.Y.Z`
|
|
14
|
+
|
|
15
|
+
## Python version
|
|
16
|
+
`python --version` output
|
|
17
|
+
|
|
18
|
+
## Configuration
|
|
19
|
+
Relevant `Config` fields (redact the DSN's credentials): `pgbouncer_mode`, `guc_provider` set?, `max_conns`/`min_conns`, etc.
|
|
20
|
+
|
|
21
|
+
## Steps to reproduce
|
|
22
|
+
1.
|
|
23
|
+
2.
|
|
24
|
+
3.
|
|
25
|
+
|
|
26
|
+
## Expected behaviour
|
|
27
|
+
What you expected to happen.
|
|
28
|
+
|
|
29
|
+
## Actual behaviour
|
|
30
|
+
What actually happened. Include error messages, stack traces, or log output.
|
|
31
|
+
|
|
32
|
+
## Minimal reproduction
|
|
33
|
+
```python
|
|
34
|
+
# paste the smallest Python snippet that triggers the bug
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Additional context
|
|
38
|
+
Any other relevant context (direct Postgres vs. PgBouncer/RDS Proxy, OS, container environment, related issues).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Propose a new helper, config option, or enhancement
|
|
4
|
+
title: '[FEAT] '
|
|
5
|
+
labels: enhancement
|
|
6
|
+
assignees: ''
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Problem / motivation
|
|
10
|
+
What problem does this solve? Who is affected?
|
|
11
|
+
|
|
12
|
+
## Proposed solution
|
|
13
|
+
Describe the API or behaviour change you'd like.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
# Example usage of the proposed feature
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Alternatives considered
|
|
20
|
+
Other approaches you evaluated and why you ruled them out.
|
|
21
|
+
|
|
22
|
+
## Acceptance criteria
|
|
23
|
+
- [ ]
|
|
24
|
+
- [ ]
|
|
25
|
+
- [ ]
|
|
26
|
+
|
|
27
|
+
## Additional context
|
|
28
|
+
Links to related issues, the equivalent Go `platform-pgcommon` behavior (if any), or prior art.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
| Version | Supported |
|
|
6
|
+
|---------|-----------|
|
|
7
|
+
| 0.x | ✅ Active |
|
|
8
|
+
|
|
9
|
+
Older major versions are not patched. Upgrade to the latest 0.x release. See [../VERSIONING.md](../VERSIONING.md) for the full policy once `1.0.0` ships.
|
|
10
|
+
|
|
11
|
+
## Reporting a Vulnerability
|
|
12
|
+
|
|
13
|
+
**Do not open a public GitHub issue for security vulnerabilities.**
|
|
14
|
+
|
|
15
|
+
Email: vijay@bcbpsolutions.com
|
|
16
|
+
|
|
17
|
+
Include in your report:
|
|
18
|
+
- Description of the vulnerability and affected component
|
|
19
|
+
- Steps to reproduce
|
|
20
|
+
- Potential impact (tenant data isolation bypass, credential leakage, DoS, etc.)
|
|
21
|
+
- Suggested fix or patch (if any)
|
|
22
|
+
|
|
23
|
+
### Response timeline
|
|
24
|
+
|
|
25
|
+
| Step | Target |
|
|
26
|
+
|------|--------|
|
|
27
|
+
| Initial acknowledgement | 48 hours |
|
|
28
|
+
| Severity assessment | 5 business days |
|
|
29
|
+
| Patch release (critical/high) | 14 days |
|
|
30
|
+
| Public disclosure | After patch ships |
|
|
31
|
+
|
|
32
|
+
We follow responsible disclosure. We will credit reporters in the release notes unless anonymity is requested.
|
|
33
|
+
|
|
34
|
+
See [../SECURITY.md](../SECURITY.md) for this library's trust model and known sensitive surface area (GUC injection, `contextvars` propagation, DSN masking).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: pip
|
|
4
|
+
directory: /
|
|
5
|
+
schedule:
|
|
6
|
+
interval: weekly
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
labels: [dependencies, python]
|
|
9
|
+
|
|
10
|
+
- package-ecosystem: github-actions
|
|
11
|
+
directory: /
|
|
12
|
+
schedule:
|
|
13
|
+
interval: weekly
|
|
14
|
+
open-pull-requests-limit: 5
|
|
15
|
+
labels: [dependencies, github-actions]
|
|
16
|
+
|
|
17
|
+
- package-ecosystem: docker
|
|
18
|
+
directory: /
|
|
19
|
+
schedule:
|
|
20
|
+
interval: weekly
|
|
21
|
+
open-pull-requests-limit: 5
|
|
22
|
+
labels: [dependencies, docker]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
## Description
|
|
2
|
+
Provide a clear description of the changes.
|
|
3
|
+
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Type of Change
|
|
7
|
+
- [ ] Bug fix
|
|
8
|
+
- [ ] New feature
|
|
9
|
+
- [ ] Refactor
|
|
10
|
+
- [ ] Documentation
|
|
11
|
+
- [ ] Test
|
|
12
|
+
- [ ] Breaking change
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Testing
|
|
17
|
+
- [ ] Unit tests added/updated (`make test-unit` — no Docker required)
|
|
18
|
+
- [ ] Integration/e2e tests added/updated if this touches GUC injection, transaction/rollback semantics, PgBouncer mode, or the migration runner (`make test-int` / `make test-e2e`)
|
|
19
|
+
- [ ] All tests passing (`make cover` — full suite, ≥97% coverage gate)
|
|
20
|
+
- [ ] Manual testing performed (if required)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Checklist
|
|
25
|
+
|
|
26
|
+
### Code Quality
|
|
27
|
+
- [ ] Code is properly formatted (`make format` — ruff format)
|
|
28
|
+
- [ ] Linting passed (`make lint` — ruff)
|
|
29
|
+
- [ ] Type checking passed (`make typecheck` — mypy --strict)
|
|
30
|
+
- [ ] No debug logs / commented code
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
### Tenancy & safety (IMPORTANT for this library)
|
|
35
|
+
- [ ] Any change touching GUC injection timing or scope (`pgcommon.guc`, `pgcommon.pool`, `pgcommon.tx`) has an integration/e2e test — see `tests/integration/test_rls_guc_injection.py` and `tests/e2e/test_rls_pgbouncer_e2e.py` for the pattern
|
|
36
|
+
- [ ] All database access still goes through `Pool.with_conn`/`run_in_tx` — no new path exposes a raw `asyncpg.Pool` or `Connection`
|
|
37
|
+
- [ ] `contextvars.ContextVar.set()` calls are paired with a `reset()` in a `finally`/context manager (see ARCHITECTURE.md's concurrency notes) — a missing pairing is the most likely cause of a cross-request GUC leak
|
|
38
|
+
- [ ] Retry-eligible code paths (`pgcommon.retry`) are only added for genuinely idempotent operations
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
### Security
|
|
43
|
+
- [ ] No secrets hardcoded
|
|
44
|
+
- [ ] `pgcommon._internal` is not imported from outside `src/pgcommon/` (enforced by `ruff`, but check anyway)
|
|
45
|
+
- [ ] `log_tenant_id`/`allow_full_statements` default behavior (opt-in only) is unaffected, or the change to it is deliberate and documented
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### Documentation
|
|
50
|
+
- [ ] README updated (config/error reference tables, pitfalls) if the public API changed
|
|
51
|
+
- [ ] `ARCHITECTURE.md` updated if a cross-cutting design decision or an asyncio/Go behavioral divergence changed
|
|
52
|
+
- [ ] `CHANGELOG.md`'s `[Unreleased]` section updated
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 🔗 Related Issue
|
|
57
|
+
Closes #<issue-id>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Deployment Notes
|
|
62
|
+
Mention anything important for deployment (e.g. a required migration, a config default change).
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# CI for https://github.com/BCBP-SOLUTIONS-FZC-LLC/platform-pgcommon-py
|
|
2
|
+
name: CI
|
|
3
|
+
|
|
4
|
+
"on":
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
pull_request:
|
|
8
|
+
branches: [main]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
validate:
|
|
16
|
+
name: Validate
|
|
17
|
+
uses: ./.github/workflows/validate.yml
|
|
18
|
+
|
|
19
|
+
coverage:
|
|
20
|
+
name: Coverage (Python ${{ matrix.python-version }})
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
needs: validate
|
|
23
|
+
strategy:
|
|
24
|
+
matrix:
|
|
25
|
+
python-version: ['3.12', '3.13', '3.14']
|
|
26
|
+
steps:
|
|
27
|
+
- name: Checkout
|
|
28
|
+
uses: actions/checkout@v7
|
|
29
|
+
|
|
30
|
+
- name: Install uv
|
|
31
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
32
|
+
with:
|
|
33
|
+
python-version: ${{ matrix.python-version }}
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies (uv sync from uv.lock)
|
|
36
|
+
run: uv sync
|
|
37
|
+
|
|
38
|
+
- name: Test with coverage (>=97% required, enforced by pyproject.toml)
|
|
39
|
+
run: uv run pytest
|
|
40
|
+
|
|
41
|
+
- name: Upload coverage report
|
|
42
|
+
uses: actions/upload-artifact@v7
|
|
43
|
+
if: always()
|
|
44
|
+
with:
|
|
45
|
+
name: coverage-report-py${{ matrix.python-version }}
|
|
46
|
+
path: coverage.xml
|
|
47
|
+
if-no-files-found: ignore
|
|
48
|
+
|
|
49
|
+
build:
|
|
50
|
+
name: Build
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
needs: coverage
|
|
53
|
+
steps:
|
|
54
|
+
- name: Checkout
|
|
55
|
+
uses: actions/checkout@v7
|
|
56
|
+
|
|
57
|
+
- name: Install uv
|
|
58
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
59
|
+
with:
|
|
60
|
+
python-version: '3.12'
|
|
61
|
+
|
|
62
|
+
- name: Build wheel + sdist
|
|
63
|
+
run: uv build
|
|
64
|
+
|
|
65
|
+
- name: Verify build artifacts
|
|
66
|
+
run: ls dist/*.whl dist/*.tar.gz
|
|
67
|
+
|
|
68
|
+
- name: Verify the pgcommon-migrate console script installs and runs
|
|
69
|
+
run: |
|
|
70
|
+
python3 -m venv /tmp/verify-venv
|
|
71
|
+
/tmp/verify-venv/bin/pip install --quiet dist/*.whl
|
|
72
|
+
/tmp/verify-venv/bin/pgcommon-migrate --help
|
|
73
|
+
|
|
74
|
+
- name: Upload build artifacts
|
|
75
|
+
uses: actions/upload-artifact@v7
|
|
76
|
+
with:
|
|
77
|
+
name: pgcommon-dist
|
|
78
|
+
path: dist/
|
|
79
|
+
if-no-files-found: error
|
|
80
|
+
|
|
81
|
+
lint-dockerfile:
|
|
82
|
+
name: Lint Dockerfile
|
|
83
|
+
runs-on: ubuntu-latest
|
|
84
|
+
steps:
|
|
85
|
+
- name: Checkout
|
|
86
|
+
uses: actions/checkout@v7
|
|
87
|
+
|
|
88
|
+
- name: Run hadolint
|
|
89
|
+
uses: hadolint/hadolint-action@v3.3.0
|
|
90
|
+
with:
|
|
91
|
+
dockerfile: Dockerfile
|
|
92
|
+
failure-threshold: warning
|
|
93
|
+
|
|
94
|
+
build-image:
|
|
95
|
+
name: Build image (cache)
|
|
96
|
+
runs-on: ubuntu-latest
|
|
97
|
+
steps:
|
|
98
|
+
- name: Checkout
|
|
99
|
+
uses: actions/checkout@v7
|
|
100
|
+
|
|
101
|
+
# Booting buildx pulls the moby/buildkit image from Docker Hub, which occasionally
|
|
102
|
+
# times out on shared GitHub-hosted runner IPs ("context deadline exceeded") — a
|
|
103
|
+
# known transient flake, not a config problem. Retry once with the same action
|
|
104
|
+
# before failing the job.
|
|
105
|
+
- name: Set up Docker Buildx
|
|
106
|
+
id: buildx
|
|
107
|
+
continue-on-error: true
|
|
108
|
+
uses: docker/setup-buildx-action@v4
|
|
109
|
+
|
|
110
|
+
- name: Set up Docker Buildx (retry)
|
|
111
|
+
if: steps.buildx.outcome == 'failure'
|
|
112
|
+
uses: docker/setup-buildx-action@v4
|
|
113
|
+
|
|
114
|
+
- name: Build (no push)
|
|
115
|
+
uses: docker/build-push-action@v7
|
|
116
|
+
with:
|
|
117
|
+
context: .
|
|
118
|
+
file: ./Dockerfile
|
|
119
|
+
push: false
|
|
120
|
+
tags: pgcommon-migrate:ci
|
|
121
|
+
cache-from: type=gha
|
|
122
|
+
cache-to: type=gha,mode=max
|
|
123
|
+
|
|
124
|
+
trivy-scan:
|
|
125
|
+
name: Trivy CVE scan
|
|
126
|
+
runs-on: ubuntu-latest
|
|
127
|
+
needs: build-image
|
|
128
|
+
steps:
|
|
129
|
+
- name: Checkout
|
|
130
|
+
uses: actions/checkout@v7
|
|
131
|
+
|
|
132
|
+
# Booting buildx pulls the moby/buildkit image from Docker Hub, which occasionally
|
|
133
|
+
# times out on shared GitHub-hosted runner IPs ("context deadline exceeded") — a
|
|
134
|
+
# known transient flake, not a config problem. Retry once with the same action
|
|
135
|
+
# before failing the job.
|
|
136
|
+
- name: Set up Docker Buildx
|
|
137
|
+
id: buildx
|
|
138
|
+
continue-on-error: true
|
|
139
|
+
uses: docker/setup-buildx-action@v4
|
|
140
|
+
|
|
141
|
+
- name: Set up Docker Buildx (retry)
|
|
142
|
+
if: steps.buildx.outcome == 'failure'
|
|
143
|
+
uses: docker/setup-buildx-action@v4
|
|
144
|
+
|
|
145
|
+
# Rebuilds against the GHA cache populated by "Build image (cache)" — fast, and
|
|
146
|
+
# keeps this job self-contained (no cross-job image tarball transfer needed).
|
|
147
|
+
- name: Build image (load, cached)
|
|
148
|
+
uses: docker/build-push-action@v7
|
|
149
|
+
with:
|
|
150
|
+
context: .
|
|
151
|
+
file: ./Dockerfile
|
|
152
|
+
push: false
|
|
153
|
+
load: true
|
|
154
|
+
tags: pgcommon-migrate:ci
|
|
155
|
+
cache-from: type=gha
|
|
156
|
+
|
|
157
|
+
- name: Run Trivy vulnerability scan
|
|
158
|
+
uses: aquasecurity/trivy-action@v0.36.0
|
|
159
|
+
with:
|
|
160
|
+
image-ref: pgcommon-migrate:ci
|
|
161
|
+
format: table
|
|
162
|
+
severity: HIGH,CRITICAL
|
|
163
|
+
ignore-unfixed: true
|
|
164
|
+
exit-code: '1'
|
|
165
|
+
scanners: vuln
|
|
166
|
+
|
|
167
|
+
smoke-tests:
|
|
168
|
+
name: Smoke tests
|
|
169
|
+
runs-on: ubuntu-latest
|
|
170
|
+
needs: build-image
|
|
171
|
+
steps:
|
|
172
|
+
- name: Checkout
|
|
173
|
+
uses: actions/checkout@v7
|
|
174
|
+
|
|
175
|
+
# Booting buildx pulls the moby/buildkit image from Docker Hub, which occasionally
|
|
176
|
+
# times out on shared GitHub-hosted runner IPs ("context deadline exceeded") — a
|
|
177
|
+
# known transient flake, not a config problem. Retry once with the same action
|
|
178
|
+
# before failing the job.
|
|
179
|
+
- name: Set up Docker Buildx
|
|
180
|
+
id: buildx
|
|
181
|
+
continue-on-error: true
|
|
182
|
+
uses: docker/setup-buildx-action@v4
|
|
183
|
+
|
|
184
|
+
- name: Set up Docker Buildx (retry)
|
|
185
|
+
if: steps.buildx.outcome == 'failure'
|
|
186
|
+
uses: docker/setup-buildx-action@v4
|
|
187
|
+
|
|
188
|
+
- name: Build image (load, cached)
|
|
189
|
+
uses: docker/build-push-action@v7
|
|
190
|
+
with:
|
|
191
|
+
context: .
|
|
192
|
+
file: ./Dockerfile
|
|
193
|
+
push: false
|
|
194
|
+
load: true
|
|
195
|
+
tags: pgcommon-migrate:ci
|
|
196
|
+
cache-from: type=gha
|
|
197
|
+
|
|
198
|
+
# This image is a migrate CLI, not a running service — "smoke test" means: the
|
|
199
|
+
# entrypoint runs and prints usage, and a real invocation against a bad/missing
|
|
200
|
+
# migration source fails cleanly (non-zero exit, readable error), rather than
|
|
201
|
+
# hanging or crashing with a traceback.
|
|
202
|
+
- name: Default CMD prints usage
|
|
203
|
+
run: docker run --rm pgcommon-migrate:ci | grep -q 'Apply or roll back pgcommon SQL migrations'
|
|
204
|
+
|
|
205
|
+
- name: up fails cleanly against a missing migration source
|
|
206
|
+
run: |
|
|
207
|
+
set +e
|
|
208
|
+
output=$(docker run --rm pgcommon-migrate:ci --dsn "postgres://x" up 2>&1)
|
|
209
|
+
status=$?
|
|
210
|
+
set -e
|
|
211
|
+
echo "$output"
|
|
212
|
+
[ "$status" -ne 0 ]
|
|
213
|
+
echo "$output" | grep -q 'migration source'
|
|
214
|
+
|
|
215
|
+
docker:
|
|
216
|
+
name: Docker image (migrate CLI)
|
|
217
|
+
runs-on: ubuntu-latest
|
|
218
|
+
needs: build
|
|
219
|
+
if: github.event_name == 'push'
|
|
220
|
+
permissions:
|
|
221
|
+
contents: read
|
|
222
|
+
packages: write
|
|
223
|
+
steps:
|
|
224
|
+
- name: Checkout
|
|
225
|
+
uses: actions/checkout@v7
|
|
226
|
+
|
|
227
|
+
- name: Log in to GHCR
|
|
228
|
+
uses: docker/login-action@v4
|
|
229
|
+
with:
|
|
230
|
+
registry: ghcr.io
|
|
231
|
+
username: ${{ github.actor }}
|
|
232
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
233
|
+
|
|
234
|
+
- name: Docker metadata
|
|
235
|
+
id: meta
|
|
236
|
+
uses: docker/metadata-action@v6
|
|
237
|
+
with:
|
|
238
|
+
images: ghcr.io/bcbp-solutions-fzc-llc/platform-pgcommon-py
|
|
239
|
+
tags: |
|
|
240
|
+
type=ref,event=branch
|
|
241
|
+
type=sha,prefix=sha-,format=short
|
|
242
|
+
|
|
243
|
+
# Booting buildx pulls the moby/buildkit image from Docker Hub, which occasionally
|
|
244
|
+
# times out on shared GitHub-hosted runner IPs ("context deadline exceeded") — a
|
|
245
|
+
# known transient flake, not a config problem. Retry once with the same action
|
|
246
|
+
# before failing the job.
|
|
247
|
+
- name: Set up Docker Buildx
|
|
248
|
+
id: buildx
|
|
249
|
+
continue-on-error: true
|
|
250
|
+
uses: docker/setup-buildx-action@v4
|
|
251
|
+
|
|
252
|
+
- name: Set up Docker Buildx (retry)
|
|
253
|
+
if: steps.buildx.outcome == 'failure'
|
|
254
|
+
uses: docker/setup-buildx-action@v4
|
|
255
|
+
|
|
256
|
+
- name: Build and push image
|
|
257
|
+
uses: docker/build-push-action@v7
|
|
258
|
+
with:
|
|
259
|
+
context: .
|
|
260
|
+
file: ./Dockerfile
|
|
261
|
+
push: true
|
|
262
|
+
tags: ${{ steps.meta.outputs.tags }}
|
|
263
|
+
cache-from: type=gha
|
|
264
|
+
cache-to: type=gha,mode=max
|
|
265
|
+
|
|
266
|
+
pr-summary:
|
|
267
|
+
name: PR summary
|
|
268
|
+
runs-on: ubuntu-latest
|
|
269
|
+
needs: [validate, build-image, lint-dockerfile, trivy-scan, smoke-tests]
|
|
270
|
+
if: always() && github.event_name == 'pull_request'
|
|
271
|
+
permissions:
|
|
272
|
+
pull-requests: write
|
|
273
|
+
steps:
|
|
274
|
+
- name: Post summary comment
|
|
275
|
+
uses: actions/github-script@v9
|
|
276
|
+
with:
|
|
277
|
+
script: |
|
|
278
|
+
const results = {
|
|
279
|
+
'Validate (Quality + Test)': '${{ needs.validate.result }}',
|
|
280
|
+
'Build image (cache)': '${{ needs.build-image.result }}',
|
|
281
|
+
'Lint Dockerfile': '${{ needs.lint-dockerfile.result }}',
|
|
282
|
+
'Trivy CVE scan': '${{ needs.trivy-scan.result }}',
|
|
283
|
+
'Smoke tests': '${{ needs.smoke-tests.result }}',
|
|
284
|
+
};
|
|
285
|
+
const icon = (r) => (r === 'success' ? '✅' : r === 'skipped' ? '⏭️' : '❌');
|
|
286
|
+
const rows = Object.entries(results)
|
|
287
|
+
.map(([name, result]) => `| ${name} | ${icon(result)} ${result} |`)
|
|
288
|
+
.join('\n');
|
|
289
|
+
const body = [
|
|
290
|
+
'### CI summary',
|
|
291
|
+
'',
|
|
292
|
+
'| Check | Result |',
|
|
293
|
+
'|---|---|',
|
|
294
|
+
rows,
|
|
295
|
+
].join('\n');
|
|
296
|
+
|
|
297
|
+
const marker = '<!-- pr-summary-bot -->';
|
|
298
|
+
const fullBody = `${marker}\n${body}`;
|
|
299
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
300
|
+
owner: context.repo.owner,
|
|
301
|
+
repo: context.repo.repo,
|
|
302
|
+
issue_number: context.issue.number,
|
|
303
|
+
});
|
|
304
|
+
const existing = comments.find((c) => c.body.includes(marker));
|
|
305
|
+
if (existing) {
|
|
306
|
+
await github.rest.issues.updateComment({
|
|
307
|
+
owner: context.repo.owner,
|
|
308
|
+
repo: context.repo.repo,
|
|
309
|
+
comment_id: existing.id,
|
|
310
|
+
body: fullBody,
|
|
311
|
+
});
|
|
312
|
+
} else {
|
|
313
|
+
await github.rest.issues.createComment({
|
|
314
|
+
owner: context.repo.owner,
|
|
315
|
+
repo: context.repo.repo,
|
|
316
|
+
issue_number: context.issue.number,
|
|
317
|
+
body: fullBody,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Reusable workflow — called by validate.yml's "Quality" job. Its job id ("quality")
|
|
2
|
+
# combines with the calling chain to produce the "Validate / Quality / quality" required
|
|
3
|
+
# status check.
|
|
4
|
+
# Runs: checkout → uv sync (dev group, from uv.lock) → format check → ruff → mypy → pip-audit
|
|
5
|
+
name: Quality Check
|
|
6
|
+
|
|
7
|
+
"on":
|
|
8
|
+
workflow_call:
|
|
9
|
+
inputs:
|
|
10
|
+
ref:
|
|
11
|
+
description: 'Git ref to check out (leave empty to use the caller ref)'
|
|
12
|
+
type: string
|
|
13
|
+
required: false
|
|
14
|
+
default: ''
|
|
15
|
+
|
|
16
|
+
permissions:
|
|
17
|
+
contents: read
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
quality:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout
|
|
24
|
+
uses: actions/checkout@v7
|
|
25
|
+
with:
|
|
26
|
+
ref: ${{ inputs.ref || github.ref }}
|
|
27
|
+
|
|
28
|
+
- name: Install uv
|
|
29
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
30
|
+
with:
|
|
31
|
+
python-version: '3.12'
|
|
32
|
+
|
|
33
|
+
- name: Install dependencies (uv sync from uv.lock)
|
|
34
|
+
run: uv sync
|
|
35
|
+
|
|
36
|
+
- name: Verify dependency resolution
|
|
37
|
+
run: uv pip check
|
|
38
|
+
|
|
39
|
+
- name: Format check (ruff format)
|
|
40
|
+
run: uv run ruff format --check .
|
|
41
|
+
|
|
42
|
+
- name: Lint (ruff)
|
|
43
|
+
run: uv run ruff check .
|
|
44
|
+
|
|
45
|
+
- name: Type check (mypy --strict)
|
|
46
|
+
run: uv run mypy
|
|
47
|
+
|
|
48
|
+
- name: Vulnerability check (pip-audit)
|
|
49
|
+
run: uv run pip-audit
|