]> git.ipfire.org Git - thirdparty/rrdtool-1.x.git/commitdiff
release: implement automated release workflow
authorTobias Oetiker <tobi@oetiker.ch>
Wed, 13 May 2026 09:46:45 +0000 (11:46 +0200)
committerTobias Oetiker <tobi@oetiker.ch>
Wed, 13 May 2026 09:46:45 +0000 (11:46 +0200)
Implements the design at
docs/superpowers/specs/2026-05-13-release-workflow-design.md.

A single workflow_dispatch on master computes the next SemVer (per the
release_type input), bumps every hard-coded version string in-place in
each parallel build job, produces source tarball + MSVC zips + .rpm +
.deb, and only after every build succeeds does the publish job commit
the bump, push the tag, and create a GitHub Release with all artifacts.

New / refactored files:
- .github/workflows/release.yml — six-job orchestrator (check-ci,
  compute-version, build-source, build-windows, build-rpm, build-deb,
  publish). check-ci asserts "Linux Build" and "Windows CI" succeeded
  for the master HEAD commit (waiting up to 30m for in-progress runs)
  before anything else runs.
- .github/workflows/release-source.yml, release-windows.yml — removed,
  folded into release.yml.
- .github/actions/bump-version/action.yml — composite action wrapping
  the shared "write VERSION + bump-version.sh + finalize-changes.pl"
  block used by all five build/publish jobs.
- conftools/bump-version.sh — version-propagation logic extracted from
  rrdtool-release; idempotent and accepts the version as positional arg.
- conftools/finalize-changes.pl — idempotent CHANGES rewrite (rename
  leading master block to versioned, prepend fresh empty placeholder).
