Back to blog

How To Use Github Actions With IPFS

How To Use Github Actions With IPFS

Justin Hunter

IPFS (InterPlanetary File System) has become the go-to solution for storing and distributing content in a content-addressable and immutable way. But manually uploading files to IPFS every time you make changes to your project can be tedious. What if you could automate this process so that every time you push code to your repository, your content gets deployed to IPFS automatically?

Today, we're going to build a GitHub Actions workflow that automatically publishes your content to IPFS using Pinata whenever you push changes to your main branch. This is perfect for static websites, documentation sites, or any content you want to distribute via IPFS.

What We'll Build

By the end of this tutorial, you'll have a GitHub Actions workflow that:

  1. Triggers on every push to your main branch
  2. Builds your project (if needed)
  3. Uploads the content to IPFS via Pinata
  4. Stores the IPFS hash for easy access
  5. Optionally updates a pinned version for consistent URLs

This approach is particularly powerful for static sites, documentation, or any content that benefits from immutability and content addressability.

Prerequisites

Before we dive in, make sure you have:

  • A GitHub repository with content you want to publish to IPFS
  • A free Pinata account (sign up at pinata.cloud)
  • Basic familiarity with GitHub Actions
  • Node.js knowledge (we'll be using the Pinata SDK)

Setting Up Your Pinata Account

First, let's get your Pinata credentials ready. Head over to app.pinata.cloud/developers/api-keys and create a new API key.

For this tutorial, you'll need an API key with Files read and write access. Once created, copy your JWT token - we'll need this for our GitHub Action.

Project Structure

Let's assume you have a project structure like this:

my-project/
├── .github/
│   └── workflows/
│       └── deploy-to-ipfs.yml
├── dist/                    # Build output directory
├── src/                     # Source files
├── package.json
└── README.md

The key file we'll be creating is .github/workflows/deploy-to-ipfs.yml - this is where our automation magic happens.

Creating the GitHub Action

Let's start by creating our workflow file:

name: Deploy to IPFS

on:
  push:
    branches: [ main ]
  workflow_dispatch:  # Allow manual triggers

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'

    - name: Install dependencies
      run: npm ci

    - name: Build project
      run: npm run build

    - name: Deploy to IPFS
      uses: ./.github/actions/deploy-ipfs
      with:
        pinata-jwt: ${{ secrets.PINATA_JWT }}
        source-dir: './dist'
        pin-name: 'My Project - ${{ github.sha }}'

Now we need to create the custom action that handles the IPFS deployment. Create the directory .github/actions/deploy-ipfs/ and add these files:

The Custom Action

First, let's create the action metadata in .github/actions/deploy-ipfs/action.yml:

name: 'Deploy to IPFS via Pinata'
description: 'Upload directory contents to IPFS using Pinata'
inputs:
  pinata-jwt:
    description: 'Pinata JWT token'
    required: true
  source-dir:
    description: 'Directory to upload to IPFS'
    required: true
    default: './dist'
  pin-name:
    description: 'Name for the pinned content'
    required: false
    default: 'GitHub Actions Deploy'
  update-existing:
    description: 'Whether to update existing pin with same name'
    required: false
    default: 'true'
outputs:
  ipfs-hash:
    description: 'The IPFS hash of the uploaded content'
  gateway-url:
    description: 'The gateway URL to access the content'
runs:
  using: 'node20'
  main: 'index.js'

Next, let's create the main logic in .github/actions/deploy-ipfs/index.js:

const core = require('@actions/core');
const fs = require('fs');
const path = require('path');
const { PinataSDK } = require('pinata');

async function run() {
  try {
    // Get inputs
    const pinataJwt = core.getInput('pinata-jwt');
    const sourceDir = core.getInput('source-dir');
    const pinName = core.getInput('pin-name');
    const updateExisting = core.getInput('update-existing') === 'true';

    // Initialize Pinata SDK
    const pinata = new PinataSDK({
      pinataJwt: pinataJwt
    });

    // Test authentication
    await pinata.testAuthentication();
    console.log('✅ Pinata authentication successful');

    // Check if source directory exists
    if (!fs.existsSync(sourceDir)) {
      throw new Error(`Source directory ${sourceDir} does not exist`);
    }

    // If updating existing, try to find and remove old version
    if (updateExisting) {
      try {
        const existingFiles = await pinata.files.public.list()
          .name(pinName)
          .limit(1);

        if (existingFiles.files.length > 0) {
          const oldFile = existingFiles.files[0];
          console.log(`🗑️ Removing old version: ${oldFile.cid}`);
          await pinata.files.public.delete([oldFile.id]);
        }
      } catch (error) {
        console.log('⚠️ Could not remove old version:', error.message);
      }
    }

    // Create a zip-like structure by reading all files
    const files = await getAllFiles(sourceDir);
    console.log(`📁 Found ${files.length} files to upload`);

    // Upload directory to IPFS
    console.log('🚀 Uploading to IPFS...');

    const result = await pinata.upload.public.fileArray(files)
      .name(pinName)
      .keyvalues({
        'deployment': 'github-actions',
        'repository': process.env.GITHUB_REPOSITORY,
        'commit': process.env.GITHUB_SHA,
        'branch': process.env.GITHUB_REF_NAME,
        'timestamp': new Date().toISOString()
      });

    console.log('✅ Upload successful!');
    console.log(`📍 IPFS Hash: ${result.cid}`);
	  // Be sure to replace yourgateway with your actual Pinata gateway name
    console.log(`🌍 Gateway URL: <https://yourgateway.mypinata.cloud/ipfs/${result.cid}`>);

    // Set outputs
    core.setOutput('ipfs-hash', result.cid);
    // Be sure to replace yourgateway with your actual Pinata gateway name
    core.setOutput('gateway-url', `https://yourgateway.mypinata.cloud/ipfs/${result.cid}`);

    // Create a summary
    await core.summary
      .addHeading('IPFS Deployment Successful! 🎉')
      .addTable([
        ['Property', 'Value'],
        ['IPFS Hash', result.cid],
        // Be sure to replace yourgateway with your actual Pinata gateway name
        ['Gateway URL', `https://yourgateway.mypinata.cloud/ipfs/${result.cid}`],
        ['Pin Name', pinName],
        ['Files Uploaded', files.length.toString()],
        ['Upload Size', formatBytes(result.size)]
      ])
      .write();

  } catch (error) {
    core.setFailed(`Action failed: ${error.message}`);
  }
}

async function getAllFiles(dirPath, arrayOfFiles = []) {
  const files = fs.readdirSync(dirPath);

  for (const file of files) {
    const fullPath = path.join(dirPath, file);

    if (fs.statSync(fullPath).isDirectory()) {
      arrayOfFiles = await getAllFiles(fullPath, arrayOfFiles);
    } else {
      // Read file and create File object
      const fileContent = fs.readFileSync(fullPath);
      const blob = new Blob([fileContent]);
      const relativePath = path.relative(process.cwd(), fullPath);

      // Create File object with correct path
      const fileObj = new File([blob], relativePath, {
        type: getMimeType(file)
      });

      arrayOfFiles.push(fileObj);
    }
  }

  return arrayOfFiles;
}

function getMimeType(filename) {
  const ext = path.extname(filename).toLowerCase();
  const mimeTypes = {
    '.html': 'text/html',
    '.css': 'text/css',
    '.js': 'application/javascript',
    '.json': 'application/json',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.gif': 'image/gif',
    '.svg': 'image/svg+xml',
    '.pdf': 'application/pdf',
    '.txt': 'text/plain',
    '.md': 'text/markdown'
  };

  return mimeTypes[ext] || 'application/octet-stream';
}

function formatBytes(bytes, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

run();

Don't forget to create a package.json file in the same directory:

{
  "name": "deploy-ipfs-action",
  "version": "1.0.0",
  "description": "GitHub Action to deploy to IPFS via Pinata",
  "main": "index.js",
  "dependencies": {
    "@actions/core": "^1.10.1",
    "pinata": "^1.0.0"
  }
}

Setting Up GitHub Secrets

For security, we need to store your Pinata JWT token as a GitHub secret:

  1. Go to your repository on GitHub
  2. Click on "Settings" → "Secrets and variables" → "Actions"
  3. Click "New repository secret"
  4. Name it PINATA_JWT and paste your JWT token as the value
  5. Click "Add secret"

Monitoring and Debugging

GitHub Actions provides excellent logging, but here are some tips for debugging:

  1. Check the Actions tab in your repository to see workflow runs
  2. Use the workflow_dispatch trigger to manually test deployments
  3. Add more console.log statements in your custom action for debugging
  4. Test your Pinata credentials locally before committing

Best Practices

  1. Pin Management: Consider implementing a strategy to unpin old versions to manage your Pinata storage limits
  2. Error Handling: Always handle errors gracefully and provide meaningful error messages
  3. Caching: Use GitHub Actions caching to speed up builds
  4. Security: Never commit API keys - always use GitHub secrets
  5. Testing: Test your workflows on feature branches before merging to main

Conclusion

Automating IPFS deployments with GitHub Actions and Pinata creates a powerful CI/CD pipeline for decentralized content distribution. Every time you push changes to your repository, your content automatically gets published to IPFS with a permanent, content-addressed hash.

This setup is particularly valuable for:

  • Static websites that need censorship resistance
  • Documentation sites for decentralized projects
  • NFT metadata and assets
  • Any content that benefits from permanent, distributed storage

The beauty of this approach is that it combines the familiar developer experience of GitHub with the permanence and distribution benefits of IPFS. Your content becomes part of the permanent web, accessible from anywhere, while your deployment process remains simple and automated.

Happy Pinning!

Subscribe to paid plan image

Share this post:

Stay up to date

Join our newsletter for the latest stories & product updates from the Pinata community.