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.
Files changed (100) hide show
  1. pgcommon-0.1.0/.env-example +28 -0
  2. pgcommon-0.1.0/.github/CODEOWNERS +10 -0
  3. pgcommon-0.1.0/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. pgcommon-0.1.0/.github/ISSUE_TEMPLATE/feature_request.md +28 -0
  5. pgcommon-0.1.0/.github/SECURITY.md +34 -0
  6. pgcommon-0.1.0/.github/dependabot.yml +22 -0
  7. pgcommon-0.1.0/.github/pull_request_template.md +62 -0
  8. pgcommon-0.1.0/.github/workflows/ci.yml +319 -0
  9. pgcommon-0.1.0/.github/workflows/quality.yml +49 -0
  10. pgcommon-0.1.0/.github/workflows/release.yml +211 -0
  11. pgcommon-0.1.0/.github/workflows/test.yml +43 -0
  12. pgcommon-0.1.0/.github/workflows/validate.yml +33 -0
  13. pgcommon-0.1.0/.gitignore +221 -0
  14. pgcommon-0.1.0/ARCHITECTURE.md +604 -0
  15. pgcommon-0.1.0/CHANGELOG.md +29 -0
  16. pgcommon-0.1.0/CONTRIBUTING.md +199 -0
  17. pgcommon-0.1.0/Dockerfile +21 -0
  18. pgcommon-0.1.0/LICENSE +201 -0
  19. pgcommon-0.1.0/Makefile +174 -0
  20. pgcommon-0.1.0/PKG-INFO +1369 -0
  21. pgcommon-0.1.0/README.md +1343 -0
  22. pgcommon-0.1.0/SECURITY.md +74 -0
  23. pgcommon-0.1.0/VERSIONING.md +215 -0
  24. pgcommon-0.1.0/docker-compose.yml +29 -0
  25. pgcommon-0.1.0/docs/Architecture.md +9 -0
  26. pgcommon-0.1.0/docs/MigrationGuide.md +61 -0
  27. pgcommon-0.1.0/docs/adr/ADR-001-contextvars-guc-propagation.md +58 -0
  28. pgcommon-0.1.0/docs/adr/ADR-002-cancellation-and-rollback-safety.md +59 -0
  29. pgcommon-0.1.0/docs/adr/ADR-003-guc-injection-timing.md +61 -0
  30. pgcommon-0.1.0/docs/adr/ADR-004-migration-runner-design.md +60 -0
  31. pgcommon-0.1.0/docs/architecture/README.md +20 -0
  32. pgcommon-0.1.0/docs/architecture/mermaid/consuming-service-wiring.mmd +27 -0
  33. pgcommon-0.1.0/docs/architecture/mermaid/documentation-assets.mmd +38 -0
  34. pgcommon-0.1.0/docs/architecture/mermaid/layer-model.mmd +56 -0
  35. pgcommon-0.1.0/docs/architecture/mermaid/migration-runner.mmd +23 -0
  36. pgcommon-0.1.0/docs/architecture/mermaid/observability-stack.mmd +15 -0
  37. pgcommon-0.1.0/docs/architecture/mermaid/pool-create-hook-composition.mmd +16 -0
  38. pgcommon-0.1.0/docs/architecture/mermaid/runinsavepoint-lifecycle.mmd +23 -0
  39. pgcommon-0.1.0/docs/architecture/mermaid/runintx-lifecycle.mmd +30 -0
  40. pgcommon-0.1.0/docs/architecture/mermaid/withconn-pgbouncer-mode.mmd +31 -0
  41. pgcommon-0.1.0/docs/architecture/mermaid/withconn-standard-mode.mmd +23 -0
  42. pgcommon-0.1.0/migrations/0001_create_example_table.down.sql +1 -0
  43. pgcommon-0.1.0/migrations/0001_create_example_table.up.sql +7 -0
  44. pgcommon-0.1.0/pyproject.toml +110 -0
  45. pgcommon-0.1.0/src/pgcommon/__init__.py +91 -0
  46. pgcommon-0.1.0/src/pgcommon/_internal/__init__.py +1 -0
  47. pgcommon-0.1.0/src/pgcommon/_internal/dsn.py +15 -0
  48. pgcommon-0.1.0/src/pgcommon/_internal/duration.py +38 -0
  49. pgcommon-0.1.0/src/pgcommon/_internal/query_event.py +22 -0
  50. pgcommon-0.1.0/src/pgcommon/_internal/sql_verb.py +36 -0
  51. pgcommon-0.1.0/src/pgcommon/_internal/sqlstate.py +20 -0
  52. pgcommon-0.1.0/src/pgcommon/config.py +222 -0
  53. pgcommon-0.1.0/src/pgcommon/errors.py +123 -0
  54. pgcommon-0.1.0/src/pgcommon/guc.py +142 -0
  55. pgcommon-0.1.0/src/pgcommon/integrations/__init__.py +5 -0
  56. pgcommon-0.1.0/src/pgcommon/integrations/fastapi.py +151 -0
  57. pgcommon-0.1.0/src/pgcommon/logger.py +71 -0
  58. pgcommon-0.1.0/src/pgcommon/metrics.py +179 -0
  59. pgcommon-0.1.0/src/pgcommon/migrate/__init__.py +8 -0
  60. pgcommon-0.1.0/src/pgcommon/migrate/__main__.py +6 -0
  61. pgcommon-0.1.0/src/pgcommon/migrate/cli.py +78 -0
  62. pgcommon-0.1.0/src/pgcommon/migrate/errors.py +12 -0
  63. pgcommon-0.1.0/src/pgcommon/migrate/runner.py +261 -0
  64. pgcommon-0.1.0/src/pgcommon/pool.py +346 -0
  65. pgcommon-0.1.0/src/pgcommon/py.typed +0 -0
  66. pgcommon-0.1.0/src/pgcommon/retry.py +121 -0
  67. pgcommon-0.1.0/src/pgcommon/tracer.py +85 -0
  68. pgcommon-0.1.0/src/pgcommon/tx.py +98 -0
  69. pgcommon-0.1.0/tests/__init__.py +0 -0
  70. pgcommon-0.1.0/tests/e2e/__init__.py +0 -0
  71. pgcommon-0.1.0/tests/e2e/conftest.py +100 -0
  72. pgcommon-0.1.0/tests/e2e/test_rls_pgbouncer_e2e.py +145 -0
  73. pgcommon-0.1.0/tests/fixtures/__init__.py +0 -0
  74. pgcommon-0.1.0/tests/fixtures/fake_asyncpg.py +146 -0
  75. pgcommon-0.1.0/tests/fixtures/fake_migrate_conn.py +66 -0
  76. pgcommon-0.1.0/tests/fixtures/fakes.py +49 -0
  77. pgcommon-0.1.0/tests/integration/__init__.py +0 -0
  78. pgcommon-0.1.0/tests/integration/conftest.py +41 -0
  79. pgcommon-0.1.0/tests/integration/test_concurrency_stress.py +98 -0
  80. pgcommon-0.1.0/tests/integration/test_error_helpers_real_violations.py +89 -0
  81. pgcommon-0.1.0/tests/integration/test_migrate_runner_integration.py +108 -0
  82. pgcommon-0.1.0/tests/integration/test_pool_lifecycle.py +89 -0
  83. pgcommon-0.1.0/tests/integration/test_retry_real_serialization_failure.py +93 -0
  84. pgcommon-0.1.0/tests/integration/test_rls_guc_injection.py +124 -0
  85. pgcommon-0.1.0/tests/integration/test_tx_and_savepoint.py +119 -0
  86. pgcommon-0.1.0/tests/unit/__init__.py +0 -0
  87. pgcommon-0.1.0/tests/unit/test_config.py +197 -0
  88. pgcommon-0.1.0/tests/unit/test_errors.py +71 -0
  89. pgcommon-0.1.0/tests/unit/test_guc.py +158 -0
  90. pgcommon-0.1.0/tests/unit/test_integrations_fastapi.py +186 -0
  91. pgcommon-0.1.0/tests/unit/test_internal.py +76 -0
  92. pgcommon-0.1.0/tests/unit/test_logger.py +54 -0
  93. pgcommon-0.1.0/tests/unit/test_metrics.py +121 -0
  94. pgcommon-0.1.0/tests/unit/test_migrate_cli.py +136 -0
  95. pgcommon-0.1.0/tests/unit/test_migrate_runner.py +365 -0
  96. pgcommon-0.1.0/tests/unit/test_pool.py +439 -0
  97. pgcommon-0.1.0/tests/unit/test_retry.py +206 -0
  98. pgcommon-0.1.0/tests/unit/test_tracer.py +107 -0
  99. pgcommon-0.1.0/tests/unit/test_tx.py +236 -0
  100. 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