- conftools/rrdtool-opt.spec — minimal RPM spec for the /opt/rrdtool
  build; AutoReqProv off (so distro coexistence isn't broken), no PHP
  bits, single combined package.
- conftools/rrdtool-env.sh.in — sourceable shell helper that puts
  /opt/rrdtool on PATH and makes Perl/Python/Tcl/Lua/Ruby bindings
  discoverable to their interpreters. Installed at
  /opt/rrdtool/bin/rrdtool-env.sh by both the .rpm and .deb.
- rrdtool-release — refactored to call bump-version.sh; SCP-to-james
  and local sanity build kept intact for the maintainer's local flow.

Deviations from the spec (small, intent-preserving):
- finalize-changes.pl is a separate file rather than perl inlined in
  every job, since the same rewrite is needed in 5 places.
- A composite action wraps the bump step for the same reason.

Things to watch on first dispatch (none verifiable without an actual
run on GitHub):
- The publish job pushes directly to master as github-actions[bot]
  via `git push origin master --follow-tags`. Branch protection on
  master must allow this (or the bot must be exempted), otherwise the
  push step fails and no Release is created. The spec explicitly opts
  for direct push over a PR-then-merge flow.
- Ubuntu 24.04 .deb runtime depends list libpng16-16 / libfreetype6;
  on 24.04 the actual packages are libpng16-16t64 / libfreetype6t64,
  which carry Provides aliases for the old names. Resolution should
  succeed on install but is the place a 24.04 user would hit issues
  if Canonical ever drops the aliases.
- The spec text says build-rpm/build-deb need build-source; the
  diagram shows them parallel. Followed the text — they wait for the
  source job to finish before running, which fail-fasts on tarball
  problems at the cost of ~3min of parallel time.

.github/actions/bump-version/action.yml [new file with mode: 0644]
.github/workflows/release-source.yml [deleted file]
.github/workflows/release-windows.yml [deleted file]
.github/workflows/release.yml [new file with mode: 0644]
conftools/bump-version.sh [new file with mode: 0755]
conftools/finalize-changes.pl [new file with mode: 0755]
conftools/rrdtool-env.sh.in [new file with mode: 0644]
conftools/rrdtool-opt.spec [new file with mode: 0644]
rrdtool-release

diff --git a/.github/actions/bump-version/action.yml b/.github/actions/bump-version/action.yml
new file mode 100644 (file)
index 0000000..32ee342
--- /dev/null
@@ -0,0 +1,28 @@
+---
+name: Bump version in working tree
+description: >
+  Apply a target version (and date) to all hard-coded version locations
+  in the source tree (VERSION, perl bindings, src/*.{h,c}, rrdtool.spec,
+  doc/rrdbuild.pod, win32/*.rc, win32/rrd_config.h) and rewrite CHANGES.
+  Idempotent — safe to run from every release job; the actual mutation
+  only happens once per release.
+
+inputs:
+  version:
+    description: New version, e.g. 1.9.1
+    required: true
+  date:
+    description: Release date, YYYY-MM-DD
+    required: true
+
+runs:
+  using: composite
+  steps:
+    - name: Apply version bump in-place
+      shell: bash
+      run: |
+        set -e
+        echo "${{ inputs.version }}" > VERSION
+        bash conftools/bump-version.sh "${{ inputs.version }}"
+        perl conftools/finalize-changes.pl \
+            "${{ inputs.version }}" "${{ inputs.date }}" CHANGES
diff --git a/.github/workflows/release-source.yml b/.github/workflows/release-source.yml
deleted file mode 100644 (file)
index 84df4f3..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
----
-name: Release Source
-
-# yamllint disable rule:line-length
-# yamllint disable-line rule:truthy
-on:
-  push:
-    tags:
-      - 'v*.*.*'
-  workflow_dispatch:
-
-jobs:
-  release:
-    name: Release Source
-    runs-on: ubuntu-latest
-    permissions:
-      contents: write
-    steps:
-    - name: Checkout
-      uses: actions/checkout@v6
-
-    - name: Get Version
-      id: get_version
-      run: echo '::set-output name=version::'$(cat VERSION)
-
-    - name: Install dependencies
-      run: |
-        sudo apt-get update
-        sudo apt-get install autopoint build-essential gettext libpango1.0-dev ghostscript
-    - name: Build Dist
-      run: |
-        ./bootstrap
-        ./configure
-        make dist
-        perl -077 -ne '/^(.+?\n.+?\n.+?)\nRRDtool/s && print $1' CHANGES > releasenotes
-    - name: Create Release
-      uses: ncipollo/release-action@v1
-      with:
-        artifacts: "rrdtool-${{ steps.get_version.outputs.version }}.tar.gz"
-        artifactContentType: "application/tar+gzip"
-        bodyFile: releasenotes
-        discussionCategory: "Release Issues"
-        name: "RRDtool Version ${{ steps.get_version.outputs.version }}"
-        token: ${{ secrets.GITHUB_TOKEN }}
-
-
diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml
deleted file mode 100644 (file)
index 7e9550e..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
----
-name: Release Windows
-
-# yamllint disable rule:line-length
-# yamllint disable-line rule:truthy
-on:
-  push:
-    tags:
-      - 'v*.*.*'
-  workflow_dispatch:
-
-defaults:
-  run:
-    shell: cmd
-
-jobs:
-  MSVC:
-    runs-on: ${{ matrix.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        # https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md
-        os: [windows-2022]
-        vcpkg_triplet: [x64-windows, x86-windows]
-        include:
-          - os: windows-2022
-            vcpkg_triplet: x64-windows
-            # https://github.com/microsoft/vcpkg/releases/tag/2025.12.12
-            # https://github.com/microsoft/vcpkg/commit/84bab45d415d22042bd0b9081aea57f362da3f35
-            vcpkgCommitId: '84bab45d415d22042bd0b9081aea57f362da3f35'
-            vcpkgPackages: 'cairo expat fontconfig freetype gettext glib libpng libxml2 pango pcre zlib'
-            configuration: 'x64'
-            nmake_configuration: 'USE_64BIT=1'
-          - os: windows-2022
-            vcpkg_triplet: x86-windows
-            vcpkgCommitId: '84bab45d415d22042bd0b9081aea57f362da3f35'
-            vcpkgPackages: 'cairo expat fontconfig freetype gettext glib libpng libxml2 pango pcre zlib'
-            configuration: 'x86'
-            nmake_configuration: ''
-    env:
-      buildDir: '${{ github.workspace }}/build/'
-    steps:
-      - uses: actions/checkout@v6
-        with:
-          submodules: true
-
-      # vcpkg-action
-      # https://github.com/johnwason/vcpkg-action
-      - name: vcpkg build
-        uses: johnwason/vcpkg-action@v7
-        id: vcpkg
-        with:
-          pkgs: '${{ matrix.vcpkgPackages }}'
-          triplet: ${{ matrix.vcpkg_triplet }}
-          cache-key: ${{ matrix.configuration }}
-          revision: '${{ matrix.vcpkgCommitId}}'
-          token: ${{ github.token }}
-
-      - name: Build ${{ matrix.configuration }}
-        run: |
-          call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.configuration }}
-          nmake -f win32\Makefile_vcpkg.msc ${{ matrix.nmake_configuration }}
-
-      - name: Collect files
-        run: |
-          win32\collect_rrdtool_vcpkg_files.bat ${{ matrix.configuration }}
-
-      - uses: actions/upload-artifact@v6
-        with:
-          name: deploy-rrdtool-MSVC-${{ matrix.configuration }}
-          path: win32/nmake_release_${{ matrix.configuration }}_vcpkg/rrdtool-*-${{ matrix.configuration }}_vcpkg/
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644 (file)
index 0000000..5f63b99
--- /dev/null
@@ -0,0 +1,536 @@
+---
+name: Release
+
+# yamllint disable rule:line-length
+# yamllint disable-line rule:truthy
+on:
+  workflow_dispatch:
+    inputs:
+      release_type:
+        description: SemVer bump from the latest v* tag
+        type: choice
+        required: true
+        default: bugfix
+        options:
+          - bugfix
+          - feature
+          - major
+
+permissions:
+  contents: read
+
+jobs:
+  # ---------------------------------------------------------------------------
+  # check-ci: branch guard + assert that "Linux Build" and "Windows CI" both
+  # succeeded for the master HEAD commit. If a run is in progress for the same
+  # commit, wait up to 30 minutes for it to finish.
+  # ---------------------------------------------------------------------------
+  check-ci:
+    name: CI green on master HEAD
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+    steps:
+      - name: Branch guard
+        run: |
+          if [ "${{ github.ref }}" != "refs/heads/master" ]; then
+            echo "::error::Releases must be dispatched from master (got ${{ github.ref }})"
+            exit 1
+          fi
+
+      - name: Checkout
+        uses: actions/checkout@v6
+
+      - name: Verify required CI workflows succeeded
+        env:
+          GH_TOKEN: ${{ github.token }}
+          SHA: ${{ github.sha }}
+        run: |
+          set -e
+          fail=0
+          for wf in "Linux Build" "Windows CI"; do
+            echo "::group::Checking '$wf' for commit $SHA"
+
+            # Wait for any in-progress run for this commit.
+            run_id=$(gh run list --repo "$GITHUB_REPOSITORY" \
+                       --workflow="$wf" --branch=master --commit="$SHA" \
+                       --status=in_progress --limit=1 \
+                       --json databaseId -q '.[0].databaseId' || true)
+            if [ -n "$run_id" ]; then
+              echo "Run $run_id is in progress; waiting (timeout 30m)..."
+              timeout 1800 gh run watch --repo "$GITHUB_REPOSITORY" \
+                  --interval 30 --exit-status "$run_id" || true
+            fi
+
+            ok=$(gh run list --repo "$GITHUB_REPOSITORY" \
+                   --workflow="$wf" --branch=master --commit="$SHA" \
+                   --status=success --limit=1 \
+                   --json conclusion -q '.[0].conclusion' || true)
+            if [ "$ok" != "success" ]; then
+              url=$(gh run list --repo "$GITHUB_REPOSITORY" \
+                      --workflow="$wf" --branch=master --commit="$SHA" \
+                      --limit=1 --json url -q '.[0].url' || true)
+              echo "::error::'$wf' is not 'success' for $SHA. Latest run: ${url:-<none>}"
+              fail=1
+            else
+              echo "OK: '$wf' succeeded for $SHA"
+            fi
+            echo "::endgroup::"
+          done
+          if [ "$fail" -ne 0 ]; then
+            exit 1
+          fi
+
+  # ---------------------------------------------------------------------------
+  # compute-version: read latest v* tag, bump per release_type, emit outputs.
+  # Read-only — does not touch the working tree or push anything.
+  # ---------------------------------------------------------------------------
+  compute-version:
+    name: Compute new version
+    needs: check-ci
+    runs-on: ubuntu-latest
+    outputs:
+      version: ${{ steps.calc.outputs.version }}
+      tag: ${{ steps.calc.outputs.tag }}
+      date: ${{ steps.calc.outputs.date }}
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          fetch-depth: 0
+
+      - id: calc
+        name: Compute next SemVer
+        env:
+          RELEASE_TYPE: ${{ inputs.release_type }}
+        run: |
+          set -e
+          LATEST=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -1)
+          LATEST=${LATEST:-v0.0.0}
+          IFS=. read -r MAJOR MINOR PATCH <<< "${LATEST#v}"
+          case "$RELEASE_TYPE" in
+            major)   NEW=$((MAJOR+1)).0.0 ;;
+            feature) NEW=${MAJOR}.$((MINOR+1)).0 ;;
+            bugfix)  NEW=${MAJOR}.${MINOR}.$((PATCH+1)) ;;
+            *) echo "::error::unknown release_type '$RELEASE_TYPE'"; exit 1 ;;
+          esac
+          DATE=$(date -u +"%Y-%m-%d")
+          echo "version=$NEW"  >> "$GITHUB_OUTPUT"
+          echo "tag=v$NEW"     >> "$GITHUB_OUTPUT"
+          echo "date=$DATE"    >> "$GITHUB_OUTPUT"
+          echo "Computed: v$NEW (date $DATE), previous tag $LATEST"
+
+  # ---------------------------------------------------------------------------
+  # build-source: bump in-place, ./bootstrap && ./configure && make dist.
+  # Produces rrdtool-X.Y.Z.tar.gz, the canonical source release artifact.
+  # ---------------------------------------------------------------------------
+  build-source:
+    name: Source tarball
+    needs: compute-version
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+
+      - name: Install build deps
+        run: |
+          sudo apt-get update
+          sudo apt-get install -y \
+              autopoint build-essential gettext libpango1.0-dev ghostscript
+
+      - name: Bump version in working tree
+        uses: ./.github/actions/bump-version
+        with:
+          version: ${{ needs.compute-version.outputs.version }}
+          date: ${{ needs.compute-version.outputs.date }}
+
+      - name: Build source tarball
+        run: |
+          set -e
+          ./bootstrap
+          ./configure
+          make dist
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: source-tarball
+          path: rrdtool-${{ needs.compute-version.outputs.version }}.tar.gz
+          if-no-files-found: error
+
+  # ---------------------------------------------------------------------------
+  # build-windows: per-arch MSVC build via vcpkg + nmake. Produces a zip per
+  # configuration. Same matrix as the previous release-windows.yml.
+  # ---------------------------------------------------------------------------
+  build-windows:
+    name: Windows MSVC ${{ matrix.configuration }}
+    needs: compute-version
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: true
+      matrix:
+        os: [windows-2022]
+        vcpkg_triplet: [x64-windows, x86-windows]
+        include:
+          - os: windows-2022
+            vcpkg_triplet: x64-windows
+            vcpkgCommitId: '84bab45d415d22042bd0b9081aea57f362da3f35'
+            vcpkgPackages: 'cairo expat fontconfig freetype gettext glib libpng libxml2 pango pcre zlib'
+            configuration: 'x64'
+            nmake_configuration: 'USE_64BIT=1'
+          - os: windows-2022
+            vcpkg_triplet: x86-windows
+            vcpkgCommitId: '84bab45d415d22042bd0b9081aea57f362da3f35'
+            vcpkgPackages: 'cairo expat fontconfig freetype gettext glib libpng libxml2 pango pcre zlib'
+            configuration: 'x86'
+            nmake_configuration: ''
+    env:
+      buildDir: '${{ github.workspace }}/build/'
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          submodules: true
+
+      - name: Bump version in working tree
+        uses: ./.github/actions/bump-version
+        with:
+          version: ${{ needs.compute-version.outputs.version }}
+          date: ${{ needs.compute-version.outputs.date }}
+
+      - name: vcpkg build
+        uses: johnwason/vcpkg-action@v7
+        id: vcpkg
+        with:
+          pkgs: '${{ matrix.vcpkgPackages }}'
+          triplet: ${{ matrix.vcpkg_triplet }}
+          cache-key: ${{ matrix.configuration }}
+          revision: '${{ matrix.vcpkgCommitId }}'
+          token: ${{ github.token }}
+
+      - name: Build ${{ matrix.configuration }}
+        shell: cmd
+        run: |
+          call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" ${{ matrix.configuration }}
+          nmake -f win32\Makefile_vcpkg.msc ${{ matrix.nmake_configuration }}
+
+      - name: Collect files
+        shell: cmd
+        run: |
+          win32\collect_rrdtool_vcpkg_files.bat ${{ matrix.configuration }}
+
+      - name: Zip release tree
+        shell: bash
+        env:
+          VERSION: ${{ needs.compute-version.outputs.version }}
+          CFG: ${{ matrix.configuration }}
+        run: |
+          set -e
+          dir="win32/nmake_release_${CFG}_vcpkg/rrdtool-${VERSION}-${CFG}_vcpkg"
+          if [ ! -d "$dir" ]; then
+            echo "::error::Expected collected dir not found: $dir"
+            ls win32/nmake_release_${CFG}_vcpkg/ || true
+            exit 1
+          fi
+          zip="rrdtool-${VERSION}-${CFG}_vcpkg.zip"
+          (cd "$(dirname "$dir")" && 7z a -tzip "../../$zip" "$(basename "$dir")")
+          ls -l "$zip"
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: windows-${{ matrix.configuration }}
+          path: rrdtool-${{ needs.compute-version.outputs.version }}-${{ matrix.configuration }}_vcpkg.zip
+          if-no-files-found: error
+
+  # ---------------------------------------------------------------------------
+  # build-rpm: per-distro container, builds a .rpm via rpmbuild from the same
+  # source tarball that build-source ships.
+  # ---------------------------------------------------------------------------
+  build-rpm:
+    name: RPM (${{ matrix.image }})
+    needs: [compute-version, build-source]
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: true
+      matrix:
+        include:
+          - image: almalinux:9
+            tag: almalinux-9
+    container:
+      image: ${{ matrix.image }}
+    steps:
+      - name: Install bootstrap deps (git for checkout)
+        run: |
+          set -e
+          dnf install -y git-core perl
+
+      - uses: actions/checkout@v6
+
+      - name: Install build deps
+        run: |
+          set -e
+          dnf install -y epel-release
+          dnf install -y 'dnf-command(config-manager)' || true
+          dnf config-manager --set-enabled crb || true
+          dnf install -y \
+              rpm-build rpmdevtools \
+              gcc make autoconf automake libtool pkgconfig \
+              groff gettext gettext-devel intltool \
+              cairo-devel pango-devel \
+              freetype-devel libpng-devel zlib-devel \
+              libxml2-devel glib2-devel libdbi-devel \
+              perl-devel perl-ExtUtils-MakeMaker \
+              python3-devel \
+              tcl-devel \
+              lua-devel \
+              ruby ruby-devel
+
+      - name: Bump version in working tree
+        uses: ./.github/actions/bump-version
+        with:
+          version: ${{ needs.compute-version.outputs.version }}
+          date: ${{ needs.compute-version.outputs.date }}
+
+      - name: Build source tarball locally for rpmbuild input
+        run: |
+          set -e
+          ./bootstrap
+          ./configure
+          make dist
+
+      - name: Stage rpmbuild tree
+        env:
+          VERSION: ${{ needs.compute-version.outputs.version }}
+        run: |
+          set -e
+          rpmdev-setuptree
+          # Render the env helper into SOURCES (it's a static template, no
+          # substitutions needed today, but kept under a versioned filename so
+          # future templating is trivial).
+          install -m 0755 conftools/rrdtool-env.sh.in ~/rpmbuild/SOURCES/rrdtool-env.sh
+          # Substitute @VERSION@ in the spec, copy to SPECS.
+          sed "s/@VERSION@/${VERSION}/g" conftools/rrdtool-opt.spec \
+              > ~/rpmbuild/SPECS/rrdtool-opt.spec
+          # Move the source tarball into SOURCES.
+          mv "rrdtool-${VERSION}.tar.gz" ~/rpmbuild/SOURCES/
+
+      - name: rpmbuild
+        run: |
+          set -e
+          rpmbuild -ba ~/rpmbuild/SPECS/rrdtool-opt.spec
+
+      - name: Collect built rpm
+        env:
+          VERSION: ${{ needs.compute-version.outputs.version }}
+        run: |
+          set -e
+          mkdir -p out
+          # Copy only the binary rpm (not the src.rpm — design says binaries only)
+          cp ~/rpmbuild/RPMS/x86_64/rrdtool-${VERSION}-*.rpm out/
+          ls -l out/
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: rpm-${{ matrix.tag }}
+          path: out/*.rpm
+          if-no-files-found: error
+
+  # ---------------------------------------------------------------------------
+  # build-deb: per-distro container, builds a .deb via fpm from a staged
+  # `make install DESTDIR=...` tree. No in-tree debian/ packaging needed.
+  # ---------------------------------------------------------------------------
+  build-deb:
+    name: DEB (${{ matrix.image }})
+    needs: [compute-version, build-source]
+    runs-on: ubuntu-latest
+    strategy:
+      fail-fast: true
+      matrix:
+        include:
+          - image: ubuntu:22.04
+            tag: ubuntu-22.04
+            distro_tag: ubuntu22.04
+          - image: ubuntu:24.04
+            tag: ubuntu-24.04
+            distro_tag: ubuntu24.04
+          - image: debian:12
+            tag: debian-12
+            distro_tag: debian12
+    container:
+      image: ${{ matrix.image }}
+    env:
+      DEBIAN_FRONTEND: noninteractive
+    steps:
+      - name: Install bootstrap deps (git for checkout)
+        run: |
+          set -e
+          apt-get update
+          apt-get install -y --no-install-recommends git ca-certificates perl
+
+      - uses: actions/checkout@v6
+
+      - name: Install build deps + fpm
+        run: |
+          set -e
+          apt-get install -y --no-install-recommends \
+              build-essential autoconf automake libtool pkg-config \
+              gettext autopoint intltool groff dc \
+              libcairo2-dev libpango1.0-dev libxml2-dev libglib2.0-dev libdbi-dev \
+              libfreetype6-dev libpng-dev zlib1g-dev \
+              libperl-dev \
+              python3-dev python3-setuptools \
+              tcl-dev \
+              liblua5.1-0-dev \
+              ruby ruby-dev rubygems-integration
+          gem install --no-document fpm
+
+      - name: Bump version in working tree
+        uses: ./.github/actions/bump-version
+        with:
+          version: ${{ needs.compute-version.outputs.version }}
+          date: ${{ needs.compute-version.outputs.date }}
+
+      - name: Configure & install into stage/
+        run: |
+          set -e
+          ./bootstrap
+          ./configure \
+              --prefix=/opt/rrdtool \
+              --sysconfdir=/opt/rrdtool/etc \
+              --localstatedir=/opt/rrdtool/var \
+              --datarootdir=/opt/rrdtool/share \
+              --mandir=/opt/rrdtool/share/man \
+              --disable-static \
+              --with-pic
+          make
+          make install DESTDIR="$PWD/stage"
+
+      - name: Post-install touches (rpath + env helper)
+        run: |
+          set -e
+          # Bake rpath into librrd.pc for downstream consumers.
+          sed -i 's|^Libs: -L\${libdir} -lrrd$|Libs: -L${libdir} -lrrd -Wl,-rpath,${libdir}|' \
+              stage/opt/rrdtool/lib/pkgconfig/librrd.pc
+          # Install the sourceable env helper.
+          install -m 0755 conftools/rrdtool-env.sh.in stage/opt/rrdtool/bin/rrdtool-env.sh
+
+      - name: Build .deb with fpm
+        env:
+          VERSION: ${{ needs.compute-version.outputs.version }}
+          DISTRO_TAG: ${{ matrix.distro_tag }}
+        run: |
+          set -e
+          mkdir -p out
+          fpm -s dir -t deb \
+              -n rrdtool \
+              -v "$VERSION" \
+              --iteration "1~${DISTRO_TAG}" \
+              --license "GPL-2.0-or-later WITH FLOSS-exception-1.0" \
+              --maintainer "Tobias Oetiker <tobi@oetiker.ch>" \
+              --vendor "oss.oetiker.ch" \
+              --url "https://oss.oetiker.ch/rrdtool/" \
+              --description "Round Robin Database Tool (upstream /opt build). Source /opt/rrdtool/bin/rrdtool-env.sh to use." \
+              --depends "libcairo2" \
+              --depends "libpango-1.0-0" \
+              --depends "libxml2" \
+              --depends "libpng16-16" \
+              --depends "libfreetype6" \
+              --depends "libdbi1" \
+              --depends "zlib1g" \
+              --deb-no-default-config-files \
+              -p out/ \
+              -C stage opt
+          ls -l out/
+
+      - uses: actions/upload-artifact@v6
+        with:
+          name: deb-${{ matrix.tag }}
+          path: out/*.deb
+          if-no-files-found: error
+
+  # ---------------------------------------------------------------------------
+  # publish: only job that mutates the repo. Runs only after every build job
+  # (and every matrix entry) has succeeded. Re-applies the version bump,
+  # commits + tags + pushes, then creates the GitHub Release with all artifacts.
+  # ---------------------------------------------------------------------------
+  publish:
+    name: Publish release
+    needs:
+      - compute-version
+      - build-source
+      - build-windows
+      - build-rpm
+      - build-deb
+    runs-on: ubuntu-latest
+    permissions:
+      contents: write
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          fetch-depth: 0
+
+      - name: Race check (master not advanced since check-ci)
+        env:
+          EXPECTED_SHA: ${{ github.sha }}
+        run: |
+          set -e
+          git fetch origin master
+          ACTUAL=$(git rev-parse origin/master)
+          if [ "$ACTUAL" != "$EXPECTED_SHA" ]; then
+            echo "::error::origin/master moved during the release run."
+            echo "  expected: $EXPECTED_SHA"
+            echo "  actual:   $ACTUAL"
+            echo "Re-dispatch the workflow after reviewing the new commits."
+            exit 1
+          fi
+
+      - name: Bump version in working tree
+        uses: ./.github/actions/bump-version
+        with:
+          version: ${{ needs.compute-version.outputs.version }}
+          date: ${{ needs.compute-version.outputs.date }}
+
+      - name: Commit, tag, push
+        env:
+          NEW: ${{ needs.compute-version.outputs.version }}
+        run: |
+          set -e
+          git config user.name  "github-actions[bot]"
+          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+          git add -u
+          git commit -m "release v$NEW"
+          git tag -a "v$NEW" -m "release v$NEW"
+          git push origin master --follow-tags
+
+      - name: Download all artifacts
+        uses: actions/download-artifact@v6
+        with:
+          path: dist
+          pattern: '*'
+          merge-multiple: true
+
+      - name: List collected artifacts
+        run: |
+          set -e
+          ls -lR dist/
+          test -n "$(ls dist/)" || { echo "::error::no artifacts collected"; exit 1; }
+
+      - name: Extract release notes from CHANGES
+        env:
+          VERSION: ${{ needs.compute-version.outputs.version }}
+        run: |
+          set -e
+          awk -v v="$VERSION" '
+            $0 ~ "^RRDtool " v " " { found=1 }
+            found && /^RRDtool / && $0 !~ "^RRDtool " v " " { exit }
+            found { print }
+          ' CHANGES > releasenotes
+          echo "----- release notes -----"
+          cat releasenotes
+          test -s releasenotes || { echo "::error::release notes empty"; exit 1; }
+
+      - name: Create GitHub Release
+        uses: ncipollo/release-action@v1
+        with:
+          tag: v${{ needs.compute-version.outputs.version }}
+          name: "RRDtool Version ${{ needs.compute-version.outputs.version }}"
+          bodyFile: releasenotes
+          artifacts: "dist/*"
+          discussionCategory: "Release Issues"
+          token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/conftools/bump-version.sh b/conftools/bump-version.sh
new file mode 100755 (executable)
index 0000000..3b87ced
--- /dev/null
@@ -0,0 +1,58 @@
+#!/bin/sh
+# Propagate VERSION (e.g. 1.9.1) into all hard-coded version locations
+# across the source tree. Idempotent: running twice with the same version
+# is a no-op the second time.
+#
+# Usage: bash conftools/bump-version.sh <version>
+#
+# Extracted from the maintainer's `rrdtool-release` script so the same
+# logic can run inside CI release jobs.
+
+set -e
+
+VERSION="${1:-}"
+if [ -z "$VERSION" ]; then
+    echo "usage: $0 <version>" >&2
+    exit 2
+fi
+
+case "$VERSION" in
+    [0-9]*.[0-9]*.[0-9]*) ;;
+    *)
+        echo "error: VERSION must look like X.Y.Z (got: '$VERSION')" >&2
+        exit 2
+        ;;
+esac
+
+NUMVERS=$(printf '%s\n' "$VERSION" | perl -n -e 'my @x=split /\./;printf "%d.%d%03d", @x')
+CURRENT_YEAR=$(date +"%Y")
+
+set -x
+
+# Perl bindings: $VERSION = NUMVERS
+perl -i -p -e 's/^\$VERSION.+/\$VERSION='"$NUMVERS"';/' bindings/perl-*/*.pm
+
+# C source: in-source RRDtool version string + Copyright year
+perl -i -p -e \
+    's/RRDtool \d\S+/RRDtool '"$VERSION"'/;
+     s/Copyright.+?Oetiker.+\d{4}/Copyright by Tobi Oetiker, 1997-'"$CURRENT_YEAR"'/' \
+    src/*.h src/*.c
+
+# Legacy rpm spec (kept for downstream consumers per design doc)
+perl -i -p -e 's/^Version:.+/Version: '"$VERSION"'/' rrdtool.spec
+
+# rrdbuild documentation: tarball name + version tag
+perl -i -p -e 's/rrdtool-[\.\d]+\d(pre\d+)?(rc\d+)?/rrdtool-'"$VERSION"'/g;
+               s/v\d+\.\d+\.\d+/v'"$VERSION"'/' doc/rrdbuild.pod
+
+# MSVC: copyright year in resource files
+perl -i -p -e 's/Copyright \(c\).+?Oetiker/Copyright (c) 1997-'"$CURRENT_YEAR"' Tobias Oetiker/' win32/*.rc
+
+# MSVC: PACKAGE_* macros and NUMVERS in win32/rrd_config.h
+perl -i -p -e \
+    's/PACKAGE_MAJOR.+\d{1}/PACKAGE_MAJOR       '"$(echo "$VERSION" | cut -d. -f1)"'/;
+     s/PACKAGE_MINOR.+\d{1}/PACKAGE_MINOR       '"$(echo "$VERSION" | cut -d. -f2)"'/;
+     s/PACKAGE_REVISION.+\d{1}/PACKAGE_REVISION    '"$(echo "$VERSION" | cut -d. -f3)"'/;
+     s/PACKAGE_VERSION.+\d{1}\"/PACKAGE_VERSION     "'"$VERSION"'"/;
+     s/NUMVERS.+\d{1}/NUMVERS             '"$NUMVERS"'/' \
+    win32/rrd_config.h
diff --git a/conftools/finalize-changes.pl b/conftools/finalize-changes.pl
new file mode 100755 (executable)
index 0000000..94e02b1
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+# Rewrite CHANGES at release time:
+#   - rename the leading "RRDtool - master ..." block to "RRDtool X.Y.Z - DATE"
+#   - prepend a fresh empty "RRDtool - master ..." placeholder
+#
+# Idempotent: if CHANGES already contains a "RRDtool X.Y.Z - DATE" line,
+# the script exits 0 without modifications. Safe to run from every release
+# job in CI; the actual mutation only happens once per release.
+#
+# Usage: perl conftools/finalize-changes.pl <version> <date> [file]
+#   <version>  e.g. 1.9.1
+#   <date>     e.g. 2026-05-13
+#   [file]     defaults to CHANGES
+
+use strict;
+use warnings;
+
+my $version = shift @ARGV;
+my $date    = shift @ARGV;
+my $file    = shift @ARGV // 'CHANGES';
+
+unless (defined $version && defined $date) {
+    die "usage: $0 <version> <date> [file]\n";
+}
+unless ($version =~ /\A[0-9]+\.[0-9]+\.[0-9]+\z/) {
+    die "version must look like X.Y.Z (got: '$version')\n";
+}
+unless ($date =~ /\A[0-9]{4}-[0-9]{2}-[0-9]{2}\z/) {
+    die "date must look like YYYY-MM-DD (got: '$date')\n";
+}
+
+open my $fh, '<', $file or die "open $file: $!\n";
+local $/;
+my $content = <$fh>;
+close $fh;
+
+# Idempotency: a "RRDtool $version - YYYY-MM-DD" header already present
+# means the rewrite ran on a previous job in the same release; just exit.
+if ($content =~ /^RRDtool \Q$version\E - \d{4}-\d{2}-\d{2}\b/m) {
+    exit 0;
+}
+
+my $master_re = qr{
+    \A
+    (\s*)                        # any leading whitespace (CHANGES has a leading \n)
+    RRDtool[ ]-[ ]master[ ]\.\.\.\n
+    =+\n
+    (.*?)                        # body of the master block
+    (?=^RRDtool[ ][0-9])          # right before the next versioned block
+}smx;
+
+unless ($content =~ $master_re) {
+    die "$file: no leading 'RRDtool - master ...' block found\n";
+}
+
+my $lead    = $1;
+my $body    = $2;
+my $hdr     = "RRDtool $version - $date";
+my $under   = '=' x length($hdr);
+my $rewrite =
+      $lead
+    . "RRDtool - master ...\n"
+    . "====================\n"
+    . "Bugfixes\n--------\n\n"
+    . "Features\n--------\n\n"
+    . "$hdr\n$under\n"
+    . $body;
+
+$content =~ s/$master_re/$rewrite/smx
+    or die "$file: substitution failed unexpectedly\n";
+
+open $fh, '>', $file or die "write $file: $!\n";
+print $fh $content;
+close $fh;
diff --git a/conftools/rrdtool-env.sh.in b/conftools/rrdtool-env.sh.in
new file mode 100644 (file)
index 0000000..3f7b435
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/sh
+# Source me to use the /opt/rrdtool installation from your shell:
+#
+#   . /opt/rrdtool/bin/rrdtool-env.sh
+#
+# Puts /opt/rrdtool/bin on PATH and makes the bundled language bindings
+# (Perl, Python, Tcl, Lua, Ruby) discoverable to their interpreters.
+# Does not modify any system file. Safe to source multiple times.
+
+ROOT=/opt/rrdtool
+
+export PATH="$ROOT/bin${PATH:+:$PATH}"
+export MANPATH="$ROOT/share/man${MANPATH:+:$MANPATH}"
+export PKG_CONFIG_PATH="$ROOT/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
+
+export PERL5LIB="$ROOT/lib/perl${PERL5LIB:+:$PERL5LIB}"
+
+PY_SP=$(ls -d "$ROOT"/lib/python*/site-packages 2>/dev/null | head -1)
+[ -n "$PY_SP" ] && export PYTHONPATH="$PY_SP${PYTHONPATH:+:$PYTHONPATH}"
+
+RB_ARCH=$(ls -d "$ROOT"/lib/ruby/*-* 2>/dev/null | head -1)
+if [ -n "$RB_ARCH" ]; then
+    export RUBYLIB="$ROOT/lib/ruby:$RB_ARCH${RUBYLIB:+:$RUBYLIB}"
+else
+    export RUBYLIB="$ROOT/lib/ruby${RUBYLIB:+:$RUBYLIB}"
+fi
+
+LUA_VDIR=$(ls -d "$ROOT"/lib/lua/5.* 2>/dev/null | head -1)
+[ -n "$LUA_VDIR" ] && export LUA_CPATH="$LUA_VDIR/?.so;${LUA_CPATH:-;;}"
+
+export TCLLIBPATH="$ROOT/lib${TCLLIBPATH:+ $TCLLIBPATH}"
diff --git a/conftools/rrdtool-opt.spec b/conftools/rrdtool-opt.spec
new file mode 100644 (file)
index 0000000..43eb407
--- /dev/null
@@ -0,0 +1,98 @@
+# RPM spec for the upstream `/opt/rrdtool` build of RRDtool.
+#
+# This is intentionally NOT a drop-in replacement for the FHS-compliant
+# distribution package. It installs everything (binaries, library, headers,
+# Perl/Python/Tcl/Lua/Ruby bindings) under /opt/rrdtool and coexists
+# with the distro's `rrdtool` package without touching the system.
+#
+# @VERSION@ is substituted by the release workflow before invoking rpmbuild.
+
+Name:           rrdtool
+Version:        @VERSION@
+Release:        1%{?dist}
+Summary:        Round Robin Database Tool (upstream /opt build)
+
+License:        GPL-2.0-or-later WITH FLOSS-exception-1.0
+URL:            https://oss.oetiker.ch/rrdtool/
+Source0:        rrdtool-%{version}.tar.gz
+Source1:        rrdtool-env.sh
+
+Prefix:         /opt/rrdtool
+
+# Disable RPM's automatic dependency scanner: it would scan our /opt-rooted
+# binaries' RPATH and emit Requires like `librrd.so.X()(64bit)` that only
+# the /opt-installed librrd can satisfy, breaking install on hosts that
+# already have the distro `rrdtool` package present.
+AutoReqProv:    no
+
+BuildRequires:  gcc, make, autoconf, automake, libtool, pkgconfig
+BuildRequires:  groff, gettext, gettext-devel, intltool
+BuildRequires:  cairo-devel >= 1.2, pango-devel >= 1.14
+BuildRequires:  freetype-devel, libpng-devel, zlib-devel
+BuildRequires:  libxml2-devel, glib2-devel, libdbi-devel
+# binding build-deps
+BuildRequires:  perl-devel, perl-ExtUtils-MakeMaker
+BuildRequires:  python3-devel
+BuildRequires:  tcl-devel
+BuildRequires:  lua-devel
+BuildRequires:  ruby, ruby-devel
+
+# Runtime: explicit because AutoReqProv is off. Listed without versions on
+# purpose so the same spec works across el8/el9/fedora.
+Requires:       cairo, pango, libxml2, libpng, freetype, libdbi, zlib, glib2
+
+%description
+RRDtool is the OpenSource industry standard high performance data
+logging and graphing system for time series data.
+
+This package installs RRDtool entirely under /opt/rrdtool so it does not
+conflict with the distribution-provided rrdtool package. To make it
+discoverable from your shell:
+
+    . /opt/rrdtool/bin/rrdtool-env.sh
+
+That puts /opt/rrdtool/bin on PATH and makes the bundled language
+bindings (Perl, Python, Tcl, Lua, Ruby) findable by their interpreters.
+
+For C/C++ consumers, set PKG_CONFIG_PATH=/opt/rrdtool/lib/pkgconfig and
+compile with `pkg-config --cflags --libs librrd`; the resulting binary
+gets /opt/rrdtool/lib baked in via -Wl,-rpath, so no system linker
+config (ld.so.conf) is required.
+
+Language bindings are present on disk but their interpreters (perl,
+python3, tcl, lua, ruby) are NOT pulled in as hard dependencies of this
+package — install them yourself if you want to use them.
+
+%prep
+%setup -q -n rrdtool-%{version}
+
+%build
+./configure \
+    --prefix=/opt/rrdtool \
+    --sysconfdir=/opt/rrdtool/etc \
+    --localstatedir=/opt/rrdtool/var \
+    --datarootdir=/opt/rrdtool/share \
+    --mandir=/opt/rrdtool/share/man \
+    --disable-static \
+    --with-pic
+make %{?_smp_mflags}
+
+%install
+make install DESTDIR=%{buildroot}
+
+# Bake the rpath into librrd.pc's Libs: line so consumers get -Wl,-rpath
+# embedded in their binaries via `pkg-config --libs librrd`. This is only
+# done in the /opt build; src/librrd.pc.in stays untouched upstream so
+# FHS-canonical builds aren't affected.
+sed -i 's|^Libs: -L\${libdir} -lrrd$|Libs: -L${libdir} -lrrd -Wl,-rpath,${libdir}|' \
+    %{buildroot}/opt/rrdtool/lib/pkgconfig/librrd.pc
+
+# Sourceable env-helper. Rendered by the workflow into Source1.
+install -m 0755 %{SOURCE1} %{buildroot}/opt/rrdtool/bin/rrdtool-env.sh
+
+%files
+/opt/rrdtool
+
+%changelog
+# Per-release entries are intentionally omitted: the canonical release
+# notes live in CHANGES at the top level of the source tarball.
index 4696142ecb56cdb0fe21f0785f7090ee43131f9a..8b758e19704c95e99975b161cfbf8e09fdfd3b3a 100755 (executable)
@@ -1,22 +1,17 @@
 #!/bin/sh
 # shellcheck disable=SC2086,SC2046,SC2029
+#
+# Maintainer-local release helper. Bumps the version (via the same
+# conftools/bump-version.sh that CI uses), builds the dist tarball,
+# does a quick sanity build out of the tarball, then SCPs the result
+# to oss.oetiker.ch. Useful for ad-hoc local releases; the canonical
+# release path is .github/workflows/release.yml.
+
 set -e
 VERSION=$(cat VERSION)
-NUMVERS=$(perl -n -e 'my @x=split /\./;printf "%d.%d%03d", @x' VERSION)
-CURRENT_YEAR=$(date +"%Y")
+
 set -x
-perl -i -p -e 's/^\$VERSION.+/\$VERSION='$NUMVERS';/' bindings/perl-*/*.pm
-perl -i -p -e 's/RRDtool \d\S+/RRDtool '$VERSION'/; s/Copyright.+?Oetiker.+\d{4}/Copyright by Tobi Oetiker, 1997-'$CURRENT_YEAR'/' src/*.h src/*.c
-perl -i -p -e 's/^Version:.+/Version: '$VERSION'/' rrdtool.spec
-perl -i -p -e 's/rrdtool-[\.\d]+\d(pre\d+)?(rc\d+)?/rrdtool-'$VERSION'/g;
-               s/v\d+\.\d+\.\d+/v'$VERSION'/' doc/rrdbuild.pod
-# Update version and Copyright years for MSVC builds
-perl -i -p -e 's/Copyright \(c\).+?Oetiker/Copyright (c) 1997-'$CURRENT_YEAR' Tobias Oetiker/' win32/*.rc
-perl -i -p -e 's/PACKAGE_MAJOR.+\d{1}/PACKAGE_MAJOR       '$(echo $VERSION | cut -d. -f1)'/;
-               s/PACKAGE_MINOR.+\d{1}/PACKAGE_MINOR       '$(echo $VERSION | cut -d. -f2)'/;
-               s/PACKAGE_REVISION.+\d{1}/PACKAGE_REVISION    '$(echo $VERSION | cut -d. -f3)'/;
-               s/PACKAGE_VERSION.+\d{1}\"/PACKAGE_VERSION     \"'$VERSION'\"/;
-               s/NUMVERS.+\d{1}/NUMVERS             '$NUMVERS'/' win32/rrd_config.h
+sh conftools/bump-version.sh "$VERSION"
 ./bootstrap
 ./configure --enable-maintainer-mode
 make dist