Skip to main content

CLI and GitHub Actions for posting to various social media platforms

Project description

Remote Media Support

All posting scripts in this repository support using remote media files (images, videos, etc.) by specifying an HTTP or HTTPS URL as the media file path. If the remote file is less than a configurable size limit (default: 5MB), the script will automatically download the file and use the downloaded local path for uploading to the social media platform.

  • The maximum allowed download size can be set using the MAX_DOWNLOAD_SIZE_MB environment variable. If not set, the default is 5MB.
  • This feature works for all scripts that accept media file paths (e.g., Post to X, Facebook, Instagram, Threads).
  • If the file is too large or cannot be downloaded, the script will log an error and exit.

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.

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.

๐Ÿš€ 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)
  • 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)
  • random() selects a random element from a list
  • attr(name) extracts an attribute from an object

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'}

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.25.1.tar.gz (71.4 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.25.1-py3-none-any.whl (68.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for social_media_posters-1.25.1.tar.gz
Algorithm Hash digest
SHA256 41217ccd094a8a9c4d5f283caf6b35cb0618d30ef882c073e510d4e8bdd9a6d3
MD5 5dc18acc96fd4018a3097287710a507b
BLAKE2b-256 d214c51d62497b9f75e52e02bc8d3ed936513a61be88c50ba55ea0457cfad8f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for social_media_posters-1.25.1.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.25.1-py3-none-any.whl.

File metadata

File hashes

Hashes for social_media_posters-1.25.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ede368c020c8510838c1090350ed6e470551c0e140ae1b7c40d8135a72a3a596
MD5 c4e29c26a0cbaf1ad3874f776d1313d6
BLAKE2b-256 67437cab1b8c4a4566711c538aeef604ca7959d345e6ba67463e67305d70d072

See more details on using hashes here.

Provenance

The following attestation bundles were made for social_media_posters-1.25.1-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