feat(serenity): split publish out of create + publish-after-populate finalize (LLMO-5492)#2584
Open
andreeastroe96 wants to merge 4 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR will trigger a minor release when merged. |
…finalize (LLMO-5492)
The onboarding fan-out published each Semrush project at create time — i.e.
published an EMPTY project. Prompts/models live only in our Postgres and were
never pushed upstream.
AC#1 — split publish out of the create path (flag-gated):
- add SERENITY_DEFER_PUBLISH env flag + isSerenityDeferPublishEnabled (default
OFF, so projects are never left unpublished until the finalize trigger lands).
- handleCreateMarket / handleCreatePrompts take a `{ publish = true }` option
(default preserves the standalone-endpoint contract).
- performSerenityFanOut passes publish:!deferPublish — drafts when the flag is on.
AC#2 — publish-after-populate mechanism:
- new finalizeSerenityProjects(): push prompts (publish deferred) + set models
per slice + publish each project exactly once. Publish is gated on population
— if prompts were requested but every push failed, publish is skipped so an
empty project never goes live. Per-slice/per-project failures are recorded and
don't abort the rest. The DRS-completion trigger that invokes it is deferred
(audit-worker/DRS, a separate repo) and must deliver the prompt payload
exactly once (prompt push is not dedup'd upstream; model sync and publish are
idempotent).
AC#3 (completion polling) is blocked on Semrush exposing a per-project status
endpoint; AC#4 (stop storing prompts in our DB) is deferred per the ticket.
Behavior-preserving in prod: the new path is inert until SERENITY_DEFER_PUBLISH
is on AND the trigger calls finalizeSerenityProjects.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…LMO-5492 AC3)
Implements AC3 (publish-completion polling), previously deferred because the
transport had no way to read a project's publish state.
- rest-transport: add getProjectStatus(ws, pid) -> GET /v1/workspaces/{ws}/
projects/{pid} (v1 default view, which echoes publish_status faithfully;
the v2/live=true view empties a never-published draft's config).
- handlers/publish-status.js (new): the reusable read/classify/poll mechanism
built on the publish_status enum (draft | publishing | initial_publish_failed
| live | live_with_unpublished_updates). classifyPublishStatus maps to
published/failed/pending; pollProjectPublished bounds the poll by attempts/
interval. The DRS/audit worker's unbounded <=900s reconcile loop consumes the
same helper; finalize uses a tiny in-Lambda bound.
- finalize: after the async publish (202) is accepted, do a bounded best-effort
confirm. Opt-in via `typeof transport.getProjectStatus === 'function'`, so the
existing 6-arg callers/tests are unchanged; new 7th `options` arg
{confirmAttempts, confirmIntervalMs}. Confirmed live -> published;
initial_publish_failed -> publishFailed (surfaced early); still draft/
publishing within budget or status unreadable -> stays published (accepted;
the worker reconciles) so an in-progress async publish is not mislabeled.
Tests: rest-transport (getProjectStatus URL), publish-status (classify + poll),
finalize (confirm outcomes). 61 passing across the three suites; lint clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
df6f629 to
372c939
Compare
…ys-defer publish Harden the Serenity onboarding publish/finalize flow against the official Semrush "AI Project Setup" sequence (serenity-docs §9-10): - Always defer publish: remove the SERENITY_DEFER_PUBLISH flag entirely. The onboarding fan-out unconditionally provisions drafts (publish: false); the single authoritative publish happens in finalize after prompts + models are pushed. Standalone POST /serenity/markets still publishes at create time. - Gate publish on models, not just prompts: finalize now publishes a project only when at least one of its slices ended with >=1 model set. Zero models -> publishSkipped/noModels (logged); all-prompt-push-failed -> publishSkipped/ noPrompts. Default-model policy is a trigger contract (documented): finalize never invents a fallback model set; the DRS-completion trigger owns body.models. - Classify the zero-quota publish failure: a 405 + text/html on the publish route (no ai.projects allocation) is a PERMANENT allocation failure. rest-transport captures content-type onto SerenityTransportError; isPublishQuotaExhausted gates it. handleCreateMarket skips the best-effort deleteProject on a quota-405 (no create->405->delete loop) and finalize records it as permanent publishFailed. - "published" means confirmed-live only: split the finalize return into published / publishPending (202 accepted but not confirmed in-budget; worker reconciles) / publishSkipped / publishFailed, so consumers never read an unconfirmed 202 as observed-live. - Fix handleUpdateModels never republishing a live project: after a successful model diff it republishes IFF the project is currently live/ live_with_unpublished_updates (best-effort), so model changes actually go live. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A BrandSemrushProject row with an empty semrushProjectId (e.g. an orphan slice left behind when create failed mid-onboarding) must be dropped from the publish loop entirely — not published, not bucketed as skipped/failed. Adds a finalize test exercising that branch, restoring 100% line/statement coverage of finalize.js so codecov/patch clears the 99.99% target. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
LLMO-5492 — T12 · push generated prompts + models to Semrush, publish-after-populate.
The onboarding fan-out (
performSerenityFanOut→handleCreateMarket) published each Semrush project at create time — i.e. published an empty project. Prompts/models only live in our Postgres (llmo_customer_config) and were never pushed upstream.Scope (this PR)
AC#1 — split publish out of the create path (flag-gated)
SERENITY_DEFER_PUBLISHenv flag +isSerenityDeferPublishEnabled(env)— default OFF, so projects are never left unpublished until the finalize trigger is live.handleCreateMarket/handleCreatePromptstake a{ publish = true }option (default preserves the standalone-endpoint contract).performSerenityFanOutpassespublish: !deferPublish— provisions drafts when the flag is on.AC#2 — publish-after-populate mechanism
src/support/serenity/handlers/finalize.js→finalizeSerenityProjects(...): push prompts (publish deferred) → set models per slice (handleUpdateModels) → publish each project exactly once.{ prompts, models, published, publishFailed }.AC#3 — publish-completion polling ✅ (now implemented)
Previously deferred as "blocked —
rest-transport.jshas nogetProjectStatus." Unblocked once serenity-docs #12 (§6) confirmed the project carries apublish_statusattribute (draft | publishing | initial_publish_failed | live | live_with_unpublished_updates).rest-transport.js— newgetProjectStatus(ws, pid)→GET /v1/workspaces/{ws}/projects/{pid}. Uses the v1 default view deliberately (it echoespublish_statusfaithfully; the v2/live=trueview empties a never-published draft's config per §10).src/support/serenity/handlers/publish-status.js(new) — the reusable read/classify/poll mechanism.classifyPublishStatusmapslive/live_with_unpublished_updates→ published,initial_publish_failed→ failed,draft/publishing/unknown → pending.pollProjectPublished(transport, ws, pid, { attempts, intervalMs, log, sleep })bounds the poll; a status-read error is non-fatal (logged, retried, reported pending). The DRS/audit worker's unbounded ≤900s reconcile loop consumes this same helper.finalize.js— after the async publish (202) is accepted, a bounded best-effort confirm within the Lambda's wall budget. Opt-in viatypeof transport.getProjectStatus === 'function', so the existing 6-arg callers/tests are unchanged; new 7thoptionsarg{ confirmAttempts, confirmIntervalMs }. Confirmed live →published;initial_publish_failed→publishFailed(surfaced early); still draft/publishing within budget or status unreadable → stayspublished(accepted on 202; the worker reconciles) so an in-progress async publish is never mislabeled as failed.Deferred / out of scope
publish-status.jshelper.Tests
handleCreateMarketpublish:false/default (2),handleCreatePromptspublish:false (1),finalizeSerenityProjects(publish-after-populate incl. empty-publish guard + partial-success, plus AC3 confirm cases: live→published, initial_publish_failed→publishFailed, publishing-within-budget→published, read-error→published, mixed),publish-status.js(read/classify + poll: first-read live, initial_publish_failed, draft→publishing→live, exhaust→pending, read-error→pending, single-attempt no-sleep),rest-transportgetProjectStatus(URL + GET + raw JSON).🤖 Generated with Claude Code