From cb6cdcd817963d08aa69152089f19c45d42e0ad6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:31:24 +0200 Subject: [PATCH 01/11] feat(ci): automate PyPI smoke test Signed-off-by: F.N. Claessen --- .github/workflows/release.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 09b7e4e0d3..bfb6e071db 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -68,3 +68,35 @@ jobs: uses: pypa/gh-action-pypi-publish@release/v1 with: packages-dir: dist/ + + pypi-smoke-test: + runs-on: ubuntu-latest + needs: + - pypi-publish + permissions: + contents: read + + steps: + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + + - name: Wait for PyPI to index the release + run: sleep 60 + + - name: Install from PyPI in a fresh venv + run: | + VERSION="${GITHUB_REF_NAME#v}" + python -m venv /tmp/fm-smoke-venv + /tmp/fm-smoke-venv/bin/pip install --no-cache-dir "flexmeasures==${VERSION}" + + - name: Verify installed version and importability + run: | + VERSION="${GITHUB_REF_NAME#v}" + /tmp/fm-smoke-venv/bin/python -c " + from flexmeasures import __version__ + expected = '${VERSION}' + assert __version__ == expected, f'expected {expected}, got {__version__}' + print(f'OK: flexmeasures {__version__} importable and version matches') + " From 122f3d03152bb98a53b24f9889e251d2254b62db Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:32:18 +0200 Subject: [PATCH 02/11] feat(ci): add Docker Hub release workflow Signed-off-by: F.N. Claessen --- .github/workflows/docker-publish.yml | 72 ++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 .github/workflows/docker-publish.yml diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000000..e73c78c42e --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,72 @@ + +# This workflow builds and pushes a Docker image to Docker Hub whenever a +# GitHub Release is published, tagging it with the release version and, +# for stable (non-pre-release) releases, also as `latest`. +# +# Requires the DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets +# to be configured under Settings -> Secrets and variables -> Actions. + +name: Publish Docker image + +on: + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Git tag to build/push (e.g. v1.0.0). Required when run manually." + required: false + +permissions: + contents: read + +jobs: + docker-publish: + runs-on: ubuntu-latest + steps: + - name: Determine version + id: version + run: | + TAG="${{ github.event.release.tag_name || inputs.tag }}" + if [ -z "$TAG" ]; then + echo "No tag available (release.tag_name and inputs.tag are both empty)." >&2 + exit 1 + fi + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + + - uses: actions/checkout@v4 + with: + ref: ${{ steps.version.outputs.tag }} + fetch-depth: 0 # full history + tags, required for hatch-vcs to resolve the version + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - uses: docker/setup-buildx-action@v3 + + - name: Build and push (versioned tag) + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: lfenergy/flexmeasures:${{ steps.version.outputs.tag }} + + - name: Build and push (latest) + if: ${{ !github.event.release.prerelease }} + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: lfenergy/flexmeasures:latest + + - name: Summary + run: | + echo "Pushed lfenergy/flexmeasures:${{ steps.version.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY" + if [ "${{ github.event.release.prerelease }}" != "true" ]; then + echo "Pushed lfenergy/flexmeasures:latest" >> "$GITHUB_STEP_SUMMARY" + fi From 0aa0e20f90b7c6d2e5226c051b93f3fde1873249 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:33:16 +0200 Subject: [PATCH 03/11] feat(ci): add script to list PRs since the given (or latest) git tag Signed-off-by: F.N. Claessen --- bin/list-merged-prs-since-tag.sh | 25 +++++++++++++++++++++++++ pyproject.toml | 7 +++++++ 2 files changed, 32 insertions(+) create mode 100755 bin/list-merged-prs-since-tag.sh diff --git a/bin/list-merged-prs-since-tag.sh b/bin/list-merged-prs-since-tag.sh new file mode 100755 index 0000000000..fb640aa90c --- /dev/null +++ b/bin/list-merged-prs-since-tag.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Lists merged PRs since the given (or latest) git tag, to help a human +# assemble documentation/changelog.rst / cli/change_log.rst / api/change_log.rst +# entries. Read-only: does not write to any changelog file or commit anything. +# +# Usage: bin/list-merged-prs-since-tag.sh [] +set -euo pipefail + +SINCE="${1:-$(git describe --tags --abbrev=0)}" +SINCE_DATE=$(git log -1 --format=%aI "$SINCE") + +echo "Merged PRs since $SINCE ($SINCE_DATE):" +echo + +gh pr list \ + --repo FlexMeasures/flexmeasures \ + --state merged \ + --search "merged:>=$SINCE_DATE" \ + --limit 200 \ + --json number,title,url \ + --template '{{range .}}* {{.title}} [see `PR #{{.number}} <{{.url}}>`_]{{"\n"}}{{end}}' + +echo +echo "Review, categorize into New features / Infrastructure / Support / Bugfixes," +echo "and paste into documentation/changelog.rst (and cli/change_log.rst, api/change_log.rst if relevant)." diff --git a/pyproject.toml b/pyproject.toml index dabffc264c..8dcf8c38d8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -193,6 +193,13 @@ help = "Generate OpenAPI specifications" script = "flexmeasures.api.scripts.generate_open_api_specs" env = {FLEXMEASURES_ENV = "documentation", FLEXMEASURES_PLUGINS = ""} +[tool.poe.tasks.changelog-check] +help = "List merged PRs since the last tag, to help assemble changelog.rst entries (does not write any files)." +shell = "bin/list-merged-prs-since-tag.sh" +args = [ + {name = "since", help = "Git tag to diff from (default: latest tag reachable from HEAD)", default = ""}, +] + [dependency-groups] dev = [ "black==24.8.0", From e6b55d0e9e1e50d8e2465f2d32d971129de48bc3 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:33:52 +0200 Subject: [PATCH 04/11] docs(ci): add dev section on the release process Signed-off-by: F.N. Claessen --- documentation/dev/release_process.rst | 68 +++++++++++++++++++++++++++ documentation/index.rst | 1 + 2 files changed, 69 insertions(+) create mode 100644 documentation/dev/release_process.rst diff --git a/documentation/dev/release_process.rst b/documentation/dev/release_process.rst new file mode 100644 index 0000000000..050919469e --- /dev/null +++ b/documentation/dev/release_process.rst @@ -0,0 +1,68 @@ +Release process +================ + +This page describes how FlexMeasures releases are cut, and which parts are automated versus manual. +The canonical, most detailed checklist still lives in `FlexMeasures/tsc RELEASE.md `_. +This page summarizes that flow and points out where CI now does the mechanical work. + +Versioning is derived entirely from git tags via ``hatch-vcs`` (see ``pyproject.toml``'s ``[tool.hatch.version]``), so there is no file where a version string needs to be bumped by hand. + +1. Prepare and test (manual) +----------------------------- + +* Ensure the corresponding GitHub milestone is complete and the version number choice matches the changes made. +* ``uv sync --group dev --group test`` and ``uv run poe test``. +* For MINOR/MAJOR releases, do manual QA using the Docker Compose stack (tutorials, UI, HEMS client), and write the accompanying blog post. + +2. Assemble the changelog (semi-automated) +------------------------------------------- + +Run: + +.. code-block:: bash + + uv run poe changelog-check + +This lists merged PRs since the last tag (via ``bin/list-merged-prs-since-tag.sh``), pre-formatted as changelog bullets. +It is read-only: it does not edit or commit anything. +Review, categorize each entry into *New features* / *Infrastructure / Support* / *Bugfixes*, +and paste them into ``documentation/changelog.rst`` (and ``documentation/cli/change_log.rst`` / ``documentation/api/change_log.rst`` where relevant). + +3. Commit, tag, and release (manual — requires GPG signing) +-------------------------------------------------------------- + +.. code-block:: bash + + git commit -S -sam "changelog updates for v" + git push + git tag -s -a v -m "" + git push --tags + +Then create the GitHub Release from the new tag. This step is intentionally manual: GPG signing requires the maintainer's personal key, which cannot be delegated to CI without exporting private key material into repository secrets. + +4. Automated publishing (CI) +------------------------------- + +Publishing the GitHub Release (step 3) triggers two workflows: + +* ``.github/workflows/release.yml`` builds the package and publishes it to PyPI via trusted (OIDC) publishing, then runs a ``pypi-smoke-test`` job that installs the just-published version into a fresh virtual environment and verifies ``flexmeasures.__version__`` matches. +* ``.github/workflows/docker-publish.yml`` builds and pushes the Docker image to Docker Hub, tagged ``lfenergy/flexmeasures:v``, and additionally as ``lfenergy/flexmeasures:latest`` for stable (non-pre-release) releases. + +Check the Actions tab for both workflow runs to confirm they succeeded; also spot-check that the new version shows up on `PyPI `_, `Docker Hub `_, and that the ReadTheDocs build for the new tag completed. + +.. note:: + The ``docker-publish.yml`` workflow can also be re-run manually (``workflow_dispatch``, with a ``tag`` input) if a run needs to be retried. + +5. Announce (manual) +---------------------- + +Publish the blog post (MINOR/MAJOR only), and announce on Mastodon, the mailing list, and the LF Energy Slack. + +6. Post-release (manual, MINOR/MAJOR only) +--------------------------------------------- + +* Start the next development cycle: an empty, signed commit ``Start v`` and tag ``v.dev0``. +* Update the milestone on GitHub, and add changelog placeholders for upcoming releases. +* Open a dependency-upgrade PR tagged ``dependency-hygiene``. + +These are infrequent, low-risk manual steps and involve a judgment call (what the next version number should be), so they are not automated. diff --git a/documentation/index.rst b/documentation/index.rst index 42312e7627..e987794db8 100644 --- a/documentation/index.rst +++ b/documentation/index.rst @@ -271,6 +271,7 @@ In :ref:`getting_started`, we have some helpful tips how to dive into this docum dev/api dev/connection-secrets dev/automated-deploy-via-GHActions + dev/release_process .. autosummary:: :caption: Code Documentation From d6d1aa56a7e3dd612629a2c761b2f9caf4be3fcc Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:38:56 +0200 Subject: [PATCH 05/11] refactor(ci): move changelog helper script into ci/ folder Keep it alongside the other CI scripts (ci/setup-postgres.sh etc.) rather than a standalone bin/ directory. Co-Authored-By: Claude Sonnet 5 --- {bin => ci}/list-merged-prs-since-tag.sh | 0 documentation/dev/release_process.rst | 2 +- pyproject.toml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename {bin => ci}/list-merged-prs-since-tag.sh (100%) diff --git a/bin/list-merged-prs-since-tag.sh b/ci/list-merged-prs-since-tag.sh similarity index 100% rename from bin/list-merged-prs-since-tag.sh rename to ci/list-merged-prs-since-tag.sh diff --git a/documentation/dev/release_process.rst b/documentation/dev/release_process.rst index 050919469e..73ee9b4181 100644 --- a/documentation/dev/release_process.rst +++ b/documentation/dev/release_process.rst @@ -23,7 +23,7 @@ Run: uv run poe changelog-check -This lists merged PRs since the last tag (via ``bin/list-merged-prs-since-tag.sh``), pre-formatted as changelog bullets. +This lists merged PRs since the last tag (via ``ci/list-merged-prs-since-tag.sh``), pre-formatted as changelog bullets. It is read-only: it does not edit or commit anything. Review, categorize each entry into *New features* / *Infrastructure / Support* / *Bugfixes*, and paste them into ``documentation/changelog.rst`` (and ``documentation/cli/change_log.rst`` / ``documentation/api/change_log.rst`` where relevant). diff --git a/pyproject.toml b/pyproject.toml index 8dcf8c38d8..63d917dcf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -195,7 +195,7 @@ env = {FLEXMEASURES_ENV = "documentation", FLEXMEASURES_PLUGINS = ""} [tool.poe.tasks.changelog-check] help = "List merged PRs since the last tag, to help assemble changelog.rst entries (does not write any files)." -shell = "bin/list-merged-prs-since-tag.sh" +shell = "ci/list-merged-prs-since-tag.sh" args = [ {name = "since", help = "Git tag to diff from (default: latest tag reachable from HEAD)", default = ""}, ] From 50e8cf13626b375f08b55edc3e9a19f07948aa81 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 22:42:10 +0200 Subject: [PATCH 06/11] feat(ci): add manually-triggered QA workflow for tutorials and HEMS Runs tutorials 1-4 and the HEMS walkthrough against the local docker compose stack, using the runner scripts kept in FlexMeasures/tsc and the HEMS example kept in FlexMeasures/flexmeasures-client (not duplicated here). Manual trigger only, to avoid slowing down every push while still cutting release QA toil. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/qa-release.yml | 94 +++++++++++++++++++++++++++ documentation/dev/release_process.rst | 4 +- 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/qa-release.yml diff --git a/.github/workflows/qa-release.yml b/.github/workflows/qa-release.yml new file mode 100644 index 0000000000..bf2545524a --- /dev/null +++ b/.github/workflows/qa-release.yml @@ -0,0 +1,94 @@ + +# Manually-triggered QA workflow for release testing. It spins up the local +# docker compose stack, then runs: +# - the toy tutorials 1-4, using the runner scripts kept in the +# FlexMeasures/tsc repo (tsc/scripts/run-tutorial*-in-docker.sh) +# - the HEMS walkthrough, using the example kept in the +# FlexMeasures/flexmeasures-client repo (examples/HEMS/HEMS_setup.py) +# +# This does not replace manual QA (e.g. UI login/graph checks, tutorial 5, +# exploratory testing) -- see documentation/dev/release_process.rst. + +name: QA (release) + +on: + workflow_dispatch: + inputs: + tsc-ref: + description: "Branch/tag/ref to use from FlexMeasures/tsc" + required: false + default: "main" + flexmeasures-client-ref: + description: "Branch/tag/ref to use from FlexMeasures/flexmeasures-client" + required: false + default: "main" + +permissions: + contents: read + +jobs: + qa: + runs-on: ubuntu-latest + steps: + - name: Checkout flexmeasures + uses: actions/checkout@v4 + + - name: Build and start the docker compose stack + run: docker compose up --build --detach --wait --wait-timeout 300 + + - name: Checkout tsc tutorial scripts + uses: actions/checkout@v4 + with: + repository: FlexMeasures/tsc + ref: ${{ inputs.tsc-ref }} + sparse-checkout: tsc/scripts + sparse-checkout-cone-mode: false + path: tsc + + - name: Run tutorials 1-4 + run: | + for script in tsc/tsc/scripts/run-tutorial-in-docker.sh \ + tsc/tsc/scripts/run-tutorial2-in-docker.sh \ + tsc/tsc/scripts/run-tutorial3-in-docker.sh \ + tsc/tsc/scripts/run-tutorial4-in-docker.sh; do + echo "::group::Running $script" + # The scripts use `docker exec -it`, which requires a tty we don't have here. + sed 's/docker exec -it/docker exec -i/' "$script" | bash + echo "::endgroup::" + done + + - name: Create HEMS admin account and user + run: | + OUTPUT=$(docker compose exec -T server flexmeasures add account --name "HEMS Admin Org") + echo "$OUTPUT" + ACCOUNT_ID=$(echo "$OUTPUT" | grep -oP '(?<=ID: )\d+') + docker compose exec -T server bash -c \ + "printf 'admin\nadmin\n' | flexmeasures add user --username admin --email admin@admin.com --account $ACCOUNT_ID --roles admin" + + - name: Checkout flexmeasures-client + uses: actions/checkout@v4 + with: + repository: FlexMeasures/flexmeasures-client + ref: ${{ inputs.flexmeasures-client-ref }} + path: flexmeasures-client + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - name: Run the HEMS example against the running stack + working-directory: flexmeasures-client + run: | + uv sync + cd examples/HEMS + uv run --project .. python3 HEMS_setup.py + + - name: Show stack logs on failure + if: failure() + run: docker compose logs + + - name: Tear down the stack + if: always() + run: docker compose down --volumes diff --git a/documentation/dev/release_process.rst b/documentation/dev/release_process.rst index 73ee9b4181..25cd0437dd 100644 --- a/documentation/dev/release_process.rst +++ b/documentation/dev/release_process.rst @@ -12,7 +12,9 @@ Versioning is derived entirely from git tags via ``hatch-vcs`` (see ``pyproject. * Ensure the corresponding GitHub milestone is complete and the version number choice matches the changes made. * ``uv sync --group dev --group test`` and ``uv run poe test``. -* For MINOR/MAJOR releases, do manual QA using the Docker Compose stack (tutorials, UI, HEMS client), and write the accompanying blog post. +* For MINOR/MAJOR releases, do QA using the Docker Compose stack, and write the accompanying blog post. + + Tutorials 1-4 and the HEMS walkthrough can be run via the manually-triggered ``QA (release)`` GitHub Actions workflow (``.github/workflows/qa-release.yml``). It spins up the local docker compose stack and runs the tutorial runner scripts from `FlexMeasures/tsc `_ and the HEMS example from `FlexMeasures/flexmeasures-client `_ (both kept in their own repos, not duplicated here). Trigger it from the Actions tab; it does not run automatically. This does not replace manual UI login/graph checks, tutorial 5, or exploratory QA. 2. Assemble the changelog (semi-automated) ------------------------------------------- From 375d6fa2f9272ae4777322bf474956095b9b953c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 1 Jul 2026 23:26:49 +0200 Subject: [PATCH 07/11] feat(ci): make qa-release.yml also validate HEMS report generation The HEMS example's report-generation step shells out to a local flexmeasures CLI, which doesn't work as-is against a dockerized server. Bind-mount examples/HEMS/configs into the server container and point FLEXMEASURES_CLI_CMD/FLEXMEASURES_CLI_CONFIG_DIR (added in FlexMeasures/flexmeasures-client#qa/hems-report-cli-override) at it, so report generation runs for real instead of being silently skipped. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/qa-release.yml | 40 +++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/.github/workflows/qa-release.yml b/.github/workflows/qa-release.yml index bf2545524a..1539f54492 100644 --- a/.github/workflows/qa-release.yml +++ b/.github/workflows/qa-release.yml @@ -4,7 +4,10 @@ # - the toy tutorials 1-4, using the runner scripts kept in the # FlexMeasures/tsc repo (tsc/scripts/run-tutorial*-in-docker.sh) # - the HEMS walkthrough, using the example kept in the -# FlexMeasures/flexmeasures-client repo (examples/HEMS/HEMS_setup.py) +# FlexMeasures/flexmeasures-client repo (examples/HEMS/HEMS_setup.py), +# including its CLI-based report generation step (see +# flexmeasures-client's FLEXMEASURES_CLI_CMD/FLEXMEASURES_CLI_CONFIG_DIR +# support, used below to run that CLI inside the `server` container) # # This does not replace manual QA (e.g. UI login/graph checks, tutorial 5, # exploratory testing) -- see documentation/dev/release_process.rst. @@ -26,6 +29,12 @@ on: permissions: contents: read +env: + # Picked up by every `docker compose` invocation in this job, so the HEMS + # configs bind mount (added via qa-hems-override.yml, see below) applies + # consistently across the `up`, `exec`, and `down` steps. + COMPOSE_FILE: docker-compose.yml:qa-hems-override.yml + jobs: qa: runs-on: ubuntu-latest @@ -33,6 +42,22 @@ jobs: - name: Checkout flexmeasures uses: actions/checkout@v4 + - name: Checkout flexmeasures-client + uses: actions/checkout@v4 + with: + repository: FlexMeasures/flexmeasures-client + ref: ${{ inputs.flexmeasures-client-ref }} + path: flexmeasures-client + + - name: Add compose override to mount the HEMS configs directory + run: | + cat > qa-hems-override.yml < Date: Thu, 2 Jul 2026 00:14:23 +0200 Subject: [PATCH 08/11] docs: add changelog entry for release automation workflows Co-Authored-By: Claude Sonnet 5 --- documentation/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index b920c47173..953e22c2fd 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -27,6 +27,7 @@ Infrastructure / Support * Document suggested cloud architecture [see `PR #2245 `_] * Document the ``TRUSTED_HOSTS`` setting to safeguard against host poisoning from clients [see `PR #2246 `_] * Document the various ways to inspect a (scheduling) job [see `PR #2247 `_] +* Automate Docker Hub image publishing and a PyPI install smoke test on release, add a manually-triggered QA workflow that runs the toy tutorials and HEMS walkthrough against a local Docker Compose stack, and add a helper script to list merged PRs since the last tag [see `PR #2260 `_] Bugfixes ----------- From 864d029d563b5d175df9de2a1acbf7bc9dbb41e0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 2 Jul 2026 10:22:48 +0200 Subject: [PATCH 09/11] feat(ci): require manual approval for Docker Hub publish Gate docker-publish.yml behind the dockerhub GitHub environment, mirroring the approval gate already used by pypi-publish.yml. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/docker-publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index e73c78c42e..887c301d20 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -23,6 +23,10 @@ permissions: jobs: docker-publish: runs-on: ubuntu-latest + # Dedicated environments with protections for publishing are strongly recommended. + # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules + environment: + name: dockerhub steps: - name: Determine version id: version From b23c43b90c36a6da7995c8b20bfd471c9095c058 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 2 Jul 2026 10:23:02 +0200 Subject: [PATCH 10/11] refactor(ci): rename workflows for clarity, move dummy-price script into ci/ Rename build.yml -> docker-build.yml and release.yml -> pypi-publish.yml so filenames reflect what they actually build/publish, and qa-release.yml -> docker-qa.yml to distinguish it from the release-publishing pair. Move generate-dummy-price.sh into ci/, alongside the other CI helper scripts, and update references accordingly. Co-Authored-By: Claude Sonnet 5 --- .github/agents/tooling-ci-specialist.md | 4 ++-- .github/workflows/{build.yml => docker-build.yml} | 2 +- .github/workflows/{qa-release.yml => docker-qa.yml} | 0 .github/workflows/{release.yml => pypi-publish.yml} | 0 {.github/workflows => ci}/generate-dummy-price.sh | 0 documentation/dev/release_process.rst | 4 ++-- 6 files changed, 5 insertions(+), 5 deletions(-) rename .github/workflows/{build.yml => docker-build.yml} (98%) rename .github/workflows/{qa-release.yml => docker-qa.yml} (100%) rename .github/workflows/{release.yml => pypi-publish.yml} (100%) rename {.github/workflows => ci}/generate-dummy-price.sh (100%) diff --git a/.github/agents/tooling-ci-specialist.md b/.github/agents/tooling-ci-specialist.md index 99515e8510..aecfb7a816 100644 --- a/.github/agents/tooling-ci-specialist.md +++ b/.github/agents/tooling-ci-specialist.md @@ -230,7 +230,7 @@ This file defines standardized environment setup for GitHub Copilot agents. When - Coverage: Python 3.11 only - Caching: pip dependencies -2. **build.yml** +2. **docker-build.yml** - Docker image build validation - PostgreSQL service - Basic functionality tests @@ -239,7 +239,7 @@ This file defines standardized environment setup for GitHub Copilot agents. When - Security analysis - Weekly schedule -4. **release.yml** +4. **pypi-publish.yml** - Package and release automation - Trigger: Push to main diff --git a/.github/workflows/build.yml b/.github/workflows/docker-build.yml similarity index 98% rename from .github/workflows/build.yml rename to .github/workflows/docker-build.yml index 6b97ca4545..10cd23ef76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/docker-build.yml @@ -49,7 +49,7 @@ jobs: run: docker exec --env-file .env fm-container flexmeasures add toy-account - name: Generate prices dummy data - run: .github/workflows/generate-dummy-price.sh + run: ci/generate-dummy-price.sh - name: Copy prices dummy data run: docker cp prices-tomorrow.csv fm-container:/app/prices-tomorrow.csv - name: Add beliefs diff --git a/.github/workflows/qa-release.yml b/.github/workflows/docker-qa.yml similarity index 100% rename from .github/workflows/qa-release.yml rename to .github/workflows/docker-qa.yml diff --git a/.github/workflows/release.yml b/.github/workflows/pypi-publish.yml similarity index 100% rename from .github/workflows/release.yml rename to .github/workflows/pypi-publish.yml diff --git a/.github/workflows/generate-dummy-price.sh b/ci/generate-dummy-price.sh similarity index 100% rename from .github/workflows/generate-dummy-price.sh rename to ci/generate-dummy-price.sh diff --git a/documentation/dev/release_process.rst b/documentation/dev/release_process.rst index 25cd0437dd..0f552febdb 100644 --- a/documentation/dev/release_process.rst +++ b/documentation/dev/release_process.rst @@ -14,7 +14,7 @@ Versioning is derived entirely from git tags via ``hatch-vcs`` (see ``pyproject. * ``uv sync --group dev --group test`` and ``uv run poe test``. * For MINOR/MAJOR releases, do QA using the Docker Compose stack, and write the accompanying blog post. - Tutorials 1-4 and the HEMS walkthrough can be run via the manually-triggered ``QA (release)`` GitHub Actions workflow (``.github/workflows/qa-release.yml``). It spins up the local docker compose stack and runs the tutorial runner scripts from `FlexMeasures/tsc `_ and the HEMS example from `FlexMeasures/flexmeasures-client `_ (both kept in their own repos, not duplicated here). Trigger it from the Actions tab; it does not run automatically. This does not replace manual UI login/graph checks, tutorial 5, or exploratory QA. + Tutorials 1-4 and the HEMS walkthrough can be run via the manually-triggered ``QA (release)`` GitHub Actions workflow (``.github/workflows/docker-qa.yml``). It spins up the local docker compose stack and runs the tutorial runner scripts from `FlexMeasures/tsc `_ and the HEMS example from `FlexMeasures/flexmeasures-client `_ (both kept in their own repos, not duplicated here). Trigger it from the Actions tab; it does not run automatically. This does not replace manual UI login/graph checks, tutorial 5, or exploratory QA. 2. Assemble the changelog (semi-automated) ------------------------------------------- @@ -47,7 +47,7 @@ Then create the GitHub Release from the new tag. This step is intentionally manu Publishing the GitHub Release (step 3) triggers two workflows: -* ``.github/workflows/release.yml`` builds the package and publishes it to PyPI via trusted (OIDC) publishing, then runs a ``pypi-smoke-test`` job that installs the just-published version into a fresh virtual environment and verifies ``flexmeasures.__version__`` matches. +* ``.github/workflows/pypi-publish.yml`` builds the package and publishes it to PyPI via trusted (OIDC) publishing, then runs a ``pypi-smoke-test`` job that installs the just-published version into a fresh virtual environment and verifies ``flexmeasures.__version__`` matches. * ``.github/workflows/docker-publish.yml`` builds and pushes the Docker image to Docker Hub, tagged ``lfenergy/flexmeasures:v``, and additionally as ``lfenergy/flexmeasures:latest`` for stable (non-pre-release) releases. Check the Actions tab for both workflow runs to confirm they succeeded; also spot-check that the new version shows up on `PyPI `_, `Docker Hub `_, and that the ReadTheDocs build for the new tag completed. From 8c62538861ea27365df60e969ff088bd691a776f Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 2 Jul 2026 22:08:48 +0200 Subject: [PATCH 11/11] fix: address PR #2260 review comments - release_process.rst: fix "cut" -> "done" wording; point QA at the in-repo documentation/tut/scripts tutorial runners (including tutorial 5) instead of the tsc repo, since those scripts live here now. - docker-qa.yml: run tutorials 1-5 from documentation/tut/scripts directly, dropping the tsc checkout step and tsc-ref input. - list-merged-prs-since-tag.sh: only list merged PRs not yet referenced by a `PR #NNNN` link in an existing changelog file, instead of listing every merged PR unconditionally. - docker-publish.yml: build the image once and push it under both the version tag and (for stable releases) `latest`, instead of building twice; add a comment explaining what `github.event.release.prerelease` is. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/docker-publish.yml | 39 ++++++++++++++++----------- .github/workflows/docker-qa.yml | 32 +++++++--------------- ci/list-merged-prs-since-tag.sh | 27 +++++++++++++++---- documentation/dev/release_process.rst | 6 ++--- 4 files changed, 58 insertions(+), 46 deletions(-) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 887c301d20..bd730f9233 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -28,7 +28,7 @@ jobs: environment: name: dockerhub steps: - - name: Determine version + - name: Determine version and image tags id: version run: | TAG="${{ github.event.release.tag_name || inputs.tag }}" @@ -38,6 +38,21 @@ jobs: fi echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + # `github.event.release.prerelease` is a boolean field GitHub sets + # on the `release` webhook payload, reflecting whether the "Set as + # a pre-release" checkbox was ticked when the GitHub Release was + # created. We only want stable releases to also claim `:latest`. + IMAGE_TAGS="lfenergy/flexmeasures:${TAG}" + if [ "${{ github.event.release.prerelease }}" != "true" ]; then + IMAGE_TAGS="${IMAGE_TAGS} + lfenergy/flexmeasures:latest" + fi + { + echo "image-tags<> "$GITHUB_OUTPUT" + - uses: actions/checkout@v4 with: ref: ${{ steps.version.outputs.tag }} @@ -51,26 +66,18 @@ jobs: - uses: docker/setup-buildx-action@v3 - - name: Build and push (versioned tag) + - name: Build and push + # Built once, pushed under both tags: pushing `latest` re-tags the + # exact image just built and pushed under the version tag, rather + # than triggering a second (functionally identical, but distinct) + # build. uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: true - tags: lfenergy/flexmeasures:${{ steps.version.outputs.tag }} - - - name: Build and push (latest) - if: ${{ !github.event.release.prerelease }} - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - push: true - tags: lfenergy/flexmeasures:latest + tags: ${{ steps.version.outputs.image-tags }} - name: Summary run: | - echo "Pushed lfenergy/flexmeasures:${{ steps.version.outputs.tag }}" >> "$GITHUB_STEP_SUMMARY" - if [ "${{ github.event.release.prerelease }}" != "true" ]; then - echo "Pushed lfenergy/flexmeasures:latest" >> "$GITHUB_STEP_SUMMARY" - fi + echo "Pushed: ${{ steps.version.outputs.image-tags }}" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docker-qa.yml b/.github/workflows/docker-qa.yml index 1539f54492..426604df30 100644 --- a/.github/workflows/docker-qa.yml +++ b/.github/workflows/docker-qa.yml @@ -1,26 +1,22 @@ # Manually-triggered QA workflow for release testing. It spins up the local # docker compose stack, then runs: -# - the toy tutorials 1-4, using the runner scripts kept in the -# FlexMeasures/tsc repo (tsc/scripts/run-tutorial*-in-docker.sh) +# - the toy tutorials 1-5, using the runner scripts kept in this repo +# (documentation/tut/scripts/run-tutorial*-in-docker.sh) # - the HEMS walkthrough, using the example kept in the # FlexMeasures/flexmeasures-client repo (examples/HEMS/HEMS_setup.py), # including its CLI-based report generation step (see # flexmeasures-client's FLEXMEASURES_CLI_CMD/FLEXMEASURES_CLI_CONFIG_DIR # support, used below to run that CLI inside the `server` container) # -# This does not replace manual QA (e.g. UI login/graph checks, tutorial 5, -# exploratory testing) -- see documentation/dev/release_process.rst. +# This does not replace manual QA (e.g. UI login/graph checks, exploratory +# testing) -- see documentation/dev/release_process.rst. name: QA (release) on: workflow_dispatch: inputs: - tsc-ref: - description: "Branch/tag/ref to use from FlexMeasures/tsc" - required: false - default: "main" flexmeasures-client-ref: description: "Branch/tag/ref to use from FlexMeasures/flexmeasures-client" required: false @@ -61,21 +57,13 @@ jobs: - name: Build and start the docker compose stack run: docker compose up --build --detach --wait --wait-timeout 300 - - name: Checkout tsc tutorial scripts - uses: actions/checkout@v4 - with: - repository: FlexMeasures/tsc - ref: ${{ inputs.tsc-ref }} - sparse-checkout: tsc/scripts - sparse-checkout-cone-mode: false - path: tsc - - - name: Run tutorials 1-4 + - name: Run tutorials 1-5 run: | - for script in tsc/tsc/scripts/run-tutorial-in-docker.sh \ - tsc/tsc/scripts/run-tutorial2-in-docker.sh \ - tsc/tsc/scripts/run-tutorial3-in-docker.sh \ - tsc/tsc/scripts/run-tutorial4-in-docker.sh; do + for script in documentation/tut/scripts/run-tutorial-in-docker.sh \ + documentation/tut/scripts/run-tutorial2-in-docker.sh \ + documentation/tut/scripts/run-tutorial3-in-docker.sh \ + documentation/tut/scripts/run-tutorial4-in-docker.sh \ + documentation/tut/scripts/run-tutorial5-in-docker.sh; do echo "::group::Running $script" # The scripts use `docker exec -it`, which requires a tty we don't have here. sed 's/docker exec -it/docker exec -i/' "$script" | bash diff --git a/ci/list-merged-prs-since-tag.sh b/ci/list-merged-prs-since-tag.sh index fb640aa90c..9f3e67c9f6 100755 --- a/ci/list-merged-prs-since-tag.sh +++ b/ci/list-merged-prs-since-tag.sh @@ -1,15 +1,25 @@ #!/usr/bin/env bash -# Lists merged PRs since the given (or latest) git tag, to help a human -# assemble documentation/changelog.rst / cli/change_log.rst / api/change_log.rst +# Lists merged PRs since the given (or latest) git tag that aren't yet +# mentioned in a changelog file, to help a human assemble +# documentation/changelog.rst / cli/change_log.rst / api/change_log.rst # entries. Read-only: does not write to any changelog file or commit anything. # -# Usage: bin/list-merged-prs-since-tag.sh [] +# Usage: ci/list-merged-prs-since-tag.sh [] set -euo pipefail SINCE="${1:-$(git describe --tags --abbrev=0)}" SINCE_DATE=$(git log -1 --format=%aI "$SINCE") -echo "Merged PRs since $SINCE ($SINCE_DATE):" +REPO_ROOT=$(git rev-parse --show-toplevel) +CHANGELOG_FILES=( + "$REPO_ROOT/documentation/changelog.rst" + "$REPO_ROOT/documentation/cli/change_log.rst" + "$REPO_ROOT/documentation/api/change_log.rst" +) +# PR numbers already referenced by a `PR #NNNN` link in any changelog file. +EXISTING_PRS=$(grep -ohP '(?<=PR #)\d+' "${CHANGELOG_FILES[@]}" 2>/dev/null | sort -un) + +echo "Merged PRs since $SINCE ($SINCE_DATE) not yet mentioned in a changelog file:" echo gh pr list \ @@ -18,8 +28,15 @@ gh pr list \ --search "merged:>=$SINCE_DATE" \ --limit 200 \ --json number,title,url \ - --template '{{range .}}* {{.title}} [see `PR #{{.number}} <{{.url}}>`_]{{"\n"}}{{end}}' + --jq '.[] | "\(.number)\t* \(.title) [see `PR #\(.number) <\(.url)>`_]"' \ + | while IFS=$'\t' read -r number line; do + if grep -qx "$number" <<< "$EXISTING_PRS"; then + continue + fi + echo "$line" + done echo echo "Review, categorize into New features / Infrastructure / Support / Bugfixes," echo "and paste into documentation/changelog.rst (and cli/change_log.rst, api/change_log.rst if relevant)." +echo "(PRs already referenced in those files are omitted above.)" diff --git a/documentation/dev/release_process.rst b/documentation/dev/release_process.rst index 0f552febdb..95429e1a09 100644 --- a/documentation/dev/release_process.rst +++ b/documentation/dev/release_process.rst @@ -1,7 +1,7 @@ Release process ================ -This page describes how FlexMeasures releases are cut, and which parts are automated versus manual. +This page describes how FlexMeasures releases are done, and which parts are automated versus manual. The canonical, most detailed checklist still lives in `FlexMeasures/tsc RELEASE.md `_. This page summarizes that flow and points out where CI now does the mechanical work. @@ -14,7 +14,7 @@ Versioning is derived entirely from git tags via ``hatch-vcs`` (see ``pyproject. * ``uv sync --group dev --group test`` and ``uv run poe test``. * For MINOR/MAJOR releases, do QA using the Docker Compose stack, and write the accompanying blog post. - Tutorials 1-4 and the HEMS walkthrough can be run via the manually-triggered ``QA (release)`` GitHub Actions workflow (``.github/workflows/docker-qa.yml``). It spins up the local docker compose stack and runs the tutorial runner scripts from `FlexMeasures/tsc `_ and the HEMS example from `FlexMeasures/flexmeasures-client `_ (both kept in their own repos, not duplicated here). Trigger it from the Actions tab; it does not run automatically. This does not replace manual UI login/graph checks, tutorial 5, or exploratory QA. + Tutorials 1-5 and the HEMS walkthrough can be run via the manually-triggered ``QA (release)`` GitHub Actions workflow (``.github/workflows/docker-qa.yml``). It spins up the local docker compose stack and runs the tutorial runner scripts from ``documentation/tut/scripts`` and the HEMS example from `FlexMeasures/flexmeasures-client `_ (kept in its own repo, not duplicated here). Trigger it from the Actions tab; it does not run automatically. This does not replace manual UI login/graph checks or exploratory QA. 2. Assemble the changelog (semi-automated) ------------------------------------------- @@ -25,7 +25,7 @@ Run: uv run poe changelog-check -This lists merged PRs since the last tag (via ``ci/list-merged-prs-since-tag.sh``), pre-formatted as changelog bullets. +This lists merged PRs since the last tag that aren't yet mentioned in a changelog file (via ``ci/list-merged-prs-since-tag.sh``, which cross-references PR numbers already linked in ``documentation/changelog.rst`` / ``cli/change_log.rst`` / ``api/change_log.rst``), pre-formatted as changelog bullets. It is read-only: it does not edit or commit anything. Review, categorize each entry into *New features* / *Infrastructure / Support* / *Bugfixes*, and paste them into ``documentation/changelog.rst`` (and ``documentation/cli/change_log.rst`` / ``documentation/api/change_log.rst`` where relevant).