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 was referenced Jun 25, 2026
rainer-friederich
added a commit
that referenced
this pull request
Jun 26, 2026
…2 read drift (#2699) <!-- mysticat-pr-skill --> ## 1. Abstract Bumps `@adobe/spacecat-shared-project-engine-client` from 1.1.1 to 1.2.0 and lands two Serenity/Semrush implementation adjustments tracked in the running issue: documentation of the v1/v2 read-layer split, and a guard test for the model-edit republish contract. ## 2. Reasoning 1.2.0 is `latest` on npm and moves the Semrush `aio/init_status` route from `/v1` to `/v2`, removing the `/v1` route from the generated contract — so the transport must follow or it no longer type-checks. While in this code, two adjustments from the migration-readiness review (serenity-docs risk analysis) were closed: the v1/v2 read-drift that silently misreads an unpublished draft as empty/US, and locking in that a standalone model-set edit always republishes to the live layer. ## 3. High-level overview of the changes - **Client bump 1.1.1 → 1.2.0.** The only behavioural consequence for us is `getInitStatus`, which now calls `GET /v2/workspaces/{id}/projects/{id}/aio/init_status` (the `/v1` route was removed upstream). `init_status` is an AIO-readiness boolean, not draft settings, so reading it from v2's live-view carries no draft-faithfulness concern. The other 1.2.0 changes (several response fields tightened to required; `addAiModel` 200→201; `createProjectTags` response object→array) are inert here — those consumers discard the response body and the transport's `unwrap` accepts any `response.ok`. - **v1/v2 read-drift documentation (issue item 2).** No code move — the only prompt read (`listPromptsByTags`) has no v1 variant, so it inherently reads the live (published) layer; an unpublished draft therefore reads empty (the "201-but-count-0" source). Added explaining comments on `listPromptsByTags` and `listTagsForProject`, and a new ADR recording which layer each Serenity read observes (v1 = draft settings, v2 prompts = live-only). - **Model-edit republish guard (issue item 1).** Behaviour was already correct (the standalone `PUT /serenity/models` path defaults `publish: true`; only brand-create batches its own publish). Added a guard test pinning that a `publish`/`publishMode` flag smuggled into the request body cannot suppress the republish. No customer-visible API shape change; the `/serenity/*` surface consumed by elmo is unchanged. ## 4. Required information - Jira / issue: #2687 - Implementation plan: docs/specs/2026-06-15-serenity-subworkspace-dual-mode-implementation-plan.md (transport table row updated for the init_status v1→v2 move) - ADR(s): docs/decisions/006-serenity-v1-v2-read-drift.md (new) ## 6. Additional information outside the code - Diffed the published 1.1.1 vs 1.2.0 client `paths` types to scope the bump: confirmed `aio/init_status` is the only route that moved (v1 removed, v2 added) and that prompts remain v2-only in 1.2.0 (no v1 `by_tags` variant exists), which is why issue item 2 is documentation rather than a version swap. - Verified the two 1.2.0 runtime response-shape changes are inert by reading every consumer: `addAiModel` (markets.js) and `createProjectTags` (markets-subworkspace.js) both discard the response body, and `unwrap` gates on `response.ok` rather than a literal `200`. ## 7. Test plan - (a) Ran the affected Serenity unit suites and the Serenity controller + OpenAPI-contract suites locally against the installed 1.2.0 client; the new model-edit guard test and the updated init_status assertion exercise the changed paths. - (b) **dev:** for a live (published) subworkspace brand, call the market-detail read (`GET /serenity/markets/:geoTargetId/:languageCode`) which triggers `getInitStatus`, and confirm `initialized` still resolves (now served by the v2 route). No prod/stage-specific steps — the change is an internal upstream-contract follow + docs/tests. ## 8. Deployment & merge order - #2584 — related (not a hard dependency). Issue item 1 also asks to ensure that publish-split refactor preserves the live-edit republish; #2584 is still open, so that confirmation remains open and should be re-checked when it lands. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
solaris007
pushed a commit
that referenced
this pull request
Jun 26, 2026
# [1.603.0](v1.602.1...v1.603.0) (2026-06-26) ### Bug Fixes * **llmo:** temporary cloudflareToken body/query fallback for CF token ([#2701](#2701)) ([b929f76](b929f76)), closes [#2681](#2681) [#2697](#2697) ### Features * add POST /sites/:siteId/entitlements admin endpoint ([#2665](#2665)) ([4374fc4](4374fc4)) * **serenity:** bump project-engine-client to 1.2.0 and document v1/v2 read drift ([#2699](#2699)) ([a1ea182](a1ea182)), closes [Hi#level](https://github.com/Hi/issues/level) [#2584](#2584)
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