diff --git a/CHANGELOG.md b/CHANGELOG.md index f40910f..ac469f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 8124983..f1fa528 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -109,6 +123,7 @@ permissions: contents: read pull-requests: write statuses: write + on: push: branches: @@ -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 @@ -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 @@ -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? diff --git a/action.yml b/action.yml index 1c5de56..c8e2926 100644 --- a/action.yml +++ b/action.yml @@ -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, @@ -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 }})