Skip to content

feat(api): publish a repository immediately - #3

Merged
sigmanor merged 3 commits into
mainfrom
feat/publish-now
Sep 2, 2026
Merged

sigmanor merged 3 commits into
mainfrom
feat/publish-now

Conversation

@sigmanor

@sigmanor sigmanor commented Sep 2, 2026

Copy link
Copy Markdown
Member

Why

A post lost after publication — a bad record deleted from the queue once it had already been sent — cannot be recovered by promoting anything: the item is gone from the queue. Promoting only moves a repository to the head of the queue, so there was no way to get a replacement post out without waiting for the message cron.

What

New POST /api/message/publish{"url": "..."} — publishes one repository now, to every integration whose api_configs row is enabled. There is deliberately no apis parameter: the enabled set is resolved server-side, because a dashboard's cached configuration must not decide what actually goes out.

RetryMessagePost and the new PublishNow now share publishManually, which differs between them in only three places — which connectors to use, when a still-unposted item may leave the queue, and the cron-history prefix:

retry publish now
connectors the requested ones every enabled one
marks posted only when every requested connector succeeded as soon as any integration accepts it (same as the cron)
on a busy mutex waits refuses with 409
already-posted item allowed (that is the point) refused with 409

A partial run is recorded in cron history as manual, so the connectors that failed are finished off with the existing "Publish again" button.

Duplicate-post guards

  • Pre-flight: the item is refused before any connector is contacted when it has already left the queue — a stale dashboard row, or a cron run that consumed it seconds earlier.
  • TryLock: publishing refuses rather than queueing behind a cron run that can hold the lock for minutes.
  • The cron now takes that same mutex, which it never did. A manual run marking its item posted mid-cron would have left the cron marking an item it never sent.

Also

PublishResult gained posted / posted_error, so "published but still in the queue" is visible instead of only logged — the bool from UpdateRepositoryPosted used to be discarded into _.

Duration warning

The request is synchronous and can take minutes: image generation retries for ~63 s, the Threads connector alone is configured with a 90 s timeout, and every integration adds its own repository lookup. Any reverse proxy in front of content-maestro needs a matching read timeout, or the browser gets a 504 while the publication keeps running. Documented in api_docs.md.

Tests

go test -race ./... — all green. 8 new tests in internal/schedule/message-publish_test.go (enabled-only fan-out and its stable order, mark-posted on partial success, nothing sent, missing url, no enabled integration, already-posted refusal, concurrent-run refusal) plus a new internal/server/api_test.go covering the error→status mapping (405/400/409/200). The four existing retry tests pass untouched, which is also what proves the cron policy did not leak into retry.

Paired with think-root/content-sentinel#15 (the dialog that drives this endpoint).

A post lost after publication - a bad record deleted from the queue once it
was already sent - cannot be recovered by promoting anything: the item is
gone from the queue. Promoting only moves a repository to the head of it, so
there was no way to get a replacement post out without waiting for the cron.

POST /api/message/publish publishes one repository right now, to every
integration whose api_configs row is enabled. The enabled set is resolved
server-side: a dashboard's cached configuration must not decide what is sent.

RetryMessagePost and the new PublishNow now share publishManually, which
differs between them in only three places - which connectors to use, when a
still-unposted item may leave the queue, and the cron-history prefix. Retry
keeps its all-or-nothing marking; publishing on demand follows the cron and
marks the item posted as soon as any integration accepts it, recording the run
as manual so the connectors that failed are finished off with the retry
endpoint.

Two guards make a duplicate post impossible: the item is refused when it has
already left the queue (a stale dashboard row, or a cron run seconds earlier),
and publishing takes the mutex with TryLock rather than queueing behind a cron
run that can hold it for minutes. The message cron now takes that same mutex,
which it never did - a manual run marking its item posted mid-cron would have
left the cron marking an item it never sent.

PublishResult gained posted/posted_error, so "published but still in the
queue" is visible instead of only logged.
Review found the pre-flight lookup mapping every failure to 500, so a typo'd
url or a row deleted since the dashboard rendered it - a routine client
mistake - was reported the same way as content-alchemist being down. That
contradicts the handler's own promise that its error statuses say why nothing
was attempted, and it pollutes 5xx alerting.

The repository package now exports ErrRepositoryNotFound and wraps both ways a
lookup can come up empty: content-alchemist answering 404, and an empty item
list. Publishing maps it to 404 and leaves 500 for a content-alchemist that
could not be reached at all.

The pre-flight lookup is deliberately kept even though the connector loop
fetches the item again a moment later: it is what turns an unpublishable
request into one clear error instead of a set of per-connector failures plus a
cron-history row for a run that could never have worked. Said so in a comment,
since the duplicate fetch reads like an oversight otherwise.

Also documents what the retry endpoint's blocking lock now means: it waits for
a cron run or a publish-now rather than refusing, the wait is unbounded, and
there is no request timeout.
Driving the endpoint against a local stack surfaced the wording: publishing a
repository with no url answered "invalid retry request: url is required", and
the dashboard shows Content Maestro's plain-text errors verbatim. The sentinel
keeps its identifier - it is what both manual endpoints reject with - but its
message no longer names one of them.
@sigmanor
sigmanor merged commit 943178a into main Sep 2, 2026
1 check passed
@sigmanor

sigmanor commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

🎉 This PR is included in version 3.9.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant