You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The release path is the finder's: every green push to main publishes over trusted publishing with a derived version, and the wheel stops shipping the test suite
Copy file name to clipboardExpand all lines: roadmap/p1-e2-outsend-ingest-and-packaging.md
+52-11Lines changed: 52 additions & 11 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,8 +10,11 @@
10
10
`cold_outreach/first_run.py` collects the campaign's fields, the operator who signs the mail and the
11
11
mailbox itself — environment first, terminal second, one error naming every variable when headless —
12
12
and the box is stored only once its SMTP login succeeds. **Every test in the repo runs** — the five
13
-
inherited files are ported and nothing is ignored. What is left is the `openoutreach[send]` extra,
14
-
which cannot be declared until this distribution is published.
13
+
inherited files are ported and nothing is ignored. **The release path is built too**: `deploy.yml`
14
+
tests and then publishes on every green push to `main`, with the version derived from the commit
15
+
count, exactly as the finder does it. What is left is arming it — a PyPI pending publisher and the
16
+
`pypi` environment, two browser steps — and then the `openoutreach[send]` extra, which cannot be
17
+
declared until this distribution is on PyPI.
15
18
-**Priority:** High — every other card here describes code that cannot run yet.
16
19
-**Effort:** Medium
17
20
-**Area:** Packaging + ingest — the receiving end of
@@ -140,13 +143,43 @@ For anyone picking this up: what an operator can do today, and what they cannot.
140
143
|`outsend init`|**Works** — the campaign's three fields, the operator, and a mailbox, from env or a TTY; runs implicitly at first send |`cold_outreach/first_run.py`|
141
144
|**A mailbox**|**Works** — `init` calls `create_verified`, so a stored box is one whose SMTP login succeeded; four transport variables cover a box that is not on Google |`cold_outreach/first_run.py`|
142
145
|**The operator**|**Works** — the name that signs the mail and the BCC address, upserted onto the one active user `seller_full_name()` reads |`cold_outreach/core/operator.py`|
143
-
|`pip install openoutreach[send]`|**Missing** — needs `openoutsend` published first | — |
146
+
| The release path |**Built, not yet armed** — tests then publish on every green push to `main`, version derived from the commit count |`.github/workflows/deploy.yml`|
147
+
|`pip install openoutsend` from PyPI |**Waiting on the trusted publisher** — the name is free, the wheel builds, installs and ingests from a clean venv | — |
148
+
|`pip install openoutreach[send]`|**Missing** — needs `openoutsend` on PyPI first | — |
149
+
150
+
## How this gets released — the finder's rule, on this side too
151
+
152
+
**Every green push to `main` publishes.** The version is *derived at publish time*, never committed:
153
+
`pyproject.toml`'s `version` is the base (major.minor, bumped by hand) and the patch is this repo's
154
+
commit count. That is not a preference — the finder tagged `v0.1.0` by hand, twelve commits of work
155
+
landed behind it, and `uvx openoutreach` kept serving the version from before all of them. **A release
156
+
nobody has to remember cannot drift.** The cost is accepted and is the same one: `main` is the release
157
+
branch, and `needs: test` is the whole gate.
158
+
159
+
**No token exists anywhere.** Publishing is PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/)
160
+
over GitHub's OIDC, so nothing is stored in the repo, in an Actions secret, or on a laptop. The
161
+
trade-off is that the publisher is registered against a *workflow filename* and an *environment name*:
162
+
`deploy.yml` and `pypi`. **Renaming either breaks the release**, and a required reviewer on the `pypi`
163
+
environment would put every push behind a click, which is the thing this removes.
164
+
165
+
**Two things have to be done by hand, once, in a browser** — they are the only reason this is not
166
+
already published:
167
+
168
+
1.**A pending publisher on PyPI** (Your projects → Publishing → *Add a pending publisher*, since the
169
+
project does not exist yet): PyPI project `openoutsend`, owner `eracle`, repository `OpenOutSend`,
170
+
workflow `deploy.yml`, environment `pypi`. The first successful run creates the project.
171
+
2.**The `pypi` environment** in the repo's GitHub settings, with **no** required reviewer.
172
+
173
+
Until both exist, the `publish-pypi` job fails at the OIDC exchange and the `test` job still passes —
174
+
a re-run publishes once they do.
144
175
145
176
## What is next, in order
146
177
147
-
1.**Publish `openoutsend`**, then declare the extra on the finder's side and grep that nothing under
148
-
`openoutreach/` imports it.
149
-
2.**Then the cards that were waiting on all of this** — bounce detection, the inbound silent skip,
178
+
1.**Arm the release** — the two browser steps above, then a push, then `pip install openoutsend`
179
+
from a clean environment to prove the artifact rather than the build.
180
+
2.**Declare `openoutreach[send]`** on the finder's side, and grep that nothing under `openoutreach/`
181
+
imports `openoutsend` — the dependency crosses one way only.
182
+
3.**Then the cards that were waiting on all of this** — bounce detection, the inbound silent skip,
150
183
and the plays that replace the one prompt template.
151
184
152
185
## Done when
@@ -180,7 +213,14 @@ For anyone picking this up: what an operator can do today, and what they cannot.
180
213
asked and `""` an operator who declined one.)*
181
214
-[x]`pip install openoutsend` puts `outsend` on the PATH with its own default SQLite store under
182
215
`~/.openoutsend/`, and the settings module is this repo's rather than a host project's.
183
-
*(Installable and running from a checkout; publishing to PyPI is what the extra below waits on.)*
216
+
*(Proven from the built wheel in a clean venv, not just from a checkout: `outsend` on the PATH,
217
+
the store migrated on first run, a piped record stored, exit 0. The wheel excludes
218
+
`cold_outreach/tests` — an installed sender has no dev extras to run it with — and the sdist
219
+
keeps it.)*
220
+
-[ ] A push to `main` releases: tests, then a build and a trusted-publishing upload whose version
221
+
nobody has to remember. *(`deploy.yml` is written and mirrors the finder's, down to the derived
222
+
patch. **Not armed** — the PyPI pending publisher and the `pypi` environment are two browser
223
+
steps nobody has done yet, and the job fails at the OIDC exchange until they are.)*
184
224
-[x] The send path runs against this repo's own tests — there is no harness at all right now.
185
225
*(pytest-django, factories for this side's models, **268 tests green and nothing ignored**. The
186
226
five inherited files are ported: what they asserted against the finder's `disqualified` flag and
@@ -192,10 +232,11 @@ For anyone picking this up: what an operator can do today, and what they cannot.
192
232
193
233
## What the next slice is
194
234
195
-
**Publishing `openoutsend`.** Everything else on this card is done, and the extra on the finder's side
196
-
is the last thing waiting on a distribution rather than on code: `openoutreach[send]` cannot name a
197
-
package that is not on PyPI. Once it is published, declare the extra and grep that nothing under
198
-
`openoutreach/` imports `openoutsend` — the dependency crosses one way only.
235
+
**Arming the release.** Everything on this card that is code is done: the wheel builds, installs and
236
+
ingests from a clean environment, and `deploy.yml` will publish it. What is left is not code at all —
237
+
the pending publisher and the `pypi` environment, described above, which only a browser can create.
238
+
Then the extra: `openoutreach[send]` cannot name a package that is not on PyPI, and once it can, the
239
+
grep that nothing under `openoutreach/` imports `openoutsend` is what keeps the dependency one-way.
0 commit comments