Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Fixed

- Add support for using ipfs-deploy-action in workflows triggered by `workflow_run` events to allow secure usage in PRs from forks.

## [1.6.0] - 2025-05-16

### Added
Expand Down
125 changes: 120 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@ The [composite action](https://docs.github.com/en/actions/sharing-automations/cr

![PR comment with CID and preview links](./screenshot-pr-comment.png)

## Table of Contents

- [Features](#features)
- [How does this compare to the other IPFS actions?](#how-does-this-compare-to-the-other-ipfs-actions)
- [Storacha configuration](#storacha-configuration)
- [Inputs](#inputs)
- [Required Inputs](#required-inputs)
- [Optional Inputs](#optional-inputs)
- [Outputs](#outputs)
- [Usage](#usage)
- [Simple Workflow (No Fork PRs)](#simple-workflow-no-fork-prs)
- [Dual Workflows (With Fork PRs)](#dual-workflows-with-fork-prs)
- [FAQ](#faq)

## Features

- 📦 Merkleizes your static site into a CAR file
Expand Down Expand Up @@ -98,9 +112,9 @@ The signing key and proof will be used as [inputs](#inputs) to the action.

## Usage

See the [IPNS Inspector](https://github.com/ipfs/ipns-inspector/blob/main/.github/workflows/build.yml) for a real-world example of this action in use.
### Simple Workflow (No Fork PRs)

Here's a basic example of how to use this action in your workflow:
For repositories that don't accept PRs from forks, you can use a single workflow:

```yaml
name: Build and Deploy to IPFS
Expand All @@ -109,6 +123,7 @@ permissions:
contents: read
pull-requests: write
statuses: write

on:
push:
branches:
Expand All @@ -118,7 +133,7 @@ on:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
outputs: # This exposes the CID output of the action to the rest of the workflow
outputs:
cid: ${{ steps.deploy.outputs.cid }}
steps:
- name: Checkout code
Expand All @@ -136,8 +151,8 @@ jobs:
- name: Build project
run: npm run build

- uses: ipfs/ipfs-deploy-action@v1
name: Deploy to IPFS
- name: Deploy to IPFS
uses: ipfs/ipfs-deploy-action@v1
id: deploy
with:
path-to-deploy: out
Expand All @@ -146,8 +161,108 @@ jobs:
github-token: ${{ github.token }}
```

### Dual Workflows (With Fork PRs)

For secure deployments of PRs from forks, use two separate workflows that pass artifacts between them:

**`.github/workflows/build.yml`** - Builds without secrets access:
```yaml
name: Build

permissions:
contents: read

on:
push:
branches:
- main
pull_request:
branches:
- main

env:
BUILD_PATH: 'out' # Update this to your build output directory

jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}


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

- name: Install dependencies
run: npm ci

- name: Build project
run: npm run build

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: website-build-${{ github.run_id }}
path: ${{ env.BUILD_PATH }}
retention-days: 1
```

**`.github/workflows/deploy.yml`** - Deploys with secrets access:
```yaml
name: Deploy

permissions:
contents: read
pull-requests: write
statuses: write

on:
workflow_run:
workflows: ["Build"]
types: [completed]

env:
BUILD_PATH: 'website-build' # Directory where artifact from build.yml will be unpacked

jobs:
deploy-ipfs:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
outputs:
cid: ${{ steps.deploy.outputs.cid }}
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: website-build-${{ github.event.workflow_run.id }}
path: ${{ env.BUILD_PATH }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Deploy to IPFS
uses: ipfs/ipfs-deploy-action@v1
id: deploy
with:
path-to-deploy: ${{ env.BUILD_PATH }}
storacha-key: ${{ secrets.STORACHA_KEY }}
storacha-proof: ${{ secrets.STORACHA_PROOF }}
github-token: ${{ github.token }}
```

See real-world examples:
- [IPFS Specs](https://github.com/ipfs/specs/tree/main/.github/workflows) - Uses the secure two-workflow pattern
- [IPFS Docs](https://github.com/ipfs/ipfs-docs/tree/main/.github/workflows) - Uses the secure two-workflow pattern

## FAQ

- How can I safely build on PRs from forks?
- Use the two-workflow pattern shown above. The build workflow runs on untrusted fork code without secrets access, while the deploy workflow only runs after a successful build and has access to secrets but never executes untrusted code. This pattern uses GitHub's `workflow_run` event to securely pass artifacts between workflows.
- What's the difference between uploading a CAR and using the Pinning API?
- Since the CAR is like a tarball of the full build with some additional metadata (merkle proofs), the upload will be as big as the build output. Pinning with the [Pinning API](https://github.com/ipfs/pinning-services-api-spec) in contrast is just a request to instruct the pinning service to retrieve and pin the data. At the time this action is first released, CAR uploads is supported by Kubo, Storacha, and Filebase, but not Pinata.
- How can I update DNSLink?
Expand Down
60 changes: 51 additions & 9 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -321,10 +321,18 @@ runs:
script: |
const cid = '${{ steps.merkleize.outputs.cid }}';

// For PR events, we need to use the head SHA
const sha = (context.eventName === 'pull_request' || context.eventName === 'pull_request_target')
? context.payload.pull_request.head.sha
: context.sha;
// Determine the correct SHA based on the event type
let sha;
if (context.eventName === 'workflow_run') {
// For workflow_run events triggered by PRs, use the PR's head SHA
sha = context.payload.workflow_run.head_sha;
} else if (context.eventName === 'pull_request' || context.eventName === 'pull_request_target') {
// For PR events, use the head SHA
sha = context.payload.pull_request.head.sha;
} else {
// For push events, use the commit SHA
sha = context.sha;
}

await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
Expand All @@ -336,26 +344,60 @@ runs:
context: 'IPFS'
});

- name: Get PR number for workflow_run
if: ${{ inputs.set-pr-comment == 'true' && github.event_name == 'workflow_run' }}
id: pr-number
uses: actions/github-script@v7
with:
github-token: ${{ inputs.github-token }}
script: |
// For workflow_run events, we need to find the PR number from the workflow run
if (context.payload.workflow_run.event === 'pull_request') {
const pulls = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
head: `${context.payload.workflow_run.head_repository.owner.login}:${context.payload.workflow_run.head_branch}`,
state: 'open'
});

if (pulls.data.length > 0) {
// Try to find the PR whose head SHA matches the workflow run's head_sha
const matchingPr = pulls.data.find(pr => pr.head.sha === context.payload.workflow_run.head_sha);

if (matchingPr) {
core.setOutput('number', matchingPr.number);
core.setOutput('sha', matchingPr.head.sha);
} else {
// Fallback: use the first PR, but warn in the summary
core.setOutput('number', pulls.data[0].number);
core.setOutput('sha', context.payload.workflow_run.head_sha);
core.warning(`Multiple PRs found for branch ${context.payload.workflow_run.head_branch}, but none matched head_sha. Using the first PR (#${pulls.data[0].number}).`);
}
} else {
core.warning(`No open PRs found for branch ${context.payload.workflow_run.head_branch} from ${context.payload.workflow_run.head_repository.owner.login}`);
}
}

- name: Find Comment to update
if: ${{ inputs.set-pr-comment == 'true' && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') }}
if: ${{ inputs.set-pr-comment == 'true' && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || (github.event_name == 'workflow_run' && steps.pr-number.outputs.number)) }}
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.pull_request.number || steps.pr-number.outputs.number }}
comment-author: 'github-actions[bot]'
body-includes: '🚀 Build'
token: ${{ inputs.github-token }}

- name: Create or update comment
if: ${{ inputs.set-pr-comment == 'true' && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') }}
if: ${{ inputs.set-pr-comment == 'true' && (github.event_name == 'pull_request' || github.event_name == 'pull_request_target' || (github.event_name == 'workflow_run' && steps.pr-number.outputs.number)) }}
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ inputs.github-token }}
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
issue-number: ${{ github.event.pull_request.number || steps.pr-number.outputs.number }}
body: |
### 🚀 Build Preview on IPFS ready
- 🔎 Commit: ${{ github.event.pull_request.head.sha || github.sha }}
- 🔎 Commit: ${{ github.event.pull_request.head.sha || steps.pr-number.outputs.sha }}
- 🔏 CID `${{ steps.merkleize.outputs.cid }}`
- 📦 Preview:
- [dweb.link](https://dweb.link/ipfs/${{ steps.merkleize.outputs.cid }})
Expand Down