From 0a7ebe1dd1888cd8c19f3ad873651b5ab4d9ffb0 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 14 Aug 2026 12:14:08 -0700 Subject: [PATCH] fix(ci): stop superseded Docs Deployment runs from showing as cancelled (#42488) Co-authored-by: Claude Code --- .github/workflows/github-action-validator.yml | 3 + .../scripts/check-docs-deploy-freshness.sh | 49 +++++++++ .../check-docs-deploy-freshness.test.sh | 100 ++++++++++++++++++ .github/workflows/superset-docs-deploy.yml | 81 ++++++++++++-- 4 files changed, 222 insertions(+), 11 deletions(-) create mode 100755 .github/workflows/scripts/check-docs-deploy-freshness.sh create mode 100755 .github/workflows/scripts/check-docs-deploy-freshness.test.sh diff --git a/.github/workflows/github-action-validator.yml b/.github/workflows/github-action-validator.yml index 87d7f953993..c1ef2eb9347 100644 --- a/.github/workflows/github-action-validator.yml +++ b/.github/workflows/github-action-validator.yml @@ -45,5 +45,8 @@ jobs: - name: Run Script run: bash .github/workflows/github-action-validator.sh + - name: Test docs-deploy freshness gate + run: bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh + - name: Check for security issues on GHA workflows uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 diff --git a/.github/workflows/scripts/check-docs-deploy-freshness.sh b/.github/workflows/scripts/check-docs-deploy-freshness.sh new file mode 100755 index 00000000000..61b062bf89f --- /dev/null +++ b/.github/workflows/scripts/check-docs-deploy-freshness.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Shared freshness gate used by the Docs Deployment workflow +# (superset-docs-deploy.yml) both up front (check-freshness) and again right +# before the deploy step (recheck-freshness). Writes an output declaring +# whether BUILD_SHA is still master's current tip, so a superseded run can +# skip cleanly instead of racing (and clobbering, or being force-cancelled +# by) a fresher run. +# +# Required env vars: +# BUILD_SHA - the commit SHA this run is building +# REPO - "owner/repo" to query, e.g. github.repository +# OUTPUT_NAME - the GITHUB_OUTPUT key to write, e.g. "is-current" +# GITHUB_OUTPUT - path to append outputs to (set by the Actions runner) +# Optional env vars: +# EVENT_NAME - if "workflow_dispatch", bypasses the check and always +# reports current, since a manual dispatch is a deliberate, +# one-off action rather than something racing other triggers +# GH_TOKEN - passed through to `gh`, needed to call the GitHub API + +set -euo pipefail + +if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then + echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT" + exit 0 +fi + +latest_sha="$(gh api "repos/${REPO}/commits/master" --jq .sha)" +if [ "${latest_sha}" = "${BUILD_SHA}" ]; then + echo "${OUTPUT_NAME}=true" >>"$GITHUB_OUTPUT" +else + echo "${OUTPUT_NAME}=false" >>"$GITHUB_OUTPUT" + echo "::notice::master has moved on to ${latest_sha} since ${BUILD_SHA} was triggered — skipping this stale run." +fi diff --git a/.github/workflows/scripts/check-docs-deploy-freshness.test.sh b/.github/workflows/scripts/check-docs-deploy-freshness.test.sh new file mode 100755 index 00000000000..c558c4986de --- /dev/null +++ b/.github/workflows/scripts/check-docs-deploy-freshness.test.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Exercises check-docs-deploy-freshness.sh against a stubbed `gh`, covering +# the dispatch-bypass, current-tip and stale-tip branches so the output +# contract (is-current / still-current) can't silently regress. Run +# directly, no extra tooling required: +# bash .github/workflows/scripts/check-docs-deploy-freshness.test.sh + +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +script_under_test="${script_dir}/check-docs-deploy-freshness.sh" + +failures=0 + +# Runs the script under test with a stubbed `gh` reporting $1 as master's +# latest sha, asserting that GITHUB_OUTPUT ends up containing exactly $4. +run_case() { + local case_name="$1" + local latest_sha="$2" + local build_sha="$3" + local event_name="$4" + local expected_line="$5" + + local workdir + workdir="$(mktemp -d)" + trap 'rm -rf "${workdir}"' RETURN + + # Fake `gh` that just echoes back the requested "latest" sha regardless of + # arguments, so the script under test never touches the network. + cat >"${workdir}/gh" <"${output_file}" + + if PATH="${workdir}:${PATH}" \ + GITHUB_OUTPUT="${output_file}" \ + OUTPUT_NAME="is-current" \ + REPO="apache/superset" \ + BUILD_SHA="${build_sha}" \ + EVENT_NAME="${event_name}" \ + GH_TOKEN="fake-token" \ + bash "${script_under_test}"; then + : + else + echo "FAIL (${case_name}): script exited non-zero" + failures=$((failures + 1)) + return + fi + + local actual + actual="$(cat "${output_file}")" + if [ "${actual}" = "${expected_line}" ]; then + echo "PASS (${case_name})" + else + echo "FAIL (${case_name}): expected '${expected_line}', got '${actual}'" + failures=$((failures + 1)) + fi +} + +# `gh` prints "should-not-be-called" for the dispatch case above the trick: +# it's never actually invoked since the bypass short-circuits before the +# `gh api` call, but the fake still needs a body. +run_case "workflow_dispatch bypasses the check" \ + "unused" "abc123" "workflow_dispatch" \ + "is-current=true" + +run_case "build sha matches master's tip" \ + "abc123" "abc123" "push" \ + "is-current=true" + +run_case "build sha is stale" \ + "def456" "abc123" "push" \ + "is-current=false" + +if [ "${failures}" -gt 0 ]; then + echo "${failures} case(s) failed" + exit 1 +fi + +echo "All cases passed" diff --git a/.github/workflows/superset-docs-deploy.yml b/.github/workflows/superset-docs-deploy.yml index b4e2e2bf07c..2a1514adb06 100644 --- a/.github/workflows/superset-docs-deploy.yml +++ b/.github/workflows/superset-docs-deploy.yml @@ -18,16 +18,6 @@ on: workflow_dispatch: {} -# Serialize deploys: the action pushes to apache/superset-site without -# rebasing, so concurrent runs race on the final push and the loser fails -# with `! [rejected] asf-site -> asf-site (fetch first)`. Cancel any -# in-progress run as soon as a newer one starts — the destination repo -# isn't touched until the final push step, so canceling mid-build is safe, -# and the freshest content always wins. -concurrency: - group: docs-deploy-asf-site - cancel-in-progress: true - permissions: contents: read actions: read @@ -48,17 +38,69 @@ jobs: env: SUPERSET_SITE_BUILD: ${{ (secrets.SUPERSET_SITE_BUILD != '' && secrets.SUPERSET_SITE_BUILD != '') || '' }} + + # Master gets frequent, sometimes bursty pushes, and each one can trigger a + # deploy attempt. Rather than let every superseded attempt get force-killed + # by the build-deploy concurrency group below (which shows up as a + # `cancelled` — i.e. red/failing-looking — check on that commit), have each + # run check up front whether it's still building master's current tip and, + # if not, skip cleanly. Deliberately outside the docs-deploy-asf-site + # concurrency group so it runs immediately for every trigger without + # blocking or being blocked by anything. + check-freshness: + runs-on: ubuntu-26.04 + outputs: + is-current: ${{ steps.check.outputs.is-current }} + steps: + # Sparse checkout: this job's only job is to be fast, so it fetches + # nothing but the freshness-check script itself. + - name: Checkout freshness-check script + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + sparse-checkout: | + .github/workflows/scripts + sparse-checkout-cone-mode: false + - name: "Check whether this is still master's current commit" + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + EVENT_NAME: ${{ github.event_name }} + REPO: ${{ github.repository }} + OUTPUT_NAME: is-current + run: .github/workflows/scripts/check-docs-deploy-freshness.sh + build-deploy: - needs: config + needs: [config, check-freshness] + # Only the run for master's current tip proceeds; anything superseded + # already skipped at check-freshness above instead of landing here. # For workflow_run triggers, only deploy when the triggering run originated # from this repository (not a fork), ensuring the checked-out code and any # local actions executed with deploy credentials are trusted. if: >- needs.config.outputs.has-secrets && + needs.check-freshness.outputs.is-current == 'true' && (github.event_name != 'workflow_run' || github.event.workflow_run.head_repository.full_name == github.repository) name: Build & Deploy runs-on: ubuntu-26.04 + # Serialize deploys: the action pushes to apache/superset-site without + # rebasing, so concurrent runs race on the final push and the loser fails + # with `! [rejected] asf-site -> asf-site (fetch first)`. Queue instead of + # canceling: a run that already passed check-freshness can still be + # sitting in the queue for a runner when a newer run starts and finishes + # first. cancel-in-progress would let that stale, queued run kill the + # newer run's in-progress deploy the moment it's finally scheduled, and + # then skip itself at the re-check below — losing the deploy entirely. + # Queuing means the stale run just waits its turn and then no-ops at the + # re-check, so the fresher content that already deployed is never + # clobbered or lost. The check-freshness gate above means it should be + # rare for more than one run to reach this point, so the queue stays + # short in practice. + concurrency: + group: docs-deploy-asf-site + cancel-in-progress: false steps: - uses: Kesin11/actions-timeline@57fc93f20c6da7fbc14063c6d24a2a5627c799ad # v3.2.0 with: @@ -130,7 +172,24 @@ jobs: working-directory: docs run: | yarn build + # The check-freshness job above narrows the window but doesn't close it: an + # older run can observe is-current=true, then sit through this build while a + # newer run's own freshness check also passes and it deploys and finishes + # first. If this (stale) run then wins entry into the concurrency group, it + # would overwrite the newer content that already deployed. Re-check right + # before the one step that actually mutates superset-site, so a stale run + # skips deploying instead of clobbering a fresher one that already ran. + - name: "Re-check freshness immediately before deploying" + id: recheck-freshness + if: github.event_name != 'workflow_dispatch' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + REPO: ${{ github.repository }} + OUTPUT_NAME: still-current + run: .github/workflows/scripts/check-docs-deploy-freshness.sh - name: deploy docs + if: github.event_name == 'workflow_dispatch' || steps.recheck-freshness.outputs.still-current == 'true' uses: ./.github/actions/github-action-push-to-another-repository env: API_TOKEN_GITHUB: ${{ secrets.SUPERSET_SITE_BUILD }}