From 1a4f87818fa2af30ce93228640ea833ada209a25 Mon Sep 17 00:00:00 2001 From: Christopher Rackauckas Date: Wed, 2 Sep 2026 10:40:41 +0000 Subject: [PATCH] Documentation: skip deployment when the run has no push credentials MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation jobs fail on pull requests from forks and on every Dependabot-triggered run, *after* the documentation has built successfully. Recent examples: SciML/FindFirstFunctions.jl#114, SciML/ModelingToolkitCourse#70, SciML/DiffEqFlux.jl#1070. ## Cause Documenter's `deploy_folder` treats deployment as possible when either credential is merely non-empty: ```julia token_ok = env_nonempty("GITHUB_TOKEN") key_ok = env_nonempty("DOCUMENTER_KEY") auth_ok = token_ok | key_ok ``` GitHub still injects a `GITHUB_TOKEN` for fork and Dependabot pull requests — it is just read-only — so `auth_ok` is true. Combined with `push_preview = true` (which the SciML docs builds use), Documenter builds the docs, attempts to push the preview to `gh-pages`, and the job dies with: ``` fatal: unable to access '...': The requested URL returned error: 403 ERROR: LoadError: failed process: ... `git push -q upstream HEAD:gh-pages` ``` The documentation itself was fine; only the push failed. The red check is noise, and it trains reviewers to ignore a failing Documentation job. ## Fix Pass the credentials only when the run can actually deploy. When they are empty Documenter reports `Deploying: ✗` and exits 0, so the job still verifies that the documentation builds — which is the only thing an untrusted pull request can verify. Preview deployment is unchanged for same-repository pull requests by a human, and deployment on push/tag/schedule is untouched. Co-Authored-By: Chris Rackauckas Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_014FEzNTLFutCmTEAZ3zBg5R --- .github/workflows/documentation.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e1d72bc..08b3eb3 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -61,6 +61,14 @@ jobs: name: "Build and Deploy Documentation" continue-on-error: ${{ inputs.continue-on-error || inputs.julia-version == 'nightly' }} runs-on: ${{ inputs.runner != '' && fromJson(inputs.runner) || (inputs.self-hosted && 'self-hosted' || inputs.os) }} + env: + # Whether this run has credentials that can actually push to gh-pages. GitHub issues a + # read-only GITHUB_TOKEN, and withholds secrets such as DOCUMENTER_KEY, for pull + # requests opened from a fork and for every Dependabot-triggered run. + CAN_DEPLOY_DOCS: >- + ${{ github.event_name != 'pull_request' + || (github.event.pull_request.head.repo.full_name == github.repository + && github.actor != 'dependabot[bot]') }} steps: - uses: actions/checkout@v7 @@ -79,8 +87,15 @@ jobs: - name: "Build and Deploy Documentation" env: - GITHUB_TOKEN: ${{ inputs.github-token || secrets.GITHUB_TOKEN }} - DOCUMENTER_KEY: ${{ inputs.documenter-key || secrets.DOCUMENTER_KEY }} + # Documenter decides it can deploy when GITHUB_TOKEN or DOCUMENTER_KEY is merely + # non-empty (`auth_ok` in Documenter's `deploy_folder`). A read-only token still + # looks non-empty, so with `push_preview = true` Documenter builds the docs, tries + # to push the preview, and the job fails on `403` — after the documentation has + # already built successfully. Passing empty values when we cannot deploy makes + # Documenter report `Deploying: ✗` and exit 0, so a fork or Dependabot pull request + # still reports whether the documentation *builds*, which is all it can verify. + GITHUB_TOKEN: ${{ env.CAN_DEPLOY_DOCS == 'true' && (inputs.github-token || secrets.GITHUB_TOKEN) || '' }} + DOCUMENTER_KEY: ${{ env.CAN_DEPLOY_DOCS == 'true' && (inputs.documenter-key || secrets.DOCUMENTER_KEY) || '' }} run: ${{ inputs.debug-documenter && 'JULIA_DEBUG="Documenter"' || '' }} julia --color=yes --project=docs/ ${{ inputs.coverage && '--code-coverage=user' || '' }} docs/make.jl - name: "Filter coverage directories to those that exist"