Skip to main content

CLI and GitHub Actions for posting to various social media platforms

Project description

Social Media Posters

A collection of GitHub Actions and a unified CLI tool for posting content to various social media platforms. Post to X (Twitter), Facebook, Instagram, Threads, LinkedIn, YouTube, and Bluesky from the command line or automate with GitHub Actions.

Remote Media Support

All actions and the social CLI support using remote media files (images, videos, thumbnails, etc.) by specifying an HTTP or HTTPS URL as the media file path. If the remote file is within the configured size limit, it is automatically downloaded and uploaded from the local path.

  • Set MAX_DOWNLOAD_SIZE_MB to change the download limit (default: 5MB)
  • This applies to every action that accepts media inputs, plus the CLI's media and video options
  • If the file is too large or cannot be downloaded, the command logs an error and exits

This makes it easy to use media hosted on the internet in your automated social media posts.

Resolving Import Errors for Common Utilities

If you encounter import errors such as Import "social_media_utils" could not be resolved when running or editing the post-to-* scripts, follow these steps to ensure Python and your editor can find the common utilities:

1. Add common to PYTHONPATH

  • Create a .env file in your project root (if it doesn't exist).
  • Add the following line:
    • On Windows:
      PYTHONPATH=${PYTHONPATH};${workspaceFolder}/common
      
    • On macOS/Linux:
      PYTHONPATH=${PYTHONPATH}:${workspaceFolder}/common
      

This allows both your scripts and VS Code to resolve imports like from social_media_utils import ....

2. (Optional) VS Code Settings

You can also add the following to .vscode/settings.json to help Pylance find the common directory:

{
  "python.analysis.extraPaths": [
    "./common"
  ]
}

3. Keep the sys.path Modification in Scripts

Each script already includes:

sys.path.insert(0, str(Path(__file__).parent.parent / 'common'))

This ensures the script works when run directly.


By following these steps, you can avoid import errors and keep your code modular and reusable across all post-to-* actions.

๐Ÿš€ CLI Tool (NEW in v1.12.0)

Install and use the social CLI for quick posting from your terminal:

# Install from source
pip install -e ".[all]"

# Post to X (Twitter)
social x --post-content "Hello World! ๐ŸŒ" --dry-run

# Post to Facebook
social facebook --post-content "Check this out!" --post-link "https://example.com"

# Upload to YouTube
social youtube --video-file "video.mp4" --video-title "My Video"

# Get help
social --help

๐Ÿ“– Complete CLI Guide - Installation, setup, examples, and troubleshooting

๐ŸŽฏ Use Cases

  • CLI: Post to social media from your terminal or scripts
  • GitHub Actions: Automate social media posts in CI/CD workflows
  • Python Scripts: Direct Python API for custom integrations

Available Actions

๐Ÿฆ Post to X (Twitter)

Post content to X (formerly Twitter) using the X API v2.

  • Supports text posts with media attachments
  • Character limit: 280 characters
  • Media support: Images and videos

๐Ÿ“˜ Post to Facebook Page

Post content to Facebook Pages using the Facebook Graph API.

  • Supports text posts with media and links
  • Schedule posts with ISO 8601 or offset format ("+1d", "+2h", "+30m")
  • No strict character limit
  • Media support: Images and videos

๐Ÿ“ธ Post to Instagram

Post content to Instagram using the Instagram Graph API.

  • Requires publicly accessible media URLs
  • Caption limit: 2200 characters
  • Media support: Images and videos with strict requirements

๐Ÿงต Post to Threads

Post content to Threads using the Threads API.

  • Character limit: 500 characters
  • Supports media and link attachments
  • Media support: Images and videos via URLs

๐Ÿ’ผ Post to LinkedIn

Post content to LinkedIn using the LinkedIn API v2.

  • Character limit: 3000 characters
  • Supports text posts with media and links
  • Media support: Images (videos not currently supported)

๐Ÿ“น Post to YouTube

Upload videos and update video metadata on YouTube using the YouTube Data API v3.

  • Supports video uploads with full metadata
  • Update existing videos (title, description, tags, privacy, etc.)
  • Schedule video publishing with ISO 8601 or offset format ("+1d", "+2h", "+30m")
  • Custom thumbnails and playlist integration
  • Media support: Video files (local or remote URLs)

Quick Start

  1. Choose the social media platform(s) you want to post to
  2. Set up the required API credentials (see individual action READMEs)
  3. Store credentials as GitHub repository secrets
  4. Use the actions in your workflows

Example Workflow

For External Users

If you're using these actions from another repository, reference them with the full repository path and version:

name: Social Media Post
on:
  push:
    branches: [ main ]

jobs:
  post-to-social-media:
    runs-on: ubuntu-latest
    steps:
      - name: Post to X
        uses: geraldnguyen/social-media-posters/post-to-x@v1.15.1
        with:
          api-key: ${{ secrets.X_API_KEY }}
          api-secret: ${{ secrets.X_API_SECRET }}
          access-token: ${{ secrets.X_ACCESS_TOKEN }}
          access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }}
          content: "๐Ÿš€ New release deployed! Check out the latest features."
      
      - name: Post to Facebook Page
        uses: geraldnguyen/social-media-posters/post-to-facebook@v1.15.1
        with:
          access-token: ${{ secrets.FB_PAGE_ACCESS_TOKEN }}
          page-id: ${{ secrets.FB_PAGE_ID }}
          content: "We've just released a new version with exciting features!"
          link: ${{ github.event.head_commit.url }}

