Productivity & Tools

GitHub Actions for Social Media: Auto-Post When You Ship Code

170 views
Serge Bulaev
Serge Bulaev
GitHub Actions for Social Media: Auto-Post When You Ship Code

TL;DR

Automatically announce releases on social media with GitHub Actions and Publora API. Complete workflow YAML, AI-generated changelogs, multi-platform customization, and GitLab CI adaptation.

Why Developers Should Announce Releases on Social Media

You just shipped a feature. The pull request is merged, CI/CD is green, the release tag is pushed. But outside your team's Slack channel, nobody knows. Your users don't check your changelog page. Potential users aren't monitoring your GitHub releases. The feature you spent weeks building goes unnoticed by the people who would benefit most from it.

This is the developer marketing gap. Shipping code is the hard part, but announcing it takes 30 seconds — if it's automated. By connecting your CI/CD pipeline to social media via GitHub Actions + Publora, every release, tag, or merged PR can automatically generate announcements across LinkedIn, X (Twitter), Telegram, and more. No manual posting. No forgetting. No context-switching from your IDE to a social media dashboard.

What you'll learn in this guide:

  • Why CI/CD social media automation matters for developer-led products
  • A complete .github/workflows/social-announce.yml you can copy into your repo
  • How to use Publora's API inside GitHub Actions (curl and Python)
  • Customizing announcement text per platform (X, LinkedIn, Telegram)
  • Adding AI-generated changelog summaries for AI workflow automation

The Case for Automated Release Announcements

Let's be direct: most open-source projects and developer tools are terrible at marketing their releases. The code is excellent, the documentation is thorough, but the announcement is a one-line tweet — if it happens at all.

72%

Of GitHub releases have zero social media announcements

3x

More adoption when releases are actively shared on social

30s

Time to set up automated announcements (once)

Automating this solves three problems at once:

  1. Consistency — every release gets announced, not just the ones you remember
  2. Speed — announcements go out within minutes of the release, not hours or days later
  3. Multi-platform reach — one automation posts to LinkedIn (for enterprise users), X (for the dev community), Telegram (for your community channel), and more

Architecture: GitHub Actions + Publora API

The setup is straightforward. GitHub Actions provides the trigger and execution environment; Publora provides the multi-platform social media API.

1

GitHub Event

Release published or tag pushed

2

Format Text

Extract release notes, build announcement

3

Publora API

Create post via REST API

4

Published

Live on LinkedIn, X, Telegram, etc.

Prerequisites

Publora Account

Sign up at app.publora.com. Connect your social media accounts (LinkedIn, X, Telegram, etc.) via the Connections page. Generate an API key under Settings.

GitHub Repository Secrets

Store your Publora API key as PUBLORA_API_KEY and your platform IDs as PUBLORA_PLATFORM_IDS in your repo's Settings → Secrets and variables → Actions.

The Complete Workflow: social-announce.yml

Here is a production-ready GitHub Actions workflow that you can copy directly into your repository. Save it as .github/workflows/social-announce.yml.

Version 1: Simple (curl)

This version uses plain curl to call the Publora API. It's the simplest approach — no dependencies, no setup, works immediately.

# .github/workflows/social-announce.yml
name: Announce Release on Social Media

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Build announcement text
        id: build-text
        run: |
          RELEASE_NAME="${{ github.event.release.name }}"
          RELEASE_TAG="${{ github.event.release.tag_name }}"
          RELEASE_URL="${{ github.event.release.html_url }}"
          REPO_NAME="${{ github.repository }}"

          # Extract first 3 lines of release body for summary
          RELEASE_BODY=$(echo '${{ github.event.release.body }}' | head -20 | sed 's/"/\\"/g')

          # Build the announcement
          ANNOUNCEMENT=$(cat <<'ENDOFTEXT'
          We just released ${RELEASE_NAME} (${RELEASE_TAG})!

          Key changes:
          ${RELEASE_BODY}

          Check it out: ${RELEASE_URL}

          #opensource #release #devtools
          ENDOFTEXT
          )

          # Use envsubst for variable expansion
          ANNOUNCEMENT=$(echo "$ANNOUNCEMENT" | \
            RELEASE_NAME="$RELEASE_NAME" \
            RELEASE_TAG="$RELEASE_TAG" \
            RELEASE_URL="$RELEASE_URL" \
            RELEASE_BODY="$RELEASE_BODY" \
            envsubst)

          echo "announcement<> $GITHUB_OUTPUT
          echo "$ANNOUNCEMENT" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Post to social media via Publora
        run: |
          curl -s -f -X POST \
            https://api.publora.com/api/v1/create-post \
            -H "Content-Type: application/json" \
            -H "x-publora-key: ${{ secrets.PUBLORA_API_KEY }}" \
            -d '{
              "content": ${{ toJSON(steps.build-text.outputs.announcement) }},
              "platforms": ${{ secrets.PUBLORA_PLATFORM_IDS }}
            }'
        continue-on-error: true  # Don't fail the release if posting fails

