From 6313b1bd1781c6790de731a27ecff5efda535690 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 13 Jun 2026 23:05:11 -0400 Subject: [PATCH 1/8] ci: add tag-triggered GitHub release workflow with CPack zip Pushing a vX.Y.Z tag now builds the project in Release, packages everything into a .zip via CPack, and publishes a GitHub Release named after the tag with the zip attached and auto-generated notes. Pre-release tags (e.g. v0.3.3-rc1) are marked as pre-releases. Uses only first-party actions plus the gh CLI with the default GITHUB_TOKEN. Wires include(CPack) into the top-level CMakeLists (ZIP on Windows, TGZ elsewhere) so cpack can produce artifacts; the project previously never called include(CPack). --- .github/workflows/release.yml | 88 +++++++++++++++++++++++++++++++++++ CMakeLists.txt | 18 +++++++ 2 files changed, 106 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..93d9d5e0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Release + +# Tag-triggered release. +# +# How to cut a release: +# +# git tag v0.3.3 # pick the next version; must start with "v" +# git push origin v0.3.3 +# +# Pushing a tag that looks like vX.Y.Z starts this workflow. It builds the +# project in Release, packages everything into a .zip with CPack, and publishes +# a GitHub Release named after the tag with the .zip attached. +# +# The project's version comes from this same tag: the top-level CMakeLists.txt +# runs `git describe --tags --match "v*"`, so the tag you push IS the version +# that gets built. No version files to keep in sync. + +on: + push: + tags: + - "v[0-9]+.[0-9]+.[0-9]+" # e.g. v0.3.3 + - "v[0-9]+.[0-9]+.[0-9]+-*" # e.g. v0.3.3-rc1 (pre-releases) + +permissions: + contents: write # needed to create the GitHub Release + +jobs: + release: + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: 'recursive' + # Full history + tags so the CMake `git describe` version resolves to + # the tag that triggered this run. + fetch-depth: 0 + fetch-tags: true + + - name: Set reusable strings + id: strings + shell: bash + run: | + echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + echo "dist-dir=${{ github.workspace }}/dist" >> "$GITHUB_OUTPUT" + + - name: Bootstrap vcpkg + shell: bash + run: "${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat" + + - name: Run vcpkg + shell: bash + run: "${{ github.workspace }}/vcpkg/vcpkg" install + + - name: Configure CMake + run: > + cmake + -D CMAKE_BUILD_TYPE=Release + -D CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -B ${{ steps.strings.outputs.build-output-dir }} + -S ${{ github.workspace }} + + - name: Build + run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release --parallel + + - name: Package (CPack -> .zip) + run: > + cpack + --config ${{ steps.strings.outputs.build-output-dir }}/CPackConfig.cmake + -C Release + -B ${{ steps.strings.outputs.dist-dir }} + + - name: Publish GitHub Release + shell: bash + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # github.ref_name is the tag that triggered this run, e.g. v0.3.3. + # Mark anything with a hyphen (v0.3.3-rc1) as a pre-release. + PRERELEASE_FLAG="" + case "${{ github.ref_name }}" in + *-*) PRERELEASE_FLAG="--prerelease" ;; + esac + + gh release create "${{ github.ref_name }}" \ + "${{ steps.strings.outputs.dist-dir }}"/*.zip \ + --title "${{ github.ref_name }}" \ + --generate-notes \ + $PRERELEASE_FLAG diff --git a/CMakeLists.txt b/CMakeLists.txt index 44fcb979..b7c99822 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -201,3 +201,21 @@ install( DESTINATION ${SYSCONFIG_INSTALL_DIR} ) + +# Packaging (CPack). This generates ${CMAKE_BINARY_DIR}/CPackConfig.cmake so that +# `cpack` can build release artifacts in CI. Keep the generator list portable: +# ZIP needs no extra tooling on the runner, unlike NSIS/DEB. +set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") +set(CPACK_PACKAGE_VENDOR "Microsoft") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "m C++ libraries") +set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}") +set(CPACK_VERBATIM_VARIABLES ON) + +if(WIN32) + set(CPACK_GENERATOR "ZIP") +else() + set(CPACK_GENERATOR "TGZ") +endif() + +include(CPack) From f034e04451c0abad1ea3771f716542fd515fcc36 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 13 Jun 2026 23:33:02 -0400 Subject: [PATCH 2/8] ci: fix release workflow tag glob, vcpkg shell, and version comment Address PR review: (1) GitHub tag filters are glob (minimatch), not regex, so the +-quantified patterns never matched normal vX.Y.Z tags; replace with the glob 'v[0-9]*' which covers releases and pre-releases. (2) Run bootstrap-vcpkg.bat and vcpkg.exe via shell: pwsh instead of bash so the batch/exe invocations work natively on windows-latest. (3) Correct the header comment: only the numeric X.Y.Z is extracted into PROJECT_VERSION, so a pre-release suffix like -rc1 is not reflected in the built version even though the release is named after the full tag. --- .github/workflows/release.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d9d5e0..086947f7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,15 +11,20 @@ name: Release # project in Release, packages everything into a .zip with CPack, and publishes # a GitHub Release named after the tag with the .zip attached. # -# The project's version comes from this same tag: the top-level CMakeLists.txt -# runs `git describe --tags --match "v*"`, so the tag you push IS the version -# that gets built. No version files to keep in sync. +# The build version comes from this same tag: the top-level CMakeLists.txt runs +# `git describe --tags --match "v*"` and extracts the numeric X.Y.Z. The release +# is always named after the full tag, but note that any pre-release suffix +# (e.g. the `-rc1` in v0.3.3-rc1) is NOT carried into the built PROJECT_VERSION, +# which would be 0.3.3 in that example. There are no version files to keep in +# sync. on: push: tags: - - "v[0-9]+.[0-9]+.[0-9]+" # e.g. v0.3.3 - - "v[0-9]+.[0-9]+.[0-9]+-*" # e.g. v0.3.3-rc1 (pre-releases) + # GitHub tag filters use glob (minimatch) syntax, not regex, so we cannot + # use `+` quantifiers here. "v[0-9]*" matches v followed by a digit then + # anything, covering both vX.Y.Z and pre-releases like vX.Y.Z-rc1. + - "v[0-9]*" permissions: contents: write # needed to create the GitHub Release @@ -44,12 +49,12 @@ jobs: echo "dist-dir=${{ github.workspace }}/dist" >> "$GITHUB_OUTPUT" - name: Bootstrap vcpkg - shell: bash - run: "${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat" + shell: pwsh + run: ${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat - name: Run vcpkg - shell: bash - run: "${{ github.workspace }}/vcpkg/vcpkg" install + shell: pwsh + run: ${{ github.workspace }}/vcpkg/vcpkg.exe install - name: Configure CMake run: > From 71922885f4bfe46a68137411163099f470891e30 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sat, 13 Jun 2026 23:51:41 -0400 Subject: [PATCH 3/8] ci: enforce vX.Y.Z tag shape and run ctest before publishing release Address PR review: (1) Tighten the tag glob to 'v[0-9]*.[0-9]*.[0-9]*' so it requires the two dots of vX.Y.Z and no longer fires on tags like v1 or v12foo; the trailing segment still permits pre-release suffixes like -rc1. (2) Build the tests (build_tests=ON/BUILD_TESTING=ON) and run ctest before packaging, so a release artifact is never published if the suite the rest of CI runs is failing. --- .github/workflows/release.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 086947f7..00c44d25 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,9 +22,11 @@ on: push: tags: # GitHub tag filters use glob (minimatch) syntax, not regex, so we cannot - # use `+` quantifiers here. "v[0-9]*" matches v followed by a digit then - # anything, covering both vX.Y.Z and pre-releases like vX.Y.Z-rc1. - - "v[0-9]*" + # use `+` quantifiers. Requiring the two dots enforces the vX.Y.Z shape and + # avoids firing on tags like v1 or v12foo. The trailing segment's `*` still + # allows pre-release suffixes such as v0.3.3-rc1. (This does not reject + # v1.2.3.4; add a runtime semver check if that strictness is needed.) + - "v[0-9]*.[0-9]*.[0-9]*" permissions: contents: write # needed to create the GitHub Release @@ -61,12 +63,19 @@ jobs: cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -D build_tests=ON + -D BUILD_TESTING=ON -B ${{ steps.strings.outputs.build-output-dir }} -S ${{ github.workspace }} - name: Build run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config Release --parallel + - name: Test + # Don't publish a release artifact that fails the suite the rest of CI runs. + working-directory: ${{ steps.strings.outputs.build-output-dir }} + run: ctest --build-config Release --output-on-failure + - name: Package (CPack -> .zip) run: > cpack From f107c1295178f38f011c7b0b0594340fac2e8fa0 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 14 Jun 2026 00:11:20 -0400 Subject: [PATCH 4/8] ci: reject non-semver release tags with a runtime validation step The on.push.tags glob can only require the vX.Y.Z shape, not digits-only segments, so tags like v1foo.2.3 or v1.2.3.4 could still trigger a release. Add a first step that regex-validates github.ref_name against strict vMAJOR.MINOR.PATCH with optional -prerelease/+build suffixes and fails fast before any build/publish when it doesn't match. --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00c44d25..90ebcc3f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,6 +35,21 @@ jobs: release: runs-on: windows-latest steps: + - name: Validate tag is strict semver + # The on.push.tags glob can only require the vX.Y.Z *shape*; it can't + # enforce digits-only segments, so tags like v1foo.2.3 or v1.2.3.4 would + # still start this job. Reject anything that isn't strict + # vMAJOR.MINOR.PATCH (with an optional -prerelease / +build suffix) so we + # never publish an unintended release. + shell: bash + run: | + tag="${{ github.ref_name }}" + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Tag '$tag' is not a strict vX.Y.Z[-prerelease][+build] semver tag; refusing to release." + exit 1 + fi + echo "Tag '$tag' accepted." + - uses: actions/checkout@v5 with: submodules: 'recursive' From 17fd8144384288b98066e46d63588614899fcb40 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 14 Jun 2026 00:25:30 -0400 Subject: [PATCH 5/8] ci: pin x64 triplet/arch and reject leading-zero semver tags Address PR review: (1) Pin VCPKG_DEFAULT_TRIPLET/VCPKG_TARGET_TRIPLET to x64-windows (job env), pass --triplet x64-windows to vcpkg install, and add -A x64 plus -D VCPKG_TARGET_TRIPLET=x64-windows to the CMake configure so vcpkg, CMake, and the packaged artifact all agree on x64 and we never publish a 32-bit build or hit an arch mismatch. (2) Tighten the validation regex so MAJOR/MINOR/PATCH each match (0|[1-9][0-9]*), rejecting leading-zero segments like v01.2.3 to actually be strict SemVer as the step name claims. --- .github/workflows/release.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90ebcc3f..08b50d77 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,12 @@ on: permissions: contents: write # needed to create the GitHub Release +env: + # Pin the architecture/triplet everywhere so vcpkg, CMake, and the packaged + # artifact all agree on x64 and we never accidentally publish a 32-bit build. + VCPKG_DEFAULT_TRIPLET: x64-windows + VCPKG_TARGET_TRIPLET: x64-windows + jobs: release: runs-on: windows-latest @@ -44,7 +50,9 @@ jobs: shell: bash run: | tag="${{ github.ref_name }}" - if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + # Strict SemVer: numeric identifiers have no leading zeroes, so each of + # MAJOR/MINOR/PATCH is either 0 or a non-zero-led digit run. + if [[ ! "$tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then echo "::error::Tag '$tag' is not a strict vX.Y.Z[-prerelease][+build] semver tag; refusing to release." exit 1 fi @@ -71,13 +79,15 @@ jobs: - name: Run vcpkg shell: pwsh - run: ${{ github.workspace }}/vcpkg/vcpkg.exe install + run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --triplet x64-windows - name: Configure CMake run: > cmake + -A x64 -D CMAKE_BUILD_TYPE=Release -D CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake + -D VCPKG_TARGET_TRIPLET=x64-windows -D build_tests=ON -D BUILD_TESTING=ON -B ${{ steps.strings.outputs.build-output-dir }} From efd66d52b52c156a9575cb1bb39147801f176ed2 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 14 Jun 2026 00:59:31 -0400 Subject: [PATCH 6/8] ci: drop unused build_tests and ineffective CMAKE_BUILD_TYPE from release configure Address PR review: build_tests is not referenced anywhere in this project's CMake (only BUILD_TESTING is), so the cache variable was misleading; removed it. CMAKE_BUILD_TYPE has no effect with the Visual Studio multi-config generator selected by -A x64 (the config is chosen by --config/--build-config/-C Release), so removed it too and added a comment explaining the multi-config behavior. --- .github/workflows/release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08b50d77..b0c9453d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,13 +82,15 @@ jobs: run: ${{ github.workspace }}/vcpkg/vcpkg.exe install --triplet x64-windows - name: Configure CMake + # The Visual Studio generator selected by -A x64 is multi-config, so the + # build type is chosen later via --config / --build-config / -C Release + # rather than CMAKE_BUILD_TYPE. Only BUILD_TESTING controls test builds + # in this project's CMake. run: > cmake -A x64 - -D CMAKE_BUILD_TYPE=Release -D CMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake -D VCPKG_TARGET_TRIPLET=x64-windows - -D build_tests=ON -D BUILD_TESTING=ON -B ${{ steps.strings.outputs.build-output-dir }} -S ${{ github.workspace }} From d9f3794b99dbead6d9e4344f382684b1993ece4a Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 14 Jun 2026 01:09:19 -0400 Subject: [PATCH 7/8] ci: detect pre-release before build metadata when publishing release Pre-release detection treated any hyphen in the tag as a pre-release, but SemVer build metadata may legally contain hyphens (e.g. v1.2.3+build-foo) and such tags pass the semver gate. Strip everything from the first '+' (build metadata) before testing for a '-', so only a hyphen in the pre-release position marks the GitHub release as a pre-release. --- .github/workflows/release.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0c9453d..f72411fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,9 +116,14 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # github.ref_name is the tag that triggered this run, e.g. v0.3.3. - # Mark anything with a hyphen (v0.3.3-rc1) as a pre-release. + # A SemVer pre-release is a '-' that comes before any '+build' metadata + # (e.g. v0.3.3-rc1). Build metadata may itself contain hyphens + # (e.g. v1.2.3+build-foo), so strip everything from the first '+' + # before testing for a pre-release hyphen to avoid false positives. + tag="${{ github.ref_name }}" + version_no_build="${tag%%+*}" PRERELEASE_FLAG="" - case "${{ github.ref_name }}" in + case "$version_no_build" in *-*) PRERELEASE_FLAG="--prerelease" ;; esac From 239cd2d2ddeb990242cd87da9e83bdd82ef536c6 Mon Sep 17 00:00:00 2001 From: Mike Grier Date: Sun, 14 Jun 2026 01:19:37 -0400 Subject: [PATCH 8/8] ci: enforce strict semver identifiers in release tag validation --- .github/workflows/release.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f72411fc..30ee8fa3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,9 +50,17 @@ jobs: shell: bash run: | tag="${{ github.ref_name }}" - # Strict SemVer: numeric identifiers have no leading zeroes, so each of - # MAJOR/MINOR/PATCH is either 0 or a non-zero-led digit run. - if [[ ! "$tag" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then + # This is the official semver.org recommended regex, adapted to POSIX + # ERE (bash [[ =~ ]]): non-capturing groups become plain groups and \d + # becomes [0-9]. Enforcing the full grammar means: + # * MAJOR/MINOR/PATCH are numeric with no leading zeroes (0 | [1-9]...) + # * each dot-separated pre-release/build identifier is non-empty and + # well-formed, so junk like v1.2.3-alpha..1 or v1.2.3+build..5 (empty + # identifiers) is rejected even though it "looks" close to semver. + # * a pre-release numeric identifier has no leading zeroes, but build + # identifiers may (per spec). + semver='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$' + if [[ ! "$tag" =~ $semver ]]; then echo "::error::Tag '$tag' is not a strict vX.Y.Z[-prerelease][+build] semver tag; refusing to release." exit 1 fi