| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
TL;DR: Prefer Solution B (which this PR adapts to). This is mostly for my benefit to document all of this research in one place along with full workflow configs to compare. It might also be a helpful resource to others weighing up which approach to implement on their projects. These were all initially adapted from a community discussion started in 2021. I've since posted an answer there with a summary of the 3 solutions detailed below. Reference - Solutions ComparisonTo focus more on the comparing differences:
Each solution highlights some bullet points of differences. Beyond that they are effectively the same in functionality. Should further context be needed, solutions B and C both link to a PR each which provides full commentary in the PR files source. UPDATE: Solution C has been deemed high risk. I will keep it documented here, but heavily discourage it given the wider attack surface when the PR author can run untrusted code vs the reduced risk of pull_request (not only from restricted permissions + secrets, but branch isolation):
Solution A - pull_request + workflow_run with ENV validationThis is the more common approach elsewhere as it's what Github demonstrates as a solution in Aug 2021. The vulnerability to LD_PRELOAD from adding untrusted content into $GITHUB_ENV is avoided by preferring $GITHUB_OUTPUT instead or when viable validating the input (such as the value only being digits for a PR number). More details with references: #4264 (comment) No full workflow example here as solutions B and C resolve this better. docs-preview-prepare.yml (partial)Avoid storing key=value pairs into a single file here. Store only the value with the key as the file name, then construct the key=value entries in the later workflow_run workflow. This is less convenient but makes validation easier while also reducing risk. - name: 'Export PR Context'
run: |
mkdir pr-context
echo '${{ github.event.pull_request.number }}' > ./pr-context/number
- name: 'Upload context artifact for workflow transfer'
uses: actions/upload-artifact@v4
with:
name: preview-build-context
path: pr-context/
retention-days: 1docs-preview-deploy.yml (partial)Verbosity will depend on how much validation you need to do, and if you use separate upload/download steps or combine via path with your build artifact (if your workflow has one).
jobs:
# NOTE: This is handled as pre-requisite job to minimize the noise from acquiring these two outputs needed for `deploy-preview` ENV:
pr-context:
name: 'Acquire PR Context'
runs-on: ubuntu-24.04
outputs:
PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }}
PR_NUMBER: ${{ steps.set-pr-context.outputs.number }}
# Skip this job (and thus the next job deploy-preview) if these conditions for `workflow_run` are not met:
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
steps:
- name: 'Retrieve and extract the pull_request context'
uses: actions/download-artifact@v4
with:
name: preview-build-context
path: pr-context/
# These are needed due this approach relying on `workflow_run`, so that it can access the build artifact:
# (uploaded from the associated `docs-preview-prepare.yml` workflow run)
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: 'Import PR Context'
id: set-pr-context
# Validate values loaded from files:
# Bash pattern matching: https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html
run: |
# Hexadecimal value only:
PR_HEADSHA=$(cat ./pr-context/head-sha)
if ! [[ "${PR_HEADSHA}" =~ ^[[:xdigit:]]+$ ]]; then
echo "Invalid SHA: ${PR_HEADSHA}"
exit 1
fi
# Number only:
PR_NUMBER=$(cat ./pr-context/number)
if ! [[ "${PR_NUMBER}" =~ ^[[:digit:]]+$ ]]; then
echo "Invalid PR number: ${PR_NUMBER}"
exit 1
fi
# Append to GITHUB_ENV or GITHUB_OUTPUT:
{
echo "head-sha=${PR_HEADSHA}"
echo "number=${PR_NUMBER}"
} >> "${GITHUB_OUTPUT}"
# For separate job above with `GITHUB_OUTPUT`, bring the outputs in as `env`:
deploy-preview:
name: 'Deploy Preview'
runs-on: ubuntu-24.04
needs: [pr-context]
env:
PR_HEADSHA: ${{ needs.pr-context.outputs.PR_HEADSHA }}
PR_NUMBER: ${{ needs.pr-context.outputs.PR_NUMBER }}Solution B - pull_request + workflow_run with gh pr view#4267 (this PR)
docs-preview-prepare.ymlname: 'Documentation (PR)'
on:
pull_request:
paths:
- 'docs/**'
- '.github/workflows/scripts/docs/build-docs.sh'
- '.github/workflows/docs-preview-prepare.yml'
concurrency:
group: deploypreview-pullrequest-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
BUILD_DIR: docs/site/
PREVIEW_SITE_NAME: dms-doc-previews
PREVIEW_SITE_PREFIX: pullrequest-${{ github.event.pull_request.number }}
permissions:
# Required by `actions/checkout` for git checkout:
contents: read
jobs:
prepare-preview:
name: 'Build Preview'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: 'Build with mkdocs-material via Docker'
working-directory: docs/
env:
PREVIEW_URL: 'https://${{ env.PREVIEW_SITE_PREFIX }}--${{ env.PREVIEW_SITE_NAME }}.netlify.app/'
run: bash ../.github/workflows/scripts/docs/build-docs.sh
- name: 'Upload artifact for workflow transfer'
uses: actions/upload-artifact@v4
with:
name: preview-build
path: ${{ env.BUILD_DIR }}
retention-days: 1docs-preview-deploy.ymlname: 'Documentation (Deploy)'
on:
workflow_run:
workflows: ['Documentation (PR)']
types:
- completed
permissions:
# Required by `actions/download-artifact`:
actions: read
# Required by `set-pr-context`:
contents: read
# Required by `marocchino/sticky-pull-request-comment` (write) + `set-pr-context` (read):
pull-requests: write
# Required by `myrotvorets/set-commit-status-action`:
statuses: write
jobs:
pr-context:
name: 'Acquire PR Context'
runs-on: ubuntu-24.04
outputs:
PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }}
PR_NUMBER: ${{ steps.set-pr-context.outputs.number }}
if: ${{ github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request' }}
steps:
- name: 'Get PR context'
id: set-pr-context
env:
GH_TOKEN: ${{ github.token }}
PR_TARGET_REPO: ${{ github.repository }}
PR_BRANCH: |-
${{
(github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login)
&& format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch)
|| github.event.workflow_run.head_branch
}}
run: |
gh pr view --repo "${PR_TARGET_REPO}" "${PR_BRANCH}" \
--json 'number,headRefOid' \
--jq '"number=\(.number)\nhead-sha=\(.headRefOid)"' \
>> $GITHUB_OUTPUT
deploy-preview:
name: 'Deploy Preview'
runs-on: ubuntu-24.04
needs: [pr-context]
env:
BUILD_DIR: docs/site/
PR_HEADSHA: ${{ needs.pr-context.outputs.PR_HEADSHA }}
PR_NUMBER: ${{ needs.pr-context.outputs.PR_NUMBER }}
PREVIEW_SITE_PREFIX: pullrequest-${{ needs.pr-context.outputs.PR_NUMBER }}
steps:
- name: 'Retrieve and extract the built docs preview'
uses: actions/download-artifact@v4
with:
name: preview-build
path: ${{ env.BUILD_DIR }}
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
# ==================== #
# Deploy preview build #
# ==================== #
- name: 'Commit Status (1/2) - Set Workflow Status as Pending'
uses: myrotvorets/set-commit-status-action@v2.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
status: pending
sha: ${{ env.PR_HEADSHA }}
context: 'Deploy Preview (pull_request => workflow_run)'
- name: 'Send preview build to Netlify'
uses: nwtgck/actions-netlify@v3.0
id: preview-netlify
timeout-minutes: 1
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
with:
fails-without-credentials: true
alias: ${{ env.PREVIEW_SITE_PREFIX }}
publish-dir: ${{ env.BUILD_DIR }}
deploy-message: 'Preview Build (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }}'
# Disable unwanted action defaults:
enable-commit-comment: false
enable-commit-status: false
enable-pull-request-comment: false
enable-github-deployment: false
- name: 'Comment on PR with preview link'
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ env.PR_NUMBER }}
header: preview-comment
recreate: true
message: |
[Documentation preview for this PR](${{ steps.preview-netlify.outputs.deploy-url }}) is ready! :tada:
Built with commit: ${{ env.PR_HEADSHA }}
- name: 'Commit Status (2/2) - Update deployment status'
uses: myrotvorets/set-commit-status-action@v2.0.1
if: ${{ always() }}
env:
DEPLOY_SUCCESS: Successfully deployed preview.
DEPLOY_FAILURE: Failed to deploy preview.
with:
token: ${{ secrets.GITHUB_TOKEN }}
status: ${{ job.status == 'success' && 'success' || 'failure' }}
sha: ${{ env.PR_HEADSHA }}
context: 'Deploy Preview (pull_request => workflow_run)'
description: ${{ job.status == 'success' && env.DEPLOY_SUCCESS || env.DEPLOY_FAILURE }}Solution C - pull_request_target + workflow_callUPDATE: As per the PR link discussion and earlier warning for Solution C at the start of this reference, you are heavily discouraged from adopting this approach.
Unlike pull_request, changes to any of these workflows cannot be run/tested when modified by the PR, as pull_request_target runs workflows from the PR base. It's also important to consider how this affects uses: <workflow ref>:
docs-preview.ymlname: 'Documentation (Preview)'
# For security reasons, it is necessary to split the workflow into two separate jobs to manage trust safely.
on:
pull_request_target:
paths:
- 'docs/**'
- '.github/workflows/scripts/docs/build-docs.sh'
concurrency:
group: deploypreview-pullrequest-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
PREVIEW_CONTEXT: |
{
"build_dir": "docs/site/",
"netlify": {
"site_name": "dms-doc-previews",
"deploy_prefix": "pullrequest-${{ github.event.pull_request.number }}"
},
"pull_request": {
"head_repo": "${{ github.event.pull_request.head.repo.full_name }}",
"head_sha": "${{ github.event.pull_request.head.sha }}",
"number": "${{ github.event.pull_request.number }}"
}
}
# This affects what permissions the `workflow_call` can be granted (they may only remove permissions needed):
# It cannot grant less than what those workflows require to run.
permissions:
contents: read
pull-requests: write
jobs:
create-context:
name: 'Create Context'
runs-on: ubuntu-24.04
outputs:
preview-context: ${{ steps.set-preview-context.outputs.preview-context }}
steps:
- id: set-preview-context
run: echo "preview-context=$(jq --compact-output <<< "${PREVIEW_CONTEXT}")" >> "${GITHUB_OUTPUT}"
prepare:
needs: [create-context]
uses: .github/workflows/docs-preview-prepare.yml
with:
preview-context: ${{ needs.create-context.outputs.preview-context }}
deploy:
needs: [create-context, prepare]
uses: .github/workflows/docs-preview-deploy.yml
secrets:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
with:
preview-context: ${{ needs.create-context.outputs.preview-context }}docs-preview-prepare.ymlname: 'Docs Preview (Build)'
on:
workflow_call:
inputs:
preview-context:
description: 'Preview Metadata (JSON)'
required: true
type: string
env:
BUILD_DIR: ${{ fromJSON( inputs.preview-context ).build_dir }}
PR_REF: ${{ fromJSON( inputs.preview-context ).pull_request.head_sha }}
PR_REPO: ${{ fromJSON( inputs.preview-context ).pull_request.head_repo }}
PREVIEW_SITE_NAME: ${{ fromJSON( inputs.preview-context ).netlify.site_name }}
PREVIEW_SITE_PREFIX: ${{ fromJSON( inputs.preview-context ).netlify.deploy_prefix }}
permissions:
# Required by `actions/checkout` for git checkout:
contents: read
jobs:
prepare-preview:
name: 'Build Preview'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
with:
ref: ${{ env.PR_REF }}
repository: ${{ env.PR_REPO }}
persist-credentials: false
- name: 'Build with mkdocs-material via Docker'
working-directory: docs/
env:
PREVIEW_URL: 'https://${{ env.PREVIEW_SITE_PREFIX }}--${{ env.PREVIEW_SITE_NAME }}.netlify.app/'
run: bash ../.github/workflows/scripts/docs/build-docs.sh
- name: 'Upload artifact for workflow transfer'
uses: actions/upload-artifact@v4
with:
name: preview-build
path: ${{ env.BUILD_DIR }}
retention-days: 1docs-preview-deploy.ymlname: 'Docs Preview (Deploy)'
on:
workflow_call:
inputs:
preview-context:
description: 'Preview Metadata (JSON)'
required: true
type: string
secrets:
NETLIFY_AUTH_TOKEN:
required: true
NETLIFY_SITE_ID:
required: true
env:
BUILD_DIR: ${{ fromJSON( inputs.preview-context ).build_dir }}
PR_HEADSHA: ${{ fromJSON( inputs.preview-context ).pull_request.head_sha }}
PR_NUMBER: ${{ fromJSON( inputs.preview-context ).pull_request.number }}
PREVIEW_SITE_PREFIX: ${{ fromJSON( inputs.preview-context ).netlify.deploy_prefix }}
permissions:
# Required by `marocchino/sticky-pull-request-comment`:
pull-requests: write
jobs:
deploy-preview:
name: 'Deploy Preview'
runs-on: ubuntu-24.04
steps:
- name: 'Retrieve and extract the built docs preview'
uses: actions/download-artifact@v4
with:
name: preview-build
path: ${{ env.BUILD_DIR }}
# ==================== #
# Deploy preview build #
# ==================== #
- name: 'Send preview build to Netlify'
uses: nwtgck/actions-netlify@v3.0
id: preview-netlify
timeout-minutes: 1
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
with:
fails-without-credentials: true
alias: ${{ env.PREVIEW_SITE_PREFIX }}
publish-dir: ${{ env.BUILD_DIR }}
deploy-message: 'Preview Build (PR #${{ env.PR_NUMBER }} @ commit: ${{ env.PR_HEADSHA }}'
# Disable unwanted action defaults:
enable-commit-comment: false
enable-commit-status: false
enable-pull-request-comment: false
enable-github-deployment: false
- name: 'Comment on PR with preview link'
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ env.PR_NUMBER }}
header: preview-comment
recreate: true
message: |
[Documentation preview for this PR](${{ steps.preview-netlify.outputs.deploy-url }}) is ready! :tada:
Built with commit: ${{ env.PR_HEADSHA }} |
Sorry, something went wrong.
Thanks a lot for this!! ⭐ Really helpful Just a small correction: there are two Solution B , I can't C see the C one 😛 |
Sorry, something went wrong.
Glad to hear that 🎉
Whoops! Thanks for pointing that out, I've corrected it 😅 |
Sorry, something went wrong.
| # Required by `set-pr-context`: | ||
| contents: read | ||
| # Required by `marocchino/sticky-pull-request-comment` (write) + `set-pr-context` (read): | ||
| pull-requests: write |
There was a problem hiding this comment.
Better to move the write permissions to the job that needs them (deploy-preview)
Sorry, something went wrong.
| && github.event.workflow_run.event == 'pull_request' | ||
| && contains(github.event.workflow_run.pull_requests.*.head.sha, github.event.workflow_run.head_sha) | ||
| PR_HEADSHA: ${{ steps.set-pr-context.outputs.head-sha }} | ||
| PR_NUMBER: ${{ steps.set-pr-context.outputs.number }} |
There was a problem hiding this comment.
| PR_NUMBER: ${{ steps.set-pr-context.outputs.number }} | |
| PR_NUMBER: ${{ steps.set-pr-context.outputs.number }} |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Description
A pull_request + workflow_run solution that should work well with PRs from forks.
My preference is still to pull_request_target + workflow_call, but that is awaiting confirmation that it was implemented securely (EDIT: It is not secure, unless the untrusted code is only executed in an environment like a container).
UPDATE: Due to review feedback of #4264 (Solution C), while I do prefer that approach I am not comfortable moving forward with it for the project and will favor this PR (Solution B).
Type of change
Checklist