Skip to content
Publishing controls

Publishing controls

PULSE drafts a great deal on its own: daily social copy, blog posts, podcast episodes, videos. Whether any of it leaves the building is decided by two layers of switches, and nothing is armed by default.

The rule

armed(channel) = PULSE_APPLY_ENABLED is exactly "true"
                 AND the channel's own setting is on
  1. PULSE_APPLY_ENABLED is the kill switch. It is off unless it is exactly the lowercase string true. When it is off, no channel can publish, whatever else is set.
  2. Each channel can then be narrowed. A channel with no setting of its own follows the kill switch. A channel setting can only turn a channel off. It can never turn one on when the kill switch is off.

The channels

Channel keyWhat it publishes
blogA Hugo post on your website, through GitHub. See Site pipeline.
podcastPodcast episodes, by adding them to the public RSS feed
audiobookThe weekly long-form book, in the same feed
youtubeReal uploads to a YouTube channel, under the operator’s Google identity
linkedinText posts on one personal LinkedIn profile
instagramNothing yet. The uploader is a stub, but the control exists so it cannot publish unguarded later.
spotifyNothing is scheduled to it today, but it has a control for the same reason

Setting a channel

A channel’s own setting comes from the first of these that holds a clear yes or no:

  1. An environment variable, PULSE_PUBLISH_<CHANNEL>, for example PULSE_PUBLISH_YOUTUBE=false. It accepts 1, t, true, y, yes, on and 0, f, false, n, no, off.
  2. A config file, at PULSE_PUBLISH_CONFIG_PATH (default /etc/runink/pulse-publishing.json), or the same JSON inline in PULSE_PUBLISH_CONFIG_JSON when there is no file.
  3. The built-in default, which is on. Combined with a kill switch that is off by default, nothing publishes until someone arms the global.

The config file accepts either shape:

[{"channel": "youtube", "armed": false}]
{"channels": [{"channel": "youtube", "armed": false}]}

A channel missing from the file has no opinion. A file that cannot be read or parsed is ignored and changes nothing in either direction.

Checking what is armed

At startup the server logs one line per channel:

PUBLISHCFG youtube: armed=false (...); channel opinion from ...; console config: ...

A refused publish logs PUBLISHCFG <channel>: DISARMED — did NOT ..., with the layer that decided.

PULSE_APPLY_ENABLED=yes, on, 1, or TRUE does not arm anything. The server warns at startup with PUBLISHCFG-AMBIGUOUS. Write exactly true.

What disarming does to queued posts

The kill switch applies at the moment of publishing, not only when a post is queued. A scheduled post that comes due on a disarmed channel is marked withheld, with the reason. That is final. Re-arming does not replay the backlog; schedule the content again to re-queue it.

Approving content does not bypass the switch either. An approval is consent to the content, and the switch is a statement about the channel.

The same switch gates server writes

PULSE_APPLY_ENABLED=true is also what allows the Site Audit’s apply modes (apply, commit, pr) to write to the server’s filesystem and run git, and what arms the daily blog drafting in the social engine.

Channel-specific safeguards

ChannelAlso needs
blogA GitHub data source with a token, and PULSE_SITE_REPO.
youtubeYOUTUBE_PRIVACY_STATUS. Unset means private, so an unreviewed upload is never public by accident.
linkedinLINKEDIN_ACCESS_TOKEN from a secret (never from the database) and LINKEDIN_AUTHOR_URN for a person. Organization pages are refused.
podcast, audiobookPULSE_PUBLIC_BASE_URL, and the episode’s audio must exist. Then submit <PULSE_PUBLIC_BASE_URL>/podcast/feed.xml once to each podcast platform.