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: truePost 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 dashboardPost 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 checksAttach 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.mp4Check 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: trueInputs
| Input | Required | Default | Description |
|---|---|---|---|
api-key | yes | none | FoPost API key. Always from a secret. Masked on read |
workspace-id | no | none | Workspace to post into. Optional when the key reaches exactly one workspace |
accounts | yes | none | Accounts to post to, one per line or comma separated. An account id, a username, or platform:username |
text | one of | none | Post body. Mutually exclusive with text-file |
text-file | one of | none | Path in the checkout whose contents become the body |
media | no | none | Images or videos. A local path is uploaded; an http(s) URL is attached as is |
schedule-at | no | none | ISO 8601 timestamp. Implies status: scheduled |
status | no | draft | draft or scheduled. Defaults to scheduled when schedule-at is set |
publish | no | false | Publish right after creating. Delivery is queued, so success means accepted, not yet live |
labels | no | none | Labels to attach, one per line or comma separated |
fail-on-error | no | true | Set to false to log a warning and keep the step green |
dry-run | no | false | Validate and print without creating, uploading, or publishing |
Outputs
| Output | Description |
|---|---|
post-id | Id of the created post. Empty on a dry run |
post-url | Dashboard URL of the created post. Empty on a dry run |
status | draft, scheduled, publishing, pending_approval, dry-run, or error |
delivery-count | Number 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-keyfrom${{ 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_requestfrom 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.