due-work-harness 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.
- due_work_harness-0.1.0/.gitattributes +1 -0
- due_work_harness-0.1.0/.github/workflows/ci.yml +205 -0
- due_work_harness-0.1.0/.github/workflows/pr-title.yml +34 -0
- due_work_harness-0.1.0/.github/workflows/release.yml +193 -0
- due_work_harness-0.1.0/.gitignore +9 -0
- due_work_harness-0.1.0/.python-version +1 -0
- due_work_harness-0.1.0/.release-please-manifest.json +1 -0
- due_work_harness-0.1.0/ADOPTING.md +163 -0
- due_work_harness-0.1.0/ARCHITECTURE.md +144 -0
- due_work_harness-0.1.0/CHANGELOG.md +25 -0
- due_work_harness-0.1.0/CONTRIBUTING.md +59 -0
- due_work_harness-0.1.0/LICENSE +202 -0
- due_work_harness-0.1.0/PKG-INFO +209 -0
- due_work_harness-0.1.0/README.md +175 -0
- due_work_harness-0.1.0/RELEASING.md +70 -0
- due_work_harness-0.1.0/check/action.yml +88 -0
- due_work_harness-0.1.0/demos/README.md +127 -0
- due_work_harness-0.1.0/demos/__init__.py +0 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/__init__.py +0 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/conftest.py +4 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/pyproject.toml +9 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/run_demo_process.py +58 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/test_handoff_inventory.py +24 -0
- due_work_harness-0.1.0/demos/dbos_transactional_outbox/test_transactional_outbox.py +301 -0
- due_work_harness-0.1.0/demos/fetch_upstream.py +44 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/__init__.py +0 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/conftest.py +6 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/pyproject.toml +9 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/settings.py +16 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/test_demo_django.py +368 -0
- due_work_harness-0.1.0/demos/procrastinate_demo_django/test_handoff_inventory.py +20 -0
- due_work_harness-0.1.0/demos/saleor_checkout/conftest.py +26 -0
- due_work_harness-0.1.0/demos/saleor_checkout/pyproject.toml +153 -0
- due_work_harness-0.1.0/demos/saleor_checkout/run.sh +30 -0
- due_work_harness-0.1.0/demos/saleor_checkout/test_handoff_inventory.py +23 -0
- due_work_harness-0.1.0/demos/saleor_checkout/test_order_confirmation.py +730 -0
- due_work_harness-0.1.0/docs/false-greens.md +65 -0
- due_work_harness-0.1.0/docs/what-a-green-result-means.md +202 -0
- due_work_harness-0.1.0/examples/adopter/adopter_app/__init__.py +0 -0
- due_work_harness-0.1.0/examples/adopter/adopter_app/summaries.py +26 -0
- due_work_harness-0.1.0/examples/adopter/pyproject.toml +22 -0
- due_work_harness-0.1.0/examples/adopter/tests/__init__.py +0 -0
- due_work_harness-0.1.0/examples/adopter/tests/conftest.py +4 -0
- due_work_harness-0.1.0/examples/adopter/tests/test_summaries_exemption.py +22 -0
- due_work_harness-0.1.0/examples/adopter/uv.lock +239 -0
- due_work_harness-0.1.0/pyproject.toml +89 -0
- due_work_harness-0.1.0/release-please-config.json +26 -0
- due_work_harness-0.1.0/scripts/check_core_is_framework_free.py +28 -0
- due_work_harness-0.1.0/src/due_work_harness/__init__.py +223 -0
- due_work_harness-0.1.0/src/due_work_harness/binding.py +531 -0
- due_work_harness-0.1.0/src/due_work_harness/coherence.py +118 -0
- due_work_harness-0.1.0/src/due_work_harness/contract.py +1673 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/__init__.py +53 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/cli.py +117 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/config.py +103 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/declarations.py +260 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/handoffs.py +374 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/project.py +261 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/report.py +43 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/scan.py +135 -0
- due_work_harness-0.1.0/src/due_work_harness/coverage/sites.py +147 -0
- due_work_harness-0.1.0/src/due_work_harness/crash_histories.py +477 -0
- due_work_harness-0.1.0/src/due_work_harness/evidence/__init__.py +13 -0
- due_work_harness-0.1.0/src/due_work_harness/evidence/observation.py +127 -0
- due_work_harness-0.1.0/src/due_work_harness/evidence/observation_report.py +76 -0
- due_work_harness-0.1.0/src/due_work_harness/exemptions.py +87 -0
- due_work_harness-0.1.0/src/due_work_harness/gap_probes.py +314 -0
- due_work_harness-0.1.0/src/due_work_harness/helpers.py +166 -0
- due_work_harness-0.1.0/src/due_work_harness/host.py +231 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/__init__.py +0 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/celery.py +193 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/dbos.py +100 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/__init__.py +103 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/callbacks.py +56 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/commits.py +90 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/lifecycle_references.py +514 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/lifecycle_states.py +630 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/references.py +35 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/selection.py +160 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/django/writes.py +107 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/postgres_plans.py +177 -0
- due_work_harness-0.1.0/src/due_work_harness/integrations/procrastinate.py +303 -0
- due_work_harness-0.1.0/src/due_work_harness/models.py +78 -0
- due_work_harness-0.1.0/src/due_work_harness/process_histories.py +101 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/__init__.py +27 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/automatic_recovery.py +1346 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/bounded_ownership.py +639 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/crash_ambiguity.py +291 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/durable_retention.py +119 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/eventual_convergence.py +308 -0
- due_work_harness-0.1.0/src/due_work_harness/profiles/fact_derived_obligations.py +389 -0
- due_work_harness-0.1.0/src/due_work_harness/py.typed +0 -0
- due_work_harness-0.1.0/src/due_work_harness/pytest_plugin.py +95 -0
- due_work_harness-0.1.0/src/due_work_harness/references/__init__.py +16 -0
- due_work_harness-0.1.0/src/due_work_harness/references/in_memory.py +632 -0
- due_work_harness-0.1.0/src/due_work_harness/references/in_memory_handoffs.py +290 -0
- due_work_harness-0.1.0/src/due_work_harness/safety/__init__.py +14 -0
- due_work_harness-0.1.0/src/due_work_harness/safety/bounded_retry.py +219 -0
- due_work_harness-0.1.0/src/due_work_harness/safety/replay_safe_execution.py +154 -0
- due_work_harness-0.1.0/src/due_work_harness/worker_death.py +13 -0
- due_work_harness-0.1.0/test/action.yml +63 -0
- due_work_harness-0.1.0/tests/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/automatic_recovery/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/automatic_recovery/test_plan_verdict.py +221 -0
- due_work_harness-0.1.0/tests/core/automatic_recovery/test_sweep.py +1132 -0
- due_work_harness-0.1.0/tests/core/conftest.py +15 -0
- due_work_harness-0.1.0/tests/core/contract/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/contract/conftest.py +33 -0
- due_work_harness-0.1.0/tests/core/contract/declarations.py +120 -0
- due_work_harness-0.1.0/tests/core/contract/reference_contract_cases.py +57 -0
- due_work_harness-0.1.0/tests/core/contract/test_binding.py +53 -0
- due_work_harness-0.1.0/tests/core/contract/test_contract.py +1092 -0
- due_work_harness-0.1.0/tests/core/contract/test_gap_probes.py +95 -0
- due_work_harness-0.1.0/tests/core/contract/test_generated_cases_are_marked.py +24 -0
- due_work_harness-0.1.0/tests/core/contract/test_safety_contract.py +176 -0
- due_work_harness-0.1.0/tests/core/coverage/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/coverage/builders.py +32 -0
- due_work_harness-0.1.0/tests/core/coverage/conftest.py +5 -0
- due_work_harness-0.1.0/tests/core/coverage/test_cli.py +105 -0
- due_work_harness-0.1.0/tests/core/coverage/test_due_work_verify.py +105 -0
- due_work_harness-0.1.0/tests/core/coverage/test_exempt_due_work_suite.py +86 -0
- due_work_harness-0.1.0/tests/core/coverage/test_scan_finds_sites.py +471 -0
- due_work_harness-0.1.0/tests/core/coverage/test_scan_requires_one_disposition.py +345 -0
- due_work_harness-0.1.0/tests/core/evidence/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/evidence/test_observation.py +62 -0
- due_work_harness-0.1.0/tests/core/profiles/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/profiles/test_bounded_ownership.py +451 -0
- due_work_harness-0.1.0/tests/core/profiles/test_crash_ambiguity.py +268 -0
- due_work_harness-0.1.0/tests/core/profiles/test_durable_retention.py +61 -0
- due_work_harness-0.1.0/tests/core/profiles/test_eventual_convergence.py +164 -0
- due_work_harness-0.1.0/tests/core/profiles/test_fact_derived_obligations.py +390 -0
- due_work_harness-0.1.0/tests/core/safety/__init__.py +0 -0
- due_work_harness-0.1.0/tests/core/safety/test_bounded_retry.py +170 -0
- due_work_harness-0.1.0/tests/core/safety/test_replay_safe_execution.py +109 -0
- due_work_harness-0.1.0/tests/core/test_crash_histories.py +121 -0
- due_work_harness-0.1.0/tests/core/test_models.py +28 -0
- due_work_harness-0.1.0/tests/core/test_process_histories.py +47 -0
- due_work_harness-0.1.0/tests/django/__init__.py +0 -0
- due_work_harness-0.1.0/tests/django/automatic_recovery/__init__.py +0 -0
- due_work_harness-0.1.0/tests/django/automatic_recovery/test_selection_inspector.py +261 -0
- due_work_harness-0.1.0/tests/django/automatic_recovery/test_terminal_states.py +536 -0
- due_work_harness-0.1.0/tests/django/conftest.py +26 -0
- due_work_harness-0.1.0/tests/django/contract/__init__.py +0 -0
- due_work_harness-0.1.0/tests/django/contract/test_contract_suites.py +123 -0
- due_work_harness-0.1.0/tests/django/settings.py +18 -0
- due_work_harness-0.1.0/tests/django/test_crash_histories.py +218 -0
- due_work_harness-0.1.0/tests/django/test_observed_selection.py +71 -0
- due_work_harness-0.1.0/tests_support/sample_production/__init__.py +11 -0
- due_work_harness-0.1.0/tests_support/sample_production/cache.py +11 -0
- due_work_harness-0.1.0/tests_support/sample_production/feed.py +25 -0
- due_work_harness-0.1.0/tests_support/sample_production/tasks.py +9 -0
- due_work_harness-0.1.0/uv.lock +1867 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
# Every job installs from uv.lock with --locked: a lock that no longer matches
|
|
16
|
+
# pyproject.toml fails CI instead of silently resolving something new.
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
lint:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v7
|
|
23
|
+
- uses: astral-sh/setup-uv@v7
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
- run: uv sync --locked --only-group lint
|
|
27
|
+
- run: uv run --no-sync ruff check
|
|
28
|
+
- run: uv run --no-sync ruff format --check
|
|
29
|
+
- name: actionlint
|
|
30
|
+
run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.7 -color
|
|
31
|
+
|
|
32
|
+
typecheck:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/checkout@v7
|
|
36
|
+
- uses: astral-sh/setup-uv@v7
|
|
37
|
+
with:
|
|
38
|
+
enable-cache: true
|
|
39
|
+
# The demos type-check against the upstream code they run (Saleor's in its own job).
|
|
40
|
+
- run: uv run --no-project python demos/fetch_upstream.py procrastinate dbos-demo-apps
|
|
41
|
+
- run: uv sync --locked --all-extras --group demos
|
|
42
|
+
- run: uv pip install -e demos/.upstream/procrastinate
|
|
43
|
+
- run: uv run --no-sync pyrefly check
|
|
44
|
+
|
|
45
|
+
core:
|
|
46
|
+
# The core must work with no framework installed: the package and pytest only.
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
strategy:
|
|
49
|
+
fail-fast: false
|
|
50
|
+
matrix:
|
|
51
|
+
python: ["3.12", "3.13", "3.14"]
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v7
|
|
54
|
+
- uses: astral-sh/setup-uv@v7
|
|
55
|
+
with:
|
|
56
|
+
enable-cache: true
|
|
57
|
+
python-version: ${{ matrix.python }}
|
|
58
|
+
- run: uv sync --locked --no-default-groups
|
|
59
|
+
- run: uv run --no-sync python scripts/check_core_is_framework_free.py
|
|
60
|
+
- run: uv run --no-sync pytest tests/core
|
|
61
|
+
|
|
62
|
+
django:
|
|
63
|
+
runs-on: ubuntu-latest
|
|
64
|
+
services:
|
|
65
|
+
postgres:
|
|
66
|
+
image: postgres:16
|
|
67
|
+
env:
|
|
68
|
+
POSTGRES_PASSWORD: postgres
|
|
69
|
+
ports: ["5432:5432"]
|
|
70
|
+
options: >-
|
|
71
|
+
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
|
|
72
|
+
env:
|
|
73
|
+
PGHOST: localhost
|
|
74
|
+
PGUSER: postgres
|
|
75
|
+
PGPASSWORD: postgres
|
|
76
|
+
PGPORT: "5432"
|
|
77
|
+
PGDATABASE: due_work_harness
|
|
78
|
+
steps:
|
|
79
|
+
- uses: actions/checkout@v7
|
|
80
|
+
- uses: astral-sh/setup-uv@v7
|
|
81
|
+
with:
|
|
82
|
+
enable-cache: true
|
|
83
|
+
- run: uv sync --locked --no-default-groups --extra django --extra celery
|
|
84
|
+
- run: uv run --no-sync pytest tests/django --ds=tests.django.settings
|
|
85
|
+
|
|
86
|
+
demos:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
services:
|
|
89
|
+
postgres:
|
|
90
|
+
image: postgres:16
|
|
91
|
+
env:
|
|
92
|
+
POSTGRES_PASSWORD: postgres
|
|
93
|
+
ports: ["5432:5432"]
|
|
94
|
+
options: >-
|
|
95
|
+
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
|
|
96
|
+
env:
|
|
97
|
+
PGHOST: localhost
|
|
98
|
+
PGUSER: postgres
|
|
99
|
+
PGPASSWORD: postgres
|
|
100
|
+
PGPORT: "5432"
|
|
101
|
+
steps:
|
|
102
|
+
- uses: actions/checkout@v7
|
|
103
|
+
with:
|
|
104
|
+
# The check action builds the harness from this checkout; its version comes from git tags.
|
|
105
|
+
fetch-depth: 0
|
|
106
|
+
- uses: astral-sh/setup-uv@v7
|
|
107
|
+
with:
|
|
108
|
+
enable-cache: true
|
|
109
|
+
- run: uv run --no-project python demos/fetch_upstream.py procrastinate dbos-demo-apps
|
|
110
|
+
- run: uv sync --locked --no-default-groups --extra django --group demos
|
|
111
|
+
# procrastinate's demos ship only in its source tree, pinned by fetch_upstream.py.
|
|
112
|
+
- run: uv pip install -e demos/.upstream/procrastinate
|
|
113
|
+
- name: procrastinate demo_django
|
|
114
|
+
env:
|
|
115
|
+
PGDATABASE: procrastinate_demo
|
|
116
|
+
run: uv run --no-sync pytest demos/procrastinate_demo_django --ds=demos.procrastinate_demo_django.settings -rxX
|
|
117
|
+
- name: DBOS transactional-outbox
|
|
118
|
+
run: uv run --no-sync pytest demos/dbos_transactional_outbox -p no:django -rxX
|
|
119
|
+
# Each demo is an adopter: its handoffs are checked the way an adopter's CI checks them.
|
|
120
|
+
- name: procrastinate demo_django, every handoff accounted for
|
|
121
|
+
uses: ./check
|
|
122
|
+
with:
|
|
123
|
+
working-directory: demos/procrastinate_demo_django
|
|
124
|
+
package: ${{ github.workspace }}
|
|
125
|
+
- name: DBOS transactional-outbox, every handoff accounted for
|
|
126
|
+
uses: ./check
|
|
127
|
+
with:
|
|
128
|
+
working-directory: demos/dbos_transactional_outbox
|
|
129
|
+
package: ${{ github.workspace }}
|
|
130
|
+
|
|
131
|
+
saleor:
|
|
132
|
+
# Saleor's checkout, unmodified, in Saleor's own environment built from its uv.lock.
|
|
133
|
+
runs-on: ubuntu-latest
|
|
134
|
+
services:
|
|
135
|
+
postgres:
|
|
136
|
+
image: postgres:16
|
|
137
|
+
env:
|
|
138
|
+
POSTGRES_PASSWORD: postgres
|
|
139
|
+
ports: ["5432:5432"]
|
|
140
|
+
options: >-
|
|
141
|
+
--health-cmd pg_isready --health-interval 5s --health-timeout 5s --health-retries 10
|
|
142
|
+
env:
|
|
143
|
+
DATABASE_URL: postgres://postgres:postgres@localhost:5432/saleor
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@v7
|
|
146
|
+
with:
|
|
147
|
+
# The check action builds the harness from this checkout; its version comes from git tags.
|
|
148
|
+
fetch-depth: 0
|
|
149
|
+
- uses: astral-sh/setup-uv@v7
|
|
150
|
+
with:
|
|
151
|
+
enable-cache: true
|
|
152
|
+
# Saleor's pin lives in fetch_upstream.py; its lock is fetched after this step.
|
|
153
|
+
cache-dependency-glob: |
|
|
154
|
+
uv.lock
|
|
155
|
+
demos/fetch_upstream.py
|
|
156
|
+
- run: uv run --no-project python demos/fetch_upstream.py saleor
|
|
157
|
+
- name: Type-check the demo against Saleor
|
|
158
|
+
run: demos/saleor_checkout/run.sh --typecheck
|
|
159
|
+
- name: Saleor, every handoff accounted for
|
|
160
|
+
uses: ./check
|
|
161
|
+
with:
|
|
162
|
+
working-directory: demos/saleor_checkout
|
|
163
|
+
package: ${{ github.workspace }}
|
|
164
|
+
- name: Saleor checkout
|
|
165
|
+
run: demos/saleor_checkout/run.sh -rxX --due-work-verify
|
|
166
|
+
|
|
167
|
+
build:
|
|
168
|
+
# The published wheel alone: it installs without any framework, imports, and ships py.typed.
|
|
169
|
+
runs-on: ubuntu-latest
|
|
170
|
+
steps:
|
|
171
|
+
- uses: actions/checkout@v7
|
|
172
|
+
with:
|
|
173
|
+
# The version is derived from tags, so build with the history that has them.
|
|
174
|
+
fetch-depth: 0
|
|
175
|
+
- uses: astral-sh/setup-uv@v7
|
|
176
|
+
with:
|
|
177
|
+
enable-cache: true
|
|
178
|
+
- run: uv build
|
|
179
|
+
- run: unzip -l dist/*.whl | grep -q "due_work_harness/py.typed"
|
|
180
|
+
- run: uv venv /tmp/wheel && uv pip install --python /tmp/wheel dist/*.whl
|
|
181
|
+
- run: /tmp/wheel/bin/python scripts/check_core_is_framework_free.py
|
|
182
|
+
|
|
183
|
+
actions:
|
|
184
|
+
# The published GitHub Actions, run against the example adopter the way a project would use them.
|
|
185
|
+
runs-on: ubuntu-latest
|
|
186
|
+
steps:
|
|
187
|
+
- uses: actions/checkout@v7
|
|
188
|
+
with:
|
|
189
|
+
fetch-depth: 0
|
|
190
|
+
- name: check passes on a project whose every handoff is accounted for
|
|
191
|
+
uses: ./check
|
|
192
|
+
with:
|
|
193
|
+
working-directory: examples/adopter
|
|
194
|
+
package: ${{ github.workspace }}
|
|
195
|
+
- name: test runs the project's generated suites
|
|
196
|
+
uses: ./test
|
|
197
|
+
with:
|
|
198
|
+
working-directory: examples/adopter
|
|
199
|
+
- name: check fails once a handoff loses its disposition
|
|
200
|
+
run: |
|
|
201
|
+
rm examples/adopter/tests/test_summaries_exemption.py
|
|
202
|
+
if uvx --from "$GITHUB_WORKSPACE" due-work-harness check --root examples/adopter; then
|
|
203
|
+
echo "::error::the check passed a project with an unaccounted handoff" >&2
|
|
204
|
+
exit 1
|
|
205
|
+
fi
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: PR title
|
|
2
|
+
|
|
3
|
+
# Pull requests are squash-merged with their title as the commit message, and
|
|
4
|
+
# release-please reads those commits to choose the next version. The title must
|
|
5
|
+
# therefore be a Conventional Commit: see RELEASING.md.
|
|
6
|
+
|
|
7
|
+
on:
|
|
8
|
+
pull_request_target:
|
|
9
|
+
types: [opened, edited, synchronize, reopened]
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
pull-requests: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
conventional-commit:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
# Reads only the title through the API; it never checks out the pull request's code.
|
|
19
|
+
- uses: amannn/action-semantic-pull-request@v5
|
|
20
|
+
env:
|
|
21
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
22
|
+
with:
|
|
23
|
+
types: |
|
|
24
|
+
feat
|
|
25
|
+
fix
|
|
26
|
+
perf
|
|
27
|
+
revert
|
|
28
|
+
docs
|
|
29
|
+
refactor
|
|
30
|
+
test
|
|
31
|
+
build
|
|
32
|
+
ci
|
|
33
|
+
chore
|
|
34
|
+
requireScope: false
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# On every push to main, release-please updates a release PR holding the next
|
|
4
|
+
# SemVer version and changelog, derived from Conventional Commit messages.
|
|
5
|
+
# Merging that PR tags vX.Y.Z and creates the GitHub release; this workflow
|
|
6
|
+
# then builds that exact tag and publishes it to PyPI with Trusted Publishing
|
|
7
|
+
# (OIDC, no stored token). See RELEASING.md.
|
|
8
|
+
#
|
|
9
|
+
# workflow_dispatch publishes an existing tag to TestPyPI or PyPI (a rehearsal,
|
|
10
|
+
# or a publish that failed after tagging), or, with no tag and TestPyPI, the
|
|
11
|
+
# dispatched commit's .devN version, which PyPI never receives.
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
push:
|
|
15
|
+
branches: [main]
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
inputs:
|
|
18
|
+
tag:
|
|
19
|
+
description: "Release tag to publish, for example v0.1.0. Empty with testpypi: publish this commit's dev version"
|
|
20
|
+
required: false
|
|
21
|
+
default: ""
|
|
22
|
+
index:
|
|
23
|
+
description: "Where to publish"
|
|
24
|
+
type: choice
|
|
25
|
+
options: [testpypi, pypi]
|
|
26
|
+
default: testpypi
|
|
27
|
+
|
|
28
|
+
permissions: {}
|
|
29
|
+
|
|
30
|
+
concurrency:
|
|
31
|
+
group: release
|
|
32
|
+
cancel-in-progress: false
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
release-please:
|
|
36
|
+
if: github.event_name == 'push'
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
permissions:
|
|
39
|
+
contents: write
|
|
40
|
+
pull-requests: write
|
|
41
|
+
outputs:
|
|
42
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
43
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
44
|
+
steps:
|
|
45
|
+
- id: release
|
|
46
|
+
uses: googleapis/release-please-action@v4
|
|
47
|
+
with:
|
|
48
|
+
config-file: release-please-config.json
|
|
49
|
+
manifest-file: .release-please-manifest.json
|
|
50
|
+
|
|
51
|
+
build:
|
|
52
|
+
needs: release-please
|
|
53
|
+
if: >-
|
|
54
|
+
always() && (
|
|
55
|
+
(github.event_name == 'push' && needs.release-please.result == 'success' && needs.release-please.outputs.release_created == 'true')
|
|
56
|
+
|| github.event_name == 'workflow_dispatch'
|
|
57
|
+
)
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
permissions:
|
|
60
|
+
contents: read
|
|
61
|
+
outputs:
|
|
62
|
+
tag: ${{ steps.tag.outputs.tag }}
|
|
63
|
+
version: ${{ steps.built.outputs.version }}
|
|
64
|
+
steps:
|
|
65
|
+
- id: tag
|
|
66
|
+
env:
|
|
67
|
+
TAG: ${{ needs.release-please.outputs.tag_name || inputs.tag }}
|
|
68
|
+
INDEX: ${{ inputs.index || 'pypi' }}
|
|
69
|
+
run: |
|
|
70
|
+
if [[ -z "$TAG" ]]; then
|
|
71
|
+
# Only a TestPyPI rehearsal may publish an untagged commit, as its .devN version.
|
|
72
|
+
if [[ "$INDEX" != "testpypi" ]]; then
|
|
73
|
+
echo "::error::publishing to PyPI needs a release tag" >&2
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
echo "tag=" >> "$GITHUB_OUTPUT"
|
|
77
|
+
exit 0
|
|
78
|
+
fi
|
|
79
|
+
# A release is a plain SemVer core version: vMAJOR.MINOR.PATCH.
|
|
80
|
+
if [[ ! "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
|
81
|
+
echo "::error::'$TAG' is not a release tag of the form vMAJOR.MINOR.PATCH" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
85
|
+
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
|
|
86
|
+
- uses: actions/checkout@v7
|
|
87
|
+
with:
|
|
88
|
+
ref: ${{ steps.tag.outputs.tag || github.sha }}
|
|
89
|
+
# The version is read from the tag, so the tag must be present.
|
|
90
|
+
fetch-depth: 0
|
|
91
|
+
- uses: astral-sh/setup-uv@v7
|
|
92
|
+
- run: uv build
|
|
93
|
+
- name: The built version is the tag
|
|
94
|
+
if: steps.tag.outputs.tag != ''
|
|
95
|
+
env:
|
|
96
|
+
VERSION: ${{ steps.tag.outputs.version }}
|
|
97
|
+
run: |
|
|
98
|
+
ls dist
|
|
99
|
+
test -f "dist/due_work_harness-${VERSION}-py3-none-any.whl"
|
|
100
|
+
test -f "dist/due_work_harness-${VERSION}.tar.gz"
|
|
101
|
+
- name: An untagged build is a dev release
|
|
102
|
+
if: steps.tag.outputs.tag == ''
|
|
103
|
+
run: |
|
|
104
|
+
ls dist
|
|
105
|
+
wheel=$(basename dist/*.whl)
|
|
106
|
+
[[ "$wheel" =~ ^due_work_harness-[0-9]+\.[0-9]+(\.[0-9]+)?\.dev[0-9]+-py3-none-any\.whl$ ]]
|
|
107
|
+
- id: built
|
|
108
|
+
run: |
|
|
109
|
+
wheel=$(basename dist/*.whl)
|
|
110
|
+
version=${wheel#due_work_harness-}
|
|
111
|
+
echo "version=${version%-py3-none-any.whl}" >> "$GITHUB_OUTPUT"
|
|
112
|
+
- name: The wheel alone installs, imports without any framework, and is typed
|
|
113
|
+
run: |
|
|
114
|
+
unzip -l dist/*.whl | grep -q "due_work_harness/py.typed"
|
|
115
|
+
uv venv /tmp/wheel && uv pip install --python /tmp/wheel dist/*.whl
|
|
116
|
+
/tmp/wheel/bin/python scripts/check_core_is_framework_free.py
|
|
117
|
+
- uses: actions/upload-artifact@v7
|
|
118
|
+
with:
|
|
119
|
+
name: dist
|
|
120
|
+
path: dist/
|
|
121
|
+
if-no-files-found: error
|
|
122
|
+
|
|
123
|
+
publish-pypi:
|
|
124
|
+
needs: build
|
|
125
|
+
if: >-
|
|
126
|
+
always() && needs.build.result == 'success'
|
|
127
|
+
&& (github.event_name == 'push' || inputs.index == 'pypi')
|
|
128
|
+
runs-on: ubuntu-latest
|
|
129
|
+
environment:
|
|
130
|
+
name: pypi
|
|
131
|
+
url: https://pypi.org/project/due-work-harness/${{ needs.build.outputs.version }}/
|
|
132
|
+
permissions:
|
|
133
|
+
id-token: write
|
|
134
|
+
steps:
|
|
135
|
+
- uses: actions/download-artifact@v8
|
|
136
|
+
with:
|
|
137
|
+
name: dist
|
|
138
|
+
path: dist/
|
|
139
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
140
|
+
|
|
141
|
+
publish-testpypi:
|
|
142
|
+
needs: build
|
|
143
|
+
if: >-
|
|
144
|
+
always() && needs.build.result == 'success'
|
|
145
|
+
&& github.event_name == 'workflow_dispatch' && inputs.index == 'testpypi'
|
|
146
|
+
runs-on: ubuntu-latest
|
|
147
|
+
environment:
|
|
148
|
+
name: testpypi
|
|
149
|
+
url: https://test.pypi.org/project/due-work-harness/${{ needs.build.outputs.version }}/
|
|
150
|
+
permissions:
|
|
151
|
+
id-token: write
|
|
152
|
+
steps:
|
|
153
|
+
- uses: actions/download-artifact@v8
|
|
154
|
+
with:
|
|
155
|
+
name: dist
|
|
156
|
+
path: dist/
|
|
157
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
158
|
+
with:
|
|
159
|
+
repository-url: https://test.pypi.org/legacy/
|
|
160
|
+
|
|
161
|
+
attach-to-release:
|
|
162
|
+
needs: [build, publish-pypi]
|
|
163
|
+
if: always() && needs.publish-pypi.result == 'success' && needs.build.outputs.tag != ''
|
|
164
|
+
runs-on: ubuntu-latest
|
|
165
|
+
permissions:
|
|
166
|
+
contents: write
|
|
167
|
+
steps:
|
|
168
|
+
- uses: actions/download-artifact@v8
|
|
169
|
+
with:
|
|
170
|
+
name: dist
|
|
171
|
+
path: dist/
|
|
172
|
+
- env:
|
|
173
|
+
GH_TOKEN: ${{ github.token }}
|
|
174
|
+
TAG: ${{ needs.build.outputs.tag }}
|
|
175
|
+
run: gh release upload "$TAG" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|
|
176
|
+
|
|
177
|
+
move-major-tag:
|
|
178
|
+
# `uses: gigaverse-app/due-work-harness/check@v0` follows the latest v0.x.y release.
|
|
179
|
+
needs: [build, publish-pypi]
|
|
180
|
+
if: always() && needs.publish-pypi.result == 'success' && needs.build.outputs.tag != ''
|
|
181
|
+
runs-on: ubuntu-latest
|
|
182
|
+
permissions:
|
|
183
|
+
contents: write
|
|
184
|
+
steps:
|
|
185
|
+
- uses: actions/checkout@v7
|
|
186
|
+
with:
|
|
187
|
+
ref: ${{ needs.build.outputs.tag }}
|
|
188
|
+
- env:
|
|
189
|
+
TAG: ${{ needs.build.outputs.tag }}
|
|
190
|
+
run: |
|
|
191
|
+
MAJOR="${TAG%%.*}"
|
|
192
|
+
git tag --force "$MAJOR" "$TAG"
|
|
193
|
+
git push --force origin "refs/tags/$MAJOR"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{".":"0.1.0"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Adopting due-work-harness
|
|
2
|
+
|
|
3
|
+
This is the path from "we use Django/Celery, Procrastinate or DBOS" to "CI fails
|
|
4
|
+
when background work can be lost or repeated". A complete, minimal adopter lives
|
|
5
|
+
in [`examples/adopter/`](examples/adopter/); the harness's own CI runs it through
|
|
6
|
+
the GitHub Actions below.
|
|
7
|
+
|
|
8
|
+
## 1. Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
uv add --dev "due-work-harness[django]" # or [celery], [procrastinate], [dbos]; combine as needed
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The core needs only pytest and pydantic. An extra adds that framework's integration.
|
|
15
|
+
|
|
16
|
+
## 2. Configure the scan
|
|
17
|
+
|
|
18
|
+
```toml
|
|
19
|
+
# pyproject.toml
|
|
20
|
+
[tool.due-work-harness]
|
|
21
|
+
production-packages = ["myapp"] # the code that must be accounted for
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
A kind is scanned whenever production code imports its framework, directly or
|
|
25
|
+
through another module of the project, so the result is the same wherever the
|
|
26
|
+
check runs, framework installed or not:
|
|
27
|
+
|
|
28
|
+
| Kind | A site is |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `django` | `transaction.on_commit(...)`, including through `sync_to_async` |
|
|
31
|
+
| `celery` | `.delay(...)` / `.apply_async(...)` and their `_on_commit` variants on a `@shared_task` / `@app.task`, and `send_task(...)` |
|
|
32
|
+
| `procrastinate` | `.defer(...)` / `.defer_async(...)` on an `@app.task`, including after `.configure(...)` |
|
|
33
|
+
| `dbos` | `DBOS.start_workflow(...)`, and `queue.enqueue(workflow, ...)` for a `@DBOS.workflow` |
|
|
34
|
+
| `dramatiq` | `.send(...)` / `.send_with_options(...)` on an `@actor` |
|
|
35
|
+
| `rq` | `enqueue`, `enqueue_call`, `enqueue_at`, `enqueue_in` on `rq`/`django_rq` or a queue they returned; `.delay(...)` on an `@job` |
|
|
36
|
+
| `django-tasks` | `.enqueue(...)` / `.aenqueue(...)` on a `django.tasks` (or `django_tasks`) `@task` |
|
|
37
|
+
|
|
38
|
+
A site is any reference to the handoff, called or not, through any alias: a
|
|
39
|
+
local name, a parameter default, `self.hook`, a re-export, `sync_to_async(...)`
|
|
40
|
+
or `functools.partial(...)` all count, in the outermost function that contains
|
|
41
|
+
them. `sites = ["celery"]` adds a kind the scan cannot detect (a framework
|
|
42
|
+
reached only through a third-party wrapper); it never removes a detected one.
|
|
43
|
+
A project helper that hands work off for its callers (Zulip's
|
|
44
|
+
`send_event_on_commit`, say) is declared in `bridges` with its own site count, so
|
|
45
|
+
each call to it becomes a site in its caller. `exclude` adds name patterns to
|
|
46
|
+
skip, and may never hide production code. See
|
|
47
|
+
[`coverage/config.py`](src/due_work_harness/coverage/config.py) for every key.
|
|
48
|
+
|
|
49
|
+
## 3. See what you have, and baseline the past
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv run due-work-harness sites # every site, and its disposition
|
|
53
|
+
uv run due-work-harness baseline # a [tool.due-work-harness.baseline] table for today's sites
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Paste the baseline into `pyproject.toml` to adopt without fixing everything at
|
|
57
|
+
once. The baseline records work that predates adoption and **only shrinks**: in
|
|
58
|
+
CI, `check --base-ref <base>` refuses any entry a change adds.
|
|
59
|
+
|
|
60
|
+
## 4. Configure the host for your tests
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
# conftest.py
|
|
64
|
+
from due_work_harness import configure
|
|
65
|
+
from due_work_harness.integrations.django import django_host
|
|
66
|
+
|
|
67
|
+
configure(django_host(production_packages={"myapp"}))
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The host tells the framework-free proofs what only your framework knows: how
|
|
71
|
+
to reach the database, count commits and freeze time.
|
|
72
|
+
|
|
73
|
+
If your code publishes through Celery, let crash histories refuse each publish
|
|
74
|
+
as a broker that is down would:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from due_work_harness.integrations.celery import celery_publication_breaker
|
|
78
|
+
|
|
79
|
+
configure(django_host(production_packages={"myapp"}, publication_breaker=celery_publication_breaker))
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 5. Give each site a disposition
|
|
83
|
+
|
|
84
|
+
**Cover it with a contract**: the proofs that it survives lost messages and
|
|
85
|
+
dead workers. Name the exact function the contract insures:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
@due_work_contract_suite(ORDER_NOTIFICATIONS, covers=(DueWorkSource(OrderService.place),))
|
|
89
|
+
class TestOrderNotificationsDueWork:
|
|
90
|
+
pass
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Start small: a `HandoffHistory` with `assert_crash_at_every_commit_converges` is
|
|
94
|
+
often the first proof worth having. See [the README](README.md) and
|
|
95
|
+
[what a green result means](docs/what-a-green-result-means.md).
|
|
96
|
+
|
|
97
|
+
**Or exempt it, with proof**, when losing the handoff genuinely costs nothing:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
@exempt_due_work_suite(
|
|
101
|
+
DueWorkSource(summaries.rename_order),
|
|
102
|
+
reason="the read path rebuilds a summary that disagrees with its order",
|
|
103
|
+
prove=LossIsAbsorbedElsewhere(strand=..., observe=..., absorb=summaries.summary),
|
|
104
|
+
)
|
|
105
|
+
class TestSummaryEvictionExemption:
|
|
106
|
+
pass
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The proof runs as a test. An exemption without one is refused, and so is a
|
|
110
|
+
proof written in the test itself (a lambda, a local function): it must be a
|
|
111
|
+
harness probe such as `LossIsAbsorbedElsewhere`, whose `absorb` is the
|
|
112
|
+
production path the reason names.
|
|
113
|
+
|
|
114
|
+
Each call then removes its function from the baseline.
|
|
115
|
+
|
|
116
|
+
## 6. Enforce it in CI
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
jobs:
|
|
120
|
+
due-work-check:
|
|
121
|
+
# Static: no database, no framework, seconds. Pins the harness version from your uv.lock.
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
steps:
|
|
124
|
+
- uses: actions/checkout@v7
|
|
125
|
+
- uses: gigaverse-app/due-work-harness/check@v0
|
|
126
|
+
|
|
127
|
+
due-work-suites:
|
|
128
|
+
# The generated contract, safety and exemption suites, in your own environment.
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
services:
|
|
131
|
+
postgres:
|
|
132
|
+
image: postgres:16
|
|
133
|
+
env: { POSTGRES_PASSWORD: postgres }
|
|
134
|
+
ports: ["5432:5432"]
|
|
135
|
+
steps:
|
|
136
|
+
- uses: actions/checkout@v7
|
|
137
|
+
- uses: gigaverse-app/due-work-harness/test@v0
|
|
138
|
+
with:
|
|
139
|
+
sync-args: --all-extras
|
|
140
|
+
pytest-args: --ds=myproject.settings
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Without GitHub Actions, the same two steps are:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
uv run due-work-harness check --base-ref origin/main
|
|
147
|
+
uv run pytest -m due_work
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Every case the harness generates carries the `due_work` mark, so `-m due_work`
|
|
151
|
+
selects exactly the adoption suites. The `test` action also passes
|
|
152
|
+
`--due-work-verify`, which fails the session unless every declaration the check
|
|
153
|
+
counted ran a case: a suite skipped by an `importorskip`, deselected or never
|
|
154
|
+
collected cannot keep its function accounted for. Add it to the plain command
|
|
155
|
+
too:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
uv run pytest -m due_work --due-work-verify
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A declaration the check counts is one pytest will run as written: a
|
|
162
|
+
module-level `Test*` class, not rebound later in its module, with no skip or
|
|
163
|
+
xfail mark, using the harness's own decorator, `DueWorkSource` and contract.
|