For Local Development

If you're developing or testing within this repository, use relative paths:

name: Social Media Post
on:
  push:
    branches: [ main ]

jobs:
  post-to-social-media:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Post to X
        uses: ./post-to-x
        with:
          api-key: ${{ secrets.X_API_KEY }}
          api-secret: ${{ secrets.X_API_SECRET }}
          access-token: ${{ secrets.X_ACCESS_TOKEN }}
          access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }}
          content: "๐Ÿš€ New release deployed! Check out the latest features."
      
      - name: Post to Facebook Page
        uses: ./post-to-facebook
        with:
          access-token: ${{ secrets.FB_PAGE_ACCESS_TOKEN }}
          page-id: ${{ secrets.FB_PAGE_ID }}
          content: "We've just released a new version with exciting features!"
          link: ${{ github.event.head_commit.url }}

Repository Structure

social-media-posters/
โ”œโ”€โ”€ post-to-x/              # X (Twitter) posting action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_x.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ post-to-facebook/       # Facebook Page posting action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_facebook.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ post-to-instagram/      # Instagram posting action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_instagram.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ post-to-threads/        # Threads posting action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_threads.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ post-to-linkedin/       # LinkedIn posting action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_linkedin.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ”œโ”€โ”€ test_post_to_linkedin.py
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ post-to-youtube/        # YouTube video upload action
โ”‚   โ”œโ”€โ”€ action.yml
โ”‚   โ”œโ”€โ”€ post_to_youtube.py
โ”‚   โ”œโ”€โ”€ requirements.txt
โ”‚   โ”œโ”€โ”€ test_post_to_youtube.py
โ”‚   โ””โ”€โ”€ README.md
โ”‚   โ””โ”€โ”€ README.md
โ”œโ”€โ”€ common/                 # Shared utilities
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ””โ”€โ”€ social_media_utils.py
โ”œโ”€โ”€ CHANGELOG.md
โ””โ”€โ”€ README.md

Common Features

All actions share these common features:

  • Error Handling: Comprehensive error handling with informative messages
  • Logging: Configurable logging levels (DEBUG, INFO, WARNING, ERROR), with GitHub Actions debug mode defaulting to DEBUG when LOG_LEVEL is not set
  • Validation: Input validation for content and media files
  • Outputs: Returns post ID and URL for further processing
  • Security: Secure handling of API credentials via GitHub secrets

Configuration Options