Important: continue-on-error

The continue-on-error: true on the posting step is intentional. You don't want a social media API failure to mark your release workflow as failed. The release itself succeeded — the announcement is a nice-to-have. If you want to be notified of failures, add a Slack/email notification step after it.

Version 2: Advanced (Python with platform-specific text)

This version uses a Python script that generates different announcement text for each platform — short and punchy for X, professional and detailed for LinkedIn, Markdown-formatted for Telegram.

# .github/workflows/social-announce.yml
name: Announce Release on Social Media

on:
  release:
    types: [published]
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Post as draft (true/false)'
        required: false
        default: 'false'

permissions:
  contents: read

jobs:
  announce:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: pip install requests

      - name: Post announcements
        env:
          PUBLORA_API_KEY: ${{ secrets.PUBLORA_API_KEY }}
          LINKEDIN_PLATFORM_ID: ${{ vars.LINKEDIN_PLATFORM_ID }}
          X_PLATFORM_ID: ${{ vars.X_PLATFORM_ID }}
          TELEGRAM_PLATFORM_ID: ${{ vars.TELEGRAM_PLATFORM_ID }}
          RELEASE_NAME: ${{ github.event.release.name || 'Manual trigger' }}
          RELEASE_TAG: ${{ github.event.release.tag_name || 'latest' }}
          RELEASE_URL: ${{ github.event.release.html_url || github.server_url }}
          RELEASE_BODY: ${{ github.event.release.body || 'No release notes.' }}
          REPO_NAME: ${{ github.repository }}
          DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
        run: python .github/scripts/announce_release.py
        continue-on-error: true

And the Python script at .github/scripts/announce_release.py:

#!/usr/bin/env python3
"""Post release announcements to social media via Publora API."""

import json
import os
import requests
import sys

API_KEY = os.environ["PUBLORA_API_KEY"]
BASE_URL = "https://api.publora.com/api/v1"
HEADERS = {
    "Content-Type": "application/json",
    "x-publora-key": API_KEY,
}

# Release metadata from GitHub Actions environment
release_name = os.environ.get("RELEASE_NAME", "New Release")
release_tag = os.environ.get("RELEASE_TAG", "latest")
release_url = os.environ.get("RELEASE_URL", "")
release_body = os.environ.get("RELEASE_BODY", "")
repo_name = os.environ.get("REPO_NAME", "")
dry_run = os.environ.get("DRY_RUN", "false") == "true"

# Extract first few bullet points from release notes
def summarize_changelog(body: str, max_items: int = 5) -> str:
    """Extract key changes from release notes markdown."""
    lines = body.strip().split("\n")
    changes = []
    for line in lines:
        line = line.strip()
        if line.startswith(("- ", "* ", "- [ ]", "- [x]")):
            # Clean up markdown checkboxes
            clean = line.lstrip("-*[] x").strip()
            if clean:
                changes.append(clean)
        if len(changes) >= max_items:
            break
    return "\n".join(f"- {c}" for c in changes) if changes else "See release notes for details."


changelog_summary = summarize_changelog(release_body)

# --- Platform-specific content ---

linkedin_text = f"""{release_name} ({release_tag}) is now available!

Here's what's new:
{changelog_summary}

We've been working hard on this release and we're excited to share it with you.

Check out the full release notes: {release_url}

#opensource #release #software #devtools #{repo_name.split('/')[-1].replace('-', '')}"""

x_text = f"""{release_name} ({release_tag}) is out!

{changelog_summary[:180]}

{release_url}

#opensource #{repo_name.split('/')[-1].replace('-', '')}"""

telegram_text = f"""**{release_name}** (`{release_tag}`) released!

**What's new:**
{changelog_summary}

[Full release notes]({release_url})"""

# --- Post to each platform ---

platforms = {
    "linkedin": {
        "id": os.environ.get("LINKEDIN_PLATFORM_ID"),
        "text": linkedin_text,
    },
    "x": {
        "id": os.environ.get("X_PLATFORM_ID"),
        "text": x_text,
    },
    "telegram": {
        "id": os.environ.get("TELEGRAM_PLATFORM_ID"),
        "text": telegram_text,
    },
}

