GitHub Action

Create, schedule, and publish social posts from a GitHub workflow.

fopost/fopost-github-action posts to every network connected to your workspace from a GitHub workflow. Announce a release the moment it ships, schedule a weekly digest from a file in the repository, or open a draft for a human to send. It is a bundled Node 20 JavaScript action built on the TypeScript SDK, so it starts in about a second.

This is a 0.x release. The input surface is still settling and a minor version may change it.

Published on the GitHub Marketplace. Source and issues: github.com/fopost/fopost-github-action. MIT licensed.

Install

There is nothing to install. Reference the action by tag from any workflow:

- uses: fopost/fopost-github-action@v0
  with:
    api-key: ${{ secrets.FOPOST_API_KEY }}
    accounts: bluesky:yourbrand, linkedin:yourbrand
    text: ${{ github.event.repository.name }} ${{ github.ref_name }} is out.
    publish: true

@v0 is a floating major tag moved to each release, so it always points at the newest release on that major. Pin a commit SHA instead for the strictest supply-chain posture.

The action is not listed in the GitHub Marketplace yet: that listing is a one-time manual step on a release and is still pending. Referencing the repository by tag works today either way; a Marketplace listing only changes how people discover it.

Getting an API key

Create one at Settings → API Keys in the dashboard. It needs the posts scope, plus media if you attach local files. Store it as a repository secret named FOPOST_API_KEY and reference it as ${{ secrets.FOPOST_API_KEY }}. Never paste the key into the workflow file, and never into a run: step.

The action calls core.setSecret on the key before it does anything else, so the runner masks it in every log line, including the ones this action writes.

Post on release

name: Announce release

on:
  release:
    types: [published]

jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
      - uses: fopost/fopost-github-action@v0
        with:
          api-key: ${{ secrets.FOPOST_API_KEY }}
          workspace-id: ${{ vars.FOPOST_WORKSPACE_ID }}
          accounts: |
            bluesky:yourbrand
            linkedin:yourbrand
            mastodon:yourbrand
          text: |
            ${{ github.event.repository.name }} ${{ github.event.release.tag_name }} is out.

            ${{ github.event.release.html_url }}
          labels: release
          publish: true

Post release notes from a file

text-file reads a file out of the checkout, so the copy lives in the repository and changes go through review like any other change.

steps:
  - uses: actions/checkout@v4

  - name: Extract the newest CHANGELOG section
    run: awk '/^## /{ if (seen++) exit } seen' CHANGELOG.md > release-notes.md

  - uses: fopost/fopost-github-action@v0
    with:
      api-key: ${{ secrets.FOPOST_API_KEY }}
      accounts: bluesky:yourbrand
      text-file: release-notes.md
      status: draft # a person sends it from the dashboard

Post on a schedule

on:
  schedule:
    - cron: '0 8 * * 1' # Mondays, 08:00 UTC

jobs:
  digest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: fopost/fopost-github-action@v0
        with:
          api-key: ${{ secrets.FOPOST_API_KEY }}
          accounts: bluesky:yourbrand, linkedin:yourbrand
          text-file: content/weekly-digest.md
          media: assets/digest-card.png
          schedule-at: '2026-09-07T15:00:00Z'
          fail-on-error: false # a missed digest should not fail the repo's checks

Attach media

Local paths are uploaded to the media library first; http(s) URLs are attached as they are.

with:
  media: |
    assets/card.png
    https://cdn.example.com/clip.mp4

Check a workflow without posting

dry-run validates every input, resolves the accounts, and prints what would go out, without creating, uploading, or publishing anything.

with:
  api-key: ${{ secrets.FOPOST_API_KEY }}
  accounts: bluesky:yourbrand
  text: Dress rehearsal.
  dry-run: true

Inputs

InputRequiredDefaultDescription
api-keyyesnoneFoPost API key. Always from a secret. Masked on read
workspace-idnononeWorkspace to post into. Optional when the key reaches exactly one workspace
accountsyesnoneAccounts to post to, one per line or comma separated. An account id, a username, or platform:username
textone ofnonePost body. Mutually exclusive with text-file
text-fileone ofnonePath in the checkout whose contents become the body
medianononeImages or videos. A local path is uploaded; an http(s) URL is attached as is
schedule-atnononeISO 8601 timestamp. Implies status: scheduled
statusnodraftdraft or scheduled. Defaults to scheduled when schedule-at is set
publishnofalsePublish right after creating. Delivery is queued, so success means accepted, not yet live
labelsnononeLabels to attach, one per line or comma separated
fail-on-errornotrueSet to false to log a warning and keep the step green
dry-runnofalseValidate and print without creating, uploading, or publishing

Outputs

OutputDescription
post-idId of the created post. Empty on a dry run
post-urlDashboard URL of the created post. Empty on a dry run
statusdraft, scheduled, publishing, pending_approval, dry-run, or error
delivery-countNumber of per-account deliveries queued by publish. 0 when not publishing

Every run also writes a job summary with the post's status, the accounts it went to, and a preview of the body.

Errors

Failures come back as one actionable line, never a stack trace and never a response header:

  • 401: the key is missing or revoked; check api-key.
  • 402: the plan does not cover the request; the message carries the upgrade URL.
  • 403: the key's scopes or workspace access do not reach this workspace.
  • 429: rate limited, with when to retry.
  • 5xx: transient; re-run the job.

Set fail-on-error: false to downgrade all of these to a warning, which is the right call for a scheduled digest that should not turn a repository's checks red.

Security

  • Pass api-key from ${{ secrets.FOPOST_API_KEY }}. A key committed to a workflow file is a key you have to rotate.
  • The key is masked with the runner before any other work, and the action redacts it again in every message it writes, so a key echoed back by an API error never lands in the log.
  • No response header is ever printed.
  • Workflows triggered by pull_request from a fork have no access to secrets, which is the behavior you want: a fork cannot post as you.
  • Verbose output goes to core.debug, visible only when the repository turns step debugging on.

Next

Related documentation
  • SDKs & Integrations

    Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.

  • SDKs Overview

    Every official FoPost client, what it covers, and how to pick one.

  • TypeScript SDK

    The official TypeScript and Node.js client for the FoPost API.

  • Python SDK

    The official Python client for the FoPost API.

  • PHP SDK

    The official PHP client for the FoPost API, with no framework and no HTTP library.

  • Ruby SDK

    The official Ruby client for the FoPost API, with no runtime dependencies.

  • Go SDK

    The official Go client for the FoPost API.

  • Rust SDK

    The official Rust client for the FoPost API, async and built on reqwest.

Was this helpful?

On this page