All actions support three methods for loading configuration parameters, with the following precedence (highest to lowest):

  1. Environment Variables: Set directly in the environment or workflow
  2. JSON Configuration File: Load parameters from a JSON file
  3. .env File: Load parameters from a .env file (for local development)

JSON Configuration File

You can provide all required and optional parameters in a JSON configuration file instead of setting them as environment variables. This is particularly useful for:

  • Managing multiple configurations for different environments
  • Simplifying local testing and development
  • Organizing complex parameter sets

How to use:

  1. Create a JSON file (default: input.json in the current directory) containing your configuration:
{
  "X_API_KEY": "your_api_key",
  "X_API_SECRET": "your_api_secret",
  "X_ACCESS_TOKEN": "your_access_token",
  "X_ACCESS_TOKEN_SECRET": "your_access_token_secret",
  "POST_CONTENT": "Hello from JSON config!",
  "LOG_LEVEL": "INFO",
  "DRY_RUN": "true"
}
  1. Optionally specify a custom file path using the INPUT_FILE environment variable:
INPUT_FILE=/path/to/my_config.json python post_to_x.py
  1. Or set it in your .env file:
INPUT_FILE=my_custom_config.json

Key features:

  • Environment variables always take precedence over JSON config values
  • The JSON file path can be absolute or relative to the current working directory
  • If INPUT_FILE is not specified, the script looks for input.json in the current directory
  • If the JSON file doesn't exist, the script continues without error (falling back to environment variables)
  • Automatic type conversion: JSON values are automatically converted to strings to match environment variable behavior:
    • Lists/Arrays: ["tag1", "tag2"] โ†’ "tag1,tag2"
    • Booleans: true โ†’ "true", false โ†’ "false"
    • Numbers: 42 โ†’ "42"
    • Null: null โ†’ ""

Example with automatic type conversion:

{
  "VIDEO_TAGS": ["classic", "moral", "fable"],
  "VIDEO_MADE_FOR_KIDS": false,
  "VIDEO_CATEGORY_ID": 24,
  "VIDEO_CONTAINS_SYNTHETIC_MEDIA": true
}

These values are automatically converted to strings that work with the scripts' string processing logic (e.g., split(), lower()).

Example workflow with JSON config:

jobs:
  post-with-json-config:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Create config file
        run: |
          cat > config.json << EOF
          {
            "POST_CONTENT": "Automated post from JSON config",
            "LOG_LEVEL": "DEBUG"
          }
          EOF
      
      - name: Post to X
        uses: ./post-to-x
        env:
          INPUT_FILE: config.json
          # Sensitive values still from secrets
          X_API_KEY: ${{ secrets.X_API_KEY }}
          X_API_SECRET: ${{ secrets.X_API_SECRET }}
          X_ACCESS_TOKEN: ${{ secrets.X_ACCESS_TOKEN }}
          X_ACCESS_TOKEN_SECRET: ${{ secrets.X_ACCESS_TOKEN_SECRET }}

Templated Content Helpers

All actions include a flexible templating engine that can pull values from environment variables, built-in timestamps, or remote JSON payloads referenced by CONTENT_JSON. In addition to basic lookups, you can apply pipe operations to transform list results:

Basic Operations

  • each:prefix(str) adds the specified prefix to each element (great for turning categories into hashtags).
  • join(str) concatenates all list items into a single string using the provided separator.

Case Transformation Operations

  • each:case_title() converts each element to Title Case
  • each:case_sentence() converts each element to Sentence case
  • each:case_upper() converts each element to UPPERCASE
  • each:case_lower() converts each element to lowercase
  • each:case_pascal() converts each element to PascalCase
  • each:case_kebab() converts each element to kebab-case
  • each:case_snake() converts each element to snake_case

Length Operations

  • max_length(int, suffix?) limits string length with optional suffix (e.g., "...")
  • each:max_length(int, suffix?) applies max_length to each item in a list
  • join_while(separator, max_length) joins items until maximum length is reached