results = []

for platform_name, config in platforms.items():
    if not config["id"]:
        print(f"Skipping {platform_name}: no platform ID configured")
        continue

    payload = {
        "content": config["text"],
        "platforms": [config["id"]],
    }

    if dry_run:
        payload["status"] = "draft"
        print(f"[DRY RUN] Creating draft for {platform_name}")

    try:
        resp = requests.post(
            f"{BASE_URL}/create-post",
            headers=HEADERS,
            json=payload,
            timeout=30,
        )
        resp.raise_for_status()
        data = resp.json()
        post_id = data.get("postGroupId", "unknown")
        print(f"Posted to {platform_name}: postGroupId={post_id}")
        results.append({"platform": platform_name, "success": True, "id": post_id})
    except requests.exceptions.RequestException as e:
        print(f"Failed to post to {platform_name}: {e}", file=sys.stderr)
        results.append({"platform": platform_name, "success": False, "error": str(e)})

# Summary
print(f"\n--- Results ---")
succeeded = sum(1 for r in results if r["success"])
print(f"{succeeded}/{len(results)} platforms posted successfully")

if not all(r["success"] for r in results):
    print("Some posts failed. Check the logs above for details.")
    # Don't sys.exit(1) — we don't want to fail the GitHub Action

Storing Secrets Securely

Never hardcode API keys in your workflow files. GitHub provides encrypted secrets specifically for this purpose.

Setting Up Repository Secrets

  1. Go to your repository on GitHub
  2. Click Settings → Secrets and variables → Actions
  3. Click New repository secret
  4. Add these secrets:
Secret Name Value Purpose
PUBLORA_API_KEY sk_your_api_key Authenticates API calls
PUBLORA_PLATFORM_IDS ["linkedin-abc","x-def"] JSON array of platform IDs (for simple curl version)

For the advanced Python version, use repository variables (not secrets) for platform IDs since they're not sensitive:

Variable Name Value
LINKEDIN_PLATFORM_ID linkedin-your-id
X_PLATFORM_ID x-your-id
TELEGRAM_PLATFORM_ID telegram-your-id

Security reminder

  • Never commit .env files or API keys to your repository
  • GitHub secrets are encrypted at rest and masked in logs
  • Use fine-grained API keys — Publora lets you create keys with specific scopes
  • Rotate keys periodically and revoke unused ones from the Publora dashboard

Customizing Announcement Text Per Platform

Each social platform has different norms, character limits, and audience expectations. The advanced Python script handles this, but here's the thinking behind each platform's formatting.

LinkedIn

  • Tone: Professional, detailed
  • Length: Up to 3,000 chars (aim for 800-1,300)
  • Format: Full paragraphs, bullet points for changes
  • Hashtags: 3-5 industry hashtags
  • CTA: "Check out the release notes" with link

X (Twitter)

  • Tone: Punchy, exciting
  • Length: 280 chars max per tweet
  • Format: Hook + 1-2 highlights + link
  • Hashtags: 1-2 relevant hashtags
  • Tip: Use threads for longer changelogs

Telegram

  • Tone: Direct, informative
  • Length: Up to 4,096 chars
  • Format: Markdown bold, inline links
  • Hashtags: Optional
  • Tip: Include full changelog — Telegram users expect detail

Instagram

  • Tone: Visual-first, casual
  • Length: 2,200 chars
  • Format: Requires image — generate a release card
  • Hashtags: Up to 30
  • Tip: Use a branded template image with version number

Adding AI-Generated Changelog Summaries

