Skip to content

Deploy

Deploy #48

name: Deploy
# Unified deploy workflow for all Netlify deploys produced by either
# "Build Documentation" or "Unit Testing and Deployment". Triggered via
# workflow_run so fork PR CI is never blocked waiting on deployment approval:
# the parent workflows always run freely, and deploys wait for approval in
# their own run which can sit pending indefinitely without affecting the PR's
# required status checks.
#
# workflow_run runs in the context of the default branch and has access to
# secrets, even when the parent run was triggered by a fork PR. GitHub loads
# this workflow file from main, so fork modifications to it are ignored.
on: # zizmor: ignore[dangerous-triggers]
# workflow_run is needed here. It runs in base-repo context so the child has
# access to Netlify secrets for fork PRs. GitHub loads this file from main
# regardless of fork modifications, and no fork code is ever checked out or
# executed (see the checkout comments in each job). Do not add a checkout of
# github.event.workflow_run.head_sha to this workflow.
workflow_run:
workflows: ["Build Documentation", "Unit Testing and Deployment"]
types: [completed]
# Deploys and commit statuses use a GitHub App installation token (see the
# per-job "Get app installation token" steps), not GITHUB_TOKEN, so the
# workflow-level token only needs read access (plus actions: read to
# download artifacts from the triggering workflow run).
permissions:
actions: read
contents: read
jobs:
setup:
name: Load PR info
runs-on: ubuntu-22.04
# Run on success and failure so image-report jobs (which must run even when
# tests fail) can consume the PR info outputs. preview and release have
# their own stricter conclusion checks.
if: github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure'
outputs:
pr_number: ${{ steps.parse.outputs.pr_number }}
head_sha: ${{ steps.parse.outputs.head_sha }}
is_fork: ${{ steps.parse.outputs.is_fork }}
steps:
- name: Download PR info artifact
if: github.event.workflow_run.event == 'pull_request'
uses: actions/download-artifact@v8
with:
name: pr-info
path: pr-info
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
# The artifact was written by the triggering run, which a fork controls, so
# only the PR number is read from it, and only if it is a plain integer. The
# head SHA and fork status come from the event payload, which a fork cannot
# forge; is_fork picks the deployment environment, so it must never come
# from the artifact.
- name: Parse PR info
id: parse
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
IS_FORK: ${{ github.event.workflow_run.head_repository.full_name != github.repository }}
run: |
pr_number=""
if [ -f pr-info/pr_number.txt ]; then
pr_number="$(tr -d '[:space:]' < pr-info/pr_number.txt)"
if ! [[ "$pr_number" =~ ^[0-9]+$ ]]; then
echo "::error::pr-info artifact holds an invalid PR number"
exit 1
fi
fi
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT"
echo "is_fork=$IS_FORK" >> "$GITHUB_OUTPUT"
preview:
name: Preview Development Documentation
runs-on: ubuntu-22.04
needs: setup
# PRs: deploy whenever a docs-build artifact exists, even if Test
# Documentation failed — reviewers still need a preview of the broken
# build. Pushes to main: require the whole workflow to succeed so dev
# docs never publish a failed build.
if: >-
github.event.workflow_run.name == 'Build Documentation' &&
(
(github.event.workflow_run.event == 'pull_request' &&
(github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure')) ||
(github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.conclusion == 'success')
)
environment:
name: ${{ github.event.workflow_run.event == 'push' && 'docs-main' || (needs.setup.outputs.is_fork == 'true' && 'fork-deploys' || 'internal-deploys') }}
steps:
- name: Get app installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.PYVISTA_BOT_APP_ID }}
private-key: ${{ secrets.PYVISTA_BOT_APP_PRIVATE_KEY }}
owner: pyvista
repositories: pyvista
# nwtgck/actions-netlify creates a GitHub Deployment; the subsequent
# step posts a commit status.
permission-deployments: write
permission-statuses: write
- name: Download docs-build artifact
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: docs-build
path: .
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Check for docs build
id: check
run: |
if [ -f index.html ]; then
echo "has_build=true" >> "$GITHUB_OUTPUT"
else
echo "has_build=false" >> "$GITHUB_OUTPUT"
fi
- name: Preview HTML documentation
if: steps.check.outputs.has_build == 'true'
id: netlify
uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668
with:
publish-dir: .
production-deploy: ${{ github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' }}
github-token: ${{ steps.app-token.outputs.token }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: false
enable-commit-status: false
github-deployment-environment: docs-preview
github-deployment-description: "Documentation preview"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DEV_SITE_ID }} # DEV site
timeout-minutes: 10
- name: Create commit status with preview link
if: steps.check.outputs.has_build == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
REPO: ${{ github.repository }}
run: |
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state=success \
-f context="Deploy / Docs Preview" \
-f description="Documentation preview is ready" \
-f target_url="$DEPLOY_URL"
- name: Add preview link to job summary
if: steps.check.outputs.has_build == 'true'
env:
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
run: |
echo "### Documentation Preview" >> $GITHUB_STEP_SUMMARY
echo "$DEPLOY_URL" >> $GITHUB_STEP_SUMMARY
doc-image-report:
name: Doc Image Report
runs-on: ubuntu-22.04
needs: setup
# Run on both success and failure — the whole point of the image report is
# to show what failed. Build-stage failures that produced no test artifact
# are handled gracefully below via continue-on-error and the has_report
# check. The report site tracks `main` through the merge queue run, whose
# commit is the one that lands, and which still holds the images after a
# merge skips the rebuild.
if: >-
(github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') &&
github.event.workflow_run.name == 'Build Documentation' &&
(github.event.workflow_run.event == 'pull_request' ||
(github.event.workflow_run.event == 'merge_group' &&
startsWith(github.event.workflow_run.head_branch, 'gh-readonly-queue/main/')))
environment:
name: ${{ github.event.workflow_run.event == 'merge_group' && 'docs-main' || (needs.setup.outputs.is_fork == 'true' && 'fork-deploys' || 'internal-deploys') }}
steps:
- name: Get app installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.PYVISTA_BOT_APP_ID }}
private-key: ${{ secrets.PYVISTA_BOT_APP_PRIVATE_KEY }}
owner: pyvista
repositories: pyvista
permission-deployments: write
permission-statuses: write
# Check out base repo (main). Never check out the PR head — the whole
# point of workflow_run is that fork code is never executed here.
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: 3.14
- name: Install tox-uv
run: pip install tox-uv
- name: Download doc failed image artifacts
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: doc-failed-test-images
path: all_doc_failed_images
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Generate HTML report
id: report
run: |
mkdir -p all_doc_failed_images
tox run -e image-report -- all_doc_failed_images _doc_image_report
if [ -f _doc_image_report/index.html ]; then
echo "has_report=true" >> "$GITHUB_OUTPUT"
else
echo "has_report=false" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to Netlify
if: steps.report.outputs.has_report == 'true'
id: netlify
uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668
with:
publish-dir: _doc_image_report
production-deploy: ${{ github.event.workflow_run.event == 'merge_group' }}
github-token: ${{ steps.app-token.outputs.token }}
deploy-message: "${{ needs.setup.outputs.pr_number != '' && format('Doc image report for PR #{0}', needs.setup.outputs.pr_number) || format('Doc image report for {0}', github.event.workflow_run.head_sha) }}"
enable-pull-request-comment: false
enable-commit-comment: false
enable-commit-status: false
github-deployment-environment: doc-image-report
github-deployment-description: "Doc image comparison report"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_DOC_IMAGE_REPORT_SITE_ID }}
timeout-minutes: 5
- name: Create commit status with report link
if: steps.report.outputs.has_report == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
REPO: ${{ github.repository }}
run: |
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state=success \
-f context="Deploy / Doc Image Report" \
-f description="Doc image comparison report is ready" \
-f target_url="$DEPLOY_URL"
- name: Add report link to job summary
if: steps.report.outputs.has_report == 'true'
env:
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
run: |
echo "### Doc Image Report" >> $GITHUB_STEP_SUMMARY
echo "$DEPLOY_URL" >> $GITHUB_STEP_SUMMARY
test-image-report:
name: Test Image Report
runs-on: ubuntu-22.04
needs: setup
# Run on both success and failure — the whole point of the image report is
# to show what failed. The report site tracks `main` through the merge queue
# run, whose commit is the one that lands, and which runs the full matrix a
# merge no longer repeats.
if: >-
(github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure') &&
github.event.workflow_run.name == 'Unit Testing and Deployment' &&
(github.event.workflow_run.event == 'pull_request' ||
(github.event.workflow_run.event == 'merge_group' &&
startsWith(github.event.workflow_run.head_branch, 'gh-readonly-queue/main/')))
environment:
name: ${{ github.event.workflow_run.event == 'merge_group' && 'docs-main' || (needs.setup.outputs.is_fork == 'true' && 'fork-deploys' || 'internal-deploys') }}
steps:
- name: Get app installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ vars.PYVISTA_BOT_APP_ID }}
private-key: ${{ secrets.PYVISTA_BOT_APP_PRIVATE_KEY }}
owner: pyvista
repositories: pyvista
permission-deployments: write
permission-statuses: write
# Check out base repo (main). Never check out the PR head.
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-python@v7
with:
python-version: 3.14
- name: Install tox-uv
run: pip install tox-uv
- name: Download all failed image artifacts
continue-on-error: true
uses: actions/download-artifact@v8
with:
pattern: failed_test_images-*
path: all_failed_images
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Generate HTML report
id: report
run: |
mkdir -p all_failed_images
tox run -e image-report -- all_failed_images _image_report
if [ -f _image_report/index.html ]; then
echo "has_report=true" >> "$GITHUB_OUTPUT"
else
echo "has_report=false" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to Netlify
if: steps.report.outputs.has_report == 'true'
id: netlify
uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668
with:
publish-dir: _image_report
production-deploy: ${{ github.event.workflow_run.event == 'merge_group' }}
github-token: ${{ steps.app-token.outputs.token }}
deploy-message: "${{ needs.setup.outputs.pr_number != '' && format('Test image report for PR #{0}', needs.setup.outputs.pr_number) || format('Test image report for {0}', github.event.workflow_run.head_sha) }}"
enable-pull-request-comment: false
enable-commit-comment: false
enable-commit-status: false
github-deployment-environment: test-image-report
github-deployment-description: "Test image comparison report"
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_IMAGE_REPORT_SITE_ID }}
timeout-minutes: 5
- name: Create commit status with report link
if: steps.report.outputs.has_report == 'true'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
REPO: ${{ github.repository }}
run: |
gh api "repos/$REPO/statuses/$HEAD_SHA" \
-f state=success \
-f context="Deploy / Test Image Report" \
-f description="Image comparison report is ready" \
-f target_url="$DEPLOY_URL"
- name: Add report link to job summary
if: steps.report.outputs.has_report == 'true'
env:
DEPLOY_URL: ${{ steps.netlify.outputs.deploy-url }}
run: |
echo "### Test Image Report" >> $GITHUB_STEP_SUMMARY
echo "$DEPLOY_URL" >> $GITHUB_STEP_SUMMARY
release:
name: Publish Release Documentation
runs-on: ubuntu-22.04
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.name == 'Build Documentation' &&
github.event.workflow_run.event == 'push' &&
startsWith(github.event.workflow_run.head_branch, 'v')
environment: docs-release
steps:
- name: Download docs-build artifact
uses: actions/download-artifact@v8
with:
name: docs-build
path: .
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Deploy Release Documentation
uses: nwtgck/actions-netlify@d22a32a27c918fe470bbc562e984f80ec48c2668
with:
publish-dir: .
production-deploy: true
github-token: ${{ secrets.GITHUB_TOKEN }}
deploy-message: "Deploy from GitHub Actions"
enable-pull-request-comment: false
enable-commit-comment: false
overwrites-pull-request-comment: false
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_STABLE_SITE_ID }} # STABLE site
timeout-minutes: 10