Advanced Operations (v1.17.0+)

  • or(fallback) returns the left value if truthy, otherwise the fallback (supports chaining for coalesce behavior)
  • || operator provides short-circuit fallback evaluation in pipeline form
  • random() selects a random element from a list
  • attr(name) extracts an attribute from an object
  • tlnw:shorten_url() shortens a URL via TLNW (TLNW_CLIENT_ID + TLNW_CLIENT_SECRET required)

New Syntax Features (v1.17.0+)

Optional Parentheses: Function calls can omit parentheses for cleaner syntax:

# Before: @{json.genres | each:prefix('#') | join(' ')}
# Now:    @{json.genres | each:prefix '#' | join ' '}

JSON Expressions as Parameters: Use json.xxx expressions directly as function parameters:

# Use a JSON field as the prefix
@{json.items | each:prefix json.tag_prefix | join json.separator}

Coalesce with or: Chain or operations to use the first truthy (non-null, non-empty, non-blank) value:

# Use youtube_link if available, otherwise use permalink
@{json.youtube_link | or json.permalink}

# Chain multiple fallbacks
@{json.primary | or json.secondary | or json.tertiary | or 'default'}

Short-circuit fallback with || (v1.28.0+): || can be used where | is used and returns the left value immediately when truthy:

# If youtube_link is truthy, it is returned and RHS is skipped
@{json.youtube_link || json.permalink}

# RHS can be a function expression and receives the non-truthy LHS
@{json.youtube_link || or json.permalink}

TLNW URL Shortener: shorten long links inline in template expressions:

@{json.permalink | tlnw:shorten_url}

Example:

CONTENT_JSON=https://example.com/data.json | stories[RANDOM]
POST_CONTENT=Summary: @{json.description | max_length(150, '...')} Tags: @{json.genres | each:case_upper() | each:prefix('#') | join_while(' ', 100)}

If the selected story exposes a genres list of mythology, tragedy, and supernatural, the rendered content is:

Summary: This is a captivating tale of ancient gods and mortals, exploring themes of destiny, sacrifice, and the supernatural forces that govern our world... Tags: #MYTHOLOGY #TRAGEDY #SUPERNATURAL

Recent Improvements (v1.25.0)

๐Ÿงต Threads Link Attachment Reliability

Significantly improved reliability of Threads posts with link attachments:

  • Fixed Format Issue: Link attachments now sent in correct JSON object format ({"url": "..."})
  • Link Validation: All links validated for URL format before publishing
  • Pre-Check Testing: Automatic accessibility check with 5-second timeout
  • Automatic Retries: Transient errors automatically retried up to 3 times with exponential backoff
  • Better Error Handling: Clear distinction between temporary and permanent errors

Result: Reduced intermittent "invalid link attachment" errors, especially on first attempt. Retry logic improves success rate for posts with links.

See Post to Threads README for detailed documentation.

Security Best Practices

  1. Never commit API credentials to your repository
  2. Use GitHub secrets to store all sensitive information
  3. Follow the principle of least privilege for API permissions
  4. Regularly rotate access tokens and API keys
  5. Monitor API usage to detect unusual activity

GitHub Actions Best Practices

When using these actions in your workflows:

Version Pinning

  • Use specific version tags (e.g., @v1.15.1) instead of branches for stability
  • Review changelogs before upgrading to new versions
  • Test in non-production environments before updating production workflows

Action References

  • External repositories: Use geraldnguyen/social-media-posters/<action-folder>@<version>
  • Within this repository: Use relative paths like ./post-to-x (requires checkout step)

Security

  • Store credentials as repository secrets, never hardcode them
  • Use environment-specific secrets for different deployment stages
  • Limit secret access to only the workflows and jobs that need them
  • Regularly audit your secrets and rotate credentials

Workflow Optimization

  • Use conditionals to control when posts are made
  • Implement dry-run mode for testing before actual posting
  • Add error handling and notifications for failures
  • Cache dependencies when possible to speed up workflow execution