Release notes can be long and technical. An AI model can summarize them into a compelling social media post. This is where AI workflow automation meets CI/CD. Here's how to add an LLM step to your workflow.

      - name: Generate AI summary
        id: ai-summary
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          RELEASE_BODY: ${{ github.event.release.body }}
          RELEASE_NAME: ${{ github.event.release.name }}
        run: |
          SUMMARY=$(python3 -c "
          import json, os, requests

          resp = requests.post('https://api.openai.com/v1/chat/completions',
            headers={
              'Authorization': f'Bearer {os.environ[\"OPENAI_API_KEY\"]}',
              'Content-Type': 'application/json'
            },
            json={
              'model': 'gpt-4o-mini',
              'messages': [{
                'role': 'system',
                'content': 'You are a developer advocate. Summarize this changelog into a compelling social media announcement. Max 250 characters. Focus on user benefits, not technical details. Be enthusiastic but authentic.'
              }, {
                'role': 'user',
                'content': f'Release: {os.environ[\"RELEASE_NAME\"]}\n\nChangelog:\n{os.environ[\"RELEASE_BODY\"]}'
              }],
              'max_tokens': 100
            },
            timeout=30
          )
          print(resp.json()['choices'][0]['message']['content'])
          ")

          echo "summary<> $GITHUB_OUTPUT
          echo "$SUMMARY" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

Then reference ${{ steps.ai-summary.outputs.summary }} in your Publora API call. The AI turns technical changelogs like "refactored database connection pooling for 40% faster query execution" into social-friendly text like "Your queries just got 40% faster. Here's what we shipped this week."

Cost note

GPT-4o-mini costs about $0.15 per 1M input tokens. A typical changelog summary request costs less than $0.001 — essentially free. If you prefer, use a free model via Ollama in your CI or skip the AI step entirely and use the template-based approach from Version 1.

Alternative Triggers: Beyond Releases

The release event is the most common trigger, but GitHub Actions supports many events that can drive social media posts.

Trigger YAML Use Case
Release published on: release: types: [published] Standard release announcement
Tag pushed on: push: tags: ['v*'] Announce when version tags are pushed
Manual trigger on: workflow_dispatch Post on demand with custom inputs
Cron schedule on: schedule: cron: '0 14 * * 1' Weekly "What we shipped" digest
PR merged on: pull_request: types: [closed] Announce major features when PR merges
Issue milestone on: milestone: types: [closed] Celebrate milestone completion

Example: Weekly Digest (Cron-Triggered)

on:
  schedule:
    - cron: '0 14 * * 1'  # Every Monday at 2 PM UTC

jobs:
  weekly-digest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Full history for git log

      - name: Build weekly digest
        id: digest
        run: |
          # Get commits from the last 7 days
          SINCE=$(date -d '7 days ago' --iso-8601)
          COMMITS=$(git log --since="$SINCE" --oneline --no-merges | head -10)

          DIGEST="What we shipped this week:

          $COMMITS

          Follow along as we build in public!

          #buildinpublic #devlog #opensource"

          echo "digest<> $GITHUB_OUTPUT
          echo "$DIGEST" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Post digest via Publora
        run: |
          curl -s -f -X POST \
            https://api.publora.com/api/v1/create-post \
            -H "Content-Type: application/json" \
            -H "x-publora-key: ${{ secrets.PUBLORA_API_KEY }}" \
            -d '{
              "content": ${{ toJSON(steps.digest.outputs.digest) }},
              "platforms": ${{ secrets.PUBLORA_PLATFORM_IDS }}
            }'
        continue-on-error: true

Error Handling and Monitoring

Production workflows need to handle failures gracefully. Here are patterns for robust social media automation in CI/CD.

continue-on-error: true

The most important setting. A social media failure should never block your release pipeline. The code shipped successfully — that's what matters.

Retry with backoff

For transient errors (network timeouts, rate limits), retry 2-3 times with increasing delays. The Python script can wrap API calls in a retry loop.

Slack/email notification

Add a final step that sends a Slack message if posting failed. You'll know to post manually while debugging the automation.

Dry run support

Add workflow_dispatch with a dry_run input that creates drafts instead of publishing. Test your workflow without spamming your followers.

      # Add after the posting step
      - name: Notify on failure
        if: steps.post-social.outcome == 'failure'
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK_URL }} \
            -H "Content-Type: application/json" \
            -d '{
              "text": "Social media announcement failed for ${{ github.event.release.tag_name }}. Check the workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
            }'

Adapting for GitLab CI/CD

If your repository lives on GitLab instead of GitHub, the Publora API calls are identical — only the CI/CD syntax changes.

# .gitlab-ci.yml
announce-release:
  stage: deploy
  image: python:3.12-slim
  rules:
    - if: $CI_COMMIT_TAG =~ /^v\d+/  # Triggers on version tags
  variables:
    PUBLORA_API_KEY: $PUBLORA_API_KEY  # Set in GitLab CI/CD Variables
  script:
    - pip install requests
    - python .gitlab/scripts/announce_release.py
  allow_failure: true  # GitLab equivalent of continue-on-error

The Python script is the same as the GitHub version — just adjust environment variable names (CI_COMMIT_TAG instead of github.event.release.tag_name, etc.).

Real-World Examples: What Good Release Announcements Look Like

Here are examples of effective automated release announcements, showing the input (GitHub release) and output (social media post).

GitHub Release (input):

