Reusable GitHub Actions workflow to deploy prebuilt static files to IPFS via Pinata.
This workflow is deploy-only: you build your app in your own job, upload the output as an artifact, then call this workflow to publish that artifact to IPFS. It does not check out or build your repo.
- Deploy a prebuilt artifact to IPFS via Pinata
- Input validation and error handling
- Automatic retry logic for network failures
- Deployment metadata and artifact uploads
- Post-deploy health checks across multiple IPFS gateways
- Job summary with access URLs and both CIDv0 and CIDv1
- Configurable timeouts and resource management
Create an organization secret in GitHub:
- Settings → Secrets and variables → Actions → New organization secret
- Name:
PINATA_JWT - Value: Your Pinata JWT token
- Repository access: Restrict to repos that need deployment
In your application repo, build the app in one job and call this workflow in a dependent job. The deploy job downloads the artifact uploaded by the build job (both jobs must be in the same workflow run).
name: Deploy to IPFS
on:
push:
branches: [dev, develop]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: 24
- run: npm ci && npm run build # use whatever toolchain you like
- uses: actions/upload-artifact@v7
with:
name: site-build
path: out # your build output directory
deploy:
needs: build
uses: <ORG>/gh-action-pinata/.github/workflows/pinata-deployment.yml@main
with:
environment: dev
project_name: my-app
build_artifact_name: site-build
build_dir: out
secrets:
PINATA_JWT: ${{ secrets.PINATA_JWT }}The build job is entirely yours — any language, package manager, or toolchain. This workflow only consumes the resulting artifact.
environment: Deployment environment name slug —^[a-zA-Z0-9_-]+$(e.g.dev,prod,staging)project_name: Project name (used for Pinata metadata)build_artifact_name: Name of the prebuilt artifact (uploaded earlier in the same run) to deploy
pinata_upload_timeout_ms: Pinata upload HTTP request timeout in milliseconds (default:300000)build_dir: Directory the artifact is extracted into and deployed from (default:"out")pinata_action_repository: Repo to source this action from (default:"gnosis/gh-action-pinata")pinata_action_ref: Git ref for pinata action repo (default:"main")health_check: Run post-deploy gateway health checks (default:true)
PINATA_JWT(required): Pinata JWT token
ipfs_hash: Deployed IPFS hash (CIDv0)pinata_url: Dedicated gateway URL for the deployment (https://gnosis.mypinata.cloud/ipfs/<hash>/)
On success the workflow writes a job summary containing:
- Primary access URL: the dedicated Pinata gateway (
gnosis.mypinata.cloud) - Alternative gateways: public IPFS gateways (
ipfs.io,dweb.link) - Both CIDs: CIDv0 (
Qm…) and CIDv1 (bafy…)
The gateway list is owned by scripts/deploy-to-pinata.sh — edit the IPFS_GATEWAYS array there to change which gateways are used and reported.
jobs:
deploy:
needs: build
uses: <ORG>/gh-action-pinata/.github/workflows/pinata-deployment.yml@main
with:
environment: prod
project_name: my-website
build_artifact_name: site-build
# Optional: increase if uploads are large/slow
pinata_upload_timeout_ms: 600000
secrets:
PINATA_JWT: ${{ secrets.PINATA_JWT }}jobs:
deploy:
needs: build
uses: <ORG>/gh-action-pinata/.github/workflows/pinata-deployment.yml@main
with:
environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }}
project_name: my-app
build_artifact_name: site-build
secrets:
PINATA_JWT: ${{ github.ref == 'refs/heads/main' && secrets.PINATA_JWT_PROD || secrets.PINATA_JWT_DEV }}- Checkout: Checks out this pinata action repo (for the uploader scripts)
- Download: Downloads the prebuilt artifact (
build_artifact_name) intobuild_dir - Validate: Validates the extracted directory exists and contains files
- Upload: Uploads the directory to Pinata via IPFS (uploader deps installed with
pnpmusing this repo’spnpm-lock.yaml) - Health check (optional): Verifies the deployment is reachable across gateways
- Artifacts: Uploads deployment metadata as artifacts
Error: Artifact directory 'out' not found after download
Solution: Confirm the build job ran upload-artifact with a name matching build_artifact_name, and that both jobs are in the same workflow run (the deploy job uses needs: on the build job).
Error: Artifact directory is empty
Solution: Ensure your build job produced files and uploaded the correct path. Check the build job logs.
Error: Authentication failed - check your PINATA_JWT token
Solution:
- Verify your
PINATA_JWTsecret is set correctly - Check token hasn't expired
- Ensure token has pinning permissions in Pinata
Error: Network error or ETIMEDOUT
Solution:
- The workflow includes automatic retries with exponential backoff
- Large uploads may take longer - check Pinata dashboard for upload status, or raise
pinata_upload_timeout_ms - Verify network connectivity from GitHub Actions
Error: Invalid or missing IPFS hash
Solution:
- Check Pinata API response in workflow logs
- Verify Pinata service status
- Check for rate limiting (429 errors)
- Secrets: Never commit
PINATA_JWTto repositories - Input validation: Project names are sanitized to prevent path traversal
- Error handling: Sensitive information is not exposed in logs
- Timeouts: All operations have reasonable timeouts to prevent hanging
- File size: Subject to Pinata API limits
- Rate limits: Subject to Pinata API rate limits (automatic retries help)