Back to blog

How To Use Github Actions With IPFS
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:
- Triggers on every push to your main branch
- Builds your project (if needed)
- Uploads the content to IPFS via Pinata
- Stores the IPFS hash for easy access
- 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:
- Go to your repository on GitHub
- Click on "Settings" → "Secrets and variables" → "Actions"
- Click "New repository secret"
- Name it
PINATA_JWT
and paste your JWT token as the value - Click "Add secret"
Monitoring and Debugging
GitHub Actions provides excellent logging, but here are some tips for debugging:
- Check the Actions tab in your repository to see workflow runs
- Use the workflow_dispatch trigger to manually test deployments
- Add more console.log statements in your custom action for debugging
- Test your Pinata credentials locally before committing
Best Practices
- Pin Management: Consider implementing a strategy to unpin old versions to manage your Pinata storage limits
- Error Handling: Always handle errors gracefully and provide meaningful error messages
- Caching: Use GitHub Actions caching to speed up builds
- Security: Never commit API keys - always use GitHub secrets
- 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!