From 65a0c65dd31bcf59b2e11530c078693805303c23 Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:05:01 +0200 Subject: [PATCH 1/4] ci: centralize Quarto docs build into reusable quarto-build.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The quartodoc -> interlinks -> render -> publish sequence was duplicated between docs.yml and release.yml, and the two used different concurrency groups while both pushing to gh-pages — the same race spotforecast2 hit ("cannot lock ref 'refs/heads/gh-pages'", fixed there in 8f8c139). - New reusable workflow quarto-build.yml (workflow_call) with a job-level gh-pages-deploy concurrency group shared by every caller, setup-uv caching (docs.yml previously had none, so each manual rebuild re-downloaded torch & friends), and uv sync instead of uv pip install. - docs.yml becomes a thin workflow_dispatch caller; a new deploy=false input allows render-only dry runs on branches. - release.yml splits into release (semantic-release + PyPI) and docs jobs; the docs job builds from main so the rendered site shows the version just bumped by the chore(release) commit, matching the old in-place render behavior. Release-job actions bumped to Node 24 releases. Playbook extracted from spotforecast2 (5212a10, 8f8c139) and spotforecast2-safe (40ca6947). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/docs.yml | 66 +++++---------------- .github/workflows/quarto-build.yml | 93 ++++++++++++++++++++++++++++++ .github/workflows/release.yml | 59 +++++++++---------- 3 files changed, 135 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/quarto-build.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 660c1684..622c5afd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,66 +2,30 @@ # .github/workflows/docs.yml — Manual Documentation Rebuild # ============================================================================= # Use workflow_dispatch to manually rebuild docs without a release. -# Normal doc deployment happens automatically inside release.yml. +# Normal doc deployment happens automatically via release.yml; both delegate +# the actual build to the reusable quarto-build.yml workflow. +# Set deploy=false for a render-only dry run (e.g. to verify doc changes on +# a branch without touching the production gh-pages site). # ============================================================================= name: Documentation on: workflow_dispatch: + inputs: + deploy: + description: "Publish the rendered site to gh-pages" + type: boolean + default: true permissions: read-all -concurrency: - group: docs-deploy - cancel-in-progress: false - -env: - QUARTO_VERSION: "1.8.27" - PYTHON_VERSION: "3.13" - jobs: - deploy-docs: - name: Build & Deploy Documentation - runs-on: ubuntu-latest + docs: + name: Documentation + uses: ./.github/workflows/quarto-build.yml + with: + deploy: ${{ inputs.deploy }} permissions: contents: write - - steps: - - name: Checkout (full history) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - with: - fetch-depth: 0 - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Set up uv - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 - - - name: Install dependencies - run: uv pip install --system -e ".[dev,docs]" - - - name: Install Quarto CLI ${{ env.QUARTO_VERSION }} - uses: quarto-dev/quarto-actions/setup@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 - with: - version: ${{ env.QUARTO_VERSION }} - - - name: Generate API reference with quartodoc - run: | - uv run python docs/quartodoc_build.py - uv run quartodoc interlinks - - - name: Render Quarto site - run: uv run quarto render - - - name: Deploy to GitHub Pages - uses: quarto-dev/quarto-actions/publish@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 - with: - target: gh-pages - path: _site - render: "false" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + secrets: inherit diff --git a/.github/workflows/quarto-build.yml b/.github/workflows/quarto-build.yml new file mode 100644 index 00000000..8e12435e --- /dev/null +++ b/.github/workflows/quarto-build.yml @@ -0,0 +1,93 @@ +# ============================================================================= +# .github/workflows/quarto-build.yml — Reusable Documentation Build +# ============================================================================= +# Single source of truth for the quartodoc + Quarto render + gh-pages deploy +# sequence. Called via workflow_call from: +# - release.yml (docs job, after every release on main) +# - docs.yml (manual rebuild via workflow_dispatch) +# The `deploy` input allows render-only dry runs (CI verification of doc +# changes on a branch) without publishing to the production gh-pages site. +# ============================================================================= + +name: Quarto Build + +on: + workflow_call: + inputs: + deploy: + description: "Publish the rendered site to gh-pages (false = render-only dry run)" + type: boolean + default: true + ref: + description: "Git ref to build from (empty = the triggering commit)" + type: string + default: "" + +permissions: read-all + +env: + QUARTO_VERSION: "1.8.27" + PYTHON_VERSION: "3.13" + +jobs: + build-docs: + name: Build & Deploy Documentation + runs-on: ubuntu-latest + permissions: + contents: write + # Serialize every gh-pages push across ALL callers (release.yml and + # docs.yml). With per-workflow groups the two workflows can race on the + # gh-pages ref and the loser fails with "cannot lock ref". + concurrency: + group: gh-pages-deploy + cancel-in-progress: false + + steps: + - name: Checkout (full history) + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ inputs.ref }} + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + # setup-uv >= v6 enables caching by default on GitHub-hosted runners; + # key on the committed lockfile + pyproject so torch & friends are + # restored instead of re-downloaded. + - name: Set up uv + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + cache-dependency-glob: | + uv.lock + pyproject.toml + + - name: Install dependencies + run: uv sync --extra dev --extra docs + + - name: Install Quarto CLI ${{ env.QUARTO_VERSION }} + uses: quarto-dev/quarto-actions/setup@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 + with: + version: ${{ env.QUARTO_VERSION }} + + - name: Generate API reference with quartodoc + run: | + uv run python docs/quartodoc_build.py + uv run quartodoc interlinks + + # `freeze: auto` + the committed _freeze/ directory means unchanged + # {python} blocks reuse stored outputs instead of re-executing. + - name: Render Quarto site + run: uv run quarto render + + - name: Deploy to GitHub Pages + if: inputs.deploy + uses: quarto-dev/quarto-actions/publish@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 + with: + target: gh-pages + path: _site + render: "false" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61d53a01..433863d5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,13 @@ # ============================================================================= # Triggers on every push to main. Tests are NOT run here — they are gated on # PRs by ci.yml (the required fast check) and nightly (full suite). -# 1. Semantic-release checks conventional commits — if a releasable change -# is found (feat:, fix:, etc.) it bumps version, publishes to PyPI, -# and creates a GitHub release. -# 2. Builds and deploys quartodoc documentation to GitHub Pages. +# 1. release job: semantic-release checks conventional commits — if a +# releasable change is found (feat:, fix:, etc.) it bumps version, +# publishes to PyPI, and creates a GitHub release. +# 2. docs job: delegates to the reusable quarto-build.yml workflow, which +# builds and deploys the quartodoc documentation to GitHub Pages +# (serialized against manual docs.yml runs via the shared +# gh-pages-deploy concurrency group). # ============================================================================= name: Release @@ -34,31 +37,30 @@ jobs: steps: # ── Checkout ────────────────────────────────────────────────────── - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # ── Python + uv ────────────────────────────────────────────────── - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: - enable-cache: true cache-dependency-glob: | uv.lock pyproject.toml - name: Install dependencies - run: uv sync --extra dev --extra docs + run: uv sync --extra dev # ── Semantic Release ────────────────────────────────────────────── # NOTE: tests are intentionally NOT run here. The fast suite is the # required check on every PR to main (ci.yml) and the full suite runs # nightly, so re-running them on the merge commit only duplicated cost # without adding signal. This pipeline publishes; it does not re-test. - - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 22 @@ -87,25 +89,18 @@ jobs: packages-dir: dist/ skip-existing: true - # ── Documentation ───────────────────────────────────────────────── - - name: Install Quarto CLI - uses: quarto-dev/quarto-actions/setup@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 - with: - version: "1.8.27" - - - name: Generate API reference - run: | - uv run --extra docs python docs/quartodoc_build.py - uv run --extra docs quartodoc interlinks - - - name: Render documentation - run: uv run --extra docs quarto render - - - name: Deploy to GitHub Pages - uses: quarto-dev/quarto-actions/publish@8a96df13519ee81fd526f2dfca5962811136661b # v2.2.0 - with: - target: gh-pages - path: _site - render: "false" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # ── Documentation (reusable workflow, shared with docs.yml) ─────────── + # Builds from `main` rather than the triggering commit: by the time this + # job starts, semantic-release has already pushed its "chore(release)" + # version-bump commit, so the rendered docs show the just-released version + # (same behavior as the old in-place render after the bump). + docs: + name: Documentation + needs: release + uses: ./.github/workflows/quarto-build.yml + with: + deploy: true + ref: main + permissions: + contents: write + secrets: inherit From 60179f6619602029aa0f67fc379081a3b6580a99 Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:05:02 +0200 Subject: [PATCH 2/4] chore(ci): migrate JS actions to Node 24 and trim scorecard push trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub forces all Node 20 actions onto Node 24 on 2026-06-16 and removes the Node 20 runner on 2026-09-16. Bump every pinned JavaScript action to its current Node 24 release (still SHA-pinned, version comment updated), matching spotforecast2-safe 04dbabc8: actions/checkout v4.x -> v6.0.2 actions/setup-python v5.6.0 -> v6.2.0 astral-sh/setup-uv v5.4.2 -> v8.1.0 actions/upload-artifact v7.0.0 -> v7.0.1 github/codeql-action unified on v3.32.6 (scorecard upload-sarif was on v3.28.5; codeql.yml already on v3.32.6) scorecard.yml: drop the push-to-main trigger — the full supply-chain scan re-ran on every merge without changing the score between weekly runs; the weekly schedule + branch_protection_rule triggers remain. Known follow-up: cycjimmy/semantic-release-action is pinned at v4.2.2 (node20 declaration; still runs after the forced migration). Bumping to v6 changes the underlying semantic-release major and is deferred. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/codeql.yml | 2 +- .github/workflows/scorecard.yml | 9 +++++---- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfa8d14f..ae0a4d3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,13 +43,13 @@ jobs: contents: read steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: | @@ -71,13 +71,13 @@ jobs: contents: read steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: | @@ -99,13 +99,13 @@ jobs: contents: read steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.13" - - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2 + - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 with: enable-cache: true cache-dependency-glob: | diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 275b7e0c..8a8bc024 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Initialize CodeQL uses: github/codeql-action/init@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 30b3ebd7..6db6aacd 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -4,12 +4,13 @@ name: Scorecard supply-chain security +# Weekly schedule + branch-protection changes only. The previous push trigger +# re-ran the full supply-chain scan on every merge to main, which added CI +# minutes without changing the score between weekly runs. on: branch_protection_rule: schedule: - cron: '20 7 * * 2' - push: - branches: ["main"] # Declare default permissions as read only. permissions: @@ -40,13 +41,13 @@ jobs: publish_results: true - name: Upload artifact - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 - name: Upload to code-scanning - uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.5 + uses: github/codeql-action/upload-sarif@820e3160e279568db735cee8ed8f8e77a6da7818 # v3.32.6 with: sarif_file: results.sarif From ac3c1dcbec7e3c813cc74711256e9bd5753de935 Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:08:35 +0200 Subject: [PATCH 3/4] fix(ci): drop workflow-level permissions from reusable quarto-build.yml A called workflow may not request more permissions than the caller's job grants. The workflow-level `permissions: read-all` demanded read on every scope while docs.yml/release.yml grant only `contents: write`, so every dispatch died with startup_failure ("Invalid workflow file ... The workflow is requesting ..."). The job-level `contents: write` declaration is the complete requirement. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/quarto-build.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/quarto-build.yml b/.github/workflows/quarto-build.yml index 8e12435e..d2763017 100644 --- a/.github/workflows/quarto-build.yml +++ b/.github/workflows/quarto-build.yml @@ -23,7 +23,10 @@ on: type: string default: "" -permissions: read-all +# NOTE: no workflow-level `permissions:` here. A called workflow may not +# request more than its caller's job grants; `read-all` would demand read on +# every scope while callers grant only `contents: write` (startup_failure). +# The job below declares exactly what it needs. env: QUARTO_VERSION: "1.8.27" From 321226c555b264b8570e815f0d65c05d770dd6da Mon Sep 17 00:00:00 2001 From: bartzbeielstein <32470350+bartzbeielstein@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:11:34 +0200 Subject: [PATCH 4/4] chore: sync uv.lock self-version with 0.12.5 release semantic-release bumps pyproject.toml in its chore(release) commit but does not re-lock, so uv.lock still recorded spotoptim 0.12.4. Every `uv run` then re-locks the file, which dirtied the working tree and made the pre-push pytest hook fail with "files were modified by this hook" despite all tests passing. Co-Authored-By: Claude Opus 4.8 (1M context) --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index da9e5e45..61052f15 100644 --- a/uv.lock +++ b/uv.lock @@ -2926,7 +2926,7 @@ wheels = [ [[package]] name = "spotoptim" -version = "0.12.4" +version = "0.12.5" source = { editable = "." } dependencies = [ { name = "black" },