Example: Production Workflow with Best Practices

name: Production Social Media Post
on:
  release:
    types: [published]

jobs:
  post-to-social-media:
    runs-on: ubuntu-latest
    steps:
      - name: Post to X
        uses: geraldnguyen/social-media-posters/post-to-x@v1.15.1
        with:
          api-key: ${{ secrets.X_API_KEY }}
          api-secret: ${{ secrets.X_API_SECRET }}
          access-token: ${{ secrets.X_ACCESS_TOKEN }}
          access-token-secret: ${{ secrets.X_ACCESS_TOKEN_SECRET }}
          content: "๐Ÿš€ New release ${{ github.event.release.tag_name }} is now available!"
          log-level: "INFO"
        continue-on-error: true
      
      - name: Notify on failure
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: 'Social media posting failed',
              body: 'The social media post workflow failed. Please check the logs.'
            })

Prerequisites by Platform

Platform Requirements
X (Twitter) X Developer Account, App with API v2 access
Facebook Facebook App, Page Admin access, Graph API permissions
Instagram Business/Creator account, Facebook App, Graph API access
Threads Threads account, approved Threads App
LinkedIn LinkedIn Developer Account, App with Share on LinkedIn product
YouTube Google Cloud Project, YouTube Data API v3, Service Account or OAuth 2.0

Rate Limits

Each platform has different rate limits:

  • X: Varies by API endpoint and access level
  • Facebook: Varies by app usage and user activity
  • Instagram: Limited by Graph API quotas
  • Threads: Subject to Meta's API rate limits
  • LinkedIn: Subject to LinkedIn API throttling limits
  • YouTube: 10,000 API quota units per day (default), upload quotas vary by account

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Update documentation
  6. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

  • Created by Gerald Nguyen
  • Built with Python and various social media SDKs
  • Inspired by the need for automated social media posting in CI/CD workflows

Support

For issues, questions, or feature requests:

  1. Check the individual action READMEs for platform-specific issues
  2. Review the CHANGELOG for recent updates
  3. Open an issue in this repository

Disclaimer

These actions are provided as-is. Users are responsible for:

  • Complying with each platform's terms of service
  • Managing their API credentials securely
  • Respecting rate limits and usage policies
  • Ensuring content meets platform guidelines

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

social_media_posters-1.29.0.tar.gz (74.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

social_media_posters-1.29.0-py3-none-any.whl (70.9 kB view details)

Uploaded Python 3

File details

Details for the file social_media_posters-1.29.0.tar.gz.

File metadata

  • Download URL: social_media_posters-1.29.0.tar.gz
  • Upload date:
  • Size: 74.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for social_media_posters-1.29.0.tar.gz
Algorithm Hash digest
SHA256 5c67d13ceed76fed23fe47f767cacbec0cad7c830744657562d6673be975020b
MD5 340498bbd425b18d01b7324f154fc5ce
BLAKE2b-256 68f200b8de93e8bd9113ece97d42cda947ee969a990b1eda6fab52eadf0fa325

See more details on using hashes here.

Provenance

The following attestation bundles were made for social_media_posters-1.29.0.tar.gz:

Publisher: publish-to-pypi.yml on geraldnguyen/social-media-posters

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file social_media_posters-1.29.0-py3-none-any.whl.

File metadata

File hashes

Hashes for social_media_posters-1.29.0-py3-none-any.whl
Algorithm Hash digest
SHA256 008c1c6accb7b0bee4141525b6a50def4e89d1170b1c80ddab3b617ac8c57feb
MD5 11ff08191992201860ab7663c1b60f0e
BLAKE2b-256 a2ca4162b49b4cbad3578861ad104e675aeb669733f0acf1226190e4efa07faa

See more details on using hashes here.

Provenance

The following attestation bundles were made for social_media_posters-1.29.0-py3-none-any.whl:

Publisher: publish-to-pypi.yml on geraldnguyen/social-media-posters

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page