Skip to content
Pinner.xyz

Deploy with GitHub Actions

Deploy your static site to IPFS via Pinner automatically every time you push to your repository, using the lumeweb/pinner-deploy-action GitHub Action.

Set up secrets

Add your Pinner API key as a repository secret:

  1. Go to Settings → Secrets and variables → Actions
  2. Click New repository secret
  3. Name: PINNER_AUTH_TOKEN
  4. Value: your API key from API Keys

Create the workflow

Create .github/workflows/deploy.yml:

name: Deploy to Pinner
 
on:
  push:
    branches: [main]
 
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build
        run: npm ci && npm run build
 
      - name: Deploy to Pinner
        uses: lumeweb/pinner-deploy-action@v0
        with:
          api-key: ${{ secrets.PINNER_AUTH_TOKEN }}
          path: ./dist

This workflow builds your site and deploys the ./dist directory to IPFS via Pinner in a single step. The action outputs the content CID and a gateway URL, which you can use in subsequent steps.

Upload and set up a domain

To automatically point a domain at the deployed content, add the domain input. The action will create or update the website for you:

- name: Deploy to Pinner
  uses: lumeweb/pinner-deploy-action@v0
  with:
    api-key: ${{ secrets.PINNER_AUTH_TOKEN }}
    path: ./dist
    domain: myapp.example.com

When domain is set, the action looks up an existing website for that domain and updates its target_hash, or creates a new website if none exists.

Deploy with IPNS for continuous updates

If your site updates frequently, use IPNS so the domain always resolves to the latest content. Pass an ipns-key (a key name or numeric ID) to publish the CID under that key:

- name: Deploy to Pinner
  uses: lumeweb/pinner-deploy-action@v0
  with:
    api-key: ${{ secrets.PINNER_AUTH_TOKEN }}
    path: ./dist
    ipns-key: my-app
    domain: myapp.example.com

The action resolves the key name to its numeric ID, publishes the new CID to IPNS, and updates the website's domain, all in one step.

Clean up previous pins

Set remove-previous: true to automatically unpin the old CID after a successful deploy. This requires either ipns-key or domain so the action can identify the previous CID:

- name: Deploy to Pinner
  uses: lumeweb/pinner-deploy-action@v0
  with:
    api-key: ${{ secrets.PINNER_AUTH_TOKEN }}
    path: ./dist
    ipns-key: my-app
    domain: myapp.example.com
    remove-previous: true

Pin an existing CID

If you already have a CID (for example, from a previous build step), you can skip the upload and just pin it:

- name: Pin existing CID
  uses: lumeweb/pinner-deploy-action@v0
  with:
    api-key: ${{ secrets.PINNER_AUTH_TOKEN }}
    cid: QmExampleCID...

Either path or cid must be provided.

Action inputs

InputRequiredDefaultDescription
api-keyYesN/APinner API key
pathNoN/ALocal directory or file to upload to IPFS
cidNoN/AExisting CID to pin (skip upload)
endpointNohttps://ipfs.pinner.xyzPinner API endpoint
ipns-keyNoN/AIPNS key name or ID to publish under
domainNoN/ADomain for gateway website setup
remove-previousNofalseRemove previous pin after deploy. Requires ipns-key or domain

Either path or cid must be provided.

Action outputs

OutputDescription
cidCID of the uploaded/pinned content
gateway-urlGateway URL for the content
ipns-nameIPNS name if ipns-key was set
website-idWebsite ID if domain was set

Deploy with the SDK

If you prefer programmatic control, you can call the @lumeweb/pinner SDK directly from a custom script:

- name: Deploy with SDK
  env:
    PINNER_AUTH_TOKEN: ${{ secrets.PINNER_AUTH_TOKEN }}
  run: node scripts/deploy.mjs
// scripts/deploy.mjs
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
import path from "path";
 
const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN });
 
// Upload a directory
const files: File[] = [];
for (const entry of fs.readdirSync("./dist", { recursive: true })) {
  const fullPath = path.join("./dist", entry.toString());
  if (!fs.statSync(fullPath).isFile()) continue;
  files.push(new File([fs.readFileSync(fullPath)], entry.toString()));
}
const operation = await pinner.uploadDirectory(files, { name: "my-site" });
const result = await operation.result;
console.log("CID:", result.cid);
 
// Update existing website or create a new one
const siteId = Number(process.env.WEBSITE_ID);
if (siteId) {
  await pinner.websites.updateWebsite(siteId, {
    domain: "mysite.example.com",
    target_hash: result.cid,
    target_type: "ipfs",
  });
  console.log("Updated website", siteId);
} else {
  const site = await pinner.websites.createWebsite({
    domain: "mysite.example.com",
    target_hash: result.cid,
    target_type: "ipfs",
  });
  console.log("Created website", site.id);
}

Publish to IPNS with the SDK

// scripts/deploy-ipns.mjs
import { Pinner } from "@lumeweb/pinner";
import fs from "fs";
import path from "path";
 
const pinner = new Pinner({ jwt: process.env.PINNER_AUTH_TOKEN });
 
// Upload a directory
const files: File[] = [];
for (const entry of fs.readdirSync("./dist", { recursive: true })) {
  const fullPath = path.join("./dist", entry.toString());
  if (!fs.statSync(fullPath).isFile()) continue;
  files.push(new File([fs.readFileSync(fullPath)], entry.toString()));
}
const operation = await pinner.uploadDirectory(files, { name: "my-site" });
const result = await operation.result;
 
// Publish to IPNS
const IPNS_KEY_ID = Number(process.env.IPNS_KEY_ID);
await pinner.ipns.publish({
  cid: result.cid,
  key_id: IPNS_KEY_ID,
});
console.log("Published to IPNS:", result.cid);

The domain resolves to the IPNS name, which always points at the latest CID.

Next steps