Title: v2.4.0 - Batch Scheduling & Telegram Markdown
Body:
- Added batch scheduling: create up to 50 posts in a single API call
- Telegram posts now support full Markdown formatting
- Fixed: timezone handling for DST transitions
- Performance: 60% faster media upload processing

LinkedIn (output):

We just released v2.4.0 - Batch Scheduling & Telegram Markdown!

Key changes:
- Batch scheduling: create up to 50 posts in a single API call
- Telegram posts now support full Markdown formatting
- Fixed timezone handling for DST transitions
- 60% faster media upload processing

Check out the full release notes: https://github.com/org/repo/releases/tag/v2.4.0

#opensource #release #devtools

X/Twitter (output):

v2.4.0 is out! Batch scheduling (50 posts/call), Telegram Markdown, and 60% faster uploads.

https://github.com/org/repo/releases/tag/v2.4.0

#opensource

Telegram (output):

v2.4.0 - Batch Scheduling & Telegram Markdown released!

What's new:
- Batch scheduling: create up to 50 posts in a single API call
- Telegram posts now support full Markdown formatting
- Fixed timezone handling for DST transitions
- 60% faster media upload processing

Full release notes

Getting Started in 5 Minutes

Here's the fastest path to automated release announcements:

  1. Create a Publora account and connect your social media accounts
  2. Generate an API key — Settings → API Keys → Create Key (docs)
  3. Add secrets to your GitHub repo — PUBLORA_API_KEY and PUBLORA_PLATFORM_IDS
  4. Copy the workflow YAML into .github/workflows/social-announce.yml
  5. Create a release on GitHub — the workflow triggers automatically

Ship code. Announce automatically.

Connect GitHub Actions to Publora and never forget to announce a release again.

Get Started Free →

Frequently Asked Questions

Can GitHub Actions post to social media automatically?

Yes. GitHub Actions can run any code when events like releases, tags, or pushes occur. By adding a workflow step that calls a social media API (like Publora), you can automatically announce releases on LinkedIn, X, Telegram, Instagram, and other platforms. The workflow YAML file lives in your repository under .github/workflows/.

Is it free to use GitHub Actions for social media posting?

GitHub Actions is free for public repositories and includes 2,000 minutes per month for private repositories on the free plan. A social media announcement workflow takes about 10-30 seconds per run, so you could run thousands of announcements per month within the free tier. Publora's API is available on the Pro plan and above.

What GitHub events can trigger social media posts?

Common triggers include: release (when you publish a GitHub Release), push with tag filter (when you push a version tag like v1.2.0), workflow_dispatch (manual trigger with inputs), schedule (cron-based posting), and pull_request with type closed. The release event is the most popular for announcement workflows.

How do I keep my Publora API key secure in GitHub Actions?

Store your API key as a GitHub Actions secret: go to your repository Settings → Secrets and variables → Actions → New repository secret. Name it PUBLORA_API_KEY. In your workflow YAML, reference it as ${{ secrets.PUBLORA_API_KEY }}. GitHub masks secrets in logs and never exposes them in workflow outputs. See the Publora authentication docs for API key management.

Can I customize the announcement text per social media platform?

Yes. You can either send the same text to all platforms in one Publora API call (using the platforms array), or make separate API calls with platform-specific text. The advanced Python workflow above shows how to generate short text for X (280 chars), detailed text for LinkedIn (1,300 chars), and Markdown-formatted text for Telegram.

How do I include the changelog or release notes in the social media post?

GitHub's release event payload includes the release body (your release notes) at github.event.release.body. You can extract key points from it using a script step (Python, Node.js, or bash) and include them in your announcement. For AI-powered summaries, pipe the changelog through an LLM API (like GPT-4o-mini) to generate a concise, social-friendly post.

Can I use this with GitLab CI/CD instead of GitHub Actions?

Yes. The Publora API calls are identical — only the CI/CD syntax differs. In GitLab CI, use a .gitlab-ci.yml file with a stage that runs curl or Python to call the Publora API. Store your API key as a GitLab CI/CD variable (Settings → CI/CD → Variables). The workflow logic is the same. See the GitLab CI/CD section above for a ready-to-use example.

What happens if the social media post fails during a GitHub Actions run?

By default, a failed API call will fail the workflow step. We recommend adding continue-on-error: true to the posting step so the release succeeds even if the announcement fails. You can also check the HTTP status code and retry, or add a Slack/email notification step that alerts you when posting fails. The release itself should never be blocked by a social media error.

Further Reading

Related Articles