diff --git a/.github/workflows/docs-trigger.yaml b/.github/workflows/docs-trigger.yaml index fb2627d1..6bdeab7c 100644 --- a/.github/workflows/docs-trigger.yaml +++ b/.github/workflows/docs-trigger.yaml @@ -5,17 +5,21 @@ # # SPDX-License-Identifier: MIT -name: Trigger Docs on Tag +name: Trigger Docs on Release +# Rebuilds the documentation after the Release workflow succeeds, so a tag it +# refuses to publish does not trigger a documentation build. on: - push: - tags: ['v*'] + workflow_run: + workflows: [Release] + types: [completed] permissions: actions: write jobs: trigger-docs: + if: github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-latest steps: - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e241ecd9..7214af2d 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -31,6 +31,23 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Fetch all history for sphinx-multiversion + # sphinx-multiversion builds every version tag in the checkout. A tag + # gets its GitHub release only after the Release workflow's tests and + # PyPI upload succeed, so drop tags without one: a release still in + # progress or refused by its tests is not documented. + - name: Drop unreleased version tags + env: + GH_TOKEN: ${{ github.token }} + run: | + released=$(gh api --paginate "repos/$GITHUB_REPOSITORY/releases?per_page=100" \ + --jq '.[] | select(.draft | not) | .tag_name') + if [[ -z "$released" ]]; then + echo "::error::No published GitHub releases found" + exit 1 + fi + git tag --list 'v*' | while read -r tag; do + grep -qxF "$tag" <<< "$released" || git tag --delete "$tag" + done - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 5fc6bd67..e136e6a9 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,13 +12,24 @@ on: tags: - 'v*' -permissions: - id-token: write - contents: write +permissions: {} jobs: + # Runs every suite on every supported Python version for the tagged commit; + # nothing is built or published unless all of them pass. + test: + uses: ./.github/workflows/test.yaml + permissions: + contents: read + id-token: write + pull-requests: read + release: + needs: test runs-on: ubuntu-latest + permissions: + id-token: write + contents: write env: PYTHON_VERSION: '3.12' diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 22f3433b..63c124f7 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -18,13 +18,21 @@ on: - 'docs/**' - '**.md' # The scheduled run executes every suite, including the ones that pull - # requests only run when related files change. + # requests only run when related files change, on the newest Python version. schedule: - cron: '0 0 * * 0' - # Runs every suite on the selected branch: before a release, on demand for - # a pull request, and to refresh the README status badge after a transient - # failure on the default branch. + # Runs every suite on the selected branch: on demand for a pull request, and + # to refresh the README status badge after a transient failure on the + # default branch. workflow_dispatch: + inputs: + python-versions: + description: Comma-separated Python versions, such as 3.12 or 3.11,3.14; empty for every supported version + type: string + default: '' + # The Release workflow runs every suite on every supported Python version + # before publishing. + workflow_call: permissions: id-token: write @@ -60,8 +68,9 @@ jobs: # requests run none. A ready pull request always runs the PyAthena suite; it # runs the SQLAlchemy tests (the compliance suites and the PyAthena suite's # SQLAlchemy tests) and the Spark tests only when their code, tests, - # dependencies, or this workflow change. Pull requests test the newest - # Python version only; the schedule and dispatch test every version. + # dependencies, or this workflow change. Pull requests and the schedule test + # the newest Python version; a dispatch tests the requested versions or + # every version, and the Release workflow every version. changes: if: >- github.event_name != 'pull_request' || @@ -81,19 +90,36 @@ jobs: EVENT_NAME: ${{ github.event_name }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} + REQUESTED_VERSIONS: ${{ inputs.python-versions }} # Every supported version, oldest first; keep in sync with the # pyproject.toml classifiers. PYTHON_VERSIONS: '["3.10", "3.11", "3.12", "3.13", "3.14"]' run: | + case "$EVENT_NAME" in + pull_request | schedule) + versions=$(jq -c '[last]' <<< "$PYTHON_VERSIONS") + ;; + workflow_dispatch) + versions=$(jq -c --arg requested "$REQUESTED_VERSIONS" ' + ($requested | split(",") | map(gsub("\\s"; "")) | map(select(. != "")) | unique) as $selected + | if $selected == [] then . + elif ($selected - .) == [] then $selected + else error("unsupported Python versions: \($selected - . | join(", "))") + end' <<< "$PYTHON_VERSIONS") + ;; + *) + # The Release workflow (a workflow_call from a tag push). + versions=$(jq -c '.' <<< "$PYTHON_VERSIONS") + ;; + esac + echo "python-versions=$versions" >> "$GITHUB_OUTPUT" if [[ "$EVENT_NAME" != "pull_request" ]]; then { - echo "python-versions=$(jq -c '.' <<< "$PYTHON_VERSIONS")" echo "sqla=true" echo "spark=true" } >> "$GITHUB_OUTPUT" exit 0 fi - echo "python-versions=$(jq -c '[last]' <<< "$PYTHON_VERSIONS")" >> "$GITHUB_OUTPUT" files=$(gh api "repos/$REPO/pulls/$PR_NUMBER/files" --paginate --jq '.[].filename') printf 'Changed files:\n%s\n' "$files" shared='^(\.github/workflows/test(-suite)?\.yaml|justfile|pyproject\.toml|uv\.lock)$' diff --git a/docs/testing.md b/docs/testing.md index e7d5e404..96cc8b3f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -155,7 +155,9 @@ It runs the offline checks (`just lint`) on each of them, including Drafts and e | --- | --- | --- | --- | --- | | Draft pull request | No | No | No | None | | Ready pull request from a branch of this repository | Yes | When related files change | When related files change | Newest supported | -| Weekly schedule and manual dispatch | Yes | Yes | Yes | All supported | +| Weekly schedule | Yes | Yes | Yes | Newest supported | +| Manual dispatch | Yes | Yes | Yes | Requested, or all supported | +| Release tag (Release workflow) | Yes | Yes | Yes | All supported | The SQLAlchemy tests are the compliance suites and the PyAthena suite's `tests/pyathena/sqlalchemy/` and `tests/pyathena/aio/sqlalchemy/`. The Spark tests are the PyAthena suite's `tests/pyathena/spark/` and `tests/pyathena/aio/spark/`. @@ -164,12 +166,16 @@ For the SQLAlchemy tests, the related files are `pyathena/sqlalchemy/`, `pyathen For the Spark tests, they are `pyathena/spark/`, `pyathena/aio/spark/`, and their PyAthena suite test directories. Changes to `pyproject.toml`, `uv.lock`, `justfile`, or the Test workflows run both. For a pull request from a branch of this repository that still changes files other than `docs/` and Markdown, marking the Draft ready for review starts the AWS jobs, and converting it back to Draft cancels AWS jobs still running. -To run every suite on every supported Python version on a branch, dispatch the workflow: +To run every suite on a branch, dispatch the workflow; it tests every supported Python version unless `python-versions` lists some of them: ```bash gh workflow run test.yaml --ref +gh workflow run test.yaml --ref -f python-versions=3.11,3.14 ``` +The Release workflow runs the same suites on every supported Python version for the tagged commit before building, and publishes nothing unless all of them pass. +If they fail, nothing is published, and the documentation leaves out the tag because it only lists tags with a GitHub release; delete the tag, fix the failure, and push the tag again. + Project policy excludes external-fork pull requests from AWS integration CI. Maintainers do not approve those jobs as a substitute for contributor testing. Checks without AWS access may still run on a fork pull request.