From 274196742f502f44b0f1820c9364f37b8f5cd693 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Oct 2026 15:17:22 +0200 Subject: [PATCH 1/4] Serialize publishes per deployed site publish.yml grouped its concurrency by the calling workflow and ref, so deploys of the same site from different workflows (a push, update-downloads, the axe report, the prerelease sync) ran in separate groups and an older build finishing last could overwrite a newer deploy. The group is now keyed on the deployed site, whichever workflow calls publish.yml. The site is picked the way the deploy steps pick it: an explicit prerelease input wins over github.ref. A called workflow inherits the caller's ref, so falling back to the ref whenever the input was false would put the quarto.org publish of an Axe Report dispatched from prerelease in the prerelease group, cancelling its sibling. --- .github/workflows/publish.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a8bf54a310..3051e7dbbd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,11 @@ permissions: statuses: write # for the Netlify deploy status concurrency: - group: ${{ format('{0} - {1} - {2}', github.workflow, github.ref, inputs.prerelease && 'prerelease' || 'main') }} + # One group per deployed site, whichever workflow calls it, so an older + # build cannot finish last and overwrite a newer deploy. + # Same site selection as the deploy steps: an explicit prerelease input wins + # over the caller's ref. + group: ${{ format('publish - {0}', (inputs.prerelease || (format('{0}', inputs.prerelease) != 'false' && github.ref == 'refs/heads/prerelease')) && 'prerelease' || 'main') }} cancel-in-progress: true jobs: From e5e55e7ba570b2c5a727655f790e6b2fea1a89cb Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Oct 2026 15:17:38 +0200 Subject: [PATCH 2/4] Publish the branch tip so a cancelling run never deploys older content With one concurrency group per site, the run that queues last cancels the one in progress. update-downloads and axe-scan passed the SHA they had just pushed, so a publish queued a few seconds late could cancel a run for a newer commit and deploy the older SHA. Push-triggered runs had the same exposure through the default checkout of github.sha. Every caller now passes a branch name, and the checkout falls back to github.ref instead of github.sha, so each run builds the branch tip as it stands when the job starts. The commit-hash job outputs that only fed the pinned refs are removed. --- .github/workflows/axe-scan.yml | 3 +-- .github/workflows/publish.yml | 10 ++++++---- .github/workflows/update-downloads.yml | 16 +++++----------- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/axe-scan.yml b/.github/workflows/axe-scan.yml index 0a603278ca..c94ba52d25 100644 --- a/.github/workflows/axe-scan.yml +++ b/.github/workflows/axe-scan.yml @@ -41,7 +41,6 @@ jobs: contents: write # commits _axe-report.md to the branch outputs: changes_detected: ${{ steps.auto-commit.outputs.changes_detected }} - commit: ${{ steps.auto-commit.outputs.commit_hash }} steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -169,7 +168,7 @@ jobs: contents: read statuses: write with: - ref: ${{ needs.scan.outputs.commit }} + ref: ${{ inputs.branch }} prerelease: ${{ inputs.prerelease }} secrets: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3051e7dbbd..e4492e8970 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,7 +8,7 @@ on: inputs: ref: required: true - description: 'The ref to trigger the workflow on' + description: 'The branch to publish; its tip is checked out when the job starts' type: string prerelease: required: true @@ -28,7 +28,8 @@ permissions: concurrency: # One group per deployed site, whichever workflow calls it, so an older - # build cannot finish last and overwrite a newer deploy. + # build cannot finish last and overwrite a newer deploy. Each run checks + # out the branch tip, so the run that wins includes every earlier change. # Same site selection as the deploy steps: an explicit prerelease input wins # over the caller's ref. group: ${{ format('publish - {0}', (inputs.prerelease || (format('{0}', inputs.prerelease) != 'false' && github.ref == 'refs/heads/prerelease')) && 'prerelease' || 'main') }} @@ -41,8 +42,9 @@ jobs: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - # if called from workflow_call event, checkout the ref otherwise use default - ref: ${{ inputs.ref || '' }} + # The branch tip, not the triggering commit: a run that cancels an + # older one must not deploy older content than it. + ref: ${{ inputs.ref || github.ref }} persist-credentials: false # deploys with NETLIFY_AUTH_TOKEN, never pushes - name: Get latest pre-release from github diff --git a/.github/workflows/update-downloads.yml b/.github/workflows/update-downloads.yml index d08096b59d..3048af3305 100644 --- a/.github/workflows/update-downloads.yml +++ b/.github/workflows/update-downloads.yml @@ -11,8 +11,6 @@ jobs: contents: write # commits the regenerated JSON and syncs it to prerelease outputs: changes_detected: ${{ steps.auto-commit.outputs.changes_detected }} - commit: ${{ steps.auto-commit.outputs.commit_hash }} - commit_prerelease: ${{ steps.sync-prerelease.outputs.commit_hash }} prerelease_version: ${{ steps.get-versions.outputs.prerelease_version }} steps: - name: Checkout Repo @@ -49,7 +47,6 @@ jobs: commit_author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> - name: Sync generated downloads to prerelease branch - id: sync-prerelease if: ${{ steps.auto-commit.outputs.changes_detected == 'true' }} env: MAIN_SHA: ${{ steps.auto-commit.outputs.commit_hash }} @@ -67,11 +64,9 @@ jobs: git commit -m "Sync generated download redirects from main" git push origin prerelease fi - echo "commit_hash=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - # If a new commit has been made with updated downloads, - # then publish the changes using the new commit as ref to checkout - # in the reusable publish workflow + # If a new commit has been made with updated downloads, + # then publish main with the reusable publish workflow publish-changes-release: needs: [ update-downloads ] if: ${{ needs.update-downloads.outputs.changes_detected == 'true' }} @@ -81,14 +76,13 @@ jobs: contents: read statuses: write with: - ref: ${{ needs.update-downloads.outputs.commit }} + ref: main prerelease: false secrets: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} # If a new commit has been made with updated downloads, - # then publish the changes using the new commit as ref to checkout - # in the reusable publish workflow + # then publish prerelease with the reusable publish workflow publish-changes-prerelease: needs: [ update-downloads ] if: ${{ needs.update-downloads.outputs.changes_detected == 'true' }} @@ -98,7 +92,7 @@ jobs: contents: read statuses: write with: - ref: ${{ needs.update-downloads.outputs.commit_prerelease }} + ref: prerelease prerelease: true secrets: NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} From 466a5e85c2e194f72459b0895f9ae38d59249724 Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Oct 2026 15:46:37 +0200 Subject: [PATCH 3/4] Keep publishes that deploy nothing out of the main group Dispatching Quarto Publish from a branch other than main or prerelease deploys neither site, but its run fell into the main group and cancelled any quarto.org publish in progress, leaving those changes unpublished. The previous group included the ref, so such a run was isolated. It now gets a group named after its ref. --- .github/workflows/publish.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e4492e8970..5e869657c8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -31,8 +31,9 @@ concurrency: # build cannot finish last and overwrite a newer deploy. Each run checks # out the branch tip, so the run that wins includes every earlier change. # Same site selection as the deploy steps: an explicit prerelease input wins - # over the caller's ref. - group: ${{ format('publish - {0}', (inputs.prerelease || (format('{0}', inputs.prerelease) != 'false' && github.ref == 'refs/heads/prerelease')) && 'prerelease' || 'main') }} + # over the caller's ref. A run that deploys neither site (a dispatch from + # another branch) gets its own group, so it never cancels a deploy. + group: ${{ format('publish - {0}', (inputs.prerelease || (format('{0}', inputs.prerelease) != 'false' && github.ref == 'refs/heads/prerelease')) && 'prerelease' || (format('{0}', inputs.prerelease) == 'false' || github.ref == 'refs/heads/main') && 'main' || github.ref) }} cancel-in-progress: true jobs: From ae3969a97b6ab3175e9c1f90e5b0dad059c5ed5e Mon Sep 17 00:00:00 2001 From: Christophe Dervieux Date: Fri, 16 Oct 2026 15:49:20 +0200 Subject: [PATCH 4/4] Render with the profile of the site being deployed A called workflow inherits the caller's ref and event. Dispatching Axe Report from prerelease called publish.yml for quarto.org with prerelease: false, but github.ref was refs/heads/prerelease, so the render used the prerelease-docs profile and quarto.org was deployed with prerelease branding and site URLs. The profile condition now carries the same guard as the prerelease deploy step, so an explicit prerelease: false always renders the release profile. --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5e869657c8..82b70dac69 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -88,7 +88,7 @@ jobs: - name: Render uses: quarto-dev/quarto-actions/render@v2 env: - QUARTO_PROFILE: ${{ (inputs.prerelease || (contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) && github.ref == 'refs/heads/prerelease')) && 'prerelease-docs' || '' }} + QUARTO_PROFILE: ${{ (inputs.prerelease || (format('{0}', inputs.prerelease) != 'false' && contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) && github.ref == 'refs/heads/prerelease')) && 'prerelease-docs' || '' }} - name: Publish release website # Only do this step if